Skip to content

Commit e7103e6

Browse files
Let AstroidManager.clear_cache act on other caches (#1521)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
1 parent c2d2057 commit e7103e6

File tree

7 files changed

+83
-36
lines changed

7 files changed

+83
-36
lines changed

astroid/inference.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
Union,
2424
)
2525

26-
import wrapt
27-
2826
from astroid import bases, decorators, helpers, nodes, protocols, util
2927
from astroid.context import (
3028
CallContext,
@@ -1037,42 +1035,32 @@ def infer_ifexp(self, context=None):
10371035
nodes.IfExp._infer = infer_ifexp # type: ignore[assignment]
10381036

10391037

1040-
# pylint: disable=dangerous-default-value
1041-
@wrapt.decorator
1042-
def _cached_generator(
1043-
func, instance: _FunctionDefT, args, kwargs, _cache={} # noqa: B006
1044-
):
1045-
node = instance
1046-
try:
1047-
return iter(_cache[func, id(node)])
1048-
except KeyError:
1049-
result = func(*args, **kwargs)
1050-
# Need to keep an iterator around
1051-
original, copy = itertools.tee(result)
1052-
_cache[func, id(node)] = list(copy)
1053-
return original
1054-
1055-
1056-
# When inferring a property, we instantiate a new `objects.Property` object,
1057-
# which in turn, because it inherits from `FunctionDef`, sets itself in the locals
1058-
# of the wrapping frame. This means that every time we infer a property, the locals
1059-
# are mutated with a new instance of the property. This is why we cache the result
1060-
# of the function's inference.
1061-
@_cached_generator
10621038
def infer_functiondef(
10631039
self: _FunctionDefT, context: Optional[InferenceContext] = None
10641040
) -> Generator[Union["Property", _FunctionDefT], None, InferenceErrorInfo]:
10651041
if not self.decorators or not bases._is_property(self):
10661042
yield self
10671043
return InferenceErrorInfo(node=self, context=context)
10681044

1045+
# When inferring a property, we instantiate a new `objects.Property` object,
1046+
# which in turn, because it inherits from `FunctionDef`, sets itself in the locals
1047+
# of the wrapping frame. This means that every time we infer a property, the locals
1048+
# are mutated with a new instance of the property. To avoid this, we detect this
1049+
# scenario and avoid passing the `parent` argument to the constructor.
1050+
parent_frame = self.parent.frame(future=True)
1051+
property_already_in_parent_locals = self.name in parent_frame.locals and any(
1052+
isinstance(val, objects.Property) for val in parent_frame.locals[self.name]
1053+
)
1054+
10691055
prop_func = objects.Property(
10701056
function=self,
10711057
name=self.name,
10721058
lineno=self.lineno,
1073-
parent=self.parent,
1059+
parent=self.parent if not property_already_in_parent_locals else None,
10741060
col_offset=self.col_offset,
10751061
)
1062+
if property_already_in_parent_locals:
1063+
prop_func.parent = self.parent
10761064
prop_func.postinit(body=[], args=self.args, doc_node=self.doc_node)
10771065
yield prop_func
10781066
return InferenceErrorInfo(node=self, context=context)

astroid/interpreter/objectmodel.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import os
2626
import pprint
2727
import types
28+
from functools import lru_cache
2829
from typing import TYPE_CHECKING, List, Optional
2930

3031
import astroid
@@ -100,6 +101,7 @@ def __get__(self, instance, cls=None):
100101
def __contains__(self, name):
101102
return name in self.attributes()
102103

104+
@lru_cache() # noqa
103105
def attributes(self) -> List[str]:
104106
"""Get the attributes which are exported by this object model."""
105107
return [o[LEN_OF_IMPL_PREFIX:] for o in dir(self) if o.startswith(IMPL_PREFIX)]

astroid/manager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from astroid.interpreter._import import spec
1919
from astroid.modutils import (
2020
NoSourceFile,
21+
_cache_normalize_path_,
2122
file_info_from_modpath,
2223
get_source_file,
2324
is_module_name_part_of_extension_package_whitelist,
@@ -365,8 +366,24 @@ def bootstrap(self):
365366
def clear_cache(self) -> None:
366367
"""Clear the underlying cache, bootstrap the builtins module and
367368
re-register transforms."""
369+
# import here because of cyclic imports
370+
# pylint: disable=import-outside-toplevel
371+
from astroid.inference_tip import clear_inference_tip_cache
372+
from astroid.interpreter.objectmodel import ObjectModel
373+
from astroid.nodes.node_classes import LookupMixIn
374+
375+
clear_inference_tip_cache()
376+
368377
self.astroid_cache.clear()
369378
AstroidManager.brain["_transform"] = TransformVisitor()
379+
380+
for lru_cache in (
381+
LookupMixIn.lookup,
382+
_cache_normalize_path_,
383+
ObjectModel.attributes,
384+
):
385+
lru_cache.cache_clear()
386+
370387
self.bootstrap()
371388

372389
# Reload brain plugins. During initialisation this is done in astroid.__init__.py

astroid/modutils.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
import sys
2323
import sysconfig
2424
import types
25+
from functools import lru_cache
2526
from pathlib import Path
26-
from typing import Dict, Set
27+
from typing import Set
2728

2829
from astroid.const import IS_JYTHON, IS_PYPY
2930
from astroid.interpreter._import import spec, util
@@ -138,21 +139,19 @@ def _handle_blacklist(blacklist, dirnames, filenames):
138139
filenames.remove(norecurs)
139140

140141

141-
_NORM_PATH_CACHE: Dict[str, str] = {}
142+
@lru_cache()
143+
def _cache_normalize_path_(path: str) -> str:
144+
return _normalize_path(path)
142145

143146

144147
def _cache_normalize_path(path: str) -> str:
145148
"""Normalize path with caching."""
146149
# _module_file calls abspath on every path in sys.path every time it's
147150
# called; on a larger codebase this easily adds up to half a second just
148151
# assembling path components. This cache alleviates that.
149-
try:
150-
return _NORM_PATH_CACHE[path]
151-
except KeyError:
152-
if not path: # don't cache result for ''
153-
return _normalize_path(path)
154-
result = _NORM_PATH_CACHE[path] = _normalize_path(path)
155-
return result
152+
if not path: # don't cache result for ''
153+
return _normalize_path(path)
154+
return _cache_normalize_path_(path)
156155

157156

158157
def load_module_from_name(dotted_name: str) -> types.ModuleType:

astroid/nodes/node_classes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def get_children(self):
367367
class LookupMixIn:
368368
"""Mixin to look up a name in the right scope."""
369369

370-
@lru_cache(maxsize=None) # pylint: disable=cache-max-size-none # noqa
370+
@lru_cache() # noqa
371371
def lookup(self, name: str) -> typing.Tuple[str, typing.List[NodeNG]]:
372372
"""Lookup where the given variable is assigned.
373373

tests/unittest_manager.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from astroid import manager, test_utils
1717
from astroid.const import IS_JYTHON
1818
from astroid.exceptions import AstroidBuildingError, AstroidImportError
19+
from astroid.modutils import is_standard_module
1920
from astroid.nodes import Const
21+
from astroid.nodes.scoped_nodes import ClassDef
2022

2123
from . import resources
2224

@@ -317,6 +319,43 @@ def test_borg(self) -> None:
317319

318320

319321
class ClearCacheTest(unittest.TestCase, resources.AstroidCacheSetupMixin):
322+
def test_clear_cache_clears_other_lru_caches(self) -> None:
323+
lrus = (
324+
astroid.nodes.node_classes.LookupMixIn.lookup,
325+
astroid.modutils._cache_normalize_path_,
326+
astroid.interpreter.objectmodel.ObjectModel.attributes,
327+
)
328+
329+
# Get a baseline for the size of the cache after simply calling bootstrap()
330+
baseline_cache_infos = [lru.cache_info() for lru in lrus]
331+
332+
# Generate some hits and misses
333+
ClassDef().lookup("garbage")
334+
is_standard_module("unittest", std_path=["garbage_path"])
335+
astroid.interpreter.objectmodel.ObjectModel().attributes()
336+
337+
# Did the hits or misses actually happen?
338+
incremented_cache_infos = [lru.cache_info() for lru in lrus]
339+
for incremented_cache, baseline_cache in zip(
340+
incremented_cache_infos, baseline_cache_infos
341+
):
342+
with self.subTest(incremented_cache=incremented_cache):
343+
self.assertGreater(
344+
incremented_cache.hits + incremented_cache.misses,
345+
baseline_cache.hits + baseline_cache.misses,
346+
)
347+
348+
astroid.MANAGER.clear_cache() # also calls bootstrap()
349+
350+
# The cache sizes are now as low or lower than the original baseline
351+
cleared_cache_infos = [lru.cache_info() for lru in lrus]
352+
for cleared_cache, baseline_cache in zip(
353+
cleared_cache_infos, baseline_cache_infos
354+
):
355+
with self.subTest(cleared_cache=cleared_cache):
356+
# less equal because the "baseline" might have had multiple calls to bootstrap()
357+
self.assertLessEqual(cleared_cache.currsize, baseline_cache.currsize)
358+
320359
def test_brain_plugins_reloaded_after_clearing_cache(self) -> None:
321360
astroid.MANAGER.clear_cache()
322361
format_call = astroid.extract_node("''.format()")

tests/unittest_modutils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def test_load_packages_without_init(self) -> None:
179179
https://github.com/PyCQA/astroid/issues/1327
180180
"""
181181
tmp_dir = Path(tempfile.gettempdir())
182-
self.addCleanup(os.chdir, os.curdir)
182+
self.addCleanup(os.chdir, os.getcwd())
183183
os.chdir(tmp_dir)
184184

185185
self.addCleanup(shutil.rmtree, tmp_dir / "src")
@@ -288,6 +288,8 @@ def test_custom_path(self) -> None:
288288
self.assertTrue(
289289
modutils.is_standard_module("data.module", (os.path.abspath(datadir),))
290290
)
291+
# "" will evaluate to cwd
292+
self.assertTrue(modutils.is_standard_module("data.module", ("",)))
291293

292294
def test_failing_edge_cases(self) -> None:
293295
# using a subpackage/submodule path as std_path argument

0 commit comments

Comments
 (0)