| import re |
| import pickle |
| import unittest |
| import warnings |
| import importlib.metadata |
| import contextlib |
| |
| try: |
| import pyfakefs.fake_filesystem_unittest as ffs |
| except ImportError: |
| from .stubs import fake_filesystem_unittest as ffs |
| |
| from . import fixtures |
| from ._context import suppress |
| from importlib.metadata import ( |
| Distribution, |
| EntryPoint, |
| PackageNotFoundError, |
| _unique, |
| distributions, |
| entry_points, |
| metadata, |
| packages_distributions, |
| version, |
| ) |
| |
| |
| @contextlib.contextmanager |
| def suppress_known_deprecation(): |
| with warnings.catch_warnings(record=True) as ctx: |
| warnings.simplefilter('default', category=DeprecationWarning) |
| yield ctx |
| |
| |
| class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): |
| version_pattern = r'\d+\.\d+(\.\d)?' |
| |
| def test_retrieves_version_of_self(self): |
| dist = Distribution.from_name('distinfo-pkg') |
| assert isinstance(dist.version, str) |
| assert re.match(self.version_pattern, dist.version) |
| |
| def test_for_name_does_not_exist(self): |
| with self.assertRaises(PackageNotFoundError): |
| Distribution.from_name('does-not-exist') |
| |
| def test_package_not_found_mentions_metadata(self): |
| """ |
| When a package is not found, that could indicate that the |
| package is not installed or that it is installed without |
| metadata. Ensure the exception mentions metadata to help |
| guide users toward the cause. See #124. |
| """ |
| with self.assertRaises(PackageNotFoundError) as ctx: |
| Distribution.from_name('does-not-exist') |
| |
| assert "metadata" in str(ctx.exception) |
| |
| # expected to fail until ABC is enforced |
| @suppress(AssertionError) |
| @suppress_known_deprecation() |
| def test_abc_enforced(self): |
| with self.assertRaises(TypeError): |
| type('DistributionSubclass', (Distribution,), {})() |
| |
| @fixtures.parameterize( |
| dict(name=None), |
| dict(name=''), |
| ) |
| def test_invalid_inputs_to_from_name(self, name): |
| with self.assertRaises(ValueError): |
| Distribution.from_name(name) |
| |
| |
| class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): |
| def test_import_nonexistent_module(self): |
| # Ensure that the MetadataPathFinder does not crash an import of a |
| # non-existent module. |
| with self.assertRaises(ImportError): |
| importlib.import_module('does_not_exist') |
| |
| def test_resolve(self): |
| ep = entry_points(group='entries')['main'] |
| self.assertEqual(ep.load().__name__, "main") |
| |
| def test_entrypoint_with_colon_in_name(self): |
| ep = entry_points(group='entries')['ns:sub'] |
| self.assertEqual(ep.value, 'mod:main') |
| |
| def test_resolve_without_attr(self): |
| ep = EntryPoint( |
| name='ep', |
| value='importlib.metadata', |
| group='grp', |
| ) |
| assert ep.load() is importlib.metadata |
| |
| |
| class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): |
| @staticmethod |
| def make_pkg(name): |
| """ |
| Create minimal metadata for a dist-info package with |
| the indicated name on the file system. |
| """ |
| return { |
| f'{name}.dist-info': { |
| 'METADATA': 'VERSION: 1.0\n', |
| }, |
| } |
| |
| def test_dashes_in_dist_name_found_as_underscores(self): |
| """ |
| For a package with a dash in the name, the dist-info metadata |
| uses underscores in the name. Ensure the metadata loads. |
| """ |
| fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir) |
| assert version('my-pkg') == '1.0' |
| |
| def test_dist_name_found_as_any_case(self): |
| """ |
| Ensure the metadata loads when queried with any case. |
| """ |
| pkg_name = 'CherryPy' |
| fixtures.build_files(self.make_pkg(pkg_name), self.site_dir) |
| assert version(pkg_name) == '1.0' |
| assert version(pkg_name.lower()) == '1.0' |
| assert version(pkg_name.upper()) == '1.0' |
| |
| def test_unique_distributions(self): |
| """ |
| Two distributions varying only by non-normalized name on |
| the file system should resolve as the same. |
| """ |
| fixtures.build_files(self.make_pkg('abc'), self.site_dir) |
| before = list(_unique(distributions())) |
| |
| alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) |
| self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) |
| fixtures.build_files(self.make_pkg('ABC'), alt_site_dir) |
| after = list(_unique(distributions())) |
| |
| assert len(after) == len(before) |
| |
| |
| class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): |
| @staticmethod |
| def pkg_with_non_ascii_description(site_dir): |
| """ |
| Create minimal metadata for a package with non-ASCII in |
| the description. |
| """ |
| contents = { |
| 'portend.dist-info': { |
| 'METADATA': 'Description: pôrˈtend', |
| }, |
| } |
| fixtures.build_files(contents, site_dir) |
| return 'portend' |
| |
| @staticmethod |
| def pkg_with_non_ascii_description_egg_info(site_dir): |
| """ |
| Create minimal metadata for an egg-info package with |
| non-ASCII in the description. |
| """ |
| contents = { |
| 'portend.dist-info': { |
| 'METADATA': """ |
| Name: portend |
| |
| pôrˈtend""", |
| }, |
| } |
| fixtures.build_files(contents, site_dir) |
| return 'portend' |
| |
| def test_metadata_loads(self): |
| pkg_name = self.pkg_with_non_ascii_description(self.site_dir) |
| meta = metadata(pkg_name) |
| assert meta['Description'] == 'pôrˈtend' |
| |
| def test_metadata_loads_egg_info(self): |
| pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir) |
| meta = metadata(pkg_name) |
| assert meta['Description'] == 'pôrˈtend' |
| |
| |
| class DiscoveryTests( |
| fixtures.EggInfoPkg, |
| fixtures.EggInfoPkgPipInstalledNoToplevel, |
| fixtures.EggInfoPkgPipInstalledNoModules, |
| fixtures.EggInfoPkgSourcesFallback, |
| fixtures.DistInfoPkg, |
| unittest.TestCase, |
| ): |
| def test_package_discovery(self): |
| dists = list(distributions()) |
| assert all(isinstance(dist, Distribution) for dist in dists) |
| assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) |
| assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) |
| assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists) |
| assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) |
| assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) |
| |
| def test_invalid_usage(self): |
| with self.assertRaises(ValueError): |
| list(distributions(context='something', name='else')) |
| |
| |
| class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): |
| def test_egg_info(self): |
| # make an `EGG-INFO` directory that's unrelated |
| self.site_dir.joinpath('EGG-INFO').mkdir() |
| # used to crash with `IsADirectoryError` |
| with self.assertRaises(PackageNotFoundError): |
| version('unknown-package') |
| |
| def test_egg(self): |
| egg = self.site_dir.joinpath('foo-3.6.egg') |
| egg.mkdir() |
| with self.add_sys_path(egg): |
| with self.assertRaises(PackageNotFoundError): |
| version('foo') |
| |
| |
| class MissingSysPath(fixtures.OnSysPath, unittest.TestCase): |
| site_dir = '/does-not-exist' |
| |
| def test_discovery(self): |
| """ |
| Discovering distributions should succeed even if |
| there is an invalid path on sys.path. |
| """ |
| importlib.metadata.distributions() |
| |
| |
| class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase): |
| site_dir = '/access-denied' |
| |
| def setUp(self): |
| super().setUp() |
| self.setUpPyfakefs() |
| self.fs.create_dir(self.site_dir, perm_bits=000) |
| |
| def test_discovery(self): |
| """ |
| Discovering distributions should succeed even if |
| there is an invalid path on sys.path. |
| """ |
| list(importlib.metadata.distributions()) |
| |
| |
| class TestEntryPoints(unittest.TestCase): |
| def __init__(self, *args): |
| super().__init__(*args) |
| self.ep = importlib.metadata.EntryPoint( |
| name='name', value='value', group='group' |
| ) |
| |
| def test_entry_point_pickleable(self): |
| revived = pickle.loads(pickle.dumps(self.ep)) |
| assert revived == self.ep |
| |
| def test_positional_args(self): |
| """ |
| Capture legacy (namedtuple) construction, discouraged. |
| """ |
| EntryPoint('name', 'value', 'group') |
| |
| def test_immutable(self): |
| """EntryPoints should be immutable""" |
| with self.assertRaises(AttributeError): |
| self.ep.name = 'badactor' |
| |
| def test_repr(self): |
| assert 'EntryPoint' in repr(self.ep) |
| assert 'name=' in repr(self.ep) |
| assert "'name'" in repr(self.ep) |
| |
| def test_hashable(self): |
| """EntryPoints should be hashable""" |
| hash(self.ep) |
| |
| def test_module(self): |
| assert self.ep.module == 'value' |
| |
| def test_attr(self): |
| assert self.ep.attr is None |
| |
| def test_sortable(self): |
| """ |
| EntryPoint objects are sortable, but result is undefined. |
| """ |
| sorted( |
| [ |
| EntryPoint(name='b', value='val', group='group'), |
| EntryPoint(name='a', value='val', group='group'), |
| ] |
| ) |
| |
| |
| class FileSystem( |
| fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase |
| ): |
| def test_unicode_dir_on_sys_path(self): |
| """ |
| Ensure a Unicode subdirectory of a directory on sys.path |
| does not crash. |
| """ |
| fixtures.build_files( |
| {self.unicode_filename(): {}}, |
| prefix=self.site_dir, |
| ) |
| list(distributions()) |
| |
| |
| class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase): |
| def test_packages_distributions_example(self): |
| self._fixture_on_path('example-21.12-py3-none-any.whl') |
| assert packages_distributions()['example'] == ['example'] |
| |
| def test_packages_distributions_example2(self): |
| """ |
| Test packages_distributions on a wheel built |
| by trampolim. |
| """ |
| self._fixture_on_path('example2-1.0.0-py3-none-any.whl') |
| assert packages_distributions()['example2'] == ['example2'] |
| |
| |
| class PackagesDistributionsTest( |
| fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase |
| ): |
| def test_packages_distributions_neither_toplevel_nor_files(self): |
| """ |
| Test a package built without 'top-level.txt' or a file list. |
| """ |
| fixtures.build_files( |
| { |
| 'trim_example-1.0.0.dist-info': { |
| 'METADATA': """ |
| Name: trim_example |
| Version: 1.0.0 |
| """, |
| } |
| }, |
| prefix=self.site_dir, |
| ) |
| packages_distributions() |
| |
| def test_packages_distributions_all_module_types(self): |
| """ |
| Test top-level modules detected on a package without 'top-level.txt'. |
| """ |
| suffixes = importlib.machinery.all_suffixes() |
| metadata = dict( |
| METADATA=""" |
| Name: all_distributions |
| Version: 1.0.0 |
| """, |
| ) |
| files = { |
| 'all_distributions-1.0.0.dist-info': metadata, |
| } |
| for i, suffix in enumerate(suffixes): |
| files.update( |
| { |
| f'importable-name {i}{suffix}': '', |
| f'in_namespace_{i}': { |
| f'mod{suffix}': '', |
| }, |
| f'in_package_{i}': { |
| '__init__.py': '', |
| f'mod{suffix}': '', |
| }, |
| } |
| ) |
| metadata.update(RECORD=fixtures.build_record(files)) |
| fixtures.build_files(files, prefix=self.site_dir) |
| |
| distributions = packages_distributions() |
| |
| for i in range(len(suffixes)): |
| assert distributions[f'importable-name {i}'] == ['all_distributions'] |
| assert distributions[f'in_namespace_{i}'] == ['all_distributions'] |
| assert distributions[f'in_package_{i}'] == ['all_distributions'] |
| |
| assert not any(name.endswith('.dist-info') for name in distributions) |
| |
| |
| class PackagesDistributionsEggTest( |
| fixtures.EggInfoPkg, |
| fixtures.EggInfoPkgPipInstalledNoToplevel, |
| fixtures.EggInfoPkgPipInstalledNoModules, |
| fixtures.EggInfoPkgSourcesFallback, |
| unittest.TestCase, |
| ): |
| def test_packages_distributions_on_eggs(self): |
| """ |
| Test old-style egg packages with a variation of 'top_level.txt', |
| 'SOURCES.txt', and 'installed-files.txt', available. |
| """ |
| distributions = packages_distributions() |
| |
| def import_names_from_package(package_name): |
| return { |
| import_name |
| for import_name, package_names in distributions.items() |
| if package_name in package_names |
| } |
| |
| # egginfo-pkg declares one import ('mod') via top_level.txt |
| assert import_names_from_package('egginfo-pkg') == {'mod'} |
| |
| # egg_with_module-pkg has one import ('egg_with_module') inferred from |
| # installed-files.txt (top_level.txt is missing) |
| assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} |
| |
| # egg_with_no_modules-pkg should not be associated with any import names |
| # (top_level.txt is empty, and installed-files.txt has no .py files) |
| assert import_names_from_package('egg_with_no_modules-pkg') == set() |
| |
| # sources_fallback-pkg has one import ('sources_fallback') inferred from |
| # SOURCES.txt (top_level.txt and installed-files.txt is missing) |
| assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} |