Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for unnecessary-default-type-args #9938

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[main]
py-version=3.10
load-plugins=pylint.extensions.typing
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
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
Loading