Improve performance by caching find_spec
Certain checkers upstream on pylint like import-error heavily use
find_spec. This method is IO intensive as it looks for files
across several search paths to return a ModuleSpec.
Since imports across files may repeat themselves it makes sense to cache
this method in order to speed up the linting process.
Closes pylint-dev/pylint#9310.
diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py
index 93096e5..e1c5ed0 100644
--- a/astroid/interpreter/_import/spec.py
+++ b/astroid/interpreter/_import/spec.py
@@ -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
@@ -440,10 +441,15 @@
:return: A module spec, which describes how the module was
found and where.
"""
+ return _find_spec(tuple(modpath), tuple(path) if path else None)
+
+
+@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[:]
@@ -468,3 +474,7 @@
spec = spec._replace(submodule_search_locations=submodule_path)
return spec
+
+
+def clear_spec_cache() -> None:
+ _find_spec.cache_clear()
diff --git a/astroid/manager.py b/astroid/manager.py
index a7a51f1..195ac66 100644
--- a/astroid/manager.py
+++ b/astroid/manager.py
@@ -442,10 +442,12 @@
# 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 clear_spec_cache
from astroid.interpreter.objectmodel import ObjectModel
from astroid.nodes._base_nodes import LookupMixIn
from astroid.nodes.scoped_nodes import ClassDef
+ clear_spec_cache()
clear_inference_tip_cache()
_invalidate_cache() # inference context cache
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 7861927..160fa94 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -23,6 +23,7 @@
AttributeInferenceError,
)
from astroid.interpreter._import import util
+from astroid.interpreter._import.spec import clear_spec_cache
from astroid.modutils import EXT_LIB_DIRS, module_in_path
from astroid.nodes import Const
from astroid.nodes.scoped_nodes import ClassDef, Module
@@ -41,6 +42,7 @@
):
def setUp(self) -> None:
super().setUp()
+ clear_spec_cache()
self.manager = test_utils.brainless_manager()
def test_ast_from_file(self) -> None:
diff --git a/tests/test_modutils.py b/tests/test_modutils.py
index 929c589..be7095e 100644
--- a/tests/test_modutils.py
+++ b/tests/test_modutils.py
@@ -22,6 +22,7 @@
from astroid import modutils
from astroid.const import PY310_PLUS
from astroid.interpreter._import import spec
+from astroid.interpreter._import.spec import clear_spec_cache
from . import resources
@@ -41,6 +42,7 @@
package = "mypypa"
def tearDown(self) -> None:
+ clear_spec_cache()
for k in list(sys.path_importer_cache):
if "MyPyPa" in k:
del sys.path_importer_cache[k]