diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py index af1c1ca35b..f933a4a995 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py @@ -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]]: diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py new file mode 100644 index 0000000000..904c1510ea --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py @@ -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 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index a312e55263..999cde30c1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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); } } } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 155f5e9b86..f3f626c9a7 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -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), diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index f583089abb..95f02be7da 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -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); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index e4fe1f16d8..cd389116f8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -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 { + 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(_) => { diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap index 982ce1d0dd..c238f46be4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap @@ -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 | ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap new file mode 100644 index 0000000000..3579871395 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap new file mode 100644 index 0000000000..c996a51d38 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +snapshot_kind: text +--- + diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 3b529bc26f..73d5fe0434 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -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(()) diff --git a/ruff.schema.json b/ruff.schema.json index 5b44257bfb..a571b40a6c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4134,6 +4134,7 @@ "UP042", "UP043", "UP044", + "UP045", "W", "W1", "W19",