mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
[pyupgrade
] - add PEP646 Unpack conversion to *
with fix (UP044
) (#13988)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
2629527559
commit
2d917d72f6
8 changed files with 211 additions and 0 deletions
19
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP044.py
vendored
Normal file
19
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP044.py
vendored
Normal 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
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
1
ruff.schema.json
generated
|
@ -4066,6 +4066,7 @@
|
|||
"UP041",
|
||||
"UP042",
|
||||
"UP043",
|
||||
"UP044",
|
||||
"W",
|
||||
"W1",
|
||||
"W19",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue