Skip to content

Commit 9393480

Browse files
committed
splat nodes
1 parent ecedd42 commit 9393480

8 files changed

Lines changed: 91 additions & 12 deletions

File tree

doc/whatsnew/fragments/8168.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix ``nested-min-max`` suggestion message to indicate it's possible to splat iterable objects.
2+
3+
Closes #8168

pylint/checkers/nested_min_max.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from astroid import nodes
1313

1414
from pylint.checkers import BaseChecker
15-
from pylint.checkers.utils import only_required_for_messages, safe_infer
15+
from pylint.checkers.utils import DICT_TYPES, only_required_for_messages, safe_infer
1616
from pylint.interfaces import INFERENCE
1717

1818
if TYPE_CHECKING:
@@ -83,6 +83,20 @@ def visit_call(self, node: nodes.Call) -> None:
8383

8484
redundant_calls = self.get_redundant_calls(fixed_node)
8585

86+
for idx, arg in enumerate(fixed_node.args):
87+
if not isinstance(arg, nodes.Const):
88+
inferred = safe_infer(arg)
89+
if isinstance(
90+
inferred, (nodes.List, nodes.Tuple, nodes.Set, *DICT_TYPES)
91+
):
92+
splat_node = nodes.Starred(lineno=inferred.lineno)
93+
splat_node.value = arg
94+
fixed_node.args = (
95+
fixed_node.args[:idx]
96+
+ [splat_node]
97+
+ fixed_node.args[idx + 1 : idx]
98+
)
99+
86100
self.add_message(
87101
"nested-min-max",
88102
node=node,

pylint/checkers/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@
238238
{"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"}
239239
)
240240

241+
DICT_TYPES = (
242+
astroid.objects.DictValues,
243+
astroid.objects.DictKeys,
244+
astroid.objects.DictItems,
245+
astroid.nodes.node_classes.Dict,
246+
)
247+
241248

242249
class NoSuchArgumentError(Exception):
243250
pass

pylint/checkers/variables.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,6 @@
108108
}
109109
)
110110

111-
DICT_TYPES = (
112-
astroid.objects.DictValues,
113-
astroid.objects.DictKeys,
114-
astroid.objects.DictItems,
115-
astroid.nodes.node_classes.Dict,
116-
)
117-
118111

119112
class VariableVisitConsumerAction(Enum):
120113
"""Reported by _check_consumer() and its sub-methods to determine the
@@ -166,7 +159,7 @@ def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) ->
166159
and unbalanced-tuple/dict-unpacking errors.
167160
"""
168161
more = ""
169-
if isinstance(inferred, DICT_TYPES):
162+
if isinstance(inferred, utils.DICT_TYPES):
170163
if isinstance(node, nodes.Assign):
171164
more = node.value.as_string()
172165
elif isinstance(node, nodes.For):
@@ -1249,7 +1242,7 @@ def visit_for(self, node: nodes.For) -> None:
12491242
targets = node.target.elts
12501243

12511244
inferred = utils.safe_infer(node.iter)
1252-
if not isinstance(inferred, DICT_TYPES):
1245+
if not isinstance(inferred, utils.DICT_TYPES):
12531246
return
12541247

