• 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_metadata_for_this_package(self):
176        md = metadata('egginfo-pkg')
177        assert md['author'] == 'Steven Ma'
178        assert md['LICENSE'] == 'Unknown'
179        assert md['Name'] == 'egginfo-pkg'
180        classifiers = md.get_all('Classifier')
181        assert 'Topic :: Software Development :: Libraries' in classifiers
182
183    @staticmethod
184    def _test_files(files):
185        root = files[0].root
186        for file in files:
187            assert file.root == root
188            assert not file.hash or file.hash.value
189            assert not file.hash or file.hash.mode == 'sha256'
190            assert not file.size or file.size >= 0
191            assert file.locate().exists()
192            assert isinstance(file.read_binary(), bytes)
193            if file.name.endswith('.py'):
194                file.read_text()
195
196    def test_file_hash_repr(self):
197        assertRegex = self.assertRegex
198
199        util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
200        assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
201
202    def test_files_dist_info(self):
203        self._test_files(files('distinfo-pkg'))
204
205    def test_files_egg_info(self):
206        self._test_files(files('egginfo-pkg'))
207
208    def test_version_egg_info_file(self):
209        self.assertEqual(version('egginfo-file'), '0.1')
210
211    def test_requires_egg_info_file(self):
212        requirements = requires('egginfo-file')
213        self.assertIsNone(requirements)
214
215    def test_requires_egg_info(self):
216        deps = requires('egginfo-pkg')
217        assert len(deps) == 2
218        assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
219
220    def test_requires_dist_info(self):
221        deps = requires('distinfo-pkg')
222        assert len(deps) == 2
223        assert all(deps)
224        assert 'wheel >= 1.0' in deps
225        assert "pytest; extra == 'test'" in deps
226
227    def test_more_complex_deps_requires_text(self):
228        requires = textwrap.dedent(
229            """
230            dep1
231            dep2
232
233            [:python_version < "3"]
234            dep3
235
236            [extra1]
237            dep4
238            dep6@ git+https://example.com/python/dep.git@v1.0.0
239
240            [extra2:python_version < "3"]
241            dep5
242            """
243        )
244        deps = sorted(Distribution._deps_from_requires_text(requires))
245        expected = [
246            'dep1',
247            'dep2',
248            'dep3; python_version < "3"',
249            'dep4; extra == "extra1"',
250            'dep5; (python_version < "3") and extra == "extra2"',
251            'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"',
252        ]
253        # It's important that the environment marker expression be
254        # wrapped in parentheses to avoid the following 'and' binding more
255        # tightly than some other part of the environment expression.
256
257        assert deps == expected
258
259    def test_as_json(self):
260        md = metadata('distinfo-pkg').json
261        assert 'name' in md
262        assert md['keywords'] == ['sample', 'package']
263        desc = md['description']
264        assert desc.startswith('Once upon a time\nThere was')
265        assert len(md['requires_dist']) == 2
266
267    def test_as_json_egg_info(self):
268        md = metadata('egginfo-pkg').json
269        assert 'name' in md
270        assert md['keywords'] == ['sample', 'package']
271        desc = md['description']
272        assert desc.startswith('Once upon a time\nThere was')
273        assert len(md['classifier']) == 2
274
275    def test_as_json_odd_case(self):
276        self.make_uppercase()
277        md = metadata('distinfo-pkg').json
278        assert 'name' in md
279        assert len(md['requires_dist']) == 2
280        assert md['keywords'] == ['SAMPLE', 'PACKAGE']
281
282
283class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
284    def test_name_normalization(self):
285        names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
286        for name in names:
287            with self.subTest(name):
288                assert distribution(name).metadata['Name'] == 'pkg.dot'
289
290    def test_name_normalization_versionless_egg_info(self):
291        names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
292        for name in names:
293            with self.subTest(name):
294                assert distribution(name).metadata['Name'] == 'pkg.lot'
295
296
297class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
298    def test_find_distributions_specified_path(self):
299        dists = Distribution.discover(path=[str(self.site_dir)])
300        assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
301
302    def test_distribution_at_pathlib(self):
303        # Demonstrate how to load metadata direct from a directory.
304        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
305        dist = Distribution.at(dist_info_path)
306        assert dist.version == '1.0.0'
307
308    def test_distribution_at_str(self):
309        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
310        dist = Distribution.at(str(dist_info_path))
311        assert dist.version == '1.0.0'
312
313
314class InvalidateCache(unittest.TestCase):
315    def test_invalidate_cache(self):
316        # No externally observable behavior, but ensures test coverage...
317        importlib.invalidate_caches()
318