diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c0f9e21d..93d4669c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -15,13 +15,13 @@ import contextlib import collections +from ._collections import freezable_defaultdict from ._compat import ( NullFinder, + Protocol, PyPy_repr, install, - Protocol, ) - from ._functools import method_cache from ._itertools import unique_everseen @@ -710,8 +710,8 @@ class Lookup: def __init__(self, path: FastPath): base = os.path.basename(path.root).lower() base_is_egg = base.endswith(".egg") - self.infos = collections.defaultdict(list) - self.eggs = collections.defaultdict(list) + self.infos = freezable_defaultdict(list) + self.eggs = freezable_defaultdict(list) for child in path.children(): low = child.lower() @@ -725,6 +725,9 @@ def __init__(self, path: FastPath): legacy_normalized = Prepared.legacy_normalize(name) self.eggs[legacy_normalized].append(path.joinpath(child)) + self.infos.freeze() + self.eggs.freeze() + def search(self, prepared): infos = ( self.infos[prepared.normalized] @@ -736,7 +739,7 @@ def search(self, prepared): if prepared else itertools.chain.from_iterable(self.eggs.values()) ) - return list(itertools.chain(infos, eggs)) + return itertools.chain(infos, eggs) class Prepared: diff --git a/importlib_metadata/_collections.py b/importlib_metadata/_collections.py new file mode 100644 index 00000000..f0357989 --- /dev/null +++ b/importlib_metadata/_collections.py @@ -0,0 +1,21 @@ +import collections + + +class freezable_defaultdict(collections.defaultdict): + """ + Mix-in to freeze a defaultdict. + + >>> dd = freezable_defaultdict(list) + >>> dd[0].append('1') + >>> dd.freeze() + >>> dd[1] + [] + >>> len(dd) + 1 + """ + + def __missing__(self, key): + return getattr(self, '_frozen', super().__missing__)(key) + + def freeze(self): + self._frozen = lambda key: self.default_factory() diff --git a/tests/test_integration.py b/tests/test_integration.py index 11835135..00e9021a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -7,6 +7,7 @@ Distribution, MetadataPathFinder, _compat, + distributions, version, ) @@ -59,3 +60,16 @@ def test_search_dist_dirs(self): """ res = MetadataPathFinder._search_paths('any-name', []) assert list(res) == [] + + def test_interleaved_discovery(self): + """ + When the search is cached, it is + possible for searches to be interleaved, so make sure + those use-cases are safe. + + Ref #293 + """ + dists = distributions() + next(dists) + version('importlib_metadata') + next(dists)