diff --git a/NEWS.rst b/NEWS.rst index 085a5305..850e8f00 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,19 @@ +v7.1.0 +====== + +Features +-------- + +- Improve import time (python/cpython#114664). + + +Bugfixes +-------- + +- Make MetadataPathFinder.find_distributions a classmethod for consistency with CPython. Closes #484. (#484) +- Allow ``MetadataPathFinder.invalidate_caches`` to be called as a classmethod. + + v7.0.2 ====== diff --git a/docs/conf.py b/docs/conf.py index 134d7534..90a1da2a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(Python #|py-)(?P\d+)', + pattern=r'(python/cpython#|Python #|py-)(?P\d+)', url='https://github.com/python/cpython/issues/{python}', ), ], diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c68d8ad8..32ee3b4d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -3,7 +3,6 @@ import os import re import abc -import csv import sys import json import zipp @@ -19,7 +18,8 @@ import posixpath import collections -from . import _adapters, _meta, _py39compat +from . import _adapters, _meta +from .compat import py39 from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -280,7 +280,7 @@ def select(self, **params) -> EntryPoints: Select entry points from self that match the given parameters (typically group and/or name). """ - return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) + return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) @property def names(self) -> Set[str]: @@ -522,6 +522,10 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): + # Delay csv import, since Distribution.files is not as widely used + # as other parts of importlib.metadata + import csv + return starmap(make_file, csv.reader(lines)) @pass_none @@ -873,8 +877,9 @@ class MetadataPathFinder(NullFinder, DistributionFinder): of Python that do not have a PathFinder find_distributions(). """ + @classmethod def find_distributions( - self, context=DistributionFinder.Context() + cls, context=DistributionFinder.Context() ) -> Iterable[PathDistribution]: """ Find distributions. @@ -884,7 +889,7 @@ def find_distributions( (or all names if ``None`` indicated) along the paths in the list of directories ``context.path``. """ - found = self._search_paths(context.name, context.path) + found = cls._search_paths(context.name, context.path) return map(PathDistribution, found) @classmethod @@ -895,6 +900,7 @@ def _search_paths(cls, name, paths): path.search(prepared) for path in map(FastPath, paths) ) + @classmethod def invalidate_caches(cls) -> None: FastPath.__new__.cache_clear() @@ -992,7 +998,7 @@ def version(distribution_name: str) -> str: _unique = functools.partial( unique_everseen, - key=_py39compat.normalized_name, + key=py39.normalized_name, ) """ Wrapper for ``distributions`` to return unique distributions by name. diff --git a/importlib_metadata/compat/__init__.py b/importlib_metadata/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/compat/py39.py similarity index 82% rename from importlib_metadata/_py39compat.py rename to importlib_metadata/compat/py39.py index fc6b8221..1f15bd97 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/compat/py39.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. - from . import Distribution, EntryPoint + from .. import Distribution, EntryPoint else: Distribution = EntryPoint = Any @@ -18,7 +18,7 @@ def normalized_name(dist: Distribution) -> Optional[str]: try: return dist._normalized_name except AttributeError: - from . import Prepared # -> delay to prevent circular imports. + from .. import Prepared # -> delay to prevent circular imports. return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) @@ -30,7 +30,7 @@ def ep_matches(ep: EntryPoint, **params) -> bool: try: return ep.matches(**params) except AttributeError: - from . import EntryPoint # -> delay to prevent circular imports. + from .. import EntryPoint # -> delay to prevent circular imports. # Reconstruct the EntryPoint object to make sure it is compatible. return EntryPoint(ep.name, ep.value, ep.group).matches(**params) diff --git a/setup.cfg b/setup.cfg index 71b66b39..02d3c7e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ testing = pyfakefs flufl.flake8 pytest-perf >= 0.9.2 + jaraco.test >= 5.4 docs = # upstream diff --git a/tests/compat/__init__.py b/tests/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/compat/py312.py b/tests/compat/py312.py new file mode 100644 index 00000000..ea9a58ba --- /dev/null +++ b/tests/compat/py312.py @@ -0,0 +1,18 @@ +import contextlib + +from .py39 import import_helper + + +@contextlib.contextmanager +def isolated_modules(): + """ + Save modules on entry and cleanup on exit. + """ + (saved,) = import_helper.modules_setup() + try: + yield + finally: + import_helper.modules_cleanup(saved) + + +vars(import_helper).setdefault('isolated_modules', isolated_modules) diff --git a/tests/compat/py39.py b/tests/compat/py39.py new file mode 100644 index 00000000..16c8b574 --- /dev/null +++ b/tests/compat/py39.py @@ -0,0 +1,9 @@ +from jaraco.test.cpython import from_test_support, try_import + + +os_helper = try_import('os_helper') or from_test_support( + 'FS_NONASCII', 'skip_unless_symlink' +) +import_helper = try_import('import_helper') or from_test_support( + 'modules_setup', 'modules_cleanup' +) diff --git a/tests/test_py39compat.py b/tests/compat/test_py39_compat.py similarity index 95% rename from tests/test_py39compat.py rename to tests/compat/test_py39_compat.py index 7e6235e4..549e518a 100644 --- a/tests/test_py39compat.py +++ b/tests/compat/test_py39_compat.py @@ -2,7 +2,7 @@ import pathlib import unittest -from . import fixtures +from .. import fixtures from importlib_metadata import ( distribution, distributions, @@ -14,8 +14,7 @@ class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): def setUp(self): - python_version = sys.version_info[:2] - if python_version < (3, 8) or python_version > (3, 9): + if sys.version_info >= (3, 10): self.skipTest("Tests specific for Python 3.8/3.9") super().setUp() diff --git a/tests/fixtures.py b/tests/fixtures.py index 7daae16a..f8082df0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -9,7 +9,8 @@ import functools import contextlib -from .py39compat import FS_NONASCII +from .compat.py312 import import_helper +from .compat.py39 import os_helper from . import _path from ._path import FilesSpec @@ -84,6 +85,7 @@ def add_sys_path(dir): def setUp(self): super().setUp() self.fixtures.enter_context(self.add_sys_path(self.site_dir)) + self.fixtures.enter_context(import_helper.isolated_modules()) class SiteBuilder(SiteDir): @@ -335,7 +337,9 @@ def record_names(file_defs): class FileBuilder: def unicode_filename(self): - return FS_NONASCII or self.skip("File system does not support non-ascii.") + return os_helper.FS_NONASCII or self.skip( + "File system does not support non-ascii." + ) def DALS(str): diff --git a/tests/py39compat.py b/tests/py39compat.py deleted file mode 100644 index 926dcad9..00000000 --- a/tests/py39compat.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - from test.support.os_helper import FS_NONASCII -except ImportError: - from test.support import FS_NONASCII # noqa diff --git a/tests/test_main.py b/tests/test_main.py index 6e0b1da7..af79e698 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,6 +5,8 @@ import importlib import importlib_metadata import contextlib +from .compat.py39 import os_helper + import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -396,6 +398,7 @@ def test_packages_distributions_all_module_types(self): assert not any(name.endswith('.dist-info') for name in distributions) + @os_helper.skip_unless_symlink def test_packages_distributions_symlinked_top_level(self) -> None: """ Distribution is resolvable from a simple top-level symlink in RECORD.