Skip to content

Commit 60daec6

Browse files
Add optional prefer-typing-namedtuple message (#8681)
Closes #8660
1 parent d7baf5d commit 60daec6

File tree

10 files changed

+71
-8
lines changed

10 files changed

+71
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from collections import namedtuple
2+
3+
Philosophy = namedtuple( # [prefer-typing-namedtuple]
4+
"Philosophy", ("goodness", "truth", "beauty")
5+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import NamedTuple
2+
3+
4+
class Philosophy(NamedTuple):
5+
goodness: str
6+
truth: bool
7+
beauty: float
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MAIN]
2+
load-plugins = pylint.extensions.code_style
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `typing.NamedTuple <https://docs.python.org/3/library/typing.html#typing.NamedTuple>`_

doc/whatsnew/fragments/8660.extension

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add new ``prefer-typing-namedtuple`` message to the ``CodeStyleChecker`` to suggest
2+
rewriting calls to ``collections.namedtuple`` as classes inheriting from ``typing.NamedTuple``
3+
on Python 3.6+.
4+
5+
Requires ``load-plugins=pylint.extensions.code_style`` and ``enable=prefer-typing-namedtuple`` to be raised.
6+
7+
Closes #8660

pylint/checkers/classes/class_checker.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66

77
from __future__ import annotations
88

9-
import collections
109
from collections import defaultdict
1110
from collections.abc import Callable, Sequence
1211
from functools import cached_property
1312
from itertools import chain, zip_longest
1413
from re import Pattern
15-
from typing import TYPE_CHECKING, Any, Union
14+
from typing import TYPE_CHECKING, Any, NamedTuple, Union
1615

1716
import astroid
1817
from astroid import bases, nodes, util
@@ -63,12 +62,19 @@
6362
# Dealing with useless override detection, with regard
6463
# to parameters vs arguments
6564

66-
_CallSignature = collections.namedtuple(
67-
"_CallSignature", "args kws starred_args starred_kws"
68-
)
69-
_ParameterSignature = collections.namedtuple(
70-
"_ParameterSignature", "args kwonlyargs varargs kwargs"
71-
)
65+
66+
class _CallSignature(NamedTuple):
67+
args: list[str | None]
68+
kws: dict[str | None, str | None]
69+
starred_args: list[str]
70+
starred_kws: list[str]
71+
72+
73+
class _ParameterSignature(NamedTuple):
74+
args: list[str]
75+
kwonlyargs: list[str]
76+
varargs: str
77+
kwargs: str
7278

7379

7480
def _signature_from_call(call: nodes.Call) -> _CallSignature:

pylint/extensions/code_style.py

+21
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ class CodeStyleChecker(BaseChecker):
6969
"default_enabled": False,
7070
},
7171
),
72+
"R6105": (
73+
"Prefer 'typing.NamedTuple' over 'collections.namedtuple'",
74+
"prefer-typing-namedtuple",
75+
"'typing.NamedTuple' uses the well-known 'class' keyword "
76+
"with type-hints for readability (it's also faster as it avoids "
77+
"an internal exec call).\n"
78+
"Disabled by default!",
79+
{
80+
"default_enabled": False,
81+
},
82+
),
7283
}
7384
options = (
7485
(
@@ -89,12 +100,22 @@ class CodeStyleChecker(BaseChecker):
89100

90101
def open(self) -> None:
91102
py_version = self.linter.config.py_version
103+
self._py36_plus = py_version >= (3, 6)
92104
self._py38_plus = py_version >= (3, 8)
93105
self._max_length: int = (
94106
self.linter.config.max_line_length_suggestions
95107
or self.linter.config.max_line_length
96108
)
97109

110+
@only_required_for_messages("prefer-typing-namedtuple")
111+
def visit_call(self, node: nodes.Call) -> None:
112+
if self._py36_plus:
113+
called = safe_infer(node.func)
114+
if called and called.qname() == "collections.namedtuple":
115+
self.add_message(
116+
"prefer-typing-namedtuple", node=node, confidence=INFERENCE
117+
)
118+
98119
@only_required_for_messages("consider-using-namedtuple-or-dataclass")
99120
def visit_dict(self, node: nodes.Dict) -> None:
100121
self._check_dict_consider_namedtuple_dataclass(node)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# pylint: disable=missing-docstring
2+
from collections import namedtuple
3+
4+
NoteHash = namedtuple('NoteHash', ['Pitch', 'Duration', 'Offset']) # [prefer-typing-namedtuple]
5+
6+
class SearchMatch(
7+
namedtuple('SearchMatch', ['els', 'index', 'iterator']) # [prefer-typing-namedtuple]
8+
):
9+
"""Adapted from primer package `music21`."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[MAIN]
2+
load-plugins=pylint.extensions.code_style
3+
enable=prefer-typing-namedtuple
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
prefer-typing-namedtuple:4:11:4:66::Prefer 'typing.NamedTuple' over 'collections.namedtuple':INFERENCE
2+
prefer-typing-namedtuple:7:4:7:59:SearchMatch:Prefer 'typing.NamedTuple' over 'collections.namedtuple':INFERENCE

0 commit comments

Comments
 (0)