Skip to content

Commit

Permalink
Detect generalized unpacking
Browse files Browse the repository at this point in the history
  • Loading branch information
netromdk committed Jan 11, 2020
1 parent 302035f commit 0b94b58
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 8 deletions.
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ exception context cause (``raise .. from ..``), ``dict`` comprehensions, infix m
multiplication, ``"..".format(..)``, imports (``import X``, ``from X import Y``, ``from X import
*``), function calls wrt. name and kwargs, ``strftime`` + ``strptime`` directives used, and
function, function and variable annotations (also ``Final`` and ``Literal``), ``continue`` in
``finally`` block, modular inverse ``pow()``, array typecodes, codecs error handler names and
encodings. It tries to detect and ignore user-defined functions, classes, arguments, and variables
with names that clash with library-defined symbols.
``finally`` block, modular inverse ``pow()``, array typecodes, codecs error handler names,
encodings, and generalized unpacking. It tries to detect and ignore user-defined functions, classes,
arguments, and variables with names that clash with library-defined symbols.

Backports of the standard library, like ``typing``, can be enabled for better results.

Expand Down
31 changes: 31 additions & 0 deletions tests/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,34 @@ def test_with_statement(self):
visitor = visit("with func():\n pass")
self.assertTrue(visitor.with_statement())
self.assertOnlyIn([(2, 5), (3, 0)], visitor.minimum_versions())

def test_generalized_unpacking(self):
if current_version() >= 3.0:
visitor = visit("(*range(4), 4)") # tuple
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

visitor = visit("[*range(4), 4]") # list
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

if current_version() >= 3.5:
visitor = visit("{*range(4), 4}") # set
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

visitor = visit("{'x': 1, **{'y': 2}}") # dict
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

visitor = visit("function(*arguments, argument)")
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

visitor = visit("function(**kw_arguments, **more_arguments)")
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())

visitor = visit("function(**{'x': 42}, arg=84)")
self.assertTrue(visitor.generalized_unpacking())
self.assertOnlyIn((3, 5), visitor.minimum_versions())
62 changes: 57 additions & 5 deletions vermin/source_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(self, config=None):
self.__codecs_error_handlers = []
self.__codecs_encodings = []
self.__with_statement = False
self.__generalized_unpacking = False

# Imported members of modules, like "exc_clear" of "sys".
self.__import_mem_mod = {}
Expand Down Expand Up @@ -183,6 +184,9 @@ def modular_inverse_pow(self):
def with_statement(self):
return self.__with_statement

def generalized_unpacking(self):
return self.__generalized_unpacking

def minimum_versions(self):
mins = [(0, 0), (0, 0)]

Expand Down Expand Up @@ -273,6 +277,9 @@ def minimum_versions(self):
if self.with_statement():
mins = combine_versions(mins, ((2, 5), (3, 0)))

if self.generalized_unpacking():
mins = combine_versions(mins, (None, (3, 5)))

for directive in self.strftime_directives():
if directive in STRFTIME_REQS:
vers = STRFTIME_REQS[directive]
Expand Down Expand Up @@ -715,6 +722,18 @@ def visit_Print(self, node):
self.__printv2 = True
self.generic_visit(node)

def __check_generalized_unpacking(self, elts):
# If any elements occur after a Starred element then generalized unpacking is used.
if hasattr(ast, "Starred"):
starred = False
for elt in elts:
if isinstance(elt, ast.Starred):
starred = True
elif starred:
self.__generalized_unpacking = True
self.__vvprint("generalized unpacking requires 3.5+")
break

def visit_Call(self, node):
if self.__is_no_line(node.lineno):
return
Expand Down Expand Up @@ -766,6 +785,7 @@ def visit_Call(self, node):
# "array.array" = 5 + 1 + 5 + 1 = 12
self.__add_array_typecode(arg.s, node.lineno, node.col_offset + 12)

self.__check_generalized_unpacking(node.args)
self.generic_visit(node)
self.__function_name = None

Expand Down Expand Up @@ -793,6 +813,13 @@ def visit_Attribute(self, node):

def visit_keyword(self, node):
if self.__function_name is not None:
# If any function keyword argument is None and value is not None then generalized unpacking is
# used.
if node.arg is None and node.value is not None:
self.__generalized_unpacking = True
self.__vvprint("generalized unpacking requires 3.5+")

# kwarg related.
exp_name = self.__function_name.split(".")

# Check if function is imported from module.
Expand Down Expand Up @@ -993,6 +1020,36 @@ def visit_Continue(self, node):
self.__continue_in_finally = True
self.__vvprint("continue in finally block requires 3.8+", line=node.lineno)

def visit_With(self, node):
self.__with_statement = True
self.__vvprint("`with` requires 2.5+")
self.generic_visit(node)

def visit_Dict(self, node):
# If any key is None and corresponding value is not None then generalized unpacking is used.
keys = len(node.keys)
values = len(node.values)
if keys > 0 and keys == values:
for i in range(keys):
if node.keys[i] is None and node.values[i] is not None:
self.__generalized_unpacking = True
self.__vvprint("generalized unpacking requires 3.5+")
break

self.generic_visit(node)

def visit_Tuple(self, node):
self.__check_generalized_unpacking(node.elts)
self.generic_visit(node)

def visit_List(self, node):
self.__check_generalized_unpacking(node.elts)
self.generic_visit(node)

def visit_Set(self, node):
self.__check_generalized_unpacking(node.elts)
self.generic_visit(node)

# Lax mode and comment-excluded lines skip conditional blocks if enabled.

def visit_If(self, node):
Expand Down Expand Up @@ -1038,11 +1095,6 @@ def visit_BoolOp(self, node):
if not self.__config.lax_mode() and not self.__is_no_line(node.lineno):
self.generic_visit(node)

def visit_With(self, node):
self.__with_statement = True
self.__vvprint("`with` requires 2.5+")
self.generic_visit(node)

# Ignore unused nodes as a speed optimization.

def visit_alias(self, node):
Expand Down

0 comments on commit 0b94b58

Please sign in to comment.