summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2023-05-16 08:25:25 -0400
committerGitHub <noreply@github.com>2023-05-16 14:25:25 +0200
commit60daec60011d0b3a6be52c9410d1d7ef0179349d (patch)
tree5bb1d73eb9f8af22ba731a0a64f7bf8f10ba562c
parentd7baf5d43fa8de68c4c169f731ae5af43ce60790 (diff)
downloadpylint-git-60daec60011d0b3a6be52c9410d1d7ef0179349d.tar.gz
Add optional `prefer-typing-namedtuple` message (#8681)HEADmain
Closes #8660
-rw-r--r--doc/data/messages/p/prefer-typing-namedtuple/bad.py5
-rw-r--r--doc/data/messages/p/prefer-typing-namedtuple/good.py7
-rw-r--r--doc/data/messages/p/prefer-typing-namedtuple/pylintrc2
-rw-r--r--doc/data/messages/p/prefer-typing-namedtuple/related.rst1
-rw-r--r--doc/whatsnew/fragments/8660.extension7
-rw-r--r--pylint/checkers/classes/class_checker.py22
-rw-r--r--pylint/extensions/code_style.py21
-rw-r--r--tests/functional/ext/code_style/cs_prefer_typing_namedtuple.py9
-rw-r--r--tests/functional/ext/code_style/cs_prefer_typing_namedtuple.rc3
-rw-r--r--tests/functional/ext/code_style/cs_prefer_typing_namedtuple.txt2
10 files changed, 71 insertions, 8 deletions
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 <https://docs.python.org/3/library/typing.html#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