From 62fbd6b1a9806d5f1a6ad43a301c41a9cda46e9f Mon Sep 17 00:00:00 2001 From: Igor Drokin Date: Thu, 21 Aug 2025 20:35:30 +0300 Subject: [PATCH 1/4] feat(UP043): enable rule trigger for stub files --- .../test/fixtures/pyupgrade/UP043.pyi | 27 +++++++++++++++++++ .../src/checkers/ast/analyze/expression.rs | 4 ++- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 + .../rules/unnecessary_default_type_args.rs | 2 +- ...r__rules__pyupgrade__tests__UP043.pyi.snap | 19 +++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi new file mode 100644 index 0000000000000..3f7572fcaa180 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi @@ -0,0 +1,27 @@ +# https://github.com/python/typeshed/blob/main/stubs/networkx/networkx/algorithms/community/louvain.pyi +from _typeshed import Incomplete +from collections.abc import Generator + +from networkx.classes.graph import Graph, _Node +from networkx.utils.backends import _dispatchable +from numpy.random import RandomState + +__all__ = ["louvain_communities", "louvain_partitions"] + +@_dispatchable +def louvain_communities( + G: Graph[_Node], + weight: str | None = "weight", + resolution: float | None = 1, + threshold: float | None = 1e-07, + max_level: int | None = None, + seed: int | RandomState | None = None, +) -> list[set[Incomplete]]: ... +@_dispatchable +def louvain_partitions( + G: Graph[_Node], + weight: str | None = "weight", + resolution: float | None = 1, + threshold: float | None = 1e-07, + seed: int | RandomState | None = None, +) -> Generator[list[set[Incomplete]], None, None]: ... \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 1851959029e64..7682d680a890f 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -142,7 +142,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) { - if checker.target_version() >= PythonVersion::PY313 { + if checker.target_version() >= PythonVersion::PY313 + || checker.semantic().in_stub_file() + { pyupgrade::rules::unnecessary_default_type_args(checker, expr); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 94a40baa9d153..fb620c45e14c1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -100,6 +100,7 @@ mod tests { #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] #[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))] #[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))] + #[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.pyi"))] #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_0.py"))] #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_1.py"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index 1bfebfe727b8b..09cfe4d70c4fe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -7,7 +7,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for unnecessary default type arguments for `Generator` and -/// `AsyncGenerator` on Python 3.13+. +/// `AsyncGenerator` on Python 3.13+ or stubs. /// /// ## Why is this bad? /// Python 3.13 introduced the ability for type parameters to specify default diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap new file mode 100644 index 0000000000000..8ff1e5faf090f --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:27:6 + | +25 | threshold: float | None = 1e-07, +26 | seed: int | RandomState | None = None, +27 | ) -> Generator[list[set[Incomplete]], None, None]: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Remove default type arguments + +ℹ Safe fix +24 24 | resolution: float | None = 1, +25 25 | threshold: float | None = 1e-07, +26 26 | seed: int | RandomState | None = None, +27 |-) -> Generator[list[set[Incomplete]], None, None]: ... + 27 |+) -> Generator[list[set[Incomplete]]]: ... From c190105e3b220a3cbdf8b1edad3ec1c956fd2dc6 Mon Sep 17 00:00:00 2001 From: Igor Drokin Date: Tue, 9 Sep 2025 15:12:08 +0300 Subject: [PATCH 2/4] feat(UP043): gated behind preview --- .../test/fixtures/pyupgrade/UP043.pyi | 86 ++++++++---- .../src/checkers/ast/analyze/expression.rs | 4 +- crates/ruff_linter/src/preview.rs | 6 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 16 ++- .../rules/unnecessary_default_type_args.rs | 2 +- ...r__rules__pyupgrade__tests__UP043.pyi.snap | 19 --- ..._pyupgrade__tests__UP043.pyi__preview.snap | 128 ++++++++++++++++++ 7 files changed, 212 insertions(+), 49 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi__preview.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi index 3f7572fcaa180..5613053d81acb 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.pyi @@ -1,27 +1,59 @@ -# https://github.com/python/typeshed/blob/main/stubs/networkx/networkx/algorithms/community/louvain.pyi -from _typeshed import Incomplete -from collections.abc import Generator - -from networkx.classes.graph import Graph, _Node -from networkx.utils.backends import _dispatchable -from numpy.random import RandomState - -__all__ = ["louvain_communities", "louvain_partitions"] - -@_dispatchable -def louvain_communities( - G: Graph[_Node], - weight: str | None = "weight", - resolution: float | None = 1, - threshold: float | None = 1e-07, - max_level: int | None = None, - seed: int | RandomState | None = None, -) -> list[set[Incomplete]]: ... -@_dispatchable -def louvain_partitions( - G: Graph[_Node], - weight: str | None = "weight", - resolution: float | None = 1, - threshold: float | None = 1e-07, - seed: int | RandomState | None = None, -) -> Generator[list[set[Incomplete]], None, None]: ... \ No newline at end of file +from collections.abc import Generator, AsyncGenerator + + +def func() -> Generator[int, None, None]: + yield 42 + + +def func() -> Generator[int, None]: + yield 42 + + +def func() -> Generator[int]: + yield 42 + + +def func() -> Generator[int, int, int]: + foo = yield 42 + return foo + + +def func() -> Generator[int, int, None]: + _ = yield 42 + return None + + +def func() -> Generator[int, None, int]: + yield 42 + return 42 + + +async def func() -> AsyncGenerator[int, None]: + yield 42 + + +async def func() -> AsyncGenerator[int]: + yield 42 + + +async def func() -> AsyncGenerator[int, int]: + foo = yield 42 + return foo + + +from typing import Generator, AsyncGenerator + + +def func() -> Generator[str, None, None]: + yield "hello" + + +async def func() -> AsyncGenerator[str, None]: + yield "hello" + + +async def func() -> AsyncGenerator[ # type: ignore + str, + None +]: + yield "hello" diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 7682d680a890f..c542c9510a7ae 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -9,6 +9,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::preview::{ is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled, + is_unnecessary_default_type_args_stubs_enabled, }; use crate::registry::Rule; use crate::rules::{ @@ -143,7 +144,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) { if checker.target_version() >= PythonVersion::PY313 - || checker.semantic().in_stub_file() + || is_unnecessary_default_type_args_stubs_enabled(checker.settings()) + && checker.semantic().in_stub_file() { pyupgrade::rules::unnecessary_default_type_args(checker, expr); } diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index f932b2b0592fb..44126d045e5a8 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -260,3 +260,9 @@ pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterS pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index fb620c45e14c1..aa2f24313cfbc 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -100,7 +100,6 @@ mod tests { #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] #[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))] #[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))] - #[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.pyi"))] #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_0.py"))] #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_1.py"))] @@ -358,4 +357,19 @@ mod tests { 2 | from pipes import quote, Template "); } + + #[test] + fn unnecessary_default_type_args_stubs_py312_preview() -> Result<()> { + let snapshot = format!("{}__preview", "UP043.pyi"); + let diagnostics = test_path( + Path::new("pyupgrade/UP043.pyi"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + unresolved_target_version: PythonVersion::PY312.into(), + ..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index 09cfe4d70c4fe..d7cd54424d323 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -7,7 +7,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for unnecessary default type arguments for `Generator` and -/// `AsyncGenerator` on Python 3.13+ or stubs. +/// `AsyncGenerator` on Python 3.13+ or in stubs. /// /// ## Why is this bad? /// Python 3.13 introduced the ability for type parameters to specify default diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap deleted file mode 100644 index 8ff1e5faf090f..0000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP043 [*] Unnecessary default type arguments - --> UP043.pyi:27:6 - | -25 | threshold: float | None = 1e-07, -26 | seed: int | RandomState | None = None, -27 | ) -> Generator[list[set[Incomplete]], None, None]: ... - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Remove default type arguments - -ℹ Safe fix -24 24 | resolution: float | None = 1, -25 25 | threshold: float | None = 1e-07, -26 26 | seed: int | RandomState | None = None, -27 |-) -> Generator[list[set[Incomplete]], None, None]: ... - 27 |+) -> Generator[list[set[Incomplete]]]: ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi__preview.snap new file mode 100644 index 0000000000000..37b80bf63d47c --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.pyi__preview.snap @@ -0,0 +1,128 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:4:15 + | +4 | def func() -> Generator[int, None, None]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +5 | yield 42 + | +help: Remove default type arguments +1 | from collections.abc import Generator, AsyncGenerator +2 | +3 | + - def func() -> Generator[int, None, None]: +4 + def func() -> Generator[int]: +5 | yield 42 +6 | +7 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:8:15 + | +8 | def func() -> Generator[int, None]: + | ^^^^^^^^^^^^^^^^^^^^ +9 | yield 42 + | +help: Remove default type arguments +5 | yield 42 +6 | +7 | + - def func() -> Generator[int, None]: +8 + def func() -> Generator[int]: +9 | yield 42 +10 | +11 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:21:15 + | +21 | def func() -> Generator[int, int, None]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +22 | _ = yield 42 +23 | return None + | +help: Remove default type arguments +18 | return foo +19 | +20 | + - def func() -> Generator[int, int, None]: +21 + def func() -> Generator[int, int]: +22 | _ = yield 42 +23 | return None +24 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:31:21 + | +31 | async def func() -> AsyncGenerator[int, None]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +32 | yield 42 + | +help: Remove default type arguments +28 | return 42 +29 | +30 | + - async def func() -> AsyncGenerator[int, None]: +31 + async def func() -> AsyncGenerator[int]: +32 | yield 42 +33 | +34 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:47:15 + | +47 | def func() -> Generator[str, None, None]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +48 | yield "hello" + | +help: Remove default type arguments +44 | from typing import Generator, AsyncGenerator +45 | +46 | + - def func() -> Generator[str, None, None]: +47 + def func() -> Generator[str]: +48 | yield "hello" +49 | +50 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:51:21 + | +51 | async def func() -> AsyncGenerator[str, None]: + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +52 | yield "hello" + | +help: Remove default type arguments +48 | yield "hello" +49 | +50 | + - async def func() -> AsyncGenerator[str, None]: +51 + async def func() -> AsyncGenerator[str]: +52 | yield "hello" +53 | +54 | + +UP043 [*] Unnecessary default type arguments + --> UP043.pyi:55:21 + | +55 | async def func() -> AsyncGenerator[ # type: ignore + | _____________________^ +56 | | str, +57 | | None +58 | | ]: + | |_^ +59 | yield "hello" + | +help: Remove default type arguments +52 | yield "hello" +53 | +54 | + - async def func() -> AsyncGenerator[ # type: ignore + - str, + - None + - ]: +55 + async def func() -> AsyncGenerator[str]: +56 | yield "hello" +note: This is an unsafe fix and may change runtime behavior From ac5e7ebdc95a447828445cff4bdf33f2e41babbf Mon Sep 17 00:00:00 2001 From: Igor Drokin Date: Tue, 9 Sep 2025 15:25:02 +0300 Subject: [PATCH 3/4] feat(UP043): gated behind preview --- .../rules/pyupgrade/rules/unnecessary_default_type_args.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index d7cd54424d323..e5d739d5a642e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -7,7 +7,8 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for unnecessary default type arguments for `Generator` and -/// `AsyncGenerator` on Python 3.13+ or in stubs. +/// `AsyncGenerator` on Python 3.13+. +/// In [preview], this rule will also apply to stub files. /// /// ## Why is this bad? /// Python 3.13 introduced the ability for type parameters to specify default @@ -59,6 +60,8 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// - [Annotating generators and coroutines](https://docs.python.org/3/library/typing.html#annotating-generators-and-coroutines) /// - [Python documentation: `typing.Generator`](https://docs.python.org/3/library/typing.html#typing.Generator) /// - [Python documentation: `typing.AsyncGenerator`](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator) +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryDefaultTypeArgs; From 577fb23eea4cb71df85f8d178e94d131c5837e88 Mon Sep 17 00:00:00 2001 From: dylwil3 Date: Tue, 9 Sep 2025 07:54:09 -0500 Subject: [PATCH 4/4] add link to pr in preview.rs --- crates/ruff_linter/src/preview.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 44126d045e5a8..466482cd48439 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -261,6 +261,7 @@ pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterS settings.preview.is_enabled() } +// https://github.com/astral-sh/ruff/pull/20027 pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled( settings: &LinterSettings, ) -> bool {