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