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(self): 93 """ 94 Entry points should only be exposed for the first package 95 on sys.path with a given name. 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_metadata_for_this_package(self): 184 md = metadata('egginfo-pkg') 185 assert md['author'] == 'Steven Ma' 186 assert md['LICENSE'] == 'Unknown' 187 assert md['Name'] == 'egginfo-pkg' 188 classifiers = md.get_all('Classifier') 189 assert 'Topic :: Software Development :: Libraries' in classifiers 190 191 def test_importlib_metadata_version(self): 192 resolved = version('importlib-metadata') 193 assert re.match(self.version_pattern, resolved) 194 195 @staticmethod 196 def _test_files(files): 197 root = files[0].root 198 for file in files: 199 assert file.root == root 200 assert not file.hash or file.hash.value 201 assert not file.hash or file.hash.mode == 'sha256' 202 assert not file.size or file.size >= 0 203 assert file.locate().exists() 204 assert isinstance(file.read_binary(), bytes) 205 if file.name.endswith('.py'): 206 file.read_text() 207 208 def test_file_hash_repr(self): 209 util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] 210 self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>') 211 212 def test_files_dist_info(self): 213 self._test_files(files('distinfo-pkg')) 214 215 def test_files_egg_info(self): 216 self._test_files(files('egginfo-pkg')) 217 218 def test_version_egg_info_file(self): 219 self.assertEqual(version('egginfo-file'), '0.1') 220 221 def test_requires_egg_info_file(self): 222 requirements = requires('egginfo-file') 223 self.assertIsNone(requirements) 224 225 def test_requires_egg_info(self): 226 deps = requires('egginfo-pkg') 227 assert len(deps) == 2 228 assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps) 229 230 def test_requires_dist_info(self): 231 deps = requires('distinfo-pkg') 232 assert len(deps) == 2 233 assert all(deps) 234 assert 'wheel >= 1.0' in deps 235 assert "pytest; extra == 'test'" in deps 236 237 def test_more_complex_deps_requires_text(self): 238 requires = textwrap.dedent( 239 """ 240 dep1 241 dep2 242 243 [:python_version < "3"] 244 dep3 245 246 [extra1] 247 dep4 248 249 [extra2:python_version < "3"] 250 dep5 251 """ 252 ) 253 deps = sorted(Distribution._deps_from_requires_text(requires)) 254 expected = [ 255 'dep1', 256 'dep2', 257 'dep3; python_version < "3"', 258 'dep4; extra == "extra1"', 259 'dep5; (python_version < "3") and extra == "extra2"', 260 ] 261 # It's important that the environment marker expression be 262 # wrapped in parentheses to avoid the following 'and' binding more 263 # tightly than some other part of the environment expression. 264 265 assert deps == expected 266 267 def test_as_json(self): 268 md = metadata('distinfo-pkg').json 269 assert 'name' in md 270 assert md['keywords'] == ['sample', 'package'] 271 desc = md['description'] 272 assert desc.startswith('Once upon a time\nThere was') 273 assert len(md['requires_dist']) == 2 274 275 def test_as_json_egg_info(self): 276 md = metadata('egginfo-pkg').json 277 assert 'name' in md 278 assert md['keywords'] == ['sample', 'package'] 279 desc = md['description'] 280 assert desc.startswith('Once upon a time\nThere was') 281 assert len(md['classifier']) == 2 282 283 def test_as_json_odd_case(self): 284 self.make_uppercase() 285 md = metadata('distinfo-pkg').json 286 assert 'name' in md 287 assert len(md['requires_dist']) == 2 288 assert md['keywords'] == ['SAMPLE', 'PACKAGE'] 289 290 291class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): 292 def test_name_normalization(self): 293 names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' 294 for name in names: 295 with self.subTest(name): 296 assert distribution(name).metadata['Name'] == 'pkg.dot' 297 298 def test_name_normalization_versionless_egg_info(self): 299 names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' 300 for name in names: 301 with self.subTest(name): 302 assert distribution(name).metadata['Name'] == 'pkg.lot' 303 304 305class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): 306 def test_find_distributions_specified_path(self): 307 dists = Distribution.discover(path=[str(self.site_dir)]) 308 assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) 309 310 def test_distribution_at_pathlib(self): 311 """Demonstrate how to load metadata direct from a directory.""" 312 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 313 dist = Distribution.at(dist_info_path) 314 assert dist.version == '1.0.0' 315 316 def test_distribution_at_str(self): 317 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 318 dist = Distribution.at(str(dist_info_path)) 319 assert dist.version == '1.0.0' 320 321 322class InvalidateCache(unittest.TestCase): 323 def test_invalidate_cache(self): 324 # No externally observable behavior, but ensures test coverage... 325 importlib.invalidate_caches() 326