Skip to content

Commit

Permalink
Add check for unnecessary-default-type-args (#9938)
Browse files Browse the repository at this point in the history
Co-authored-by: Jacob Walls <[email protected]>
  • Loading branch information
cdce8p and jacobtylerwalls committed Sep 20, 2024
1 parent bd97b93 commit b28c1f6
Show file tree
Hide file tree
Showing 23 changed files with 119 additions and 14 deletions.
4 changes: 4 additions & 0 deletions doc/data/messages/u/unnecessary-default-type-args/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from collections.abc import AsyncGenerator, Generator

a1: AsyncGenerator[int, None] # [unnecessary-default-type-args]
b1: Generator[int, None, None] # [unnecessary-default-type-args]
6 changes: 6 additions & 0 deletions doc/data/messages/u/unnecessary-default-type-args/details.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
At the moment, this check only works for ``Generator`` and ``AsyncGenerator``.

Starting with Python 3.13, the ``SendType`` and ``ReturnType`` default to ``None``.
As such it's no longer necessary to specify them. The ``collections.abc`` variants
don't validate the number of type arguments. Therefore the defaults for these
can be used in earlier versions as well.
4 changes: 4 additions & 0 deletions doc/data/messages/u/unnecessary-default-type-args/good.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from collections.abc import AsyncGenerator, Generator

a1: AsyncGenerator[int]
b1: Generator[int]
2 changes: 2 additions & 0 deletions doc/data/messages/u/unnecessary-default-type-args/pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[main]
load-plugins=pylint.extensions.typing
2 changes: 2 additions & 0 deletions doc/data/messages/u/unnecessary-default-type-args/related.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `Python documentation for AsyncGenerator <https://docs.python.org/3.13/library/typing.html#typing.AsyncGenerator>`_
- `Python documentation for Generator <https://docs.python.org/3.13/library/typing.html#typing.Generator>`_
3 changes: 3 additions & 0 deletions doc/user_guide/checkers/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,9 @@ Typing checker Messages
:consider-alternative-union-syntax (R6003): *Consider using alternative Union syntax instead of '%s'%s*
Emitted when 'typing.Union' or 'typing.Optional' is used instead of the
alternative Union syntax 'int | None'.
:unnecessary-default-type-args (R6007): *Type `%s` has unnecessary default type args. Change it to `%s`.*
Emitted when types have default type args which can be omitted. Mainly used
for `typing.Generator` and `typing.AsyncGenerator`.
:redundant-typehint-argument (R6006): *Type `%s` is used more than once in union type annotation. Remove redundant typehints.*
Duplicated type arguments will be skipped by `mypy` tool, therefore should be
removed to avoid confusion.
Expand Down
1 change: 1 addition & 0 deletions doc/user_guide/messages/messages_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ All messages in the refactor category:
refactor/too-many-statements
refactor/trailing-comma-tuple
refactor/unnecessary-comprehension
refactor/unnecessary-default-type-args
refactor/unnecessary-dict-index-lookup
refactor/unnecessary-list-index-lookup
refactor/use-a-generator
Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/fragments/9938.new_check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add ``unnecessary-default-type-args`` to the ``typing`` extension to detect the use
of unnecessary default type args for ``typing.Generator`` and ``typing.AsyncGenerator``.

Refs #9938
2 changes: 1 addition & 1 deletion pylint/checkers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def predicate(obj: Any) -> bool:

def _annotated_unpack_infer(
stmt: nodes.NodeNG, context: InferenceContext | None = None
) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult], None, None]:
) -> Generator[tuple[nodes.NodeNG, SuccessfulInferenceResult]]:
"""Recursively generate nodes inferred by the given statement.
If the inferred value is a list or a tuple, recurse on the elements.
Expand Down
4 changes: 2 additions & 2 deletions pylint/checkers/symilar.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def _get_similarity_report(
# pylint: disable = too-many-locals
def _find_common(
self, lineset1: LineSet, lineset2: LineSet
) -> Generator[Commonality, None, None]:
) -> Generator[Commonality]:
"""Find similarities in the two given linesets.
This the core of the algorithm. The idea is to compute the hashes of a
Expand Down Expand Up @@ -541,7 +541,7 @@ def _find_common(
if eff_cmn_nb > self.namespace.min_similarity_lines:
yield com

def _iter_sims(self) -> Generator[Commonality, None, None]:
def _iter_sims(self) -> Generator[Commonality]:
"""Iterate on similarities among all files, by making a Cartesian
product.
"""
Expand Down
4 changes: 1 addition & 3 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,7 @@ class C: ...
return frame.lineno < defframe.lineno # type: ignore[no-any-return]


def _infer_name_module(
node: nodes.Import, name: str
) -> Generator[InferenceResult, None, None]:
def _infer_name_module(node: nodes.Import, name: str) -> Generator[InferenceResult]:
context = astroid.context.InferenceContext()
context.lookupname = name
return node.infer(context, asname=False) # type: ignore[no-any-return]
Expand Down
35 changes: 35 additions & 0 deletions pylint/extensions/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class DeprecatedTypingAliasMsg(NamedTuple):
parent_subscript: bool = False


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

Expand Down Expand Up @@ -130,6 +131,12 @@ class TypingChecker(BaseChecker):
"Duplicated type arguments will be skipped by `mypy` tool, therefore should be "
"removed to avoid confusion.",
),
"R6007": (
"Type `%s` has unnecessary default type args. Change it to `%s`.",
"unnecessary-default-type-args",
"Emitted when types have default type args which can be omitted. "
"Mainly used for `typing.Generator` and `typing.AsyncGenerator`.",
),
}
options = (
(
Expand Down Expand Up @@ -174,6 +181,7 @@ def open(self) -> None:
self._py37_plus = py_version >= (3, 7)
self._py39_plus = py_version >= (3, 9)
self._py310_plus = py_version >= (3, 10)
self._py313_plus = py_version >= (3, 13)

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

self._check_union_types(types, node)

@only_required_for_messages("unnecessary-default-type-args")
def visit_subscript(self, node: nodes.Subscript) -> None:
inferred = safe_infer(node.value)
if ( # pylint: disable=too-many-boolean-expressions
isinstance(inferred, nodes.ClassDef)
and (
inferred.qname() in {"typing.Generator", "typing.AsyncGenerator"}
and self._py313_plus
or inferred.qname()
in {"_collections_abc.Generator", "_collections_abc.AsyncGenerator"}
)
and isinstance(node.slice, nodes.Tuple)
and all(
(isinstance(el, nodes.Const) and el.value is None)
for el in node.slice.elts[1:]
)
):
suggested_str = (
f"{node.value.as_string()}[{node.slice.elts[0].as_string()}]"
)
self.add_message(
"unnecessary-default-type-args",
args=(node.as_string(), suggested_str),
node=node,
confidence=HIGH,
)

@staticmethod
def _is_deprecated_union_annotation(
annotation: nodes.NodeNG, union_name: str
Expand Down
4 changes: 2 additions & 2 deletions pylint/pyreverse/diadefslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def add_class(self, node: nodes.ClassDef) -> None:

def get_ancestors(
self, node: nodes.ClassDef, level: int
) -> Generator[nodes.ClassDef, None, None]:
) -> Generator[nodes.ClassDef]:
"""Return ancestor nodes of a class node."""
if level == 0:
return
Expand All @@ -95,7 +95,7 @@ def get_ancestors(

def get_associated(
self, klass_node: nodes.ClassDef, level: int
) -> Generator[nodes.ClassDef, None, None]:
) -> Generator[nodes.ClassDef]:
"""Return associated nodes of a class node."""
if level == 0:
return
Expand Down
2 changes: 1 addition & 1 deletion pylint/testutils/checker_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def assertNoMessages(self) -> Iterator[None]:
@contextlib.contextmanager
def assertAddsMessages(
self, *messages: MessageTest, ignore_position: bool = False
) -> Generator[None, None, None]:
) -> Generator[None]:
"""Assert that exactly the given method adds the given messages.
The list of messages must exactly match *all* the messages added by the
Expand Down
6 changes: 3 additions & 3 deletions pylint/testutils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _patch_streams(out: TextIO) -> Iterator[None]:
@contextlib.contextmanager
def _test_sys_path(
replacement_sys_path: list[str] | None = None,
) -> Generator[None, None, None]:
) -> Generator[None]:
original_path = sys.path
try:
if replacement_sys_path is not None:
Expand All @@ -40,7 +40,7 @@ def _test_sys_path(
@contextlib.contextmanager
def _test_cwd(
current_working_directory: str | Path | None = None,
) -> Generator[None, None, None]:
) -> Generator[None]:
original_dir = os.getcwd()
try:
if current_working_directory is not None:
Expand All @@ -53,7 +53,7 @@ def _test_cwd(
@contextlib.contextmanager
def _test_environ_pythonpath(
new_pythonpath: str | None = None,
) -> Generator[None, None, None]:
) -> Generator[None]:
original_pythonpath = os.environ.get("PYTHONPATH")
if new_pythonpath:
os.environ["PYTHONPATH"] = new_pythonpath
Expand Down
2 changes: 1 addition & 1 deletion pylint/utils/pragma_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class InvalidPragmaError(PragmaParserError):
"""Thrown in case the pragma is invalid."""


def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter, None, None]:
def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter]:
action: str | None = None
messages: list[str] = []
assignment_required = False
Expand Down
17 changes: 17 additions & 0 deletions tests/functional/ext/typing/unnecessary_default_type_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# pylint: disable=missing-docstring,deprecated-typing-alias
import collections.abc as ca
import typing as t

a1: t.Generator[int, str, str]
a2: t.Generator[int, None, None]
a3: t.Generator[int]
b1: t.AsyncGenerator[int, str]
b2: t.AsyncGenerator[int, None]
b3: t.AsyncGenerator[int]

c1: ca.Generator[int, str, str]
c2: ca.Generator[int, None, None] # [unnecessary-default-type-args]
c3: ca.Generator[int]
d1: ca.AsyncGenerator[int, str]
d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args]
d3: ca.AsyncGenerator[int]
3 changes: 3 additions & 0 deletions tests/functional/ext/typing/unnecessary_default_type_args.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[main]
py-version=3.10
load-plugins=pylint.extensions.typing
2 changes: 2 additions & 0 deletions tests/functional/ext/typing/unnecessary_default_type_args.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
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
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
17 changes: 17 additions & 0 deletions tests/functional/ext/typing/unnecessary_default_type_args_py313.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# pylint: disable=missing-docstring,deprecated-typing-alias
import collections.abc as ca
import typing as t

a1: t.Generator[int, str, str]
a2: t.Generator[int, None, None] # [unnecessary-default-type-args]
a3: t.Generator[int]
b1: t.AsyncGenerator[int, str]
b2: t.AsyncGenerator[int, None] # [unnecessary-default-type-args]
b3: t.AsyncGenerator[int]

c1: ca.Generator[int, str, str]
c2: ca.Generator[int, None, None] # [unnecessary-default-type-args]
c3: ca.Generator[int]
d1: ca.AsyncGenerator[int, str]
d2: ca.AsyncGenerator[int, None] # [unnecessary-default-type-args]
d3: ca.AsyncGenerator[int]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[main]
py-version=3.13
load-plugins=pylint.extensions.typing
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
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
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
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
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
2 changes: 1 addition & 1 deletion tests/pyreverse/test_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@


@pytest.fixture
def project(get_project: GetProjectCallable) -> Generator[Project, None, None]:
def project(get_project: GetProjectCallable) -> Generator[Project]:
with _test_cwd(TESTS):
project = get_project("data", "data")
linker = inspector.Linker(project)
Expand Down

0 comments on commit b28c1f6

Please sign in to comment.