1 import re
2 import textwrap
3 import unittest
4 import warnings
5 import importlib
6 import contextlib
7
8 from . import fixtures
9 from 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
22 def suppress_known_deprecation():
23 with warnings.catch_warnings(record=True) as ctx:
24 warnings.simplefilter('default', category=DeprecationWarning)
25 yield ctx
26
27
28 class ESC[4;38;5;81mAPITests(
29 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkg,
30 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkg,
31 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkgWithDot,
32 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoFile,
33 ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase,
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
304 class ESC[4;38;5;81mLegacyDots(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkgWithDotLegacy, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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
318 class ESC[4;38;5;81mOffSysPathTests(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkgOffPath, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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
335 class ESC[4;38;5;81mInvalidateCache(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
336 def test_invalidate_cache(self):
337 # No externally observable behavior, but ensures test coverage...
338 importlib.invalidate_caches()