[pyupgrade] - add PEP646 Unpack conversion to * with fix (UP044) (#13988)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Steve C 2024-10-31 02:58:34 -04:00 committed by GitHub
parent 2629527559
commit 2d917d72f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 211 additions and 0 deletions

View file

@ -0,0 +1,19 @@
from typing import Generic, TypeVarTuple, Unpack
Shape = TypeVarTuple('Shape')
class C(Generic[Unpack[Shape]]):
pass
class D(Generic[Unpack [Shape]]):
pass
def f(*args: Unpack[tuple[int, ...]]): pass
def f(*args: Unpack[other.Type]): pass
# Not valid unpackings but they are valid syntax
def foo(*args: Unpack[int | str]) -> None: pass
def foo(*args: Unpack[int and str]) -> None: pass
def foo(*args: Unpack[int > str]) -> None: pass

View file

@ -150,6 +150,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ruff::rules::subscript_with_parenthesized_tuple(checker, subscript);
}
if checker.enabled(Rule::NonPEP646Unpack) {
pyupgrade::rules::use_pep646_unpack(checker, subscript);
}
pandas_vet::rules::subscript(checker, value, expr);
}
Expr::Tuple(ast::ExprTuple {

View file

@ -528,6 +528,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
(Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
(Pyupgrade, "043") => (RuleGroup::Preview, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
(Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack),
// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),

View file

@ -235,4 +235,18 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn unpack_pep_646_py311() -> Result<()> {
let diagnostics = test_path(
Path::new("pyupgrade/UP044.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
target_version: PythonVersion::Py311,
..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View file

@ -35,6 +35,7 @@ pub(crate) use unpacked_list_comprehension::*;
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::*;
@ -77,6 +78,7 @@ mod unpacked_list_comprehension;
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;

View file

@ -0,0 +1,87 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprSubscript;
use crate::{checkers::ast::Checker, settings::types::PythonVersion};
/// ## What it does
/// Checks for uses of `Unpack[]` on Python 3.11 and above, and suggests
/// using `*` instead.
///
/// ## Why is this bad?
/// [PEP 646] introduced a new syntax for unpacking sequences based on the `*`
/// operator. This syntax is more concise and readable than the previous
/// `typing.Unpack` syntax.
///
/// ## Example
///
/// ```python
/// from typing import Unpack
///
///
/// def foo(*args: Unpack[tuple[int, ...]]) -> None:
/// pass
/// ```
///
/// Use instead:
///
/// ```python
/// def foo(*args: *tuple[int, ...]) -> None:
/// pass
/// ```
///
/// ## References
/// - [PEP 646](https://peps.python.org/pep-0646/#unpack-for-backwards-compatibility)
#[violation]
pub struct NonPEP646Unpack;
impl Violation for NonPEP646Unpack {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `*` for unpacking")
}
fn fix_title(&self) -> Option<String> {
Some("Convert to `*` for unpacking".to_string())
}
}
/// UP044
pub(crate) fn use_pep646_unpack(checker: &mut Checker, expr: &ExprSubscript) {
if checker.settings.target_version < PythonVersion::Py311 {
return;
}
if !checker.semantic().seen_typing() {
return;
}
let ExprSubscript {
range,
value,
slice,
..
} = expr;
if !checker.semantic().match_typing_expr(value, "Unpack") {
return;
}
// Skip semantically invalid subscript calls (e.g. `Unpack[str | num]`).
if !(slice.is_name_expr() || slice.is_subscript_expr() || slice.is_attribute_expr()) {
return;
}
let mut diagnostic = Diagnostic::new(NonPEP646Unpack, *range);
let inner = checker.locator().slice(slice.as_ref());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("*{inner}"),
*range,
)));
checker.diagnostics.push(diagnostic);
}

View file

@ -0,0 +1,83 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text
---
UP044.py:5:17: UP044 [*] Use `*` for unpacking
|
3 | Shape = TypeVarTuple('Shape')
4 |
5 | class C(Generic[Unpack[Shape]]):
| ^^^^^^^^^^^^^ UP044
6 | pass
|
= help: Convert to `*` for unpacking
Safe fix
2 2 |
3 3 | Shape = TypeVarTuple('Shape')
4 4 |
5 |-class C(Generic[Unpack[Shape]]):
5 |+class C(Generic[*Shape]):
6 6 | pass
7 7 |
8 8 | class D(Generic[Unpack [Shape]]):
UP044.py:8:17: UP044 [*] Use `*` for unpacking
|
6 | pass
7 |
8 | class D(Generic[Unpack [Shape]]):
| ^^^^^^^^^^^^^^^ UP044
9 | pass
|
= help: Convert to `*` for unpacking
Safe fix
5 5 | class C(Generic[Unpack[Shape]]):
6 6 | pass
7 7 |
8 |-class D(Generic[Unpack [Shape]]):
8 |+class D(Generic[*Shape]):
9 9 | pass
10 10 |
11 11 | def f(*args: Unpack[tuple[int, ...]]): pass
UP044.py:11:14: UP044 [*] Use `*` for unpacking
|
9 | pass
10 |
11 | def f(*args: Unpack[tuple[int, ...]]): pass
| ^^^^^^^^^^^^^^^^^^^^^^^ UP044
12 |
13 | def f(*args: Unpack[other.Type]): pass
|
= help: Convert to `*` for unpacking
Safe fix
8 8 | class D(Generic[Unpack [Shape]]):
9 9 | pass
10 10 |
11 |-def f(*args: Unpack[tuple[int, ...]]): pass
11 |+def f(*args: *tuple[int, ...]): pass
12 12 |
13 13 | def f(*args: Unpack[other.Type]): pass
14 14 |
UP044.py:13:14: UP044 [*] Use `*` for unpacking
|
11 | def f(*args: Unpack[tuple[int, ...]]): pass
12 |
13 | def f(*args: Unpack[other.Type]): pass
| ^^^^^^^^^^^^^^^^^^ UP044
|
= help: Convert to `*` for unpacking
Safe fix
10 10 |
11 11 | def f(*args: Unpack[tuple[int, ...]]): pass
12 12 |
13 |-def f(*args: Unpack[other.Type]): pass
13 |+def f(*args: *other.Type): pass
14 14 |
15 15 |
16 16 | # Not valid unpackings but they are valid syntax

1
ruff.schema.json generated
View file

@ -4066,6 +4066,7 @@
"UP041",
"UP042",
"UP043",
"UP044",
"W",
"W1",
"W19",