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