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_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 304class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): 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 318class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): 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 335class InvalidateCache(unittest.TestCase): 336 def test_invalidate_cache(self): 337 # No externally observable behavior, but ensures test coverage... 338 importlib.invalidate_caches() 339