From 60daec60011d0b3a6be52c9410d1d7ef0179349d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 16 May 2023 08:25:25 -0400 Subject: Add optional `prefer-typing-namedtuple` message (#8681) Closes #8660 --- .../messages/p/prefer-typing-namedtuple/bad.py | 5 +++++ .../messages/p/prefer-typing-namedtuple/good.py | 7 +++++++ .../messages/p/prefer-typing-namedtuple/pylintrc | 2 ++ .../p/prefer-typing-namedtuple/related.rst | 1 + doc/whatsnew/fragments/8660.extension | 7 +++++++ pylint/checkers/classes/class_checker.py | 22 ++++++++++++++-------- pylint/extensions/code_style.py | 21 +++++++++++++++++++++ .../ext/code_style/cs_prefer_typing_namedtuple.py | 9 +++++++++ .../ext/code_style/cs_prefer_typing_namedtuple.rc | 3 +++ .../ext/code_style/cs_prefer_typing_namedtuple.txt | 2 ++ 10 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 doc/data/messages/p/prefer-typing-namedtuple/bad.py create mode 100644 doc/data/messages/p/prefer-typing-namedtuple/good.py create mode 100644 doc/data/messages/p/prefer-typing-namedtuple/pylintrc create mode 100644 doc/data/messages/p/prefer-typing-namedtuple/related.rst create mode 100644 doc/whatsnew/fragments/8660.extension create mode 100644 tests/functional/ext/code_style/cs_prefer_typing_namedtuple.py create mode 100644 tests/functional/ext/code_style/cs_prefer_typing_namedtuple.rc create mode 100644 tests/functional/ext/code_style/cs_prefer_typing_namedtuple.txt diff --git a/doc/data/messages/p/prefer-typing-namedtuple/bad.py b/doc/data/messages/p/prefer-typing-namedtuple/bad.py new file mode 100644 index 000000000..d555b0f26 --- /dev/null +++ b/doc/data/messages/p/prefer-typing-namedtuple/bad.py @@ -0,0 +1,5 @@ +from collections import namedtuple + +Philosophy = namedtuple( # [prefer-typing-namedtuple] + "Philosophy", ("goodness", "truth", "beauty") +) diff --git a/doc/data/messages/p/prefer-typing-namedtuple/good.py b/doc/data/messages/p/prefer-typing-namedtuple/good.py new file mode 100644 index 000000000..ef094aacd --- /dev/null +++ b/doc/data/messages/p/prefer-typing-namedtuple/good.py @@ -0,0 +1,7 @@ +from typing import NamedTuple + + +class Philosophy(NamedTuple): + goodness: str + truth: bool + beauty: float diff --git a/doc/data/messages/p/prefer-typing-namedtuple/pylintrc b/doc/data/messages/p/prefer-typing-namedtuple/pylintrc new file mode 100644 index 000000000..b001506b6 --- /dev/null +++ b/doc/data/messages/p/prefer-typing-namedtuple/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins = pylint.extensions.code_style diff --git a/doc/data/messages/p/prefer-typing-namedtuple/related.rst b/doc/data/messages/p/prefer-typing-namedtuple/related.rst new file mode 100644 index 000000000..a8d3da44c --- /dev/null +++ b/doc/data/messages/p/prefer-typing-namedtuple/related.rst @@ -0,0 +1 @@ +- `typing.NamedTuple `_ diff --git a/doc/whatsnew/fragments/8660.extension b/doc/whatsnew/fragments/8660.extension new file mode 100644 index 000000000..2090d03ac --- /dev/null +++ b/doc/whatsnew/fragments/8660.extension @@ -0,0 +1,7 @@ +Add new ``prefer-typing-namedtuple`` message to the ``CodeStyleChecker`` to suggest +rewriting calls to ``collections.namedtuple`` as classes inheriting from ``typing.NamedTuple`` +on Python 3.6+. + +Requires ``load-plugins=pylint.extensions.code_style`` and ``enable=prefer-typing-namedtuple`` to be raised. + +Closes #8660 diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index 77a795bcb..bdfb0968a 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -6,13 +6,12 @@ from __future__ import annotations -import collections from collections import defaultdict from collections.abc import Callable, Sequence from functools import cached_property from itertools import chain, zip_longest from re import Pattern -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, NamedTuple, Union import astroid from astroid import bases, nodes, util @@ -63,12 +62,19 @@ ASTROID_TYPE_COMPARATORS = { # Dealing with useless override detection, with regard # to parameters vs arguments -_CallSignature = collections.namedtuple( - "_CallSignature", "args kws starred_args starred_kws" -) -_ParameterSignature = collections.namedtuple( - "_ParameterSignature", "args kwonlyargs varargs kwargs" -) + +class _CallSignature(NamedTuple): + args: list[str | None] + kws: dict[str | None, str | None] + starred_args: list[str] + starred_kws: list[str] + + +class _ParameterSignature(NamedTuple): + args: list[str] + kwonlyargs: list[str] + varargs: str + kwargs: str def _signature_from_call(call: nodes.Call) -> _CallSignature: diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 5ce1ae476..622601c75 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -69,6 +69,17 @@ class CodeStyleChecker(BaseChecker): "default_enabled": False, }, ), + "R6105": ( + "Prefer 'typing.NamedTuple' over 'collections.namedtuple'", + "prefer-typing-namedtuple", + "'typing.NamedTuple' uses the well-known 'class' keyword " + "with type-hints for readability (it's also faster as it avoids " + "an internal exec call).\n" + "Disabled by default!", + { + "default_enabled": False, + }, + ), } options = ( ( @@ -89,12 +100,22 @@ class CodeStyleChecker(BaseChecker): def open(self) -> None: py_version = self.linter.config.py_version + self._py36_plus = py_version >= (3, 6) self._py38_plus = py_version >= (3, 8) self._max_length: int = ( self.linter.config.max_line_length_suggestions or self.linter.config.max_line_length ) + @only_required_for_messages("prefer-typing-namedtuple") + def visit_call(self, node: nodes.Call) -> None: + if self._py36_plus: + called = safe_infer(node.func) + if called and called.qname() == "collections.namedtuple": + self.add_message( + "prefer-typing-namedtuple", node=node, confidence=INFERENCE + ) + @only_required_for_messages("consider-using-namedtuple-or-dataclass") def visit_dict(self, node: nodes.Dict) -> None: self._check_dict_consider_namedtuple_dataclass(node) diff --git a/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.py b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.py new file mode 100644 index 000000000..7b0e7c58d --- /dev/null +++ b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.py @@ -0,0 +1,9 @@ +# pylint: disable=missing-docstring +from collections import namedtuple + +NoteHash = namedtuple('NoteHash', ['Pitch', 'Duration', 'Offset']) # [prefer-typing-namedtuple] + +class SearchMatch( + namedtuple('SearchMatch', ['els', 'index', 'iterator']) # [prefer-typing-namedtuple] +): + """Adapted from primer package `music21`.""" diff --git a/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.rc b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.rc new file mode 100644 index 000000000..ee4eb7e68 --- /dev/null +++ b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.rc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=prefer-typing-namedtuple diff --git a/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.txt b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.txt new file mode 100644 index 000000000..a9bf6751b --- /dev/null +++ b/tests/functional/ext/code_style/cs_prefer_typing_namedtuple.txt @@ -0,0 +1,2 @@ +prefer-typing-namedtuple:4:11:4:66::Prefer 'typing.NamedTuple' over 'collections.namedtuple':INFERENCE +prefer-typing-namedtuple:7:4:7:59:SearchMatch:Prefer 'typing.NamedTuple' over 'collections.namedtuple':INFERENCE -- cgit v1.2.1