Skip to content

Commit b28c1f6

Browse files
Add check for unnecessary-default-type-args (#9938)
Co-authored-by: Jacob Walls <[email protected]>
1 parent bd97b93 commit b28c1f6

23 files changed

+119
-14
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from collections.abc import AsyncGenerator, Generator
2+
3+
a1: AsyncGenerator[int, None] # [unnecessary-default-type-args]
4+
b1: Generator[int, None, None] # [unnecessary-default-type-args]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
At the moment, this check only works for ``Generator`` and ``AsyncGenerator``.
2+
3+
Starting with Python 3.13, the ``SendType`` and ``ReturnType`` default to ``None``.
4+
As such it's no longer necessary to specify them. The ``collections.abc`` variants
5+
don't validate the number of type arguments. Therefore the defaults for these
6+
can be used in earlier versions as well.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from collections.abc import AsyncGenerator, Generator
2+
3+
a1: AsyncGenerator[int]
4+
b1: Generator[int]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[main]
2+
load-plugins=pylint.extensions.typing
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- `Python documentation for AsyncGenerator <https://docs.python.org/3.13/library/typing.html#typing.AsyncGenerator>`_
2+
- `Python documentation for Generator <https://docs.python.org/3.13/library/typing.html#typing.Generator>`_

doc/user_guide/checkers/extensions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,9 @@ Typing checker Messages
688688
:consider-alternative-union-syntax (R6003): *Consider using alternative Union syntax instead of '%s'%s*
689689
Emitted when 'typing.Union' or 'typing.Optional' is used instead of the
690690
alternative Union syntax 'int | None'.
691+
:unnecessary-default-type-args (R6007): *Type `%s` has unnecessary default type args. Change it to `%s`.*
692+
Emitted when types have default type args which can be omitted. Mainly used
693+
for `typing.Generator` and `typing.AsyncGenerator`.
691694
:redundant-typehint-argument (R6006): *Type `%s` is used more than once in union type annotation. Remove redundant typehints.*
692695
Duplicated type arguments will be skipped by `mypy` tool, therefore should be
693696
removed to avoid confusion.

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ All messages in the refactor category:
545545
refactor/too-many-statements
546546
refactor/trailing-comma-tuple
547547
refactor/unnecessary-comprehension
548+
refactor/unnecessary-default-type-args
548549
refactor/unnecessary-dict-index-lookup
549550
refactor/unnecessary-list-index-lookup
550551
refactor/use-a-generator

doc/whatsnew/fragments/9938.new_check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add ``unnecessary-default-type-args`` to the ``typing`` extension to detect the use
2+
of unnecessary default type args for ``typing.Generator`` and ``typing.AsyncGenerator``.
3+
4+
Refs #9938

pylint/checkers/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def predicate(obj: Any) -> bool:
3535

3636
def _annotated_unpack_infer(
3737
stmt: nodes.NodeNG, context: InferenceContext | None = None
38-
) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult], None, None]:
38+
) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult]]:
3939
"""Recursively generate nodes inferred by the given statement.
4040
4141
If the inferred value is a list or a tuple, recurse on the elements.

pylint/checkers/symilar.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ def _get_similarity_report(
468468
# pylint: disable = too-many-locals
469469
def _find_common(
470470
self, lineset1: LineSet, lineset2: LineSet
471-
) -> Generator[Commonality, None, None]:
471+
) -> Generator[Commonality]:
472472
"""Find similarities in the two given linesets.
473473
474474
This the core of the algorithm. The idea is to compute the hashes of a
@@ -541,7 +541,7 @@ def _find_common(
541541
if eff_cmn_nb > self.namespace.min_similarity_lines:
542542
yield com
543543

544-
def _iter_sims(self) -> Generator[Commonality, None, None]:
544+
def _iter_sims(self) -> Generator[Commonality]:
545545
"""Iterate on similarities among all files, by making a Cartesian
546546
product.
547547
"""

pylint/checkers/variables.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,7 @@ class C: ...
249249
return frame.lineno < defframe.lineno # type: ignore[no-any-return]
250250

251251

252-
def _infer_name_module(
253-
node: nodes.Import, name: str
254-
) -> Generator[InferenceResult, None, None]:
252+
def _infer_name_module(node: nodes.Import, name: str) -> Generator[InferenceResult]:
255253
context = astroid.context.InferenceContext()
256254
context.lookupname = name
257255
return node.infer(context, asname=False) # type: ignore[no-any-return]

pylint/extensions/typing.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class DeprecatedTypingAliasMsg(NamedTuple):
8585
parent_subscript: bool = False
8686

8787

88+
# pylint: disable-next=too-many-instance-attributes
8889
class TypingChecker(BaseChecker):
8990
"""Find issue specifically related to type annotations."""
9091

@@ -130,6 +131,12 @@ class TypingChecker(BaseChecker):
130131
"Duplicated type arguments will be skipped by `mypy` tool, therefore should be "
131132
"removed to avoid confusion.",
132133
),
134+
"R6007": (
135+
"Type `%s` has unnecessary default type args. Change it to `%s`.",
136+
"unnecessary-default-type-args",
137+
"Emitted when types have default type args which can be omitted. "
138+
"Mainly used for `typing.Generator` and `typing.AsyncGenerator`.",
139+
),
133140
}
134141
options = (
135142
(
@@ -174,6 +181,7 @@ def open(self) -> None:
174181
self._py37_plus = py_version >= (3, 7)
175182
self._py39_plus = py_version >= (3, 9)
176183
self._py310_plus = py_version >= (3, 10)
184+
self._py313_plus = py_version >= (3, 13)
177185

178186
self._should_check_typing_alias = self._py39_plus or (
179187
self._py37_plus and self.linter.config.runtime_typing is False
@@ -248,6 +256,33 @@ def visit_annassign(self, node: nodes.AnnAssign) -> None:
248256

249257
self._check_union_types(types, node)
250258

259+
@only_required_for_messages("unnecessary-default-type-args")
260+
def visit_subscript(self, node: nodes.Subscript) -> None:
261+
inferred = safe_infer(node.value)
262+
if ( # pylint: disable=too-many-boolean-expressions
263+
isinstance(inferred, nodes.ClassDef)
264+
and (
265+
inferred.qname() in {"typing.Generator", "typing.AsyncGenerator"}
266+
and self._py313_plus
267+
or inferred.qname()
268+
in {"_collections_abc.Generator", "_collections_abc.AsyncGenerator"}
269+
)
270+
and isinstance(node.slice, nodes.Tuple)
271+
and all(
272+
(isinstance(el, nodes.Const) and el.value is None)
273+
for el in node.slice.elts[1:]
274+
)
275+
):
276+
suggested_str = (
277+
f"{node.value.as_string()}[{node.slice.elts[0].as_string()}]"
278+
)
279+
self.add_message(
280+
"unnecessary-default-type-args",
281+
args=(node.as_string(), suggested_str),
282+
node=node,
283+
confidence=HIGH,
284+
)
285+
251286
@staticmethod
252287
def _is_deprecated_union_annotation(
253288
annotation: nodes.NodeNG, union_name: str

pylint/pyreverse/diadefslib.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def add_class(self, node: nodes.ClassDef) -> None:
8484

8585
def get_ancestors(
8686
self, node: nodes.ClassDef, level: int
87-
) -> Generator[nodes.ClassDef, None, None]:
87+
) -> Generator[nodes.ClassDef]:
8888
"""Return ancestor nodes of a class node."""
8989
if level == 0:
9090
return
@@ -95,7 +95,7 @@ def get_ancestors(
9595

9696
def get_associated(
9797
self, klass_node: nodes.ClassDef, level: int
98-
) -> Generator[nodes.ClassDef, None, None]:
98+
) -> Generator[nodes.ClassDef]:
9999
"""Return associated nodes of a class node."""
100100
if level == 0:
101101
return

pylint/testutils/checker_test_case.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def assertNoMessages(self) -> Iterator[None]:
4040
@contextlib.contextmanager
4141
def assertAddsMessages(
4242
self, *messages: MessageTest, ignore_position: bool = False
43-
) -> Generator[None, None, None]:
43+
) -> Generator[None]:
4444
"""Assert that exactly the given method adds the given messages.
4545
4646
The list of messages must exactly match *all* the messages added by the

pylint/testutils/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def _patch_streams(out: TextIO) -> Iterator[None]:
2727
@contextlib.contextmanager
2828
def _test_sys_path(
2929
replacement_sys_path: list[str] | None = None,
30-
) -> Generator[None, None, None]:
30+
) -> Generator[None]:
3131
original_path = sys.path
3232
try:
3333
if replacement_sys_path is not None:
@@ -40,7 +40,7 @@ def _test_sys_path(
4040
@contextlib.contextmanager
4141
def _test_cwd(
4242
current_working_directory: str | Path | None = None,
43-
) -> Generator[None, None, None]:
43+
) -> Generator[None]:
4444
original_dir = os.getcwd()
4545
try:
4646
if current_working_directory is not None:
@@ -53,7 +53,7 @@ def _test_cwd(
5353
@contextlib.contextmanager
5454
def _test_environ_pythonpath(
5555
new_pythonpath: str | None = None,
56-
) -> Generator[None, None, None]:
56+
) -> Generator[None]:
5757
original_pythonpath = os.environ.get("PYTHONPATH")
5858
if new_pythonpath:
5959
os.environ["PYTHONPATH"] = new_pythonpath

pylint/utils/pragma_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class InvalidPragmaError(PragmaParserError):
8686
"""Thrown in case the pragma is invalid."""
8787

8888

89-
def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter, None, None]:
89+
def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter]:
9090
action: str | None = None
9191
messages: list[str] = []
9292
assignment_required = False
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# pylint: disable=missing-docstring,deprecated-typing-alias
2+
import collections.abc as ca
3+
import typing as t
4+
5+
a1: t.Generator[int, str, str]
6+
a2: t.Generator[int, None, None]
7+
a3: t.Generator[int]
8+
b1: t.AsyncGenerator[int, str]
9+
b2: t.AsyncGenerator[int, None]
10+
b3: t.AsyncGenerator[int]
11+
12+
c1: ca.Generator[int, str, str]
13+
c2: ca.Generator[int, None, None] # [unnecessary-default-type-args]
14+
c3: ca.Generator[int]
15+
d1: ca.AsyncGenerator[int, str]
16+
d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args]
17+
d3: ca.AsyncGenerator[int]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[main]
2+
py-version=3.10
3+
load-plugins=pylint.extensions.typing
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
unnecessary-default-type-args:13:4:13:33::Type `ca.Generator[int, None, None]` has unnecessary default type args. Change it to `ca.Generator[int]`.:HIGH
2+
unnecessary-default-type-args:16:4:16:32::Type `ca.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `ca.AsyncGenerator[int]`.:HIGH
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# pylint: disable=missing-docstring,deprecated-typing-alias
2+
import collections.abc as ca
3+
import typing as t
4+
5+
a1: t.Generator[int, str, str]
6+
a2: t.Generator[int, None, None] # [unnecessary-default-type-args]
7+
a3: t.Generator[int]
8+
b1: t.AsyncGenerator[int, str]
9+
b2: t.AsyncGenerator[int, None] # [unnecessary-default-type-args]
10+
b3: t.AsyncGenerator[int]
11+
12+
c1: ca.Generator[int, str, str]
13+
c2: ca.Generator[int, None, None] # [unnecessary-default-type-args]
14+
c3: ca.Generator[int]
15+
d1: ca.AsyncGenerator[int, str]
16+
d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args]
17+
d3: ca.AsyncGenerator[int]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[main]
2+
py-version=3.13
3+
load-plugins=pylint.extensions.typing
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
unnecessary-default-type-args:6:4:6:32::Type `t.Generator[int, None, None]` has unnecessary default type args. Change it to `t.Generator[int]`.:HIGH
2+
unnecessary-default-type-args:9:4:9:31::Type `t.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `t.AsyncGenerator[int]`.:HIGH
3+
unnecessary-default-type-args:13:4:13:33::Type `ca.Generator[int, None, None]` has unnecessary default type args. Change it to `ca.Generator[int]`.:HIGH
4+
unnecessary-default-type-args:16:4:16:32::Type `ca.AsyncGenerator[int, None]` has unnecessary default type args. Change it to `ca.AsyncGenerator[int]`.:HIGH

tests/pyreverse/test_inspector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727

2828
@pytest.fixture
29-
def project(get_project: GetProjectCallable) -> Generator[Project, None, None]:
29+
def project(get_project: GetProjectCallable) -> Generator[Project]:
3030
with _test_cwd(TESTS):
3131
project = get_project("data", "data")
3232
linker = inspector.Linker(project)

0 commit comments

Comments
 (0)