[pyupgrade] Split UP007 to two individual rules for Union and Optional (UP007, UP045) (#15313)
Some checks are pending
CI / cargo build (release) (push) Waiting to run
CI / cargo shear (push) Blocked by required conditions
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 (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 / 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 / benchmarks (push) Blocked by required conditions

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
InSync 2025-01-07 17:22:59 +07:00 committed by GitHub
parent ce9c4968ae
commit 0dc00e63f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 509 additions and 373 deletions

View file

@ -1,16 +1,7 @@
import typing
from typing import Optional
from typing import Union
def f(x: Optional[str]) -> None:
...
def f(x: typing.Optional[str]) -> None:
...
def f(x: Union[str, int, Union[float, bytes]]) -> None:
...
@ -52,9 +43,6 @@ def f(x: Union[("str", "int"), float]) -> None:
def f() -> None:
x: Optional[str]
x = Optional[str]
x = Union[str, int]
x = Union["str", "int"]
x: Union[str, int]
@ -85,31 +73,6 @@ def f(x: Union[str, lambda: int]) -> None:
...
def f(x: Optional[int : float]) -> None:
...
def f(x: Optional[str, int : float]) -> None:
...
def f(x: Optional[int, float]) -> None:
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7131
class ServiceRefOrValue:
service_specification: Optional[
list[ServiceSpecificationRef]
| list[ServiceSpecification]
] = None
# Regression test for: https://github.com/astral-sh/ruff/issues/7201
class ServiceRefOrValue:
service_specification: Optional[str]is not True = None
# Regression test for: https://github.com/astral-sh/ruff/issues/7452
class Collection(Protocol[*_B0]):
def __iter__(self) -> Iterator[Union[*_B0]]:

View file

@ -0,0 +1,44 @@
import typing
from typing import Optional
def f(x: Optional[str]) -> None:
...
def f(x: typing.Optional[str]) -> None:
...
def f() -> None:
x: Optional[str]
x = Optional[str]
def f(x: list[Optional[int]]) -> None:
...
def f(x: Optional[int : float]) -> None:
...
def f(x: Optional[str, int : float]) -> None:
...
def f(x: Optional[int, float]) -> None:
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7131
class ServiceRefOrValue:
service_specification: Optional[
list[ServiceSpecificationRef]
| list[ServiceSpecification]
] = None
# Regression test for: https://github.com/astral-sh/ruff/issues/7201
class ServiceRefOrValue:
service_specification: Optional[str]is not True = None

View file

@ -27,7 +27,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Ex) Optional[...], Union[...]
if checker.any_enabled(&[
Rule::FutureRewritableTypeAnnotation,
Rule::NonPEP604Annotation,
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
]) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic)
{
@ -43,7 +44,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
}
if checker.enabled(Rule::NonPEP604Annotation) {
if checker.any_enabled(&[
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
]) {
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37
@ -51,7 +55,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep604_annotation(checker, expr, slice, operator);
pyupgrade::rules::non_pep604_annotation(checker, expr, slice, operator);
}
}
}

View file

@ -501,7 +501,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "004") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessObjectInheritance),
(Pyupgrade, "005") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedUnittestAlias),
(Pyupgrade, "006") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP585Annotation),
(Pyupgrade, "007") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604Annotation),
(Pyupgrade, "007") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604AnnotationUnion),
(Pyupgrade, "008") => (RuleGroup::Stable, rules::pyupgrade::rules::SuperCallWithParameters),
(Pyupgrade, "009") => (RuleGroup::Stable, rules::pyupgrade::rules::UTF8EncodingDeclaration),
(Pyupgrade, "010") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryFutureImport),
@ -538,6 +538,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
(Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
(Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack),
(Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional),
// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),

View file

@ -39,7 +39,8 @@ mod tests {
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_3.py"))]
#[test_case(Rule::NonPEP604Annotation, Path::new("UP007.py"))]
#[test_case(Rule::NonPEP604AnnotationUnion, Path::new("UP007.py"))]
#[test_case(Rule::NonPEP604AnnotationUnion, Path::new("UP045.py"))]
#[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))]
#[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))]
#[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))]
@ -111,6 +112,19 @@ mod tests {
Ok(())
}
#[test]
fn up007_preview() -> Result<()> {
let diagnostics = test_path(
Path::new("pyupgrade/UP045.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationUnion)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn async_timeout_error_alias_not_applied_py310() -> Result<()> {
let diagnostics = test_path(
@ -201,7 +215,10 @@ mod tests {
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::Py37,
..settings::LinterSettings::for_rule(Rule::NonPEP604Annotation)
..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
])
},
)?;
assert_messages!(diagnostics);
@ -214,7 +231,10 @@ mod tests {
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::Py310,
..settings::LinterSettings::for_rule(Rule::NonPEP604Annotation)
..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
])
},
)?;
assert_messages!(diagnostics);

