From 86146c7354c7bf231a6d44bf067e110435cafbc8 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jan 2025 15:11:36 -0500 Subject: [PATCH 01/77] rename old up040 tests --- .../pyupgrade/{UP040.py => UP040_0.py} | 0 crates/ruff_linter/src/rules/pyupgrade/mod.rs | 4 +-- ..._rules__pyupgrade__tests__UP040_0.py.snap} | 34 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP040.py => UP040_0.py} (100%) rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP040.py.snap => ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap} (83%) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_0.py diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 315cafa5a7b1ca..6e33324dad92d8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -101,7 +101,7 @@ mod tests { #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] - #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] + #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_0.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); @@ -142,7 +142,7 @@ mod tests { #[test] fn non_pep695_type_alias_not_applied_py311() -> Result<()> { let diagnostics = test_path( - Path::new("pyupgrade/UP040.py"), + Path::new("pyupgrade/UP040_0.py"), &settings::LinterSettings { target_version: PythonVersion::Py311, ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap similarity index 83% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap index 0e68a81b0682d2..2d8ead155922c9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 4 | # UP040 5 | x: typing.TypeAlias = int @@ -20,7 +20,7 @@ UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of th 7 7 | 8 8 | # UP040 simple generic -UP040.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 4 | # UP040 5 | x: typing.TypeAlias = int @@ -41,7 +41,7 @@ UP040.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of th 8 8 | # UP040 simple generic 9 9 | T = typing.TypeVar["T"] -UP040.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 8 | # UP040 simple generic 9 | T = typing.TypeVar["T"] @@ -62,7 +62,7 @@ UP040.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 12 12 | # UP040 call style generic 13 13 | T = typing.TypeVar("T") -UP040.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 12 | # UP040 call style generic 13 | T = typing.TypeVar("T") @@ -83,7 +83,7 @@ UP040.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 16 16 | # UP040 bounded generic 17 17 | T = typing.TypeVar("T", bound=int) -UP040.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 16 | # UP040 bounded generic 17 | T = typing.TypeVar("T", bound=int) @@ -104,7 +104,7 @@ UP040.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 20 20 | # UP040 constrained generic 21 21 | T = typing.TypeVar("T", int, str) -UP040.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 20 | # UP040 constrained generic 21 | T = typing.TypeVar("T", int, str) @@ -125,7 +125,7 @@ UP040.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 24 24 | # UP040 contravariant generic 25 25 | T = typing.TypeVar("T", contravariant=True) -UP040.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 24 | # UP040 contravariant generic 25 | T = typing.TypeVar("T", contravariant=True) @@ -146,7 +146,7 @@ UP040.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 28 28 | # UP040 covariant generic 29 29 | T = typing.TypeVar("T", covariant=True) -UP040.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 28 | # UP040 covariant generic 29 | T = typing.TypeVar("T", covariant=True) @@ -167,7 +167,7 @@ UP040.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 32 32 | # UP040 in class scope 33 33 | T = typing.TypeVar["T"] -UP040.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 34 | class Foo: 35 | # reference to global variable @@ -188,7 +188,7 @@ UP040.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 38 38 | # reference to class variable 39 39 | TCLS = typing.TypeVar["TCLS"] -UP040.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword | 38 | # reference to class variable 39 | TCLS = typing.TypeVar["TCLS"] @@ -209,7 +209,7 @@ UP040.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of t 42 42 | # UP040 won't add generics in fix 43 43 | T = typing.TypeVar(*args) -UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 42 | # UP040 won't add generics in fix 43 | T = typing.TypeVar(*args) @@ -230,7 +230,7 @@ UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 46 46 | # OK 47 47 | x: TypeAlias -UP040.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword +UP040_0.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword | 51 | # type alias. 52 | T = typing.TypeVar["T"] @@ -249,7 +249,7 @@ UP040.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation inst 55 55 | 56 56 | from typing import TypeVar, Annotated, TypeAliasType -UP040.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +UP040_0.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | 61 | # https://github.com/astral-sh/ruff/issues/11422 62 | T = TypeVar("T") @@ -274,7 +274,7 @@ UP040.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignme 67 65 | # Bound 68 66 | T = TypeVar("T", bound=SupportGt) -UP040.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +UP040_0.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | 67 | # Bound 68 | T = TypeVar("T", bound=SupportGt) @@ -299,7 +299,7 @@ UP040.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignme 73 71 | # Multiple bounds 74 72 | T1 = TypeVar("T1", bound=SupportGt) -UP040.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword +UP040_0.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword | 75 | T2 = TypeVar("T2") 76 | T3 = TypeVar("T3") @@ -320,7 +320,7 @@ UP040.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment ins 79 79 | # No type_params 80 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) -UP040.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +UP040_0.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | 79 | # No type_params 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) @@ -339,7 +339,7 @@ UP040.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignmen 82 82 | 83 83 | # OK: Other name -UP040.py:81:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +UP040_0.py:81:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | 79 | # No type_params 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) From 72f966d4f0bd029f830b7a7243624648da97aaaa Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jan 2025 15:07:41 -0500 Subject: [PATCH 02/77] handle simplest generic class case --- .../test/fixtures/pyupgrade/UP040_1.py | 11 ++ .../src/checkers/ast/analyze/statement.rs | 3 + .../pyupgrade/rules/use_pep695_type_alias.rs | 150 +++++++++++++++--- 3 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py new file mode 100644 index 00000000000000..b368647d1c04a2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -0,0 +1,11 @@ +from typing import Generic, TypeVar + +T = TypeVar("T", bound=float) + + +class A(Generic[T]): + pass + + +def f(t: T): + pass diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 5611a307f44df6..efeb821d8eb3e8 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -554,6 +554,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::DataclassEnum) { ruff::rules::dataclass_enum(checker, class_def); } + if checker.enabled(Rule::NonPEP695TypeAlias) { + pyupgrade::rules::non_pep695_generic_class(checker, class_def); + } } Stmt::Import(ast::StmtImport { names, range: _ }) => { if checker.enabled(Rule::MultipleImportsOnOneLine) { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index a2b09b46ae72af..520e062b2cad74 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -3,6 +3,7 @@ use itertools::Itertools; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::name::Name; +use ruff_python_ast::StmtClassDef; use ruff_python_ast::{ self as ast, visitor::{self, Visitor}, @@ -59,6 +60,7 @@ pub(crate) struct NonPEP695TypeAlias { enum TypeAliasKind { TypeAlias, TypeAliasType, + GenericClass, } impl Violation for NonPEP695TypeAlias { @@ -73,12 +75,25 @@ impl Violation for NonPEP695TypeAlias { let type_alias_method = match type_alias_kind { TypeAliasKind::TypeAlias => "`TypeAlias` annotation", TypeAliasKind::TypeAliasType => "`TypeAliasType` assignment", + TypeAliasKind::GenericClass => "`Generic` subclass", }; - format!("Type alias `{name}` uses {type_alias_method} instead of the `type` keyword") + match type_alias_kind { + TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => format!( + "Type alias `{name}` uses {type_alias_method} instead of the `type` keyword" + ), + TypeAliasKind::GenericClass => format!( + "Generic class `{name}` uses {type_alias_method} instead of type parameters" + ), + } } fn fix_title(&self) -> Option { - Some("Use the `type` keyword".to_string()) + match self.type_alias_kind { + TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => { + Some("Use the `type` keyword".to_string()) + } + TypeAliasKind::GenericClass => Some("Use type parameters".to_string()), + } } } @@ -214,6 +229,84 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) )); } +/// UP040 +pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtClassDef { + range, + decorator_list, + name, + type_params, + arguments, + body, + } = class_def; + + // it's a runtime error to mix type_params and Generic, so bail out early if we see existing + // type_params + if type_params.is_some() { + return; + } + + let Some(arguments) = arguments.as_ref() else { + return; + }; + + let [Expr::Subscript(ExprSubscript { value, slice, .. })] = arguments.args.as_ref() else { + return; + }; + + if !checker.semantic().match_typing_expr(value, "Generic") { + return; + } + + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(slice); + visitor.vars + }; + + // Type variables must be unique; filter while preserving order. + let vars = vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + let generator = checker.generator(); + checker.diagnostics.push( + Diagnostic::new( + NonPEP695TypeAlias { + name: name.to_string(), + type_alias_kind: TypeAliasKind::GenericClass, + }, + TextRange::new(name.range().start(), arguments.range().end()), + ) + .with_fix(Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&Stmt::from(StmtClassDef { + range: TextRange::default(), + decorator_list: decorator_list.clone(), + name: name.clone(), + type_params: create_type_params(&vars).map(Box::new), + // checked for a single argument above, so this is always None + arguments: None, + body: body.clone(), + })), + *range, + ), + // The fix should be safe given the assumptions here: + // 1. No existing type_params to conflict with + // 2. A single, Generic argument to the class + Applicability::Safe, + )), + ); +} + /// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type. fn create_diagnostic( generator: Generator, @@ -224,6 +317,34 @@ fn create_diagnostic( applicability: Applicability, type_alias_kind: TypeAliasKind, ) -> Diagnostic { + let type_params = create_type_params(vars); + + Diagnostic::new( + NonPEP695TypeAlias { + name: name.to_string(), + type_alias_kind, + }, + stmt_range, + ) + .with_fix(Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&Stmt::from(StmtTypeAlias { + range: TextRange::default(), + name: Box::new(Expr::Name(ExprName { + range: TextRange::default(), + id: name, + ctx: ast::ExprContext::Load, + })), + type_params, + value: Box::new(value.clone()), + })), + stmt_range, + ), + applicability, + )) +} + +fn create_type_params(vars: &[TypeVar]) -> Option { let type_params = if vars.is_empty() { None } else { @@ -257,30 +378,7 @@ fn create_diagnostic( .collect(), }) }; - - Diagnostic::new( - NonPEP695TypeAlias { - name: name.to_string(), - type_alias_kind, - }, - stmt_range, - ) - .with_fix(Fix::applicable_edit( - Edit::range_replacement( - generator.stmt(&Stmt::from(StmtTypeAlias { - range: TextRange::default(), - name: Box::new(Expr::Name(ExprName { - range: TextRange::default(), - id: name, - ctx: ast::ExprContext::Load, - })), - type_params, - value: Box::new(value.clone()), - })), - stmt_range, - ), - applicability, - )) + type_params } #[derive(Debug)] From 7d0c1f9b4127e238e941d91623981d850b748039 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jan 2025 15:13:51 -0500 Subject: [PATCH 03/77] run new test and add snap --- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 + ...__rules__pyupgrade__tests__UP040_1.py.snap | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 6e33324dad92d8..877e5ebd37be5b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -102,6 +102,7 @@ mod tests { #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_0.py"))] + #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap new file mode 100644 index 00000000000000..7e436fb66704b1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +snapshot_kind: text +--- +UP040_1.py:6:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters + | +6 | class A(Generic[T]): + | ^^^^^^^^^^^^^ UP040 +7 | pass + | + = help: Use type parameters + +ℹ Safe fix +3 3 | T = TypeVar("T", bound=float) +4 4 | +5 5 | +6 |-class A(Generic[T]): + 6 |+class A[T: float]: +7 7 | pass +8 8 | +9 9 | From 12874e842bdafd4dd73211ce80ea754a0398c216 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 09:22:48 -0500 Subject: [PATCH 04/77] extend rule to TypeVarTuple --- .../test/fixtures/pyupgrade/UP040_1.py | 7 +- .../pyupgrade/rules/use_pep695_type_alias.rs | 95 +++++++++++++------ ...__rules__pyupgrade__tests__UP040_1.py.snap | 38 ++++++-- 3 files changed, 101 insertions(+), 39 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py index b368647d1c04a2..a54de9ff3e8d26 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -1,11 +1,16 @@ -from typing import Generic, TypeVar +from typing import Generic, TypeVar, TypeVarTuple T = TypeVar("T", bound=float) +Ts = TypeVarTuple("Ts") class A(Generic[T]): pass +class B(Generic[*Ts]): + pass + + def f(t: T): pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 520e062b2cad74..cbd705d56b8655 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -3,13 +3,13 @@ use itertools::Itertools; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::name::Name; -use ruff_python_ast::StmtClassDef; use ruff_python_ast::{ self as ast, visitor::{self, Visitor}, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Keyword, Stmt, StmtAnnAssign, StmtAssign, StmtTypeAlias, TypeParam, TypeParamTypeVar, }; +use ruff_python_ast::{StmtClassDef, TypeParamTypeVarTuple}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; @@ -148,6 +148,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssig expr_name_to_type_var(checker.semantic(), name).unwrap_or(TypeVar { name, restriction: None, + kind: TypeVarKind::Var, }) }) }) @@ -352,29 +353,47 @@ fn create_type_params(vars: &[TypeVar]) -> Option { range: TextRange::default(), type_params: vars .iter() - .map(|TypeVar { name, restriction }| { - TypeParam::TypeVar(TypeParamTypeVar { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - bound: match restriction { - Some(TypeVarRestriction::Bound(bound)) => { - Some(Box::new((*bound).clone())) - } - Some(TypeVarRestriction::Constraint(constraints)) => { - Some(Box::new(Expr::Tuple(ast::ExprTuple { + .map( + |TypeVar { + name, + restriction, + kind, + }| { + match kind { + TypeVarKind::Var => { + TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), - elts: constraints.iter().map(|expr| (*expr).clone()).collect(), - ctx: ast::ExprContext::Load, - parenthesized: true, - }))) + name: Identifier::new(name.id.clone(), TextRange::default()), + bound: match restriction { + Some(TypeVarRestriction::Bound(bound)) => { + Some(Box::new((*bound).clone())) + } + Some(TypeVarRestriction::Constraint(constraints)) => { + Some(Box::new(Expr::Tuple(ast::ExprTuple { + range: TextRange::default(), + elts: constraints + .iter() + .map(|expr| (*expr).clone()) + .collect(), + ctx: ast::ExprContext::Load, + parenthesized: true, + }))) + } + None => None, + }, + // We don't handle defaults here yet. Should perhaps be a different rule since + // defaults are only valid in 3.13+. + default: None, + }) } - None => None, - }, - // We don't handle defaults here yet. Should perhaps be a different rule since - // defaults are only valid in 3.13+. - default: None, - }) - }) + TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), + } + }, + ) .collect(), }) }; @@ -389,10 +408,18 @@ enum TypeVarRestriction<'a> { Constraint(Vec<&'a Expr>), } +#[derive(Debug)] +enum TypeVarKind { + Var, + Tuple, + // ParamSpec, +} + #[derive(Debug)] struct TypeVar<'a> { name: &'a ExprName, restriction: Option>, + kind: TypeVarKind, } struct TypeVarReferenceVisitor<'a> { @@ -437,17 +464,25 @@ fn expr_name_to_type_var<'a>( return Some(TypeVar { name, restriction: None, + kind: TypeVarKind::Var, }); } } Expr::Call(ExprCall { func, arguments, .. }) => { - if semantic.match_typing_expr(func, "TypeVar") - && arguments - .args - .first() - .is_some_and(Expr::is_string_literal_expr) + let kind = if semantic.match_typing_expr(func, "TypeVar") { + TypeVarKind::Var + } else if semantic.match_typing_expr(func, "TypeVarTuple") { + TypeVarKind::Tuple + } else { + return None; + }; + + if arguments + .args + .first() + .is_some_and(Expr::is_string_literal_expr) { let restriction = if let Some(bound) = arguments.find_keyword("bound") { Some(TypeVarRestriction::Bound(&bound.value)) @@ -459,7 +494,11 @@ fn expr_name_to_type_var<'a>( None }; - return Some(TypeVar { name, restriction }); + return Some(TypeVar { + name, + restriction, + kind, + }); } } _ => {} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap index 7e436fb66704b1..d44c54ae17a34b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -2,20 +2,38 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP040_1.py:6:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP040_1.py:7:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters | -6 | class A(Generic[T]): +7 | class A(Generic[T]): | ^^^^^^^^^^^^^ UP040 -7 | pass +8 | pass | = help: Use type parameters ℹ Safe fix -3 3 | T = TypeVar("T", bound=float) -4 4 | +4 4 | Ts = TypeVarTuple("Ts") 5 5 | -6 |-class A(Generic[T]): - 6 |+class A[T: float]: -7 7 | pass -8 8 | -9 9 | +6 6 | +7 |-class A(Generic[T]): + 7 |+class A[T: float]: +8 8 | pass +9 9 | +10 10 | + +UP040_1.py:11:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters + | +11 | class B(Generic[*Ts]): + | ^^^^^^^^^^^^^^^ UP040 +12 | pass + | + = help: Use type parameters + +ℹ Safe fix +8 8 | pass +9 9 | +10 10 | +11 |-class B(Generic[*Ts]): + 11 |+class B[*Ts]: +12 12 | pass +13 13 | +14 14 | From b27b281c21b428f8638c39b1b2c3cbfe45062307 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 09:32:55 -0500 Subject: [PATCH 05/77] extend the rule to ParamSpec --- .../test/fixtures/pyupgrade/UP040_1.py | 7 ++- .../pyupgrade/rules/use_pep695_type_alias.rs | 11 +++- ...__rules__pyupgrade__tests__UP040_1.py.snap | 56 ++++++++++++------- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py index a54de9ff3e8d26..17b9ba8b73d694 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -1,7 +1,8 @@ -from typing import Generic, TypeVar, TypeVarTuple +from typing import Generic, ParamSpec, TypeVar, TypeVarTuple T = TypeVar("T", bound=float) Ts = TypeVarTuple("Ts") +P = ParamSpec("P") class A(Generic[T]): @@ -12,5 +13,9 @@ class B(Generic[*Ts]): pass +class C(Generic[P]): + pass + + def f(t: T): pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index cbd705d56b8655..e86da63c480a08 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -9,7 +9,7 @@ use ruff_python_ast::{ Expr, ExprCall, ExprName, ExprSubscript, Identifier, Keyword, Stmt, StmtAnnAssign, StmtAssign, StmtTypeAlias, TypeParam, TypeParamTypeVar, }; -use ruff_python_ast::{StmtClassDef, TypeParamTypeVarTuple}; +use ruff_python_ast::{StmtClassDef, TypeParamParamSpec, TypeParamTypeVarTuple}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; @@ -391,6 +391,11 @@ fn create_type_params(vars: &[TypeVar]) -> Option { name: Identifier::new(name.id.clone(), TextRange::default()), default: None, }), + TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), } }, ) @@ -412,7 +417,7 @@ enum TypeVarRestriction<'a> { enum TypeVarKind { Var, Tuple, - // ParamSpec, + ParamSpec, } #[derive(Debug)] @@ -475,6 +480,8 @@ fn expr_name_to_type_var<'a>( TypeVarKind::Var } else if semantic.match_typing_expr(func, "TypeVarTuple") { TypeVarKind::Tuple + } else if semantic.match_typing_expr(func, "ParamSpec") { + TypeVarKind::ParamSpec } else { return None; }; diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap index d44c54ae17a34b..015ad2137da46d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -2,38 +2,56 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP040_1.py:7:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP040_1.py:8:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters | -7 | class A(Generic[T]): +8 | class A(Generic[T]): | ^^^^^^^^^^^^^ UP040 -8 | pass +9 | pass | = help: Use type parameters ℹ Safe fix -4 4 | Ts = TypeVarTuple("Ts") -5 5 | +5 5 | P = ParamSpec("P") 6 6 | -7 |-class A(Generic[T]): - 7 |+class A[T: float]: -8 8 | pass -9 9 | +7 7 | +8 |-class A(Generic[T]): + 8 |+class A[T: float]: +9 9 | pass 10 10 | +11 11 | -UP040_1.py:11:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP040_1.py:12:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -11 | class B(Generic[*Ts]): +12 | class B(Generic[*Ts]): | ^^^^^^^^^^^^^^^ UP040 -12 | pass +13 | pass | = help: Use type parameters ℹ Safe fix -8 8 | pass -9 9 | +9 9 | pass 10 10 | -11 |-class B(Generic[*Ts]): - 11 |+class B[*Ts]: -12 12 | pass -13 13 | -14 14 | +11 11 | +12 |-class B(Generic[*Ts]): + 12 |+class B[*Ts]: +13 13 | pass +14 14 | +15 15 | + +UP040_1.py:16:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters + | +16 | class C(Generic[P]): + | ^^^^^^^^^^^^^ UP040 +17 | pass + | + = help: Use type parameters + +ℹ Safe fix +13 13 | pass +14 14 | +15 15 | +16 |-class C(Generic[P]): + 16 |+class C[**P]: +17 17 | pass +18 18 | +19 19 | From 405048681262557eb29c1b5c4a87df08dca9b848 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 09:43:22 -0500 Subject: [PATCH 06/77] factor out From for TypeParam --- .../pyupgrade/rules/use_pep695_type_alias.rs | 113 ++++++++---------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index e86da63c480a08..25d91f9bfa83d5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -318,8 +318,6 @@ fn create_diagnostic( applicability: Applicability, type_alias_kind: TypeAliasKind, ) -> Diagnostic { - let type_params = create_type_params(vars); - Diagnostic::new( NonPEP695TypeAlias { name: name.to_string(), @@ -336,7 +334,7 @@ fn create_diagnostic( id: name, ctx: ast::ExprContext::Load, })), - type_params, + type_params: create_type_params(vars), value: Box::new(value.clone()), })), stmt_range, @@ -346,63 +344,14 @@ fn create_diagnostic( } fn create_type_params(vars: &[TypeVar]) -> Option { - let type_params = if vars.is_empty() { - None - } else { - Some(ast::TypeParams { - range: TextRange::default(), - type_params: vars - .iter() - .map( - |TypeVar { - name, - restriction, - kind, - }| { - match kind { - TypeVarKind::Var => { - TypeParam::TypeVar(TypeParamTypeVar { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - bound: match restriction { - Some(TypeVarRestriction::Bound(bound)) => { - Some(Box::new((*bound).clone())) - } - Some(TypeVarRestriction::Constraint(constraints)) => { - Some(Box::new(Expr::Tuple(ast::ExprTuple { - range: TextRange::default(), - elts: constraints - .iter() - .map(|expr| (*expr).clone()) - .collect(), - ctx: ast::ExprContext::Load, - parenthesized: true, - }))) - } - None => None, - }, - // We don't handle defaults here yet. Should perhaps be a different rule since - // defaults are only valid in 3.13+. - default: None, - }) - } - TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - default: None, - }), - TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - default: None, - }), - } - }, - ) - .collect(), - }) - }; - type_params + if vars.is_empty() { + return None; + } + + Some(ast::TypeParams { + range: TextRange::default(), + type_params: vars.iter().map(TypeParam::from).collect(), + }) } #[derive(Debug)] @@ -427,6 +376,50 @@ struct TypeVar<'a> { kind: TypeVarKind, } +impl<'a> From<&'a TypeVar<'a>> for TypeParam { + fn from( + TypeVar { + name, + restriction, + kind, + }: &'a TypeVar<'a>, + ) -> Self { + match kind { + TypeVarKind::Var => { + TypeParam::TypeVar(TypeParamTypeVar { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + bound: match restriction { + Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())), + Some(TypeVarRestriction::Constraint(constraints)) => { + Some(Box::new(Expr::Tuple(ast::ExprTuple { + range: TextRange::default(), + elts: constraints.iter().map(|expr| (*expr).clone()).collect(), + ctx: ast::ExprContext::Load, + parenthesized: true, + }))) + } + None => None, + }, + // We don't handle defaults here yet. Should perhaps be a different rule since + // defaults are only valid in 3.13+. + default: None, + }) + } + TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), + TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), + } + } +} + struct TypeVarReferenceVisitor<'a> { vars: Vec>, semantic: &'a SemanticModel<'a>, From a58cd2629833b025dbeb7b4a0d53cac417d16b8f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 12:26:23 -0500 Subject: [PATCH 07/77] handle generic function with TypeParams --- .../src/checkers/ast/analyze/statement.rs | 3 + .../pyupgrade/rules/use_pep695_type_alias.rs | 100 +++++++++++++++++- ...__rules__pyupgrade__tests__UP040_1.py.snap | 18 +++- 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index efeb821d8eb3e8..3d2c0b441fdbc3 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -376,6 +376,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::PytestParameterWithDefaultArgument) { flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def); } + if checker.enabled(Rule::NonPEP695TypeAlias) { + pyupgrade::rules::non_pep695_generic_function(checker, function_def); + } } Stmt::Return(_) => { if checker.enabled(Rule::ReturnOutsideFunction) { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 25d91f9bfa83d5..88109359157357 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -9,7 +9,7 @@ use ruff_python_ast::{ Expr, ExprCall, ExprName, ExprSubscript, Identifier, Keyword, Stmt, StmtAnnAssign, StmtAssign, StmtTypeAlias, TypeParam, TypeParamTypeVar, }; -use ruff_python_ast::{StmtClassDef, TypeParamParamSpec, TypeParamTypeVarTuple}; +use ruff_python_ast::{StmtClassDef, StmtFunctionDef, TypeParamParamSpec, TypeParamTypeVarTuple}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; @@ -61,6 +61,7 @@ enum TypeAliasKind { TypeAlias, TypeAliasType, GenericClass, + GenericFunction, } impl Violation for NonPEP695TypeAlias { @@ -76,6 +77,7 @@ impl Violation for NonPEP695TypeAlias { TypeAliasKind::TypeAlias => "`TypeAlias` annotation", TypeAliasKind::TypeAliasType => "`TypeAliasType` assignment", TypeAliasKind::GenericClass => "`Generic` subclass", + TypeAliasKind::GenericFunction => "Generic function", }; match type_alias_kind { TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => format!( @@ -84,6 +86,9 @@ impl Violation for NonPEP695TypeAlias { TypeAliasKind::GenericClass => format!( "Generic class `{name}` uses {type_alias_method} instead of type parameters" ), + TypeAliasKind::GenericFunction => { + format!("Generic function `{name}` should use type parameters") + } } } @@ -92,7 +97,9 @@ impl Violation for NonPEP695TypeAlias { TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => { Some("Use the `type` keyword".to_string()) } - TypeAliasKind::GenericClass => Some("Use type parameters".to_string()), + TypeAliasKind::GenericClass | TypeAliasKind::GenericFunction => { + Some("Use type parameters".to_string()) + } } } } @@ -308,6 +315,95 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl ); } +/// UP040 +pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtFunctionDef { + range, + is_async, + decorator_list, + name, + type_params, + parameters, + returns, + body, + } = function_def; + + // TODO(brent) handle methods, for now return early in a class body. For example, an additional + // generic parameter on the method needs to be handled separately from one already on the class + // + // ```python + // T = TypeVar("T") + // S = TypeVar("S") + // + // class Foo(Generic[T]): + // def bar(self, x: T, y: S) -> S: ... + // + // + // class Foo[T]: + // def bar[S](self, x: T, y: S) -> S: ... + // ``` + if checker.semantic().current_scope().kind.is_class() { + return; + } + + // invalid to mix old-style and new-style generics + if type_params.is_some() { + return; + } + + let mut type_vars = Vec::new(); + for parameter in parameters.iter() { + if let Some(annotation) = parameter.annotation() { + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(annotation); + visitor.vars + }; + type_vars.extend(vars); + } + } + + // Type variables must be unique; filter while preserving order. + let type_vars = type_vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + let generator = checker.generator(); + checker.diagnostics.push( + Diagnostic::new( + NonPEP695TypeAlias { + name: name.to_string(), + type_alias_kind: TypeAliasKind::GenericFunction, + }, + TextRange::new(name.range().start(), parameters.range().end()), + ) + .with_fix(Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&Stmt::from(StmtFunctionDef { + range: TextRange::default(), + is_async: *is_async, + decorator_list: decorator_list.clone(), + name: name.clone(), + type_params: create_type_params(&type_vars).map(Box::new), + parameters: parameters.clone(), + returns: returns.clone(), + body: body.clone(), + })), + *range, + ), + Applicability::Safe, + )), + ); +} + /// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type. fn create_diagnostic( generator: Generator, diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap index 015ad2137da46d..067528dff392ca 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -54,4 +54,20 @@ UP040_1.py:16:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of 16 |+class C[**P]: 17 17 | pass 18 18 | -19 19 | +19 19 | + +UP040_1.py:20:5: UP040 [*] Generic function `f` should use type parameters + | +20 | def f(t: T): + | ^^^^^^^ UP040 +21 | pass + | + = help: Use type parameters + +ℹ Safe fix +17 17 | pass +18 18 | +19 19 | +20 |-def f(t: T): + 20 |+def f[T: float](t: T): +21 21 | pass From 32401732f9fdd232b6fd0352105388ed5091c5e3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 13:12:40 -0500 Subject: [PATCH 08/77] preserve comments in functions --- .../pyupgrade/rules/use_pep695_type_alias.rs | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 88109359157357..948fdbfe03ce26 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -322,14 +322,10 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } let StmtFunctionDef { - range, - is_async, - decorator_list, name, type_params, parameters, - returns, - body, + .. } = function_def; // TODO(brent) handle methods, for now return early in a class body. For example, an additional @@ -376,29 +372,27 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); - let generator = checker.generator(); + if type_vars.is_empty() { + return; + } + + // build the fix as a String to avoid removing comments from the entire function body + let mut type_params = String::from("["); + for tv in type_vars { + tv.fmt_into(&mut type_params, checker.source()); + } + type_params.push(']'); + checker.diagnostics.push( Diagnostic::new( NonPEP695TypeAlias { name: name.to_string(), type_alias_kind: TypeAliasKind::GenericFunction, }, - TextRange::new(name.range().start(), parameters.range().end()), + TextRange::new(name.start(), parameters.end()), ) .with_fix(Fix::applicable_edit( - Edit::range_replacement( - generator.stmt(&Stmt::from(StmtFunctionDef { - range: TextRange::default(), - is_async: *is_async, - decorator_list: decorator_list.clone(), - name: name.clone(), - type_params: create_type_params(&type_vars).map(Box::new), - parameters: parameters.clone(), - returns: returns.clone(), - body: body.clone(), - })), - *range, - ), + Edit::insertion(type_params, name.end()), Applicability::Safe, )), ); @@ -472,6 +466,33 @@ struct TypeVar<'a> { kind: TypeVarKind, } +impl<'a> TypeVar<'a> { + /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover + /// the `TypeVarRestriction` values for generic bounds and constraints. + fn fmt_into(&self, s: &mut String, source: &str) { + s.push_str(&self.name.id); + if let Some(restriction) = &self.restriction { + s.push_str(": "); + match restriction { + TypeVarRestriction::Bound(bound) => { + s.push_str(&source[bound.range()]); + } + TypeVarRestriction::Constraint(vec) => { + let len = vec.len(); + s.push('('); + for (i, v) in vec.iter().enumerate() { + s.push_str(&source[v.range()]); + if i < len - 1 { + s.push_str(", "); + } + } + s.push(')'); + } + } + } + } +} + impl<'a> From<&'a TypeVar<'a>> for TypeParam { fn from( TypeVar { From 3cadd12667c88309da3a76d1317a732a1b6fc065 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 14:02:39 -0500 Subject: [PATCH 09/77] use Edit::insertion for classes too --- .../pyupgrade/rules/use_pep695_type_alias.rs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 948fdbfe03ce26..f2fdedfa0b94b8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -244,12 +244,10 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl } let StmtClassDef { - range, - decorator_list, name, type_params, arguments, - body, + .. } = class_def; // it's a runtime error to mix type_params and Generic, so bail out early if we see existing @@ -262,6 +260,8 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl return; }; + // TODO(brent) only accept a single, Generic argument for now. I think it should be fine to have + // other arguments, but this simplifies the fix just to delete the argument list for now let [Expr::Subscript(ExprSubscript { value, slice, .. })] = arguments.args.as_ref() else { return; }; @@ -280,36 +280,28 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl }; // Type variables must be unique; filter while preserving order. - let vars = vars + let type_vars = vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); - let generator = checker.generator(); + // build the fix as a String to avoid removing comments from the entire function body + let mut type_params = String::from("["); + for tv in type_vars { + tv.fmt_into(&mut type_params, checker.source()); + } + type_params.push_str("]"); + checker.diagnostics.push( Diagnostic::new( NonPEP695TypeAlias { name: name.to_string(), type_alias_kind: TypeAliasKind::GenericClass, }, - TextRange::new(name.range().start(), arguments.range().end()), + TextRange::new(name.start(), arguments.end()), ) .with_fix(Fix::applicable_edit( - Edit::range_replacement( - generator.stmt(&Stmt::from(StmtClassDef { - range: TextRange::default(), - decorator_list: decorator_list.clone(), - name: name.clone(), - type_params: create_type_params(&vars).map(Box::new), - // checked for a single argument above, so this is always None - arguments: None, - body: body.clone(), - })), - *range, - ), - // The fix should be safe given the assumptions here: - // 1. No existing type_params to conflict with - // 2. A single, Generic argument to the class + Edit::replacement(type_params, name.end(), arguments.end()), Applicability::Safe, )), ); @@ -470,6 +462,11 @@ impl<'a> TypeVar<'a> { /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover /// the `TypeVarRestriction` values for generic bounds and constraints. fn fmt_into(&self, s: &mut String, source: &str) { + match self.kind { + TypeVarKind::Var => {} + TypeVarKind::Tuple => s.push('*'), + TypeVarKind::ParamSpec => s.push_str("**"), + } s.push_str(&self.name.id); if let Some(restriction) = &self.restriction { s.push_str(": "); From d2dabc058385069683876b0a3524086c2b0be558 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 14:22:10 -0500 Subject: [PATCH 10/77] factor out fmt_type_vars, sort by kind and then TypeVarTuple and ParamSpec just works for functions too --- .../test/fixtures/pyupgrade/UP040_1.py | 9 ++ .../pyupgrade/rules/use_pep695_type_alias.rs | 44 +++++-- ...__rules__pyupgrade__tests__UP040_1.py.snap | 116 ++++++++++++------ 3 files changed, 116 insertions(+), 53 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py index 17b9ba8b73d694..36d169df5fecf5 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from typing import Generic, ParamSpec, TypeVar, TypeVarTuple T = TypeVar("T", bound=float) @@ -19,3 +20,11 @@ class C(Generic[P]): def f(t: T): pass + + +def g(ts: tuple[*Ts]): + pass + + +def h(p: Callable[P, T]): + pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index f2fdedfa0b94b8..78f829482cf967 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -280,17 +280,21 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl }; // Type variables must be unique; filter while preserving order. - let type_vars = vars + let mut type_vars = vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); - // build the fix as a String to avoid removing comments from the entire function body - let mut type_params = String::from("["); - for tv in type_vars { - tv.fmt_into(&mut type_params, checker.source()); + if type_vars.is_empty() { + return; } - type_params.push_str("]"); + + // generally preserve order, but sort by kind so that the order will be TypeVar..., + // TypeVarTuple..., ParamSpec... + type_vars.sort_by_key(|tv| tv.kind); + + // build the fix as a String to avoid removing comments from the entire function body + let type_params = fmt_type_vars(&type_vars, checker); checker.diagnostics.push( Diagnostic::new( @@ -359,7 +363,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } // Type variables must be unique; filter while preserving order. - let type_vars = type_vars + let mut type_vars = type_vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); @@ -368,12 +372,12 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & return; } + // generally preserve order, but sort by kind so that the order will be TypeVar..., + // TypeVarTuple..., ParamSpec... + type_vars.sort_by_key(|tv| tv.kind); + // build the fix as a String to avoid removing comments from the entire function body - let mut type_params = String::from("["); - for tv in type_vars { - tv.fmt_into(&mut type_params, checker.source()); - } - type_params.push(']'); + let type_params = fmt_type_vars(&type_vars, checker); checker.diagnostics.push( Diagnostic::new( @@ -444,7 +448,7 @@ enum TypeVarRestriction<'a> { Constraint(Vec<&'a Expr>), } -#[derive(Debug)] +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] enum TypeVarKind { Var, Tuple, @@ -458,6 +462,20 @@ struct TypeVar<'a> { kind: TypeVarKind, } +fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { + let nvars = type_vars.len(); + let mut type_params = String::from("["); + for (i, tv) in type_vars.iter().enumerate() { + tv.fmt_into(&mut type_params, checker.source()); + if i < nvars - 1 { + type_params.push_str(", "); + } + } + type_params.push_str("]"); + + type_params +} + impl<'a> TypeVar<'a> { /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover /// the `TypeVarRestriction` values for generic bounds and constraints. diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap index 067528dff392ca..d628e0d83a7478 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -2,72 +2,108 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP040_1.py:8:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters - | -8 | class A(Generic[T]): - | ^^^^^^^^^^^^^ UP040 -9 | pass - | - = help: Use type parameters +UP040_1.py:9:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters + | + 9 | class A(Generic[T]): + | ^^^^^^^^^^^^^ UP040 +10 | pass + | + = help: Use type parameters ℹ Safe fix -5 5 | P = ParamSpec("P") -6 6 | -7 7 | -8 |-class A(Generic[T]): - 8 |+class A[T: float]: -9 9 | pass -10 10 | +6 6 | P = ParamSpec("P") +7 7 | +8 8 | +9 |-class A(Generic[T]): + 9 |+class A[T: float]: +10 10 | pass 11 11 | +12 12 | -UP040_1.py:12:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP040_1.py:13:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -12 | class B(Generic[*Ts]): +13 | class B(Generic[*Ts]): | ^^^^^^^^^^^^^^^ UP040 -13 | pass +14 | pass | = help: Use type parameters ℹ Safe fix -9 9 | pass -10 10 | +10 10 | pass 11 11 | -12 |-class B(Generic[*Ts]): - 12 |+class B[*Ts]: -13 13 | pass -14 14 | +12 12 | +13 |-class B(Generic[*Ts]): + 13 |+class B[*Ts]: +14 14 | pass 15 15 | +16 16 | -UP040_1.py:16:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP040_1.py:17:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters | -16 | class C(Generic[P]): +17 | class C(Generic[P]): | ^^^^^^^^^^^^^ UP040 -17 | pass +18 | pass | = help: Use type parameters ℹ Safe fix -13 13 | pass -14 14 | +14 14 | pass 15 15 | -16 |-class C(Generic[P]): - 16 |+class C[**P]: -17 17 | pass -18 18 | +16 16 | +17 |-class C(Generic[P]): + 17 |+class C[**P]: +18 18 | pass 19 19 | +20 20 | -UP040_1.py:20:5: UP040 [*] Generic function `f` should use type parameters +UP040_1.py:21:5: UP040 [*] Generic function `f` should use type parameters | -20 | def f(t: T): +21 | def f(t: T): | ^^^^^^^ UP040 -21 | pass +22 | pass | = help: Use type parameters ℹ Safe fix -17 17 | pass -18 18 | +18 18 | pass 19 19 | -20 |-def f(t: T): - 20 |+def f[T: float](t: T): -21 21 | pass +20 20 | +21 |-def f(t: T): + 21 |+def f[T: float](t: T): +22 22 | pass +23 23 | +24 24 | + +UP040_1.py:25:5: UP040 [*] Generic function `g` should use type parameters + | +25 | def g(ts: tuple[*Ts]): + | ^^^^^^^^^^^^^^^^^ UP040 +26 | pass + | + = help: Use type parameters + +ℹ Safe fix +22 22 | pass +23 23 | +24 24 | +25 |-def g(ts: tuple[*Ts]): + 25 |+def g[*Ts](ts: tuple[*Ts]): +26 26 | pass +27 27 | +28 28 | + +UP040_1.py:29:5: UP040 [*] Generic function `h` should use type parameters + | +29 | def h(p: Callable[P, T]): + | ^^^^^^^^^^^^^^^^^^^^ UP040 +30 | pass + | + = help: Use type parameters + +ℹ Safe fix +26 26 | pass +27 27 | +28 28 | +29 |-def h(p: Callable[P, T]): + 29 |+def h[T: float, **P](p: Callable[P, T]): +30 30 | pass From 278286b784ee05351870f582a766a20e64530bc1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 14:26:22 -0500 Subject: [PATCH 11/77] test that comments are preserved --- .../test/fixtures/pyupgrade/UP040_1.py | 8 +- ...__rules__pyupgrade__tests__UP040_1.py.snap | 105 ++++++++++-------- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py index 36d169df5fecf5..38a7a01738f0cb 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -7,6 +7,7 @@ class A(Generic[T]): + # Comments in a class body are preserved pass @@ -26,5 +27,10 @@ def g(ts: tuple[*Ts]): pass -def h(p: Callable[P, T]): +def h( + p: Callable[P, T], + # Comment in the middle of a parameter list should be preserved + another_param, + and_another, +): pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap index d628e0d83a7478..5e26267e050cb9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap @@ -6,7 +6,8 @@ UP040_1.py:9:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of t | 9 | class A(Generic[T]): | ^^^^^^^^^^^^^ UP040 -10 | pass +10 | # Comments in a class body are preserved +11 | pass | = help: Use type parameters @@ -16,94 +17,102 @@ UP040_1.py:9:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of t 8 8 | 9 |-class A(Generic[T]): 9 |+class A[T: float]: -10 10 | pass -11 11 | +10 10 | # Comments in a class body are preserved +11 11 | pass 12 12 | -UP040_1.py:13:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP040_1.py:14:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -13 | class B(Generic[*Ts]): +14 | class B(Generic[*Ts]): | ^^^^^^^^^^^^^^^ UP040 -14 | pass +15 | pass | = help: Use type parameters ℹ Safe fix -10 10 | pass -11 11 | +11 11 | pass 12 12 | -13 |-class B(Generic[*Ts]): - 13 |+class B[*Ts]: -14 14 | pass -15 15 | +13 13 | +14 |-class B(Generic[*Ts]): + 14 |+class B[*Ts]: +15 15 | pass 16 16 | +17 17 | -UP040_1.py:17:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP040_1.py:18:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters | -17 | class C(Generic[P]): +18 | class C(Generic[P]): | ^^^^^^^^^^^^^ UP040 -18 | pass +19 | pass | = help: Use type parameters ℹ Safe fix -14 14 | pass -15 15 | +15 15 | pass 16 16 | -17 |-class C(Generic[P]): - 17 |+class C[**P]: -18 18 | pass -19 19 | +17 17 | +18 |-class C(Generic[P]): + 18 |+class C[**P]: +19 19 | pass 20 20 | +21 21 | -UP040_1.py:21:5: UP040 [*] Generic function `f` should use type parameters +UP040_1.py:22:5: UP040 [*] Generic function `f` should use type parameters | -21 | def f(t: T): +22 | def f(t: T): | ^^^^^^^ UP040 -22 | pass +23 | pass | = help: Use type parameters ℹ Safe fix -18 18 | pass -19 19 | +19 19 | pass 20 20 | -21 |-def f(t: T): - 21 |+def f[T: float](t: T): -22 22 | pass -23 23 | +21 21 | +22 |-def f(t: T): + 22 |+def f[T: float](t: T): +23 23 | pass 24 24 | +25 25 | -UP040_1.py:25:5: UP040 [*] Generic function `g` should use type parameters +UP040_1.py:26:5: UP040 [*] Generic function `g` should use type parameters | -25 | def g(ts: tuple[*Ts]): +26 | def g(ts: tuple[*Ts]): | ^^^^^^^^^^^^^^^^^ UP040 -26 | pass +27 | pass | = help: Use type parameters ℹ Safe fix -22 22 | pass -23 23 | +23 23 | pass 24 24 | -25 |-def g(ts: tuple[*Ts]): - 25 |+def g[*Ts](ts: tuple[*Ts]): -26 26 | pass -27 27 | +25 25 | +26 |-def g(ts: tuple[*Ts]): + 26 |+def g[*Ts](ts: tuple[*Ts]): +27 27 | pass 28 28 | +29 29 | -UP040_1.py:29:5: UP040 [*] Generic function `h` should use type parameters +UP040_1.py:30:5: UP040 [*] Generic function `h` should use type parameters | -29 | def h(p: Callable[P, T]): - | ^^^^^^^^^^^^^^^^^^^^ UP040 -30 | pass +30 | def h( + | _____^ +31 | | p: Callable[P, T], +32 | | # Comment in the middle of a parameter list should be preserved +33 | | another_param, +34 | | and_another, +35 | | ): + | |_^ UP040 +36 | pass | = help: Use type parameters ℹ Safe fix -26 26 | pass -27 27 | +27 27 | pass 28 28 | -29 |-def h(p: Callable[P, T]): - 29 |+def h[T: float, **P](p: Callable[P, T]): -30 30 | pass +29 29 | +30 |-def h( + 30 |+def h[T: float, **P]( +31 31 | p: Callable[P, T], +32 32 | # Comment in the middle of a parameter list should be preserved +33 33 | another_param, From 3af9428219916e783dcee81749ce6712d9f0966f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 14:29:17 -0500 Subject: [PATCH 12/77] test that methods are not currently changed, but add todo --- .../test/fixtures/pyupgrade/UP040_1.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py index 38a7a01738f0cb..51ca3ef78a4121 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py @@ -34,3 +34,20 @@ def h( and_another, ): pass + + +# TODO(brent) we should also apply the fix to methods, but it will need a +# little more work. these should be left alone for now but be fixed eventually. +class NotGeneric: + # -> generic_method[T: float](t: T) + def generic_method(t: T): + pass + + +# This one is strange in particular because of the mix of old- and new-style +# generics, but according to the PEP, this is okay "if the class, function, or +# type alias does not use the new syntax." `more_generic` doesn't use the new +# syntax, so it can use T from the module and U from the class scope. +class MixedGenerics[U]: + def more_generic(u: U, t: T): + pass From 8176629913d250e5275296f621549bb92079284f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 14:38:00 -0500 Subject: [PATCH 13/77] clippy --- .../src/rules/pyupgrade/rules/use_pep695_type_alias.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 78f829482cf967..713fd2df1bcb37 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -348,7 +348,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } let mut type_vars = Vec::new(); - for parameter in parameters.iter() { + for parameter in parameters { if let Some(annotation) = parameter.annotation() { let vars = { let mut visitor = TypeVarReferenceVisitor { @@ -471,12 +471,12 @@ fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { type_params.push_str(", "); } } - type_params.push_str("]"); + type_params.push(']'); type_params } -impl<'a> TypeVar<'a> { +impl TypeVar<'_> { /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover /// the `TypeVarRestriction` values for generic bounds and constraints. fn fmt_into(&self, s: &mut String, source: &str) { From ca928689f26f2395301a0afc478d938a1051a4ad Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 17:15:34 -0500 Subject: [PATCH 14/77] separate up040 and (new) up046 with shared super module --- .../src/rules/pyupgrade/rules/mod.rs | 4 +- .../src/rules/pyupgrade/rules/pep695.rs | 216 ++++++ .../rules/pep695/use_pep695_type_alias.rs | 269 ++++++++ .../rules/pep695/use_pep695_type_parameter.rs | 245 +++++++ .../pyupgrade/rules/use_pep695_type_alias.rs | 639 ------------------ 5 files changed, 732 insertions(+), 641 deletions(-) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index ac3ea97d308568..d15801edc36df5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -14,6 +14,7 @@ pub(crate) use native_literals::*; pub(crate) use open_alias::*; pub(crate) use os_error_alias::*; pub(crate) use outdated_version_block::*; +pub(crate) use pep695::*; pub(crate) use printf_string_formatting::*; pub(crate) use quoted_annotation::*; pub(crate) use redundant_open_modes::*; @@ -36,7 +37,6 @@ pub(crate) use use_pep585_annotation::*; pub(crate) use use_pep604_annotation::*; pub(crate) use use_pep604_isinstance::*; pub(crate) use use_pep646_unpack::*; -pub(crate) use use_pep695_type_alias::*; pub(crate) use useless_metaclass_type::*; pub(crate) use useless_object_inheritance::*; pub(crate) use yield_in_for_loop::*; @@ -57,6 +57,7 @@ mod native_literals; mod open_alias; mod os_error_alias; mod outdated_version_block; +mod pep695; mod printf_string_formatting; mod quoted_annotation; mod redundant_open_modes; @@ -79,7 +80,6 @@ mod use_pep585_annotation; mod use_pep604_annotation; mod use_pep604_isinstance; mod use_pep646_unpack; -mod use_pep695_type_alias; mod useless_metaclass_type; mod useless_object_inheritance; mod yield_in_for_loop; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs new file mode 100644 index 00000000000000..83d1293fe724a2 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -0,0 +1,216 @@ +//! Shared code for [`use_pep695_type_alias`] (UP040) and [`use_pep695_type_parameter`] (UP046) + +use ruff_python_ast::{ + self as ast, + visitor::{self, Visitor}, + Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam, + TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, +}; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; + +pub(crate) use use_pep695_type_alias::*; +pub(crate) use use_pep695_type_parameter::*; + +mod use_pep695_type_alias; +mod use_pep695_type_parameter; + +#[derive(Debug)] +enum TypeVarRestriction<'a> { + /// A type variable with a bound, e.g., `TypeVar("T", bound=int)`. + Bound(&'a Expr), + /// A type variable with constraints, e.g., `TypeVar("T", int, str)`. + Constraint(Vec<&'a Expr>), +} + +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +enum TypeVarKind { + Var, + Tuple, + ParamSpec, +} + +#[derive(Debug)] +struct TypeVar<'a> { + name: &'a ExprName, + restriction: Option>, + kind: TypeVarKind, +} + +fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { + let nvars = type_vars.len(); + let mut type_params = String::from("["); + for (i, tv) in type_vars.iter().enumerate() { + tv.fmt_into(&mut type_params, checker.source()); + if i < nvars - 1 { + type_params.push_str(", "); + } + } + type_params.push(']'); + + type_params +} + +impl TypeVar<'_> { + /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover + /// the `TypeVarRestriction` values for generic bounds and constraints. + fn fmt_into(&self, s: &mut String, source: &str) { + match self.kind { + TypeVarKind::Var => {} + TypeVarKind::Tuple => s.push('*'), + TypeVarKind::ParamSpec => s.push_str("**"), + } + s.push_str(&self.name.id); + if let Some(restriction) = &self.restriction { + s.push_str(": "); + match restriction { + TypeVarRestriction::Bound(bound) => { + s.push_str(&source[bound.range()]); + } + TypeVarRestriction::Constraint(vec) => { + let len = vec.len(); + s.push('('); + for (i, v) in vec.iter().enumerate() { + s.push_str(&source[v.range()]); + if i < len - 1 { + s.push_str(", "); + } + } + s.push(')'); + } + } + } + } +} + +impl<'a> From<&'a TypeVar<'a>> for TypeParam { + fn from( + TypeVar { + name, + restriction, + kind, + }: &'a TypeVar<'a>, + ) -> Self { + match kind { + TypeVarKind::Var => { + TypeParam::TypeVar(TypeParamTypeVar { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + bound: match restriction { + Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())), + Some(TypeVarRestriction::Constraint(constraints)) => { + Some(Box::new(Expr::Tuple(ast::ExprTuple { + range: TextRange::default(), + elts: constraints.iter().map(|expr| (*expr).clone()).collect(), + ctx: ast::ExprContext::Load, + parenthesized: true, + }))) + } + None => None, + }, + // We don't handle defaults here yet. Should perhaps be a different rule since + // defaults are only valid in 3.13+. + default: None, + }) + } + TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), + TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { + range: TextRange::default(), + name: Identifier::new(name.id.clone(), TextRange::default()), + default: None, + }), + } + } +} + +struct TypeVarReferenceVisitor<'a> { + vars: Vec>, + semantic: &'a SemanticModel<'a>, +} + +/// Recursively collects the names of type variable references present in an expression. +impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { + fn visit_expr(&mut self, expr: &'a Expr) { + match expr { + Expr::Name(name) if name.ctx.is_load() => { + self.vars.extend(expr_name_to_type_var(self.semantic, name)); + } + _ => visitor::walk_expr(self, expr), + } + } +} + +fn expr_name_to_type_var<'a>( + semantic: &'a SemanticModel, + name: &'a ExprName, +) -> Option> { + let Some(Stmt::Assign(StmtAssign { value, .. })) = semantic + .lookup_symbol(name.id.as_str()) + .and_then(|binding_id| { + semantic + .binding(binding_id) + .source + .map(|node_id| semantic.statement(node_id)) + }) + else { + return None; + }; + + match value.as_ref() { + Expr::Subscript(ExprSubscript { + value: ref subscript_value, + .. + }) => { + if semantic.match_typing_expr(subscript_value, "TypeVar") { + return Some(TypeVar { + name, + restriction: None, + kind: TypeVarKind::Var, + }); + } + } + Expr::Call(ExprCall { + func, arguments, .. + }) => { + let kind = if semantic.match_typing_expr(func, "TypeVar") { + TypeVarKind::Var + } else if semantic.match_typing_expr(func, "TypeVarTuple") { + TypeVarKind::Tuple + } else if semantic.match_typing_expr(func, "ParamSpec") { + TypeVarKind::ParamSpec + } else { + return None; + }; + + if arguments + .args + .first() + .is_some_and(Expr::is_string_literal_expr) + { + let restriction = if let Some(bound) = arguments.find_keyword("bound") { + Some(TypeVarRestriction::Bound(&bound.value)) + } else if arguments.args.len() > 1 { + Some(TypeVarRestriction::Constraint( + arguments.args.iter().skip(1).collect(), + )) + } else { + None + }; + + return Some(TypeVar { + name, + restriction, + kind, + }); + } + } + _ => {} + } + None +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs new file mode 100644 index 00000000000000..faeb425613717f --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -0,0 +1,269 @@ +use itertools::Itertools; + +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{ + self as ast, visitor::Visitor, Expr, ExprCall, ExprName, Keyword, Stmt, StmtAnnAssign, + StmtAssign, StmtTypeAlias, TypeParam, +}; +use ruff_python_codegen::Generator; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; +use crate::settings::types::PythonVersion; + +use super::{expr_name_to_type_var, TypeVar, TypeVarKind, TypeVarReferenceVisitor}; + +/// ## What it does +/// Checks for use of `TypeAlias` annotations and `TypeAliasType` assignments +/// for declaring type aliases. +/// +/// ## Why is this bad? +/// The `type` keyword was introduced in Python 3.12 by [PEP 695] for defining +/// type aliases. The `type` keyword is easier to read and provides cleaner +/// support for generics. +/// +/// ## Known problems +/// [PEP 695] uses inferred variance for type parameters, instead of the +/// `covariant` and `contravariant` keywords used by `TypeParam` variables. As +/// such, rewriting a `TypeParam` variable to a `type` alias may change its +/// variance. +/// +/// Unlike `TypeParam` variables, [PEP 695]-style `type` aliases cannot be used +/// at runtime. For example, calling `isinstance` on a `type` alias will throw +/// a `TypeError`. As such, rewriting a `TypeParam` via the `type` keyword will +/// cause issues for parameters that are used for such runtime checks. +/// +/// ## Example +/// ```python +/// ListOfInt: TypeAlias = list[int] +/// PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) +/// ``` +/// +/// Use instead: +/// ```python +/// type ListOfInt = list[int] +/// type PositiveInt = Annotated[int, Gt(0)] +/// ``` +/// +/// [PEP 695]: https://peps.python.org/pep-0695/ +#[derive(ViolationMetadata)] +pub(crate) struct NonPEP695TypeAlias { + name: String, + type_alias_kind: TypeAliasKind, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum TypeAliasKind { + TypeAlias, + TypeAliasType, +} + +impl Violation for NonPEP695TypeAlias { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; + + #[derive_message_formats] + fn message(&self) -> String { + let NonPEP695TypeAlias { + name, + type_alias_kind, + } = self; + let type_alias_method = match type_alias_kind { + TypeAliasKind::TypeAlias => "`TypeAlias` annotation", + TypeAliasKind::TypeAliasType => "`TypeAliasType` assignment", + }; + match type_alias_kind { + TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => format!( + "Type alias `{name}` uses {type_alias_method} instead of the `type` keyword" + ), + } + } + + fn fix_title(&self) -> Option { + match self.type_alias_kind { + TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => { + Some("Use the `type` keyword".to_string()) + } + } + } +} + +/// UP040 +pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssign) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtAssign { targets, value, .. } = stmt; + + let Expr::Call(ExprCall { + func, arguments, .. + }) = value.as_ref() + else { + return; + }; + + let [Expr::Name(target_name)] = targets.as_slice() else { + return; + }; + + let [Expr::StringLiteral(name), value] = arguments.args.as_ref() else { + return; + }; + + if &name.value != target_name.id.as_str() { + return; + } + + let type_params = match arguments.keywords.as_ref() { + [] => &[], + [Keyword { + arg: Some(name), + value: Expr::Tuple(type_params), + .. + }] if name.as_str() == "type_params" => type_params.elts.as_slice(), + _ => return, + }; + + if !checker + .semantic() + .match_typing_expr(func.as_ref(), "TypeAliasType") + { + return; + } + + let Some(vars) = type_params + .iter() + .map(|expr| { + expr.as_name_expr().map(|name| { + expr_name_to_type_var(checker.semantic(), name).unwrap_or(TypeVar { + name, + restriction: None, + kind: TypeVarKind::Var, + }) + }) + }) + .collect::>>() + else { + return; + }; + + checker.diagnostics.push(create_diagnostic( + checker.generator(), + stmt.range(), + target_name.id.clone(), + value, + &vars, + Applicability::Safe, + TypeAliasKind::TypeAliasType, + )); +} + +/// UP040 +pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtAnnAssign { + target, + annotation, + value, + .. + } = stmt; + + if !checker + .semantic() + .match_typing_expr(annotation, "TypeAlias") + { + return; + } + + let Expr::Name(ExprName { id: name, .. }) = target.as_ref() else { + return; + }; + + let Some(value) = value else { + return; + }; + + // TODO(zanie): We should check for generic type variables used in the value and define them + // as type params instead + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(value); + visitor.vars + }; + + // Type variables must be unique; filter while preserving order. + let vars = vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + checker.diagnostics.push(create_diagnostic( + checker.generator(), + stmt.range(), + name.clone(), + value, + &vars, + // The fix is only safe in a type stub because new-style aliases have different runtime behavior + // See https://github.com/astral-sh/ruff/issues/6434 + if checker.source_type.is_stub() { + Applicability::Safe + } else { + Applicability::Unsafe + }, + TypeAliasKind::TypeAlias, + )); +} + +/// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type. +fn create_diagnostic( + generator: Generator, + stmt_range: TextRange, + name: Name, + value: &Expr, + vars: &[TypeVar], + applicability: Applicability, + type_alias_kind: TypeAliasKind, +) -> Diagnostic { + Diagnostic::new( + NonPEP695TypeAlias { + name: name.to_string(), + type_alias_kind, + }, + stmt_range, + ) + .with_fix(Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&Stmt::from(StmtTypeAlias { + range: TextRange::default(), + name: Box::new(Expr::Name(ExprName { + range: TextRange::default(), + id: name, + ctx: ast::ExprContext::Load, + })), + type_params: create_type_params(vars), + value: Box::new(value.clone()), + })), + stmt_range, + ), + applicability, + )) +} + +fn create_type_params(vars: &[TypeVar]) -> Option { + if vars.is_empty() { + return None; + } + + Some(ast::TypeParams { + range: TextRange::default(), + type_params: vars.iter().map(TypeParam::from).collect(), + }) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs new file mode 100644 index 00000000000000..8c3b7eec77c557 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -0,0 +1,245 @@ +use itertools::Itertools; + +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; +use ruff_python_ast::{StmtClassDef, StmtFunctionDef}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; +use crate::settings::types::PythonVersion; + +use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; + +/// ## What it does +/// +/// Checks for use of `TypeParam`, `TypeParamTuple`, and `ParamSpec` annotations on generic +/// functions and classes. +/// +/// ## Why is this bad? +/// +/// Special type parameter syntax was introduced in Python 3.12 by [PEP 695] for defining generic +/// functions and classes. This syntax is easier to read and provides cleaner support for generics. +/// +/// ## Known problems +/// +/// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and +/// `contravariant` keywords used by `TypeParam` variables. As such, rewriting a `TypeParam` +/// variable to an in-line type parameter may change its variance. +/// +/// Unlike `TypeParam` variables, [PEP 695]-style type parameters cannot be used at runtime. For +/// example, calling `isinstance(x, T)` with type parameter `T` will raise a `TypeError`. As such, +/// rewriting a `TypeParam` as a type parameter will cause issues for parameters that are used for +/// such runtime checks. +/// +/// ## Example +/// ```python +/// T = TypeVar("T") +/// class GenericClass(Generic[T]): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class GenericClass[T]: +/// ... +/// ``` +/// +/// [PEP 695]: https://peps.python.org/pep-0695/ +#[derive(ViolationMetadata)] +pub(crate) struct NonPEP695TypeParameter { + name: String, + generic_kind: GenericKind, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum GenericKind { + GenericClass, + GenericFunction, +} + +impl Violation for NonPEP695TypeParameter { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; + + #[derive_message_formats] + fn message(&self) -> String { + let NonPEP695TypeParameter { name, generic_kind } = self; + let generic_method = match generic_kind { + GenericKind::GenericClass => "`Generic` subclass", + GenericKind::GenericFunction => "Generic function", + }; + match generic_kind { + GenericKind::GenericClass => { + format!("Generic class `{name}` uses {generic_method} instead of type parameters") + } + GenericKind::GenericFunction => { + format!("Generic function `{name}` should use type parameters") + } + } + } + + fn fix_title(&self) -> Option { + match self.generic_kind { + GenericKind::GenericClass | GenericKind::GenericFunction => { + Some("Use type parameters".to_string()) + } + } + } +} + +/// UP040 +pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtClassDef { + name, + type_params, + arguments, + .. + } = class_def; + + // it's a runtime error to mix type_params and Generic, so bail out early if we see existing + // type_params + if type_params.is_some() { + return; + } + + let Some(arguments) = arguments.as_ref() else { + return; + }; + + // TODO(brent) only accept a single, Generic argument for now. I think it should be fine to have + // other arguments, but this simplifies the fix just to delete the argument list for now + let [Expr::Subscript(ExprSubscript { value, slice, .. })] = arguments.args.as_ref() else { + return; + }; + + if !checker.semantic().match_typing_expr(value, "Generic") { + return; + } + + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(slice); + visitor.vars + }; + + // Type variables must be unique; filter while preserving order. + let mut type_vars = vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + if type_vars.is_empty() { + return; + } + + // generally preserve order, but sort by kind so that the order will be TypeVar..., + // TypeVarTuple..., ParamSpec... + type_vars.sort_by_key(|tv| tv.kind); + + // build the fix as a String to avoid removing comments from the entire function body + let type_params = fmt_type_vars(&type_vars, checker); + + checker.diagnostics.push( + Diagnostic::new( + NonPEP695TypeParameter { + name: name.to_string(), + generic_kind: GenericKind::GenericClass, + }, + TextRange::new(name.start(), arguments.end()), + ) + .with_fix(Fix::applicable_edit( + Edit::replacement(type_params, name.end(), arguments.end()), + Applicability::Safe, + )), + ); +} + +/// UP040 +pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + let StmtFunctionDef { + name, + type_params, + parameters, + .. + } = function_def; + + // TODO(brent) handle methods, for now return early in a class body. For example, an additional + // generic parameter on the method needs to be handled separately from one already on the class + // + // ```python + // T = TypeVar("T") + // S = TypeVar("S") + // + // class Foo(Generic[T]): + // def bar(self, x: T, y: S) -> S: ... + // + // + // class Foo[T]: + // def bar[S](self, x: T, y: S) -> S: ... + // ``` + if checker.semantic().current_scope().kind.is_class() { + return; + } + + // invalid to mix old-style and new-style generics + if type_params.is_some() { + return; + } + + let mut type_vars = Vec::new(); + for parameter in parameters { + if let Some(annotation) = parameter.annotation() { + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(annotation); + visitor.vars + }; + type_vars.extend(vars); + } + } + + // Type variables must be unique; filter while preserving order. + let mut type_vars = type_vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + if type_vars.is_empty() { + return; + } + + // generally preserve order, but sort by kind so that the order will be TypeVar..., + // TypeVarTuple..., ParamSpec... + type_vars.sort_by_key(|tv| tv.kind); + + // build the fix as a String to avoid removing comments from the entire function body + let type_params = fmt_type_vars(&type_vars, checker); + + checker.diagnostics.push( + Diagnostic::new( + NonPEP695TypeParameter { + name: name.to_string(), + generic_kind: GenericKind::GenericFunction, + }, + TextRange::new(name.start(), parameters.end()), + ) + .with_fix(Fix::applicable_edit( + Edit::insertion(type_params, name.end()), + Applicability::Safe, + )), + ); +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs deleted file mode 100644 index 713fd2df1bcb37..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ /dev/null @@ -1,639 +0,0 @@ -use itertools::Itertools; - -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::name::Name; -use ruff_python_ast::{ - self as ast, - visitor::{self, Visitor}, - Expr, ExprCall, ExprName, ExprSubscript, Identifier, Keyword, Stmt, StmtAnnAssign, StmtAssign, - StmtTypeAlias, TypeParam, TypeParamTypeVar, -}; -use ruff_python_ast::{StmtClassDef, StmtFunctionDef, TypeParamParamSpec, TypeParamTypeVarTuple}; -use ruff_python_codegen::Generator; -use ruff_python_semantic::SemanticModel; -use ruff_text_size::{Ranged, TextRange}; - -use crate::checkers::ast::Checker; -use crate::settings::types::PythonVersion; - -/// ## What it does -/// Checks for use of `TypeAlias` annotations and `TypeAliasType` assignments -/// for declaring type aliases. -/// -/// ## Why is this bad? -/// The `type` keyword was introduced in Python 3.12 by [PEP 695] for defining -/// type aliases. The `type` keyword is easier to read and provides cleaner -/// support for generics. -/// -/// ## Known problems -/// [PEP 695] uses inferred variance for type parameters, instead of the -/// `covariant` and `contravariant` keywords used by `TypeParam` variables. As -/// such, rewriting a `TypeParam` variable to a `type` alias may change its -/// variance. -/// -/// Unlike `TypeParam` variables, [PEP 695]-style `type` aliases cannot be used -/// at runtime. For example, calling `isinstance` on a `type` alias will throw -/// a `TypeError`. As such, rewriting a `TypeParam` via the `type` keyword will -/// cause issues for parameters that are used for such runtime checks. -/// -/// ## Example -/// ```python -/// ListOfInt: TypeAlias = list[int] -/// PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) -/// ``` -/// -/// Use instead: -/// ```python -/// type ListOfInt = list[int] -/// type PositiveInt = Annotated[int, Gt(0)] -/// ``` -/// -/// [PEP 695]: https://peps.python.org/pep-0695/ -#[derive(ViolationMetadata)] -pub(crate) struct NonPEP695TypeAlias { - name: String, - type_alias_kind: TypeAliasKind, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum TypeAliasKind { - TypeAlias, - TypeAliasType, - GenericClass, - GenericFunction, -} - -impl Violation for NonPEP695TypeAlias { - const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; - - #[derive_message_formats] - fn message(&self) -> String { - let NonPEP695TypeAlias { - name, - type_alias_kind, - } = self; - let type_alias_method = match type_alias_kind { - TypeAliasKind::TypeAlias => "`TypeAlias` annotation", - TypeAliasKind::TypeAliasType => "`TypeAliasType` assignment", - TypeAliasKind::GenericClass => "`Generic` subclass", - TypeAliasKind::GenericFunction => "Generic function", - }; - match type_alias_kind { - TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => format!( - "Type alias `{name}` uses {type_alias_method} instead of the `type` keyword" - ), - TypeAliasKind::GenericClass => format!( - "Generic class `{name}` uses {type_alias_method} instead of type parameters" - ), - TypeAliasKind::GenericFunction => { - format!("Generic function `{name}` should use type parameters") - } - } - } - - fn fix_title(&self) -> Option { - match self.type_alias_kind { - TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => { - Some("Use the `type` keyword".to_string()) - } - TypeAliasKind::GenericClass | TypeAliasKind::GenericFunction => { - Some("Use type parameters".to_string()) - } - } - } -} - -/// UP040 -pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssign) { - if checker.settings.target_version < PythonVersion::Py312 { - return; - } - - let StmtAssign { targets, value, .. } = stmt; - - let Expr::Call(ExprCall { - func, arguments, .. - }) = value.as_ref() - else { - return; - }; - - let [Expr::Name(target_name)] = targets.as_slice() else { - return; - }; - - let [Expr::StringLiteral(name), value] = arguments.args.as_ref() else { - return; - }; - - if &name.value != target_name.id.as_str() { - return; - } - - let type_params = match arguments.keywords.as_ref() { - [] => &[], - [Keyword { - arg: Some(name), - value: Expr::Tuple(type_params), - .. - }] if name.as_str() == "type_params" => type_params.elts.as_slice(), - _ => return, - }; - - if !checker - .semantic() - .match_typing_expr(func.as_ref(), "TypeAliasType") - { - return; - } - - let Some(vars) = type_params - .iter() - .map(|expr| { - expr.as_name_expr().map(|name| { - expr_name_to_type_var(checker.semantic(), name).unwrap_or(TypeVar { - name, - restriction: None, - kind: TypeVarKind::Var, - }) - }) - }) - .collect::>>() - else { - return; - }; - - checker.diagnostics.push(create_diagnostic( - checker.generator(), - stmt.range(), - target_name.id.clone(), - value, - &vars, - Applicability::Safe, - TypeAliasKind::TypeAliasType, - )); -} - -/// UP040 -pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) { - if checker.settings.target_version < PythonVersion::Py312 { - return; - } - - let StmtAnnAssign { - target, - annotation, - value, - .. - } = stmt; - - if !checker - .semantic() - .match_typing_expr(annotation, "TypeAlias") - { - return; - } - - let Expr::Name(ExprName { id: name, .. }) = target.as_ref() else { - return; - }; - - let Some(value) = value else { - return; - }; - - // TODO(zanie): We should check for generic type variables used in the value and define them - // as type params instead - let vars = { - let mut visitor = TypeVarReferenceVisitor { - vars: vec![], - semantic: checker.semantic(), - }; - visitor.visit_expr(value); - visitor.vars - }; - - // Type variables must be unique; filter while preserving order. - let vars = vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - checker.diagnostics.push(create_diagnostic( - checker.generator(), - stmt.range(), - name.clone(), - value, - &vars, - // The fix is only safe in a type stub because new-style aliases have different runtime behavior - // See https://github.com/astral-sh/ruff/issues/6434 - if checker.source_type.is_stub() { - Applicability::Safe - } else { - Applicability::Unsafe - }, - TypeAliasKind::TypeAlias, - )); -} - -/// UP040 -pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { - if checker.settings.target_version < PythonVersion::Py312 { - return; - } - - let StmtClassDef { - name, - type_params, - arguments, - .. - } = class_def; - - // it's a runtime error to mix type_params and Generic, so bail out early if we see existing - // type_params - if type_params.is_some() { - return; - } - - let Some(arguments) = arguments.as_ref() else { - return; - }; - - // TODO(brent) only accept a single, Generic argument for now. I think it should be fine to have - // other arguments, but this simplifies the fix just to delete the argument list for now - let [Expr::Subscript(ExprSubscript { value, slice, .. })] = arguments.args.as_ref() else { - return; - }; - - if !checker.semantic().match_typing_expr(value, "Generic") { - return; - } - - let vars = { - let mut visitor = TypeVarReferenceVisitor { - vars: vec![], - semantic: checker.semantic(), - }; - visitor.visit_expr(slice); - visitor.vars - }; - - // Type variables must be unique; filter while preserving order. - let mut type_vars = vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - if type_vars.is_empty() { - return; - } - - // generally preserve order, but sort by kind so that the order will be TypeVar..., - // TypeVarTuple..., ParamSpec... - type_vars.sort_by_key(|tv| tv.kind); - - // build the fix as a String to avoid removing comments from the entire function body - let type_params = fmt_type_vars(&type_vars, checker); - - checker.diagnostics.push( - Diagnostic::new( - NonPEP695TypeAlias { - name: name.to_string(), - type_alias_kind: TypeAliasKind::GenericClass, - }, - TextRange::new(name.start(), arguments.end()), - ) - .with_fix(Fix::applicable_edit( - Edit::replacement(type_params, name.end(), arguments.end()), - Applicability::Safe, - )), - ); -} - -/// UP040 -pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { - if checker.settings.target_version < PythonVersion::Py312 { - return; - } - - let StmtFunctionDef { - name, - type_params, - parameters, - .. - } = function_def; - - // TODO(brent) handle methods, for now return early in a class body. For example, an additional - // generic parameter on the method needs to be handled separately from one already on the class - // - // ```python - // T = TypeVar("T") - // S = TypeVar("S") - // - // class Foo(Generic[T]): - // def bar(self, x: T, y: S) -> S: ... - // - // - // class Foo[T]: - // def bar[S](self, x: T, y: S) -> S: ... - // ``` - if checker.semantic().current_scope().kind.is_class() { - return; - } - - // invalid to mix old-style and new-style generics - if type_params.is_some() { - return; - } - - let mut type_vars = Vec::new(); - for parameter in parameters { - if let Some(annotation) = parameter.annotation() { - let vars = { - let mut visitor = TypeVarReferenceVisitor { - vars: vec![], - semantic: checker.semantic(), - }; - visitor.visit_expr(annotation); - visitor.vars - }; - type_vars.extend(vars); - } - } - - // Type variables must be unique; filter while preserving order. - let mut type_vars = type_vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - if type_vars.is_empty() { - return; - } - - // generally preserve order, but sort by kind so that the order will be TypeVar..., - // TypeVarTuple..., ParamSpec... - type_vars.sort_by_key(|tv| tv.kind); - - // build the fix as a String to avoid removing comments from the entire function body - let type_params = fmt_type_vars(&type_vars, checker); - - checker.diagnostics.push( - Diagnostic::new( - NonPEP695TypeAlias { - name: name.to_string(), - type_alias_kind: TypeAliasKind::GenericFunction, - }, - TextRange::new(name.start(), parameters.end()), - ) - .with_fix(Fix::applicable_edit( - Edit::insertion(type_params, name.end()), - Applicability::Safe, - )), - ); -} - -/// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type. -fn create_diagnostic( - generator: Generator, - stmt_range: TextRange, - name: Name, - value: &Expr, - vars: &[TypeVar], - applicability: Applicability, - type_alias_kind: TypeAliasKind, -) -> Diagnostic { - Diagnostic::new( - NonPEP695TypeAlias { - name: name.to_string(), - type_alias_kind, - }, - stmt_range, - ) - .with_fix(Fix::applicable_edit( - Edit::range_replacement( - generator.stmt(&Stmt::from(StmtTypeAlias { - range: TextRange::default(), - name: Box::new(Expr::Name(ExprName { - range: TextRange::default(), - id: name, - ctx: ast::ExprContext::Load, - })), - type_params: create_type_params(vars), - value: Box::new(value.clone()), - })), - stmt_range, - ), - applicability, - )) -} - -fn create_type_params(vars: &[TypeVar]) -> Option { - if vars.is_empty() { - return None; - } - - Some(ast::TypeParams { - range: TextRange::default(), - type_params: vars.iter().map(TypeParam::from).collect(), - }) -} - -#[derive(Debug)] -enum TypeVarRestriction<'a> { - /// A type variable with a bound, e.g., `TypeVar("T", bound=int)`. - Bound(&'a Expr), - /// A type variable with constraints, e.g., `TypeVar("T", int, str)`. - Constraint(Vec<&'a Expr>), -} - -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -enum TypeVarKind { - Var, - Tuple, - ParamSpec, -} - -#[derive(Debug)] -struct TypeVar<'a> { - name: &'a ExprName, - restriction: Option>, - kind: TypeVarKind, -} - -fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { - let nvars = type_vars.len(); - let mut type_params = String::from("["); - for (i, tv) in type_vars.iter().enumerate() { - tv.fmt_into(&mut type_params, checker.source()); - if i < nvars - 1 { - type_params.push_str(", "); - } - } - type_params.push(']'); - - type_params -} - -impl TypeVar<'_> { - /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover - /// the `TypeVarRestriction` values for generic bounds and constraints. - fn fmt_into(&self, s: &mut String, source: &str) { - match self.kind { - TypeVarKind::Var => {} - TypeVarKind::Tuple => s.push('*'), - TypeVarKind::ParamSpec => s.push_str("**"), - } - s.push_str(&self.name.id); - if let Some(restriction) = &self.restriction { - s.push_str(": "); - match restriction { - TypeVarRestriction::Bound(bound) => { - s.push_str(&source[bound.range()]); - } - TypeVarRestriction::Constraint(vec) => { - let len = vec.len(); - s.push('('); - for (i, v) in vec.iter().enumerate() { - s.push_str(&source[v.range()]); - if i < len - 1 { - s.push_str(", "); - } - } - s.push(')'); - } - } - } - } -} - -impl<'a> From<&'a TypeVar<'a>> for TypeParam { - fn from( - TypeVar { - name, - restriction, - kind, - }: &'a TypeVar<'a>, - ) -> Self { - match kind { - TypeVarKind::Var => { - TypeParam::TypeVar(TypeParamTypeVar { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - bound: match restriction { - Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())), - Some(TypeVarRestriction::Constraint(constraints)) => { - Some(Box::new(Expr::Tuple(ast::ExprTuple { - range: TextRange::default(), - elts: constraints.iter().map(|expr| (*expr).clone()).collect(), - ctx: ast::ExprContext::Load, - parenthesized: true, - }))) - } - None => None, - }, - // We don't handle defaults here yet. Should perhaps be a different rule since - // defaults are only valid in 3.13+. - default: None, - }) - } - TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - default: None, - }), - TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { - range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), - default: None, - }), - } - } -} - -struct TypeVarReferenceVisitor<'a> { - vars: Vec>, - semantic: &'a SemanticModel<'a>, -} - -/// Recursively collects the names of type variable references present in an expression. -impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { - fn visit_expr(&mut self, expr: &'a Expr) { - match expr { - Expr::Name(name) if name.ctx.is_load() => { - self.vars.extend(expr_name_to_type_var(self.semantic, name)); - } - _ => visitor::walk_expr(self, expr), - } - } -} - -fn expr_name_to_type_var<'a>( - semantic: &'a SemanticModel, - name: &'a ExprName, -) -> Option> { - let Some(Stmt::Assign(StmtAssign { value, .. })) = semantic - .lookup_symbol(name.id.as_str()) - .and_then(|binding_id| { - semantic - .binding(binding_id) - .source - .map(|node_id| semantic.statement(node_id)) - }) - else { - return None; - }; - - match value.as_ref() { - Expr::Subscript(ExprSubscript { - value: ref subscript_value, - .. - }) => { - if semantic.match_typing_expr(subscript_value, "TypeVar") { - return Some(TypeVar { - name, - restriction: None, - kind: TypeVarKind::Var, - }); - } - } - Expr::Call(ExprCall { - func, arguments, .. - }) => { - let kind = if semantic.match_typing_expr(func, "TypeVar") { - TypeVarKind::Var - } else if semantic.match_typing_expr(func, "TypeVarTuple") { - TypeVarKind::Tuple - } else if semantic.match_typing_expr(func, "ParamSpec") { - TypeVarKind::ParamSpec - } else { - return None; - }; - - if arguments - .args - .first() - .is_some_and(Expr::is_string_literal_expr) - { - let restriction = if let Some(bound) = arguments.find_keyword("bound") { - Some(TypeVarRestriction::Bound(&bound.value)) - } else if arguments.args.len() > 1 { - Some(TypeVarRestriction::Constraint( - arguments.args.iter().skip(1).collect(), - )) - } else { - None - }; - - return Some(TypeVar { - name, - restriction, - kind, - }); - } - } - _ => {} - } - None -} From 9ad964204b0bc3d4b3f3e413ca39989ccdeff860 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 17:34:48 -0500 Subject: [PATCH 15/77] finish separating UP040 and new UP046 --- .../pyupgrade/{UP040_1.py => UP046.py} | 0 .../src/checkers/ast/analyze/statement.rs | 4 ++-- crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 2 +- .../rules/pep695/use_pep695_type_parameter.rs | 4 ++-- ...r__rules__pyupgrade__tests__UP046.py.snap} | 24 +++++++++---------- ruff.schema.json | 1 + 7 files changed, 19 insertions(+), 17 deletions(-) rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP040_1.py => UP046.py} (100%) rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap => ruff_linter__rules__pyupgrade__tests__UP046.py.snap} (72%) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_1.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 3d2c0b441fdbc3..616155caf5a11c 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -376,7 +376,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::PytestParameterWithDefaultArgument) { flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def); } - if checker.enabled(Rule::NonPEP695TypeAlias) { + if checker.enabled(Rule::NonPEP695TypeParameter) { pyupgrade::rules::non_pep695_generic_function(checker, function_def); } } @@ -557,7 +557,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::DataclassEnum) { ruff::rules::dataclass_enum(checker, class_def); } - if checker.enabled(Rule::NonPEP695TypeAlias) { + if checker.enabled(Rule::NonPEP695TypeParameter) { pyupgrade::rules::non_pep695_generic_class(checker, class_def); } } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 91123b2724a213..1e74c067aac26c 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -540,6 +540,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs), (Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack), (Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional), + (Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695TypeParameter), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 877e5ebd37be5b..eda387fba2b0d2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -102,8 +102,8 @@ mod tests { #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_0.py"))] - #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] + #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP046.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 8c3b7eec77c557..a802238e34124b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -87,7 +87,7 @@ impl Violation for NonPEP695TypeParameter { } } -/// UP040 +/// UP046 pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { if checker.settings.target_version < PythonVersion::Py312 { return; @@ -161,7 +161,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl ); } -/// UP040 +/// UP046 pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { if checker.settings.target_version < PythonVersion::Py312 { return; diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap similarity index 72% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 5e26267e050cb9..db22b8690dcf06 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -2,10 +2,10 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP040_1.py:9:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046.py:9:7: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | 9 | class A(Generic[T]): - | ^^^^^^^^^^^^^ UP040 + | ^^^^^^^^^^^^^ UP046 10 | # Comments in a class body are preserved 11 | pass | @@ -21,10 +21,10 @@ UP040_1.py:9:7: UP040 [*] Generic class `A` uses `Generic` subclass instead of t 11 11 | pass 12 12 | -UP040_1.py:14:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046.py:14:7: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | 14 | class B(Generic[*Ts]): - | ^^^^^^^^^^^^^^^ UP040 + | ^^^^^^^^^^^^^^^ UP046 15 | pass | = help: Use type parameters @@ -39,10 +39,10 @@ UP040_1.py:14:7: UP040 [*] Generic class `B` uses `Generic` subclass instead of 16 16 | 17 17 | -UP040_1.py:18:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046.py:18:7: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | 18 | class C(Generic[P]): - | ^^^^^^^^^^^^^ UP040 + | ^^^^^^^^^^^^^ UP046 19 | pass | = help: Use type parameters @@ -57,10 +57,10 @@ UP040_1.py:18:7: UP040 [*] Generic class `C` uses `Generic` subclass instead of 20 20 | 21 21 | -UP040_1.py:22:5: UP040 [*] Generic function `f` should use type parameters +UP046.py:22:5: UP046 [*] Generic function `f` should use type parameters | 22 | def f(t: T): - | ^^^^^^^ UP040 + | ^^^^^^^ UP046 23 | pass | = help: Use type parameters @@ -75,10 +75,10 @@ UP040_1.py:22:5: UP040 [*] Generic function `f` should use type parameters 24 24 | 25 25 | -UP040_1.py:26:5: UP040 [*] Generic function `g` should use type parameters +UP046.py:26:5: UP046 [*] Generic function `g` should use type parameters | 26 | def g(ts: tuple[*Ts]): - | ^^^^^^^^^^^^^^^^^ UP040 + | ^^^^^^^^^^^^^^^^^ UP046 27 | pass | = help: Use type parameters @@ -93,7 +93,7 @@ UP040_1.py:26:5: UP040 [*] Generic function `g` should use type parameters 28 28 | 29 29 | -UP040_1.py:30:5: UP040 [*] Generic function `h` should use type parameters +UP046.py:30:5: UP046 [*] Generic function `h` should use type parameters | 30 | def h( | _____^ @@ -102,7 +102,7 @@ UP040_1.py:30:5: UP040 [*] Generic function `h` should use type parameters 33 | | another_param, 34 | | and_another, 35 | | ): - | |_^ UP040 + | |_^ UP046 36 | pass | = help: Use type parameters diff --git a/ruff.schema.json b/ruff.schema.json index 7469867d0197c7..05bbce31c51b09 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4187,6 +4187,7 @@ "UP043", "UP044", "UP045", + "UP046", "W", "W1", "W19", From ea0e276231eef420c2f8d91420d45e633a9a6935 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 17:35:19 -0500 Subject: [PATCH 16/77] Revert "rename old up040 tests" This reverts commit 7a8cd3d3790e1048ba743d9146ea45c802069bea. --- .../pyupgrade/{UP040_0.py => UP040.py} | 0 crates/ruff_linter/src/rules/pyupgrade/mod.rs | 4 +-- ...r__rules__pyupgrade__tests__UP040.py.snap} | 34 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP040_0.py => UP040.py} (100%) rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap => ruff_linter__rules__pyupgrade__tests__UP040.py.snap} (83%) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040_0.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index eda387fba2b0d2..0f3c1a3ceffdea 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -101,7 +101,7 @@ mod tests { #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] - #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040_0.py"))] + #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP046.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -143,7 +143,7 @@ mod tests { #[test] fn non_pep695_type_alias_not_applied_py311() -> Result<()> { let diagnostics = test_path( - Path::new("pyupgrade/UP040_0.py"), + Path::new("pyupgrade/UP040.py"), &settings::LinterSettings { target_version: PythonVersion::Py311, ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap similarity index 83% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index 2d8ead155922c9..0e68a81b0682d2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -UP040_0.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 4 | # UP040 5 | x: typing.TypeAlias = int @@ -20,7 +20,7 @@ UP040_0.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 7 7 | 8 8 | # UP040 simple generic -UP040_0.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 4 | # UP040 5 | x: typing.TypeAlias = int @@ -41,7 +41,7 @@ UP040_0.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 8 8 | # UP040 simple generic 9 9 | T = typing.TypeVar["T"] -UP040_0.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 8 | # UP040 simple generic 9 | T = typing.TypeVar["T"] @@ -62,7 +62,7 @@ UP040_0.py:10:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 12 12 | # UP040 call style generic 13 13 | T = typing.TypeVar("T") -UP040_0.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 12 | # UP040 call style generic 13 | T = typing.TypeVar("T") @@ -83,7 +83,7 @@ UP040_0.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 16 16 | # UP040 bounded generic 17 17 | T = typing.TypeVar("T", bound=int) -UP040_0.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 16 | # UP040 bounded generic 17 | T = typing.TypeVar("T", bound=int) @@ -104,7 +104,7 @@ UP040_0.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 20 20 | # UP040 constrained generic 21 21 | T = typing.TypeVar("T", int, str) -UP040_0.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 20 | # UP040 constrained generic 21 | T = typing.TypeVar("T", int, str) @@ -125,7 +125,7 @@ UP040_0.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 24 24 | # UP040 contravariant generic 25 25 | T = typing.TypeVar("T", contravariant=True) -UP040_0.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 24 | # UP040 contravariant generic 25 | T = typing.TypeVar("T", contravariant=True) @@ -146,7 +146,7 @@ UP040_0.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 28 28 | # UP040 covariant generic 29 29 | T = typing.TypeVar("T", covariant=True) -UP040_0.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 28 | # UP040 covariant generic 29 | T = typing.TypeVar("T", covariant=True) @@ -167,7 +167,7 @@ UP040_0.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 32 32 | # UP040 in class scope 33 33 | T = typing.TypeVar["T"] -UP040_0.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 34 | class Foo: 35 | # reference to global variable @@ -188,7 +188,7 @@ UP040_0.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 38 38 | # reference to class variable 39 39 | TCLS = typing.TypeVar["TCLS"] -UP040_0.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword | 38 | # reference to class variable 39 | TCLS = typing.TypeVar["TCLS"] @@ -209,7 +209,7 @@ UP040_0.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of 42 42 | # UP040 won't add generics in fix 43 43 | T = typing.TypeVar(*args) -UP040_0.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | 42 | # UP040 won't add generics in fix 43 | T = typing.TypeVar(*args) @@ -230,7 +230,7 @@ UP040_0.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of 46 46 | # OK 47 47 | x: TypeAlias -UP040_0.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword | 51 | # type alias. 52 | T = typing.TypeVar["T"] @@ -249,7 +249,7 @@ UP040_0.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation in 55 55 | 56 56 | from typing import TypeVar, Annotated, TypeAliasType -UP040_0.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +UP040.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | 61 | # https://github.com/astral-sh/ruff/issues/11422 62 | T = TypeVar("T") @@ -274,7 +274,7 @@ UP040_0.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assign 67 65 | # Bound 68 66 | T = TypeVar("T", bound=SupportGt) -UP040_0.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +UP040.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | 67 | # Bound 68 | T = TypeVar("T", bound=SupportGt) @@ -299,7 +299,7 @@ UP040_0.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assign 73 71 | # Multiple bounds 74 72 | T1 = TypeVar("T1", bound=SupportGt) -UP040_0.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword +UP040.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword | 75 | T2 = TypeVar("T2") 76 | T3 = TypeVar("T3") @@ -320,7 +320,7 @@ UP040_0.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment i 79 79 | # No type_params 80 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) -UP040_0.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +UP040.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | 79 | # No type_params 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) @@ -339,7 +339,7 @@ UP040_0.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignm 82 82 | 83 83 | # OK: Other name -UP040_0.py:81:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +UP040.py:81:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | 79 | # No type_params 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) From 7d616f8188c96d2febc3a976332261fb41b32c40 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 17:57:47 -0500 Subject: [PATCH 17/77] document fmt_type_vars --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index 83d1293fe724a2..df13b5ac229dfd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -39,6 +39,8 @@ struct TypeVar<'a> { kind: TypeVarKind, } +/// Format a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T, *Ts, **P]`). +/// See [`TypeVar::fmt_into`] for further details. fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { let nvars = type_vars.len(); let mut type_params = String::from("["); From c6c434cbc75497dbba65caef892801759aeff6d2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 17 Jan 2025 22:47:53 -0500 Subject: [PATCH 18/77] tidy Violation code after split --- .../pyupgrade/rules/pep695/use_pep695_type_alias.rs | 12 ++---------- .../rules/pep695/use_pep695_type_parameter.rs | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index faeb425613717f..609f632ed14e7e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -73,19 +73,11 @@ impl Violation for NonPEP695TypeAlias { TypeAliasKind::TypeAlias => "`TypeAlias` annotation", TypeAliasKind::TypeAliasType => "`TypeAliasType` assignment", }; - match type_alias_kind { - TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => format!( - "Type alias `{name}` uses {type_alias_method} instead of the `type` keyword" - ), - } + format!("Type alias `{name}` uses {type_alias_method} instead of the `type` keyword") } fn fix_title(&self) -> Option { - match self.type_alias_kind { - TypeAliasKind::TypeAlias | TypeAliasKind::TypeAliasType => { - Some("Use the `type` keyword".to_string()) - } - } + Some("Use the `type` keyword".to_string()) } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index a802238e34124b..c3da1a9105adbe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -64,13 +64,9 @@ impl Violation for NonPEP695TypeParameter { #[derive_message_formats] fn message(&self) -> String { let NonPEP695TypeParameter { name, generic_kind } = self; - let generic_method = match generic_kind { - GenericKind::GenericClass => "`Generic` subclass", - GenericKind::GenericFunction => "Generic function", - }; match generic_kind { GenericKind::GenericClass => { - format!("Generic class `{name}` uses {generic_method} instead of type parameters") + format!("Generic class `{name}` uses `Generic` subclass instead of type parameters") } GenericKind::GenericFunction => { format!("Generic function `{name}` should use type parameters") @@ -79,11 +75,7 @@ impl Violation for NonPEP695TypeParameter { } fn fix_title(&self) -> Option { - match self.generic_kind { - GenericKind::GenericClass | GenericKind::GenericFunction => { - Some("Use type parameters".to_string()) - } - } + Some("Use type parameters".to_string()) } } From 821df96466db307af4ae98e58c978bb8f4657419 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:20:46 -0500 Subject: [PATCH 19/77] bail out on duplicate type variables --- .../resources/test/fixtures/pyupgrade/UP046.py | 5 +++++ .../rules/pep695/use_pep695_type_parameter.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 51ca3ef78a4121..233ff8d85090f2 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -36,6 +36,11 @@ def h( pass +# These cases are not handled +class D(Generic[T, T]): # duplicate generic variable, runtime error + pass + + # TODO(brent) we should also apply the fix to methods, but it will need a # little more work. these should be left alone for now but be fixed eventually. class NotGeneric: diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index c3da1a9105adbe..1eac1206acfa1d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -122,6 +122,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl }; // Type variables must be unique; filter while preserving order. + let nvars = vars.len(); let mut type_vars = vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) @@ -131,6 +132,11 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl return; } + // non-unique type variables are runtime errors, so just bail out here + if type_vars.len() < nvars { + return; + } + // generally preserve order, but sort by kind so that the order will be TypeVar..., // TypeVarTuple..., ParamSpec... type_vars.sort_by_key(|tv| tv.kind); @@ -205,6 +211,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } // Type variables must be unique; filter while preserving order. + let nvars = type_vars.len(); let mut type_vars = type_vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) @@ -214,6 +221,11 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & return; } + // non-unique type variables are runtime errors, so just bail out here + if type_vars.len() < nvars { + return; + } + // generally preserve order, but sort by kind so that the order will be TypeVar..., // TypeVarTuple..., ParamSpec... type_vars.sort_by_key(|tv| tv.kind); From 3891fec7121ccf73d603058d7bd7a0130153195b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:23:40 -0500 Subject: [PATCH 20/77] delete runtime documentation from type aliases --- .../pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 1eac1206acfa1d..a83b2a24900709 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -27,11 +27,6 @@ use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; /// `contravariant` keywords used by `TypeParam` variables. As such, rewriting a `TypeParam` /// variable to an in-line type parameter may change its variance. /// -/// Unlike `TypeParam` variables, [PEP 695]-style type parameters cannot be used at runtime. For -/// example, calling `isinstance(x, T)` with type parameter `T` will raise a `TypeError`. As such, -/// rewriting a `TypeParam` as a type parameter will cause issues for parameters that are used for -/// such runtime checks. -/// /// ## Example /// ```python /// T = TypeVar("T") From 57c65d8419f0a6bcd9ee361605445b048475013d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:28:33 -0500 Subject: [PATCH 21/77] preserve order --- .../rules/pep695/use_pep695_type_parameter.rs | 12 ++---------- ...ff_linter__rules__pyupgrade__tests__UP046.py.snap | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index a83b2a24900709..d765c67183652b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -118,7 +118,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl // Type variables must be unique; filter while preserving order. let nvars = vars.len(); - let mut type_vars = vars + let type_vars = vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); @@ -132,10 +132,6 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl return; } - // generally preserve order, but sort by kind so that the order will be TypeVar..., - // TypeVarTuple..., ParamSpec... - type_vars.sort_by_key(|tv| tv.kind); - // build the fix as a String to avoid removing comments from the entire function body let type_params = fmt_type_vars(&type_vars, checker); @@ -207,7 +203,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & // Type variables must be unique; filter while preserving order. let nvars = type_vars.len(); - let mut type_vars = type_vars + let type_vars = type_vars .into_iter() .unique_by(|TypeVar { name, .. }| name.id.as_str()) .collect::>(); @@ -221,10 +217,6 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & return; } - // generally preserve order, but sort by kind so that the order will be TypeVar..., - // TypeVarTuple..., ParamSpec... - type_vars.sort_by_key(|tv| tv.kind); - // build the fix as a String to avoid removing comments from the entire function body let type_params = fmt_type_vars(&type_vars, checker); diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index db22b8690dcf06..1579b0a7ea0d17 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -112,7 +112,7 @@ UP046.py:30:5: UP046 [*] Generic function `h` should use type parameters 28 28 | 29 29 | 30 |-def h( - 30 |+def h[T: float, **P]( + 30 |+def h[**P, T: float]( 31 31 | p: Callable[P, T], 32 32 | # Comment in the middle of a parameter list should be preserved 33 33 | another_param, From 9315d279b07f610e6e823218ef02a72e951092a7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:37:38 -0500 Subject: [PATCH 22/77] import TypeVar and fix mkdocs formatting --- .../rules/pep695/use_pep695_type_parameter.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index d765c67183652b..1f860e19aadcd9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -28,16 +28,20 @@ use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; /// variable to an in-line type parameter may change its variance. /// /// ## Example +/// /// ```python +/// from typing import TypeVar +/// /// T = TypeVar("T") -/// class GenericClass(Generic[T]): -/// ... +/// +/// +/// class GenericClass(Generic[T]): ... /// ``` /// /// Use instead: +/// /// ```python -/// class GenericClass[T]: -/// ... +/// class GenericClass[T]: ... /// ``` /// /// [PEP 695]: https://peps.python.org/pep-0695/ From 080ab06cf7f6582d8fcd0f954c1221bf4594dacd Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:39:59 -0500 Subject: [PATCH 23/77] rename TypeVarKind -> TypeParamKind and variants --- .../src/rules/pyupgrade/rules/pep695.rs | 28 +++++++++---------- .../rules/pep695/use_pep695_type_alias.rs | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index df13b5ac229dfd..f8e7ba644faf89 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -26,9 +26,9 @@ enum TypeVarRestriction<'a> { } #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -enum TypeVarKind { - Var, - Tuple, +enum TypeParamKind { + TypeVar, + TypeVarTuple, ParamSpec, } @@ -36,7 +36,7 @@ enum TypeVarKind { struct TypeVar<'a> { name: &'a ExprName, restriction: Option>, - kind: TypeVarKind, + kind: TypeParamKind, } /// Format a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T, *Ts, **P]`). @@ -60,9 +60,9 @@ impl TypeVar<'_> { /// the `TypeVarRestriction` values for generic bounds and constraints. fn fmt_into(&self, s: &mut String, source: &str) { match self.kind { - TypeVarKind::Var => {} - TypeVarKind::Tuple => s.push('*'), - TypeVarKind::ParamSpec => s.push_str("**"), + TypeParamKind::TypeVar => {} + TypeParamKind::TypeVarTuple => s.push('*'), + TypeParamKind::ParamSpec => s.push_str("**"), } s.push_str(&self.name.id); if let Some(restriction) = &self.restriction { @@ -96,7 +96,7 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { }: &'a TypeVar<'a>, ) -> Self { match kind { - TypeVarKind::Var => { + TypeParamKind::TypeVar => { TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), name: Identifier::new(name.id.clone(), TextRange::default()), @@ -117,12 +117,12 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { default: None, }) } - TypeVarKind::Tuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { + TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), name: Identifier::new(name.id.clone(), TextRange::default()), default: None, }), - TypeVarKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { + TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { range: TextRange::default(), name: Identifier::new(name.id.clone(), TextRange::default()), default: None, @@ -173,7 +173,7 @@ fn expr_name_to_type_var<'a>( return Some(TypeVar { name, restriction: None, - kind: TypeVarKind::Var, + kind: TypeParamKind::TypeVar, }); } } @@ -181,11 +181,11 @@ fn expr_name_to_type_var<'a>( func, arguments, .. }) => { let kind = if semantic.match_typing_expr(func, "TypeVar") { - TypeVarKind::Var + TypeParamKind::TypeVar } else if semantic.match_typing_expr(func, "TypeVarTuple") { - TypeVarKind::Tuple + TypeParamKind::TypeVarTuple } else if semantic.match_typing_expr(func, "ParamSpec") { - TypeVarKind::ParamSpec + TypeParamKind::ParamSpec } else { return None; }; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 609f632ed14e7e..1126daae26bb2f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{expr_name_to_type_var, TypeVar, TypeVarKind, TypeVarReferenceVisitor}; +use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisitor}; /// ## What it does /// Checks for use of `TypeAlias` annotations and `TypeAliasType` assignments @@ -132,7 +132,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssig expr_name_to_type_var(checker.semantic(), name).unwrap_or(TypeVar { name, restriction: None, - kind: TypeVarKind::Var, + kind: TypeParamKind::TypeVar, }) }) }) From f27da8d65fa521689c37b35bd92581c4653cb419 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 08:54:22 -0500 Subject: [PATCH 24/77] document and test that multiple base classes are not handled --- .../ruff_linter/resources/test/fixtures/pyupgrade/UP046.py | 5 +++++ .../pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 233ff8d85090f2..867169d3873d91 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -56,3 +56,8 @@ def generic_method(t: T): class MixedGenerics[U]: def more_generic(u: U, t: T): pass + + +# TODO(brent) we should also handle multiple base classes +class Multiple(NotGeneric, Generic[T]): + pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 1f860e19aadcd9..15cfd70e8b2c2f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -27,6 +27,10 @@ use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; /// `contravariant` keywords used by `TypeParam` variables. As such, rewriting a `TypeParam` /// variable to an in-line type parameter may change its variance. /// +/// The rule currently excludes cases where it conceptually should be able to give a diagnostic. In +/// particular, it skips generic classes with multiple base classes, and it skips generic methods in +/// generic or non-generic classes. +/// /// ## Example /// /// ```python From 5997743f67d2c0098220796ab2d98273ffe99384 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 09:28:35 -0500 Subject: [PATCH 25/77] limit class diagnostic to Generic parameter --- .../rules/pep695/use_pep695_type_parameter.rs | 10 ++++++++-- ...ff_linter__rules__pyupgrade__tests__UP046.py.snap | 12 ++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 15cfd70e8b2c2f..6feafa37fc1b8b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -107,7 +107,13 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl // TODO(brent) only accept a single, Generic argument for now. I think it should be fine to have // other arguments, but this simplifies the fix just to delete the argument list for now - let [Expr::Subscript(ExprSubscript { value, slice, .. })] = arguments.args.as_ref() else { + let [Expr::Subscript(ExprSubscript { + value, + slice, + range, + .. + })] = arguments.args.as_ref() + else { return; }; @@ -149,7 +155,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl name: name.to_string(), generic_kind: GenericKind::GenericClass, }, - TextRange::new(name.start(), arguments.end()), + *range, ) .with_fix(Fix::applicable_edit( Edit::replacement(type_params, name.end(), arguments.end()), diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 1579b0a7ea0d17..b41bd7791b7d4d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -2,10 +2,10 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP046.py:9:7: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | 9 | class A(Generic[T]): - | ^^^^^^^^^^^^^ UP046 + | ^^^^^^^^^^ UP046 10 | # Comments in a class body are preserved 11 | pass | @@ -21,10 +21,10 @@ UP046.py:9:7: UP046 [*] Generic class `A` uses `Generic` subclass instead of typ 11 11 | pass 12 12 | -UP046.py:14:7: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | 14 | class B(Generic[*Ts]): - | ^^^^^^^^^^^^^^^ UP046 + | ^^^^^^^^^^^^ UP046 15 | pass | = help: Use type parameters @@ -39,10 +39,10 @@ UP046.py:14:7: UP046 [*] Generic class `B` uses `Generic` subclass instead of ty 16 16 | 17 17 | -UP046.py:18:7: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | 18 | class C(Generic[P]): - | ^^^^^^^^^^^^^ UP046 + | ^^^^^^^^^^ UP046 19 | pass | = help: Use type parameters From 224311b0e072111fc2c84f63055bafbd46d10b5a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 10:39:20 -0500 Subject: [PATCH 26/77] comment on python version checks --- .../rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 6feafa37fc1b8b..2bebf26e466d0a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -84,6 +84,7 @@ impl Violation for NonPEP695TypeParameter { /// UP046 pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { + // PEP-695 syntax is only available on Python 3.12+ if checker.settings.target_version < PythonVersion::Py312 { return; } @@ -166,6 +167,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl /// UP046 pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { + // PEP-695 syntax is only available on Python 3.12+ if checker.settings.target_version < PythonVersion::Py312 { return; } From 1e4c20d0d4baac20eff637e396dbebeaf341194c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 10:39:36 -0500 Subject: [PATCH 27/77] avoid AST names in documentation --- .../rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 2bebf26e466d0a..9d7303db4bdd0a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -13,8 +13,8 @@ use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; /// ## What it does /// -/// Checks for use of `TypeParam`, `TypeParamTuple`, and `ParamSpec` annotations on generic -/// functions and classes. +/// Checks for use of standalone type variables and parameter specifications in generic functions +/// and classes. /// /// ## Why is this bad? /// From 188f26d0cf219250e81772c3c1caffcc14c22038 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 10:45:32 -0500 Subject: [PATCH 28/77] TypeParam -> TypeVar in docs --- .../rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs | 8 ++++---- .../pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 1126daae26bb2f..7b11d5561898e5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -26,13 +26,13 @@ use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisit /// /// ## Known problems /// [PEP 695] uses inferred variance for type parameters, instead of the -/// `covariant` and `contravariant` keywords used by `TypeParam` variables. As -/// such, rewriting a `TypeParam` variable to a `type` alias may change its +/// `covariant` and `contravariant` keywords used by `TypeVar` variables. As +/// such, rewriting a `TypeVar` variable to a `type` alias may change its /// variance. /// -/// Unlike `TypeParam` variables, [PEP 695]-style `type` aliases cannot be used +/// Unlike `TypeVar` variables, [PEP 695]-style `type` aliases cannot be used /// at runtime. For example, calling `isinstance` on a `type` alias will throw -/// a `TypeError`. As such, rewriting a `TypeParam` via the `type` keyword will +/// a `TypeError`. As such, rewriting a `TypeVar` via the `type` keyword will /// cause issues for parameters that are used for such runtime checks. /// /// ## Example diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 9d7303db4bdd0a..48727ad4e77388 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -24,8 +24,8 @@ use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; /// ## Known problems /// /// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and -/// `contravariant` keywords used by `TypeParam` variables. As such, rewriting a `TypeParam` -/// variable to an in-line type parameter may change its variance. +/// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to +/// an in-line type parameter may change its variance. /// /// The rule currently excludes cases where it conceptually should be able to give a diagnostic. In /// particular, it skips generic classes with multiple base classes, and it skips generic methods in From 55be0d8563c2dbcf4126238f6414efc998dc76e1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 11:41:15 -0500 Subject: [PATCH 29/77] impl Display for DisplayTypeVar and DisplayTypeVars wrappers --- .../src/rules/pyupgrade/rules/pep695.rs | 77 ++++++++++++------- .../rules/pep695/use_pep695_type_parameter.rs | 16 ++-- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index f8e7ba644faf89..856b06007845b3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -1,5 +1,7 @@ //! Shared code for [`use_pep695_type_alias`] (UP040) and [`use_pep695_type_parameter`] (UP046) +use std::fmt::Display; + use ruff_python_ast::{ self as ast, visitor::{self, Visitor}, @@ -9,8 +11,6 @@ use ruff_python_ast::{ use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; - pub(crate) use use_pep695_type_alias::*; pub(crate) use use_pep695_type_parameter::*; @@ -39,51 +39,74 @@ struct TypeVar<'a> { kind: TypeParamKind, } -/// Format a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T, *Ts, **P]`). -/// See [`TypeVar::fmt_into`] for further details. -fn fmt_type_vars(type_vars: &[TypeVar], checker: &Checker) -> String { - let nvars = type_vars.len(); - let mut type_params = String::from("["); - for (i, tv) in type_vars.iter().enumerate() { - tv.fmt_into(&mut type_params, checker.source()); - if i < nvars - 1 { - type_params.push_str(", "); +/// Wrapper for formatting a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T, +/// *Ts, **P]`). See [`DisplayTypeVar`] for further details. +struct DisplayTypeVars<'a> { + type_vars: &'a [TypeVar<'a>], + source: &'a str, +} + +impl Display for DisplayTypeVars<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("[")?; + let nvars = self.type_vars.len(); + for (i, tv) in self.type_vars.iter().enumerate() { + write!(f, "{}", tv.display(self.source))?; + if i < nvars - 1 { + f.write_str(", ")?; + } } + f.write_str("]")?; + + Ok(()) } - type_params.push(']'); +} - type_params +/// Used for displaying `type_var`. `source` is the whole file, which will be sliced to recover the +/// `TypeVarRestriction` values for generic bounds and constraints. +struct DisplayTypeVar<'a> { + type_var: &'a TypeVar<'a>, + source: &'a str, } impl TypeVar<'_> { - /// Format `self` into `s`, where `source` is the whole file, which will be sliced to recover - /// the `TypeVarRestriction` values for generic bounds and constraints. - fn fmt_into(&self, s: &mut String, source: &str) { - match self.kind { + fn display<'a>(&'a self, source: &'a str) -> DisplayTypeVar<'a> { + DisplayTypeVar { + type_var: self, + source, + } + } +} + +impl Display for DisplayTypeVar<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.type_var.kind { TypeParamKind::TypeVar => {} - TypeParamKind::TypeVarTuple => s.push('*'), - TypeParamKind::ParamSpec => s.push_str("**"), + TypeParamKind::TypeVarTuple => f.write_str("*")?, + TypeParamKind::ParamSpec => f.write_str("**")?, } - s.push_str(&self.name.id); - if let Some(restriction) = &self.restriction { - s.push_str(": "); + f.write_str(&self.type_var.name.id)?; + if let Some(restriction) = &self.type_var.restriction { + f.write_str(": ")?; match restriction { TypeVarRestriction::Bound(bound) => { - s.push_str(&source[bound.range()]); + f.write_str(&self.source[bound.range()])?; } TypeVarRestriction::Constraint(vec) => { let len = vec.len(); - s.push('('); + f.write_str("(")?; for (i, v) in vec.iter().enumerate() { - s.push_str(&source[v.range()]); + f.write_str(&self.source[v.range()])?; if i < len - 1 { - s.push_str(", "); + f.write_str(", ")?; } } - s.push(')'); + f.write_str(")")?; } } } + + Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 48727ad4e77388..8a8fb0d4bc0605 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -9,7 +9,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{fmt_type_vars, TypeVar, TypeVarReferenceVisitor}; +use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// ## What it does /// @@ -148,7 +148,10 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl } // build the fix as a String to avoid removing comments from the entire function body - let type_params = fmt_type_vars(&type_vars, checker); + let type_params = DisplayTypeVars { + type_vars: &type_vars, + source: checker.source(), + }; checker.diagnostics.push( Diagnostic::new( @@ -159,7 +162,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl *range, ) .with_fix(Fix::applicable_edit( - Edit::replacement(type_params, name.end(), arguments.end()), + Edit::replacement(type_params.to_string(), name.end(), arguments.end()), Applicability::Safe, )), ); @@ -234,7 +237,10 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } // build the fix as a String to avoid removing comments from the entire function body - let type_params = fmt_type_vars(&type_vars, checker); + let type_params = DisplayTypeVars { + type_vars: &type_vars, + source: checker.source(), + }; checker.diagnostics.push( Diagnostic::new( @@ -245,7 +251,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & TextRange::new(name.start(), parameters.end()), ) .with_fix(Fix::applicable_edit( - Edit::insertion(type_params, name.end()), + Edit::insertion(type_params.to_string(), name.end()), Applicability::Safe, )), ); From 01c7c125b0bcdb60500d7e56ac6c9abffee0f6c4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:56:58 -0500 Subject: [PATCH 30/77] Remove Ord and PartialOrd Co-authored-by: Alex Waygood --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index 856b06007845b3..fa0d41e571cdf0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -25,7 +25,7 @@ enum TypeVarRestriction<'a> { Constraint(Vec<&'a Expr>), } -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] enum TypeParamKind { TypeVar, TypeVarTuple, From f62a68da2556c1eaf5d9ed86a82166ec14d1b0a8 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:58:11 -0500 Subject: [PATCH 31/77] Fix type alias wording Co-authored-by: Alex Waygood --- .../src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 7b11d5561898e5..8da78feb330bcf 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -27,8 +27,8 @@ use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisit /// ## Known problems /// [PEP 695] uses inferred variance for type parameters, instead of the /// `covariant` and `contravariant` keywords used by `TypeVar` variables. As -/// such, rewriting a `TypeVar` variable to a `type` alias may change its -/// variance. +/// such, rewriting a type alias using a PEP-695 `type` statement may change +/// the variance of the alias's type parameters. /// /// Unlike `TypeVar` variables, [PEP 695]-style `type` aliases cannot be used /// at runtime. For example, calling `isinstance` on a `type` alias will throw From cfe9c4a4a8dcba04b091df199fce306b1266e8fc Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:02:59 -0500 Subject: [PATCH 32/77] Apply docs suggestions Co-authored-by: Alex Waygood --- .../rules/pep695/use_pep695_type_parameter.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 8a8fb0d4bc0605..48bc678f9f108a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -25,11 +25,10 @@ use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// /// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and /// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to -/// an in-line type parameter may change its variance. +/// an inline type parameter may change its variance. /// -/// The rule currently excludes cases where it conceptually should be able to give a diagnostic. In -/// particular, it skips generic classes with multiple base classes, and it skips generic methods in -/// generic or non-generic classes. +/// The rule currently skips generic classes with multiple base classes, and skips +/// generic methods in generic or non-generic classes. /// /// ## Example /// @@ -39,13 +38,15 @@ use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// T = TypeVar("T") /// /// -/// class GenericClass(Generic[T]): ... +/// class GenericClass(Generic[T]): +/// var: T /// ``` /// /// Use instead: /// /// ```python -/// class GenericClass[T]: ... +/// class GenericClass[T]: +/// var: T /// ``` /// /// [PEP 695]: https://peps.python.org/pep-0695/ From f12036e0d11cdeb00d119ccf10566b63c79bc2e5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 13:14:56 -0500 Subject: [PATCH 33/77] factor out `check_type_vars` and check vars.is_empty first --- .../rules/pep695/use_pep695_type_parameter.rs | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 48bc678f9f108a..3f53db9ae9e175 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -132,21 +132,9 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl visitor.vars }; - // Type variables must be unique; filter while preserving order. - let nvars = vars.len(); - let type_vars = vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - if type_vars.is_empty() { + let Some(type_vars) = check_type_vars(vars) else { return; - } - - // non-unique type variables are runtime errors, so just bail out here - if type_vars.len() < nvars { - return; - } + }; // build the fix as a String to avoid removing comments from the entire function body let type_params = DisplayTypeVars { @@ -169,6 +157,27 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl ); } +/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. +fn check_type_vars(vars: Vec>) -> Option>> { + if vars.is_empty() { + return None; + } + + // Type variables must be unique; filter while preserving order. + let nvars = vars.len(); + let type_vars = vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + // non-unique type variables are runtime errors, so just bail out here + if type_vars.len() < nvars { + return None; + } + + Some(type_vars) +} + /// UP046 pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { // PEP-695 syntax is only available on Python 3.12+ @@ -221,21 +230,9 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & } } - // Type variables must be unique; filter while preserving order. - let nvars = type_vars.len(); - let type_vars = type_vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - if type_vars.is_empty() { - return; - } - - // non-unique type variables are runtime errors, so just bail out here - if type_vars.len() < nvars { + let Some(type_vars) = check_type_vars(type_vars) else { return; - } + }; // build the fix as a String to avoid removing comments from the entire function body let type_params = DisplayTypeVars { From 30a0f04501634c3bd41f08dfa1404a84a8194460 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 13:20:13 -0500 Subject: [PATCH 34/77] add `See also` section pointing to `unused-private-type-var` --- .../pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 3f53db9ae9e175..3db33666bbe058 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -49,6 +49,13 @@ use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// var: T /// ``` /// +/// ## See also +/// +/// This rule replaces standalone type variables in class and function signatures but doesn't remove +/// the corresponding type variables even if they are unused after the fix. See +/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused type +/// variables. +/// /// [PEP 695]: https://peps.python.org/pep-0695/ #[derive(ViolationMetadata)] pub(crate) struct NonPEP695TypeParameter { From 933002ed4ed02a1b7d66870ebee0abc2060ce9ed Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 13:32:27 -0500 Subject: [PATCH 35/77] update type alias docs around isinstance Co-authored-by: Alex Waygood --- .../pyupgrade/rules/pep695/use_pep695_type_alias.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 8da78feb330bcf..79aa06b8de313f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -30,10 +30,13 @@ use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisit /// such, rewriting a type alias using a PEP-695 `type` statement may change /// the variance of the alias's type parameters. /// -/// Unlike `TypeVar` variables, [PEP 695]-style `type` aliases cannot be used -/// at runtime. For example, calling `isinstance` on a `type` alias will throw -/// a `TypeError`. As such, rewriting a `TypeVar` via the `type` keyword will -/// cause issues for parameters that are used for such runtime checks. +/// Unlike type aliases that use simple assignments, definitions created using +/// [PEP 695] `type` statements cannot be used as drop-in replacements at +/// runtime for the value on the right-hand side of the statement. This means +/// that while for some simple old-style type aliases you can use them as the +/// second argument to an `isinstance()` call (for example), doing the same +/// with a [PEP 695] `type` statement will always raise `TypeError` at +/// runtime. /// /// ## Example /// ```python From 274cf715a94aa80055e8588cef52a616248ff69c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 13:41:24 -0500 Subject: [PATCH 36/77] add constrained class test --- .../test/fixtures/pyupgrade/UP046.py | 5 + ...er__rules__pyupgrade__tests__UP046.py.snap | 148 ++++++++++-------- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 867169d3873d91..1e6fe747f12251 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -1,6 +1,7 @@ from collections.abc import Callable from typing import Generic, ParamSpec, TypeVar, TypeVarTuple +S = TypeVar("S", str, bytes) # constrained type variable T = TypeVar("T", bound=float) Ts = TypeVarTuple("Ts") P = ParamSpec("P") @@ -19,6 +20,10 @@ class C(Generic[P]): pass +class Constrained(Generic[S]): + pass + + def f(t: T): pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index b41bd7791b7d4d..a16c6cff89dcd3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -2,117 +2,135 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046.py:10:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | - 9 | class A(Generic[T]): +10 | class A(Generic[T]): | ^^^^^^^^^^ UP046 -10 | # Comments in a class body are preserved -11 | pass +11 | # Comments in a class body are preserved +12 | pass | = help: Use type parameters ℹ Safe fix -6 6 | P = ParamSpec("P") -7 7 | +7 7 | P = ParamSpec("P") 8 8 | -9 |-class A(Generic[T]): - 9 |+class A[T: float]: -10 10 | # Comments in a class body are preserved -11 11 | pass -12 12 | +9 9 | +10 |-class A(Generic[T]): + 10 |+class A[T: float]: +11 11 | # Comments in a class body are preserved +12 12 | pass +13 13 | -UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046.py:15:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -14 | class B(Generic[*Ts]): +15 | class B(Generic[*Ts]): | ^^^^^^^^^^^^ UP046 -15 | pass +16 | pass | = help: Use type parameters ℹ Safe fix -11 11 | pass -12 12 | +12 12 | pass 13 13 | -14 |-class B(Generic[*Ts]): - 14 |+class B[*Ts]: -15 15 | pass -16 16 | +14 14 | +15 |-class B(Generic[*Ts]): + 15 |+class B[*Ts]: +16 16 | pass 17 17 | +18 18 | -UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046.py:19:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | -18 | class C(Generic[P]): +19 | class C(Generic[P]): | ^^^^^^^^^^ UP046 -19 | pass +20 | pass | = help: Use type parameters ℹ Safe fix -15 15 | pass -16 16 | +16 16 | pass 17 17 | -18 |-class C(Generic[P]): - 18 |+class C[**P]: -19 19 | pass -20 20 | +18 18 | +19 |-class C(Generic[P]): + 19 |+class C[**P]: +20 20 | pass 21 21 | +22 22 | -UP046.py:22:5: UP046 [*] Generic function `f` should use type parameters +UP046.py:23:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters | -22 | def f(t: T): - | ^^^^^^^ UP046 -23 | pass +23 | class Constrained(Generic[S]): + | ^^^^^^^^^^ UP046 +24 | pass | = help: Use type parameters ℹ Safe fix -19 19 | pass -20 20 | +20 20 | pass 21 21 | -22 |-def f(t: T): - 22 |+def f[T: float](t: T): -23 23 | pass -24 24 | +22 22 | +23 |-class Constrained(Generic[S]): + 23 |+class Constrained[S: (str, bytes)]: +24 24 | pass 25 25 | +26 26 | -UP046.py:26:5: UP046 [*] Generic function `g` should use type parameters +UP046.py:27:5: UP046 [*] Generic function `f` should use type parameters | -26 | def g(ts: tuple[*Ts]): - | ^^^^^^^^^^^^^^^^^ UP046 -27 | pass +27 | def f(t: T): + | ^^^^^^^ UP046 +28 | pass | = help: Use type parameters ℹ Safe fix -23 23 | pass -24 24 | +24 24 | pass 25 25 | -26 |-def g(ts: tuple[*Ts]): - 26 |+def g[*Ts](ts: tuple[*Ts]): -27 27 | pass -28 28 | +26 26 | +27 |-def f(t: T): + 27 |+def f[T: float](t: T): +28 28 | pass 29 29 | +30 30 | -UP046.py:30:5: UP046 [*] Generic function `h` should use type parameters +UP046.py:31:5: UP046 [*] Generic function `g` should use type parameters | -30 | def h( +31 | def g(ts: tuple[*Ts]): + | ^^^^^^^^^^^^^^^^^ UP046 +32 | pass + | + = help: Use type parameters + +ℹ Safe fix +28 28 | pass +29 29 | +30 30 | +31 |-def g(ts: tuple[*Ts]): + 31 |+def g[*Ts](ts: tuple[*Ts]): +32 32 | pass +33 33 | +34 34 | + +UP046.py:35:5: UP046 [*] Generic function `h` should use type parameters + | +35 | def h( | _____^ -31 | | p: Callable[P, T], -32 | | # Comment in the middle of a parameter list should be preserved -33 | | another_param, -34 | | and_another, -35 | | ): +36 | | p: Callable[P, T], +37 | | # Comment in the middle of a parameter list should be preserved +38 | | another_param, +39 | | and_another, +40 | | ): | |_^ UP046 -36 | pass +41 | pass | = help: Use type parameters ℹ Safe fix -27 27 | pass -28 28 | -29 29 | -30 |-def h( - 30 |+def h[**P, T: float]( -31 31 | p: Callable[P, T], -32 32 | # Comment in the middle of a parameter list should be preserved -33 33 | another_param, +32 32 | pass +33 33 | +34 34 | +35 |-def h( + 35 |+def h[**P, T: float]( +36 36 | p: Callable[P, T], +37 37 | # Comment in the middle of a parameter list should be preserved +38 38 | another_param, From 28d9b4a247d5f2e607ea2f76474ffb1af82a4d70 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 13:42:47 -0500 Subject: [PATCH 37/77] add constrained function test --- .../resources/test/fixtures/pyupgrade/UP046.py | 4 ++++ ...ter__rules__pyupgrade__tests__UP046.py.snap | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 1e6fe747f12251..fba4fb31880e87 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -41,6 +41,10 @@ def h( pass +def i(s: S): + pass + + # These cases are not handled class D(Generic[T, T]): # duplicate generic variable, runtime error pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index a16c6cff89dcd3..432206c7a63c15 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -134,3 +134,21 @@ UP046.py:35:5: UP046 [*] Generic function `h` should use type parameters 36 36 | p: Callable[P, T], 37 37 | # Comment in the middle of a parameter list should be preserved 38 38 | another_param, + +UP046.py:44:5: UP046 [*] Generic function `i` should use type parameters + | +44 | def i(s: S): + | ^^^^^^^ UP046 +45 | pass + | + = help: Use type parameters + +ℹ Safe fix +41 41 | pass +42 42 | +43 43 | +44 |-def i(s: S): + 44 |+def i[S: (str, bytes)](s: S): +45 45 | pass +46 46 | +47 47 | From bfb0b294a4b65b99c387749d266d9ee386f99585 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:04:27 -0500 Subject: [PATCH 38/77] bail out on `default` for now --- .../resources/test/fixtures/pyupgrade/UP046.py | 14 +++++++++++++- .../src/rules/pyupgrade/rules/pep695.rs | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index fba4fb31880e87..7cfefb381ca0f0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Generic, ParamSpec, TypeVar, TypeVarTuple +from typing import Any, Generic, ParamSpec, TypeVar, TypeVarTuple S = TypeVar("S", str, bytes) # constrained type variable T = TypeVar("T", bound=float) @@ -70,3 +70,15 @@ def more_generic(u: U, t: T): # TODO(brent) we should also handle multiple base classes class Multiple(NotGeneric, Generic[T]): pass + + +# TODO(brent) default requires 3.13 +V = TypeVar("V", default=Any, bound=str) + + +class DefaultTypeVar(Generic[V]): # -> [V: str = Any] + pass + + +def default_var(v: V): + pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index fa0d41e571cdf0..815f5e9640253d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -218,6 +218,22 @@ fn expr_name_to_type_var<'a>( .first() .is_some_and(Expr::is_string_literal_expr) { + // TODO(brent) `default` was added in PEP 696 and Python 3.13 but can't be used in + // generic type parameters before that + // + // ```python + // T = TypeVar("T", default=Any, bound=str) + // class slice(Generic[T]): ... + // ``` + // + // becomes + // + // ```python + // class slice[T: str = Any]: ... + // ``` + if arguments.find_keyword("default").is_some() { + return None; + } let restriction = if let Some(bound) = arguments.find_keyword("bound") { Some(TypeVarRestriction::Bound(&bound.value)) } else if arguments.args.len() > 1 { From e7749edabc29eb5a0b873d10ad8fcc62dda1eb24 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:19:33 -0500 Subject: [PATCH 39/77] skip nested classes and functions --- .../test/fixtures/pyupgrade/UP046.py | 11 ++++++++++ .../rules/pep695/use_pep695_type_parameter.rs | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 7cfefb381ca0f0..755179c342bbaa 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -82,3 +82,14 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any] def default_var(v: V): pass + + +# nested classes and functions are skipped +class Outer: + class Inner(Generic[T]): + pass + + +def outer(): + def inner(t: T): + pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 3db33666bbe058..bcc026aaddc74a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -3,7 +3,7 @@ use itertools::Itertools; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; -use ruff_python_ast::{StmtClassDef, StmtFunctionDef}; +use ruff_python_ast::{Stmt, StmtClassDef, StmtFunctionDef}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -90,6 +90,15 @@ impl Violation for NonPEP695TypeParameter { } } +/// Check if the current statement is nested within another [`StmtClassDef`] or [`StmtFunctionDef`]. +fn in_nested_context(checker: &Checker) -> bool { + checker + .semantic() + .current_statements() + .skip(1) // skip the immediate parent, we only call this within a class or function + .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) +} + /// UP046 pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { // PEP-695 syntax is only available on Python 3.12+ @@ -97,6 +106,11 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl return; } + // don't try to handle generic classes inside other functions or classes + if in_nested_context(checker) { + return; + } + let StmtClassDef { name, type_params, @@ -192,6 +206,11 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & return; } + // don't try to handle generic functions inside other functions or classes + if in_nested_context(checker) { + return; + } + let StmtFunctionDef { name, type_params, From 108e5becb154ef95f7c90fb6789530433034ed87 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:22:35 -0500 Subject: [PATCH 40/77] document nesting and `default` limitations --- .../pyupgrade/rules/pep695/use_pep695_type_parameter.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index bcc026aaddc74a..3fd77d69f0a0ac 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -27,8 +27,10 @@ use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to /// an inline type parameter may change its variance. /// -/// The rule currently skips generic classes with multiple base classes, and skips -/// generic methods in generic or non-generic classes. +/// The rule currently skips generic classes with multiple base classes, and skips generic methods +/// in generic or non-generic classes. It also skips generic functions and classes nested inside of +/// other functions or classes. Finally, this rule skips type parameters with the `default` argument +/// introduced in [PEP 696] and implemented in Python 3.13. /// /// ## Example /// @@ -57,6 +59,7 @@ use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; /// variables. /// /// [PEP 695]: https://peps.python.org/pep-0695/ +/// [PEP 696]: https://peps.python.org/pep-0696/ #[derive(ViolationMetadata)] pub(crate) struct NonPEP695TypeParameter { name: String, From 550fc440de86de9141bb0ffbc86a74e75e8b07f5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:29:45 -0500 Subject: [PATCH 41/77] move shared function and class code to parent module --- .../src/rules/pyupgrade/rules/pep695.rs | 33 +++++++++++++++++ .../rules/pep695/use_pep695_type_parameter.rs | 36 ++----------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs index 815f5e9640253d..96ff8509a7c696 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs @@ -2,6 +2,7 @@ use std::fmt::Display; +use itertools::Itertools; use ruff_python_ast::{ self as ast, visitor::{self, Visitor}, @@ -14,6 +15,8 @@ use ruff_text_size::{Ranged, TextRange}; pub(crate) use use_pep695_type_alias::*; pub(crate) use use_pep695_type_parameter::*; +use crate::checkers::ast::Checker; + mod use_pep695_type_alias; mod use_pep695_type_parameter; @@ -255,3 +258,33 @@ fn expr_name_to_type_var<'a>( } None } + +/// Check if the current statement is nested within another [`StmtClassDef`] or [`StmtFunctionDef`]. +fn in_nested_context(checker: &Checker) -> bool { + checker + .semantic() + .current_statements() + .skip(1) // skip the immediate parent, we only call this within a class or function + .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) +} + +/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. +fn check_type_vars(vars: Vec>) -> Option>> { + if vars.is_empty() { + return None; + } + + // Type variables must be unique; filter while preserving order. + let nvars = vars.len(); + let type_vars = vars + .into_iter() + .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .collect::>(); + + // non-unique type variables are runtime errors, so just bail out here + if type_vars.len() < nvars { + return None; + } + + Some(type_vars) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs index 3fd77d69f0a0ac..48b0c06a39d380 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs @@ -1,15 +1,13 @@ -use itertools::Itertools; - use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; -use ruff_python_ast::{Stmt, StmtClassDef, StmtFunctionDef}; +use ruff_python_ast::{StmtClassDef, StmtFunctionDef}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{DisplayTypeVars, TypeVar, TypeVarReferenceVisitor}; +use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor}; /// ## What it does /// @@ -93,15 +91,6 @@ impl Violation for NonPEP695TypeParameter { } } -/// Check if the current statement is nested within another [`StmtClassDef`] or [`StmtFunctionDef`]. -fn in_nested_context(checker: &Checker) -> bool { - checker - .semantic() - .current_statements() - .skip(1) // skip the immediate parent, we only call this within a class or function - .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) -} - /// UP046 pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtClassDef) { // PEP-695 syntax is only available on Python 3.12+ @@ -181,27 +170,6 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl ); } -/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. -fn check_type_vars(vars: Vec>) -> Option>> { - if vars.is_empty() { - return None; - } - - // Type variables must be unique; filter while preserving order. - let nvars = vars.len(); - let type_vars = vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - // non-unique type variables are runtime errors, so just bail out here - if type_vars.len() < nvars { - return None; - } - - Some(type_vars) -} - /// UP046 pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { // PEP-695 syntax is only available on Python 3.12+ From 8f96a2692241dd0e7b9b3c1012b515bc70ecb658 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:36:29 -0500 Subject: [PATCH 42/77] rename to mod.rs --- .../src/rules/pyupgrade/rules/{pep695.rs => pep695/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/ruff_linter/src/rules/pyupgrade/rules/{pep695.rs => pep695/mod.rs} (100%) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs similarity index 100% rename from crates/ruff_linter/src/rules/pyupgrade/rules/pep695.rs rename to crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs From d2c84eb2a2e63cb36540454bec7f9233af9275a9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 15:51:55 -0500 Subject: [PATCH 43/77] separate tests --- .../test/fixtures/pyupgrade/UP046.py | 31 ---- .../test/fixtures/pyupgrade/UP047.py | 43 +++++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 + ...er__rules__pyupgrade__tests__UP046.py.snap | 156 +++++------------- ...er__rules__pyupgrade__tests__UP047.py.snap | 81 +++++++++ 5 files changed, 164 insertions(+), 148 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 755179c342bbaa..09bfa38f46f91e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -1,4 +1,3 @@ -from collections.abc import Callable from typing import Any, Generic, ParamSpec, TypeVar, TypeVarTuple S = TypeVar("S", str, bytes) # constrained type variable @@ -24,27 +23,6 @@ class Constrained(Generic[S]): pass -def f(t: T): - pass - - -def g(ts: tuple[*Ts]): - pass - - -def h( - p: Callable[P, T], - # Comment in the middle of a parameter list should be preserved - another_param, - and_another, -): - pass - - -def i(s: S): - pass - - # These cases are not handled class D(Generic[T, T]): # duplicate generic variable, runtime error pass @@ -80,16 +58,7 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any] pass -def default_var(v: V): - pass - - # nested classes and functions are skipped class Outer: class Inner(Generic[T]): pass - - -def outer(): - def inner(t: T): - pass diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py new file mode 100644 index 00000000000000..cc0e99255d392e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py @@ -0,0 +1,43 @@ +from collections.abc import Callable +from typing import Any, ParamSpec, TypeVar, TypeVarTuple + +S = TypeVar("S", str, bytes) # constrained type variable +T = TypeVar("T", bound=float) +Ts = TypeVarTuple("Ts") +P = ParamSpec("P") + + +def f(t: T): + pass + + +def g(ts: tuple[*Ts]): + pass + + +def h( + p: Callable[P, T], + # Comment in the middle of a parameter list should be preserved + another_param, + and_another, +): + pass + + +def i(s: S): + pass + + +# these cases are not handled + +# TODO(brent) default requires 3.13 +V = TypeVar("V", default=Any, bound=str) + + +def default_var(v: V): + pass + + +def outer(): + def inner(t: T): + pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 0f3c1a3ceffdea..96db243e537c42 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -104,6 +104,7 @@ mod tests { #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP046.py"))] + #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP047.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 432206c7a63c15..e1e5e17613d0ee 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -2,153 +2,75 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP046.py:10:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | -10 | class A(Generic[T]): + 9 | class A(Generic[T]): | ^^^^^^^^^^ UP046 -11 | # Comments in a class body are preserved -12 | pass +10 | # Comments in a class body are preserved +11 | pass | = help: Use type parameters ℹ Safe fix -7 7 | P = ParamSpec("P") +6 6 | P = ParamSpec("P") +7 7 | 8 8 | -9 9 | -10 |-class A(Generic[T]): - 10 |+class A[T: float]: -11 11 | # Comments in a class body are preserved -12 12 | pass -13 13 | +9 |-class A(Generic[T]): + 9 |+class A[T: float]: +10 10 | # Comments in a class body are preserved +11 11 | pass +12 12 | -UP046.py:15:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -15 | class B(Generic[*Ts]): +14 | class B(Generic[*Ts]): | ^^^^^^^^^^^^ UP046 -16 | pass +15 | pass | = help: Use type parameters ℹ Safe fix -12 12 | pass +11 11 | pass +12 12 | 13 13 | -14 14 | -15 |-class B(Generic[*Ts]): - 15 |+class B[*Ts]: -16 16 | pass +14 |-class B(Generic[*Ts]): + 14 |+class B[*Ts]: +15 15 | pass +16 16 | 17 17 | -18 18 | -UP046.py:19:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | -19 | class C(Generic[P]): +18 | class C(Generic[P]): | ^^^^^^^^^^ UP046 -20 | pass +19 | pass | = help: Use type parameters ℹ Safe fix -16 16 | pass +15 15 | pass +16 16 | 17 17 | -18 18 | -19 |-class C(Generic[P]): - 19 |+class C[**P]: -20 20 | pass +18 |-class C(Generic[P]): + 18 |+class C[**P]: +19 19 | pass +20 20 | 21 21 | -22 22 | -UP046.py:23:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters +UP046.py:22:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters | -23 | class Constrained(Generic[S]): +22 | class Constrained(Generic[S]): | ^^^^^^^^^^ UP046 -24 | pass +23 | pass | = help: Use type parameters ℹ Safe fix -20 20 | pass +19 19 | pass +20 20 | 21 21 | -22 22 | -23 |-class Constrained(Generic[S]): - 23 |+class Constrained[S: (str, bytes)]: -24 24 | pass -25 25 | -26 26 | - -UP046.py:27:5: UP046 [*] Generic function `f` should use type parameters - | -27 | def f(t: T): - | ^^^^^^^ UP046 -28 | pass - | - = help: Use type parameters - -ℹ Safe fix -24 24 | pass -25 25 | -26 26 | -27 |-def f(t: T): - 27 |+def f[T: float](t: T): -28 28 | pass -29 29 | -30 30 | - -UP046.py:31:5: UP046 [*] Generic function `g` should use type parameters - | -31 | def g(ts: tuple[*Ts]): - | ^^^^^^^^^^^^^^^^^ UP046 -32 | pass - | - = help: Use type parameters - -ℹ Safe fix -28 28 | pass -29 29 | -30 30 | -31 |-def g(ts: tuple[*Ts]): - 31 |+def g[*Ts](ts: tuple[*Ts]): -32 32 | pass -33 33 | -34 34 | - -UP046.py:35:5: UP046 [*] Generic function `h` should use type parameters - | -35 | def h( - | _____^ -36 | | p: Callable[P, T], -37 | | # Comment in the middle of a parameter list should be preserved -38 | | another_param, -39 | | and_another, -40 | | ): - | |_^ UP046 -41 | pass - | - = help: Use type parameters - -ℹ Safe fix -32 32 | pass -33 33 | -34 34 | -35 |-def h( - 35 |+def h[**P, T: float]( -36 36 | p: Callable[P, T], -37 37 | # Comment in the middle of a parameter list should be preserved -38 38 | another_param, - -UP046.py:44:5: UP046 [*] Generic function `i` should use type parameters - | -44 | def i(s: S): - | ^^^^^^^ UP046 -45 | pass - | - = help: Use type parameters - -ℹ Safe fix -41 41 | pass -42 42 | -43 43 | -44 |-def i(s: S): - 44 |+def i[S: (str, bytes)](s: S): -45 45 | pass -46 46 | -47 47 | +22 |-class Constrained(Generic[S]): + 22 |+class Constrained[S: (str, bytes)]: +23 23 | pass +24 24 | +25 25 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap new file mode 100644 index 00000000000000..2cdb17c2b60bb8 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap @@ -0,0 +1,81 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +snapshot_kind: text +--- +UP047.py:10:5: UP046 [*] Generic function `f` should use type parameters + | +10 | def f(t: T): + | ^^^^^^^ UP046 +11 | pass + | + = help: Use type parameters + +ℹ Safe fix +7 7 | P = ParamSpec("P") +8 8 | +9 9 | +10 |-def f(t: T): + 10 |+def f[T: float](t: T): +11 11 | pass +12 12 | +13 13 | + +UP047.py:14:5: UP046 [*] Generic function `g` should use type parameters + | +14 | def g(ts: tuple[*Ts]): + | ^^^^^^^^^^^^^^^^^ UP046 +15 | pass + | + = help: Use type parameters + +ℹ Safe fix +11 11 | pass +12 12 | +13 13 | +14 |-def g(ts: tuple[*Ts]): + 14 |+def g[*Ts](ts: tuple[*Ts]): +15 15 | pass +16 16 | +17 17 | + +UP047.py:18:5: UP046 [*] Generic function `h` should use type parameters + | +18 | def h( + | _____^ +19 | | p: Callable[P, T], +20 | | # Comment in the middle of a parameter list should be preserved +21 | | another_param, +22 | | and_another, +23 | | ): + | |_^ UP046 +24 | pass + | + = help: Use type parameters + +ℹ Safe fix +15 15 | pass +16 16 | +17 17 | +18 |-def h( + 18 |+def h[**P, T: float]( +19 19 | p: Callable[P, T], +20 20 | # Comment in the middle of a parameter list should be preserved +21 21 | another_param, + +UP047.py:27:5: UP046 [*] Generic function `i` should use type parameters + | +27 | def i(s: S): + | ^^^^^^^ UP046 +28 | pass + | + = help: Use type parameters + +ℹ Safe fix +24 24 | pass +25 25 | +26 26 | +27 |-def i(s: S): + 27 |+def i[S: (str, bytes)](s: S): +28 28 | pass +29 29 | +30 30 | From 7326ee34c93232b53dc097424caa01e50663d512 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 16:03:30 -0500 Subject: [PATCH 44/77] update rule codes --- ...inter__rules__pyupgrade__tests__UP047.py.snap | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap index 2cdb17c2b60bb8..d1e6024f7d8416 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap @@ -2,10 +2,10 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP047.py:10:5: UP046 [*] Generic function `f` should use type parameters +UP047.py:10:5: UP047 [*] Generic function `f` should use type parameters | 10 | def f(t: T): - | ^^^^^^^ UP046 + | ^^^^^^^ UP047 11 | pass | = help: Use type parameters @@ -20,10 +20,10 @@ UP047.py:10:5: UP046 [*] Generic function `f` should use type parameters 12 12 | 13 13 | -UP047.py:14:5: UP046 [*] Generic function `g` should use type parameters +UP047.py:14:5: UP047 [*] Generic function `g` should use type parameters | 14 | def g(ts: tuple[*Ts]): - | ^^^^^^^^^^^^^^^^^ UP046 + | ^^^^^^^^^^^^^^^^^ UP047 15 | pass | = help: Use type parameters @@ -38,7 +38,7 @@ UP047.py:14:5: UP046 [*] Generic function `g` should use type parameters 16 16 | 17 17 | -UP047.py:18:5: UP046 [*] Generic function `h` should use type parameters +UP047.py:18:5: UP047 [*] Generic function `h` should use type parameters | 18 | def h( | _____^ @@ -47,7 +47,7 @@ UP047.py:18:5: UP046 [*] Generic function `h` should use type parameters 21 | | another_param, 22 | | and_another, 23 | | ): - | |_^ UP046 + | |_^ UP047 24 | pass | = help: Use type parameters @@ -62,10 +62,10 @@ UP047.py:18:5: UP046 [*] Generic function `h` should use type parameters 20 20 | # Comment in the middle of a parameter list should be preserved 21 21 | another_param, -UP047.py:27:5: UP046 [*] Generic function `i` should use type parameters +UP047.py:27:5: UP047 [*] Generic function `i` should use type parameters | 27 | def i(s: S): - | ^^^^^^^ UP046 + | ^^^^^^^ UP047 28 | pass | = help: Use type parameters From 811e949350e8a461e2461963491ffb0e1aaeadb4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 16:03:41 -0500 Subject: [PATCH 45/77] rename rules and separate --- .../src/checkers/ast/analyze/statement.rs | 4 +- crates/ruff_linter/src/codes.rs | 3 +- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 4 +- .../src/rules/pyupgrade/rules/pep695/mod.rs | 6 +- ...rameter.rs => use_pep695_generic_class.rs} | 120 ++----------- .../pep695/use_pep695_generic_function.rs | 157 ++++++++++++++++++ 6 files changed, 178 insertions(+), 116 deletions(-) rename crates/ruff_linter/src/rules/pyupgrade/rules/pep695/{use_pep695_type_parameter.rs => use_pep695_generic_class.rs} (53%) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 616155caf5a11c..8482f47a6db949 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -376,7 +376,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::PytestParameterWithDefaultArgument) { flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def); } - if checker.enabled(Rule::NonPEP695TypeParameter) { + if checker.enabled(Rule::NonPEP695GenericFunction) { pyupgrade::rules::non_pep695_generic_function(checker, function_def); } } @@ -557,7 +557,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::DataclassEnum) { ruff::rules::dataclass_enum(checker, class_def); } - if checker.enabled(Rule::NonPEP695TypeParameter) { + if checker.enabled(Rule::NonPEP695GenericClass) { pyupgrade::rules::non_pep695_generic_class(checker, class_def); } } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 1e74c067aac26c..7c6d393476b5a0 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -540,7 +540,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs), (Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack), (Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional), - (Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695TypeParameter), + (Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass), + (Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 96db243e537c42..6b7256bba8e079 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -103,8 +103,8 @@ mod tests { #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] - #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP046.py"))] - #[test_case(Rule::NonPEP695TypeParameter, Path::new("UP047.py"))] + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046.py"))] + #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 96ff8509a7c696..6ad867cce2a6c1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -12,13 +12,15 @@ use ruff_python_ast::{ use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; +pub(crate) use use_pep695_generic_class::*; +pub(crate) use use_pep695_generic_function::*; pub(crate) use use_pep695_type_alias::*; -pub(crate) use use_pep695_type_parameter::*; use crate::checkers::ast::Checker; +mod use_pep695_generic_class; +mod use_pep695_generic_function; mod use_pep695_type_alias; -mod use_pep695_type_parameter; #[derive(Debug)] enum TypeVarRestriction<'a> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs similarity index 53% rename from crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs rename to crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 48b0c06a39d380..9d0eb01a5100c3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -1,8 +1,8 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::StmtClassDef; use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; -use ruff_python_ast::{StmtClassDef, StmtFunctionDef}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; @@ -11,13 +11,12 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// ## What it does /// -/// Checks for use of standalone type variables and parameter specifications in generic functions -/// and classes. +/// Checks for use of standalone type variables and parameter specifications in generic classes. /// /// ## Why is this bad? /// /// Special type parameter syntax was introduced in Python 3.12 by [PEP 695] for defining generic -/// functions and classes. This syntax is easier to read and provides cleaner support for generics. +/// classes. This syntax is easier to read and provides cleaner support for generics. /// /// ## Known problems /// @@ -26,8 +25,8 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// an inline type parameter may change its variance. /// /// The rule currently skips generic classes with multiple base classes, and skips generic methods -/// in generic or non-generic classes. It also skips generic functions and classes nested inside of -/// other functions or classes. Finally, this rule skips type parameters with the `default` argument +/// in generic or non-generic classes. It also skips generic classes nested inside of other +/// functions or classes. Finally, this rule skips type parameters with the `default` argument /// introduced in [PEP 696] and implemented in Python 3.13. /// /// ## Example @@ -59,31 +58,17 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// [PEP 695]: https://peps.python.org/pep-0695/ /// [PEP 696]: https://peps.python.org/pep-0696/ #[derive(ViolationMetadata)] -pub(crate) struct NonPEP695TypeParameter { +pub(crate) struct NonPEP695GenericClass { name: String, - generic_kind: GenericKind, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum GenericKind { - GenericClass, - GenericFunction, -} - -impl Violation for NonPEP695TypeParameter { +impl Violation for NonPEP695GenericClass { const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; #[derive_message_formats] fn message(&self) -> String { - let NonPEP695TypeParameter { name, generic_kind } = self; - match generic_kind { - GenericKind::GenericClass => { - format!("Generic class `{name}` uses `Generic` subclass instead of type parameters") - } - GenericKind::GenericFunction => { - format!("Generic function `{name}` should use type parameters") - } - } + let NonPEP695GenericClass { name } = self; + format!("Generic class `{name}` uses `Generic` subclass instead of type parameters") } fn fix_title(&self) -> Option { @@ -157,9 +142,8 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl checker.diagnostics.push( Diagnostic::new( - NonPEP695TypeParameter { + NonPEP695GenericClass { name: name.to_string(), - generic_kind: GenericKind::GenericClass, }, *range, ) @@ -169,85 +153,3 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl )), ); } - -/// UP046 -pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { - // PEP-695 syntax is only available on Python 3.12+ - if checker.settings.target_version < PythonVersion::Py312 { - return; - } - - // don't try to handle generic functions inside other functions or classes - if in_nested_context(checker) { - return; - } - - let StmtFunctionDef { - name, - type_params, - parameters, - .. - } = function_def; - - // TODO(brent) handle methods, for now return early in a class body. For example, an additional - // generic parameter on the method needs to be handled separately from one already on the class - // - // ```python - // T = TypeVar("T") - // S = TypeVar("S") - // - // class Foo(Generic[T]): - // def bar(self, x: T, y: S) -> S: ... - // - // - // class Foo[T]: - // def bar[S](self, x: T, y: S) -> S: ... - // ``` - if checker.semantic().current_scope().kind.is_class() { - return; - } - - // invalid to mix old-style and new-style generics - if type_params.is_some() { - return; - } - - let mut type_vars = Vec::new(); - for parameter in parameters { - if let Some(annotation) = parameter.annotation() { - let vars = { - let mut visitor = TypeVarReferenceVisitor { - vars: vec![], - semantic: checker.semantic(), - }; - visitor.visit_expr(annotation); - visitor.vars - }; - type_vars.extend(vars); - } - } - - let Some(type_vars) = check_type_vars(type_vars) else { - return; - }; - - // build the fix as a String to avoid removing comments from the entire function body - let type_params = DisplayTypeVars { - type_vars: &type_vars, - source: checker.source(), - }; - - checker.diagnostics.push( - Diagnostic::new( - NonPEP695TypeParameter { - name: name.to_string(), - generic_kind: GenericKind::GenericFunction, - }, - TextRange::new(name.start(), parameters.end()), - ) - .with_fix(Fix::applicable_edit( - Edit::insertion(type_params.to_string(), name.end()), - Applicability::Safe, - )), - ); -} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs new file mode 100644 index 00000000000000..701b20f1389d9d --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -0,0 +1,157 @@ +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::StmtFunctionDef; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; +use crate::settings::types::PythonVersion; + +use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor}; + +/// ## What it does +/// +/// Checks for use of standalone type variables and parameter specifications in generic functions. +/// +/// ## Why is this bad? +/// +/// Special type parameter syntax was introduced in Python 3.12 by [PEP 695] for defining generic +/// functions. This syntax is easier to read and provides cleaner support for generics. +/// +/// ## Known problems +/// +/// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and +/// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to +/// an inline type parameter may change its variance. +/// +/// The rule currently skips generic functions nested inside of other functions or classes and those +/// with type parameters containing the `default` argument introduced in [PEP 696] and implemented +/// in Python 3.13. +/// +/// ## Example +/// +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T") +/// +/// +/// def generic_function(t: T): +/// var: T +/// ``` +/// +/// Use instead: +/// +/// ```python +/// def generic_function[T](t: T): +/// var: T +/// ``` +/// +/// ## See also +/// +/// This rule replaces standalone type variables in class and function signatures but doesn't remove +/// the corresponding type variables even if they are unused after the fix. See +/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused type +/// variables. +/// +/// [PEP 695]: https://peps.python.org/pep-0695/ +/// [PEP 696]: https://peps.python.org/pep-0696/ +#[derive(ViolationMetadata)] +pub(crate) struct NonPEP695GenericFunction { + name: String, +} + +impl Violation for NonPEP695GenericFunction { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; + + #[derive_message_formats] + fn message(&self) -> String { + let NonPEP695GenericFunction { name } = self; + format!("Generic function `{name}` should use type parameters") + } + + fn fix_title(&self) -> Option { + Some("Use type parameters".to_string()) + } +} + +/// UP047 +pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: &StmtFunctionDef) { + // PEP-695 syntax is only available on Python 3.12+ + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + // don't try to handle generic functions inside other functions or classes + if in_nested_context(checker) { + return; + } + + let StmtFunctionDef { + name, + type_params, + parameters, + .. + } = function_def; + + // TODO(brent) handle methods, for now return early in a class body. For example, an additional + // generic parameter on the method needs to be handled separately from one already on the class + // + // ```python + // T = TypeVar("T") + // S = TypeVar("S") + // + // class Foo(Generic[T]): + // def bar(self, x: T, y: S) -> S: ... + // + // + // class Foo[T]: + // def bar[S](self, x: T, y: S) -> S: ... + // ``` + if checker.semantic().current_scope().kind.is_class() { + return; + } + + // invalid to mix old-style and new-style generics + if type_params.is_some() { + return; + } + + let mut type_vars = Vec::new(); + for parameter in parameters { + if let Some(annotation) = parameter.annotation() { + let vars = { + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + }; + visitor.visit_expr(annotation); + visitor.vars + }; + type_vars.extend(vars); + } + } + + let Some(type_vars) = check_type_vars(type_vars) else { + return; + }; + + // build the fix as a String to avoid removing comments from the entire function body + let type_params = DisplayTypeVars { + type_vars: &type_vars, + source: checker.source(), + }; + + checker.diagnostics.push( + Diagnostic::new( + NonPEP695GenericFunction { + name: name.to_string(), + }, + TextRange::new(name.start(), parameters.end()), + ) + .with_fix(Fix::applicable_edit( + Edit::insertion(type_params.to_string(), name.end()), + Applicability::Safe, + )), + ); +} From 450dfd41df7a4532057176324644ef870f212401 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 16:05:44 -0500 Subject: [PATCH 46/77] update ruff schema --- ruff.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index 05bbce31c51b09..91830622a3da51 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3653,7 +3653,6 @@ "PLW0", "PLW01", "PLW010", - "PLW0101", "PLW0108", "PLW012", "PLW0120", @@ -4188,6 +4187,7 @@ "UP044", "UP045", "UP046", + "UP047", "W", "W1", "W19", From 59301517a7018cf1841c145e390bb8a615bedcd7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 16:19:08 -0500 Subject: [PATCH 47/77] Revert "update ruff schema" This reverts commit 450dfd41df7a4532057176324644ef870f212401. --- ruff.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index 91830622a3da51..05bbce31c51b09 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3653,6 +3653,7 @@ "PLW0", "PLW01", "PLW010", + "PLW0101", "PLW0108", "PLW012", "PLW0120", @@ -4187,7 +4188,6 @@ "UP044", "UP045", "UP046", - "UP047", "W", "W1", "W19", From 4a6dbf1865cbe949ee58f7e62cd784e03c785430 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 20 Jan 2025 16:26:11 -0500 Subject: [PATCH 48/77] only add UP047 to schema (don't remove PLW0101) --- ruff.schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.schema.json b/ruff.schema.json index 05bbce31c51b09..c2e3b4d1c10090 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4188,6 +4188,7 @@ "UP044", "UP045", "UP046", + "UP047", "W", "W1", "W19", From b9e03bcf68081e797f8f4170d9c69fb3bc032a4b Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:50:24 -0500 Subject: [PATCH 49/77] Simplify `check_type_vars` Co-authored-by: Alex Waygood --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 6ad867cce2a6c1..67cad792fc46af 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -276,17 +276,7 @@ fn check_type_vars(vars: Vec>) -> Option>> { return None; } - // Type variables must be unique; filter while preserving order. - let nvars = vars.len(); - let type_vars = vars - .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) - .collect::>(); - - // non-unique type variables are runtime errors, so just bail out here - if type_vars.len() < nvars { - return None; - } - - Some(type_vars) + // If any type varaibles were not unique, just bail out here + // this is a runtime error and we can't predict what the user wanted + (vars.iter().unique_by(|tvar| &tvar.name.id).count() == vars.len()).then_some(vars) } From 1531bf124ce7ecb2a7ea16b943e0ca85334a7dca Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:53:03 -0500 Subject: [PATCH 50/77] Fix many documentation issues Co-authored-by: Alex Waygood --- .../rules/pep695/use_pep695_generic_class.rs | 14 +++++++------- .../pep695/use_pep695_generic_function.rs | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 9d0eb01a5100c3..b239258556c748 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -21,11 +21,11 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// ## Known problems /// /// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and -/// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to -/// an inline type parameter may change its variance. +/// `contravariant` keywords used by `TypeVar` variables. As such, replacing a `TypeVar` variable +/// with an inline type parameter may change its variance. /// -/// The rule currently skips generic classes with multiple base classes, and skips generic methods -/// in generic or non-generic classes. It also skips generic classes nested inside of other +/// The rule currently skips generic classes with multiple base classes. It also skips +/// generic classes nested inside of other /// functions or classes. Finally, this rule skips type parameters with the `default` argument /// introduced in [PEP 696] and implemented in Python 3.13. /// @@ -50,10 +50,10 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// /// ## See also /// -/// This rule replaces standalone type variables in class and function signatures but doesn't remove +/// This rule replaces standalone type variables in classes but doesn't remove /// the corresponding type variables even if they are unused after the fix. See -/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused type -/// variables. +/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused +/// private type variables. /// /// [PEP 695]: https://peps.python.org/pep-0695/ /// [PEP 696]: https://peps.python.org/pep-0696/ diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index 701b20f1389d9d..250509c2708b2e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -21,8 +21,8 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// ## Known problems /// /// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and -/// `contravariant` keywords used by `TypeVar` variables. As such, rewriting a `TypeVar` variable to -/// an inline type parameter may change its variance. +/// `contravariant` keywords used by `TypeVar` variables. As such, replacing a `TypeVar` variable +/// with an inline type parameter may change its variance. /// /// The rule currently skips generic functions nested inside of other functions or classes and those /// with type parameters containing the `default` argument introduced in [PEP 696] and implemented @@ -36,23 +36,23 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// T = TypeVar("T") /// /// -/// def generic_function(t: T): -/// var: T +/// def generic_function(var: T) -> T: +/// return var /// ``` /// /// Use instead: /// /// ```python -/// def generic_function[T](t: T): -/// var: T +/// def generic_function[T](var: T) -> T: +/// return var /// ``` /// /// ## See also /// -/// This rule replaces standalone type variables in class and function signatures but doesn't remove +/// This rule replaces standalone type variables in function signatures but doesn't remove /// the corresponding type variables even if they are unused after the fix. See -/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused type -/// variables. +/// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused +/// private type variables. /// /// [PEP 695]: https://peps.python.org/pep-0695/ /// [PEP 696]: https://peps.python.org/pep-0696/ From 96c36dd4bc8394d9d978e9c6fc8bea2302d1aa0b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 09:09:37 -0500 Subject: [PATCH 51/77] fix typo --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 67cad792fc46af..ec5d34d308d185 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -276,7 +276,7 @@ fn check_type_vars(vars: Vec>) -> Option>> { return None; } - // If any type varaibles were not unique, just bail out here + // If any type variables were not unique, just bail out here // this is a runtime error and we can't predict what the user wanted (vars.iter().unique_by(|tvar| &tvar.name.id).count() == vars.len()).then_some(vars) } From 07d3ca348b57f9a77cdade41e955b0e13234b2a7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 09:09:46 -0500 Subject: [PATCH 52/77] update see also sections --- .../rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs | 3 +++ .../pyupgrade/rules/pep695/use_pep695_generic_function.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index b239258556c748..97468fe0f22b4e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -55,6 +55,9 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused /// private type variables. /// +/// This rule only applies to generic classes and does not include generic functions. See +/// [`non-pep695-generic-function`](non-pep695-generic-function.md) for the function version. +/// /// [PEP 695]: https://peps.python.org/pep-0695/ /// [PEP 696]: https://peps.python.org/pep-0696/ #[derive(ViolationMetadata)] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index 250509c2708b2e..141d7bc156d24f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -54,6 +54,9 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// [`unused-private-type-var`](unused-private-type-var.md) for a rule to clean up unused /// private type variables. /// +/// This rule only applies to generic functions and does not include generic classes. See +/// [`non-pep695-generic-class`](non-pep695-generic-class.md) for the class version. +/// /// [PEP 695]: https://peps.python.org/pep-0695/ /// [PEP 696]: https://peps.python.org/pep-0696/ #[derive(ViolationMetadata)] From 39d0a5b2b3926a28463abbee9225f840bd20211c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 12:57:16 -0500 Subject: [PATCH 53/77] track default value on TypeVar but don't use it yet the main motivation here was to differentiate early returns from `expr_name_to_type_var` caused by default values from those caused by the variable not being a TypeVar, but it has the nice side effect of setting us up to handle defaults correctly in the future --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 23 ++++++++++++++----- .../rules/pep695/use_pep695_type_alias.rs | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index ec5d34d308d185..1fa09c8a674109 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -42,6 +42,7 @@ struct TypeVar<'a> { name: &'a ExprName, restriction: Option>, kind: TypeParamKind, + default: Option<&'a Expr>, } /// Wrapper for formatting a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T, @@ -121,6 +122,7 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { name, restriction, kind, + default: _, // TODO(brent) see below }: &'a TypeVar<'a>, ) -> Self { match kind { @@ -202,6 +204,7 @@ fn expr_name_to_type_var<'a>( name, restriction: None, kind: TypeParamKind::TypeVar, + default: None, }); } } @@ -236,9 +239,9 @@ fn expr_name_to_type_var<'a>( // ```python // class slice[T: str = Any]: ... // ``` - if arguments.find_keyword("default").is_some() { - return None; - } + let default = arguments + .find_keyword("default") + .map(|default| &default.value); let restriction = if let Some(bound) = arguments.find_keyword("bound") { Some(TypeVarRestriction::Bound(&bound.value)) } else if arguments.args.len() > 1 { @@ -253,6 +256,7 @@ fn expr_name_to_type_var<'a>( name, restriction, kind, + default, }); } } @@ -276,7 +280,14 @@ fn check_type_vars(vars: Vec>) -> Option>> { return None; } - // If any type variables were not unique, just bail out here - // this is a runtime error and we can't predict what the user wanted - (vars.iter().unique_by(|tvar| &tvar.name.id).count() == vars.len()).then_some(vars) + // If any type variables were not unique, just bail out here. this is a runtime error and we + // can't predict what the user wanted. also bail out if any Python 3.13+ default values are + // found on the type parameters + (vars + .iter() + .unique_by(|tvar| &tvar.name.id) + .filter(|tvar| tvar.default.is_none()) + .count() + == vars.len()) + .then_some(vars) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 79aa06b8de313f..194c7e56171f59 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -136,6 +136,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssig name, restriction: None, kind: TypeParamKind::TypeVar, + default: None, }) }) }) From e0f1251819947a5a2c78775307fe136e3e2da35f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 13:20:01 -0500 Subject: [PATCH 54/77] only emit diagnostics if there's an unknown type in class def --- .../test/fixtures/pyupgrade/UP046.py | 6 + .../rules/pep695/use_pep695_generic_class.rs | 106 +++++++++++++----- ...er__rules__pyupgrade__tests__UP046.py.snap | 12 +- 3 files changed, 95 insertions(+), 29 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 09bfa38f46f91e..4a08f4e7013392 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -23,6 +23,12 @@ class Constrained(Generic[S]): pass +# This case gets a diagnostic but not a fix because we can't look up the bounds +# or constraints on the generic type from another module +class ExternalType(Generic[T, SupportsRichComparisonT]): + pass + + # These cases are not handled class D(Generic[T, T]): # duplicate generic variable, runtime error pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 97468fe0f22b4e..4f3ae178cc726d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -1,13 +1,14 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::StmtClassDef; +use ruff_python_ast::{visitor, StmtClassDef}; use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; +use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor}; +use super::{check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTypeVars, TypeVar}; /// ## What it does /// @@ -29,6 +30,10 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// functions or classes. Finally, this rule skips type parameters with the `default` argument /// introduced in [PEP 696] and implemented in Python 3.13. /// +/// This rule can only offer a fix if all of the generic types in the class definition are defined +/// in the current module. For external type parameters, a diagnostic is emitted without a suggested +/// fix. +/// /// ## Example /// /// ```python @@ -66,7 +71,7 @@ pub(crate) struct NonPEP695GenericClass { } impl Violation for NonPEP695GenericClass { - const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { @@ -124,35 +129,80 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl return; } - let vars = { - let mut visitor = TypeVarReferenceVisitor { - vars: vec![], - semantic: checker.semantic(), - }; - visitor.visit_expr(slice); - visitor.vars - }; + let mut diagnostic = Diagnostic::new( + NonPEP695GenericClass { + name: name.to_string(), + }, + *range, + ); - let Some(type_vars) = check_type_vars(vars) else { - return; + let mut visitor = TypeVarReferenceVisitor { + vars: vec![], + semantic: checker.semantic(), + any_skipped: false, }; + visitor.visit_expr(slice); + + // if any of the parameters have been skipped, this indicates that we could not resolve the type + // to a `TypeVar`, `TypeVarTuple`, or `ParamSpec`, and thus our fix would remove it from the + // signature incorrectly. We can still offer the diagnostic created above without a Fix. For + // example, + // + // ```python + // from somewhere import SomethingElse + // + // T = TypeVar("T") + // + // class Class(Generic[T, SomethingElse]): ... + // ``` + // + // should not be converted to + // + // ```python + // class Class[T]: ... + // ``` + // + // just because we can't confirm that `SomethingElse` is a `TypeVar` + if !visitor.any_skipped { + let Some(type_vars) = check_type_vars(visitor.vars) else { + return; + }; - // build the fix as a String to avoid removing comments from the entire function body - let type_params = DisplayTypeVars { - type_vars: &type_vars, - source: checker.source(), - }; + // build the fix as a String to avoid removing comments from the entire function body + let type_params = DisplayTypeVars { + type_vars: &type_vars, + source: checker.source(), + }; - checker.diagnostics.push( - Diagnostic::new( - NonPEP695GenericClass { - name: name.to_string(), - }, - *range, - ) - .with_fix(Fix::applicable_edit( + diagnostic.set_fix(Fix::applicable_edit( Edit::replacement(type_params.to_string(), name.end(), arguments.end()), Applicability::Safe, - )), - ); + )); + } + + checker.diagnostics.push(diagnostic); +} + +/// Copy of [`super::TypeVarReferenceVisitor`] with additional tracking of non-TypeVars encountered +/// to avoid replacing generic parameters when an unknown `TypeVar` is encountered. +struct TypeVarReferenceVisitor<'a> { + vars: Vec>, + semantic: &'a SemanticModel<'a>, + any_skipped: bool, +} + +/// Recursively collects the names of type variable references present in an expression. +impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { + fn visit_expr(&mut self, expr: &'a Expr) { + match expr { + Expr::Name(name) if name.ctx.is_load() => { + if let Some(var) = expr_name_to_type_var(self.semantic, name) { + self.vars.push(var); + } else { + self.any_skipped = true; + } + } + _ => visitor::walk_expr(self, expr), + } + } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index e1e5e17613d0ee..ca61207c979e08 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -73,4 +73,14 @@ UP046.py:22:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass in 22 |+class Constrained[S: (str, bytes)]: 23 23 | pass 24 24 | -25 25 | +25 25 | + +UP046.py:28:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters + | +26 | # This case gets a diagnostic but not a fix because we can't look up the bounds +27 | # or constraints on the generic type from another module +28 | class ExternalType(Generic[T, SupportsRichComparisonT]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP046 +29 | pass + | + = help: Use type parameters From d034f627677e45ae9338e68e46f5935dfce815d3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 13:41:55 -0500 Subject: [PATCH 55/77] mark fixes as unsafe, document them, and test --- .../test/fixtures/pyupgrade/UP047.py | 12 ++ .../rules/pep695/use_pep695_generic_class.rs | 12 +- .../pep695/use_pep695_generic_function.rs | 17 ++- ...er__rules__pyupgrade__tests__UP046.py.snap | 8 +- ...er__rules__pyupgrade__tests__UP047.py.snap | 126 ++++++++++-------- 5 files changed, 108 insertions(+), 67 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py index cc0e99255d392e..31de01064c56f4 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py @@ -1,6 +1,8 @@ from collections.abc import Callable from typing import Any, ParamSpec, TypeVar, TypeVarTuple +from somewhere import Something + S = TypeVar("S", str, bytes) # constrained type variable T = TypeVar("T", bound=float) Ts = TypeVarTuple("Ts") @@ -28,6 +30,16 @@ def i(s: S): pass +# NOTE this case is the reason the fix is marked unsafe. If we can't confirm +# that one of the type parameters (`Something` in this case) is a TypeVar, +# which we can't do across module boundaries, we will not convert it to a +# generic type parameter. This leads to code that mixes old-style standalone +# TypeVars with the new-style generic syntax and will be rejected by type +# checkers +def broken_fix(okay: T, bad: Something): + pass + + # these cases are not handled # TODO(brent) default requires 3.13 diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 4f3ae178cc726d..03e8545cbe6738 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -21,10 +21,6 @@ use super::{check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTy /// /// ## Known problems /// -/// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and -/// `contravariant` keywords used by `TypeVar` variables. As such, replacing a `TypeVar` variable -/// with an inline type parameter may change its variance. -/// /// The rule currently skips generic classes with multiple base classes. It also skips /// generic classes nested inside of other /// functions or classes. Finally, this rule skips type parameters with the `default` argument @@ -34,6 +30,12 @@ use super::{check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTy /// in the current module. For external type parameters, a diagnostic is emitted without a suggested /// fix. /// +/// ## Fix safety +/// +/// This fix is marked as unsafe, as [PEP 695] uses inferred variance for type parameters, instead +/// of the `covariant` and `contravariant` keywords used by `TypeVar` variables. As such, replacing +/// a `TypeVar` variable with an inline type parameter may change its variance. +/// /// ## Example /// /// ```python @@ -176,7 +178,7 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl diagnostic.set_fix(Fix::applicable_edit( Edit::replacement(type_params.to_string(), name.end(), arguments.end()), - Applicability::Safe, + Applicability::Unsafe, )); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index 141d7bc156d24f..0d845e367e3786 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -20,14 +20,21 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// /// ## Known problems /// -/// [PEP 695] uses inferred variance for type parameters, instead of the `covariant` and -/// `contravariant` keywords used by `TypeVar` variables. As such, replacing a `TypeVar` variable -/// with an inline type parameter may change its variance. -/// /// The rule currently skips generic functions nested inside of other functions or classes and those /// with type parameters containing the `default` argument introduced in [PEP 696] and implemented /// in Python 3.13. /// +/// ## Fix safety +/// +/// This fix is marked unsafe, as [PEP 695] uses inferred variance for type parameters, instead of +/// the `covariant` and `contravariant` keywords used by `TypeVar` variables. As such, replacing a +/// `TypeVar` variable with an inline type parameter may change its variance. +/// +/// Additionally, if the rule cannot determine whether a parameter annotation corresponds to a type +/// variable (e.g. for a type imported from another module), it will not add the type to the generic +/// type parameter list. This causes the function to have a mix of old-style type variables and +/// new-style generic type parameters, which will be rejected by type checkers. +/// /// ## Example /// /// ```python @@ -154,7 +161,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & ) .with_fix(Fix::applicable_edit( Edit::insertion(type_params.to_string(), name.end()), - Applicability::Safe, + Applicability::Unsafe, )), ); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index ca61207c979e08..26f85680bea075 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -11,7 +11,7 @@ UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of typ | = help: Use type parameters -ℹ Safe fix +ℹ Unsafe fix 6 6 | P = ParamSpec("P") 7 7 | 8 8 | @@ -29,7 +29,7 @@ UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of ty | = help: Use type parameters -ℹ Safe fix +ℹ Unsafe fix 11 11 | pass 12 12 | 13 13 | @@ -47,7 +47,7 @@ UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of ty | = help: Use type parameters -ℹ Safe fix +ℹ Unsafe fix 15 15 | pass 16 16 | 17 17 | @@ -65,7 +65,7 @@ UP046.py:22:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass in | = help: Use type parameters -ℹ Safe fix +ℹ Unsafe fix 19 19 | pass 20 20 | 21 21 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap index d1e6024f7d8416..3aca4fffec1fbd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap @@ -2,80 +2,100 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP047.py:10:5: UP047 [*] Generic function `f` should use type parameters +UP047.py:12:5: UP047 [*] Generic function `f` should use type parameters | -10 | def f(t: T): +12 | def f(t: T): | ^^^^^^^ UP047 -11 | pass +13 | pass | = help: Use type parameters -ℹ Safe fix -7 7 | P = ParamSpec("P") -8 8 | -9 9 | -10 |-def f(t: T): - 10 |+def f[T: float](t: T): -11 11 | pass -12 12 | -13 13 | +ℹ Unsafe fix +9 9 | P = ParamSpec("P") +10 10 | +11 11 | +12 |-def f(t: T): + 12 |+def f[T: float](t: T): +13 13 | pass +14 14 | +15 15 | -UP047.py:14:5: UP047 [*] Generic function `g` should use type parameters +UP047.py:16:5: UP047 [*] Generic function `g` should use type parameters | -14 | def g(ts: tuple[*Ts]): +16 | def g(ts: tuple[*Ts]): | ^^^^^^^^^^^^^^^^^ UP047 -15 | pass +17 | pass | = help: Use type parameters -ℹ Safe fix -11 11 | pass -12 12 | -13 13 | -14 |-def g(ts: tuple[*Ts]): - 14 |+def g[*Ts](ts: tuple[*Ts]): -15 15 | pass -16 16 | -17 17 | +ℹ Unsafe fix +13 13 | pass +14 14 | +15 15 | +16 |-def g(ts: tuple[*Ts]): + 16 |+def g[*Ts](ts: tuple[*Ts]): +17 17 | pass +18 18 | +19 19 | -UP047.py:18:5: UP047 [*] Generic function `h` should use type parameters +UP047.py:20:5: UP047 [*] Generic function `h` should use type parameters | -18 | def h( +20 | def h( | _____^ -19 | | p: Callable[P, T], -20 | | # Comment in the middle of a parameter list should be preserved -21 | | another_param, -22 | | and_another, -23 | | ): +21 | | p: Callable[P, T], +22 | | # Comment in the middle of a parameter list should be preserved +23 | | another_param, +24 | | and_another, +25 | | ): | |_^ UP047 -24 | pass +26 | pass | = help: Use type parameters -ℹ Safe fix -15 15 | pass -16 16 | -17 17 | -18 |-def h( - 18 |+def h[**P, T: float]( -19 19 | p: Callable[P, T], -20 20 | # Comment in the middle of a parameter list should be preserved -21 21 | another_param, +ℹ Unsafe fix +17 17 | pass +18 18 | +19 19 | +20 |-def h( + 20 |+def h[**P, T: float]( +21 21 | p: Callable[P, T], +22 22 | # Comment in the middle of a parameter list should be preserved +23 23 | another_param, -UP047.py:27:5: UP047 [*] Generic function `i` should use type parameters +UP047.py:29:5: UP047 [*] Generic function `i` should use type parameters | -27 | def i(s: S): +29 | def i(s: S): | ^^^^^^^ UP047 -28 | pass +30 | pass | = help: Use type parameters -ℹ Safe fix -24 24 | pass -25 25 | -26 26 | -27 |-def i(s: S): - 27 |+def i[S: (str, bytes)](s: S): -28 28 | pass -29 29 | -30 30 | +ℹ Unsafe fix +26 26 | pass +27 27 | +28 28 | +29 |-def i(s: S): + 29 |+def i[S: (str, bytes)](s: S): +30 30 | pass +31 31 | +32 32 | + +UP047.py:39:5: UP047 [*] Generic function `broken_fix` should use type parameters + | +37 | # TypeVars with the new-style generic syntax and will be rejected by type +38 | # checkers +39 | def broken_fix(okay: T, bad: Something): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP047 +40 | pass + | + = help: Use type parameters + +ℹ Unsafe fix +36 36 | # generic type parameter. This leads to code that mixes old-style standalone +37 37 | # TypeVars with the new-style generic syntax and will be rejected by type +38 38 | # checkers +39 |-def broken_fix(okay: T, bad: Something): + 39 |+def broken_fix[T: float](okay: T, bad: Something): +40 40 | pass +41 41 | +42 42 | From d55e2aed0b9bb320656b8ddf743418b0b6b34497 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 14:32:51 -0500 Subject: [PATCH 56/77] add a special case for typing.AnyStr --- .../test/fixtures/pyupgrade/UP046.py | 8 +++- .../test/fixtures/pyupgrade/UP047.py | 6 ++- .../src/rules/pyupgrade/rules/pep695/mod.rs | 41 +++++++++++++++++-- .../rules/pep695/use_pep695_generic_class.rs | 39 ++++++++++++++++-- ...er__rules__pyupgrade__tests__UP046.py.snap | 20 +++++++++ ...er__rules__pyupgrade__tests__UP047.py.snap | 20 ++++++++- 6 files changed, 125 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 4a08f4e7013392..b10d3e5c5547ca 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -1,4 +1,4 @@ -from typing import Any, Generic, ParamSpec, TypeVar, TypeVarTuple +from typing import Any, AnyStr, Generic, ParamSpec, TypeVar, TypeVarTuple S = TypeVar("S", str, bytes) # constrained type variable T = TypeVar("T", bound=float) @@ -29,6 +29,12 @@ class ExternalType(Generic[T, SupportsRichComparisonT]): pass +# typing.AnyStr is a common external type variable, so treat it specially as a +# known TypeVar +class MyStr(Generic[AnyStr]): + pass + + # These cases are not handled class D(Generic[T, T]): # duplicate generic variable, runtime error pass diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py index 31de01064c56f4..621d7afc566ce3 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Any, ParamSpec, TypeVar, TypeVarTuple +from typing import Any, AnyStr, ParamSpec, TypeVar, TypeVarTuple from somewhere import Something @@ -40,6 +40,10 @@ def broken_fix(okay: T, bad: Something): pass +def any_str_param(s: AnyStr): + pass + + # these cases are not handled # TODO(brent) default requires 3.13 diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 1fa09c8a674109..c50a1b657b8617 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -5,6 +5,7 @@ use std::fmt::Display; use itertools::Itertools; use ruff_python_ast::{ self as ast, + name::Name, visitor::{self, Visitor}, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, @@ -27,7 +28,7 @@ enum TypeVarRestriction<'a> { /// A type variable with a bound, e.g., `TypeVar("T", bound=int)`. Bound(&'a Expr), /// A type variable with constraints, e.g., `TypeVar("T", int, str)`. - Constraint(Vec<&'a Expr>), + Constraint(Vec), } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -102,7 +103,12 @@ impl Display for DisplayTypeVar<'_> { let len = vec.len(); f.write_str("(")?; for (i, v) in vec.iter().enumerate() { - f.write_str(&self.source[v.range()])?; + // typing.AnyStr special case doesn't have a real range + if let Expr::Name(name) = v { + f.write_str(&name.id.to_string())?; + } else { + f.write_str(&self.source[v.range()])?; + } if i < len - 1 { f.write_str(", ")?; } @@ -170,6 +176,35 @@ struct TypeVarReferenceVisitor<'a> { impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { + // special case for typing.AnyStr, which is a commonly-imported type variable in the + // standard library with the definition: + // + // ```python + // AnyStr = TypeVar('AnyStr', bytes, str) + // ``` + // + // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be + // much to keep updated here. See + // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 + e @ Expr::Name(name) if self.semantic.match_typing_expr(e, "AnyStr") => { + self.vars.push(TypeVar { + name, + restriction: Some(TypeVarRestriction::Constraint(vec![ + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("bytes"), + ctx: ruff_python_ast::ExprContext::Load, + }), + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("str"), + ctx: ruff_python_ast::ExprContext::Load, + }), + ])), + kind: TypeParamKind::TypeVar, + default: None, + }) + } Expr::Name(name) if name.ctx.is_load() => { self.vars.extend(expr_name_to_type_var(self.semantic, name)); } @@ -246,7 +281,7 @@ fn expr_name_to_type_var<'a>( Some(TypeVarRestriction::Bound(&bound.value)) } else if arguments.args.len() > 1 { Some(TypeVarRestriction::Constraint( - arguments.args.iter().skip(1).collect(), + arguments.args.iter().cloned().skip(1).collect(), )) } else { None diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 03e8545cbe6738..7f7093bd89b735 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -1,14 +1,18 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{visitor, StmtClassDef}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{visitor, ExprName, StmtClassDef}; use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; use ruff_python_semantic::SemanticModel; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTypeVars, TypeVar}; +use super::{ + check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTypeVars, TypeParamKind, + TypeVar, TypeVarRestriction, +}; /// ## What it does /// @@ -197,6 +201,35 @@ struct TypeVarReferenceVisitor<'a> { impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { + // special case for typing.AnyStr, which is a commonly-imported type variable in the + // standard library with the definition: + // + // ```python + // AnyStr = TypeVar('AnyStr', bytes, str) + // ``` + // + // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be + // much to keep updated here. See + // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 + e @ Expr::Name(name) if self.semantic.match_typing_expr(e, "AnyStr") => { + self.vars.push(TypeVar { + name, + restriction: Some(TypeVarRestriction::Constraint(vec![ + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("bytes"), + ctx: ruff_python_ast::ExprContext::Load, + }), + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("str"), + ctx: ruff_python_ast::ExprContext::Load, + }), + ])), + kind: TypeParamKind::TypeVar, + default: None, + }) + } Expr::Name(name) if name.ctx.is_load() => { if let Some(var) = expr_name_to_type_var(self.semantic, name) { self.vars.push(var); diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 26f85680bea075..e9749648fb8ee6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -84,3 +84,23 @@ UP046.py:28:20: UP046 Generic class `ExternalType` uses `Generic` subclass inste 29 | pass | = help: Use type parameters + +UP046.py:34:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters + | +32 | # typing.AnyStr is a common external type variable, so treat it specially as a +33 | # known TypeVar +34 | class MyStr(Generic[AnyStr]): + | ^^^^^^^^^^^^^^^ UP046 +35 | pass + | + = help: Use type parameters + +ℹ Unsafe fix +31 31 | +32 32 | # typing.AnyStr is a common external type variable, so treat it specially as a +33 33 | # known TypeVar +34 |-class MyStr(Generic[AnyStr]): + 34 |+class MyStr[AnyStr: (bytes, str)]: +35 35 | pass +36 36 | +37 37 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap index 3aca4fffec1fbd..0dfce10f6c6189 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap @@ -98,4 +98,22 @@ UP047.py:39:5: UP047 [*] Generic function `broken_fix` should use type parameter 39 |+def broken_fix[T: float](okay: T, bad: Something): 40 40 | pass 41 41 | -42 42 | +42 42 | + +UP047.py:43:5: UP047 [*] Generic function `any_str_param` should use type parameters + | +43 | def any_str_param(s: AnyStr): + | ^^^^^^^^^^^^^^^^^^^^^^^^ UP047 +44 | pass + | + = help: Use type parameters + +ℹ Unsafe fix +40 40 | pass +41 41 | +42 42 | +43 |-def any_str_param(s: AnyStr): + 43 |+def any_str_param[AnyStr: (bytes, str)](s: AnyStr): +44 44 | pass +45 45 | +46 46 | From c3f38585680f3de3df6ee970edfdbb7f50614dd7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 14:37:36 -0500 Subject: [PATCH 57/77] clippy --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs | 6 +++--- .../pyupgrade/rules/pep695/use_pep695_generic_class.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index c50a1b657b8617..f8c2cb9c271e1c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -105,7 +105,7 @@ impl Display for DisplayTypeVar<'_> { for (i, v) in vec.iter().enumerate() { // typing.AnyStr special case doesn't have a real range if let Expr::Name(name) = v { - f.write_str(&name.id.to_string())?; + f.write_str(name.id.as_ref())?; } else { f.write_str(&self.source[v.range()])?; } @@ -203,7 +203,7 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { ])), kind: TypeParamKind::TypeVar, default: None, - }) + }); } Expr::Name(name) if name.ctx.is_load() => { self.vars.extend(expr_name_to_type_var(self.semantic, name)); @@ -281,7 +281,7 @@ fn expr_name_to_type_var<'a>( Some(TypeVarRestriction::Bound(&bound.value)) } else if arguments.args.len() > 1 { Some(TypeVarRestriction::Constraint( - arguments.args.iter().cloned().skip(1).collect(), + arguments.args.iter().skip(1).cloned().collect(), )) } else { None diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index 7f7093bd89b735..ecaf359e4af6d0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -228,7 +228,7 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { ])), kind: TypeParamKind::TypeVar, default: None, - }) + }); } Expr::Name(name) if name.ctx.is_load() => { if let Some(var) = expr_name_to_type_var(self.semantic, name) { From cee09a48d76a32eb87b46804a51b550139021065 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 14:57:45 -0500 Subject: [PATCH 58/77] re-unify visitors initially I thought the any_skipped check would be more invasive, but carrying around an extra bool in the cases where we don't care about it seems preferable to duplicating the AnyStr code especially --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 9 ++- .../rules/pep695/use_pep695_generic_class.rs | 67 ++----------------- .../pep695/use_pep695_generic_function.rs | 1 + .../rules/pep695/use_pep695_type_alias.rs | 1 + 4 files changed, 15 insertions(+), 63 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index f8c2cb9c271e1c..8e3f90497d545f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -170,6 +170,9 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { struct TypeVarReferenceVisitor<'a> { vars: Vec>, semantic: &'a SemanticModel<'a>, + /// Tracks whether any non-TypeVars are have been seen to avoid replacing generic parameters + /// when an unknown `TypeVar` is encountered. + any_skipped: bool, } /// Recursively collects the names of type variable references present in an expression. @@ -206,7 +209,11 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { }); } Expr::Name(name) if name.ctx.is_load() => { - self.vars.extend(expr_name_to_type_var(self.semantic, name)); + if let Some(var) = expr_name_to_type_var(self.semantic, name) { + self.vars.push(var); + } else { + self.any_skipped = true; + } } _ => visitor::walk_expr(self, expr), } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index ecaf359e4af6d0..dbc1898b28d251 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -1,18 +1,14 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::name::Name; -use ruff_python_ast::{visitor, ExprName, StmtClassDef}; -use ruff_python_ast::{visitor::Visitor, Expr, ExprSubscript}; -use ruff_python_semantic::SemanticModel; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::StmtClassDef; +use ruff_python_ast::{Expr, ExprSubscript}; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; -use super::{ - check_type_vars, expr_name_to_type_var, in_nested_context, DisplayTypeVars, TypeParamKind, - TypeVar, TypeVarRestriction, -}; +use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor}; /// ## What it does /// @@ -188,56 +184,3 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl checker.diagnostics.push(diagnostic); } - -/// Copy of [`super::TypeVarReferenceVisitor`] with additional tracking of non-TypeVars encountered -/// to avoid replacing generic parameters when an unknown `TypeVar` is encountered. -struct TypeVarReferenceVisitor<'a> { - vars: Vec>, - semantic: &'a SemanticModel<'a>, - any_skipped: bool, -} - -/// Recursively collects the names of type variable references present in an expression. -impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { - fn visit_expr(&mut self, expr: &'a Expr) { - match expr { - // special case for typing.AnyStr, which is a commonly-imported type variable in the - // standard library with the definition: - // - // ```python - // AnyStr = TypeVar('AnyStr', bytes, str) - // ``` - // - // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be - // much to keep updated here. See - // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 - e @ Expr::Name(name) if self.semantic.match_typing_expr(e, "AnyStr") => { - self.vars.push(TypeVar { - name, - restriction: Some(TypeVarRestriction::Constraint(vec![ - Expr::Name(ExprName { - range: TextRange::default(), - id: Name::from("bytes"), - ctx: ruff_python_ast::ExprContext::Load, - }), - Expr::Name(ExprName { - range: TextRange::default(), - id: Name::from("str"), - ctx: ruff_python_ast::ExprContext::Load, - }), - ])), - kind: TypeParamKind::TypeVar, - default: None, - }); - } - Expr::Name(name) if name.ctx.is_load() => { - if let Some(var) = expr_name_to_type_var(self.semantic, name) { - self.vars.push(var); - } else { - self.any_skipped = true; - } - } - _ => visitor::walk_expr(self, expr), - } - } -} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index 0d845e367e3786..ecaf8b57cf6daf 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -134,6 +134,7 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & let mut visitor = TypeVarReferenceVisitor { vars: vec![], semantic: checker.semantic(), + any_skipped: false, }; visitor.visit_expr(annotation); visitor.vars diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 194c7e56171f59..c2340c6090e73a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -190,6 +190,7 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) let mut visitor = TypeVarReferenceVisitor { vars: vec![], semantic: checker.semantic(), + any_skipped: false, }; visitor.visit_expr(value); visitor.vars From 550cf6daf7750044c00504d52a4fd7daf590443d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 15:02:56 -0500 Subject: [PATCH 59/77] use unsafe_edit --- .../rules/pep695/use_pep695_generic_class.rs | 11 ++++++----- .../rules/pep695/use_pep695_generic_function.rs | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index dbc1898b28d251..bdc2a98e62e356 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::StmtClassDef; @@ -176,10 +176,11 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl source: checker.source(), }; - diagnostic.set_fix(Fix::applicable_edit( - Edit::replacement(type_params.to_string(), name.end(), arguments.end()), - Applicability::Unsafe, - )); + diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement( + type_params.to_string(), + name.end(), + arguments.end(), + ))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index ecaf8b57cf6daf..0654f4d60f1eec 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::StmtFunctionDef; @@ -160,9 +160,9 @@ pub(crate) fn non_pep695_generic_function(checker: &mut Checker, function_def: & }, TextRange::new(name.start(), parameters.end()), ) - .with_fix(Fix::applicable_edit( - Edit::insertion(type_params.to_string(), name.end()), - Applicability::Unsafe, - )), + .with_fix(Fix::unsafe_edit(Edit::insertion( + type_params.to_string(), + name.end(), + ))), ); } From 06993aa2ae3afc4a3257d143a623b095a74773b9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 15:36:33 -0500 Subject: [PATCH 60/77] fix typo --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 8e3f90497d545f..3de4228bcc5e63 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -170,8 +170,8 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { struct TypeVarReferenceVisitor<'a> { vars: Vec>, semantic: &'a SemanticModel<'a>, - /// Tracks whether any non-TypeVars are have been seen to avoid replacing generic parameters - /// when an unknown `TypeVar` is encountered. + /// Tracks whether any non-TypeVars have been seen to avoid replacing generic parameters when an + /// unknown `TypeVar` is encountered. any_skipped: bool, } From 9fadf46933bb5cc7f6eafbcd22418dfdd8021e3a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 21 Jan 2025 16:18:22 -0500 Subject: [PATCH 61/77] add a test with multiple generics --- .../test/fixtures/pyupgrade/UP046.py | 4 ++++ ...er__rules__pyupgrade__tests__UP046.py.snap | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index b10d3e5c5547ca..914c8f51751ff9 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -35,6 +35,10 @@ class MyStr(Generic[AnyStr]): pass +class MultipleGenerics(Generic[S, T, Ts, P]): + pass + + # These cases are not handled class D(Generic[T, T]): # duplicate generic variable, runtime error pass diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index e9749648fb8ee6..7eee393cc978d4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -103,4 +103,22 @@ UP046.py:34:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead 34 |+class MyStr[AnyStr: (bytes, str)]: 35 35 | pass 36 36 | -37 37 | +37 37 | + +UP046.py:38:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters + | +38 | class MultipleGenerics(Generic[S, T, Ts, P]): + | ^^^^^^^^^^^^^^^^^^^^ UP046 +39 | pass + | + = help: Use type parameters + +ℹ Unsafe fix +35 35 | pass +36 36 | +37 37 | +38 |-class MultipleGenerics(Generic[S, T, Ts, P]): + 38 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: +39 39 | pass +40 40 | +41 41 | From 6c696f1ad8b89519237739dcefc80d4189295b0e Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 08:41:11 -0500 Subject: [PATCH 62/77] slightly more realistic class code --- .../test/fixtures/pyupgrade/UP046.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 914c8f51751ff9..4a318793d3776f 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -8,35 +8,43 @@ class A(Generic[T]): # Comments in a class body are preserved - pass + var: T class B(Generic[*Ts]): - pass + var: tuple[*Ts] class C(Generic[P]): - pass + var: P class Constrained(Generic[S]): - pass + var: S # This case gets a diagnostic but not a fix because we can't look up the bounds # or constraints on the generic type from another module class ExternalType(Generic[T, SupportsRichComparisonT]): - pass + var: T + compare: SupportsRichComparisonT # typing.AnyStr is a common external type variable, so treat it specially as a # known TypeVar class MyStr(Generic[AnyStr]): - pass + s: AnyStr class MultipleGenerics(Generic[S, T, Ts, P]): - pass + var: S + typ: T + tup: tuple[*Ts] + pep: P + + +class Multiple(NotGeneric, Generic[T]): + var: T # These cases are not handled @@ -48,8 +56,8 @@ class D(Generic[T, T]): # duplicate generic variable, runtime error # little more work. these should be left alone for now but be fixed eventually. class NotGeneric: # -> generic_method[T: float](t: T) - def generic_method(t: T): - pass + def generic_method(t: T) -> T: + return t # This one is strange in particular because of the mix of old- and new-style @@ -57,8 +65,8 @@ def generic_method(t: T): # type alias does not use the new syntax." `more_generic` doesn't use the new # syntax, so it can use T from the module and U from the class scope. class MixedGenerics[U]: - def more_generic(u: U, t: T): - pass + def more_generic(u: U, t: T) -> tuple[U, T]: + return (u, t) # TODO(brent) we should also handle multiple base classes @@ -71,10 +79,10 @@ class Multiple(NotGeneric, Generic[T]): class DefaultTypeVar(Generic[V]): # -> [V: str = Any] - pass + var: V # nested classes and functions are skipped class Outer: class Inner(Generic[T]): - pass + var: T From a15703e50f06bb241d507866deb3a3da1d733d39 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 08:43:33 -0500 Subject: [PATCH 63/77] slightly more realistic function code --- .../test/fixtures/pyupgrade/UP047.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py index 621d7afc566ce3..48b5d431de6fbc 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py @@ -9,12 +9,12 @@ P = ParamSpec("P") -def f(t: T): - pass +def f(t: T) -> T: + return t -def g(ts: tuple[*Ts]): - pass +def g(ts: tuple[*Ts]) -> tuple[*Ts]: + return ts def h( @@ -22,12 +22,12 @@ def h( # Comment in the middle of a parameter list should be preserved another_param, and_another, -): - pass +) -> Callable[P, T]: + return p -def i(s: S): - pass +def i(s: S) -> S: + return s # NOTE this case is the reason the fix is marked unsafe. If we can't confirm @@ -36,12 +36,12 @@ def i(s: S): # generic type parameter. This leads to code that mixes old-style standalone # TypeVars with the new-style generic syntax and will be rejected by type # checkers -def broken_fix(okay: T, bad: Something): - pass +def broken_fix(okay: T, bad: Something) -> tuple[T, Something]: + return (okay, bad) -def any_str_param(s: AnyStr): - pass +def any_str_param(s: AnyStr) -> AnyStr: + return s # these cases are not handled @@ -50,10 +50,10 @@ def any_str_param(s: AnyStr): V = TypeVar("V", default=Any, bound=str) -def default_var(v: V): - pass +def default_var(v: V) -> V: + return v def outer(): - def inner(t: T): - pass + def inner(t: T) -> T: + return t From 9948059ee7a9b7775475bd611c76058e62fec6db Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 08:46:12 -0500 Subject: [PATCH 64/77] update snapshots --- ...er__rules__pyupgrade__tests__UP046.py.snap | 70 ++++++++++--------- ...er__rules__pyupgrade__tests__UP047.py.snap | 62 ++++++++-------- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 7eee393cc978d4..058ec1fc4c3b91 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -7,7 +7,7 @@ UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of typ 9 | class A(Generic[T]): | ^^^^^^^^^^ UP046 10 | # Comments in a class body are preserved -11 | pass +11 | var: T | = help: Use type parameters @@ -18,24 +18,24 @@ UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of typ 9 |-class A(Generic[T]): 9 |+class A[T: float]: 10 10 | # Comments in a class body are preserved -11 11 | pass +11 11 | var: T 12 12 | UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | 14 | class B(Generic[*Ts]): | ^^^^^^^^^^^^ UP046 -15 | pass +15 | var: tuple[*Ts] | = help: Use type parameters ℹ Unsafe fix -11 11 | pass +11 11 | var: T 12 12 | 13 13 | 14 |-class B(Generic[*Ts]): 14 |+class B[*Ts]: -15 15 | pass +15 15 | var: tuple[*Ts] 16 16 | 17 17 | @@ -43,17 +43,17 @@ UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of ty | 18 | class C(Generic[P]): | ^^^^^^^^^^ UP046 -19 | pass +19 | var: P | = help: Use type parameters ℹ Unsafe fix -15 15 | pass +15 15 | var: tuple[*Ts] 16 16 | 17 17 | 18 |-class C(Generic[P]): 18 |+class C[**P]: -19 19 | pass +19 19 | var: P 20 20 | 21 21 | @@ -61,17 +61,17 @@ UP046.py:22:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass in | 22 | class Constrained(Generic[S]): | ^^^^^^^^^^ UP046 -23 | pass +23 | var: S | = help: Use type parameters ℹ Unsafe fix -19 19 | pass +19 19 | var: P 20 20 | 21 21 | 22 |-class Constrained(Generic[S]): 22 |+class Constrained[S: (str, bytes)]: -23 23 | pass +23 23 | var: S 24 24 | 25 25 | @@ -81,44 +81,46 @@ UP046.py:28:20: UP046 Generic class `ExternalType` uses `Generic` subclass inste 27 | # or constraints on the generic type from another module 28 | class ExternalType(Generic[T, SupportsRichComparisonT]): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP046 -29 | pass +29 | var: T +30 | compare: SupportsRichComparisonT | = help: Use type parameters -UP046.py:34:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters +UP046.py:35:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters | -32 | # typing.AnyStr is a common external type variable, so treat it specially as a -33 | # known TypeVar -34 | class MyStr(Generic[AnyStr]): +33 | # typing.AnyStr is a common external type variable, so treat it specially as a +34 | # known TypeVar +35 | class MyStr(Generic[AnyStr]): | ^^^^^^^^^^^^^^^ UP046 -35 | pass +36 | s: AnyStr | = help: Use type parameters ℹ Unsafe fix -31 31 | -32 32 | # typing.AnyStr is a common external type variable, so treat it specially as a -33 33 | # known TypeVar -34 |-class MyStr(Generic[AnyStr]): - 34 |+class MyStr[AnyStr: (bytes, str)]: -35 35 | pass -36 36 | +32 32 | +33 33 | # typing.AnyStr is a common external type variable, so treat it specially as a +34 34 | # known TypeVar +35 |-class MyStr(Generic[AnyStr]): + 35 |+class MyStr[AnyStr: (bytes, str)]: +36 36 | s: AnyStr 37 37 | +38 38 | -UP046.py:38:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters +UP046.py:39:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters | -38 | class MultipleGenerics(Generic[S, T, Ts, P]): +39 | class MultipleGenerics(Generic[S, T, Ts, P]): | ^^^^^^^^^^^^^^^^^^^^ UP046 -39 | pass +40 | var: S +41 | typ: T | = help: Use type parameters ℹ Unsafe fix -35 35 | pass -36 36 | +36 36 | s: AnyStr 37 37 | -38 |-class MultipleGenerics(Generic[S, T, Ts, P]): - 38 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: -39 39 | pass -40 40 | -41 41 | +38 38 | +39 |-class MultipleGenerics(Generic[S, T, Ts, P]): + 39 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: +40 40 | var: S +41 41 | typ: T +42 42 | tup: tuple[*Ts] diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap index 0dfce10f6c6189..fd3dde6026e335 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap @@ -4,9 +4,9 @@ snapshot_kind: text --- UP047.py:12:5: UP047 [*] Generic function `f` should use type parameters | -12 | def f(t: T): +12 | def f(t: T) -> T: | ^^^^^^^ UP047 -13 | pass +13 | return t | = help: Use type parameters @@ -14,27 +14,27 @@ UP047.py:12:5: UP047 [*] Generic function `f` should use type parameters 9 9 | P = ParamSpec("P") 10 10 | 11 11 | -12 |-def f(t: T): - 12 |+def f[T: float](t: T): -13 13 | pass +12 |-def f(t: T) -> T: + 12 |+def f[T: float](t: T) -> T: +13 13 | return t 14 14 | 15 15 | UP047.py:16:5: UP047 [*] Generic function `g` should use type parameters | -16 | def g(ts: tuple[*Ts]): +16 | def g(ts: tuple[*Ts]) -> tuple[*Ts]: | ^^^^^^^^^^^^^^^^^ UP047 -17 | pass +17 | return ts | = help: Use type parameters ℹ Unsafe fix -13 13 | pass +13 13 | return t 14 14 | 15 15 | -16 |-def g(ts: tuple[*Ts]): - 16 |+def g[*Ts](ts: tuple[*Ts]): -17 17 | pass +16 |-def g(ts: tuple[*Ts]) -> tuple[*Ts]: + 16 |+def g[*Ts](ts: tuple[*Ts]) -> tuple[*Ts]: +17 17 | return ts 18 18 | 19 19 | @@ -46,14 +46,14 @@ UP047.py:20:5: UP047 [*] Generic function `h` should use type parameters 22 | | # Comment in the middle of a parameter list should be preserved 23 | | another_param, 24 | | and_another, -25 | | ): +25 | | ) -> Callable[P, T]: | |_^ UP047 -26 | pass +26 | return p | = help: Use type parameters ℹ Unsafe fix -17 17 | pass +17 17 | return ts 18 18 | 19 19 | 20 |-def h( @@ -64,19 +64,19 @@ UP047.py:20:5: UP047 [*] Generic function `h` should use type parameters UP047.py:29:5: UP047 [*] Generic function `i` should use type parameters | -29 | def i(s: S): +29 | def i(s: S) -> S: | ^^^^^^^ UP047 -30 | pass +30 | return s | = help: Use type parameters ℹ Unsafe fix -26 26 | pass +26 26 | return p 27 27 | 28 28 | -29 |-def i(s: S): - 29 |+def i[S: (str, bytes)](s: S): -30 30 | pass +29 |-def i(s: S) -> S: + 29 |+def i[S: (str, bytes)](s: S) -> S: +30 30 | return s 31 31 | 32 32 | @@ -84,9 +84,9 @@ UP047.py:39:5: UP047 [*] Generic function `broken_fix` should use type parameter | 37 | # TypeVars with the new-style generic syntax and will be rejected by type 38 | # checkers -39 | def broken_fix(okay: T, bad: Something): +39 | def broken_fix(okay: T, bad: Something) -> tuple[T, Something]: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP047 -40 | pass +40 | return (okay, bad) | = help: Use type parameters @@ -94,26 +94,26 @@ UP047.py:39:5: UP047 [*] Generic function `broken_fix` should use type parameter 36 36 | # generic type parameter. This leads to code that mixes old-style standalone 37 37 | # TypeVars with the new-style generic syntax and will be rejected by type 38 38 | # checkers -39 |-def broken_fix(okay: T, bad: Something): - 39 |+def broken_fix[T: float](okay: T, bad: Something): -40 40 | pass +39 |-def broken_fix(okay: T, bad: Something) -> tuple[T, Something]: + 39 |+def broken_fix[T: float](okay: T, bad: Something) -> tuple[T, Something]: +40 40 | return (okay, bad) 41 41 | 42 42 | UP047.py:43:5: UP047 [*] Generic function `any_str_param` should use type parameters | -43 | def any_str_param(s: AnyStr): +43 | def any_str_param(s: AnyStr) -> AnyStr: | ^^^^^^^^^^^^^^^^^^^^^^^^ UP047 -44 | pass +44 | return s | = help: Use type parameters ℹ Unsafe fix -40 40 | pass +40 40 | return (okay, bad) 41 41 | 42 42 | -43 |-def any_str_param(s: AnyStr): - 43 |+def any_str_param[AnyStr: (bytes, str)](s: AnyStr): -44 44 | pass +43 |-def any_str_param(s: AnyStr) -> AnyStr: + 43 |+def any_str_param[AnyStr: (bytes, str)](s: AnyStr) -> AnyStr: +44 44 | return s 45 45 | 46 46 | From 4500160b3fb8f66fef35832d4705774e2d73efd7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 08:48:27 -0500 Subject: [PATCH 65/77] fix imports, make Multiple -> MultipleBaseClasses --- .../test/fixtures/pyupgrade/UP046.py | 4 +- ...er__rules__pyupgrade__tests__UP046.py.snap | 152 +++++++++--------- 2 files changed, 79 insertions(+), 77 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 4a318793d3776f..7f003cf9626f83 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -1,5 +1,7 @@ from typing import Any, AnyStr, Generic, ParamSpec, TypeVar, TypeVarTuple +from somewhere import SupportsRichComparisonT + S = TypeVar("S", str, bytes) # constrained type variable T = TypeVar("T", bound=float) Ts = TypeVarTuple("Ts") @@ -43,7 +45,7 @@ class MultipleGenerics(Generic[S, T, Ts, P]): pep: P -class Multiple(NotGeneric, Generic[T]): +class MultipleBaseClasses(list, Generic[T]): var: T diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 058ec1fc4c3b91..4c6a34221b47f5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -2,125 +2,125 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP046.py:9:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046.py:11:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | - 9 | class A(Generic[T]): +11 | class A(Generic[T]): | ^^^^^^^^^^ UP046 -10 | # Comments in a class body are preserved -11 | var: T +12 | # Comments in a class body are preserved +13 | var: T | = help: Use type parameters ℹ Unsafe fix -6 6 | P = ParamSpec("P") -7 7 | -8 8 | -9 |-class A(Generic[T]): - 9 |+class A[T: float]: -10 10 | # Comments in a class body are preserved -11 11 | var: T -12 12 | +8 8 | P = ParamSpec("P") +9 9 | +10 10 | +11 |-class A(Generic[T]): + 11 |+class A[T: float]: +12 12 | # Comments in a class body are preserved +13 13 | var: T +14 14 | -UP046.py:14:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046.py:16:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | -14 | class B(Generic[*Ts]): +16 | class B(Generic[*Ts]): | ^^^^^^^^^^^^ UP046 -15 | var: tuple[*Ts] +17 | var: tuple[*Ts] | = help: Use type parameters ℹ Unsafe fix -11 11 | var: T -12 12 | -13 13 | -14 |-class B(Generic[*Ts]): - 14 |+class B[*Ts]: -15 15 | var: tuple[*Ts] -16 16 | -17 17 | +13 13 | var: T +14 14 | +15 15 | +16 |-class B(Generic[*Ts]): + 16 |+class B[*Ts]: +17 17 | var: tuple[*Ts] +18 18 | +19 19 | -UP046.py:18:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046.py:20:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | -18 | class C(Generic[P]): +20 | class C(Generic[P]): | ^^^^^^^^^^ UP046 -19 | var: P +21 | var: P | = help: Use type parameters ℹ Unsafe fix -15 15 | var: tuple[*Ts] -16 16 | -17 17 | -18 |-class C(Generic[P]): - 18 |+class C[**P]: -19 19 | var: P -20 20 | -21 21 | +17 17 | var: tuple[*Ts] +18 18 | +19 19 | +20 |-class C(Generic[P]): + 20 |+class C[**P]: +21 21 | var: P +22 22 | +23 23 | -UP046.py:22:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters +UP046.py:24:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters | -22 | class Constrained(Generic[S]): +24 | class Constrained(Generic[S]): | ^^^^^^^^^^ UP046 -23 | var: S +25 | var: S | = help: Use type parameters ℹ Unsafe fix -19 19 | var: P -20 20 | -21 21 | -22 |-class Constrained(Generic[S]): - 22 |+class Constrained[S: (str, bytes)]: -23 23 | var: S -24 24 | -25 25 | +21 21 | var: P +22 22 | +23 23 | +24 |-class Constrained(Generic[S]): + 24 |+class Constrained[S: (str, bytes)]: +25 25 | var: S +26 26 | +27 27 | -UP046.py:28:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters +UP046.py:30:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters | -26 | # This case gets a diagnostic but not a fix because we can't look up the bounds -27 | # or constraints on the generic type from another module -28 | class ExternalType(Generic[T, SupportsRichComparisonT]): +28 | # This case gets a diagnostic but not a fix because we can't look up the bounds +29 | # or constraints on the generic type from another module +30 | class ExternalType(Generic[T, SupportsRichComparisonT]): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP046 -29 | var: T -30 | compare: SupportsRichComparisonT +31 | var: T +32 | compare: SupportsRichComparisonT | = help: Use type parameters -UP046.py:35:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters +UP046.py:37:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters | -33 | # typing.AnyStr is a common external type variable, so treat it specially as a -34 | # known TypeVar -35 | class MyStr(Generic[AnyStr]): +35 | # typing.AnyStr is a common external type variable, so treat it specially as a +36 | # known TypeVar +37 | class MyStr(Generic[AnyStr]): | ^^^^^^^^^^^^^^^ UP046 -36 | s: AnyStr +38 | s: AnyStr | = help: Use type parameters ℹ Unsafe fix -32 32 | -33 33 | # typing.AnyStr is a common external type variable, so treat it specially as a -34 34 | # known TypeVar -35 |-class MyStr(Generic[AnyStr]): - 35 |+class MyStr[AnyStr: (bytes, str)]: -36 36 | s: AnyStr -37 37 | -38 38 | +34 34 | +35 35 | # typing.AnyStr is a common external type variable, so treat it specially as a +36 36 | # known TypeVar +37 |-class MyStr(Generic[AnyStr]): + 37 |+class MyStr[AnyStr: (bytes, str)]: +38 38 | s: AnyStr +39 39 | +40 40 | -UP046.py:39:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters +UP046.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters | -39 | class MultipleGenerics(Generic[S, T, Ts, P]): +41 | class MultipleGenerics(Generic[S, T, Ts, P]): | ^^^^^^^^^^^^^^^^^^^^ UP046 -40 | var: S -41 | typ: T +42 | var: S +43 | typ: T | = help: Use type parameters ℹ Unsafe fix -36 36 | s: AnyStr -37 37 | -38 38 | -39 |-class MultipleGenerics(Generic[S, T, Ts, P]): - 39 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: -40 40 | var: S -41 41 | typ: T -42 42 | tup: tuple[*Ts] +38 38 | s: AnyStr +39 39 | +40 40 | +41 |-class MultipleGenerics(Generic[S, T, Ts, P]): + 41 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: +42 42 | var: S +43 43 | typ: T +44 44 | tup: tuple[*Ts] From c6abe126530ff01e3f1819aff38933fed7cbb5bd Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 08:58:56 -0500 Subject: [PATCH 66/77] fix AnyStr handling Co-authored-by: Alex Waygood --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 99 ++++++++++--------- .../rules/pep695/use_pep695_type_alias.rs | 4 +- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 3de4228bcc5e63..de9a6407a5c29c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -28,7 +28,10 @@ enum TypeVarRestriction<'a> { /// A type variable with a bound, e.g., `TypeVar("T", bound=int)`. Bound(&'a Expr), /// A type variable with constraints, e.g., `TypeVar("T", int, str)`. - Constraint(Vec), + Constraint(Vec<&'a Expr>), + /// `AnyStr` is a special case: the only public `TypeVar` defined in the standard library, + /// and thus the only one that we recognise when imported from another module. + AnyStr, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -40,7 +43,7 @@ enum TypeParamKind { #[derive(Debug)] struct TypeVar<'a> { - name: &'a ExprName, + name: &'a str, restriction: Option>, kind: TypeParamKind, default: Option<&'a Expr>, @@ -92,23 +95,19 @@ impl Display for DisplayTypeVar<'_> { TypeParamKind::TypeVarTuple => f.write_str("*")?, TypeParamKind::ParamSpec => f.write_str("**")?, } - f.write_str(&self.type_var.name.id)?; + f.write_str(self.type_var.name)?; if let Some(restriction) = &self.type_var.restriction { f.write_str(": ")?; match restriction { TypeVarRestriction::Bound(bound) => { f.write_str(&self.source[bound.range()])?; } + TypeVarRestriction::AnyStr => f.write_str("(bytes, str)")?, TypeVarRestriction::Constraint(vec) => { let len = vec.len(); f.write_str("(")?; for (i, v) in vec.iter().enumerate() { - // typing.AnyStr special case doesn't have a real range - if let Expr::Name(name) = v { - f.write_str(name.id.as_ref())?; - } else { - f.write_str(&self.source[v.range()])?; - } + f.write_str(&self.source[v.range()])?; if i < len - 1 { f.write_str(", ")?; } @@ -135,7 +134,7 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { TypeParamKind::TypeVar => { TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), + name: Identifier::new(*name, TextRange::default()), bound: match restriction { Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())), Some(TypeVarRestriction::Constraint(constraints)) => { @@ -146,6 +145,25 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { parenthesized: true, }))) } + Some(TypeVarRestriction::AnyStr) => { + Some(Box::new(Expr::Tuple(ast::ExprTuple { + range: TextRange::default(), + elts: vec![ + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("str"), + ctx: ast::ExprContext::Load, + }), + Expr::Name(ExprName { + range: TextRange::default(), + id: Name::from("bytes"), + ctx: ast::ExprContext::Load, + }), + ], + ctx: ast::ExprContext::Load, + parenthesized: true, + }))) + } None => None, }, // We don't handle defaults here yet. Should perhaps be a different rule since @@ -155,12 +173,12 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { } TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), + name: Identifier::new(*name, TextRange::default()), default: None, }), TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { range: TextRange::default(), - name: Identifier::new(name.id.clone(), TextRange::default()), + name: Identifier::new(*name, TextRange::default()), default: None, }), } @@ -178,36 +196,27 @@ struct TypeVarReferenceVisitor<'a> { /// Recursively collects the names of type variable references present in an expression. impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { + // special case for typing.AnyStr, which is a commonly-imported type variable in the + // standard library with the definition: + // + // ```python + // AnyStr = TypeVar('AnyStr', bytes, str) + // ``` + // + // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be + // much to keep updated here. See + // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 + if self.semantic.match_typing_expr(expr, "AnyStr") { + self.vars.push(TypeVar { + name: "AnyStr", + restriction: Some(TypeVarRestriction::AnyStr), + kind: TypeParamKind::TypeVar, + default: None, + }); + return; + } + match expr { - // special case for typing.AnyStr, which is a commonly-imported type variable in the - // standard library with the definition: - // - // ```python - // AnyStr = TypeVar('AnyStr', bytes, str) - // ``` - // - // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be - // much to keep updated here. See - // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 - e @ Expr::Name(name) if self.semantic.match_typing_expr(e, "AnyStr") => { - self.vars.push(TypeVar { - name, - restriction: Some(TypeVarRestriction::Constraint(vec![ - Expr::Name(ExprName { - range: TextRange::default(), - id: Name::from("bytes"), - ctx: ruff_python_ast::ExprContext::Load, - }), - Expr::Name(ExprName { - range: TextRange::default(), - id: Name::from("str"), - ctx: ruff_python_ast::ExprContext::Load, - }), - ])), - kind: TypeParamKind::TypeVar, - default: None, - }); - } Expr::Name(name) if name.ctx.is_load() => { if let Some(var) = expr_name_to_type_var(self.semantic, name) { self.vars.push(var); @@ -243,7 +252,7 @@ fn expr_name_to_type_var<'a>( }) => { if semantic.match_typing_expr(subscript_value, "TypeVar") { return Some(TypeVar { - name, + name: &name.id, restriction: None, kind: TypeParamKind::TypeVar, default: None, @@ -288,14 +297,14 @@ fn expr_name_to_type_var<'a>( Some(TypeVarRestriction::Bound(&bound.value)) } else if arguments.args.len() > 1 { Some(TypeVarRestriction::Constraint( - arguments.args.iter().skip(1).cloned().collect(), + arguments.args.iter().skip(1).collect(), )) } else { None }; return Some(TypeVar { - name, + name: &name.id, restriction, kind, default, @@ -327,7 +336,7 @@ fn check_type_vars(vars: Vec>) -> Option>> { // found on the type parameters (vars .iter() - .unique_by(|tvar| &tvar.name.id) + .unique_by(|tvar| tvar.name) .filter(|tvar| tvar.default.is_none()) .count() == vars.len()) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index c2340c6090e73a..1fb63f8bee5ca5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -133,7 +133,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssig .map(|expr| { expr.as_name_expr().map(|name| { expr_name_to_type_var(checker.semantic(), name).unwrap_or(TypeVar { - name, + name: &name.id, restriction: None, kind: TypeParamKind::TypeVar, default: None, @@ -199,7 +199,7 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) // Type variables must be unique; filter while preserving order. let vars = vars .into_iter() - .unique_by(|TypeVar { name, .. }| name.id.as_str()) + .unique_by(|tvar| tvar.name) .collect::>(); checker.diagnostics.push(create_diagnostic( From 8bc0d9eb0c23df741e0f6095e0e7976e80904b22 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 09:18:43 -0500 Subject: [PATCH 67/77] add UP040 tests with `default` and bail early --- .../test/fixtures/pyupgrade/UP040.py | 10 +- .../rules/pep695/use_pep695_type_alias.rs | 5 + ...er__rules__pyupgrade__tests__UP040.py.snap | 191 +++++++++--------- 3 files changed, 110 insertions(+), 96 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py index e107f8da254947..337b9b2f1dac78 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py @@ -1,5 +1,5 @@ import typing -from typing import TypeAlias +from typing import Any, TypeAlias # UP040 x: typing.TypeAlias = int @@ -43,6 +43,10 @@ class Foo: T = typing.TypeVar(*args) x: typing.TypeAlias = list[T] +# `default` should be skipped for now, added in Python 3.13 +T = typing.TypeVar("T", default=Any) +x: typing.TypeAlias = list[T] + # OK x: TypeAlias x: int = 1 @@ -85,3 +89,7 @@ class Foo: PositiveList = TypeAliasType( "PositiveList2", list[Annotated[T, Gt(0)]], type_params=(T,) ) + +# `default` should be skipped for now, added in Python 3.13 +T = typing.TypeVar("T", default=Any) +AnyList = TypeAliasType("AnyList", list[T], typep_params=(T,)) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs index 1fb63f8bee5ca5..04843aea5f4700 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs @@ -202,6 +202,11 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) .unique_by(|tvar| tvar.name) .collect::>(); + // TODO(brent) handle `default` arg for Python 3.13+ + if vars.iter().any(|tv| tv.default.is_some()) { + return; + } + checker.diagnostics.push(create_diagnostic( checker.generator(), stmt.range(), diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index 0e68a81b0682d2..b4b16d21ff3c90 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +snapshot_kind: text --- UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword | @@ -11,7 +12,7 @@ UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of th = help: Use the `type` keyword ℹ Unsafe fix -2 2 | from typing import TypeAlias +2 2 | from typing import Any, TypeAlias 3 3 | 4 4 | # UP040 5 |-x: typing.TypeAlias = int @@ -216,7 +217,7 @@ UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 44 | x: typing.TypeAlias = list[T] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 45 | -46 | # OK +46 | # `default` should be skipped for now, added in Python 3.13 | = help: Use the `type` keyword @@ -227,135 +228,135 @@ UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t 44 |-x: typing.TypeAlias = list[T] 44 |+type x = list[T] 45 45 | -46 46 | # OK -47 47 | x: TypeAlias +46 46 | # `default` should be skipped for now, added in Python 3.13 +47 47 | T = typing.TypeVar("T", default=Any) -UP040.py:53:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword +UP040.py:57:1: UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `type` keyword | -51 | # type alias. -52 | T = typing.TypeVar["T"] -53 | Decorator: TypeAlias = typing.Callable[[T], T] +55 | # type alias. +56 | T = typing.TypeVar["T"] +57 | Decorator: TypeAlias = typing.Callable[[T], T] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 | = help: Use the `type` keyword ℹ Unsafe fix -50 50 | # Ensure that "T" appears only once in the type parameters for the modernized -51 51 | # type alias. -52 52 | T = typing.TypeVar["T"] -53 |-Decorator: TypeAlias = typing.Callable[[T], T] - 53 |+type Decorator[T] = typing.Callable[[T], T] -54 54 | -55 55 | -56 56 | from typing import TypeVar, Annotated, TypeAliasType - -UP040.py:63:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +54 54 | # Ensure that "T" appears only once in the type parameters for the modernized +55 55 | # type alias. +56 56 | T = typing.TypeVar["T"] +57 |-Decorator: TypeAlias = typing.Callable[[T], T] + 57 |+type Decorator[T] = typing.Callable[[T], T] +58 58 | +59 59 | +60 60 | from typing import TypeVar, Annotated, TypeAliasType + +UP040.py:67:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | -61 | # https://github.com/astral-sh/ruff/issues/11422 -62 | T = TypeVar("T") -63 | / PositiveList = TypeAliasType( -64 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) -65 | | ) +65 | # https://github.com/astral-sh/ruff/issues/11422 +66 | T = TypeVar("T") +67 | / PositiveList = TypeAliasType( +68 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) +69 | | ) | |_^ UP040 -66 | -67 | # Bound +70 | +71 | # Bound | = help: Use the `type` keyword ℹ Safe fix -60 60 | -61 61 | # https://github.com/astral-sh/ruff/issues/11422 -62 62 | T = TypeVar("T") -63 |-PositiveList = TypeAliasType( -64 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) -65 |-) - 63 |+type PositiveList[T] = list[Annotated[T, Gt(0)]] -66 64 | -67 65 | # Bound -68 66 | T = TypeVar("T", bound=SupportGt) - -UP040.py:69:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword +64 64 | +65 65 | # https://github.com/astral-sh/ruff/issues/11422 +66 66 | T = TypeVar("T") +67 |-PositiveList = TypeAliasType( +68 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) +69 |-) + 67 |+type PositiveList[T] = list[Annotated[T, Gt(0)]] +70 68 | +71 69 | # Bound +72 70 | T = TypeVar("T", bound=SupportGt) + +UP040.py:73:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword | -67 | # Bound -68 | T = TypeVar("T", bound=SupportGt) -69 | / PositiveList = TypeAliasType( -70 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) -71 | | ) +71 | # Bound +72 | T = TypeVar("T", bound=SupportGt) +73 | / PositiveList = TypeAliasType( +74 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) +75 | | ) | |_^ UP040 -72 | -73 | # Multiple bounds +76 | +77 | # Multiple bounds | = help: Use the `type` keyword ℹ Safe fix -66 66 | -67 67 | # Bound -68 68 | T = TypeVar("T", bound=SupportGt) -69 |-PositiveList = TypeAliasType( -70 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) -71 |-) - 69 |+type PositiveList[T: SupportGt] = list[Annotated[T, Gt(0)]] -72 70 | -73 71 | # Multiple bounds -74 72 | T1 = TypeVar("T1", bound=SupportGt) - -UP040.py:77:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword +70 70 | +71 71 | # Bound +72 72 | T = TypeVar("T", bound=SupportGt) +73 |-PositiveList = TypeAliasType( +74 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) +75 |-) + 73 |+type PositiveList[T: SupportGt] = list[Annotated[T, Gt(0)]] +76 74 | +77 75 | # Multiple bounds +78 76 | T1 = TypeVar("T1", bound=SupportGt) + +UP040.py:81:1: UP040 [*] Type alias `Tuple3` uses `TypeAliasType` assignment instead of the `type` keyword | -75 | T2 = TypeVar("T2") -76 | T3 = TypeVar("T3") -77 | Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) +79 | T2 = TypeVar("T2") +80 | T3 = TypeVar("T3") +81 | Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 -78 | -79 | # No type_params +82 | +83 | # No type_params | = help: Use the `type` keyword ℹ Safe fix -74 74 | T1 = TypeVar("T1", bound=SupportGt) -75 75 | T2 = TypeVar("T2") -76 76 | T3 = TypeVar("T3") -77 |-Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) - 77 |+type Tuple3[T1: SupportGt, T2, T3] = tuple[T1, T2, T3] -78 78 | -79 79 | # No type_params -80 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) - -UP040.py:80:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +78 78 | T1 = TypeVar("T1", bound=SupportGt) +79 79 | T2 = TypeVar("T2") +80 80 | T3 = TypeVar("T3") +81 |-Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) + 81 |+type Tuple3[T1: SupportGt, T2, T3] = tuple[T1, T2, T3] +82 82 | +83 83 | # No type_params +84 84 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) + +UP040.py:84:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | -79 | # No type_params -80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) +83 | # No type_params +84 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 -81 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) +85 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) | = help: Use the `type` keyword ℹ Safe fix -77 77 | Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) -78 78 | -79 79 | # No type_params -80 |-PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) - 80 |+type PositiveInt = Annotated[int, Gt(0)] -81 81 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) +81 81 | Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3)) 82 82 | -83 83 | # OK: Other name - -UP040.py:81:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword +83 83 | # No type_params +84 |-PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) + 84 |+type PositiveInt = Annotated[int, Gt(0)] +85 85 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) +86 86 | +87 87 | # OK: Other name + +UP040.py:85:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignment instead of the `type` keyword | -79 | # No type_params -80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) -81 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) +83 | # No type_params +84 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) +85 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 -82 | -83 | # OK: Other name +86 | +87 | # OK: Other name | = help: Use the `type` keyword ℹ Safe fix -78 78 | -79 79 | # No type_params -80 80 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) -81 |-PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) - 81 |+type PositiveInt = Annotated[int, Gt(0)] 82 82 | -83 83 | # OK: Other name -84 84 | T = TypeVar("T", bound=SupportGt) +83 83 | # No type_params +84 84 | PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)]) +85 |-PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=()) + 85 |+type PositiveInt = Annotated[int, Gt(0)] +86 86 | +87 87 | # OK: Other name +88 88 | T = TypeVar("T", bound=SupportGt) From bfdb491572651ee51ffb1aaa2ec13dc08afa2c4a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 09:37:00 -0500 Subject: [PATCH 68/77] check that AnyStr constraints are bound to builtins --- .../resources/test/fixtures/pyupgrade/UP046.py | 11 +++++++++++ .../src/rules/pyupgrade/rules/pep695/mod.rs | 8 +++++++- ...uff_linter__rules__pyupgrade__tests__UP046.py.snap | 8 ++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py index 7f003cf9626f83..c42318dc4a0492 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py @@ -88,3 +88,14 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any] class Outer: class Inner(Generic[T]): var: T + + +# replacing AnyStr requires specifying the constraints `str` and `bytes`, so it +# can't be replaced if these have been shadowed. this test should stay at the +# bottom of the file because it doesn't seem possible to restore `str` to its +# builtin state +str = "string" + + +class BadStr(Generic[AnyStr]): + var: AnyStr diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index de9a6407a5c29c..5a97ab93220978 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -206,7 +206,13 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { // As of 01/2025, this line hasn't been modified in 8 years, so hopefully there won't be // much to keep updated here. See // https://github.com/python/cpython/blob/383af395af828f40d9543ee0a8fdc5cc011d43db/Lib/typing.py#L2806 - if self.semantic.match_typing_expr(expr, "AnyStr") { + // + // to replace AnyStr with an annotation like [AnyStr: (bytes, str)], we also have to make + // sure that `bytes` and `str` have their builtin values and have not been shadowed + if self.semantic.match_typing_expr(expr, "AnyStr") + && self.semantic.has_builtin_binding("bytes") + && self.semantic.has_builtin_binding("str") + { self.vars.push(TypeVar { name: "AnyStr", restriction: Some(TypeVarRestriction::AnyStr), diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap index 4c6a34221b47f5..0c2e6de76fe1ea 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap @@ -124,3 +124,11 @@ UP046.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subcla 42 42 | var: S 43 43 | typ: T 44 44 | tup: tuple[*Ts] + +UP046.py:100:14: UP046 Generic class `BadStr` uses `Generic` subclass instead of type parameters + | +100 | class BadStr(Generic[AnyStr]): + | ^^^^^^^^^^^^^^^ UP046 +101 | var: AnyStr + | + = help: Use type parameters From c9753858f412e1de0fad6f9c1f55a0d32f7398d4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 09:43:37 -0500 Subject: [PATCH 69/77] add note on type checkers --- .../rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs | 3 +++ .../pyupgrade/rules/pep695/use_pep695_generic_function.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs index bdc2a98e62e356..3a46fa15214760 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs @@ -30,6 +30,9 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// in the current module. For external type parameters, a diagnostic is emitted without a suggested /// fix. /// +/// Not all type checkers fully support PEP 695 yet, so even valid fixes suggested by this rule may +/// cause type checking to fail. +/// /// ## Fix safety /// /// This fix is marked as unsafe, as [PEP 695] uses inferred variance for type parameters, instead diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs index 0654f4d60f1eec..f1109dd8a0f275 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs @@ -24,6 +24,9 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// with type parameters containing the `default` argument introduced in [PEP 696] and implemented /// in Python 3.13. /// +/// Not all type checkers fully support PEP 695 yet, so even valid fixes suggested by this rule may +/// cause type checking to fail. +/// /// ## Fix safety /// /// This fix is marked unsafe, as [PEP 695] uses inferred variance for type parameters, instead of From f3e669337b45ae063a3c381bf76d9ce2a969320d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 09:44:37 -0500 Subject: [PATCH 70/77] update module comment --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 5a97ab93220978..e7cf9039df2605 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -1,4 +1,6 @@ -//! Shared code for [`use_pep695_type_alias`] (UP040) and [`use_pep695_type_parameter`] (UP046) +//! Shared code for [`use_pep695_type_alias`] (UP040), +//! [`use_pep695_generic_class`] (UP046), and [`use_pep695_generic_function`] +//! (UP047) use std::fmt::Display; From e2c92ffc247ba0f77a46fda9cc387c6a1e7e613b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:05:16 -0500 Subject: [PATCH 71/77] move str shadow test to separate fixture --- .../pyupgrade/{UP046.py => UP046_0.py} | 11 ---------- .../test/fixtures/pyupgrade/UP046_1.py | 12 ++++++++++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 3 ++- ..._rules__pyupgrade__tests__UP046_0.py.snap} | 22 ++++++------------- ...__rules__pyupgrade__tests__UP046_1.py.snap | 11 ++++++++++ 5 files changed, 32 insertions(+), 27 deletions(-) rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP046.py => UP046_0.py} (87%) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_1.py rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP046.py.snap => ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap} (73%) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_1.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py similarity index 87% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py index c42318dc4a0492..7f003cf9626f83 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py @@ -88,14 +88,3 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any] class Outer: class Inner(Generic[T]): var: T - - -# replacing AnyStr requires specifying the constraints `str` and `bytes`, so it -# can't be replaced if these have been shadowed. this test should stay at the -# bottom of the file because it doesn't seem possible to restore `str` to its -# builtin state -str = "string" - - -class BadStr(Generic[AnyStr]): - var: AnyStr diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_1.py new file mode 100644 index 00000000000000..95aaf734606d15 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_1.py @@ -0,0 +1,12 @@ +"""Replacing AnyStr requires specifying the constraints `bytes` and `str`, so +it can't be replaced if these have been shadowed. This test is in a separate +fixture because it doesn't seem possible to restore `str` to its builtin state +""" + +from typing import AnyStr, Generic + +str = "string" + + +class BadStr(Generic[AnyStr]): + var: AnyStr diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 6b7256bba8e079..f10421e4ffaeef 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -103,7 +103,8 @@ mod tests { #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] - #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046.py"))] + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))] + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))] #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap similarity index 73% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap index 0c2e6de76fe1ea..8508f0bdf04d46 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs snapshot_kind: text --- -UP046.py:11:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters +UP046_0.py:11:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | 11 | class A(Generic[T]): | ^^^^^^^^^^ UP046 @@ -21,7 +21,7 @@ UP046.py:11:9: UP046 [*] Generic class `A` uses `Generic` subclass instead of ty 13 13 | var: T 14 14 | -UP046.py:16:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters +UP046_0.py:16:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of type parameters | 16 | class B(Generic[*Ts]): | ^^^^^^^^^^^^ UP046 @@ -39,7 +39,7 @@ UP046.py:16:9: UP046 [*] Generic class `B` uses `Generic` subclass instead of ty 18 18 | 19 19 | -UP046.py:20:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters +UP046_0.py:20:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of type parameters | 20 | class C(Generic[P]): | ^^^^^^^^^^ UP046 @@ -57,7 +57,7 @@ UP046.py:20:9: UP046 [*] Generic class `C` uses `Generic` subclass instead of ty 22 22 | 23 23 | -UP046.py:24:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters +UP046_0.py:24:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass instead of type parameters | 24 | class Constrained(Generic[S]): | ^^^^^^^^^^ UP046 @@ -75,7 +75,7 @@ UP046.py:24:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass in 26 26 | 27 27 | -UP046.py:30:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters +UP046_0.py:30:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters | 28 | # This case gets a diagnostic but not a fix because we can't look up the bounds 29 | # or constraints on the generic type from another module @@ -86,7 +86,7 @@ UP046.py:30:20: UP046 Generic class `ExternalType` uses `Generic` subclass inste | = help: Use type parameters -UP046.py:37:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters +UP046_0.py:37:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead of type parameters | 35 | # typing.AnyStr is a common external type variable, so treat it specially as a 36 | # known TypeVar @@ -106,7 +106,7 @@ UP046.py:37:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instead 39 39 | 40 40 | -UP046.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters +UP046_0.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters | 41 | class MultipleGenerics(Generic[S, T, Ts, P]): | ^^^^^^^^^^^^^^^^^^^^ UP046 @@ -124,11 +124,3 @@ UP046.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subcla 42 42 | var: S 43 43 | typ: T 44 44 | tup: tuple[*Ts] - -UP046.py:100:14: UP046 Generic class `BadStr` uses `Generic` subclass instead of type parameters - | -100 | class BadStr(Generic[AnyStr]): - | ^^^^^^^^^^^^^^^ UP046 -101 | var: AnyStr - | - = help: Use type parameters diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_1.py.snap new file mode 100644 index 00000000000000..95de85d97b9913 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_1.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +snapshot_kind: text +--- +UP046_1.py:11:14: UP046 Generic class `BadStr` uses `Generic` subclass instead of type parameters + | +11 | class BadStr(Generic[AnyStr]): + | ^^^^^^^^^^^^^^^ UP046 +12 | var: AnyStr + | + = help: Use type parameters From fa1007a9ba2918f45fbd71fb14bf606ffedde15c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:35:28 -0500 Subject: [PATCH 72/77] clarify comment Co-authored-by: Alex Waygood --- crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py index 7f003cf9626f83..b59c40e3e0c03e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py @@ -26,7 +26,7 @@ class Constrained(Generic[S]): # This case gets a diagnostic but not a fix because we can't look up the bounds -# or constraints on the generic type from another module +# or constraints on the TypeVar imported from another module class ExternalType(Generic[T, SupportsRichComparisonT]): var: T compare: SupportsRichComparisonT From 76d03b235b6328b4f8f74beb7c2eb37a411cb6ab Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:39:10 -0500 Subject: [PATCH 73/77] fix tuple Co-authored-by: Alex Waygood --- crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py index b59c40e3e0c03e..6af12c47f0df9d 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py @@ -38,7 +38,7 @@ class MyStr(Generic[AnyStr]): s: AnyStr -class MultipleGenerics(Generic[S, T, Ts, P]): +class MultipleGenerics(Generic[S, T, *Ts, P]): var: S typ: T tup: tuple[*Ts] From e47f47879089ed2256625a33476a66b9126a4fac Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:42:46 -0500 Subject: [PATCH 74/77] update snapshots --- .../ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap index 8508f0bdf04d46..2b643d7ecd8aec 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap @@ -78,7 +78,7 @@ UP046_0.py:24:19: UP046 [*] Generic class `Constrained` uses `Generic` subclass UP046_0.py:30:20: UP046 Generic class `ExternalType` uses `Generic` subclass instead of type parameters | 28 | # This case gets a diagnostic but not a fix because we can't look up the bounds -29 | # or constraints on the generic type from another module +29 | # or constraints on the TypeVar imported from another module 30 | class ExternalType(Generic[T, SupportsRichComparisonT]): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP046 31 | var: T @@ -108,8 +108,8 @@ UP046_0.py:37:13: UP046 [*] Generic class `MyStr` uses `Generic` subclass instea UP046_0.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subclass instead of type parameters | -41 | class MultipleGenerics(Generic[S, T, Ts, P]): - | ^^^^^^^^^^^^^^^^^^^^ UP046 +41 | class MultipleGenerics(Generic[S, T, *Ts, P]): + | ^^^^^^^^^^^^^^^^^^^^^ UP046 42 | var: S 43 | typ: T | @@ -119,7 +119,7 @@ UP046_0.py:41:24: UP046 [*] Generic class `MultipleGenerics` uses `Generic` subc 38 38 | s: AnyStr 39 39 | 40 40 | -41 |-class MultipleGenerics(Generic[S, T, Ts, P]): +41 |-class MultipleGenerics(Generic[S, T, *Ts, P]): 41 |+class MultipleGenerics[S: (str, bytes), T: float, *Ts, **P]: 42 42 | var: S 43 43 | typ: T From 196b9057216f4229ce796fe176dbde31958b5595 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:42:03 -0500 Subject: [PATCH 75/77] update module names to match rule names --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 12 ++++++------ ..._generic_class.rs => non_pep695_generic_class.rs} | 0 ...ic_function.rs => non_pep695_generic_function.rs} | 0 ...pep695_type_alias.rs => non_pep695_type_alias.rs} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename crates/ruff_linter/src/rules/pyupgrade/rules/pep695/{use_pep695_generic_class.rs => non_pep695_generic_class.rs} (100%) rename crates/ruff_linter/src/rules/pyupgrade/rules/pep695/{use_pep695_generic_function.rs => non_pep695_generic_function.rs} (100%) rename crates/ruff_linter/src/rules/pyupgrade/rules/pep695/{use_pep695_type_alias.rs => non_pep695_type_alias.rs} (100%) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index e7cf9039df2605..70dd40c95d9469 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -15,15 +15,15 @@ use ruff_python_ast::{ use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; -pub(crate) use use_pep695_generic_class::*; -pub(crate) use use_pep695_generic_function::*; -pub(crate) use use_pep695_type_alias::*; +pub(crate) use non_pep695_generic_class::*; +pub(crate) use non_pep695_generic_function::*; +pub(crate) use non_pep695_type_alias::*; use crate::checkers::ast::Checker; -mod use_pep695_generic_class; -mod use_pep695_generic_function; -mod use_pep695_type_alias; +mod non_pep695_generic_class; +mod non_pep695_generic_function; +mod non_pep695_type_alias; #[derive(Debug)] enum TypeVarRestriction<'a> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs similarity index 100% rename from crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_class.rs rename to crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs similarity index 100% rename from crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_generic_function.rs rename to crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs similarity index 100% rename from crates/ruff_linter/src/rules/pyupgrade/rules/pep695/use_pep695_type_alias.rs rename to crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs From 35a1c7e2d30d39a26f233ad4c40325c613f8727d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 10:45:56 -0500 Subject: [PATCH 76/77] simplify with ? Co-authored-by: Alex Waygood --- .../src/rules/pyupgrade/rules/pep695/mod.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 70dd40c95d9469..1847ac98e0e915 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -241,17 +241,11 @@ fn expr_name_to_type_var<'a>( semantic: &'a SemanticModel, name: &'a ExprName, ) -> Option> { - let Some(Stmt::Assign(StmtAssign { value, .. })) = semantic + let StmtAssign { value, .. } = semantic .lookup_symbol(name.id.as_str()) - .and_then(|binding_id| { - semantic - .binding(binding_id) - .source - .map(|node_id| semantic.statement(node_id)) - }) - else { - return None; - }; + .and_then(|binding_id| semantic.binding(binding_id).source) + .map(|node_id| semantic.statement(node_id))? + .as_assign_stmt()?; match value.as_ref() { Expr::Subscript(ExprSubscript { From 971cb09da326db15f4b97f52074f99c4fd144dc0 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 22 Jan 2025 11:10:12 -0500 Subject: [PATCH 77/77] fix module names in docs Co-authored-by: Alex Waygood --- crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 1847ac98e0e915..860f2d60dcf819 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -1,5 +1,5 @@ -//! Shared code for [`use_pep695_type_alias`] (UP040), -//! [`use_pep695_generic_class`] (UP046), and [`use_pep695_generic_function`] +//! Shared code for [`non_pep695_type_alias`] (UP040), +//! [`non_pep695_generic_class`] (UP046), and [`non_pep695_generic_function`] //! (UP047) use std::fmt::Display;