Skip to content

Commit 3ce9af4

Browse files
crazybolillojacobtylerwalls
authored andcommitted
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.
1 parent 47c0b8f commit 3ce9af4

File tree

4 files changed

+17
-1
lines changed

4 files changed

+17
-1
lines changed

astroid/interpreter/_import/spec.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import warnings
1717
import zipimport
1818
from collections.abc import Iterator, Sequence
19+
from functools import lru_cache
1920
from pathlib import Path
2021
from typing import Any, Literal, NamedTuple, Protocol
2122

@@ -440,10 +441,15 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
440441
:return: A module spec, which describes how the module was
441442
found and where.
442443
"""
444+
return _find_spec(tuple(modpath), tuple(path) if path else None)
445+
446+
447+
@lru_cache(maxsize=1024)
448+
def _find_spec(modpath: tuple, path: tuple) -> ModuleSpec:
443449
_path = path or sys.path
444450

445451
# Need a copy for not mutating the argument.
446-
modpath = modpath[:]
452+
modpath = list(modpath)
447453

448454
submodule_path = None
449455
module_parts = modpath[:]
@@ -468,3 +474,7 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
468474
spec = spec._replace(submodule_search_locations=submodule_path)
469475

470476
return spec
477+
478+
479+
def clear_spec_cache() -> None:
480+
_find_spec.cache_clear()

astroid/manager.py

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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)