Compare commits

...

13 Commits

Author SHA1 Message Date
Charlie Marsh
f3baec49df Remove from build 2024-09-20 17:28:58 -04:00
Charlie Marsh
e6fde89e26 Disable zstd on powerpc64 2024-09-20 17:26:50 -04:00
Charlie Marsh
7579a792c7 Add test coverage for non-Python globs (#13430) 2024-09-20 20:46:00 +00:00
Charlie Marsh
0bbc138037 Upgrade to latest cargo-dist version (#13416)
## Summary

Follows https://github.com/astral-sh/uv/pull/7092.
2024-09-20 15:59:32 -04:00
Charlie Marsh
ff11db61b4 Add Python version support to ruff analyze CLI (#13426) 2024-09-20 15:40:47 -04:00
Charlie Marsh
2823487bf8 Respect lint.exclude in ruff check --add-noqa (#13427)
## Summary

Closes https://github.com/astral-sh/ruff/issues/13423.
2024-09-20 19:39:36 +00:00
Charlie Marsh
910fac781d Add exclude support to ruff analyze (#13425)
## Summary

Closes https://github.com/astral-sh/ruff/issues/13424.
2024-09-20 15:34:35 -04:00
Carl Meyer
149fb2090e [red-knot] more efficient UnionBuilder::add (#13411)
Avoid quadratic time in subsumed elements when adding a super-type of
existing union elements.

Reserve space in advance when adding multiple elements (from another
union) to a union.

Make union elements a `Box<[Type]>` instead of an `FxOrderSet`; the set
doesn't buy much since the rules of union uniqueness are defined in
terms of supertype/subtype, not in terms of simple type identity.

Move sealed-boolean handling out of a separate `UnionBuilder::simplify`
method and into `UnionBuilder::add`; now that `add` is iterating
existing elements anyway, this is more efficient.

Remove `UnionType::contains`, since it's now `O(n)` and we shouldn't
really need it, generally we care about subtype/supertype, not type
identity. (Right now it's used for `Type::Unbound`, which shouldn't even
be a type.)

Add support for `is_subtype_of` for the `object` type.

Addresses comments on https://github.com/astral-sh/ruff/pull/13401
2024-09-20 10:49:45 -07:00
Carl Meyer
40c65dcfa7 [red-knot] dedicated error message for all-union-elements not callable (#13412)
This was mentioned in an earlier review, and seemed easy enough to just
do it. No need to repeat all the types twice when it gives no additional
information.
2024-09-20 08:08:43 -07:00
yahayaohinoyi
03f3a4e855 [pycodestyle] Fix: Don't autofix if the first line ends in a question mark? (D400) (#13399)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-09-20 11:05:26 +00:00
Micha Reiser
531ebf6dff Fix parentheses around return type annotations (#13381) 2024-09-20 09:23:53 +02:00
Rupert Tombs
7c2011599f Correct Some value is incorrect (#13418) 2024-09-20 08:25:58 +02:00
Charlie Marsh
17e90823da Some minor internal refactors for module graph (#13417) 2024-09-20 00:21:30 -04:00
46 changed files with 2070 additions and 463 deletions

View File

@@ -1,3 +1,5 @@
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
@@ -64,7 +66,7 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.18.0/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
uses: actions/upload-artifact@v4
with:

View File

@@ -233,9 +233,9 @@ inherits = "release"
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.18.0"
cargo-dist-version = "0.22.1"
# CI backends to support
ci = ["github"]
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell"]
# The archive format to use for windows builds (defaults .zip)
@@ -266,11 +266,11 @@ targets = [
auto-includes = false
# Whether cargo-dist should create a GitHub Release or use an existing draft
create-release = true
# Publish jobs to run in CI
# Which actions to run on pull requests
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# The stage during which the GitHub Release should be created
# Which phase cargo-dist should use to create the GitHub release
github-release = "announce"
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
@@ -278,9 +278,11 @@ build-local-artifacts = false
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
# Announcement jobs to run in CI
# Post-announce jobs to run in CI
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
# Whether to install an updater program
install-updater = false
# Path that installers should place binaries in
install-path = "CARGO_HOME"

View File

@@ -38,7 +38,12 @@ test-case = { workspace = true }
[build-dependencies]
path-slash = { workspace = true }
walkdir = { workspace = true }
zip = { workspace = true, features = ["zstd", "deflate"] }
[target.'cfg(not(target_arch = "powerpc64"))'.build-dependencies]
zip = { workspace = true, features = ["deflate", "zstd"] }
[target.'cfg(target_arch = "powerpc64")'.build-dependencies]
zip = { workspace = true, features = ["deflate"] }
[dev-dependencies]
ruff_db = { workspace = true, features = ["os", "testing"] }

View File

@@ -30,10 +30,17 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
// We can't use `#[cfg(...)]` here because the target-arch in a build script is the
// architecture of the system running the build script and not the architecture of the build-target.
// That's why we use the `TARGET` environment variable here.
let method = if std::env::var("TARGET").unwrap().contains("wasm32") {
CompressionMethod::Deflated
} else {
CompressionMethod::Zstd
#[cfg(target_arch = "powerpc64")]
let method = CompressionMethod::Deflated;
#[cfg(not(target_arch = "powerpc64"))]
let method = {
let target = std::env::var("TARGET").unwrap();
if target.contains("wasm32") || target.contains("powerpc64") {
CompressionMethod::Deflated
} else {
CompressionMethod::Zstd
}
};
let options = FileOptions::default()

View File

@@ -54,6 +54,13 @@ impl TryFrom<(&str, &str)> for PythonVersion {
}
}
impl From<(u8, u8)> for PythonVersion {
fn from(value: (u8, u8)) -> Self {
let (major, minor) = value;
Self { major, minor }
}
}
impl fmt::Display for PythonVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let PythonVersion { major, minor } = self;

View File

@@ -362,7 +362,7 @@ impl<'db> Type<'db> {
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
match self {
Type::Unbound => true,
Type::Union(union) => union.contains(db, Type::Unbound),
Type::Union(union) => union.elements(db).contains(&Type::Unbound),
// Unbound can't appear in an intersection, because an intersection with Unbound
// simplifies to just Unbound.
_ => false,
@@ -422,6 +422,8 @@ impl<'db> Type<'db> {
.elements(db)
.iter()
.any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
(_, Type::Instance(class)) if class.is_stdlib_symbol(db, "builtins", "object") => true,
(Type::Instance(class), _) if class.is_stdlib_symbol(db, "builtins", "object") => false,
// TODO
_ => false,
}
@@ -817,8 +819,16 @@ impl<'db> CallOutcome<'db> {
node,
"call-non-callable",
format_args!(
"Union element '{}' of type '{}' is not callable.",
"Object of type '{}' is not callable (due to union element '{}').",
called_ty.display(db),
elem.display(db),
),
),
_ if not_callable.len() == outcomes.len() => builder.add_diagnostic(
node,
"call-non-callable",
format_args!(
"Object of type '{}' is not callable.",
called_ty.display(db)
),
),
@@ -826,9 +836,9 @@ impl<'db> CallOutcome<'db> {
node,
"call-non-callable",
format_args!(
"Union elements {} of type '{}' are not callable.",
"Object of type '{}' is not callable (due to union elements {}).",
called_ty.display(db),
not_callable.display(db),
called_ty.display(db)
),
),
}
@@ -994,16 +1004,16 @@ impl<'db> ClassType<'db> {
pub struct UnionType<'db> {
/// The union type includes values in any of these types.
#[return_ref]
elements: FxOrderSet<Type<'db>>,
elements_boxed: Box<[Type<'db>]>,
}
impl<'db> UnionType<'db> {
pub fn contains(&self, db: &'db dyn Db, ty: Type<'db>) -> bool {
self.elements(db).contains(&ty)
fn elements(self, db: &'db dyn Db) -> &'db [Type<'db>] {
self.elements_boxed(db)
}
/// Create a union from a list of elements
/// (which may be eagerly simplified into a different variant of [`Type`] altogether)
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
pub fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
elements: impl IntoIterator<Item = T>,
@@ -1017,13 +1027,13 @@ impl<'db> UnionType<'db> {
}
/// Apply a transformation function to all elements of the union,
/// and create a new union from the resulting set of types
/// and create a new union from the resulting set of types.
pub fn map(
&self,
db: &'db dyn Db,
transform_fn: impl Fn(&Type<'db>) -> Type<'db>,
) -> Type<'db> {
Self::from_elements(db, self.elements(db).into_iter().map(transform_fn))
Self::from_elements(db, self.elements(db).iter().map(transform_fn))
}
}
@@ -1127,6 +1137,8 @@ mod tests {
}
}
#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
#[test_case(Ty::Any, Ty::IntLiteral(1))]
#[test_case(Ty::Never, Ty::IntLiteral(1))]
@@ -1144,6 +1156,7 @@ mod tests {
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
}
#[test_case(Ty::BuiltinInstance("object"), Ty::BuiltinInstance("int"))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
@@ -1152,6 +1165,8 @@ mod tests {
assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
}
#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::Never, Ty::IntLiteral(1))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
@@ -1164,6 +1179,7 @@ mod tests {
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
}
#[test_case(Ty::BuiltinInstance("object"), Ty::BuiltinInstance("int"))]
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
#[test_case(Ty::Any, Ty::IntLiteral(1))]
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]

View File

@@ -27,10 +27,10 @@
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
use crate::{Db, FxOrderSet};
use ordermap::set::MutableValues;
use smallvec::SmallVec;
pub(crate) struct UnionBuilder<'db> {
elements: FxOrderSet<Type<'db>>,
elements: Vec<Type<'db>>,
db: &'db dyn Db,
}
@@ -38,7 +38,7 @@ impl<'db> UnionBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
db,
elements: FxOrderSet::default(),
elements: vec![],
}
}
@@ -46,60 +46,70 @@ impl<'db> UnionBuilder<'db> {
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
match ty {
Type::Union(union) => {
for element in union.elements(self.db) {
let new_elements = union.elements(self.db);
self.elements.reserve(new_elements.len());
for element in new_elements {
self = self.add(*element);
}
}
Type::Never => {}
_ => {
let mut remove = vec![];
for element in &self.elements {
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
} else {
None
};
let mut to_add = ty;
let mut to_remove = SmallVec::<[usize; 2]>::new();
for (index, element) in self.elements.iter().enumerate() {
if Some(*element) == bool_pair {
to_add = builtins_symbol_ty(self.db, "bool");
to_remove.push(index);
// The type we are adding is a BooleanLiteral, which doesn't have any
// subtypes. And we just found that the union already contained our
// mirror-image BooleanLiteral, so it can't also contain bool or any
// supertype of bool. Therefore, we are done.
break;
}
if ty.is_subtype_of(self.db, *element) {
return self;
} else if element.is_subtype_of(self.db, ty) {
remove.push(*element);
to_remove.push(index);
}
}
for element in remove {
self.elements.remove(&element);
match to_remove[..] {
[] => self.elements.push(to_add),
[index] => self.elements[index] = to_add,
_ => {
let mut current_index = 0;
let mut to_remove = to_remove.into_iter();
let mut next_to_remove_index = to_remove.next();
self.elements.retain(|_| {
let retain = if Some(current_index) == next_to_remove_index {
next_to_remove_index = to_remove.next();
false
} else {
true
};
current_index += 1;
retain
});
self.elements.push(to_add);
}
}
self.elements.insert(ty);
}
}
self
}
/// Performs the following normalizations:
/// - Replaces `Literal[True,False]` with `bool`.
/// - TODO For enums `E` with members `X1`,...,`Xn`, replaces
/// `Literal[E.X1,...,E.Xn]` with `E`.
fn simplify(&mut self) {
if let Some(true_index) = self.elements.get_index_of(&Type::BooleanLiteral(true)) {
if self.elements.contains(&Type::BooleanLiteral(false)) {
*self.elements.get_index_mut2(true_index).unwrap() =
builtins_symbol_ty(self.db, "bool");
self.elements.remove(&Type::BooleanLiteral(false));
}
}
}
pub(crate) fn build(mut self) -> Type<'db> {
pub(crate) fn build(self) -> Type<'db> {
match self.elements.len() {
0 => Type::Never,
1 => self.elements[0],
_ => {
self.simplify();
match self.elements.len() {
0 => Type::Never,
1 => self.elements[0],
_ => {
self.elements.shrink_to_fit();
Type::Union(UnionType::new(self.db, self.elements))
}
}
}
_ => Type::Union(UnionType::new(self.db, self.elements.into())),
}
}
}
@@ -293,12 +303,6 @@ mod tests {
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
impl<'db> UnionType<'db> {
fn elements_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
self.elements(db).into_iter().copied().collect()
}
}
fn setup_db() -> TestDb {
let db = TestDb::new();
@@ -326,7 +330,7 @@ mod tests {
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t1]);
assert_eq!(union.elements(&db), &[t0, t1]);
}
#[test]
@@ -363,10 +367,10 @@ mod tests {
let t3 = Type::IntLiteral(17);
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t3]);
assert_eq!(union.elements(&db), &[t0, t3]);
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
assert_eq!(union.elements_vec(&db), &[bool_ty, t3]);
assert_eq!(union.elements(&db), &[bool_ty, t3]);
}
#[test]
@@ -378,7 +382,7 @@ mod tests {
let u1 = UnionType::from_elements(&db, [t0, t1]);
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t1, t2]);
assert_eq!(union.elements(&db), &[t0, t1, t2]);
}
#[test]
@@ -386,18 +390,37 @@ mod tests {
let db = setup_db();
let t0 = builtins_symbol_ty(&db, "str").to_instance(&db);
let t1 = Type::LiteralString;
let t2 = Type::Unknown;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
let u2 = UnionType::from_elements(&db, [t0, t1, t2]);
assert_eq!(u0, t0);
assert_eq!(u1, t0);
assert_eq!(u2.expect_union().elements_vec(&db), &[t0, t2]);
}
#[test]
fn build_union_no_simplify_any() {}
fn build_union_no_simplify_unknown() {
let db = setup_db();
let t0 = builtins_symbol_ty(&db, "str").to_instance(&db);
let t1 = Type::Unknown;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
}
#[test]
fn build_union_subsume_multiple() {
let db = setup_db();
let str_ty = builtins_symbol_ty(&db, "str").to_instance(&db);
let int_ty = builtins_symbol_ty(&db, "int").to_instance(&db);
let object_ty = builtins_symbol_ty(&db, "object").to_instance(&db);
let unknown_ty = Type::Unknown;
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
}
impl<'db> IntersectionType<'db> {
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
@@ -477,7 +500,7 @@ mod tests {
.add_positive(u0)
.build()
.expect_union();
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements_vec(&db)[..] else {
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
panic!("expected a union of two intersections");
};
assert_eq!(i0.pos_vec(&db), &[ta, t0]);

View File

@@ -3486,7 +3486,7 @@ mod tests {
assert_file_diagnostics(
&db,
"src/a.py",
&["Union element 'Literal[1]' of type 'Literal[1] | Literal[f]' is not callable."],
&["Object of type 'Literal[1] | Literal[f]' is not callable (due to union element 'Literal[1]')."],
);
assert_public_ty(&db, "src/a.py", "x", "Unknown | int");
@@ -3515,7 +3515,7 @@ mod tests {
&db,
"src/a.py",
&[
r#"Union elements Literal[1], Literal["foo"] of type 'Literal[1] | Literal["foo"] | Literal[f]' are not callable."#,
r#"Object of type 'Literal[1] | Literal["foo"] | Literal[f]' is not callable (due to union elements Literal[1], Literal["foo"])."#,
],
);
assert_public_ty(&db, "src/a.py", "x", "Unknown | int");
@@ -3523,6 +3523,31 @@ mod tests {
Ok(())
}
#[test]
fn call_union_with_all_not_callable() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
if flag:
f = 1
else:
f = 'foo'
x = f()
",
)?;
assert_file_diagnostics(
&db,
"src/a.py",
&[r#"Object of type 'Literal[1] | Literal["foo"]' is not callable."#],
);
assert_public_ty(&db, "src/a.py", "x", "Unknown");
Ok(())
}
#[test]
fn invalid_callable() {
let mut db = setup_db();

View File

@@ -114,22 +114,19 @@ fn lint_maybe_undefined(context: &SemanticLintContext, name: &ast::ExprName) {
return;
}
let semantic = &context.semantic;
match name.ty(semantic) {
Type::Unbound => {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name '{}' used when not defined.", &name.id),
name.start(),
));
}
Type::Union(union) if union.contains(semantic.db(), Type::Unbound) => {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name '{}' used when possibly not defined.", &name.id),
name.start(),
));
}
_ => {}
let ty = name.ty(semantic);
if ty.is_unbound() {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name '{}' used when not defined.", &name.id),
name.start(),
));
} else if ty.may_be_unbound(semantic.db()) {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name '{}' used when possibly not defined.", &name.id),
name.start(),
));
}
}

