python (3.12.0)
1 import re
2 import pickle
3 import unittest
4 import warnings
5 import importlib.metadata
6 import contextlib
7 import itertools
8
9 try:
10 import pyfakefs.fake_filesystem_unittest as ffs
11 except ImportError:
12 from .stubs import fake_filesystem_unittest as ffs
13
14 from . import fixtures
15 from ._context import suppress
16 from importlib.metadata import (
17 Distribution,
18 EntryPoint,
19 PackageNotFoundError,
20 _unique,
21 distributions,
22 entry_points,
23 metadata,
24 packages_distributions,
25 version,
26 )
27
28
29 @contextlib.contextmanager
30 def suppress_known_deprecation():
31 with warnings.catch_warnings(record=True) as ctx:
32 warnings.simplefilter('default', category=DeprecationWarning)
33 yield ctx
34
35
36 class ESC[4;38;5;81mBasicTests(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkg, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
37 version_pattern = r'\d+\.\d+(\.\d)?'
38
39 def test_retrieves_version_of_self(self):
40 dist = Distribution.from_name('distinfo-pkg')
41 assert isinstance(dist.version, str)
42 assert re.match(self.version_pattern, dist.version)
43
44 def test_for_name_does_not_exist(self):
45 with self.assertRaises(PackageNotFoundError):
46 Distribution.from_name('does-not-exist')
47
48 def test_package_not_found_mentions_metadata(self):
49 """
50 When a package is not found, that could indicate that the
51 package is not installed or that it is installed without
52 metadata. Ensure the exception mentions metadata to help
53 guide users toward the cause. See #124.
54 """
55 with self.assertRaises(PackageNotFoundError) as ctx:
56 Distribution.from_name('does-not-exist')
57
58 assert "metadata" in str(ctx.exception)
59
60 # expected to fail until ABC is enforced
61 @suppress(AssertionError)
62 @suppress_known_deprecation()
63 def test_abc_enforced(self):
64 with self.assertRaises(TypeError):
65 type('DistributionSubclass', (Distribution,), {})()
66
67 @fixtures.parameterize(
68 dict(name=None),
69 dict(name=''),
70 )
71 def test_invalid_inputs_to_from_name(self, name):
72 with self.assertRaises(ValueError):
73 Distribution.from_name(name)
74
75
76 class ESC[4;38;5;81mImportTests(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkg, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
77 def test_import_nonexistent_module(self):
78 # Ensure that the MetadataPathFinder does not crash an import of a
79 # non-existent module.
80 with self.assertRaises(ImportError):
81 importlib.import_module('does_not_exist')
82
83 def test_resolve(self):
84 ep = entry_points(group='entries')['main']
85 self.assertEqual(ep.load().__name__, "main")
86
87 def test_entrypoint_with_colon_in_name(self):
88 ep = entry_points(group='entries')['ns:sub']
89 self.assertEqual(ep.value, 'mod:main')
90
91 def test_resolve_without_attr(self):
92 ep = EntryPoint(
93 name='ep',
94 value='importlib.metadata',
95 group='grp',
96 )
97 assert ep.load() is importlib.metadata
98
99
100 class ESC[4;38;5;81mNameNormalizationTests(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mSiteDir, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
101 @staticmethod
102 def make_pkg(name):
103 """
104 Create minimal metadata for a dist-info package with
105 the indicated name on the file system.
106 """
107 return {
108 f'{name}.dist-info': {
109 'METADATA': 'VERSION: 1.0\n',
110 },
111 }
112
113 def test_dashes_in_dist_name_found_as_underscores(self):
114 """
115 For a package with a dash in the name, the dist-info metadata
116 uses underscores in the name. Ensure the metadata loads.
117 """
118 fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir)
119 assert version('my-pkg') == '1.0'
120
121 def test_dist_name_found_as_any_case(self):
122 """
123 Ensure the metadata loads when queried with any case.
124 """
125 pkg_name = 'CherryPy'
126 fixtures.build_files(self.make_pkg(pkg_name), self.site_dir)
127 assert version(pkg_name) == '1.0'
128 assert version(pkg_name.lower()) == '1.0'
129 assert version(pkg_name.upper()) == '1.0'
130
131 def test_unique_distributions(self):
132 """
133 Two distributions varying only by non-normalized name on
134 the file system should resolve as the same.
135 """
136 fixtures.build_files(self.make_pkg('abc'), self.site_dir)
137 before = list(_unique(distributions()))
138
139 alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
140 self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
141 fixtures.build_files(self.make_pkg('ABC'), alt_site_dir)
142 after = list(_unique(distributions()))
143
144 assert len(after) == len(before)
145
146
147 class ESC[4;38;5;81mNonASCIITests(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mSiteDir, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
148 @staticmethod
149 def pkg_with_non_ascii_description(site_dir):
150 """
151 Create minimal metadata for a package with non-ASCII in
152 the description.
153 """
154 contents = {
155 'portend.dist-info': {
156 'METADATA': 'Description: pôrˈtend',
157 },
158 }
159 fixtures.build_files(contents, site_dir)
160 return 'portend'
161
162 @staticmethod
163 def pkg_with_non_ascii_description_egg_info(site_dir):
164 """
165 Create minimal metadata for an egg-info package with
166 non-ASCII in the description.
167 """
168 contents = {
169 'portend.dist-info': {
170 'METADATA': """
171 Name: portend
172
173 pôrˈtend""",
174 },
175 }
176 fixtures.build_files(contents, site_dir)
177 return 'portend'
178
179 def test_metadata_loads(self):
180 pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
181 meta = metadata(pkg_name)
182 assert meta['Description'] == 'pôrˈtend'
183
184 def test_metadata_loads_egg_info(self):
185 pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
186 meta = metadata(pkg_name)
187 assert meta['Description'] == 'pôrˈtend'
188
189
190 class ESC[4;38;5;81mDiscoveryTests(
191 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkg,
192 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgPipInstalledNoToplevel,
193 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgPipInstalledNoModules,
194 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgSourcesFallback,
195 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mDistInfoPkg,
196 ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase,
197 ):
198 def test_package_discovery(self):
199 dists = list(distributions())
200 assert all(isinstance(dist, Distribution) for dist in dists)
201 assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists)
202 assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists)
203 assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists)
204 assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists)
205 assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
206
207 def test_invalid_usage(self):
208 with self.assertRaises(ValueError):
209 list(distributions(context='something', name='else'))
210
211
212 class ESC[4;38;5;81mDirectoryTest(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mSiteDir, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
213 def test_egg_info(self):
214 # make an `EGG-INFO` directory that's unrelated
215 self.site_dir.joinpath('EGG-INFO').mkdir()
216 # used to crash with `IsADirectoryError`
217 with self.assertRaises(PackageNotFoundError):
218 version('unknown-package')
219
220 def test_egg(self):
221 egg = self.site_dir.joinpath('foo-3.6.egg')
222 egg.mkdir()
223 with self.add_sys_path(egg):
224 with self.assertRaises(PackageNotFoundError):
225 version('foo')
226
227
228 class ESC[4;38;5;81mMissingSysPath(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
229 site_dir = '/does-not-exist'
230
231 def test_discovery(self):
232 """
233 Discovering distributions should succeed even if
234 there is an invalid path on sys.path.
235 """
236 importlib.metadata.distributions()
237
238
239 class ESC[4;38;5;81mInaccessibleSysPath(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mffsESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
240 site_dir = '/access-denied'
241
242 def setUp(self):
243 super().setUp()
244 self.setUpPyfakefs()
245 self.fs.create_dir(self.site_dir, perm_bits=000)
246
247 def test_discovery(self):
248 """
249 Discovering distributions should succeed even if
250 there is an invalid path on sys.path.
251 """
252 list(importlib.metadata.distributions())
253
254
255 class ESC[4;38;5;81mTestEntryPoints(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
256 def __init__(self, *args):
257 super().__init__(*args)
258 self.ep = importlib.metadata.EntryPoint(
259 name='name', value='value', group='group'
260 )
261
262 def test_entry_point_pickleable(self):
263 revived = pickle.loads(pickle.dumps(self.ep))
264 assert revived == self.ep
265
266 def test_positional_args(self):
267 """
268 Capture legacy (namedtuple) construction, discouraged.
269 """
270 EntryPoint('name', 'value', 'group')
271
272 def test_immutable(self):
273 """EntryPoints should be immutable"""
274 with self.assertRaises(AttributeError):
275 self.ep.name = 'badactor'
276
277 def test_repr(self):
278 assert 'EntryPoint' in repr(self.ep)
279 assert 'name=' in repr(self.ep)
280 assert "'name'" in repr(self.ep)
281
282 def test_hashable(self):
283 """EntryPoints should be hashable"""
284 hash(self.ep)
285
286 def test_module(self):
287 assert self.ep.module == 'value'
288
289 def test_attr(self):
290 assert self.ep.attr is None
291
292 def test_sortable(self):
293 """
294 EntryPoint objects are sortable, but result is undefined.
295 """
296 sorted(
297 [
298 EntryPoint(name='b', value='val', group='group'),
299 EntryPoint(name='a', value='val', group='group'),
300 ]
301 )
302
303
304 class ESC[4;38;5;81mFileSystem(
305 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mSiteDir, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mFileBuilder, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase
306 ):
307 def test_unicode_dir_on_sys_path(self):
308 """
309 Ensure a Unicode subdirectory of a directory on sys.path
310 does not crash.
311 """
312 fixtures.build_files(
313 {self.unicode_filename(): {}},
314 prefix=self.site_dir,
315 )
316 list(distributions())
317
318
319 class ESC[4;38;5;81mPackagesDistributionsPrebuiltTest(ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mZipFixtures, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
320 def test_packages_distributions_example(self):
321 self._fixture_on_path('example-21.12-py3-none-any.whl')
322 assert packages_distributions()['example'] == ['example']
323
324 def test_packages_distributions_example2(self):
325 """
326 Test packages_distributions on a wheel built
327 by trampolim.
328 """
329 self._fixture_on_path('example2-1.0.0-py3-none-any.whl')
330 assert packages_distributions()['example2'] == ['example2']
331
332
333 class ESC[4;38;5;81mPackagesDistributionsTest(
334 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mOnSysPath, ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mSiteDir, ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase
335 ):
336 def test_packages_distributions_neither_toplevel_nor_files(self):
337 """
338 Test a package built without 'top-level.txt' or a file list.
339 """
340 fixtures.build_files(
341 {
342 'trim_example-1.0.0.dist-info': {
343 'METADATA': """
344 Name: trim_example
345 Version: 1.0.0
346 """,
347 }
348 },
349 prefix=self.site_dir,
350 )
351 packages_distributions()
352
353 def test_packages_distributions_all_module_types(self):
354 """
355 Test top-level modules detected on a package without 'top-level.txt'.
356 """
357 suffixes = importlib.machinery.all_suffixes()
358 metadata = dict(
359 METADATA="""
360 Name: all_distributions
361 Version: 1.0.0
362 """,
363 )
364 files = {
365 'all_distributions-1.0.0.dist-info': metadata,
366 }
367 for i, suffix in enumerate(suffixes):
368 files.update(
369 {
370 f'importable-name {i}{suffix}': '',
371 f'in_namespace_{i}': {
372 f'mod{suffix}': '',
373 },
374 f'in_package_{i}': {
375 '__init__.py': '',
376 f'mod{suffix}': '',
377 },
378 }
379 )
380 metadata.update(RECORD=fixtures.build_record(files))
381 fixtures.build_files(files, prefix=self.site_dir)
382
383 distributions = packages_distributions()
384
385 for i in range(len(suffixes)):
386 assert distributions[f'importable-name {i}'] == ['all_distributions']
387 assert distributions[f'in_namespace_{i}'] == ['all_distributions']
388 assert distributions[f'in_package_{i}'] == ['all_distributions']
389
390 assert not any(name.endswith('.dist-info') for name in distributions)
391
392
393 class ESC[4;38;5;81mPackagesDistributionsEggTest(
394 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkg,
395 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgPipInstalledNoToplevel,
396 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgPipInstalledNoModules,
397 ESC[4;38;5;149mfixturesESC[4;38;5;149m.ESC[4;38;5;149mEggInfoPkgSourcesFallback,
398 ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase,
399 ):
400 def test_packages_distributions_on_eggs(self):
401 """
402 Test old-style egg packages with a variation of 'top_level.txt',
403 'SOURCES.txt', and 'installed-files.txt', available.
404 """
405 distributions = packages_distributions()
406
407 def import_names_from_package(package_name):
408 return {
409 import_name
410 for import_name, package_names in distributions.items()
411 if package_name in package_names
412 }
413
414 # egginfo-pkg declares one import ('mod') via top_level.txt
415 assert import_names_from_package('egginfo-pkg') == {'mod'}
416
417 # egg_with_module-pkg has one import ('egg_with_module') inferred from
418 # installed-files.txt (top_level.txt is missing)
419 assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'}
420
421 # egg_with_no_modules-pkg should not be associated with any import names
422 # (top_level.txt is empty, and installed-files.txt has no .py files)
423 assert import_names_from_package('egg_with_no_modules-pkg') == set()
424
425 # sources_fallback-pkg has one import ('sources_fallback') inferred from
426 # SOURCES.txt (top_level.txt and installed-files.txt is missing)
427 assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'}