diff --git a/doc/data/messages/u/unnecessary-default-type-args/bad.py b/doc/data/messages/u/unnecessary-default-type-args/bad.py new file mode 100644 index 00000000000..e3d97799a53 --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/bad.py @@ -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] diff --git a/doc/data/messages/u/unnecessary-default-type-args/details.rst b/doc/data/messages/u/unnecessary-default-type-args/details.rst new file mode 100644 index 00000000000..8e0b1a645dd --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/details.rst @@ -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 validated the number of type arguments. Therefore the defaults for these +can be used in earlier versions as well. diff --git a/doc/data/messages/u/unnecessary-default-type-args/good.py b/doc/data/messages/u/unnecessary-default-type-args/good.py new file mode 100644 index 00000000000..e77c0ee4292 --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/good.py @@ -0,0 +1,4 @@ +from collections.abc import AsyncGenerator, Generator + +a1: AsyncGenerator[int] +b1: Generator[int] diff --git a/doc/data/messages/u/unnecessary-default-type-args/related.rst b/doc/data/messages/u/unnecessary-default-type-args/related.rst new file mode 100644 index 00000000000..1f988ae98bb --- /dev/null +++ b/doc/data/messages/u/unnecessary-default-type-args/related.rst @@ -0,0 +1,2 @@ +- `Python documentation for AsyncGenerator `_ +- `Python documentation for Generator `_ diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 95462f92189..00f9963b72e 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -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. diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 6ad50562f8c..fc487fc25c0 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -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 diff --git a/doc/whatsnew/fragments/9938.new_check b/doc/whatsnew/fragments/9938.new_check new file mode 100644 index 00000000000..a13556f7de4 --- /dev/null +++ b/doc/whatsnew/fragments/9938.new_check @@ -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 diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 2956465cf67..f9ef83babb1 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -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.""" @@ -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 = ( ( @@ -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 @@ -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 diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.py b/tests/functional/ext/typing/unnecessary_default_type_args.py new file mode 100644 index 00000000000..e2d1d700de2 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.py @@ -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] diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.rc b/tests/functional/ext/typing/unnecessary_default_type_args.rc new file mode 100644 index 00000000000..825e13ec0bf --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.rc @@ -0,0 +1,2 @@ +[main] +load-plugins=pylint.extensions.typing diff --git a/tests/functional/ext/typing/unnecessary_default_type_args.txt b/tests/functional/ext/typing/unnecessary_default_type_args.txt new file mode 100644 index 00000000000..2d36ba46a66 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args.txt @@ -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 diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.py b/tests/functional/ext/typing/unnecessary_default_type_args_py313.py new file mode 100644 index 00000000000..9dec4c4075a --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.py @@ -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] diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc b/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc new file mode 100644 index 00000000000..d2db5fe7caf --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.rc @@ -0,0 +1,3 @@ +[main] +py-version=3.13 +load-plugins=pylint.extensions.typing diff --git a/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt b/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt new file mode 100644 index 00000000000..228f4996638 --- /dev/null +++ b/tests/functional/ext/typing/unnecessary_default_type_args_py313.txt @@ -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