View File

@@ -152,20 +152,23 @@ pub enum AnalyzeCommand {
pub struct AnalyzeGraphCommand {
/// List of files or directories to include.
#[clap(help = "List of files or directories to include [default: .]")]
pub files: Vec<PathBuf>,
files: Vec<PathBuf>,
/// The direction of the import map. By default, generates a dependency map, i.e., a map from
/// file to files that it depends on. Use `--direction dependents` to generate a map from file
/// to files that depend on it.
#[clap(long, value_enum, default_value_t)]
pub direction: Direction,
direction: Direction,
/// Attempt to detect imports from string literals.
#[clap(long)]
pub detect_string_imports: bool,
detect_string_imports: bool,
/// Enable preview mode. Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
target_version: Option<PythonVersion>,
}
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
@@ -789,6 +792,7 @@ impl AnalyzeGraphCommand {
None
},
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
target_version: self.target_version,
..ExplicitConfigOverrides::default()
};

View File

@@ -10,7 +10,9 @@ use ruff_linter::linter::add_noqa_to_path;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile,
};
use crate::args::ConfigArguments;
@@ -57,6 +59,15 @@ pub(crate) fn add_noqa(
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path);
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(
resolved_file.path(),
resolved_file.file_name(),
&settings.linter.exclude,
)
{
return None;
}
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,

