• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import re
2import textwrap
3import unittest
4import warnings
5import importlib
6import contextlib
7
8from . import fixtures
9from importlib.metadata import (
10    Distribution,
11    PackageNotFoundError,
12    distribution,
13    entry_points,
14    files,
15    metadata,
16    requires,
17    version,
18)
19
20
21@contextlib.contextmanager
22def suppress_known_deprecation():
23    with warnings.catch_warnings(record=True) as ctx:
24        warnings.simplefilter('default')
25        yield ctx
26
27
28class APITests(
29    fixtures.EggInfoPkg,
30    fixtures.DistInfoPkg,
31    fixtures.DistInfoPkgWithDot,
32    fixtures.EggInfoFile,
33    unittest.TestCase,
34):
35
36    version_pattern = r'\d+\.\d+(\.\d)?'
37
38    def test_retrieves_version_of_self(self):
39        pkg_version = version('egginfo-pkg')
40        assert isinstance(pkg_version, str)
41        assert re.match(self.version_pattern, pkg_version)
42
43    def test_retrieves_version_of_distinfo_pkg(self):
44        pkg_version = version('distinfo-pkg')
45        assert isinstance(pkg_version, str)
46        assert re.match(self.version_pattern, pkg_version)
47
48    def test_for_name_does_not_exist(self):
49        with self.assertRaises(PackageNotFoundError):
50            distribution('does-not-exist')
51
52    def test_name_normalization(self):
53        names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
54        for name in names:
55            with self.subTest(name):
56                assert distribution(name).metadata['Name'] == 'pkg.dot'
57
58    def test_prefix_not_matched(self):
59        prefixes = 'p', 'pkg', 'pkg.'
60        for prefix in prefixes:
61            with self.subTest(prefix):
62                with self.assertRaises(PackageNotFoundError):
63                    distribution(prefix)
64
65    def test_for_top_level(self):
66        self.assertEqual(
67            distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod'
68        )
69
70    def test_read_text(self):
71        top_level = [
72            path for path in files('egginfo-pkg') if path.name == 'top_level.txt'
73        ][0]
74        self.assertEqual(top_level.read_text(), 'mod\n')
75
76    def test_entry_points(self):
77        eps = entry_points()
78        assert 'entries' in eps.groups
79        entries = eps.select(group='entries')
80        assert 'main' in entries.names
81        ep = entries['main']
82        self.assertEqual(ep.value, 'mod:main')
83        self.assertEqual(ep.extras, [])
84
85    def test_entry_points_distribution(self):
86        entries = entry_points(group='entries')
87        for entry in ("main", "ns:sub"):
88            ep = entries[entry]
89            self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
90            self.assertEqual(ep.dist.version, "1.0.0")
91
92    def test_entry_points_unique_packages(self):
93        # Entry points should only be exposed for the first package
94        # on sys.path with a given name.
95        alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
96        self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
97        alt_pkg = {
98            "distinfo_pkg-1.1.0.dist-info": {
99                "METADATA": """
100                Name: distinfo-pkg
101                Version: 1.1.0
102                """,
103                "entry_points.txt": """
104                [entries]
105                main = mod:altmain
106            """,
107            },
108        }
109        fixtures.build_files(alt_pkg, alt_site_dir)
110        entries = entry_points(group='entries')
111        assert not any(
112            ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0'
113            for ep in entries
114        )
115        # ns:sub doesn't exist in alt_pkg
116        assert 'ns:sub' not in entries
117
118    def test_entry_points_missing_name(self):
119        with self.assertRaises(KeyError):
120            entry_points(group='entries')['missing']
121
122    def test_entry_points_missing_group(self):
123        assert entry_points(group='missing') == ()
124
125    def test_entry_points_dict_construction(self):
126        # Prior versions of entry_points() returned simple lists and
127        # allowed casting those lists into maps by name using ``dict()``.
128        # Capture this now deprecated use-case.
129        with suppress_known_deprecation() as caught:
130            eps = dict(entry_points(group='entries'))
131
132        assert 'main' in eps
133        assert eps['main'] == entry_points(group='entries')['main']
134
135        # check warning
136        expected = next(iter(caught))
137        assert expected.category is DeprecationWarning
138        assert "Construction of dict of EntryPoints is deprecated" in str(expected)
139
140    def test_entry_points_by_index(self):
141        """
142        Prior versions of Distribution.entry_points would return a
143        tuple that allowed access by index.
144        Capture this now deprecated use-case
145        See python/importlib_metadata#300 and bpo-44246.
146        """
147        eps = distribution('distinfo-pkg').entry_points
148        with suppress_known_deprecation() as caught:
149            eps[0]
150
151        # check warning
152        expected = next(iter(caught))
153        assert expected.category is DeprecationWarning
154        assert "Accessing entry points by index is deprecated" in str(expected)
155
156    def test_entry_points_groups_getitem(self):
157        # Prior versions of entry_points() returned a dict. Ensure
158        # that callers using '.__getitem__()' are supported but warned to
159        # migrate.
160        with suppress_known_deprecation():
161            entry_points()['entries'] == entry_points(group='entries')
162
163            with self.assertRaises(KeyError):
164                entry_points()['missing']
165
166    def test_entry_points_groups_get(self):
167        # Prior versions of entry_points() returned a dict. Ensure
168        # that callers using '.get()' are supported but warned to
169        # migrate.
170        with suppress_known_deprecation():
171            entry_points().get('missing', 'default') == 'default'
172            entry_points().get('entries', 'default') == entry_points()['entries']
173            entry_points().get('missing', ()) == ()
174
175    def test_entry_points_allows_no_attributes(self):
176        ep = entry_points().select(group='entries', name='main')
177        with self.assertRaises(AttributeError):
178            ep.foo = 4
179
180    def test_metadata_for_this_package(self):
181        md = metadata('egginfo-pkg')
182        assert md['author'] == 'Steven Ma'
183        assert md['LICENSE'] == 'Unknown'
184        assert md['Name'] == 'egginfo-pkg'
185        classifiers = md.get_all('Classifier')
186        assert 'Topic :: Software Development :: Libraries' in classifiers
187
188    @staticmethod
189    def _test_files(files):
190        root = files[0].root
191        for file in files:
192            assert file.root == root
193            assert not file.hash or file.hash.value
194            assert not file.hash or file.hash.mode == 'sha256'
195            assert not file.size or file.size >= 0
196            assert file.locate().exists()
197            assert isinstance(file.read_binary(), bytes)
198            if file.name.endswith('.py'):
199                file.read_text()
200
201    def test_file_hash_repr(self):
202        assertRegex = self.assertRegex
203
204        util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
205        assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
206
207    def test_files_dist_info(self):
208        self._test_files(files('distinfo-pkg'))
209
210    def test_files_egg_info(self):
211        self._test_files(files('egginfo-pkg'))
212
213    def test_version_egg_info_file(self):
214        self.assertEqual(version('egginfo-file'), '0.1')
215
216    def test_requires_egg_info_file(self):
217        requirements = requires('egginfo-file')
218        self.assertIsNone(requirements)
219
220    def test_requires_egg_info(self):
221        deps = requires('egginfo-pkg')
222        assert len(deps) == 2
223        assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
224
225    def test_requires_egg_info_empty(self):
226        fixtures.build_files(
227            {
228                'requires.txt': '',
229            },
230            self.site_dir.joinpath('egginfo_pkg.egg-info'),
231        )
232        deps = requires('egginfo-pkg')
233        assert deps == []
234
235    def test_requires_dist_info(self):
236        deps = requires('distinfo-pkg')
237        assert len(deps) == 2
238        assert all(deps)
239        assert 'wheel >= 1.0' in deps
240        assert "pytest; extra == 'test'" in deps
241
242    def test_more_complex_deps_requires_text(self):
243        requires = textwrap.dedent(
244            """
245            dep1
246            dep2
247
248            [:python_version < "3"]
249            dep3
250
251            [extra1]
252            dep4
253            dep6@ git+https://example.com/python/dep.git@v1.0.0
254
255            [extra2:python_version < "3"]
256            dep5
257            """
258        )
259        deps = sorted(Distribution._deps_from_requires_text(requires))
260        expected = [
261            'dep1',
262            'dep2',
263            'dep3; python_version < "3"',
264            'dep4; extra == "extra1"',
265            'dep5; (python_version < "3") and extra == "extra2"',
266            'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"',
267        ]
268        # It's important that the environment marker expression be
269        # wrapped in parentheses to avoid the following 'and' binding more
270        # tightly than some other part of the environment expression.
271
272        assert deps == expected
273
274    def test_as_json(self):
275        md = metadata('distinfo-pkg').json
276        assert 'name' in md
277        assert md['keywords'] == ['sample', 'package']
278        desc = md['description']
279        assert desc.startswith('Once upon a time\nThere was')
280        assert len(md['requires_dist']) == 2
281
282    def test_as_json_egg_info(self):
283        md = metadata('egginfo-pkg').json
284        assert 'name' in md
285        assert md['keywords'] == ['sample', 'package']
286        desc = md['description']
287        assert desc.startswith('Once upon a time\nThere was')
288        assert len(md['classifier']) == 2
289
290    def test_as_json_odd_case(self):
291        self.make_uppercase()
292        md = metadata('distinfo-pkg').json
293        assert 'name' in md
294        assert len(md['requires_dist']) == 2
295        assert md['keywords'] == ['SAMPLE', 'PACKAGE']
296
297
298class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
299    def test_name_normalization(self):
300        names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
301        for name in names:
302            with self.subTest(name):
303                assert distribution(name).metadata['Name'] == 'pkg.dot'
304
305    def test_name_normalization_versionless_egg_info(self):
306        names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
307        for name in names:
308            with self.subTest(name):
309                assert distribution(name).metadata['Name'] == 'pkg.lot'
310
311
312class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
313    def test_find_distributions_specified_path(self):
314        dists = Distribution.discover(path=[str(self.site_dir)])
315        assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
316
317    def test_distribution_at_pathlib(self):
318        # Demonstrate how to load metadata direct from a directory.
319        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
320        dist = Distribution.at(dist_info_path)
321        assert dist.version == '1.0.0'
322
323    def test_distribution_at_str(self):
324        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
325        dist = Distribution.at(str(dist_info_path))
326        assert dist.version == '1.0.0'
327
328
329class InvalidateCache(unittest.TestCase):
330    def test_invalidate_cache(self):
331        # No externally observable behavior, but ensures test coverage...
332        importlib.invalidate_caches()
333