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