• 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', category=DeprecationWarning)
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_normalized(self):
93        """
94        Entry points should only be exposed for the first package
95        on sys.path with a given name (even when normalized).
96        """
97        alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
98        self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
99        alt_pkg = {
100            "DistInfo_pkg-1.1.0.dist-info": {
101                "METADATA": """
102                Name: distinfo-pkg
103                Version: 1.1.0
104                """,
105                "entry_points.txt": """
106                [entries]
107                main = mod:altmain
108            """,
109            },
110        }
111        fixtures.build_files(alt_pkg, alt_site_dir)
112        entries = entry_points(group='entries')
113        assert not any(
114            ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0'
115            for ep in entries
116        )
117        # ns:sub doesn't exist in alt_pkg
118        assert 'ns:sub' not in entries.names
119
120    def test_entry_points_missing_name(self):
121        with self.assertRaises(KeyError):
122            entry_points(group='entries')['missing']
123
124    def test_entry_points_missing_group(self):
125        assert entry_points(group='missing') == ()
126
127    def test_entry_points_dict_construction(self):
128        """
129        Prior versions of entry_points() returned simple lists and
130        allowed casting those lists into maps by name using ``dict()``.
131        Capture this now deprecated use-case.
132        """
133        with suppress_known_deprecation() as caught:
134            eps = dict(entry_points(group='entries'))
135
136        assert 'main' in eps
137        assert eps['main'] == entry_points(group='entries')['main']
138
139        # check warning
140        expected = next(iter(caught))
141        assert expected.category is DeprecationWarning
142        assert "Construction of dict of EntryPoints is deprecated" in str(expected)
143
144    def test_entry_points_by_index(self):
145        """
146        Prior versions of Distribution.entry_points would return a
147        tuple that allowed access by index.
148        Capture this now deprecated use-case
149        See python/importlib_metadata#300 and bpo-44246.
150        """
151        eps = distribution('distinfo-pkg').entry_points
152        with suppress_known_deprecation() as caught:
153            eps[0]
154
155        # check warning
156        expected = next(iter(caught))
157        assert expected.category is DeprecationWarning
158        assert "Accessing entry points by index is deprecated" in str(expected)
159
160    def test_entry_points_groups_getitem(self):
161        """
162        Prior versions of entry_points() returned a dict. Ensure
163        that callers using '.__getitem__()' are supported but warned to
164        migrate.
165        """
166        with suppress_known_deprecation():
167            entry_points()['entries'] == entry_points(group='entries')
168
169            with self.assertRaises(KeyError):
170                entry_points()['missing']
171
172    def test_entry_points_groups_get(self):
173        """
174        Prior versions of entry_points() returned a dict. Ensure
175        that callers using '.get()' are supported but warned to
176        migrate.
177        """
178        with suppress_known_deprecation():
179            entry_points().get('missing', 'default') == 'default'
180            entry_points().get('entries', 'default') == entry_points()['entries']
181            entry_points().get('missing', ()) == ()
182
183    def test_entry_points_allows_no_attributes(self):
184        ep = entry_points().select(group='entries', name='main')
185        with self.assertRaises(AttributeError):
186            ep.foo = 4
187
188    def test_metadata_for_this_package(self):
189        md = metadata('egginfo-pkg')
190        assert md['author'] == 'Steven Ma'
191        assert md['LICENSE'] == 'Unknown'
192        assert md['Name'] == 'egginfo-pkg'
193        classifiers = md.get_all('Classifier')
194        assert 'Topic :: Software Development :: Libraries' in classifiers
195
196    @staticmethod
197    def _test_files(files):
198        root = files[0].root
199        for file in files:
200            assert file.root == root
201            assert not file.hash or file.hash.value
202            assert not file.hash or file.hash.mode == 'sha256'
203            assert not file.size or file.size >= 0
204            assert file.locate().exists()
205            assert isinstance(file.read_binary(), bytes)
206            if file.name.endswith('.py'):
207                file.read_text()
208
209    def test_file_hash_repr(self):
210        util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
211        self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
212
213    def test_files_dist_info(self):
214        self._test_files(files('distinfo-pkg'))
215
216    def test_files_egg_info(self):
217        self._test_files(files('egginfo-pkg'))
218
219    def test_version_egg_info_file(self):
220        self.assertEqual(version('egginfo-file'), '0.1')
221
222    def test_requires_egg_info_file(self):
223        requirements = requires('egginfo-file')
224        self.assertIsNone(requirements)
225
226    def test_requires_egg_info(self):
227        deps = requires('egginfo-pkg')
228        assert len(deps) == 2
229        assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
230
231    def test_requires_egg_info_empty(self):
232        fixtures.build_files(
233            {
234                'requires.txt': '',
235            },
236            self.site_dir.joinpath('egginfo_pkg.egg-info'),
237        )
238        deps = requires('egginfo-pkg')
239        assert deps == []
240
241    def test_requires_dist_info(self):
242        deps = requires('distinfo-pkg')
243        assert len(deps) == 2
244        assert all(deps)
245        assert 'wheel >= 1.0' in deps
246        assert "pytest; extra == 'test'" in deps
247
248    def test_more_complex_deps_requires_text(self):
249        requires = textwrap.dedent(
250            """
251            dep1
252            dep2
253
254            [:python_version < "3"]
255            dep3
256
257            [extra1]
258            dep4
259            dep6@ git+https://example.com/python/dep.git@v1.0.0
260
261            [extra2:python_version < "3"]
262            dep5
263            """
264        )
265        deps = sorted(Distribution._deps_from_requires_text(requires))
266        expected = [
267            'dep1',
268            'dep2',
269            'dep3; python_version < "3"',
270            'dep4; extra == "extra1"',
271            'dep5; (python_version < "3") and extra == "extra2"',
272            'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"',
273        ]
274        # It's important that the environment marker expression be
275        # wrapped in parentheses to avoid the following 'and' binding more
276        # tightly than some other part of the environment expression.
277
278        assert deps == expected
279
280    def test_as_json(self):
281        md = metadata('distinfo-pkg').json
282        assert 'name' in md
283        assert md['keywords'] == ['sample', 'package']
284        desc = md['description']
285        assert desc.startswith('Once upon a time\nThere was')
286        assert len(md['requires_dist']) == 2
287
288    def test_as_json_egg_info(self):
289        md = metadata('egginfo-pkg').json
290        assert 'name' in md
291        assert md['keywords'] == ['sample', 'package']
292        desc = md['description']
293        assert desc.startswith('Once upon a time\nThere was')
294        assert len(md['classifier']) == 2
295
296    def test_as_json_odd_case(self):
297        self.make_uppercase()
298        md = metadata('distinfo-pkg').json
299        assert 'name' in md
300        assert len(md['requires_dist']) == 2
301        assert md['keywords'] == ['SAMPLE', 'PACKAGE']
302
303
304class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
305    def test_name_normalization(self):
306        names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
307        for name in names:
308            with self.subTest(name):
309                assert distribution(name).metadata['Name'] == 'pkg.dot'
310
311    def test_name_normalization_versionless_egg_info(self):
312        names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
313        for name in names:
314            with self.subTest(name):
315                assert distribution(name).metadata['Name'] == 'pkg.lot'
316
317
318class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
319    def test_find_distributions_specified_path(self):
320        dists = Distribution.discover(path=[str(self.site_dir)])
321        assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
322
323    def test_distribution_at_pathlib(self):
324        """Demonstrate how to load metadata direct from a directory."""
325        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
326        dist = Distribution.at(dist_info_path)
327        assert dist.version == '1.0.0'
328
329    def test_distribution_at_str(self):
330        dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
331        dist = Distribution.at(str(dist_info_path))
332        assert dist.version == '1.0.0'
333
334
335class InvalidateCache(unittest.TestCase):
336    def test_invalidate_cache(self):
337        # No externally observable behavior, but ensures test coverage...
338        importlib.invalidate_caches()
339