Skip to content

Commit

Permalink
Fix incorrect scope for functools partials (#1097)
Browse files Browse the repository at this point in the history
* Use scope parent
* Do not set name onto parent frame for partials
* Add test case that captures broken scopes
  • Loading branch information
Alphadelta14 committed Aug 1, 2021
1 parent e178626 commit ac95965
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 12 deletions.
2 changes: 1 addition & 1 deletion astroid/brain/brain_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _functools_partial_inference(node, context=None):
doc=inferred_wrapped_function.doc,
lineno=inferred_wrapped_function.lineno,
col_offset=inferred_wrapped_function.col_offset,
parent=inferred_wrapped_function.parent,
parent=node.parent,
)
partial_function.postinit(
args=inferred_wrapped_function.args,
Expand Down
12 changes: 2 additions & 10 deletions astroid/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1209,16 +1209,8 @@ def _filter_stmts(self, stmts, frame, offset):
# want to clear previous assignments if any (hence the test on
# optional_assign)
if not (optional_assign or are_exclusive(_stmts[pindex], node)):
if (
# In case of partial function node, if the statement is different
# from the origin function then it can be deleted otherwise it should
# remain to be able to correctly infer the call to origin function.
not node.is_function
or node.qname() != "PartialFunction"
or node.name != _stmts[pindex].name
):
del _stmt_parents[pindex]
del _stmts[pindex]
del _stmt_parents[pindex]
del _stmts[pindex]
if isinstance(node, AssignName):
if not optional_assign and stmt.parent is mystmt.parent:
_stmts = []
Expand Down
5 changes: 4 additions & 1 deletion astroid/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,10 @@ class PartialFunction(scoped_nodes.FunctionDef):
def __init__(
self, call, name=None, doc=None, lineno=None, col_offset=None, parent=None
):
super().__init__(name, doc, lineno, col_offset, parent)
super().__init__(name, doc, lineno, col_offset, parent=None)
# A typical FunctionDef automatically adds its name to the parent scope,
# but a partial should not, so defer setting parent until after init
self.parent = parent
self.filled_positionals = len(call.positional_arguments[1:])
self.filled_args = call.positional_arguments[1:]
self.filled_keywords = call.keyword_arguments
Expand Down
40 changes: 40 additions & 0 deletions tests/unittest_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,46 @@ def other_test(a, b, *, c=1):
assert isinstance(inferred, astroid.Const)
assert inferred.value == expected_value

def test_partial_assignment(self):
"""Make sure partials are not assigned to original scope."""
ast_nodes = astroid.extract_node(
"""
from functools import partial
def test(a, b): #@
return a + b
test2 = partial(test, 1)
test2 #@
def test3_scope(a):
test3 = partial(test, a)
test3 #@
"""
)
func1, func2, func3 = ast_nodes
assert func1.parent.scope() == func2.parent.scope()
assert func1.parent.scope() != func3.parent.scope()
partial_func3 = next(func3.infer())
# use scope of parent, so that it doesn't just refer to self
scope = partial_func3.parent.scope()
assert scope.name == "test3_scope", "parented by closure"

def test_partial_does_not_affect_scope(self):
"""Make sure partials are not automatically assigned."""
ast_nodes = astroid.extract_node(
"""
from functools import partial
def test(a, b):
return a + b
def scope():
test2 = partial(test, 1)
test2 #@
"""
)
test2 = next(ast_nodes.infer())
mod_scope = test2.root()
scope = test2.parent.scope()
assert set(mod_scope) == {"test", "scope", "partial"}
assert set(scope) == {"test2"}


def test_http_client_brain():
node = astroid.extract_node(
Expand Down

0 comments on commit ac95965

Please sign in to comment.