Skip to content

Commit 64177cd

Browse files
committed
[3.15] Detect PEP 798 unpacking in comprehension values
1 parent 495ce25 commit 64177cd

4 files changed

Lines changed: 53 additions & 15 deletions

File tree

README.rst

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -141,21 +141,22 @@ Features detected include v2/v3 ``print expr`` and ``print(expr)``, ``long``, f-
141141
asynchronous comprehensions, ``await`` in comprehensions, asynchronous ``for``-loops, boolean
142142
constants, named expressions, keyword-only parameters, positional-only parameters, ``nonlocal``,
143143
``yield from``, exception context cause (``raise .. from ..``), ``except*``, ``set`` literals,
144-
``set`` comprehensions, ``dict`` comprehensions, infix matrix multiplication, ``"..".format(..)``,
145-
imports (``import X``, ``from X import Y``, ``from X import *``), function calls wrt. name and
146-
kwargs, ``strftime`` + ``strptime`` directives used, function and variable annotations (also
147-
``Final`` and ``Literal``), ``continue`` in ``finally`` block, modular inverse ``pow()``, array
148-
typecodes, codecs error handler names, encodings, ``%`` formatting and directives for bytes and
149-
bytearray, ``with`` statement, asynchronous ``with`` statement, multiple context expressions in a
150-
``with`` statement, multiple context expressions in a ``with`` statement grouped with parenthesis,
151-
unpacking assignment, generalized unpacking, ellipsis literal (``...``) out of slices, dictionary
152-
union (``{..} | {..}``), dictionary union merge (``a = {..}; a |= {..}``), builtin generic type
153-
annotations (``list[str]``), function decorators, class decorators, relaxed decorators,
154-
``metaclass`` class keyword, pattern matching with ``match``, union types written as ``X | Y``, type
155-
alias statements (``type X = SomeType``), type alias statements with lambdas/comprehensions in class
156-
scopes, generic classes (``class C[T]: ...``), and template string literals (``t'{var}'``). It tries
157-
to detect and ignore user-defined functions, classes, arguments, and variables with names that clash
158-
with library-defined symbols.
144+
``set`` comprehensions, ``dict`` comprehensions, unpacking in comprehension value expressions (``[*x
145+
for x in range(10)]``), infix matrix multiplication, ``"..".format(..)``, imports (``import X``,
146+
``from X import Y``, ``from X import *``), function calls wrt. name and kwargs, ``strftime`` +
147+
``strptime`` directives used, function and variable annotations (also ``Final`` and ``Literal``),
148+
``continue`` in ``finally`` block, modular inverse ``pow()``, array typecodes, codecs error handler
149+
names, encodings, ``%`` formatting and directives for bytes and bytearray, ``with`` statement,
150+
asynchronous ``with`` statement, multiple context expressions in a ``with`` statement, multiple
151+
context expressions in a ``with`` statement grouped with parenthesis, unpacking assignment,
152+
generalized unpacking, ellipsis literal (``...``) out of slices, dictionary union (``{..} |
153+
{..}``), dictionary union merge (``a = {..}; a |= {..}``), builtin generic type annotations
154+
(``list[str]``), function decorators, class decorators, relaxed decorators, ``metaclass`` class
155+
keyword, pattern matching with ``match``, union types written as ``X | Y``, type alias statements
156+
(``type X = SomeType``), type alias statements with lambdas/comprehensions in class scopes, generic
157+
classes (``class C[T]: ...``), and template string literals (``t'{var}'``). It tries to detect and
158+
ignore user-defined functions, classes, arguments, and variables with names that clash with
159+
library-defined symbols.
159160

160161
Caveats
161162
=======

tests/lang.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,19 @@ def lower_upper(template):
11731173
visitor = self.visit("f'hello'")
11741174
self.assertFalse(visitor.template_string_literal())
11751175

1176+
@VerminTest.skipUnlessVersion(3, 15)
1177+
def test_unpacking_in_comprehension(self):
1178+
visitor = self.visit("[*x for x in range(10)]")
1179+
self.assertTrue(visitor.unpacking_in_comprehension())
1180+
self.assertOnlyIn((3, 15), visitor.minimum_versions())
1181+
1182+
visitor = self.visit("{*x for x in range(10)}")
1183+
self.assertTrue(visitor.unpacking_in_comprehension())
1184+
self.assertOnlyIn((3, 15), visitor.minimum_versions())
1185+
1186+
visitor = self.visit("[x for x in range(10)]")
1187+
self.assertFalse(visitor.unpacking_in_comprehension())
1188+
11761189
@VerminTest.skipUnlessVersion(3, 5)
11771190
def test_bytes_format(self):
11781191
visitor = self.visit("b'%x' % 10")

