Skip to content

Commit 1b2f354

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 58 seconds to 52 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 1b2f354

File tree

4 files changed

+27
-3
lines changed

4 files changed

+27
-3
lines changed

astroid/interpreter/_import/spec.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import warnings
1717
import zipimport
1818
from collections.abc import Iterator, Sequence
19+
from dataclasses import dataclass, field
20+
from functools import lru_cache
1921
from pathlib import Path
2022
from typing import Any, Literal, NamedTuple, Protocol
2123

@@ -423,7 +425,19 @@ def _find_spec_with_path(
423425
raise ImportError(f"No module named {'.'.join(module_parts)}")
424426

425427

428+
@dataclass(frozen=True)
429+
class SpecArgs:
430+
key: tuple
431+
modpath: list[str] = field(compare=False, hash=False)
432+
path: Sequence[str] | None = field(compare=False, hash=False)
433+
434+
426435
def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSpec:
436+
return _find_spec(SpecArgs(tuple(modpath), modpath, path))
437+
438+
439+
@lru_cache(maxsize=1024)
440+
def _find_spec(args: SpecArgs) -> ModuleSpec:
427441
"""Find a spec for the given module.
428442
429443
:type modpath: list or tuple
@@ -440,10 +454,10 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
440454
:return: A module spec, which describes how the module was
441455
found and where.
442456
"""
443-
_path = path or sys.path
457+
_path = args.path or sys.path
444458

445459
# Need a copy for not mutating the argument.
446-
modpath = modpath[:]
460+
modpath = args.modpath[:]
447461

448462
submodule_path = None
449463
module_parts = modpath[:]
@@ -452,7 +466,7 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
452466
while modpath:
453467
modname = modpath.pop(0)
454468
finder, spec = _find_spec_with_path(
455-
_path, modname, module_parts, processed, submodule_path or path
469+
_path, modname, module_parts, processed, submodule_path or args.path
456470
)
457471
processed.append(modname)
458472
if modpath:
@@ -468,3 +482,7 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
468482
spec = spec._replace(submodule_search_locations=submodule_path)
469483

470484
return spec
485+
486+
487+
def clear_spec_cache() -> None:
488+
_find_spec.cache_clear()

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)