-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[
pyupgrade
] Classes with mixed type variable style (UP050
)
- Loading branch information
1 parent
172f62d
commit 66916ae
Showing
9 changed files
with
648 additions
and
19 deletions.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from typing import Generic, ParamSpec, TypeVar, TypeVarTuple | ||
|
||
|
||
_A = TypeVar('_A') | ||
_B = TypeVar('_B', bound=int) | ||
_C = TypeVar('_C', str, bytes) | ||
_D = TypeVar('_D', default=int) | ||
_E = TypeVar('_E', bound=int, default=int) | ||
_F = TypeVar('_F', str, bytes, default=str) | ||
|
||
_As = TypeVarTuple('_As') | ||
_Bs = TypeVarTuple('_Bs', bound=tuple[int, str]) | ||
_Cs = TypeVarTuple('_Cs', default=tuple[int, str]) | ||
|
||
|
||
_P1 = ParamSpec('_P1') | ||
_P2 = ParamSpec('_P2', infer_variance=True) | ||
_P3 = ParamSpec('_P3', default=[int, str]) | ||
|
||
|
||
### Errors | ||
|
||
class C[T](Generic[_A]): ... | ||
class C[T](Generic[_B], str): ... | ||
class C[T](int, Generic[_C]): ... | ||
class C[T](bytes, Generic[_D], bool): ... | ||
class C[T](Generic[_E], list[_E]): ... | ||
class C[T](list[_F], Generic[_F]): ... | ||
|
||
class C[*Ts](Generic[_As]): ... | ||
class C[*Ts](Generic[_Bs], tuple[*Bs]): ... | ||
class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... | ||
|
||
|
||
class C[**P](Generic[_P1]): ... | ||
class C[**P](Generic[_P2]): ... | ||
class C[**P](Generic[_P3]): ... | ||
|
||
|
||
class C[T](Generic[T, _A]): ... | ||
|
||
|
||
# See `is_existing_param_of_same_class` | ||
# `expr_name_to_type_var` doesn't handle named expressions, | ||
# only simple assignments, so there is no fix. | ||
class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ... | ||
|
||
|
||
class C(Generic[_B]): | ||
class D[T](Generic[_B, T]): ... | ||
|
||
|
||
class C[T]: | ||
class D[U](Generic[T, U]): ... | ||
|
||
|
||
class C[T](Generic[_C], Generic[_D]): ... | ||
|
||
|
||
class C[ | ||
T # Comment | ||
](Generic[_E]): ... | ||
|
||
|
||
### No errors | ||
|
||
class C(Generic[_A]): ... | ||
class C[_A]: ... | ||
class C[_A](list[_A]): ... | ||
class C[_A](list[Generic[_A]]): ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
crates/ruff_linter/src/rules/pyupgrade/rules/pep695/class_with_mixed_type_vars.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
use std::iter; | ||
|
||
use crate::checkers::ast::Checker; | ||
use crate::fix::edits::{remove_argument, Parentheses}; | ||
use crate::rules::pyupgrade::rules::pep695::{ | ||
expr_name_to_type_var, find_generic, DisplayTypeVars, TypeParamKind, TypeVar, | ||
}; | ||
use crate::settings::types::PythonVersion; | ||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, ViolationMetadata}; | ||
use ruff_python_ast::{Arguments, Expr, ExprSubscript, ExprTuple, StmtClassDef, TypeParams}; | ||
use ruff_python_semantic::{Binding, BindingKind, SemanticModel}; | ||
|
||
/// ## What it does | ||
/// Checks for classes that both have PEP 695 type parameter list | ||
/// and inherit from `typing.Generic` or `typing_extensions.Generic`. | ||
/// | ||
/// ## Why is this bad? | ||
/// Such classes cause errors at runtime: | ||
/// | ||
/// ```python | ||
/// from typing import Generic, TypeVar | ||
/// | ||
/// U = TypeVar("U") | ||
/// | ||
/// # TypeError: Cannot inherit from Generic[...] multiple times. | ||
/// class C[T](Generic[U]): ... | ||
/// ``` | ||
/// | ||
/// ## Example | ||
/// | ||
/// ```python | ||
/// from typing import Generic, ParamSpec, TypeVar, TypeVarTuple | ||
/// | ||
/// U = TypeVar("U") | ||
/// P = ParamSpec("P") | ||
/// Ts = TypeVarTuple("Ts") | ||
/// | ||
/// class C[T](Generic[U, P, Ts]): ... | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// | ||
/// ```python | ||
/// class C[T, U, **P, *Ts]: ... | ||
/// ``` | ||
#[derive(ViolationMetadata)] | ||
pub(crate) struct ClassWithMixedTypeVars; | ||
|
||
impl Violation for ClassWithMixedTypeVars { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
|
||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
"Class with type parameter list inherits from `Generic`".to_string() | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
Some("Convert to new-style".to_string()) | ||
} | ||
} | ||
|
||
/// UP050 | ||
pub(crate) fn class_with_mixed_type_vars(checker: &mut Checker, class_def: &StmtClassDef) { | ||
if checker.settings.target_version < PythonVersion::Py312 { | ||
return; | ||
} | ||
|
||
let semantic = checker.semantic(); | ||
let StmtClassDef { | ||
type_params, | ||
arguments, | ||
.. | ||
} = class_def; | ||
|
||
let Some(type_params) = type_params.as_deref() else { | ||
return; | ||
}; | ||
|
||
let Some(arguments) = arguments else { | ||
return; | ||
}; | ||
|
||
let Some((generic_base, old_style_type_vars)) = | ||
typing_generic_base_and_arguments(arguments, semantic) | ||
else { | ||
return; | ||
}; | ||
|
||
let mut diagnostic = Diagnostic::new(ClassWithMixedTypeVars, generic_base.range); | ||
|
||
if let Some(fix) = convert_type_vars( | ||
generic_base, | ||
old_style_type_vars, | ||
type_params, | ||
arguments, | ||
checker, | ||
) { | ||
diagnostic.set_fix(fix); | ||
} | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} | ||
|
||
fn typing_generic_base_and_arguments<'a>( | ||
class_arguments: &'a Arguments, | ||
semantic: &SemanticModel, | ||
) -> Option<(&'a ExprSubscript, &'a Expr)> { | ||
let (_, base @ ExprSubscript { slice, .. }) = find_generic(class_arguments, semantic)?; | ||
|
||
Some((base, slice.as_ref())) | ||
} | ||
|
||
fn convert_type_vars( | ||
generic_base: &ExprSubscript, | ||
old_style_type_vars: &Expr, | ||
type_params: &TypeParams, | ||
class_arguments: &Arguments, | ||
checker: &Checker, | ||
) -> Option<Fix> { | ||
let mut type_vars = type_params | ||
.type_params | ||
.iter() | ||
.map(|param| TypeVar::from(param)) | ||
.collect::<Vec<_>>(); | ||
|
||
let mut converted_type_vars = match old_style_type_vars { | ||
expr @ Expr::Name(_) => { | ||
generic_arguments_to_type_vars(iter::once(expr), type_params, checker)? | ||
} | ||
Expr::Tuple(ExprTuple { elts, .. }) => { | ||
generic_arguments_to_type_vars(elts.iter(), type_params, checker)? | ||
} | ||
_ => return None, | ||
}; | ||
|
||
type_vars.append(&mut converted_type_vars); | ||
|
||
let source = checker.source(); | ||
let new_type_params = DisplayTypeVars { | ||
type_vars: &type_vars, | ||
source, | ||
}; | ||
|
||
let remove_generic_base = | ||
remove_argument(generic_base, class_arguments, Parentheses::Remove, source).ok()?; | ||
let replace_type_params = | ||
Edit::range_replacement(new_type_params.to_string(), type_params.range); | ||
|
||
Some(Fix::unsafe_edits( | ||
remove_generic_base, | ||
[replace_type_params], | ||
)) | ||
} | ||
|
||
fn generic_arguments_to_type_vars<'a>( | ||
exprs: impl Iterator<Item = &'a Expr>, | ||
existing_type_params: &TypeParams, | ||
checker: &'a Checker, | ||
) -> Option<Vec<TypeVar<'a>>> { | ||
let is_existing_param_of_same_class = |binding: &Binding| { | ||
// This first check should have been unnecessary, | ||
// as a type parameter list can only contains type-parameter bindings. | ||
// Named expressions, for example, are syntax errors. | ||
// However, Ruff doesn't know that yet (#11118), | ||
// so here it shall remain. | ||
matches!(binding.kind, BindingKind::TypeParam) | ||
&& existing_type_params.range.contains_range(binding.range) | ||
}; | ||
|
||
let semantic = checker.semantic(); | ||
let target_version = checker.settings.target_version; | ||
|
||
let mut type_vars = vec![]; | ||
|
||
for expr in exprs { | ||
let name = expr.as_name_expr()?; | ||
let binding = semantic.only_binding(name).map(|id| semantic.binding(id))?; | ||
|
||
if is_existing_param_of_same_class(binding) { | ||
continue; | ||
} | ||
|
||
let type_var = expr_name_to_type_var(semantic, name)?; | ||
|
||
match (&type_var.kind, &type_var.restriction) { | ||
(TypeParamKind::TypeVarTuple, Some(_)) => return None, | ||
(TypeParamKind::ParamSpec, Some(_)) => return None, | ||
_ => {} | ||
} | ||
|
||
if type_var.default.is_some() && target_version < PythonVersion::Py313 { | ||
return None; | ||
} | ||
|
||
type_vars.push(type_var); | ||
} | ||
|
||
Some(type_vars) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.