12551248
values = self._nodes_to_unpack(inferred)
@@ -2886,7 +2879,7 @@ def _check_unpacking(
28862879
@staticmethod
28872880
def _nodes_to_unpack(node: nodes.NodeNG) -> list[nodes.NodeNG] | None:
28882881
"""Return the list of values of the `Assign` node."""
2889-
if isinstance(node, (nodes.Tuple, nodes.List) + DICT_TYPES):
2882+
if isinstance(node, (nodes.Tuple, nodes.List) + utils.DICT_TYPES):
28902883
return node.itered() # type: ignore[no-any-return]
28912884
if isinstance(node, astroid.Instance) and any(
28922885
ancestor.qname() == "typing.NamedTuple" for ancestor in node.ancestors()
@@ -2912,7 +2905,7 @@ def _report_unbalanced_unpacking(
29122905

29132906
symbol = (
29142907
"unbalanced-dict-unpacking"
2915-
if isinstance(inferred, DICT_TYPES)
2908+
if isinstance(inferred, utils.DICT_TYPES)
29162909
else "unbalanced-tuple-unpacking"
29172910
)
29182911
self.add_message(symbol, node=node, args=args, confidence=INFERENCE)

tests/functional/n/nested_min_max.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,26 @@
1919
# This is too complicated (for now) as there is no clear better way to write it
2020
max(max(i for i in range(10)), 0)
2121
max(max(max(i for i in range(10)), 0), 1)
22+
23+
# These examples can be improved by splicing
24+
lst = [1, 2]
25+
max(3, max(lst)) # [nested-min-max]
26+
max(3, *lst)
27+
28+
nums = (1, 2,)
29+
max(3, max(nums)) # [nested-min-max]
30+
max(3, *nums)
31+
32+
nums = {1, 2}
33+
max(3, max(nums)) # [nested-min-max]
34+
max(3, *nums)
35+
36+
nums = {1: 2, 7: 10}
37+
max(3, max(nums)) # [nested-min-max]
38+
max(3, *nums)
39+
40+
max(3, max(nums.values())) # [nested-min-max]
41+
max(3, *nums.values())
42+
43+
lst2 = [3, 7, 10]
44+
max(3, max(nums), max(lst2)) # [nested-min-max]

tests/functional/n/nested_min_max.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ nested-min-max:8:0:8:25::Do not use nested call of 'min'; it's possible to do 'm
66
nested-min-max:11:0:11:25::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3, 4)' instead:INFERENCE
77
nested-min-max:12:0:12:40::Do not use nested call of 'min'; it's possible to do 'min(len([]), len([1]), len([1, 2]))' instead:INFERENCE
88
nested-min-max:17:0:17:27::Do not use nested call of 'orig_min'; it's possible to do 'orig_min(1, 2, 3)' instead:INFERENCE
9+
nested-min-max:25:0:25:16::Do not use nested call of 'max'; it's possible to do 'max(3, *lst)' instead:INFERENCE
10+
nested-min-max:29:0:29:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE
11+
nested-min-max:33:0:33:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE
12+
nested-min-max:37:0:37:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE
13+
nested-min-max:40:0:40:26::Do not use nested call of 'max'; it's possible to do 'max(3, *nums.values())' instead:INFERENCE
14+
nested-min-max:44:0:44:28::Do not use nested call of 'max'; it's possible to do 'max(3, *nums, *lst2)' instead:INFERENCE
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""All of these nested-min-max messages should suggest splatting."""
2+
3+
lst = [1, 2]
4+
max(3, max(lst)) # [nested-min-max]
5+
6+
nums = (1, 2,)
7+
max(3, max(nums)) # [nested-min-max]
8+
9+
nums = {1, 2}
10+
max(3, max(nums)) # [nested-min-max]
11+
12+
nums = {1: 2, 7: 10}
13+
max(3, max(nums)) # [nested-min-max]
14+
15+
max(3, max(nums.values())) # [nested-min-max]
16+
17+
lst2 = [3, 7, 10]
18+
max(3, max(nums), max(lst2)) # [nested-min-max]

tests/test_self.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,21 @@ def test_no_name_in_module(self) -> None:
13021302
[module, "-E"], expected_output="", unexpected_output=unexpected
13031303
)
13041304

1305+
def test_nested_min_max_splats(self) -> None:
1306+
"""Test that the nested-min-max message suggests to splat an iterable"""
1307+
module = join(HERE, "regrtest_data", "nested_min_max.py")
1308+
expected_str = f"""
1309+
************* Module nested_min_max
1310+
{module}:4:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *lst)' instead (nested-min-max)
1311+
{module}:7:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead (nested-min-max)
1312+
{module}:10:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead (nested-min-max)
1313+
{module}:13:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead (nested-min-max)
1314+
{module}:15:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *nums.values())' instead (nested-min-max)
1315+
{module}:18:0: W3301: Do not use nested call of 'max'; it's possible to do 'max(3, *nums, *lst2)' instead (nested-min-max)
1316+
""" # noqa: E501
1317+
expected = textwrap.dedent(expected_str)
1318+
self._test_output([module], expected_output=expected)
1319+
13051320

13061321
class TestCallbackOptions:
13071322
"""Test for all callback options we support."""

0 commit comments

Comments
 (0)