• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import re
2import json
3import pickle
4import textwrap
5import unittest
6import warnings
7import importlib.metadata
8
9try:
10    import pyfakefs.fake_filesystem_unittest as ffs
11except ImportError:
12    from .stubs import fake_filesystem_unittest as ffs
13
14from . import fixtures
15from importlib.metadata import (
16    Distribution,
17    EntryPoint,
18    PackageNotFoundError,
19    distributions,
20    entry_points,
21    metadata,
22    version,
23)
24
25
26class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
27    version_pattern = r'\d+\.\d+(\.\d)?'
28
29    def test_retrieves_version_of_self(self):
30        dist = Distribution.from_name('distinfo-pkg')
31        assert isinstance(dist.version, str)
32        assert re.match(self.version_pattern, dist.version)
33
34    def test_for_name_does_not_exist(self):
35        with self.assertRaises(PackageNotFoundError):
36            Distribution.from_name('does-not-exist')
37
38    def test_package_not_found_mentions_metadata(self):
39        # When a package is not found, that could indicate that the
40        # packgae is not installed or that it is installed without
41        # metadata. Ensure the exception mentions metadata to help
42        # guide users toward the cause. See #124.
43        with self.assertRaises(PackageNotFoundError) as ctx:
44            Distribution.from_name('does-not-exist')
45
46        assert "metadata" in str(ctx.exception)
47
48    def test_new_style_classes(self):
49        self.assertIsInstance(Distribution, type)
50
51
52class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
53    def test_import_nonexistent_module(self):
54        # Ensure that the MetadataPathFinder does not crash an import of a
55        # non-existent module.
56        with self.assertRaises(ImportError):
57            importlib.import_module('does_not_exist')
58
59    def test_resolve(self):
60        ep = entry_points(group='entries')['main']
61        self.assertEqual(ep.load().__name__, "main")
62
63    def test_entrypoint_with_colon_in_name(self):
64        ep = entry_points(group='entries')['ns:sub']
65        self.assertEqual(ep.value, 'mod:main')
66
67    def test_resolve_without_attr(self):
68        ep = EntryPoint(
69            name='ep',
70            value='importlib.metadata',
71            group='grp',
72        )
73        assert ep.load() is importlib.metadata
74
75
76class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
77    @staticmethod
78    def pkg_with_dashes(site_dir):
79        """
80        Create minimal metadata for a package with dashes
81        in the name (and thus underscores in the filename).
82        """
83        metadata_dir = site_dir / 'my_pkg.dist-info'
84        metadata_dir.mkdir()
85        metadata = metadata_dir / 'METADATA'
86        with metadata.open('w', encoding='utf-8') as strm:
87            strm.write('Version: 1.0\n')
88        return 'my-pkg'
89
90    def test_dashes_in_dist_name_found_as_underscores(self):
91        # For a package with a dash in the name, the dist-info metadata
92        # uses underscores in the name. Ensure the metadata loads.
93        pkg_name = self.pkg_with_dashes(self.site_dir)
94        assert version(pkg_name) == '1.0'
95
96    @staticmethod
97    def pkg_with_mixed_case(site_dir):
98        """
99        Create minimal metadata for a package with mixed case
100        in the name.
101        """
102        metadata_dir = site_dir / 'CherryPy.dist-info'
103        metadata_dir.mkdir()
104        metadata = metadata_dir / 'METADATA'
105        with metadata.open('w', encoding='utf-8') as strm:
106            strm.write('Version: 1.0\n')
107        return 'CherryPy'
108
109    def test_dist_name_found_as_any_case(self):
110        # Ensure the metadata loads when queried with any case.
111        pkg_name = self.pkg_with_mixed_case(self.site_dir)
112        assert version(pkg_name) == '1.0'
113        assert version(pkg_name.lower()) == '1.0'
114        assert version(pkg_name.upper()) == '1.0'
115
116
117class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
118    @staticmethod
119    def pkg_with_non_ascii_description(site_dir):
120        """
121        Create minimal metadata for a package with non-ASCII in
122        the description.
123        """
124        metadata_dir = site_dir / 'portend.dist-info'
125        metadata_dir.mkdir()
126        metadata = metadata_dir / 'METADATA'
127        with metadata.open('w', encoding='utf-8') as fp:
128            fp.write('Description: pôrˈtend')
129        return 'portend'
130
131    @staticmethod
132    def pkg_with_non_ascii_description_egg_info(site_dir):
133        """
134        Create minimal metadata for an egg-info package with
135        non-ASCII in the description.
136        """
137        metadata_dir = site_dir / 'portend.dist-info'
138        metadata_dir.mkdir()
139        metadata = metadata_dir / 'METADATA'
140        with metadata.open('w', encoding='utf-8') as fp:
141            fp.write(
142                textwrap.dedent(
143                    """
144                Name: portend
145
146                pôrˈtend
147                """
148                ).strip()
149            )
150        return 'portend'
151
152    def test_metadata_loads(self):
153        pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
154        meta = metadata(pkg_name)
155        assert meta['Description'] == 'pôrˈtend'
156
157    def test_metadata_loads_egg_info(self):
158        pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
159        meta = metadata(pkg_name)
160        assert meta['Description'] == 'pôrˈtend'
161
162
163class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase):
164    def test_package_discovery(self):
165        dists = list(distributions())
166        assert all(isinstance(dist, Distribution) for dist in dists)
167        assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists)
168        assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
169
170    def test_invalid_usage(self):
171        with self.assertRaises(ValueError):
172            list(distributions(context='something', name='else'))
173
174
175class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
176    def test_egg_info(self):
177        # make an `EGG-INFO` directory that's unrelated
178        self.site_dir.joinpath('EGG-INFO').mkdir()
179        # used to crash with `IsADirectoryError`
180        with self.assertRaises(PackageNotFoundError):
181            version('unknown-package')
182
183    def test_egg(self):
184        egg = self.site_dir.joinpath('foo-3.6.egg')
185        egg.mkdir()
186        with self.add_sys_path(egg):
187            with self.assertRaises(PackageNotFoundError):
188                version('foo')
189
190
191class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
192    site_dir = '/does-not-exist'
193
194    def test_discovery(self):
195        """
196        Discovering distributions should succeed even if
197        there is an invalid path on sys.path.
198        """
199        importlib.metadata.distributions()
200
201
202class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
203    site_dir = '/access-denied'
204
205    def setUp(self):
206        super(InaccessibleSysPath, self).setUp()
207        self.setUpPyfakefs()
208        self.fs.create_dir(self.site_dir, perm_bits=000)
209
210    def test_discovery(self):
211        """
212        Discovering distributions should succeed even if
213        there is an invalid path on sys.path.
214        """
215        list(importlib.metadata.distributions())
216
217
218class TestEntryPoints(unittest.TestCase):
219    def __init__(self, *args):
220        super(TestEntryPoints, self).__init__(*args)
221        self.ep = importlib.metadata.EntryPoint('name', 'value', 'group')
222
223    def test_entry_point_pickleable(self):
224        revived = pickle.loads(pickle.dumps(self.ep))
225        assert revived == self.ep
226
227    def test_immutable(self):
228        """EntryPoints should be immutable"""
229        with self.assertRaises(AttributeError):
230            self.ep.name = 'badactor'
231
232    def test_repr(self):
233        assert 'EntryPoint' in repr(self.ep)
234        assert 'name=' in repr(self.ep)
235        assert "'name'" in repr(self.ep)
236
237    def test_hashable(self):
238        # EntryPoints should be hashable.
239        hash(self.ep)
240
241    def test_json_dump(self):
242        # json should not expect to be able to dump an EntryPoint.
243        with self.assertRaises(Exception):
244            with warnings.catch_warnings(record=True):
245                json.dumps(self.ep)
246
247    def test_module(self):
248        assert self.ep.module == 'value'
249
250    def test_attr(self):
251        assert self.ep.attr is None
252
253    def test_sortable(self):
254        # EntryPoint objects are sortable, but result is undefined.
255        sorted(
256            [
257                EntryPoint('b', 'val', 'group'),
258                EntryPoint('a', 'val', 'group'),
259            ]
260        )
261
262
263class FileSystem(
264    fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase
265):
266    def test_unicode_dir_on_sys_path(self):
267        # Ensure a Unicode subdirectory of a directory on sys.path
268        # does not crash.
269        fixtures.build_files(
270            {self.unicode_filename(): {}},
271            prefix=self.site_dir,
272        )
273        list(distributions())
274