vermin/source_state.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ def __init__(self, config, path=None, source=None):
127127
# `t'text {var}'`
128128
self.template_string_literal = False
129129

130+
# Track depth of nested comprehension value expressions (PEP 798).
131+
self.comprehension_depth = 0
132+
133+
# `*x` unpacking in comprehension value expressions (PEP 798).
134+
self.unpacking_in_comprehension = False
135+
130136
# Imported members of modules, like "exc_clear" of "sys".
131137
self.import_mem_mod = {}
132138

vermin/source_visitor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ def metaclass_class_keyword(self):
276276
def generic_class(self):
277277
return self.__s.generic_class
278278

279+
def unpacking_in_comprehension(self):
280+
return self.__s.unpacking_in_comprehension
281+
279282
def __get_source_line(self, line, col=0):
280283
if self.__s.source is None:
281284
return None # pragma: no cover
@@ -529,6 +532,9 @@ def minimum_versions(self):
529532
if self.generic_class():
530533
mins = self.__add_versions_entity(mins, (None, (3, 12)), "generic class `class C[T]: ...`")
531534

535+
if self.unpacking_in_comprehension():
536+
mins = self.__add_versions_entity(mins, (None, (3, 15)),
537+
"unpacking in comprehension (PEP 798)")
532538
for directive in self.strftime_directives():
533539
if directive in STRFTIME_REQS:
534540
vers = STRFTIME_REQS[directive]
@@ -1080,6 +1086,10 @@ def visit_Starred(self, node):
10801086
if isinstance(node.ctx, ast.Store):
10811087
self.__s.unpacking_assignment = True
10821088
self.__vvprint("unpacking assignment", versions=[None, (3, 0)])
1089+
elif self.__s.comprehension_depth > 0 and isinstance(node.ctx, ast.Load):
1090+
# PEP 798: starred expression in comprehension value expression (3.15+).
1091+
self.__s.unpacking_in_comprehension = True
1092+
self.__vvprint("unpacking in comprehension (PEP 798)", versions=[None, (3, 15)])
10831093
self.generic_visit(node)
10841094

10851095
def __check_generalized_unpacking(self, node):
@@ -2062,30 +2072,38 @@ def __handle_comprehensions(self, comps):
20622072
return user_defs_copy
20632073

20642074
def visit_ListComp(self, node):
2075+
self.__s.comprehension_depth += 1
20652076
user_defs_copy = self.__handle_comprehensions(node.generators)
20662077
self.generic_visit(node)
20672078
self.__s.user_defs = user_defs_copy
2079+
self.__s.comprehension_depth -= 1
20682080

20692081
def visit_SetComp(self, node):
2082+
self.__s.comprehension_depth += 1
20702083
self.__s.set_comp = True
20712084
self.__vvprint("set comprehensions", versions=[(2, 7), (3, 0)])
20722085

20732086
user_defs_copy = self.__handle_comprehensions(node.generators)
20742087
self.generic_visit(node)
20752088
self.__s.user_defs = user_defs_copy
2089+
self.__s.comprehension_depth -= 1
20762090

20772091
def visit_GeneratorExp(self, node):
2092+
self.__s.comprehension_depth += 1
20782093
user_defs_copy = self.__handle_comprehensions(node.generators)
20792094
self.generic_visit(node)
20802095
self.__s.user_defs = user_defs_copy
2096+
self.__s.comprehension_depth -= 1
20812097

20822098
def visit_DictComp(self, node):
2099+
self.__s.comprehension_depth += 1
20832100
self.__s.dict_comp = True
20842101
self.__vvprint("dict comprehensions", versions=[(2, 7), (3, 0)])
20852102

20862103
user_defs_copy = self.__handle_comprehensions(node.generators)
20872104
self.generic_visit(node)
20882105
self.__s.user_defs = user_defs_copy
2106+
self.__s.comprehension_depth -= 1
20892107

20902108
def visit_comprehension(self, node):
20912109
if hasattr(node, "is_async") and node.is_async == 1:

0 commit comments

Comments
 (0)