View File

@@ -8,7 +8,7 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports};
use ruff_linter::{warn_user, warn_user_once};
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, ResolvedFile};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile};
use rustc_hash::FxHashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@@ -59,6 +59,12 @@ pub(crate) fn analyze_graph(
.filter_map(|package| package.parent())
.map(Path::to_path_buf)
.filter_map(|path| SystemPathBuf::from_path_buf(path).ok()),
pyproject_config
.settings
.analyze
.target_version
.as_tuple()
.into(),
)?;
// Create a cache for resolved globs.
@@ -74,19 +80,30 @@ pub(crate) fn analyze_graph(
continue;
};
let path = resolved_file.into_path();
let path = resolved_file.path();
let package = path
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(Clone::clone);
// Resolve the per-file settings.
let settings = resolver.resolve(&path);
let settings = resolver.resolve(path);
let string_imports = settings.analyze.detect_string_imports;
let include_dependencies = settings.analyze.include_dependencies.get(&path).cloned();
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
// Skip excluded files.
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(
resolved_file.path(),
resolved_file.file_name(),
&settings.analyze.exclude,
)
{
continue;
}
// Ignore non-Python files.
let source_type = match settings.analyze.extension.get(&path) {
let source_type = match settings.analyze.extension.get(path) {
None => match SourceType::from(&path) {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
@@ -106,19 +123,19 @@ pub(crate) fn analyze_graph(
warn!("Failed to convert package to system path");
continue;
};
let Ok(path) = SystemPathBuf::from_path_buf(path) else {
let Ok(path) = SystemPathBuf::from_path_buf(resolved_file.into_path()) else {
warn!("Failed to convert path to system path");
continue;
};
let db = db.snapshot();
let glob_resolver = glob_resolver.clone();
let root = root.clone();
let result = inner_result.clone();
let glob_resolver = glob_resolver.clone();
scope.spawn(move |_| {
// Identify any imports via static analysis.
let mut imports =
ruff_graph::generate(&path, package.as_deref(), string_imports, &db)
ModuleImports::detect(&db, &path, package.as_deref(), string_imports)
.unwrap_or_else(|err| {
warn!("Failed to generate import map for {path}: {err}");
ModuleImports::default()

View File

@@ -226,13 +226,14 @@ fn globs() -> Result<()> {
root.child("ruff.toml").write_str(indoc::indoc! {r#"
[analyze]
include-dependencies = { "ruff/a.py" = ["ruff/b.py"], "ruff/b.py" = ["ruff/*.py"] }
include-dependencies = { "ruff/a.py" = ["ruff/b.py"], "ruff/b.py" = ["ruff/*.py"], "ruff/c.py" = ["*.json"] }
"#})?;
root.child("ruff").child("__init__.py").write_str("")?;
root.child("ruff").child("a.py").write_str("")?;
root.child("ruff").child("b.py").write_str("")?;
root.child("ruff").child("c.py").write_str("")?;
root.child("ruff").child("d.json").write_str("")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
@@ -252,7 +253,50 @@ fn globs() -> Result<()> {
"ruff/b.py",
"ruff/c.py"
],
"ruff/c.py": []
"ruff/c.py": [
"ruff/d.json"
]
}
----- stderr -----
"###);
});
Ok(())
}
#[test]
fn exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
root.child("ruff.toml").write_str(indoc::indoc! {r#"
[analyze]
exclude = ["ruff/c.py"]
"#})?;
root.child("ruff").child("__init__.py").write_str("")?;
root.child("ruff")
.child("a.py")
.write_str(indoc::indoc! {r#"
import ruff.b
"#})?;
root.child("ruff").child("b.py").write_str("")?;
root.child("ruff").child("c.py").write_str("")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": []
}
----- stderr -----

View File

@@ -1619,6 +1619,58 @@ print(
Ok(())
}
#[test]
fn add_noqa_exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
exclude = ["excluded.py"]
select = ["RUF015"]
"#,
)?;
let test_path = tempdir.path().join("noqa.py");
fs::write(
&test_path,
r#"
def first_square():
return [x * x for x in range(20)][0]
"#,
)?;
let exclude_path = tempdir.path().join("excluded.py");
fs::write(
&exclude_path,
r#"
def first_square():
return [x * x for x in range(20)][0]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(STDIN_BASE_OPTIONS)
.args(["--add-noqa"]), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Added 1 noqa directive.
"###);
});
Ok(())
}
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python() -> Result<()> {

View File

@@ -389,7 +389,9 @@ formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
# Analyze Settings
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = Py37
analyze.detect_string_imports = false
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@@ -34,13 +34,15 @@ tracing-subscriber = { workspace = true, optional = true }
tracing-tree = { workspace = true, optional = true }
rustc-hash = { workspace = true }
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
[target.'cfg(not(any(target_arch = "wasm32", target_arch = "powerpc64")))'.dependencies]
zip = { workspace = true, features = ["zstd"] }
[target.'cfg(target_arch="wasm32")'.dependencies]
web-time = { version = "1.1.0" }
[target.'cfg(any(target_arch = "wasm32", target_arch = "powerpc64"))'.dependencies]
zip = { workspace = true, features = ["deflate"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-time = { version = "1.1.0" }
[dev-dependencies]
insta = { workspace = true }
tempfile = { workspace = true }

View File

@@ -194,6 +194,10 @@ pub(crate) struct Args {
/// Format the files. Without this flag, the python files are not modified
#[arg(long)]
pub(crate) write: bool,
#[arg(long)]
pub(crate) preview: bool,
/// Control the verbosity of the output
#[arg(long, default_value_t, value_enum)]
pub(crate) format: Format,
@@ -235,7 +239,8 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<ExitCode> {
let all_success = if args.multi_project {
format_dev_multi_project(args, error_file)?
} else {
let result = format_dev_project(&args.files, args.stability_check, args.write)?;
let result =
format_dev_project(&args.files, args.stability_check, args.write, args.preview)?;
let error_count = result.error_count();
if result.error_count() > 0 {
@@ -344,7 +349,12 @@ fn format_dev_multi_project(
for project_path in project_paths {
debug!(parent: None, "Starting {}", project_path.display());
match format_dev_project(&[project_path.clone()], args.stability_check, args.write) {
match format_dev_project(
&[project_path.clone()],
args.stability_check,
args.write,
args.preview,
) {
Ok(result) => {
total_errors += result.error_count();
total_files += result.file_count;
@@ -442,6 +452,7 @@ fn format_dev_project(
files: &[PathBuf],
stability_check: bool,
write: bool,
preview: bool,
) -> anyhow::Result<CheckRepoResult> {
let start = Instant::now();
@@ -477,7 +488,14 @@ fn format_dev_project(
#[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() };
iter.map(|path| {
let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
let result = format_dir_entry(
path,
stability_check,
write,
preview,
&black_options,
&resolver,
);
pb_span.pb_inc(1);
result
})
@@ -532,6 +550,7 @@ fn format_dir_entry(
resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool,
write: bool,
preview: bool,
options: &BlackOptions,
resolver: &Resolver,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
@@ -544,6 +563,10 @@ fn format_dir_entry(
let path = resolved_file.into_path();
let mut options = options.to_py_format_options(&path);
if preview {
options = options.with_preview(PreviewMode::Enabled);
}
let settings = resolver.resolve(&path);
// That's a bad way of doing this but it's not worth doing something better for format_dev
if settings.formatter.line_width != LineWidth::default() {
@@ -551,9 +574,8 @@ fn format_dir_entry(
}
// Handle panics (mostly in `debug_assert!`)
let result = match catch_unwind(|| format_dev_file(&path, stability_check, write, options)) {
Ok(result) => result,
Err(panic) => {
let result = catch_unwind(|| format_dev_file(&path, stability_check, write, options))
.unwrap_or_else(|panic| {
if let Some(message) = panic.downcast_ref::<String>() {
Err(CheckFileError::Panic {
message: message.clone(),
@@ -568,8 +590,7 @@ fn format_dir_entry(
message: "(Panic didn't set a string message)".to_string(),
})
}
}
};
});
Ok((result, path))
}

View File

@@ -16,7 +16,10 @@ pub struct ModuleDb {
impl ModuleDb {
/// Initialize a [`ModuleDb`] from the given source root.
pub fn from_src_roots(mut src_roots: impl Iterator<Item = SystemPathBuf>) -> Result<Self> {
pub fn from_src_roots(
mut src_roots: impl Iterator<Item = SystemPathBuf>,
target_version: PythonVersion,
) -> Result<Self> {
let search_paths = {
// Use the first source root.
let src_root = src_roots
@@ -37,7 +40,7 @@ impl ModuleDb {
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
target_version,
search_paths,
},
)?;

View File

@@ -21,6 +21,39 @@ mod settings;
pub struct ModuleImports(BTreeSet<SystemPathBuf>);
impl ModuleImports {
/// Detect the [`ModuleImports`] for a given Python file.
pub fn detect(
db: &ModuleDb,
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: bool,
) -> Result<Self> {
// Read and parse the source code.
let file = system_path_to_file(db, path)?;
let parsed = parsed_module(db, file);
let module_path =
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
let model = SemanticModel::new(db, file);
// Collect the imports.
let imports =
Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
let Some(resolved) = Resolver::new(&model).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
continue;
};
resolved_imports.insert(path.to_path_buf());
}
Ok(resolved_imports)
}
/// Insert a file path into the module imports.
pub fn insert(&mut self, path: SystemPathBuf) {
self.0.insert(path);
@@ -91,35 +124,3 @@ impl FromIterator<(SystemPathBuf, ModuleImports)> for ImportMap {
map
}
}
/// Generate the module imports for a given Python file.
pub fn generate(
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: bool,
db: &ModuleDb,
) -> Result<ModuleImports> {
// Read and parse the source code.
let file = system_path_to_file(db, path)?;
let parsed = parsed_module(db, file);
let module_path =
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
let model = SemanticModel::new(db, file);
// Collect the imports.
let imports = Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
let Some(resolved) = Resolver::new(&model).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
continue;
};
resolved_imports.insert(path.to_path_buf());
}
Ok(resolved_imports)
}

View File

@@ -1,5 +1,5 @@
use ruff_linter::display_settings;
use ruff_linter::settings::types::{ExtensionMapping, PreviewMode};
use ruff_linter::settings::types::{ExtensionMapping, FilePatternSet, PreviewMode, PythonVersion};
use ruff_macros::CacheKey;
use std::collections::BTreeMap;
use std::fmt;
@@ -7,7 +7,9 @@ use std::path::PathBuf;
#[derive(Debug, Default, Clone, CacheKey)]
pub struct AnalyzeSettings {
pub exclude: FilePatternSet,
pub preview: PreviewMode,
pub target_version: PythonVersion,
pub detect_string_imports: bool,
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
pub extension: ExtensionMapping,
@@ -20,7 +22,9 @@ impl fmt::Display for AnalyzeSettings {
formatter = f,
namespace = "analyze",
fields = [
self.exclude,
self.preview,
self.target_version | debug,
self.detect_string_imports,
self.extension | debug,
self.include_dependencies | debug,

View File

@@ -0,0 +1,20 @@
def f():
"Here's a line ending in a question mark?"
...
def f():
"""Here's a line ending in an exclamation mark!"""
...
def f():
"""Here's a line ending in a colon:"""
...
def f():
"""Here's a line ending in a semi colon;"""
...
def f():
"""Here's a line ending with a whitespace """
...

View File

@@ -30,7 +30,7 @@ use crate::registry::Rule;
/// ```console
/// Traceback (most recent call last):
/// File "tmp.py", line 2, in <module>
/// raise RuntimeError("Some value is incorrect")
/// raise RuntimeError("'Some value' is incorrect")
/// RuntimeError: 'Some value' is incorrect
/// ```
///

View File

@@ -29,7 +29,9 @@ mod tests {
#[test_case(Rule::UndocumentedParam, Path::new("sections.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D400.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D403.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D.py"))]

View File

@@ -1,7 +1,7 @@
use ruff_text_size::TextLen;
use strum::IntoEnumIterator;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines};
use ruff_text_size::Ranged;
@@ -47,14 +47,18 @@ use crate::rules::pydocstyle::helpers::logical_line;
#[violation]
pub struct EndsInPeriod;
impl AlwaysFixableViolation for EndsInPeriod {
impl Violation for EndsInPeriod {
/// `None` in the case a fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("First line should end with a period")
}
fn fix_title(&self) -> String {
"Add period".to_string()
fn fix_title(&self) -> Option<String> {
Some("Add period".to_string())
}
}
@@ -104,7 +108,7 @@ pub(crate) fn ends_with_period(checker: &mut Checker, docstring: &Docstring) {
if !trimmed.ends_with('.') {
let mut diagnostic = Diagnostic::new(EndsInPeriod, docstring.range());
// Best-effort fix: avoid adding a period after other punctuation marks.
if !trimmed.ends_with([':', ';']) {
if !trimmed.ends_with([':', ';', '?', '!']) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
".".to_string(),
line.start() + trimmed.text_len(),

View File

@@ -1,7 +1,7 @@
use ruff_text_size::TextLen;
use strum::IntoEnumIterator;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines};
use ruff_text_size::Ranged;
@@ -46,14 +46,18 @@ use crate::rules::pydocstyle::helpers::logical_line;
#[violation]
pub struct EndsInPunctuation;
impl AlwaysFixableViolation for EndsInPunctuation {
impl Violation for EndsInPunctuation {
/// `None` in the case a fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("First line should end with a period, question mark, or exclamation point")
}
fn fix_title(&self) -> String {
"Add closing punctuation".to_string()
fn fix_title(&self) -> Option<String> {
Some("Add closing punctuation".to_string())
}
}

View File

@@ -194,7 +194,7 @@ D.py:487:5: D400 [*] First line should end with a period
489 489 |
490 490 |
D.py:514:5: D400 [*] First line should end with a period
D.py:514:5: D400 First line should end with a period
|
513 | def valid_google_string(): # noqa: D400
514 | """Test a valid something!"""
@@ -202,16 +202,6 @@ D.py:514:5: D400 [*] First line should end with a period
|
= help: Add period
Unsafe fix
511 511 |
512 512 |
513 513 | def valid_google_string(): # noqa: D400
514 |- """Test a valid something!"""
514 |+ """Test a valid something!."""
515 515 |
516 516 |
517 517 | @expect("D415: First line should end with a period, question mark, "
D.py:520:5: D400 [*] First line should end with a period
|
518 | "or exclamation point (not 'g')")
@@ -328,6 +318,4 @@ D.py:664:5: D400 [*] First line should end with a period
665 |+ but continuations shouldn't be considered multi-line."
666 666 |
667 667 |
668 668 |
668 668 |

View File

@@ -0,0 +1,55 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D400_415.py:2:5: D400 First line should end with a period
|
1 | def f():
2 | "Here's a line ending in a question mark?"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
3 | ...
|
= help: Add period
D400_415.py:7:5: D400 First line should end with a period
|
6 | def f():
7 | """Here's a line ending in an exclamation mark!"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
8 | ...
|
= help: Add period
D400_415.py:11:5: D400 First line should end with a period
|
10 | def f():
11 | """Here's a line ending in a colon:"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
12 | ...
|
= help: Add period
D400_415.py:15:5: D400 First line should end with a period
|
14 | def f():
15 | """Here's a line ending in a semi colon;"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
16 | ...
|
= help: Add period
D400_415.py:19:5: D400 [*] First line should end with a period
|
18 | def f():
19 | """Here's a line ending with a whitespace """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
20 | ...
|
= help: Add period
Unsafe fix
16 16 | ...
17 17 |
18 18 | def f():
19 |- """Here's a line ending with a whitespace """
19 |+ """Here's a line ending with a whitespace. """
20 20 | ...

View File

@@ -0,0 +1,37 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D400_415.py:11:5: D415 First line should end with a period, question mark, or exclamation point
|
10 | def f():
11 | """Here's a line ending in a colon:"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
12 | ...
|
= help: Add closing punctuation
D400_415.py:15:5: D415 First line should end with a period, question mark, or exclamation point
|
14 | def f():
15 | """Here's a line ending in a semi colon;"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
16 | ...
|
= help: Add closing punctuation
D400_415.py:19:5: D415 [*] First line should end with a period, question mark, or exclamation point
|
18 | def f():
19 | """Here's a line ending with a whitespace """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
20 | ...
|
= help: Add closing punctuation
Unsafe fix
16 16 | ...
17 17 |
18 18 | def f():
19 |- """Here's a line ending with a whitespace """
19 |+ """Here's a line ending with a whitespace. """
20 20 | ...

View File

@@ -0,0 +1,37 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D415.py:11:5: D415 First line should end with a period, question mark, or exclamation point
|
10 | def f():
11 | """Here's a line ending in a colon:"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
12 | ...
|
= help: Add closing punctuation
D415.py:15:5: D415 First line should end with a period, question mark, or exclamation point
|
14 | def f():
15 | """Here's a line ending in a semi colon;"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
16 | ...
|
= help: Add closing punctuation
D415.py:19:5: D415 [*] First line should end with a period, question mark, or exclamation point
|
18 | def f():
19 | """Here's a line ending with a whitespace """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
20 | ...
|
= help: Add closing punctuation
Unsafe fix
16 16 | ...
17 17 |
18 18 | def f():
19 |- """Here's a line ending with a whitespace """
19 |+ """Here's a line ending with a whitespace. """
20 20 | ...

View File

@@ -82,30 +82,6 @@ func([1, 2, 3,], bar)
func([(x, y,) for (x, y) in z], bar)
# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged.
def func() -> [1, 2, 3,]:
pass
def func() -> ([1, 2, 3,]):
pass
def func() -> ([1, 2, 3,]):
pass
def func() -> ( # comment
[1, 2, 3,]):
pass
def func() -> (
[1, 2, 3,] # comment
):
pass
def func() -> (
[1, 2, 3,]
# comment
):
pass
# Ensure that nested lists are hugged.
func([

View File

@@ -0,0 +1,176 @@
# Tests for functions without parameters or a dangling comment
# Black's overall behavior is to:
# 1. Print the return type on the same line as the function header if it fits
# 2. Parenthesize the return type if it doesn't fit.
# The exception to this are subscripts, see below
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def no_parameters_string_return_type() -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def no_parameters_overlong_string_return_type() -> (
"ALongIdentifierButDoesntGetParenthesized"
):
pass
# Name return type that fits on the same line as the function header
def no_parameters_name_return_type() -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def no_parameters_overlong_name_return_type() -> (
ALongIdentifierButDoesntGetParenthesized
):
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union() -> (
A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE
):
pass
def test_return_union_with_elements_exceeding_length() -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline strings (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation() -> """str
| list[str]
""":
pass
def test_return_multiline_string_binary_expression_return_type_annotation() -> """str
| list[str]
""" + "b":
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type() -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb"
):
pass
def test_extralong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def no_parameters_subscript_return_type() -> list[str]:
pass
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
# and supporting it requires extra complexity (best_fitting! layout)
def no_parameters_overlong_subscript_return_type_with_single_element() -> (
list[xxxxxxxxxxxxxxxxxxxxx]
):
pass
# 2. Black: Removes the parentheses when the subscript fits after breaking individual elements.
# This is somewhat wasteful because the below list actually fits on a single line when splitting after
# `list[`. It is also inconsistent with how subscripts are normally formatted where it first tries to fit the entire subscript,
# then splits after `list[` but keeps all elements on a single line, and finally, splits after each element.
# IMO: Splitting after the `list[` and trying to keep the elements together when possible seems more consistent.
def no_parameters_subscript_return_type_multiple_elements() -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Black removes the parentheses even the elements exceed the configured line width.
# So does Ruff.
def no_parameters_subscript_return_type_multiple_overlong_elements() -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Black parenthesizes the subscript if its name doesn't fit on the header line.
# So does Ruff
def no_parameters_subscriptreturn_type_with_overlong_value_() -> (
liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
):
pass
# Black: It removes the parentheses when the subscript contains multiple elements as
# `no_parameters_subscript_return_type_multiple_overlong_elements` shows. However, it doesn't
# when the subscript contains a single element. Black then keeps the parentheses.
# Ruff removes the parentheses in this case for consistency.
def no_parameters_overlong_subscript_return_type_with_overlong_single_element() -> (
list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
):
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_binary_expression_return_type_annotation() -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]:
pass
#########################################################################################
# Other
#########################################################################################
# Don't paranthesize lists
def f() -> [
a,
b,
]: pass

View File

@@ -0,0 +1,195 @@
# Tests for functions with parameters.
# The main difference to functions without parameters is that the return type never gets
# parenthesized for values that can't be split (NeedsParentheses::BestFit).
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def parameters_string_return_type(a) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def parameters_overlong_string_return_type(
a,
) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# Name return type that fits on the same line as the function header
def parameters_name_return_type(a) -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def parameters_overlong_name_return_type(
a,
) -> ALongIdentifierButDoesntGetParenthesized:
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union(
a,
) -> A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE:
pass
def test_return_union_with_elements_exceeding_length(
a,
) -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline stirngs (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation(a) -> """str
| list[str]
""":
pass
def test_return_multiline_string_binary_expression_return_type_annotation(a) -> """str
| list[str]
""" + "b":
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type(a) -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type(
a,
) -> "liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb":
pass
def test_extralong_implicit_concatenated_string_return_type(
a,
) -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def parameters_subscript_return_type(a) -> list[str]:
pass
# Unlike with no-parameters, the return type gets never parenthesized.
def parameters_overlong_subscript_return_type_with_single_element(
a
) -> list[xxxxxxxxxxxxxxxxxxxxx]:
pass
def parameters_subscript_return_type_multiple_elements(a) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_subscript_return_type_multiple_overlong_elements(a) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_subscriptreturn_type_with_overlong_value_(
a
) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_overlong_subscript_return_type_with_overlong_single_element(
a
) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Not even in this very ridiculous case
def a():
def b():
def c():
def d():
def e():
def f():
def g():
def h():
def i():
def j():
def k():
def l():
def m():
def n():
def o():
def p():
def q():
def r():
def s():
def t():
def u():
def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeedooooong(
a,
) -> list[
int,
float
]: ...
#########################################################################################
# Magic comma in return type
#########################################################################################
# Black only splits the return type. Ruff also breaks the parameters. This is probably a bug.
def parameters_subscriptreturn_type_with_overlong_value_(a) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]:
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_return_multiline_string_binary_expression_return_type_annotation(
a,
) -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]:
pass

View File

@@ -8,6 +8,7 @@ use crate::expression::parentheses::{
};
use crate::expression::CallChainLayout;
use crate::prelude::*;
use crate::preview::is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled;
#[derive(Default)]
pub struct FormatExprSubscript {
@@ -103,19 +104,25 @@ impl NeedsParentheses for ExprSubscript {
} else {
match self.value.needs_parentheses(self.into(), context) {
OptionalParentheses::BestFit => {
if parent.as_stmt_function_def().is_some_and(|function_def| {
function_def
.returns
.as_deref()
.and_then(Expr::as_subscript_expr)
== Some(self)
}) {
// Don't use the best fitting layout for return type annotation because it results in the
// return type expanding before the parameters.
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
if let Some(function) = parent.as_stmt_function_def() {
if function.returns.as_deref().is_some_and(|returns| {
AnyNodeRef::ptr_eq(returns.into(), self.into())
}) {
if is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(context) &&
function.parameters.is_empty() && !context.comments().has(&*function.parameters) {
// Apply the `optional_parentheses` layout when the subscript
// is in a return type position of a function without parameters.
// This ensures the subscript is parenthesized if it has a very
// long name that goes over the line length limit.
return OptionalParentheses::Multiline
}
// Don't use the best fitting layout for return type annotation because it results in the
// return type expanding before the parameters.
return OptionalParentheses::Never;
}
}
OptionalParentheses::BestFit
}
parentheses => parentheses,
}

View File

@@ -19,7 +19,10 @@ use crate::expression::parentheses::{
OptionalParentheses, Parentheses, Parenthesize,
};
use crate::prelude::*;
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
use crate::preview::{
is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled,
is_hug_parens_with_braces_and_square_brackets_enabled,
};
mod binary_like;
pub(crate) mod expr_attribute;
@@ -324,7 +327,7 @@ fn format_with_parentheses_comments(
)
}
/// Wraps an expression in an optional parentheses except if its [`NeedsParentheses::needs_parentheses`] implementation
/// Wraps an expression in optional parentheses except if its [`NeedsParentheses::needs_parentheses`] implementation
/// indicates that it is okay to omit the parentheses. For example, parentheses can always be omitted for lists,
/// because they already bring their own parentheses.
pub(crate) fn maybe_parenthesize_expression<'a, T>(
@@ -382,23 +385,38 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
OptionalParentheses::Always => OptionalParentheses::Always,
// The reason to add parentheses is to avoid a syntax error when breaking an expression over multiple lines.
// Therefore, it is unnecessary to add an additional pair of parentheses if an outer expression
// is parenthesized.
_ if f.context().node_level().is_parenthesized() => OptionalParentheses::Never,
// is parenthesized. Unless, it's the `Parenthesize::IfBreaksParenthesizedNested` layout
// where parenthesizing nested `maybe_parenthesized_expression` is explicitly desired.
_ if f.context().node_level().is_parenthesized() => {
if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(
f.context(),
) {
OptionalParentheses::Never
} else if matches!(parenthesize, Parenthesize::IfBreaksParenthesizedNested) {
return parenthesize_if_expands(
&expression.format().with_options(Parentheses::Never),
)
.with_indent(!is_expression_huggable(expression, f.context()))
.fmt(f);
} else {
return expression.format().with_options(Parentheses::Never).fmt(f);
}
}
needs_parentheses => needs_parentheses,
};
match needs_parentheses {
OptionalParentheses::Multiline => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.fmt(f)
}
Parenthesize::IfRequired => {
expression.format().with_options(Parentheses::Never).fmt(f)
}
Parenthesize::Optional | Parenthesize::IfBreaks => {
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
if can_omit_optional_parentheses(expression, f.context()) {
optional_parentheses(&expression.format().with_options(Parentheses::Never))
.fmt(f)
@@ -411,7 +429,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
}
},
OptionalParentheses::BestFit => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.fmt(f)
}
@@ -435,13 +453,13 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
}
},
OptionalParentheses::Never => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.with_indent(!is_expression_huggable(expression, f.context()))
.fmt(f)
}
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfRequired => {
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfRequired | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
expression.format().with_options(Parentheses::Never).fmt(f)
}
},

View File

@@ -56,10 +56,15 @@ pub(crate) enum Parenthesize {
/// Adding parentheses is desired to prevent the comments from wandering.
IfRequired,
/// Parenthesizes the expression if the group doesn't fit on a line (e.g., even name expressions are parenthesized), or if
/// the expression doesn't break, but _does_ reports that it always requires parentheses in this position (e.g., walrus
/// operators in function return annotations).
IfBreaksOrIfRequired,
/// Same as [`Self::IfBreaks`] except that it uses [`parenthesize_if_expands`] for expressions
/// with the layout [`NeedsParentheses::BestFit`] which is used by non-splittable
/// expressions like literals, name, and strings.
IfBreaksParenthesized,
/// Same as [`Self::IfBreaksParenthesized`] but uses [`parenthesize_if_expands`] for nested
/// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses
/// when outer parentheses are present.
IfBreaksParenthesizedNested,
}
impl Parenthesize {
@@ -416,27 +421,25 @@ impl Format<PyFormatContext<'_>> for FormatEmptyParenthesized<'_> {
debug_assert!(self.comments[end_of_line_split..]
.iter()
.all(|comment| comment.line_position().is_own_line()));
write!(
f,
[group(&format_args![
token(self.left),
// end-of-line comments
trailing_comments(&self.comments[..end_of_line_split]),
// Avoid unstable formatting with
// ```python
// x = () - (#
// )
// ```
// Without this the comment would go after the empty tuple first, but still expand
// the bin op. In the second formatting pass they are trailing bin op comments
// so the bin op collapse. Suboptimally we keep parentheses around the bin op in
// either case.
(!self.comments[..end_of_line_split].is_empty()).then_some(hard_line_break()),
// own line comments, which need to be indented
soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])),
token(self.right)
])]
)
group(&format_args![
token(self.left),
// end-of-line comments
trailing_comments(&self.comments[..end_of_line_split]),
// Avoid unstable formatting with
// ```python
// x = () - (#
// )
// ```
// Without this the comment would go after the empty tuple first, but still expand
// the bin op. In the second formatting pass they are trailing bin op comments
// so the bin op collapse. Suboptimally we keep parentheses around the bin op in
// either case.
(!self.comments[..end_of_line_split].is_empty()).then_some(hard_line_break()),
// own line comments, which need to be indented
soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])),
token(self.right)
])
.fmt(f)
}
}

View File

@@ -112,7 +112,7 @@ impl FormatNodeRule<WithItem> for FormatWithItem {
maybe_parenthesize_expression(
context_expr,
item,
Parenthesize::IfBreaksOrIfRequired,
Parenthesize::IfBreaksParenthesizedNested,
)
.fmt(f)?;
} else {

View File

@@ -29,3 +29,10 @@ pub(crate) fn is_comprehension_leading_expression_comments_same_line_enabled(
) -> bool {
context.is_preview()
}
/// See [#9447](https://github.com/astral-sh/ruff/issues/9447)
pub(crate) fn is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}

View File

@@ -1,6 +1,3 @@
use ruff_formatter::write;
use ruff_python_ast::{NodeKind, StmtFunctionDef};
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
};
@@ -10,6 +7,8 @@ use crate::prelude::*;
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
use crate::statement::stmt_class_def::FormatDecorators;
use crate::statement::suite::SuiteKind;
use ruff_formatter::write;
use ruff_python_ast::{NodeKind, StmtFunctionDef};
#[derive(Default)]
pub struct FormatStmtFunctionDef;
@@ -112,23 +111,23 @@ fn format_function_header(f: &mut PyFormatter, item: &StmtFunctionDef) -> Format
write!(f, [token("def"), space(), name.format()])?;
if let Some(type_params) = type_params.as_ref() {
write!(f, [type_params.format()])?;
type_params.format().fmt(f)?;
}
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [parameters.format()])?;
parameters.format().fmt(f)?;
if let Some(return_annotation) = returns.as_ref() {
if let Some(return_annotation) = returns.as_deref() {
write!(f, [space(), token("->"), space()])?;
if return_annotation.is_tuple_expr() {
let parentheses = if comments.has_leading(return_annotation.as_ref()) {
let parentheses = if comments.has_leading(return_annotation) {
Parentheses::Always
} else {
Parentheses::Never
};
write!(f, [return_annotation.format().with_options(parentheses)])?;
} else if comments.has_trailing(return_annotation.as_ref()) {
return_annotation.format().with_options(parentheses).fmt(f)
} else if comments.has_trailing(return_annotation) {
// Intentionally parenthesize any return annotations with trailing comments.
// This avoids an instability in cases like:
// ```python
@@ -156,15 +155,17 @@ fn format_function_header(f: &mut PyFormatter, item: &StmtFunctionDef) -> Format
// requires that the parent be aware of how the child is formatted, which
// is challenging. As a compromise, we break those expressions to avoid an
// instability.
write!(
f,
[return_annotation.format().with_options(Parentheses::Always)]
)?;
return_annotation
.format()
.with_options(Parentheses::Always)
.fmt(f)
} else {
let parenthesize = if parameters.is_empty() && !comments.has(parameters.as_ref()) {
// If the parameters are empty, add parentheses if the return annotation
// breaks at all.
Parenthesize::IfBreaksOrIfRequired
// If the parameters are empty, add parentheses around literal expressions
// (any non splitable expression) but avoid parenthesizing subscripts and
// other parenthesized expressions unless necessary.
Parenthesize::IfBreaksParenthesized
} else {
// Otherwise, use our normal rules for parentheses, which allows us to break
// like:
@@ -179,17 +180,11 @@ fn format_function_header(f: &mut PyFormatter, item: &StmtFunctionDef) -> Format
// ```
Parenthesize::IfBreaks
};
write!(
f,
[maybe_parenthesize_expression(
return_annotation,
item,
parenthesize
)]
)?;
maybe_parenthesize_expression(return_annotation, item, parenthesize).fmt(f)
}
} else {
Ok(())
}
Ok(())
});
group(&format_inner).fmt(f)

View File

@@ -155,20 +155,7 @@ def SimplePyFn(
```diff
--- Black
+++ Ruff
@@ -29,14 +29,18 @@
# magic trailing comma in return type, no params
-def a() -> tuple[
- a,
- b,
-]: ...
+def a() -> (
+ tuple[
+ a,
+ b,
+ ]
+): ...
@@ -36,7 +36,9 @@
# magic trailing comma in return type, params
@@ -179,26 +166,7 @@ def SimplePyFn(
p,
q,
]:
@@ -68,11 +72,13 @@
# long return type, no param list
-def foo() -> list[
- Loooooooooooooooooooooooooooooooooooong,
- Loooooooooooooooooooong,
- Looooooooooooong,
-]: ...
+def foo() -> (
+ list[
+ Loooooooooooooooooooooooooooooooooooong,
+ Loooooooooooooooooooong,
+ Looooooooooooong,
+ ]
+): ...
# long function name, no param list, no return value
@@ -93,7 +99,11 @@
@@ -93,7 +95,11 @@
# unskippable type hint (??)
@@ -211,7 +179,7 @@ def SimplePyFn(
pass
@@ -112,7 +122,13 @@
@@ -112,7 +118,13 @@
# don't lose any comments (no magic)
@@ -226,7 +194,7 @@ def SimplePyFn(
... # 6
@@ -120,12 +136,18 @@
@@ -120,12 +132,18 @@
def foo( # 1
a, # 2
b,
@@ -283,12 +251,10 @@ def foo(
# magic trailing comma in return type, no params
def a() -> (
tuple[
a,
b,
]
): ...
def a() -> tuple[
a,
b,
]: ...
# magic trailing comma in return type, params
@@ -326,13 +292,11 @@ def aaaaaaaaaaaaaaaaa(
# long return type, no param list
def foo() -> (
list[
Loooooooooooooooooooooooooooooooooooong,
Loooooooooooooooooooong,
Looooooooooooong,
]
): ...
def foo() -> list[
Loooooooooooooooooooooooooooooooooooong,
Loooooooooooooooooooong,
Looooooooooooong,
]: ...
# long function name, no param list, no return value
@@ -592,5 +556,3 @@ def SimplePyFn(
Buffer[UInt8, 2],
]: ...
```

View File

@@ -88,30 +88,6 @@ func([1, 2, 3,], bar)
func([(x, y,) for (x, y) in z], bar)
# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged.
def func() -> [1, 2, 3,]:
pass
def func() -> ([1, 2, 3,]):
pass
def func() -> ([1, 2, 3,]):
pass
def func() -> ( # comment
[1, 2, 3,]):
pass
def func() -> (
[1, 2, 3,] # comment
):
pass
def func() -> (
[1, 2, 3,]
# comment
):
pass
# Ensure that nested lists are hugged.
func([
@@ -329,68 +305,6 @@ func(
)
# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged.
def func() -> (
[
1,
2,
3,
]
):
pass
def func() -> (
[
1,
2,
3,
]
):
pass
def func() -> (
[
1,
2,
3,
]
):
pass
def func() -> ( # comment
[
1,
2,
3,
]
):
pass
def func() -> (
[
1,
2,
3,
] # comment
):
pass
def func() -> (
[
1,
2,
3,
]
# comment
):
pass
# Ensure that nested lists are hugged.
func(
[
@@ -611,56 +525,7 @@ func(
foo(
# comment
@@ -167,33 +145,27 @@
# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged.
-def func() -> (
- [
- 1,
- 2,
- 3,
- ]
-):
+def func() -> ([
+ 1,
+ 2,
+ 3,
+]):
pass
-def func() -> (
- [
- 1,
- 2,
- 3,
- ]
-):
+def func() -> ([
+ 1,
+ 2,
+ 3,
+]):
pass
-def func() -> (
- [
- 1,
- 2,
- 3,
- ]
-):
+def func() -> ([
+ 1,
+ 2,
+ 3,
+]):
pass
@@ -229,56 +201,46 @@
@@ -167,56 +145,46 @@
# Ensure that nested lists are hugged.
@@ -747,6 +612,3 @@ func(
-)
+])
```

View File

@@ -521,4 +521,67 @@ def process_board_action(
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -131,32 +131,24 @@
# Breaking return type annotations. Black adds parentheses if the parameters are
# empty; otherwise, it leverages the expressions own parentheses if possible.
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
- Set[
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- ]
-): ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+]: ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
- Set[
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- ]
-): ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+]: ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
- Set[
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- ]
-): ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+]: ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
- Set[
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- ]
-): ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
@@ -257,11 +249,8 @@
): ...
-def double() -> (
- first_item
- and foo.bar.baz().bop(
- 1,
- )
+def double() -> first_item and foo.bar.baz().bop(
+ 1,
):
return 2 * a
```

View File

@@ -0,0 +1,492 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/return_type_no_parameters.py
---
## Input
```python
# Tests for functions without parameters or a dangling comment
# Black's overall behavior is to:
# 1. Print the return type on the same line as the function header if it fits
# 2. Parenthesize the return type if it doesn't fit.
# The exception to this are subscripts, see below
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def no_parameters_string_return_type() -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def no_parameters_overlong_string_return_type() -> (
"ALongIdentifierButDoesntGetParenthesized"
):
pass
# Name return type that fits on the same line as the function header
def no_parameters_name_return_type() -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def no_parameters_overlong_name_return_type() -> (
ALongIdentifierButDoesntGetParenthesized
):
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union() -> (
A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE
):
pass
def test_return_union_with_elements_exceeding_length() -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline strings (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation() -> """str
| list[str]
""":
pass
def test_return_multiline_string_binary_expression_return_type_annotation() -> """str
| list[str]
""" + "b":
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type() -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb"
):
pass
def test_extralong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def no_parameters_subscript_return_type() -> list[str]:
pass
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
# and supporting it requires extra complexity (best_fitting! layout)
def no_parameters_overlong_subscript_return_type_with_single_element() -> (
list[xxxxxxxxxxxxxxxxxxxxx]
):
pass
# 2. Black: Removes the parentheses when the subscript fits after breaking individual elements.
# This is somewhat wasteful because the below list actually fits on a single line when splitting after
# `list[`. It is also inconsistent with how subscripts are normally formatted where it first tries to fit the entire subscript,
# then splits after `list[` but keeps all elements on a single line, and finally, splits after each element.
# IMO: Splitting after the `list[` and trying to keep the elements together when possible seems more consistent.
def no_parameters_subscript_return_type_multiple_elements() -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Black removes the parentheses even the elements exceed the configured line width.
# So does Ruff.
def no_parameters_subscript_return_type_multiple_overlong_elements() -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Black parenthesizes the subscript if its name doesn't fit on the header line.
# So does Ruff
def no_parameters_subscriptreturn_type_with_overlong_value_() -> (
liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
):
pass
# Black: It removes the parentheses when the subscript contains multiple elements as
# `no_parameters_subscript_return_type_multiple_overlong_elements` shows. However, it doesn't
# when the subscript contains a single element. Black then keeps the parentheses.
# Ruff removes the parentheses in this case for consistency.
def no_parameters_overlong_subscript_return_type_with_overlong_single_element() -> (
list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
):
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_binary_expression_return_type_annotation() -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]:
pass
#########################################################################################
# Other
#########################################################################################
# Don't paranthesize lists
def f() -> [
a,
b,
]: pass
```
## Output
```python
# Tests for functions without parameters or a dangling comment
# Black's overall behavior is to:
# 1. Print the return type on the same line as the function header if it fits
# 2. Parenthesize the return type if it doesn't fit.
# The exception to this are subscripts, see below
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def no_parameters_string_return_type() -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def no_parameters_overlong_string_return_type() -> (
"ALongIdentifierButDoesntGetParenthesized"
):
pass
# Name return type that fits on the same line as the function header
def no_parameters_name_return_type() -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def no_parameters_overlong_name_return_type() -> (
ALongIdentifierButDoesntGetParenthesized
):
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union() -> (
A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE
):
pass
def test_return_union_with_elements_exceeding_length() -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline strings (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation() -> (
"""str
| list[str]
"""
):
pass
def test_return_multiline_string_binary_expression_return_type_annotation() -> (
"""str
| list[str]
"""
+ "b"
):
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type() -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb"
):
pass
def test_extralong_implicit_concatenated_string_return_type() -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def no_parameters_subscript_return_type() -> list[str]:
pass
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
# and supporting it requires extra complexity (best_fitting! layout)
def no_parameters_overlong_subscript_return_type_with_single_element() -> (
list[xxxxxxxxxxxxxxxxxxxxx]
):
pass
# 2. Black: Removes the parentheses when the subscript fits after breaking individual elements.
# This is somewhat wasteful because the below list actually fits on a single line when splitting after
# `list[`. It is also inconsistent with how subscripts are normally formatted where it first tries to fit the entire subscript,
# then splits after `list[` but keeps all elements on a single line, and finally, splits after each element.
# IMO: Splitting after the `list[` and trying to keep the elements together when possible seems more consistent.
def no_parameters_subscript_return_type_multiple_elements() -> (
list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]
):
pass
# Black removes the parentheses even the elements exceed the configured line width.
# So does Ruff.
def no_parameters_subscript_return_type_multiple_overlong_elements() -> (
list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]
):
pass
# Black parenthesizes the subscript if its name doesn't fit on the header line.
# So does Ruff
def no_parameters_subscriptreturn_type_with_overlong_value_() -> (
liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]
):
pass
# Black: It removes the parentheses when the subscript contains multiple elements as
# `no_parameters_subscript_return_type_multiple_overlong_elements` shows. However, it doesn't
# when the subscript contains a single element. Black then keeps the parentheses.
# Ruff removes the parentheses in this case for consistency.
def no_parameters_overlong_subscript_return_type_with_overlong_single_element() -> (
list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
):
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_binary_expression_return_type_annotation() -> (
aaaaaaaaaaaaaaaaaaaaaaaaaa
> [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]
):
pass
#########################################################################################
# Other
#########################################################################################
# Don't paranthesize lists
def f() -> (
[
a,
b,
]
):
pass
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -58,11 +58,9 @@
#########################################################################################
-def test_return_multiline_string_type_annotation() -> (
- """str
+def test_return_multiline_string_type_annotation() -> """str
| list[str]
-"""
-):
+""":
pass
@@ -108,9 +106,9 @@
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
# and supporting it requires extra complexity (best_fitting! layout)
-def no_parameters_overlong_subscript_return_type_with_single_element() -> (
- list[xxxxxxxxxxxxxxxxxxxxx]
-):
+def no_parameters_overlong_subscript_return_type_with_single_element() -> list[
+ xxxxxxxxxxxxxxxxxxxxx
+]:
pass
@@ -119,23 +117,18 @@
# `list[`. It is also inconsistent with how subscripts are normally formatted where it first tries to fit the entire subscript,
# then splits after `list[` but keeps all elements on a single line, and finally, splits after each element.
# IMO: Splitting after the `list[` and trying to keep the elements together when possible seems more consistent.
-def no_parameters_subscript_return_type_multiple_elements() -> (
- list[
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
- ]
-):
+def no_parameters_subscript_return_type_multiple_elements() -> list[
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+]:
pass
# Black removes the parentheses even the elements exceed the configured line width.
# So does Ruff.
-def no_parameters_subscript_return_type_multiple_overlong_elements() -> (
- list[
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
- ]
-):
+def no_parameters_subscript_return_type_multiple_overlong_elements() -> list[
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+]:
pass
@@ -154,11 +147,9 @@
# `no_parameters_subscript_return_type_multiple_overlong_elements` shows. However, it doesn't
# when the subscript contains a single element. Black then keeps the parentheses.
# Ruff removes the parentheses in this case for consistency.
-def no_parameters_overlong_subscript_return_type_with_overlong_single_element() -> (
- list[
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- ]
-):
+def no_parameters_overlong_subscript_return_type_with_overlong_single_element() -> list[
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+]:
pass
@@ -167,13 +158,10 @@
#########################################################################################
-def test_binary_expression_return_type_annotation() -> (
- aaaaaaaaaaaaaaaaaaaaaaaaaa
- > [
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
- bbbbbbbbbbbbbbbbbbbbbbbbb,
- ]
-):
+def test_binary_expression_return_type_annotation() -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
+ bbbbbbbbbbbbbbbbbbbbbbbbb,
+]:
pass
@@ -183,10 +171,8 @@
# Don't paranthesize lists
-def f() -> (
- [
- a,
- b,
- ]
-):
+def f() -> [
+ a,
+ b,
+]:
pass
```

View File

@@ -0,0 +1,414 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/return_type_parameters.py
---
## Input
```python
# Tests for functions with parameters.
# The main difference to functions without parameters is that the return type never gets
# parenthesized for values that can't be split (NeedsParentheses::BestFit).
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def parameters_string_return_type(a) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def parameters_overlong_string_return_type(
a,
) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# Name return type that fits on the same line as the function header
def parameters_name_return_type(a) -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def parameters_overlong_name_return_type(
a,
) -> ALongIdentifierButDoesntGetParenthesized:
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union(
a,
) -> A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE:
pass
def test_return_union_with_elements_exceeding_length(
a,
) -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline stirngs (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation(a) -> """str
| list[str]
""":
pass
def test_return_multiline_string_binary_expression_return_type_annotation(a) -> """str
| list[str]
""" + "b":
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type(a) -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type(
a,
) -> "liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb":
pass
def test_extralong_implicit_concatenated_string_return_type(
a,
) -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def parameters_subscript_return_type(a) -> list[str]:
pass
# Unlike with no-parameters, the return type gets never parenthesized.
def parameters_overlong_subscript_return_type_with_single_element(
a
) -> list[xxxxxxxxxxxxxxxxxxxxx]:
pass
def parameters_subscript_return_type_multiple_elements(a) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_subscript_return_type_multiple_overlong_elements(a) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_subscriptreturn_type_with_overlong_value_(
a
) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_overlong_subscript_return_type_with_overlong_single_element(
a
) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Not even in this very ridiculous case
def a():
def b():
def c():
def d():
def e():
def f():
def g():
def h():
def i():
def j():
def k():
def l():
def m():
def n():
def o():
def p():
def q():
def r():
def s():
def t():
def u():
def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeedooooong(
a,
) -> list[
int,
float
]: ...
#########################################################################################
# Magic comma in return type
#########################################################################################
# Black only splits the return type. Ruff also breaks the parameters. This is probably a bug.
def parameters_subscriptreturn_type_with_overlong_value_(a) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]:
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_return_multiline_string_binary_expression_return_type_annotation(
a,
) -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]:
pass
```
## Output
```python
# Tests for functions with parameters.
# The main difference to functions without parameters is that the return type never gets
# parenthesized for values that can't be split (NeedsParentheses::BestFit).
#########################################################################################
# Return types that use NeedsParantheses::BestFit layout with the exception of subscript
#########################################################################################
# String return type that fits on the same line
def parameters_string_return_type(a) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# String return type that exceeds the line length
def parameters_overlong_string_return_type(
a,
) -> "ALongIdentifierButDoesntGetParenthesized":
pass
# Name return type that fits on the same line as the function header
def parameters_name_return_type(a) -> ALongIdentifierButDoesntGetParenthesized:
pass
# Name return type that exceeds the configured line width
def parameters_overlong_name_return_type(
a,
) -> ALongIdentifierButDoesntGetParenthesized:
pass
#########################################################################################
# Unions
#########################################################################################
def test_return_overlong_union(
a,
) -> A | B | C | DDDDDDDDDDDDDDDDDDDDDDDD | EEEEEEEEEEEEEEEEEEEEEE:
pass
def test_return_union_with_elements_exceeding_length(
a,
) -> (
A
| B
| Ccccccccccccccccccccccccccccccccc
| DDDDDDDDDDDDDDDDDDDDDDDD
| EEEEEEEEEEEEEEEEEEEEEE
):
pass
#########################################################################################
# Multiline stirngs (NeedsParentheses::Never)
#########################################################################################
def test_return_multiline_string_type_annotation(
a,
) -> """str
| list[str]
""":
pass
def test_return_multiline_string_binary_expression_return_type_annotation(
a,
) -> (
"""str
| list[str]
"""
+ "b"
):
pass
#########################################################################################
# Implicit concatenated strings (NeedsParentheses::Multiline)
#########################################################################################
def test_implicit_concatenated_string_return_type(a) -> "str" "bbbbbbbbbbbbbbbb":
pass
def test_overlong_implicit_concatenated_string_return_type(
a,
) -> "liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb":
pass
def test_extralong_implicit_concatenated_string_return_type(
a,
) -> (
"liiiiiiiiiiiisssssst[str]"
"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccccccccccccccccccc"
):
pass
#########################################################################################
# Subscript
#########################################################################################
def parameters_subscript_return_type(a) -> list[str]:
pass
# Unlike with no-parameters, the return type gets never parenthesized.
def parameters_overlong_subscript_return_type_with_single_element(
a,
) -> list[xxxxxxxxxxxxxxxxxxxxx]:
pass
def parameters_subscript_return_type_multiple_elements(
a,
) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_subscript_return_type_multiple_overlong_elements(
a,
) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]:
pass
def parameters_subscriptreturn_type_with_overlong_value_(
a,
) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
def parameters_overlong_subscript_return_type_with_overlong_single_element(
a,
) -> list[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]:
pass
# Not even in this very ridiculous case
def a():
def b():
def c():
def d():
def e():
def f():
def g():
def h():
def i():
def j():
def k():
def l():
def m():
def n():
def o():
def p():
def q():
def r():
def s():
def t():
def u():
def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeedooooong(
a,
) -> list[
int,
float,
]: ...
#########################################################################################
# Magic comma in return type
#########################################################################################
# Black only splits the return type. Ruff also breaks the parameters. This is probably a bug.
def parameters_subscriptreturn_type_with_overlong_value_(
a,
) -> liiiiiiiiiiiiiiiiiiiiist[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
]:
pass
#########################################################################################
# can_omit_optional_parentheses_layout
#########################################################################################
def test_return_multiline_string_binary_expression_return_type_annotation(
a,
) -> aaaaaaaaaaaaaaaaaaaaaaaaaa > [
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbbbbbbb,
]:
pass
```

View File

@@ -215,7 +215,9 @@ impl Configuration {
let analyze_defaults = AnalyzeSettings::default();
let analyze = AnalyzeSettings {
exclude: FilePatternSet::try_from_iter(analyze.exclude.unwrap_or_default())?,
preview: analyze_preview,
target_version,
extension: self.extension.clone().unwrap_or_default(),
detect_string_imports: analyze
.detect_string_imports
@@ -1218,7 +1220,9 @@ impl FormatConfiguration {
#[derive(Clone, Debug, Default)]
pub struct AnalyzeConfiguration {
pub exclude: Option<Vec<FilePattern>>,
pub preview: Option<PreviewMode>,
pub direction: Option<Direction>,
pub detect_string_imports: Option<bool>,
pub include_dependencies: Option<BTreeMap<PathBuf, (PathBuf, Vec<String>)>>,
@@ -1228,6 +1232,15 @@ impl AnalyzeConfiguration {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: AnalyzeOptions, project_root: &Path) -> Result<Self> {
Ok(Self {
exclude: options.exclude.map(|paths| {
paths
.into_iter()
.map(|pattern| {
let absolute = fs::normalize_path_to(&pattern, project_root);
FilePattern::User(pattern, absolute)
})
.collect()
}),
preview: options.preview.map(PreviewMode::from),
direction: options.direction,
detect_string_imports: options.detect_string_imports,
@@ -1246,6 +1259,7 @@ impl AnalyzeConfiguration {
#[allow(clippy::needless_pass_by_value)]
pub fn combine(self, config: Self) -> Self {
Self {
exclude: self.exclude.or(config.exclude),
preview: self.preview.or(config.preview),
direction: self.direction.or(config.direction),
detect_string_imports: self.detect_string_imports.or(config.detect_string_imports),

View File

@@ -3320,6 +3320,27 @@ pub struct FormatOptions {
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnalyzeOptions {
/// A list of file patterns to exclude from analysis in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
///
/// Exclusions are based on globs, and can be either:
///
/// - Single-path patterns, like `.mypy_cache` (to exclude any directory
/// named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
/// `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
/// - Relative patterns, like `directory/foo.py` (to exclude that specific
/// file) or `directory/*.py` (to exclude any Python files in
/// `directory`). Note that these paths are relative to the project root
/// (e.g., the directory containing your `pyproject.toml`).
///
/// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"
exclude = ["generated"]
"#
)]
pub exclude: Option<Vec<String>>,
/// Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable
/// commands.
#[option(

10
ruff.schema.json generated
View File

@@ -779,6 +779,16 @@
}
]
},
"exclude": {
"description": "A list of file patterns to exclude from analysis in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"include-dependencies": {
"description": "A map from file path to the list of file paths or globs that should be considered dependencies of that file, regardless of whether relevant imports are detected.",
"type": [