Skip to content

Commit

Permalink
[pyupgrade] - add PEP646 Unpack conversion to * (UP044)
Browse files Browse the repository at this point in the history
  • Loading branch information
diceroll123 committed Oct 30, 2024
1 parent 1607d88 commit f52e85a
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 0 deletions.
11 changes: 11 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP044.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
}
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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;
}

let mut diagnostic = Diagnostic::new(NonPEP646Unpack, *range);

let inner = checker.locator().slice(slice.as_ref());

if checker.settings.preview.is_enabled() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("*{inner}"),
*range,
)));
}

checker.diagnostics.push(diagnostic);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
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
|
= 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
1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f52e85a

Please sign in to comment.