Skip to content

Commit 2dfce27

Browse files
committed
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. Local testing shows that caching reduces the total amount of calls to find_module methods (used by find_spec) by about 50%. Linting the test repository in the related issue goes from 40 seconds to 37 seconds. This was on a NVME disk and after warmup, so timing gains may be bigger on slower file systems like the one mentioned in the referenced issue. Closes pylint-dev/pylint#9310.
1 parent 7a3b482 commit 2dfce27

File tree

4 files changed

+24
-0
lines changed

4 files changed

+24
-0
lines changed

astroid/interpreter/_import/spec.py

+18
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424

2525
from . import util
2626

27+
_spec_cache = {}
28+
29+
30+
def clear_spec_cache():
31+
_spec_cache.clear()
32+
2733

2834
# The MetaPathFinder protocol comes from typeshed, which says:
2935
# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
@@ -423,6 +429,18 @@ def _find_spec_with_path(
423429
raise ImportError(f"No module named {'.'.join(module_parts)}")
424430

425431

432+
def spec_cache(func):
433+
def wrapper(*args):
434+
key = ".".join(args[0])
435+
if key not in _spec_cache:
436+
_spec_cache[key] = func(*args)
437+
438+
return _spec_cache[key]
439+
440+
return wrapper
441+
442+
443+
@spec_cache
426444
def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSpec:
427445
"""Find a spec for the given module.
428446

astroid/manager.py

+2
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,12 @@ def clear_cache(self) -> None:
442442
# pylint: disable=import-outside-toplevel
443443
from astroid.brain.helpers import register_all_brains
444444
from astroid.inference_tip import clear_inference_tip_cache
445+
from astroid.interpreter._import.spec import clear_spec_cache
445446
from astroid.interpreter.objectmodel import ObjectModel
446447
from astroid.nodes._base_nodes import LookupMixIn
447448
from astroid.nodes.scoped_nodes import ClassDef
448449

450+
clear_spec_cache()
449451
clear_inference_tip_cache()
450452
_invalidate_cache() # inference context cache
451453

tests/test_manager.py

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
AttributeInferenceError,
2424
)
2525
from astroid.interpreter._import import util
26+
from astroid.interpreter._import.spec import clear_spec_cache
2627
from astroid.modutils import EXT_LIB_DIRS, module_in_path
2728
from astroid.nodes import Const
2829
from astroid.nodes.scoped_nodes import ClassDef, Module
@@ -41,6 +42,7 @@ class AstroidManagerTest(
4142
):
4243
def setUp(self) -> None:
4344
super().setUp()
45+
clear_spec_cache()
4446
self.manager = test_utils.brainless_manager()
4547

4648
def test_ast_from_file(self) -> None:

tests/test_modutils.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from astroid import modutils
2323
from astroid.const import PY310_PLUS
2424
from astroid.interpreter._import import spec
25+
from astroid.interpreter._import.spec import clear_spec_cache
2526

2627
from . import resources
2728

@@ -41,6 +42,7 @@ class ModuleFileTest(unittest.TestCase):
4142
package = "mypypa"
4243

4344
def tearDown(self) -> None:
45+
clear_spec_cache()
4446
for k in list(sys.path_importer_cache):
4547
if "MyPyPa" in k:
4648
del sys.path_importer_cache[k]

0 commit comments

Comments
 (0)