Enable automatic rewrites of typing.Deque and typing.DefaultDict (#4420)

This commit is contained in:
Charlie Marsh 2023-05-15 18:33:24 -04:00 committed by GitHub
parent 838ba1ca3d
commit 2414469ac3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 370 additions and 212 deletions

View file

@ -56,3 +56,11 @@ def f(x: "List['Li' 'st[str]']") -> None:
def f(x: "Li" "st['List[str]']") -> None: def f(x: "Li" "st['List[str]']") -> None:
... ...
def f(x: typing.Deque[str]) -> None:
...
def f(x: typing.DefaultDict[str, str]) -> None:
...

View file

@ -2257,26 +2257,40 @@ where
match &expr.node { match &expr.node {
ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => { ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => {
// Ex) Optional[...], Union[...] // Ex) Optional[...], Union[...]
if self if self.settings.rules.any_enabled(&[
.settings Rule::MissingFutureAnnotationsImport,
.rules Rule::NonPEP604Annotation,
.enabled(Rule::MissingFutureAnnotationsImport) ]) {
&& (self.settings.target_version < PythonVersion::Py310 if let Some(operator) =
&& (self.settings.target_version >= PythonVersion::Py37 analyze::typing::to_pep604_operator(value, slice, &self.ctx)
&& !self.ctx.future_annotations() {
&& self.ctx.in_annotation())) if self
&& analyze::typing::is_pep604_builtin(value, &self.ctx) .settings
{ .rules
flake8_future_annotations::rules::missing_future_annotations(self, value); .enabled(Rule::MissingFutureAnnotationsImport)
} {
if self.settings.rules.enabled(Rule::NonPEP604Annotation) if self.settings.target_version < PythonVersion::Py310
&& (self.settings.target_version >= PythonVersion::Py310 && self.settings.target_version >= PythonVersion::Py37
|| (self.settings.target_version >= PythonVersion::Py37 && !self.ctx.future_annotations()
&& self.ctx.future_annotations() && self.ctx.in_annotation()
&& self.ctx.in_annotation())) {
&& analyze::typing::is_pep604_builtin(value, &self.ctx) flake8_future_annotations::rules::missing_future_annotations(
{ self, value,
pyupgrade::rules::use_pep604_annotation(self, expr, value, slice); );
}
}
if self.settings.rules.enabled(Rule::NonPEP604Annotation) {
if self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.ctx.future_annotations()
&& self.ctx.in_annotation())
{
pyupgrade::rules::use_pep604_annotation(
self, expr, slice, operator,
);
}
}
}
} }
if self.ctx.match_typing_expr(value, "Literal") { if self.ctx.match_typing_expr(value, "Literal") {
@ -2332,28 +2346,42 @@ where
} }
// Ex) List[...] // Ex) List[...]
if self if self.settings.rules.any_enabled(&[
.settings Rule::MissingFutureAnnotationsImport,
.rules Rule::NonPEP585Annotation,
.enabled(Rule::MissingFutureAnnotationsImport) ]) {
&& (self.settings.target_version < PythonVersion::Py39 if let Some(replacement) =
&& (self.settings.target_version >= PythonVersion::Py37 analyze::typing::to_pep585_generic(expr, &self.ctx)
&& !self.ctx.future_annotations() {
&& self.ctx.in_annotation())) if self
&& analyze::typing::is_pep585_builtin(expr, &self.ctx) .settings
{ .rules
flake8_future_annotations::rules::missing_future_annotations( .enabled(Rule::MissingFutureAnnotationsImport)
self, expr, {
); if self.settings.target_version < PythonVersion::Py39
} && self.settings.target_version >= PythonVersion::Py37
if self.settings.rules.enabled(Rule::NonPEP585Annotation) && !self.ctx.future_annotations()
&& (self.settings.target_version >= PythonVersion::Py39 && self.ctx.in_annotation()
|| (self.settings.target_version >= PythonVersion::Py37 {
&& self.ctx.future_annotations() flake8_future_annotations::rules::missing_future_annotations(
&& self.ctx.in_annotation())) self, expr,
&& analyze::typing::is_pep585_builtin(expr, &self.ctx) );
{ }
pyupgrade::rules::use_pep585_annotation(self, expr); }
if self.settings.rules.enabled(Rule::NonPEP585Annotation) {
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.ctx.future_annotations()
&& self.ctx.in_annotation())
{
pyupgrade::rules::use_pep585_annotation(
self,
expr,
&replacement,
);
}
}
}
} }
self.handle_node_load(expr); self.handle_node_load(expr);
@ -2396,26 +2424,36 @@ where
} }
ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => { ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => {
// Ex) typing.List[...] // Ex) typing.List[...]
if self if self.settings.rules.any_enabled(&[
.settings Rule::MissingFutureAnnotationsImport,
.rules Rule::NonPEP585Annotation,
.enabled(Rule::MissingFutureAnnotationsImport) ]) {
&& (self.settings.target_version < PythonVersion::Py39 if let Some(replacement) = analyze::typing::to_pep585_generic(expr, &self.ctx) {
&& (self.settings.target_version >= PythonVersion::Py37 if self
&& !self.ctx.future_annotations() .settings
&& self.ctx.in_annotation())) .rules
&& analyze::typing::is_pep585_builtin(expr, &self.ctx) .enabled(Rule::MissingFutureAnnotationsImport)
{ {
flake8_future_annotations::rules::missing_future_annotations(self, expr); if self.settings.target_version < PythonVersion::Py39
} && self.settings.target_version >= PythonVersion::Py37
if self.settings.rules.enabled(Rule::NonPEP585Annotation) && !self.ctx.future_annotations()
&& (self.settings.target_version >= PythonVersion::Py39 && self.ctx.in_annotation()
|| (self.settings.target_version >= PythonVersion::Py37 {
&& self.ctx.future_annotations() flake8_future_annotations::rules::missing_future_annotations(
&& self.ctx.in_annotation())) self, expr,
&& analyze::typing::is_pep585_builtin(expr, &self.ctx) );
{ }
pyupgrade::rules::use_pep585_annotation(self, expr); }
if self.settings.rules.enabled(Rule::NonPEP585Annotation) {
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.ctx.future_annotations()
&& self.ctx.in_annotation())
{
pyupgrade::rules::use_pep585_annotation(self, expr, &replacement);
}
}
}
} }
if self.settings.rules.enabled(Rule::DatetimeTimezoneUTC) if self.settings.rules.enabled(Rule::DatetimeTimezoneUTC)
&& self.settings.target_version >= PythonVersion::Py311 && self.settings.target_version >= PythonVersion::Py311

View file

@ -2,13 +2,17 @@ use rustpython_parser::ast::Expr;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::compose_call_path;
use ruff_python_semantic::analyze::typing::ModuleMember;
use crate::autofix::actions::get_or_import_symbol;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::AsRule; use crate::registry::AsRule;
#[violation] #[violation]
pub struct NonPEP585Annotation { pub struct NonPEP585Annotation {
name: String, from: String,
to: String,
} }
impl Violation for NonPEP585Annotation { impl Violation for NonPEP585Annotation {
@ -16,44 +20,60 @@ impl Violation for NonPEP585Annotation {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let NonPEP585Annotation { name } = self; let NonPEP585Annotation { from, to } = self;
format!( format!("Use `{to}` instead of `{from}` for type annotation")
"Use `{}` instead of `{}` for type annotations",
name.to_lowercase(),
name,
)
} }
fn autofix_title(&self) -> Option<String> { fn autofix_title(&self) -> Option<String> {
let NonPEP585Annotation { name } = self; let NonPEP585Annotation { to, .. } = self;
Some(format!("Replace `{name}` with `{}`", name.to_lowercase())) Some(format!("Replace with `{to}`"))
} }
} }
/// UP006 /// UP006
pub(crate) fn use_pep585_annotation(checker: &mut Checker, expr: &Expr) { pub(crate) fn use_pep585_annotation(
if let Some(binding) = checker checker: &mut Checker,
.ctx expr: &Expr,
.resolve_call_path(expr) replacement: &ModuleMember,
.and_then(|call_path| call_path.last().copied()) ) {
{ let Some(from) = compose_call_path(expr) else {
let fixable = !checker.ctx.in_complex_string_type_definition(); return;
let mut diagnostic = Diagnostic::new( };
NonPEP585Annotation { let mut diagnostic = Diagnostic::new(
name: binding.to_string(), NonPEP585Annotation {
}, from,
expr.range(), to: replacement.to_string(),
); },
if fixable && checker.patch(diagnostic.kind.rule()) { expr.range(),
let binding = binding.to_lowercase(); );
if checker.ctx.is_builtin(&binding) { let fixable = !checker.ctx.in_complex_string_type_definition();
#[allow(deprecated)] if fixable && checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( match replacement {
binding, ModuleMember::BuiltIn(name) => {
expr.range(), // Built-in type, like `list`.
))); if checker.ctx.is_builtin(name) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
(*name).to_string(),
expr.range(),
)));
}
}
ModuleMember::Member(module, member) => {
// Imported type, like `collections.deque`.
diagnostic.try_set_fix(|| {
let (import_edit, binding) = get_or_import_symbol(
module,
member,
expr.start(),
&checker.ctx,
&checker.importer,
checker.locator,
)?;
let reference_edit = Edit::range_replacement(binding, expr.range());
Ok(Fix::suggested_edits(import_edit, [reference_edit]))
});
} }
} }
checker.diagnostics.push(diagnostic);
} }
checker.diagnostics.push(diagnostic);
} }

