[pyupgrade] Split UP007 to two individual rules for Union and Optional (UP007, UP045) (#15313)

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 import typing
from typing import Optional
from typing import Union 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: 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: def f() -> None:
x: Optional[str]
x = Optional[str]
x = Union[str, int] x = Union[str, int]
x = Union["str", "int"] 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 # Regression test for: https://github.com/astral-sh/ruff/issues/7452
class Collection(Protocol[*_B0]): class Collection(Protocol[*_B0]):
def __iter__(self) -> Iterator[Union[*_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[...] // Ex) Optional[...], Union[...]
if checker.any_enabled(&[ if checker.any_enabled(&[
Rule::FutureRewritableTypeAnnotation, Rule::FutureRewritableTypeAnnotation,
Rule::NonPEP604Annotation, Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
]) { ]) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic) 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() if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py310 || checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37 || (checker.settings.target_version >= PythonVersion::Py37
@ -51,7 +55,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing) && !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, "004") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessObjectInheritance),
(Pyupgrade, "005") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedUnittestAlias), (Pyupgrade, "005") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedUnittestAlias),
(Pyupgrade, "006") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP585Annotation), (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, "008") => (RuleGroup::Stable, rules::pyupgrade::rules::SuperCallWithParameters),
(Pyupgrade, "009") => (RuleGroup::Stable, rules::pyupgrade::rules::UTF8EncodingDeclaration), (Pyupgrade, "009") => (RuleGroup::Stable, rules::pyupgrade::rules::UTF8EncodingDeclaration),
(Pyupgrade, "010") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryFutureImport), (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, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
(Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs), (Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
(Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack), (Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack),
(Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional),
// pydocstyle // pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), (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_1.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_3.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::NonPEP604Isinstance, Path::new("UP038.py"))]
#[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))]
#[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))]
@ -111,6 +112,19 @@ mod tests {
Ok(()) 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] #[test]
fn async_timeout_error_alias_not_applied_py310() -> Result<()> { fn async_timeout_error_alias_not_applied_py310() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(
@ -201,7 +215,10 @@ mod tests {
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::Py37, target_version: PythonVersion::Py37,
..settings::LinterSettings::for_rule(Rule::NonPEP604Annotation) ..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
])
}, },
)?; )?;
assert_messages!(diagnostics); assert_messages!(diagnostics);
@ -214,7 +231,10 @@ mod tests {
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::Py310, target_version: PythonVersion::Py310,
..settings::LinterSettings::for_rule(Rule::NonPEP604Annotation) ..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
])
}, },
)?; )?;
assert_messages!(diagnostics); 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_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union}; use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
use ruff_python_ast::{self as ast, Expr}; 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 ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix::edits::pad; use crate::fix::edits::pad;
use crate::settings::types::PythonVersion; use crate::settings::types::{PreviewMode, PythonVersion};
/// ## What it does /// ## What it does
/// Check for type annotations that can be rewritten based on [PEP 604] syntax. /// 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 /// 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 /// ## Fix safety
/// This rule's fix is marked as unsafe, as it may lead to runtime errors when /// 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, /// 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/ /// [PEP 604]: https://peps.python.org/pep-0604/
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
pub(crate) struct NonPEP604Annotation; pub(crate) struct NonPEP604AnnotationUnion;
impl Violation for NonPEP604Annotation { impl Violation for NonPEP604AnnotationUnion {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats] #[derive_message_formats]
@ -65,8 +72,64 @@ impl Violation for NonPEP604Annotation {
} }
} }
/// UP007 /// ## What it does
pub(crate) fn use_pep604_annotation( /// 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, checker: &mut Checker,
expr: &Expr, expr: &Expr,
slice: &Expr, slice: &Expr,
@ -86,7 +149,23 @@ pub(crate) fn use_pep604_annotation(
match operator { match operator {
Pep604Operator::Optional => { 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 { if fixable {
match slice { match slice {
Expr::Tuple(_) => { Expr::Tuple(_) => {
@ -110,7 +189,11 @@ pub(crate) fn use_pep604_annotation(
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
Pep604Operator::Union => { 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 { if fixable {
match slice { match slice {
Expr::Slice(_) => { Expr::Slice(_) => {

View file

@ -2,432 +2,296 @@
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text 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: 5 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
7 | ... 6 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
3 3 | from typing import Union 2 2 | from typing import Union
3 3 |
4 4 | 4 4 |
5 5 | 5 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
6 |-def f(x: Optional[str]) -> None: 5 |+def f(x: str | int | Union[float, bytes]) -> None:
6 |+def f(x: str | None) -> None: 6 6 | ...
7 7 | ... 7 7 |
8 8 | 8 8 |
9 9 |
UP007.py:10:10: UP007 [*] Use `X | Y` for type annotations UP007.py:5:26: UP007 [*] Use `X | Y` for type annotations
| |
10 | def f(x: typing.Optional[str]) -> None: 5 | def f(x: Union[str, int, Union[float, bytes]]) -> None:
| ^^^^^^^^^^^^^^^^^^^^ UP007
11 | ...
|
= 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 |
UP007.py:14: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 | ^^^^^^^^^^^^^^^^^^^ UP007
15 | ... 6 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
11 11 | ... 2 2 | from typing import Union
12 12 | 3 3 |
13 13 | 4 4 |
14 |-def f(x: Union[str, int, Union[float, bytes]]) -> None: 5 |-def f(x: Union[str, int, Union[float, bytes]]) -> None:
14 |+def f(x: Union[str, int, float | bytes]) -> None: 5 |+def f(x: Union[str, int, float | bytes]) -> None:
15 15 | ... 6 6 | ...
16 16 | 7 7 |
17 17 | 8 8 |
UP007.py:18:10: UP007 [*] Use `X | Y` for type annotations UP007.py:9: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 | ^^^^^^^^^^^^^^^^^^^^^^ UP007
19 | ... 10 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
15 15 | ... 6 6 | ...
16 16 | 7 7 |
17 17 | 8 8 |
18 |-def f(x: typing.Union[str, int]) -> None: 9 |-def f(x: typing.Union[str, int]) -> None:
18 |+def f(x: str | int) -> None: 9 |+def f(x: str | int) -> None:
19 19 | ... 10 10 | ...
20 20 | 11 11 |
21 21 | 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 | ^^^^^^^^^^^^^^^^^^^^^^^^ UP007
23 | ... 14 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
19 19 | ... 10 10 | ...
20 20 | 11 11 |
21 21 | 12 12 |
22 |-def f(x: typing.Union[(str, int)]) -> None: 13 |-def f(x: typing.Union[(str, int)]) -> None:
22 |+def f(x: str | int) -> None: 13 |+def f(x: str | int) -> None:
23 23 | ... 14 14 | ...
24 24 | 15 15 |
25 25 | 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 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
27 | ... 18 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
23 23 | ... 14 14 | ...
24 24 | 15 15 |
25 25 | 16 16 |
26 |-def f(x: typing.Union[(str, int), float]) -> None: 17 |-def f(x: typing.Union[(str, int), float]) -> None:
26 |+def f(x: str | int | float) -> None: 17 |+def f(x: str | int | float) -> None:
27 27 | ... 18 18 | ...
28 28 | 19 19 |
29 29 | 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 | ^^^^^^^^^^^^^^^^^^^^ UP007
31 | ... 22 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
27 27 | ... 18 18 | ...
28 28 | 19 19 |
29 29 | 20 20 |
30 |-def f(x: typing.Union[(int,)]) -> None: 21 |-def f(x: typing.Union[(int,)]) -> None:
30 |+def f(x: int) -> None: 21 |+def f(x: int) -> None:
31 31 | ... 22 22 | ...
32 32 | 23 23 |
33 33 | 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 | ^^^^^^^^^^^^^^^^ UP007
35 | ... 26 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
31 31 | ... 22 22 | ...
32 32 | 23 23 |
33 33 | 24 24 |
34 |-def f(x: typing.Union[()]) -> None: 25 |-def f(x: typing.Union[()]) -> None:
34 |+def f(x: ()) -> None: 25 |+def f(x: ()) -> None:
35 35 | ... 26 26 | ...
36 36 | 27 27 |
37 37 | 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 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
39 | ... 30 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
35 35 | ... 26 26 | ...
36 36 | 27 27 |
37 37 | 28 28 |
38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: 29 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
38 |+def f(x: "str | int | Union[float, bytes]") -> None: 29 |+def f(x: "str | int | Union[float, bytes]") -> None:
39 39 | ... 30 30 | ...
40 40 | 31 31 |
41 41 | 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 | ^^^^^^^^^^^^^^^^^^^ UP007
39 | ... 30 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
35 35 | ... 26 26 | ...
36 36 | 27 27 |
37 37 | 28 28 |
38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: 29 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None:
38 |+def f(x: "Union[str, int, float | bytes]") -> None: 29 |+def f(x: "Union[str, int, float | bytes]") -> None:
39 39 | ... 30 30 | ...
40 40 | 31 31 |
41 41 | 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 | ^^^^^^^^^^^^^^^^^^^^^^ UP007
43 | ... 34 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
39 39 | ... 30 30 | ...
40 40 | 31 31 |
41 41 | 32 32 |
42 |-def f(x: "typing.Union[str, int]") -> None: 33 |-def f(x: "typing.Union[str, int]") -> None:
42 |+def f(x: "str | int") -> None: 33 |+def f(x: "str | int") -> None:
43 43 | ... 34 34 | ...
44 44 | 35 35 |
45 45 | 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: 45 | def f() -> None:
55 | x: Optional[str] 46 | x = Union[str, int]
| ^^^^^^^^^^^^^ 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]
| ^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^ UP007
59 | x = Union["str", "int"] 47 | x = Union["str", "int"]
60 | x: Union[str, int] 48 | x: Union[str, int]
| |
= help: Convert to `X | Y` = 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] 46 | x = Union[str, int]
59 | x = Union["str", "int"] 47 | x = Union["str", "int"]
60 | x: Union[str, int] 48 | x: Union[str, int]
| ^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^ UP007
61 | x: Union["str", "int"] 49 | x: Union["str", "int"]
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
57 57 | 45 45 | def f() -> None:
58 58 | x = Union[str, int] 46 46 | x = Union[str, int]
59 59 | x = Union["str", "int"] 47 47 | x = Union["str", "int"]
60 |- x: Union[str, int] 48 |- x: Union[str, int]
60 |+ x: str | int 48 |+ x: str | int
61 61 | x: Union["str", "int"] 49 49 | x: Union["str", "int"]
62 62 | 50 50 |
63 63 | 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"] 47 | x = Union["str", "int"]
60 | x: Union[str, int] 48 | x: Union[str, int]
61 | x: Union["str", "int"] 49 | x: Union["str", "int"]
| ^^^^^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^^^^^ UP007
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
58 58 | x = Union[str, int] 46 46 | x = Union[str, int]
59 59 | x = Union["str", "int"] 47 47 | x = Union["str", "int"]
60 60 | x: Union[str, int] 48 48 | x: Union[str, int]
61 |- x: Union["str", "int"] 49 |- x: Union["str", "int"]
61 |+ x: "str" | "int" 49 |+ x: "str" | "int"
62 62 | 50 50 |
63 63 | 51 51 |
64 64 | def f(x: Union[int : float]) -> None: 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 UP007.py:64:10: UP007 Use `X | Y` for type annotations
| |
64 | def f(x: Union[int : float]) -> None: 64 | def f(x: Union[str, x := int]) -> None:
| ^^^^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^^^^^^ UP007
65 | ... 65 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
UP007.py:68:10: UP007 Use `X | Y` for type annotations UP007.py:68:10: UP007 Use `X | Y` for type annotations
| |
68 | def f(x: Union[str, int : float]) -> None: 68 | def f(x: Union[lambda: int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^^^^ UP007
69 | ... 69 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
UP007.py:72:10: UP007 Use `X | Y` for type annotations UP007.py:72:10: UP007 Use `X | Y` for type annotations
| |
72 | def f(x: Union[x := int]) -> None: 72 | def f(x: Union[str, lambda: int]) -> None:
| ^^^^^^^^^^^^^^^ UP007 | ^^^^^^^^^^^^^^^^^^^^^^^ UP007
73 | ... 73 | ...
| |
= help: Convert to `X | Y` = 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: 82 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
| ^^^^^^^^^^^^^^^^^^^^ UP007 83 | def f(x: Union[int, str, bytes]) -> None:
77 | ...
|
= 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 | ^^^^^^^^^^^^^^^^^^^^^^ UP007
121 | ... 84 | ...
| |
= help: Convert to `X | Y` = help: Convert to `X | Y`
Safe fix Safe fix
117 117 | 80 80 |
118 118 | 81 81 |
119 119 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609 82 82 | # Regression test for: https://github.com/astral-sh/ruff/issues/8609
120 |-def f(x: Union[int, str, bytes]) -> None: 83 |-def f(x: Union[int, str, bytes]) -> None:
120 |+def f(x: int | str | bytes) -> None: 83 |+def f(x: int | str | bytes) -> None:
121 121 | ... 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<()> { fn redirects() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("ruff/redirects.py"), Path::new("ruff/redirects.py"),
&settings::LinterSettings::for_rules(vec![Rule::NonPEP604Annotation]), &settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationUnion),
)?; )?;
assert_messages!(diagnostics); assert_messages!(diagnostics);
Ok(()) Ok(())

1
ruff.schema.json generated
View file

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