summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2020-03-06 10:46:26 +0100
committerClaudiu Popa <pcmanticore@gmail.com>2020-03-06 10:54:48 +0100
commit17a5ee681bcf4aacffcc4ec5afbc3436cfdc4537 (patch)
tree10c691821e62b58c4c4a9efd5b29ef828306c24c
parent88fd426e14c34cb5771fd6c06f5a1ba50bb03292 (diff)
downloadastroid-git-17a5ee681bcf4aacffcc4ec5afbc3436cfdc4537.tar.gz
Cache the inference of FunctionDef to prevent property inference mutating locals
When inferring a property, we instantiate a new `objects.Property` object, which in turn, because it inherits from `FunctionDef`, sets itself in the locals of the wrapping frame. This means that everytime we infer a property, the locals are mutated with a new instance of the property. Using `context` with `path_wrapper` would not have helped, because we call `inferred()` on functions in multiple places in pylint's codebase.
-rw-r--r--astroid/inference.py24
-rw-r--r--tests/unittest_inference.py22
2 files changed, 45 insertions, 1 deletions
diff --git a/astroid/inference.py b/astroid/inference.py
index 975b7d97..683f8609 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -25,6 +25,7 @@ import functools
import itertools
import operator
+import wrapt
from astroid import bases
from astroid import context as contextmod
from astroid import exceptions
@@ -949,10 +950,30 @@ def infer_ifexp(self, context=None):
nodes.IfExp._infer = infer_ifexp
+# pylint: disable=dangerous-default-value
+@wrapt.decorator
+def _cached_generator(func, instance, args, kwargs, _cache={}):
+ node = args[0]
+ try:
+ return iter(_cache[func, id(node)])
+ except KeyError:
+ result = func(*args, **kwargs)
+ # Need to keep an iterator around
+ original, copy = itertools.tee(result)
+ _cache[func, id(node)] = list(copy)
+ return original
+
+
+# When inferring a property, we instantiate a new `objects.Property` object,
+# which in turn, because it inherits from `FunctionDef`, sets itself in the locals
+# of the wrapping frame. This means that everytime we infer a property, the locals
+# are mutated with a new instance of the property. This is why we cache the result
+# of the function's inference.
+@_cached_generator
def infer_functiondef(self, context=None):
if not self.decorators or not bases._is_property(self):
yield self
- return
+ return dict(node=self, context=context)
prop_func = objects.Property(
function=self,
@@ -964,6 +985,7 @@ def infer_functiondef(self, context=None):
)
prop_func.postinit(body=[], args=self.args)
yield prop_func
+ return dict(node=self, context=context)
nodes.FunctionDef._infer = infer_functiondef
diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py
index 751a4586..49ff6cdc 100644
--- a/tests/unittest_inference.py
+++ b/tests/unittest_inference.py
@@ -5707,5 +5707,27 @@ def test_self_reference_infer_does_not_trigger_recursion_error():
assert inferred is util.Uninferable
+def test_inferring_properties_multiple_time_does_not_mutate_locals_multiple_times():
+ code = """
+ class A:
+ @property
+ def a(self):
+ return 42
+
+ A()
+ """
+ node = extract_node(code)
+ # Infer the class
+ cls = next(node.infer())
+ prop, = cls.getattr("a")
+
+ # Try to infer the property function *multiple* times. `A.locals` should be modified only once
+ for _ in range(3):
+ prop.inferred()
+ a_locals = cls.locals["a"]
+ # [FunctionDef, Property]
+ assert len(a_locals) == 2
+
+
if __name__ == "__main__":
unittest.main()