diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 65361b4f3568a3..8e4b23aec639c0 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -3645,6 +3645,7 @@ d: Literal[None,] | Literal[None] .args(["--stdin-filename", "test.py"]) .arg("--preview") .arg("--diff") + .arg("--unsafe-fixes") .arg("-") .pass_stdin(snippet), @" success: false diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index f3f23663d9cdf4..40e18a5b4b23b5 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -86,3 +86,7 @@ def good_func(arg1: Literal[int] | None): print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1 print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ + +# Regression test for https://github.com/astral-sh/ruff/issues/20729. +# Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. +options = Literal["foo", "bar", None] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index b3e35c21c23366..d4ccea5ae01ca8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -39,7 +39,11 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ``` /// /// ## Fix safety and availability -/// This rule's fix is marked as safe unless the literal contains comments. +/// In Python files, this rule's fix is marked as unsafe because replacing +/// `Literal[...]` can change runtime-visible annotation objects, such as the +/// result of `typing.get_args`. +/// +/// In stub files, the fix is marked as safe unless the literal contains comments. /// /// There is currently no fix available when applying the fix would lead to /// a `TypeError` from an expression of the form `None | None` or when we @@ -62,10 +66,10 @@ impl Violation for RedundantNoneLiteral { match self.union_kind { UnionKind::NoUnion => "Use `None` rather than `Literal[None]`".to_string(), UnionKind::TypingOptional => { - "Use `Optional[Literal[...]]` rather than `Literal[None, ...]` ".to_string() + "Use `Optional[Literal[...]]` rather than `Literal[None, ...]`".to_string() } UnionKind::BitOr => { - "Use `Literal[...] | None` rather than `Literal[None, ...]` ".to_string() + "Use `Literal[...] | None` rather than `Literal[None, ...]`".to_string() } } } @@ -192,7 +196,9 @@ fn create_fix( } } - let applicability = if checker.comment_ranges().intersects(literal_expr.range()) { + let applicability = if checker.comment_ranges().intersects(literal_expr.range()) + || !checker.source_type.is_stub() + { Applicability::Unsafe } else { Applicability::Safe diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap index 64a0ea6d4dceab..95ccdb7063d92a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap @@ -15,6 +15,7 @@ help: Replace with `None` 4 + def func1(arg1: None): 5 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:8:25 @@ -30,6 +31,7 @@ help: Replace with `None` 8 + def func2(arg1: None | int): 9 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:12:24 @@ -45,8 +47,9 @@ help: Replace with `None` 12 + def func3() -> None: 13 | ... | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:16:30 | 16 | def func4(arg1: Literal[int, None, float]): @@ -60,6 +63,7 @@ help: Replace with `Literal[...] | None` 16 + def func4(arg1: Literal[int, float] | None): 17 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:20:25 @@ -75,6 +79,7 @@ help: Replace with `None` 20 + def func5(arg1: None): 21 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:20:31 @@ -90,8 +95,9 @@ help: Replace with `None` 20 + def func5(arg1: None): 21 | ... | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:26:5 | 24 | def func6(arg1: Literal[ @@ -157,6 +163,7 @@ help: Replace with `None` 42 + def func9(arg1: Union[None, None]): 43 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:52:9 @@ -173,8 +180,9 @@ help: Replace with `None` 52 + None # Y061 None inside "Literal[]" expression. Replace with "None" 53 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:53:15 | 51 | # From flake8-pyi @@ -191,6 +199,7 @@ help: Replace with `Literal[...] | None` 53 + Literal[True] | None # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" 54 | | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:62:9 @@ -208,6 +217,7 @@ help: Replace with `None` 62 + None # Y061 None inside "Literal[]" expression. Replace with "None" 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:62:15 @@ -225,8 +235,9 @@ help: Replace with `None` 62 + None # Y061 None inside "Literal[]" expression. Replace with "None" 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:63:12 | 61 | # only emit Y061... @@ -243,8 +254,9 @@ help: Replace with `Literal[...] | None` 63 + Literal[1, "foo"] | None # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" 64 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:63:25 | 61 | # only emit Y061... @@ -261,8 +273,9 @@ help: Replace with `Literal[...] | None` 63 + Literal[1, "foo"] | None # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" 64 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:68:9 | 66 | # and there are no None members in the Literal[] slice, @@ -277,8 +290,9 @@ help: Replace with `Literal[...] | None` 68 + Literal[True, True] | None # Y062 Duplicate "Literal[]" member "True" 69 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:68:21 | 66 | # and there are no None members in the Literal[] slice, @@ -293,6 +307,7 @@ help: Replace with `Literal[...] | None` 68 + Literal[True, True] | None # Y062 Duplicate "Literal[]" member "True" 69 | | +note: This is an unsafe fix and may change runtime behavior PYI061 Use `None` rather than `Literal[None]` --> PYI061.py:72:12 @@ -333,6 +348,7 @@ help: Replace with `None` 74 + z: Union[None, None] 75 | | +note: This is an unsafe fix and may change runtime behavior PYI061 Use `None` rather than `Literal[None]` --> PYI061.py:76:18 @@ -392,7 +408,7 @@ PYI061 Use `None` rather than `Literal[None]` | help: Replace with `None` -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:83:18 | 82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265) @@ -408,8 +424,9 @@ help: Replace with `Literal[...] | None` 83 + print((Literal[1] | None).__dict__) # Should become (Literal[1] | None).__dict__ 84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method() | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:84:18 | 82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265) @@ -426,8 +443,9 @@ help: Replace with `Literal[...] | None` 84 + print((Literal[1] | None).method()) # Should become (Literal[1] | None).method() 85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0] | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:85:18 | 83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__ @@ -444,8 +462,9 @@ help: Replace with `Literal[...] | None` 85 + print((Literal[1] | None)[0]) # Should become (Literal[1] | None)[0] 86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1 | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:86:18 | 84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method() @@ -462,8 +481,9 @@ help: Replace with `Literal[...] | None` 86 + print((Literal[1] | None) + 1) # Should become (Literal[1] | None) + 1 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:87:18 | 85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0] @@ -479,18 +499,39 @@ help: Replace with `Literal[...] | None` 87 + print((Literal[1] | None) * 2) # Should become (Literal[1] | None) * 2 88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.py:88:19 | 86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ | ^^^^ +89 | +90 | # Regression test for https://github.com/astral-sh/ruff/issues/20729. | help: Replace with `Literal[...] | None` | 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 - print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ 88 + print((Literal[1] | None).__dict__) # Should become ((Literal[1] | None)).__dict__ +89 | + | +note: This is an unsafe fix and may change runtime behavior + +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` + --> PYI061.py:92:33 | +90 | # Regression test for https://github.com/astral-sh/ruff/issues/20729. +91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. +92 | options = Literal["foo", "bar", None] + | ^^^^ + | +help: Replace with `Literal[...] | None` + | +91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. + - options = Literal["foo", "bar", None] +92 + options = Literal["foo", "bar"] | None + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap index 64718817406fe9..2dc7d767869164 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap @@ -43,7 +43,7 @@ help: Replace with `None` 11 | | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:13:30 | 13 | def func4(arg1: Literal[int, None, float]): ... @@ -85,7 +85,7 @@ help: Replace with `None` 17 | | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:21:5 | 19 | def func6(arg1: Literal[ @@ -165,7 +165,7 @@ help: Replace with `None` 43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:43:15 | 41 | # From flake8-pyi diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap index 993ed95ea88d50..ea86e80b20ef2c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap @@ -15,6 +15,7 @@ help: Replace with `None` 4 + def func1(arg1: None): 5 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:8:25 @@ -30,6 +31,7 @@ help: Replace with `None` 8 + def func2(arg1: None | int): 9 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:12:24 @@ -45,8 +47,9 @@ help: Replace with `None` 12 + def func3() -> None: 13 | ... | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:16:30 | 16 | def func4(arg1: Literal[int, None, float]): @@ -64,6 +67,7 @@ help: Replace with `Optional[Literal[...]]` 16 + def func4(arg1: Optional[Literal[int, float]]): 17 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:20:25 @@ -79,6 +83,7 @@ help: Replace with `None` 20 + def func5(arg1: None): 21 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:20:31 @@ -94,8 +99,9 @@ help: Replace with `None` 20 + def func5(arg1: None): 21 | ... | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:26:5 | 24 | def func6(arg1: Literal[ @@ -165,6 +171,7 @@ help: Replace with `None` 42 + def func9(arg1: Union[None, None]): 43 | ... | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:52:9 @@ -181,8 +188,9 @@ help: Replace with `None` 52 + None # Y061 None inside "Literal[]" expression. Replace with "None" 53 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:53:15 | 51 | # From flake8-pyi @@ -203,6 +211,7 @@ help: Replace with `Optional[Literal[...]]` 53 + Optional[Literal[True]] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" 54 | | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:62:9 @@ -220,6 +229,7 @@ help: Replace with `None` 62 + None # Y061 None inside "Literal[]" expression. Replace with "None" 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | +note: This is an unsafe fix and may change runtime behavior PYI061 [*] Use `None` rather than `Literal[None]` --> PYI061.py:62:15 @@ -237,8 +247,9 @@ help: Replace with `None` 62 + None # Y061 None inside "Literal[]" expression. Replace with "None" 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:63:12 | 61 | # only emit Y061... @@ -259,8 +270,9 @@ help: Replace with `Optional[Literal[...]]` 63 + Optional[Literal[1, "foo"]] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" 64 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:63:25 | 61 | # only emit Y061... @@ -281,8 +293,9 @@ help: Replace with `Optional[Literal[...]]` 63 + Optional[Literal[1, "foo"]] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" 64 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:68:9 | 66 | # and there are no None members in the Literal[] slice, @@ -301,8 +314,9 @@ help: Replace with `Optional[Literal[...]]` 68 + Optional[Literal[True, True]] # Y062 Duplicate "Literal[]" member "True" 69 | | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:68:21 | 66 | # and there are no None members in the Literal[] slice, @@ -321,6 +335,7 @@ help: Replace with `Optional[Literal[...]]` 68 + Optional[Literal[True, True]] # Y062 Duplicate "Literal[]" member "True" 69 | | +note: This is an unsafe fix and may change runtime behavior PYI061 Use `None` rather than `Literal[None]` --> PYI061.py:72:12 @@ -361,6 +376,7 @@ help: Replace with `None` 74 + z: Union[None, None] 75 | | +note: This is an unsafe fix and may change runtime behavior PYI061 Use `None` rather than `Literal[None]` --> PYI061.py:76:18 @@ -420,7 +436,7 @@ PYI061 Use `None` rather than `Literal[None]` | help: Replace with `None` -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:83:18 | 82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265) @@ -440,8 +456,9 @@ help: Replace with `Optional[Literal[...]]` 83 + print(Optional[Literal[1]].__dict__) # Should become (Literal[1] | None).__dict__ 84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method() | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:84:18 | 82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265) @@ -462,8 +479,9 @@ help: Replace with `Optional[Literal[...]]` 84 + print(Optional[Literal[1]].method()) # Should become (Literal[1] | None).method() 85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0] | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:85:18 | 83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__ @@ -484,8 +502,9 @@ help: Replace with `Optional[Literal[...]]` 85 + print(Optional[Literal[1]][0]) # Should become (Literal[1] | None)[0] 86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1 | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:86:18 | 84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method() @@ -506,8 +525,9 @@ help: Replace with `Optional[Literal[...]]` 86 + print(Optional[Literal[1]] + 1) # Should become (Literal[1] | None) + 1 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:87:18 | 85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0] @@ -527,14 +547,17 @@ help: Replace with `Optional[Literal[...]]` 87 + print(Optional[Literal[1]] * 2) # Should become (Literal[1] | None) * 2 88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ | +note: This is an unsafe fix and may change runtime behavior -PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` --> PYI061.py:88:19 | 86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ | ^^^^ +89 | +90 | # Regression test for https://github.com/astral-sh/ruff/issues/20729. | help: Replace with `Optional[Literal[...]]` | @@ -545,4 +568,26 @@ help: Replace with `Optional[Literal[...]]` 87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2 - print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__ 88 + print((Optional[Literal[1]]).__dict__) # Should become ((Literal[1] | None)).__dict__ +89 | + | +note: This is an unsafe fix and may change runtime behavior + +PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` + --> PYI061.py:92:33 + | +90 | # Regression test for https://github.com/astral-sh/ruff/issues/20729. +91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. +92 | options = Literal["foo", "bar", None] + | ^^^^ | +help: Replace with `Optional[Literal[...]]` + | + - from typing import Literal, Union +1 + from typing import Literal, Union, Optional +2 | +-------------------------------------------------------------------------------- +91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. + - options = Literal["foo", "bar", None] +92 + options = Optional[Literal["foo", "bar"]] + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap index 64718817406fe9..2dc7d767869164 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap @@ -43,7 +43,7 @@ help: Replace with `None` 11 | | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:13:30 | 13 | def func4(arg1: Literal[int, None, float]): ... @@ -85,7 +85,7 @@ help: Replace with `None` 17 | | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:21:5 | 19 | def func6(arg1: Literal[ @@ -165,7 +165,7 @@ help: Replace with `None` 43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | -PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` +PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` --> PYI061.pyi:43:15 | 41 | # From flake8-pyi