View file

@ -1,4 +1,6 @@
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_diagnostics::{
Applicability, Diagnostic, DiagnosticKind, Edit, Fix, FixAvailability, Violation,
};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
use ruff_python_ast::{self as ast, Expr};
@ -6,8 +8,9 @@ use ruff_python_semantic::analyze::typing::Pep604Operator;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix::edits::pad;
use crate::settings::types::PythonVersion;
use crate::settings::types::{PreviewMode, PythonVersion};
/// ## What it does
/// Check for type annotations that can be rewritten based on [PEP 604] syntax.
@ -37,6 +40,10 @@ use crate::settings::types::PythonVersion;
/// foo: int | str = 1
/// ```
///
/// ## Preview
/// In preview mode, this rule only checks for usages of `typing.Union`,
/// while `UP045` checks for `typing.Optional`.
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may lead to runtime errors when
/// alongside libraries that rely on runtime type annotations, like Pydantic,
@ -50,9 +57,9 @@ use crate::settings::types::PythonVersion;
///
/// [PEP 604]: https://peps.python.org/pep-0604/
#[derive(ViolationMetadata)]
pub(crate) struct NonPEP604Annotation;
pub(crate) struct NonPEP604AnnotationUnion;
impl Violation for NonPEP604Annotation {
impl Violation for NonPEP604AnnotationUnion {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
@ -65,8 +72,64 @@ impl Violation for NonPEP604Annotation {
}
}
/// UP007
pub(crate) fn use_pep604_annotation(
/// ## What it does
/// Check for `typing.Optional` annotations that can be rewritten based on [PEP 604] syntax.
///
/// ## Why is this bad?
/// [PEP 604] introduced a new syntax for union type annotations based on the
/// `|` operator. This syntax is more concise and readable than the previous
/// `typing.Optional` syntax.
///
/// This rule is enabled when targeting Python 3.10 or later (see:
/// [`target-version`]). By default, it's _also_ enabled for earlier Python
/// versions if `from __future__ import annotations` is present, as
/// `__future__` annotations are not evaluated at runtime. If your code relies
/// on runtime type annotations (either directly or via a library like
/// Pydantic), you can disable this behavior for Python versions prior to 3.10
/// by setting [`lint.pyupgrade.keep-runtime-typing`] to `true`.
///
/// ## Example
/// ```python
/// from typing import Optional
///
/// foo: Optional[int] = None
/// ```
///
/// Use instead:
/// ```python
/// foo: int | None = None
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may lead to runtime errors when
/// alongside libraries that rely on runtime type annotations, like Pydantic,
/// on Python versions prior to Python 3.10. It may also lead to runtime errors
/// in unusual and likely incorrect type annotations where the type does not
/// support the `|` operator.
///
/// ## Options
/// - `target-version`
/// - `lint.pyupgrade.keep-runtime-typing`
///
/// [PEP 604]: https://peps.python.org/pep-0604/
#[derive(ViolationMetadata)]
pub(crate) struct NonPEP604AnnotationOptional;
impl Violation for NonPEP604AnnotationOptional {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Use `X | None` for type annotations".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Convert to `X | None`".to_string())
}
}
/// UP007, UP045
pub(crate) fn non_pep604_annotation(
checker: &mut Checker,
expr: &Expr,
slice: &Expr,
@ -86,7 +149,23 @@ pub(crate) fn use_pep604_annotation(
match operator {
Pep604Operator::Optional => {
let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range());
let (rule, diagnostic_kind) = match checker.settings.preview {
PreviewMode::Disabled => (
Rule::NonPEP604AnnotationUnion,
DiagnosticKind::from(NonPEP604AnnotationUnion),
),
PreviewMode::Enabled => (
Rule::NonPEP604AnnotationOptional,
DiagnosticKind::from(NonPEP604AnnotationOptional),
),
};
if !checker.enabled(rule) {
return;
}
let mut diagnostic = Diagnostic::new(diagnostic_kind, expr.range());
if fixable {
match slice {
Expr::Tuple(_) => {
@ -110,7 +189,11 @@ pub(crate) fn use_pep604_annotation(
checker.diagnostics.push(diagnostic);
}
Pep604Operator::Union => {
let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range());
if !checker.enabled(Rule::NonPEP604AnnotationUnion) {
return;
}
let mut diagnostic = Diagnostic::new(NonPEP604AnnotationUnion, expr.range());
if fixable {
match slice {
Expr::Slice(_) => {

View file

@ -2,432 +2,296 @@
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text
---
UP007.py:6:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:5:10: UP007 [*] Use `X | Y` for type annotations
|
6 | def f(x: Optional[str]) -> None:
| ^^^^^^^^^^^^^ UP007
7 | ...
5 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
6 | ...
|
= help: Convert to `X | Y`
Safe fix
3 3 | from typing import Union
2 2 | from typing import Union
3 3 |
4 4 |
5 5 |
6 |-def f(x: Optional[str]) -> None:
6 |+def f(x: str | None) -> None:
7 7 | ...
5 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
5 |+def f(x: str | int | Union[float, bytes]) -> None:
6 6 | ...
7 7 |
8 8 |
9 9 |
UP007.py:10:10: UP007 [*] Use `X | Y` for type annotations
|
10 | def f(x: typing.Optional[str]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
11 | ...
|
= help: Convert to `X | Y`
UP007.py:5:26: UP007 [*] Use `X | Y` for type annotations
|
5 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^^^^^^^ UP007
6 | ...
|
= help: Convert to `X | Y`
Safe fix
7 7 | ...
8 8 |
9 9 |
10 |-def f(x: typing.Optional[str]) -> None:
10 |+def f(x: str | None) -> None:
11 11 | ...
12 12 |
13 13 |
2 2 | from typing import Union
3 3 |
4 4 |
5 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
5 |+def f(x: Union[str, int, float | bytes]) -> None:
6 6 | ...
7 7 |
8 8 |
UP007.py:14:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:9:10: UP007 [*] Use `X | Y` for type annotations
|
14 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
15 | ...
|
= help: Convert to `X | Y`
Safe fix
11 11 | ...
12 12 |
13 13 |
14 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
14 |+def f(x: str | int | Union[float, bytes]) -> None:
15 15 | ...
16 16 |
17 17 |
UP007.py:14:26: UP007 [*] Use `X | Y` for type annotations
|
14 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^^^^^^^ UP007
15 | ...
|
= help: Convert to `X | Y`
Safe fix
11 11 | ...
12 12 |
13 13 |
14 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
14 |+def f(x: Union[str, int, float | bytes]) -> None:
15 15 | ...
16 16 |
17 17 |
UP007.py:18:10: UP007 [*] Use `X | Y` for type annotations
|
18 | def f(x: typing.Union[str, int]) -> None:
9 | def f(x: typing.Union[str, int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP007
19 | ...
10 | ...
|
= help: Convert to `X | Y`
Safe fix
15 15 | ...
16 16 |
17 17 |
18 |-def f(x: typing.Union[str, int]) -> None:
18 |+def f(x: str | int) -> None:
19 19 | ...
20 20 |
21 21 |
6 6 | ...
7 7 |
8 8 |
9 |-def f(x: typing.Union[str, int]) -> None:
9 |+def f(x: str | int) -> None:
10 10 | ...
11 11 |
12 12 |
UP007.py:22:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:13:10: UP007 [*] Use `X | Y` for type annotations
|
22 | def f(x: typing.Union[(str, int)]) -> None:
13 | def f(x: typing.Union[(str, int)]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP007
23 | ...
14 | ...
|
= help: Convert to `X | Y`
Safe fix
19 19 | ...
20 20 |
21 21 |
22 |-def f(x: typing.Union[(str, int)]) -> None:
22 |+def f(x: str | int) -> None:
23 23 | ...
24 24 |
25 25 |
10 10 | ...
11 11 |
12 12 |
13 |-def f(x: typing.Union[(str, int)]) -> None:
13 |+def f(x: str | int) -> None:
14 14 | ...
15 15 |
16 16 |
UP007.py:26:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:17:10: UP007 [*] Use `X | Y` for type annotations
|
26 | def f(x: typing.Union[(str, int), float]) -> None:
17 | def f(x: typing.Union[(str, int), float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
27 | ...
18 | ...
|
= help: Convert to `X | Y`
Safe fix
23 23 | ...
24 24 |
25 25 |
26 |-def f(x: typing.Union[(str, int), float]) -> None:
26 |+def f(x: str | int | float) -> None:
27 27 | ...
28 28 |
29 29 |
14 14 | ...
15 15 |
16 16 |
17 |-def f(x: typing.Union[(str, int), float]) -> None:
17 |+def f(x: str | int | float) -> None:
18 18 | ...
19 19 |
20 20 |
UP007.py:30:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:21:10: UP007 [*] Use `X | Y` for type annotations
|
30 | def f(x: typing.Union[(int,)]) -> None:
21 | def f(x: typing.Union[(int,)]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
31 | ...
22 | ...
|
= help: Convert to `X | Y`
Safe fix
27 27 | ...
28 28 |
29 29 |
30 |-def f(x: typing.Union[(int,)]) -> None:
30 |+def f(x: int) -> None:
31 31 | ...
32 32 |
33 33 |
18 18 | ...
19 19 |
20 20 |
21 |-def f(x: typing.Union[(int,)]) -> None:
21 |+def f(x: int) -> None:
22 22 | ...
23 23 |
24 24 |
UP007.py:34:10: UP007 [*] Use `X | Y` for type annotations
UP007.py:25:10: UP007 [*] Use `X | Y` for type annotations
|
34 | def f(x: typing.Union[()]) -> None:
25 | def f(x: typing.Union[()]) -> None:
| ^^^^^^^^^^^^^^^^ UP007
35 | ...
26 | ...
|
= help: Convert to `X | Y`
Safe fix
31 31 | ...
32 32 |
33 33 |
34 |-def f(x: typing.Union[()]) -> None:
34 |+def f(x: ()) -> None:
35 35 | ...
36 36 |
37 37 |
22 22 | ...
23 23 |
24 24 |
25 |-def f(x: typing.Union[()]) -> None:
25 |+def f(x: ()) -> None:
26 26 | ...
27 27 |
28 28 |
UP007.py:38:11: UP007 [*] Use `X | Y` for type annotations
UP007.py:29:11: UP007 [*] Use `X | Y` for type annotations
|
38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None:
29 | def f(x: "Union[str, int, Union[float, bytes]]") -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
39 | ...
30 | ...
|
= help: Convert to `X | Y`
Safe fix
35 35 | ...
36 36 |
37 37 |
38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
38 |+def f(x: "str | int | Union[float, bytes]") -> None:
39 39 | ...
40 40 |
41 41 |
26 26 | ...
27 27 |
28 28 |
29 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
29 |+def f(x: "str | int | Union[float, bytes]") -> None:
30 30 | ...
31 31 |
32 32 |
UP007.py:38:27: UP007 [*] Use `X | Y` for type annotations
UP007.py:29:27: UP007 [*] Use `X | Y` for type annotations
|
38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None:
29 | def f(x: "Union[str, int, Union[float, bytes]]") -> None:
| ^^^^^^^^^^^^^^^^^^^ UP007
39 | ...
30 | ...
|
= help: Convert to `X | Y`
Safe fix
35 35 | ...
36 36 |
37 37 |
38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
38 |+def f(x: "Union[str, int, float | bytes]") -> None:
39 39 | ...
40 40 |
41 41 |
26 26 | ...
27 27 |
28 28 |
29 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
29 |+def f(x: "Union[str, int, float | bytes]") -> None:
30 30 | ...
31 31 |
32 32 |
UP007.py:42:11: UP007 [*] Use `X | Y` for type annotations
UP007.py:33:11: UP007 [*] Use `X | Y` for type annotations
|
42 | def f(x: "typing.Union[str, int]") -> None:
33 | def f(x: "typing.Union[str, int]") -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP007
43 | ...
34 | ...
|
= help: Convert to `X | Y`
Safe fix
39 39 | ...
40 40 |
41 41 |
42 |-def f(x: "typing.Union[str, int]") -> None:
42 |+def f(x: "str | int") -> None:
43 43 | ...
44 44 |
45 45 |
30 30 | ...
31 31 |
32 32 |
33 |-def f(x: "typing.Union[str, int]") -> None:
33 |+def f(x: "str | int") -> None:
34 34 | ...
35 35 |
36 36 |
UP007.py:55:8: UP007 [*] Use `X | Y` for type annotations
UP007.py:46:9: UP007 Use `X | Y` for type annotations
|
54 | def f() -> None:
55 | x: Optional[str]
| ^^^^^^^^^^^^^ UP007
56 | x = Optional[str]
|
= help: Convert to `X | Y`
Safe fix
52 52 |
53 53 |
54 54 | def f() -> None:
55 |- x: Optional[str]
55 |+ x: str | None
56 56 | x = Optional[str]
57 57 |
58 58 | x = Union[str, int]
UP007.py:56:9: UP007 Use `X | Y` for type annotations
|
54 | def f() -> None:
55 | x: Optional[str]
56 | x = Optional[str]
| ^^^^^^^^^^^^^ UP007
57 |
58 | x = Union[str, int]
|
= help: Convert to `X | Y`
UP007.py:58:9: UP007 Use `X | Y` for type annotations
|
56 | x = Optional[str]
57 |
58 | x = Union[str, int]
45 | def f() -> None:
46 | x = Union[str, int]
| ^^^^^^^^^^^^^^^ UP007
59 | x = Union["str", "int"]
60 | x: Union[str, int]
47 | x = Union["str", "int"]
48 | x: Union[str, int]
|
= help: Convert to `X | Y`
UP007.py:60:8: UP007 [*] Use `X | Y` for type annotations
UP007.py:48:8: UP007 [*] Use `X | Y` for type annotations
|
58 | x = Union[str, int]
59 | x = Union["str", "int"]
60 | x: Union[str, int]
46 | x = Union[str, int]
47 | x = Union["str", "int"]
48 | x: Union[str, int]
| ^^^^^^^^^^^^^^^ UP007
61 | x: Union["str", "int"]
49 | x: Union["str", "int"]
|
= help: Convert to `X | Y`
Safe fix
57 57 |
58 58 | x = Union[str, int]
59 59 | x = Union["str", "int"]
60 |- x: Union[str, int]
60 |+ x: str | int
61 61 | x: Union["str", "int"]
62 62 |
63 63 |
45 45 | def f() -> None:
46 46 | x = Union[str, int]
47 47 | x = Union["str", "int"]
48 |- x: Union[str, int]
48 |+ x: str | int
49 49 | x: Union["str", "int"]
50 50 |
51 51 |
UP007.py:61:8: UP007 [*] Use `X | Y` for type annotations
UP007.py:49:8: UP007 [*] Use `X | Y` for type annotations
|
59 | x = Union["str", "int"]
60 | x: Union[str, int]
61 | x: Union["str", "int"]
47 | x = Union["str", "int"]
48 | x: Union[str, int]
49 | x: Union["str", "int"]
| ^^^^^^^^^^^^^^^^^^^ UP007
|
= help: Convert to `X | Y`
Safe fix
58 58 | x = Union[str, int]
59 59 | x = Union["str", "int"]
60 60 | x: Union[str, int]
61 |- x: Union["str", "int"]
61 |+ x: "str" | "int"
62 62 |
63 63 |
64 64 | def f(x: Union[int : float]) -> None:
46 46 | x = Union[str, int]
47 47 | x = Union["str", "int"]
48 48 | x: Union[str, int]
49 |- x: Union["str", "int"]
49 |+ x: "str" | "int"
50 50 |
51 51 |
52 52 | def f(x: Union[int : float]) -> None:
UP007.py:52:10: UP007 Use `X | Y` for type annotations
|
52 | def f(x: Union[int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP007
53 | ...
|
= help: Convert to `X | Y`
UP007.py:56:10: UP007 Use `X | Y` for type annotations
|
56 | def f(x: Union[str, int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^ UP007
57 | ...
|
= help: Convert to `X | Y`
UP007.py:60:10: UP007 Use `X | Y` for type annotations
|
60 | def f(x: Union[x := int]) -> None:
| ^^^^^^^^^^^^^^^ UP007
61 | ...
|
= help: Convert to `X | Y`
UP007.py:64:10: UP007 Use `X | Y` for type annotations
|
64 | def f(x: Union[int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP007
64 | def f(x: Union[str, x := int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
65 | ...
|
= help: Convert to `X | Y`
UP007.py:68:10: UP007 Use `X | Y` for type annotations
|
68 | def f(x: Union[str, int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^ UP007
68 | def f(x: Union[lambda: int]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP007
69 | ...
|
= help: Convert to `X | Y`
UP007.py:72:10: UP007 Use `X | Y` for type annotations
|
72 | def f(x: Union[x := int]) -> None:
| ^^^^^^^^^^^^^^^ UP007
72 | def f(x: Union[str, lambda: int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^ UP007
73 | ...
|
= help: Convert to `X | Y`
UP007.py:76:10: UP007 Use `X | Y` for type annotations
UP007.py:83:10: UP007 [*] Use `X | Y` for type annotations
|
76 | def f(x: Union[str, x := int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
77 | ...
82 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
83 | def f(x: Union[int, str, bytes]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP007
84 | ...
|
= help: Convert to `X | Y`
UP007.py:80:10: UP007 Use `X | Y` for type annotations
|
80 | def f(x: Union[lambda: int]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP007
81 | ...
|
= help: Convert to `X | Y`
UP007.py:84:10: UP007 Use `X | Y` for type annotations
|
84 | def f(x: Union[str, lambda: int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^ UP007
85 | ...
|
= help: Convert to `X | Y`
UP007.py:88:10: UP007 Use `X | Y` for type annotations
|
88 | def f(x: Optional[int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ UP007
89 | ...
|
= help: Convert to `X | Y`
UP007.py:92:10: UP007 Use `X | Y` for type annotations
|
92 | def f(x: Optional[str, int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
93 | ...
|
= help: Convert to `X | Y`
UP007.py:96:10: UP007 Use `X | Y` for type annotations
|
96 | def f(x: Optional[int, float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
97 | ...
|
= help: Convert to `X | Y`
UP007.py:102:28: UP007 [*] Use `X | Y` for type annotations
|
100 | # Regression test for: https://github.com/astral-sh/ruff/issues/7131
101 | class ServiceRefOrValue:
102 | service_specification: Optional[
| ____________________________^
103 | | list[ServiceSpecificationRef]
104 | | | list[ServiceSpecification]
105 | | ] = None
| |_____^ UP007
|
= help: Convert to `X | Y`
Safe fix
99 99 |
100 100 | # Regression test for: https://github.com/astral-sh/ruff/issues/7131
101 101 | class ServiceRefOrValue:
102 |- service_specification: Optional[
103 |- list[ServiceSpecificationRef]
104 |- | list[ServiceSpecification]
105 |- ] = None
102 |+ service_specification: list[ServiceSpecificationRef] | list[ServiceSpecification] | None = None
106 103 |
107 104 |
108 105 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
UP007.py:110:28: UP007 [*] Use `X | Y` for type annotations
|
108 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
109 | class ServiceRefOrValue:
110 | service_specification: Optional[str]is not True = None
| ^^^^^^^^^^^^^ UP007
|
= help: Convert to `X | Y`
Safe fix
107 107 |
108 108 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
109 109 | class ServiceRefOrValue:
110 |- service_specification: Optional[str]is not True = None
110 |+ service_specification: str | None is not True = None
111 111 |
112 112 |
113 113 | # Regression test for: https://github.com/astral-sh/ruff/issues/7452
UP007.py:120:10: UP007 [*] Use `X | Y` for type annotations
|
119 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
120 | def f(x: Union[int, str, bytes]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^ UP007
121 | ...
|
= help: Convert to `X | Y`
Safe fix
117 117 |
118 118 |
119 119 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
120 |-def f(x: Union[int, str, bytes]) -> None:
120 |+def f(x: int | str | bytes) -> None:
121 121 | ...
80 80 |
81 81 |
82 82 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
83 |-def f(x: Union[int, str, bytes]) -> None:
83 |+def f(x: int | str | bytes) -> None:
84 84 | ...

View file

@ -0,0 +1,151 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text
---
UP045.py:5:10: UP007 [*] Use `X | Y` for type annotations
|
5 | def f(x: Optional[str]) -> None:
| ^^^^^^^^^^^^^ UP007
6 | ...
|
= help: Convert to `X | Y`
Safe fix
2 2 | from typing import Optional
3 3 |
4 4 |
5 |-def f(x: Optional[str]) -> None:
5 |+def f(x: str | None) -> None:
6 6 | ...
7 7 |
8 8 |
UP045.py:9:10: UP007 [*] Use `X | Y` for type annotations
|
9 | def f(x: typing.Optional[str]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
10 | ...
|
= help: Convert to `X | Y`
Safe fix
6 6 | ...
7 7 |
8 8 |
9 |-def f(x: typing.Optional[str]) -> None:
9 |+def f(x: str | None) -> None:
10 10 | ...
11 11 |
12 12 |
UP045.py:14:8: UP007 [*] Use `X | Y` for type annotations
|
13 | def f() -> None:
14 | x: Optional[str]
| ^^^^^^^^^^^^^ UP007
15 | x = Optional[str]
|
= help: Convert to `X | Y`
Safe fix
11 11 |
12 12 |
13 13 | def f() -> None:
14 |- x: Optional[str]
14 |+ x: str | None
15 15 | x = Optional[str]
16 16 |
17 17 |
UP045.py:15:9: UP007 Use `X | Y` for type annotations
|
13 | def f() -> None:
14 | x: Optional[str]
15 | x = Optional[str]
| ^^^^^^^^^^^^^ UP007
|
= help: Convert to `X | Y`
UP045.py:18:15: UP007 [*] Use `X | Y` for type annotations
|
18 | def f(x: list[Optional[int]]) -> None:
| ^^^^^^^^^^^^^ UP007
19 | ...
|
= help: Convert to `X | Y`
Safe fix
15 15 | x = Optional[str]
16 16 |
17 17 |
18 |-def f(x: list[Optional[int]]) -> None:
18 |+def f(x: list[int | None]) -> None:
19 19 | ...
20 20 |
21 21 |
UP045.py:22:10: UP007 Use `X | Y` for type annotations
|
22 | def f(x: Optional[int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ UP007
23 | ...
|
= help: Convert to `X | Y`
UP045.py:26:10: UP007 Use `X | Y` for type annotations
|
26 | def f(x: Optional[str, int : float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
27 | ...
|
= help: Convert to `X | Y`
UP045.py:30:10: UP007 Use `X | Y` for type annotations
|
30 | def f(x: Optional[int, float]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
31 | ...
|
= help: Convert to `X | Y`
UP045.py:36:28: UP007 [*] Use `X | Y` for type annotations
|
34 | # Regression test for: https://github.com/astral-sh/ruff/issues/7131
35 | class ServiceRefOrValue:
36 | service_specification: Optional[
| ____________________________^
37 | | list[ServiceSpecificationRef]
38 | | | list[ServiceSpecification]
39 | | ] = None
| |_____^ UP007
|
= help: Convert to `X | Y`
Safe fix
33 33 |
34 34 | # Regression test for: https://github.com/astral-sh/ruff/issues/7131
35 35 | class ServiceRefOrValue:
36 |- service_specification: Optional[
37 |- list[ServiceSpecificationRef]
38 |- | list[ServiceSpecification]
39 |- ] = None
36 |+ service_specification: list[ServiceSpecificationRef] | list[ServiceSpecification] | None = None
40 37 |
41 38 |
42 39 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
UP045.py:44:28: UP007 [*] Use `X | Y` for type annotations
|
42 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
43 | class ServiceRefOrValue:
44 | service_specification: Optional[str]is not True = None
| ^^^^^^^^^^^^^ UP007
|
= help: Convert to `X | Y`
Safe fix
41 41 |
42 42 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201
43 43 | class ServiceRefOrValue:
44 |- service_specification: Optional[str]is not True = None
44 |+ service_specification: str | None is not True = None

View file

@ -0,0 +1,5 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text
---

View file

@ -374,7 +374,7 @@ mod tests {
fn redirects() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/redirects.py"),
&settings::LinterSettings::for_rules(vec![Rule::NonPEP604Annotation]),
&settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationUnion),
)?;
assert_messages!(diagnostics);
Ok(())

1
ruff.schema.json generated
View file

@ -4134,6 +4134,7 @@
"UP042",
"UP043",
"UP044",
"UP045",
"W",
"W1",
"W19",