View file

@ -4,6 +4,7 @@ use rustpython_parser::ast::{self, Constant, Expr, ExprKind, Operator};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::unparse_expr; use ruff_python_ast::helpers::unparse_expr;
use ruff_python_semantic::analyze::typing::Pep604Operator;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::AsRule; use crate::registry::AsRule;
@ -56,55 +57,18 @@ fn union(elts: &[Expr]) -> Expr {
} }
} }
/// Returns `true` if any argument in the slice is a string.
fn any_arg_is_str(slice: &Expr) -> bool {
match &slice.node {
ExprKind::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
}) => true,
ExprKind::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(any_arg_is_str),
_ => false,
}
}
#[derive(Copy, Clone)]
enum TypingMember {
Union,
Optional,
}
/// UP007 /// UP007
pub(crate) fn use_pep604_annotation( pub(crate) fn use_pep604_annotation(
checker: &mut Checker, checker: &mut Checker,
expr: &Expr, expr: &Expr,
value: &Expr,
slice: &Expr, slice: &Expr,
operator: Pep604Operator,
) { ) {
// If any of the _arguments_ are forward references, we can't use PEP 604.
// Ex) `Union["str", "int"]` can't be converted to `"str" | "int"`.
if any_arg_is_str(slice) {
return;
}
let Some(typing_member) = checker.ctx.resolve_call_path(value).as_ref().and_then(|call_path| {
if checker.ctx.match_typing_call_path(call_path, "Optional") {
Some(TypingMember::Optional)
} else if checker.ctx.match_typing_call_path(call_path, "Union") {
Some(TypingMember::Union)
} else {
None
}
}) else {
return;
};
// Avoid fixing forward references, or types not in an annotation. // Avoid fixing forward references, or types not in an annotation.
let fixable = let fixable =
checker.ctx.in_type_definition() && !checker.ctx.in_complex_string_type_definition(); checker.ctx.in_type_definition() && !checker.ctx.in_complex_string_type_definition();
match operator {
match typing_member { Pep604Operator::Optional => {
TypingMember::Optional => {
let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range());
if fixable && checker.patch(diagnostic.kind.rule()) { if fixable && checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)] #[allow(deprecated)]
@ -115,7 +79,7 @@ pub(crate) fn use_pep604_annotation(
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
TypingMember::Union => { Pep604Operator::Union => {
let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range());
if fixable && checker.patch(diagnostic.kind.rule()) { if fixable && checker.patch(diagnostic.kind.rule()) {
match &slice.node { match &slice.node {

View file

@ -1,15 +1,15 @@
--- ---
source: crates/ruff/src/rules/pyupgrade/mod.rs source: crates/ruff/src/rules/pyupgrade/mod.rs
--- ---
UP006.py:4:10: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:4:10: UP006 [*] Use `list` instead of `typing.List` for type annotation
| |
4 | def f(x: typing.List[str]) -> None: 4 | def f(x: typing.List[str]) -> None:
| ^^^^^^^^^^^ UP006 | ^^^^^^^^^^^ UP006
5 | ... 5 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
1 1 | import typing 1 1 | import typing
2 2 | 2 2 |
3 3 | 3 3 |
@ -19,15 +19,15 @@ UP006.py:4:10: UP006 [*] Use `list` instead of `List` for type annotations
6 6 | 6 6 |
7 7 | 7 7 |
UP006.py:11:10: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:11:10: UP006 [*] Use `list` instead of `List` for type annotation
| |
11 | def f(x: List[str]) -> None: 11 | def f(x: List[str]) -> None:
| ^^^^ UP006 | ^^^^ UP006
12 | ... 12 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
8 8 | from typing import List 8 8 | from typing import List
9 9 | 9 9 |
10 10 | 10 10 |
@ -37,15 +37,15 @@ UP006.py:11:10: UP006 [*] Use `list` instead of `List` for type annotations
13 13 | 13 13 |
14 14 | 14 14 |
UP006.py:18:10: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:18:10: UP006 [*] Use `list` instead of `t.List` for type annotation
| |
18 | def f(x: t.List[str]) -> None: 18 | def f(x: t.List[str]) -> None:
| ^^^^^^ UP006 | ^^^^^^ UP006
19 | ... 19 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
15 15 | import typing as t 15 15 | import typing as t
16 16 | 16 16 |
17 17 | 17 17 |
@ -55,15 +55,15 @@ UP006.py:18:10: UP006 [*] Use `list` instead of `List` for type annotations
20 20 | 20 20 |
21 21 | 21 21 |
UP006.py:25:10: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:25:10: UP006 [*] Use `list` instead of `IList` for type annotation
| |
25 | def f(x: IList[str]) -> None: 25 | def f(x: IList[str]) -> None:
| ^^^^^ UP006 | ^^^^^ UP006
26 | ... 26 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
22 22 | from typing import List as IList 22 22 | from typing import List as IList
23 23 | 23 23 |
24 24 | 24 24 |
@ -73,15 +73,15 @@ UP006.py:25:10: UP006 [*] Use `list` instead of `List` for type annotations
27 27 | 27 27 |
28 28 | 28 28 |
UP006.py:29:11: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:29:11: UP006 [*] Use `list` instead of `List` for type annotation
| |
29 | def f(x: "List[str]") -> None: 29 | def f(x: "List[str]") -> None:
| ^^^^ UP006 | ^^^^ UP006
30 | ... 30 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
26 26 | ... 26 26 | ...
27 27 | 27 27 |
28 28 | 28 28 |
@ -91,15 +91,15 @@ UP006.py:29:11: UP006 [*] Use `list` instead of `List` for type annotations
31 31 | 31 31 |
32 32 | 32 32 |
UP006.py:33:12: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:33:12: UP006 [*] Use `list` instead of `List` for type annotation
| |
33 | def f(x: r"List[str]") -> None: 33 | def f(x: r"List[str]") -> None:
| ^^^^ UP006 | ^^^^ UP006
34 | ... 34 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
30 30 | ... 30 30 | ...
31 31 | 31 31 |
32 32 | 32 32 |
@ -109,15 +109,15 @@ UP006.py:33:12: UP006 [*] Use `list` instead of `List` for type annotations
35 35 | 35 35 |
36 36 | 36 36 |
UP006.py:37:11: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:37:11: UP006 [*] Use `list` instead of `List` for type annotation
| |
37 | def f(x: "List[str]") -> None: 37 | def f(x: "List[str]") -> None:
| ^^^^ UP006 | ^^^^ UP006
38 | ... 38 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
34 34 | ... 34 34 | ...
35 35 | 35 35 |
36 36 | 36 36 |
@ -127,15 +127,15 @@ UP006.py:37:11: UP006 [*] Use `list` instead of `List` for type annotations
39 39 | 39 39 |
40 40 | 40 40 |
UP006.py:41:13: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:41:13: UP006 [*] Use `list` instead of `List` for type annotation
| |
41 | def f(x: """List[str]""") -> None: 41 | def f(x: """List[str]""") -> None:
| ^^^^ UP006 | ^^^^ UP006
42 | ... 42 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
38 38 | ... 38 38 | ...
39 39 | 39 39 |
40 40 | 40 40 |
@ -145,23 +145,23 @@ UP006.py:41:13: UP006 [*] Use `list` instead of `List` for type annotations
43 43 | 43 43 |
44 44 | 44 44 |
UP006.py:45:10: UP006 Use `list` instead of `List` for type annotations UP006.py:45:10: UP006 Use `list` instead of `List` for type annotation
| |
45 | def f(x: "Li" "st[str]") -> None: 45 | def f(x: "Li" "st[str]") -> None:
| ^^^^^^^^^^^^^^ UP006 | ^^^^^^^^^^^^^^ UP006
46 | ... 46 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
UP006.py:49:11: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:49:11: UP006 [*] Use `list` instead of `List` for type annotation
| |
49 | def f(x: "List['List[str]']") -> None: 49 | def f(x: "List['List[str]']") -> None:
| ^^^^ UP006 | ^^^^ UP006
50 | ... 50 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
46 46 | ... 46 46 | ...
47 47 | 47 47 |
48 48 | 48 48 |
@ -171,15 +171,15 @@ UP006.py:49:11: UP006 [*] Use `list` instead of `List` for type annotations
51 51 | 51 51 |
52 52 | 52 52 |
UP006.py:49:17: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:49:17: UP006 [*] Use `list` instead of `List` for type annotation
| |
49 | def f(x: "List['List[str]']") -> None: 49 | def f(x: "List['List[str]']") -> None:
| ^^^^ UP006 | ^^^^ UP006
50 | ... 50 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
46 46 | ... 46 46 | ...
47 47 | 47 47 |
48 48 | 48 48 |
@ -189,15 +189,15 @@ UP006.py:49:17: UP006 [*] Use `list` instead of `List` for type annotations
51 51 | 51 51 |
52 52 | 52 52 |
UP006.py:53:11: UP006 [*] Use `list` instead of `List` for type annotations UP006.py:53:11: UP006 [*] Use `list` instead of `List` for type annotation
| |
53 | def f(x: "List['Li' 'st[str]']") -> None: 53 | def f(x: "List['Li' 'st[str]']") -> None:
| ^^^^ UP006 | ^^^^ UP006
54 | ... 54 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
50 50 | ... 50 50 | ...
51 51 | 51 51 |
52 52 | 52 52 |
@ -207,28 +207,78 @@ UP006.py:53:11: UP006 [*] Use `list` instead of `List` for type annotations
55 55 | 55 55 |
56 56 | 56 56 |
UP006.py:53:16: UP006 Use `list` instead of `List` for type annotations UP006.py:53:16: UP006 Use `list` instead of `List` for type annotation
| |
53 | def f(x: "List['Li' 'st[str]']") -> None: 53 | def f(x: "List['Li' 'st[str]']") -> None:
| ^^^^^^^^^^^^^^ UP006 | ^^^^^^^^^^^^^^ UP006
54 | ... 54 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
UP006.py:57:10: UP006 Use `list` instead of `List` for type annotations UP006.py:57:10: UP006 Use `list` instead of `List` for type annotation
| |
57 | def f(x: "Li" "st['List[str]']") -> None: 57 | def f(x: "Li" "st['List[str]']") -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP006 | ^^^^^^^^^^^^^^^^^^^^^^ UP006
58 | ... 58 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
UP006.py:57:10: UP006 Use `list` instead of `List` for type annotations UP006.py:57:10: UP006 Use `list` instead of `List` for type annotation
| |
57 | def f(x: "Li" "st['List[str]']") -> None: 57 | def f(x: "Li" "st['List[str]']") -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP006 | ^^^^^^^^^^^^^^^^^^^^^^ UP006
58 | ... 58 | ...
| |
= help: Replace `List` with `list` = help: Replace with `list`
UP006.py:61:10: UP006 [*] Use `collections.deque` instead of `typing.Deque` for type annotation
|
61 | def f(x: typing.Deque[str]) -> None:
| ^^^^^^^^^^^^ UP006
62 | ...
|
= help: Replace with `collections.deque`
Suggested fix
20 20 |
21 21 |
22 22 | from typing import List as IList
23 |+import collections
23 24 |
24 25 |
25 26 | def f(x: IList[str]) -> None:
--------------------------------------------------------------------------------
58 59 | ...
59 60 |
60 61 |
61 |-def f(x: typing.Deque[str]) -> None:
62 |+def f(x: collections.deque[str]) -> None:
62 63 | ...
63 64 |
64 65 |
UP006.py:65:10: UP006 [*] Use `collections.defaultdict` instead of `typing.DefaultDict` for type annotation
|
65 | def f(x: typing.DefaultDict[str, str]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP006
66 | ...
|
= help: Replace with `collections.defaultdict`
Suggested fix
20 20 |
21 21 |
22 22 | from typing import List as IList
23 |+import collections
23 24 |
24 25 |
25 26 | def f(x: IList[str]) -> None:
--------------------------------------------------------------------------------
62 63 | ...
63 64 |
64 65 |
65 |-def f(x: typing.DefaultDict[str, str]) -> None:
66 |+def f(x: collections.defaultdict[str, str]) -> None:
66 67 | ...

View file

@ -1,16 +1,16 @@
--- ---
source: crates/ruff/src/rules/pyupgrade/mod.rs source: crates/ruff/src/rules/pyupgrade/mod.rs
--- ---
future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotations future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotation
| |
34 | def f(x: int) -> List[int]: 34 | def f(x: int) -> List[int]:
| ^^^^ UP006 | ^^^^ UP006
35 | y = List[int]() 35 | y = List[int]()
36 | y.append(x) 36 | y.append(x)
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
31 31 | return cls(x=0, y=0) 31 31 | return cls(x=0, y=0)
32 32 | 32 32 |
33 33 | 33 33 |

View file

@ -1,16 +1,16 @@
--- ---
source: crates/ruff/src/rules/pyupgrade/mod.rs source: crates/ruff/src/rules/pyupgrade/mod.rs
--- ---
future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotations future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotation
| |
34 | def f(x: int) -> List[int]: 34 | def f(x: int) -> List[int]:
| ^^^^ UP006 | ^^^^ UP006
35 | y = List[int]() 35 | y = List[int]()
36 | y.append(x) 36 | y.append(x)
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
31 31 | return cls(x=0, y=0) 31 31 | return cls(x=0, y=0)
32 32 | 32 32 |
33 33 | 33 33 |
@ -20,7 +20,7 @@ future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type ann
36 36 | y.append(x) 36 36 | y.append(x)
37 37 | return y 37 37 | return y
future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type annotations future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type annotation
| |
35 | def f(x: int) -> List[int]: 35 | def f(x: int) -> List[int]:
36 | y = List[int]() 36 | y = List[int]()
@ -28,9 +28,9 @@ future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type anno
37 | y.append(x) 37 | y.append(x)
38 | return y 38 | return y
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
32 32 | 32 32 |
33 33 | 33 33 |
34 34 | def f(x: int) -> List[int]: 34 34 | def f(x: int) -> List[int]:
@ -40,32 +40,32 @@ future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type anno
37 37 | return y 37 37 | return y
38 38 | 38 38 |
future_annotations.py:42:27: UP006 [*] Use `list` instead of `List` for type annotations future_annotations.py:42:27: UP006 [*] Use `list` instead of `List` for type annotation
| |
42 | x: Optional[int] = None 42 | x: Optional[int] = None
43 | 43 |
44 | MyList: TypeAlias = Union[List[int], List[str]] 44 | MyList: TypeAlias = Union[List[int], List[str]]
| ^^^^ UP006 | ^^^^ UP006
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
39 39 | 39 39 |
40 40 | x: Optional[int] = None 40 40 | x: Optional[int] = None
41 41 | 41 41 |
42 |-MyList: TypeAlias = Union[List[int], List[str]] 42 |-MyList: TypeAlias = Union[List[int], List[str]]
42 |+MyList: TypeAlias = Union[list[int], List[str]] 42 |+MyList: TypeAlias = Union[list[int], List[str]]
future_annotations.py:42:38: UP006 [*] Use `list` instead of `List` for type annotations future_annotations.py:42:38: UP006 [*] Use `list` instead of `List` for type annotation
| |
42 | x: Optional[int] = None 42 | x: Optional[int] = None
43 | 43 |
44 | MyList: TypeAlias = Union[List[int], List[str]] 44 | MyList: TypeAlias = Union[List[int], List[str]]
| ^^^^ UP006 | ^^^^ UP006
| |
= help: Replace `List` with `list` = help: Replace with `list`
Suggested fix Fix
39 39 | 39 39 |
40 40 | x: Optional[int] = None 40 40 | x: Optional[int] = None
41 41 | 41 41 |

View file

@ -2,8 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, ExprKind, Operator};
use ruff_python_ast::call_path::{from_unqualified_name, CallPath}; use ruff_python_ast::call_path::{from_unqualified_name, CallPath};
use ruff_python_stdlib::typing::{ use ruff_python_stdlib::typing::{
IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS,
SUBSCRIPTS,
}; };
use crate::context::Context; use crate::context::Context;
@ -62,23 +61,90 @@ pub fn match_annotated_subscript<'a>(
}) })
} }
/// Returns `true` if `Expr` represents a reference to a typing object with a #[derive(Debug, Clone, Eq, PartialEq)]
/// PEP 585 built-in. pub enum ModuleMember {
pub fn is_pep585_builtin(expr: &Expr, context: &Context) -> bool { /// A builtin symbol, like `"list"`.
context.resolve_call_path(expr).map_or(false, |call_path| { BuiltIn(&'static str),
PEP_585_BUILTINS_ELIGIBLE.contains(&call_path.as_slice()) /// A module member, like `("collections", "deque")`.
Member(&'static str, &'static str),
}
impl std::fmt::Display for ModuleMember {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ModuleMember::BuiltIn(name) => std::write!(f, "{name}"),
ModuleMember::Member(module, member) => std::write!(f, "{module}.{member}"),
}
}
}
/// Returns the PEP 585 standard library generic variant for a `typing` module reference, if such
/// a variant exists.
pub fn to_pep585_generic(expr: &Expr, context: &Context) -> Option<ModuleMember> {
context.resolve_call_path(expr).and_then(|call_path| {
let [module, name] = call_path.as_slice() else {
return None;
};
PEP_585_GENERICS
.iter()
.find_map(|((from_module, from_member), (to_module, to_member))| {
if module == from_module && name == from_member {
if to_module.is_empty() {
Some(ModuleMember::BuiltIn(to_member))
} else {
Some(ModuleMember::Member(to_module, to_member))
}
} else {
None
}
})
}) })
} }
/// Returns `true` if `Expr` represents a reference to a typing object with a #[derive(Debug, Copy, Clone)]
/// PEP 603 built-in. pub enum Pep604Operator {
pub fn is_pep604_builtin(expr: &Expr, context: &Context) -> bool { /// The union operator, e.g., `Union[str, int]`, expressible as `str | int` after PEP 604.
context.resolve_call_path(expr).map_or(false, |call_path| { Union,
context.match_typing_call_path(&call_path, "Optional") /// The union operator, e.g., `Optional[str]`, expressible as `str | None` after PEP 604.
|| context.match_typing_call_path(&call_path, "Union") Optional,
})
} }
/// Return the PEP 604 operator variant to which the given subscript [`Expr`] corresponds, if any.
pub fn to_pep604_operator(value: &Expr, slice: &Expr, context: &Context) -> Option<Pep604Operator> {
/// Returns `true` if any argument in the slice is a string.
fn any_arg_is_str(slice: &Expr) -> bool {
match &slice.node {
ExprKind::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
}) => true,
ExprKind::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(any_arg_is_str),
_ => false,
}
}
// If any of the _arguments_ are forward references, we can't use PEP 604.
// Ex) `Union["str", "int"]` can't be converted to `"str" | "int"`.
if any_arg_is_str(slice) {
return None;
}
context
.resolve_call_path(value)
.as_ref()
.and_then(|call_path| {
if context.match_typing_call_path(call_path, "Optional") {
Some(Pep604Operator::Optional)
} else if context.match_typing_call_path(call_path, "Union") {
Some(Pep604Operator::Union)
} else {
None
}
})
}
/// Return `true` if `Expr` represents a reference to a type annotation that resolves to an
/// immutable type.
pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool { pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool {
match &expr.node { match &expr.node {
ExprKind::Name(_) | ExprKind::Attribute(_) => { ExprKind::Name(_) | ExprKind::Attribute(_) => {
@ -144,6 +210,7 @@ const IMMUTABLE_FUNCS: &[&[&str]] = &[
&["re", "compile"], &["re", "compile"],
]; ];
/// Return `true` if `func` is a function that returns an immutable object.
pub fn is_immutable_func( pub fn is_immutable_func(
context: &Context, context: &Context,
func: &Expr, func: &Expr,

View file

@ -187,15 +187,26 @@ pub const PEP_593_SUBSCRIPTS: &[&[&str]] = &[
&["typing_extensions", "Annotated"], &["typing_extensions", "Annotated"],
]; ];
type ModuleMember = (&'static str, &'static str);
type SymbolReplacement = (ModuleMember, ModuleMember);
// See: https://peps.python.org/pep-0585/ // See: https://peps.python.org/pep-0585/
pub const PEP_585_BUILTINS_ELIGIBLE: &[&[&str]] = &[ pub const PEP_585_GENERICS: &[SymbolReplacement] = &[
&["typing", "Dict"], (("typing", "Dict"), ("", "dict")),
&["typing", "FrozenSet"], (("typing", "FrozenSet"), ("", "frozenset")),
&["typing", "List"], (("typing", "List"), ("", "list")),
&["typing", "Set"], (("typing", "Set"), ("", "set")),
&["typing", "Tuple"], (("typing", "Tuple"), ("", "tuple")),
&["typing", "Type"], (("typing", "Type"), ("", "type")),
&["typing_extensions", "Type"], (("typing_extensions", "Type"), ("", "type")),
(("typing", "Deque"), ("collections", "deque")),
(("typing_extensions", "Deque"), ("collections", "deque")),
(("typing", "DefaultDict"), ("collections", "defaultdict")),
(
("typing_extensions", "DefaultDict"),
("collections", "defaultdict"),
),
]; ];
// See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 // See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91