Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance by caching find_spec #2408

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion astroid/interpreter/_import/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import warnings
import zipimport
from collections.abc import Iterator, Sequence
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal, NamedTuple, Protocol

Expand Down Expand Up @@ -440,10 +441,15 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
:return: A module spec, which describes how the module was
found and where.
"""
return _find_spec(tuple(modpath), tuple(path) if path else None)
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved


@lru_cache(maxsize=1024)
def _find_spec(modpath: tuple, path: tuple) -> ModuleSpec:
_path = path or sys.path

# Need a copy for not mutating the argument.
modpath = modpath[:]
modpath = list(modpath)

submodule_path = None
module_parts = modpath[:]
Expand Down
2 changes: 2 additions & 0 deletions astroid/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ def clear_cache(self) -> None:
# pylint: disable=import-outside-toplevel
from astroid.brain.helpers import register_all_brains
from astroid.inference_tip import clear_inference_tip_cache
from astroid.interpreter._import.spec import _find_spec
from astroid.interpreter.objectmodel import ObjectModel
from astroid.nodes._base_nodes import LookupMixIn
from astroid.nodes.scoped_nodes import ClassDef
Expand All @@ -459,6 +460,7 @@ def clear_cache(self) -> None:
util.is_namespace,
ObjectModel.attributes,
ClassDef._metaclass_lookup_attribute,
_find_spec,
):
lru_cache.cache_clear() # type: ignore[attr-defined]

Expand Down
26 changes: 0 additions & 26 deletions tests/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from pathlib import Path

from astroid import builder
from astroid.manager import AstroidManager
from astroid.nodes.scoped_nodes import Module

DATA_DIR = Path("testdata") / "python3"
Expand All @@ -34,28 +33,3 @@ def tearDown(self) -> None:
for key in list(sys.path_importer_cache):
if key.startswith(datadir):
del sys.path_importer_cache[key]


class AstroidCacheSetupMixin:
"""Mixin for handling test isolation issues with the astroid cache.

When clearing the astroid cache, some tests fail due to
cache inconsistencies, where some objects had a different
builtins object referenced.
This saves the builtins module and TransformVisitor and
replaces them after the tests finish.
The builtins module is special, since some of the
transforms for a couple of its objects (str, bytes etc)
are executed only once, so astroid_bootstrapping will be
useless for retrieving the original builtins module.
"""

@classmethod
def setup_class(cls):
cls._builtins = AstroidManager().astroid_cache.get("builtins")
cls._transforms = AstroidManager.brain["_transform"]

@classmethod
def teardown_class(cls):
AstroidManager().astroid_cache["builtins"] = cls._builtins
AstroidManager.brain["_transform"] = cls._transforms
8 changes: 4 additions & 4 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ def _get_file_from_object(obj) -> str:
return obj.__file__


class AstroidManagerTest(
resources.SysPathSetup, resources.AstroidCacheSetupMixin, unittest.TestCase
):
class AstroidManagerTest(resources.SysPathSetup, unittest.TestCase):
def setUp(self) -> None:
super().setUp()
self.manager = test_utils.brainless_manager()
self.manager.clear_cache()

def test_ast_from_file(self) -> None:
filepath = unittest.__file__
Expand Down Expand Up @@ -391,9 +390,10 @@ def test_denied_modules_raise(self) -> None:
self.manager.ast_from_module_name("math")


class IsolatedAstroidManagerTest(resources.AstroidCacheSetupMixin, unittest.TestCase):
class IsolatedAstroidManagerTest(unittest.TestCase):
def test_no_user_warning(self):
mgr = manager.AstroidManager()
self.addCleanup(mgr.clear_cache)
with warnings.catch_warnings():
warnings.filterwarnings("error", category=UserWarning)
mgr.ast_from_module_name("setuptools")
Expand Down
1 change: 1 addition & 0 deletions tests/test_modutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ModuleFileTest(unittest.TestCase):
package = "mypypa"

def tearDown(self) -> None:
astroid.MANAGER.clear_cache()
for k in list(sys.path_importer_cache):
if "MyPyPa" in k:
del sys.path_importer_cache[k]
Expand Down
3 changes: 2 additions & 1 deletion tests/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
HAS_NUMPY = True


class NonRegressionTests(resources.AstroidCacheSetupMixin, unittest.TestCase):
class NonRegressionTests(unittest.TestCase):
def setUp(self) -> None:
sys.path.insert(0, resources.find("data"))
MANAGER.always_load_extensions = True
self.addCleanup(MANAGER.clear_cache)

def tearDown(self) -> None:
MANAGER.always_load_extensions = False
Expand Down