mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
[pyupgrade
] Avoid PEP-604 unions with typing.NamedTuple
(UP007
, UP045
) (#18682)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
<!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? --> ## Summary Make `UP045` ignore `Optional[NamedTuple]` as `NamedTuple` is a function (not a proper type). Rewriting it to `NamedTuple | None` breaks at runtime. While type checkers currently accept `NamedTuple` as a type, they arguably shouldn't. Therefore, we outright ignore it and don't touch or lint on it. For a more detailed discussion, see the linked issue. ## Test Plan Added examples to the existing tests. ## Related Issues Fixes: https://github.com/astral-sh/ruff/issues/18619
This commit is contained in:
parent
4963835d0d
commit
28ab61d885
4 changed files with 103 additions and 0 deletions
|
@ -90,3 +90,52 @@ class AClass:
|
||||||
|
|
||||||
def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
|
def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
|
||||||
print(param)
|
print(param)
|
||||||
|
|
||||||
|
|
||||||
|
from typing import NamedTuple, Union
|
||||||
|
|
||||||
|
import typing_extensions
|
||||||
|
from typing_extensions import (
|
||||||
|
NamedTuple as NamedTupleTE,
|
||||||
|
Union as UnionTE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/18619
|
||||||
|
# Don't emit lint for `NamedTuple`
|
||||||
|
a_plain_1: Union[NamedTuple, int] = None
|
||||||
|
a_plain_2: Union[int, NamedTuple] = None
|
||||||
|
a_plain_3: Union[NamedTuple, None] = None
|
||||||
|
a_plain_4: Union[None, NamedTuple] = None
|
||||||
|
a_plain_te_1: UnionTE[NamedTupleTE, int] = None
|
||||||
|
a_plain_te_2: UnionTE[int, NamedTupleTE] = None
|
||||||
|
a_plain_te_3: UnionTE[NamedTupleTE, None] = None
|
||||||
|
a_plain_te_4: UnionTE[None, NamedTupleTE] = None
|
||||||
|
a_plain_typing_1: UnionTE[typing.NamedTuple, int] = None
|
||||||
|
a_plain_typing_2: UnionTE[int, typing.NamedTuple] = None
|
||||||
|
a_plain_typing_3: UnionTE[typing.NamedTuple, None] = None
|
||||||
|
a_plain_typing_4: UnionTE[None, typing.NamedTuple] = None
|
||||||
|
a_string_1: "Union[NamedTuple, int]" = None
|
||||||
|
a_string_2: "Union[int, NamedTuple]" = None
|
||||||
|
a_string_3: "Union[NamedTuple, None]" = None
|
||||||
|
a_string_4: "Union[None, NamedTuple]" = None
|
||||||
|
a_string_te_1: "UnionTE[NamedTupleTE, int]" = None
|
||||||
|
a_string_te_2: "UnionTE[int, NamedTupleTE]" = None
|
||||||
|
a_string_te_3: "UnionTE[NamedTupleTE, None]" = None
|
||||||
|
a_string_te_4: "UnionTE[None, NamedTupleTE]" = None
|
||||||
|
a_string_typing_1: "typing.Union[typing.NamedTuple, int]" = None
|
||||||
|
a_string_typing_2: "typing.Union[int, typing.NamedTuple]" = None
|
||||||
|
a_string_typing_3: "typing.Union[typing.NamedTuple, None]" = None
|
||||||
|
a_string_typing_4: "typing.Union[None, typing.NamedTuple]" = None
|
||||||
|
|
||||||
|
b_plain_1: Union[NamedTuple] = None
|
||||||
|
b_plain_2: Union[NamedTuple, None] = None
|
||||||
|
b_plain_te_1: UnionTE[NamedTupleTE] = None
|
||||||
|
b_plain_te_2: UnionTE[NamedTupleTE, None] = None
|
||||||
|
b_plain_typing_1: UnionTE[typing.NamedTuple] = None
|
||||||
|
b_plain_typing_2: UnionTE[typing.NamedTuple, None] = None
|
||||||
|
b_string_1: "Union[NamedTuple]" = None
|
||||||
|
b_string_2: "Union[NamedTuple, None]" = None
|
||||||
|
b_string_te_1: "UnionTE[NamedTupleTE]" = None
|
||||||
|
b_string_te_2: "UnionTE[NamedTupleTE, None]" = None
|
||||||
|
b_string_typing_1: "typing.Union[typing.NamedTuple]" = None
|
||||||
|
b_string_typing_2: "typing.Union[typing.NamedTuple, None]" = None
|
||||||
|
|
|
@ -47,3 +47,25 @@ class ServiceRefOrValue:
|
||||||
# Test for: https://github.com/astral-sh/ruff/issues/18508
|
# Test for: https://github.com/astral-sh/ruff/issues/18508
|
||||||
# Optional[None] should not be offered a fix
|
# Optional[None] should not be offered a fix
|
||||||
foo: Optional[None] = None
|
foo: Optional[None] = None
|
||||||
|
|
||||||
|
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
import typing_extensions
|
||||||
|
from typing_extensions import (
|
||||||
|
NamedTuple as NamedTupleTE,
|
||||||
|
Optional as OptionalTE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/18619
|
||||||
|
# Don't emit lint for `NamedTuple`
|
||||||
|
a1: Optional[NamedTuple] = None
|
||||||
|
a2: typing.Optional[NamedTuple] = None
|
||||||
|
a3: OptionalTE[NamedTuple] = None
|
||||||
|
a4: typing_extensions.Optional[NamedTuple] = None
|
||||||
|
a5: Optional[typing.NamedTuple] = None
|
||||||
|
a6: typing.Optional[typing.NamedTuple] = None
|
||||||
|
a7: OptionalTE[typing.NamedTuple] = None
|
||||||
|
a8: typing_extensions.Optional[typing.NamedTuple] = None
|
||||||
|
a9: "Optional[NamedTuple]" = None
|
||||||
|
a10: Optional[NamedTupleTE] = None
|
||||||
|
|
|
@ -132,6 +132,17 @@ pub(crate) fn non_pep604_annotation(
|
||||||
slice: &Expr,
|
slice: &Expr,
|
||||||
operator: Pep604Operator,
|
operator: Pep604Operator,
|
||||||
) {
|
) {
|
||||||
|
// `NamedTuple` is not a type; it's a type constructor. Using it in a type annotation doesn't
|
||||||
|
// make much sense. But since type checkers will currently (incorrectly) _not_ complain about it
|
||||||
|
// being used in a type annotation, we just ignore `Optional[typing.NamedTuple]` and
|
||||||
|
// `Union[...]` containing `NamedTuple`.
|
||||||
|
// <https://github.com/astral-sh/ruff/issues/18619>
|
||||||
|
if is_optional_named_tuple(checker, operator, slice)
|
||||||
|
|| is_union_with_named_tuple(checker, operator, slice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid fixing forward references, types not in an annotation, and expressions that would
|
// Avoid fixing forward references, types not in an annotation, and expressions that would
|
||||||
// lead to invalid syntax.
|
// lead to invalid syntax.
|
||||||
let fixable = checker.semantic().in_type_definition()
|
let fixable = checker.semantic().in_type_definition()
|
||||||
|
@ -273,6 +284,25 @@ fn is_allowed_value(expr: &Expr) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this is an `Optional[typing.NamedTuple]` annotation.
|
||||||
|
fn is_optional_named_tuple(checker: &Checker, operator: Pep604Operator, slice: &Expr) -> bool {
|
||||||
|
matches!(operator, Pep604Operator::Optional) && is_named_tuple(checker, slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this is a `Union[...]` annotation containing `typing.NamedTuple`.
|
||||||
|
fn is_union_with_named_tuple(checker: &Checker, operator: Pep604Operator, slice: &Expr) -> bool {
|
||||||
|
matches!(operator, Pep604Operator::Union)
|
||||||
|
&& (is_named_tuple(checker, slice)
|
||||||
|
|| slice
|
||||||
|
.as_tuple_expr()
|
||||||
|
.is_some_and(|tuple| tuple.elts.iter().any(|elt| is_named_tuple(checker, elt))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this is a `typing.NamedTuple` annotation.
|
||||||
|
fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool {
|
||||||
|
checker.semantic().match_typing_expr(expr, "NamedTuple")
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if this is an `Optional[None]` annotation.
|
/// Return `true` if this is an `Optional[None]` annotation.
|
||||||
fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool {
|
fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool {
|
||||||
matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_))
|
matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_))
|
||||||
|
|
|
@ -314,3 +314,5 @@ UP007.py:91:26: UP007 [*] Use `X | Y` for type annotations
|
||||||
91 |-def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
|
91 |-def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
|
||||||
91 |+def myfunc(param: "tuple[int | 'AClass' | None, str]"):
|
91 |+def myfunc(param: "tuple[int | 'AClass' | None, str]"):
|
||||||
92 92 | print(param)
|
92 92 | print(param)
|
||||||
|
93 93 |
|
||||||
|
94 94 |
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue