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') 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 # Entry points should only be exposed for the first package 94 # on sys.path with a given name. 95 alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) 96 self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) 97 alt_pkg = { 98 "distinfo_pkg-1.1.0.dist-info": { 99 "METADATA": """ 100 Name: distinfo-pkg 101 Version: 1.1.0 102 """, 103 "entry_points.txt": """ 104 [entries] 105 main = mod:altmain 106 """, 107 }, 108 } 109 fixtures.build_files(alt_pkg, alt_site_dir) 110 entries = entry_points(group='entries') 111 assert not any( 112 ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0' 113 for ep in entries 114 ) 115 # ns:sub doesn't exist in alt_pkg 116 assert 'ns:sub' not in entries 117 118 def test_entry_points_missing_name(self): 119 with self.assertRaises(KeyError): 120 entry_points(group='entries')['missing'] 121 122 def test_entry_points_missing_group(self): 123 assert entry_points(group='missing') == () 124 125 def test_entry_points_dict_construction(self): 126 # Prior versions of entry_points() returned simple lists and 127 # allowed casting those lists into maps by name using ``dict()``. 128 # Capture this now deprecated use-case. 129 with suppress_known_deprecation() as caught: 130 eps = dict(entry_points(group='entries')) 131 132 assert 'main' in eps 133 assert eps['main'] == entry_points(group='entries')['main'] 134 135 # check warning 136 expected = next(iter(caught)) 137 assert expected.category is DeprecationWarning 138 assert "Construction of dict of EntryPoints is deprecated" in str(expected) 139 140 def test_entry_points_by_index(self): 141 """ 142 Prior versions of Distribution.entry_points would return a 143 tuple that allowed access by index. 144 Capture this now deprecated use-case 145 See python/importlib_metadata#300 and bpo-44246. 146 """ 147 eps = distribution('distinfo-pkg').entry_points 148 with suppress_known_deprecation() as caught: 149 eps[0] 150 151 # check warning 152 expected = next(iter(caught)) 153 assert expected.category is DeprecationWarning 154 assert "Accessing entry points by index is deprecated" in str(expected) 155 156 def test_entry_points_groups_getitem(self): 157 # Prior versions of entry_points() returned a dict. Ensure 158 # that callers using '.__getitem__()' are supported but warned to 159 # migrate. 160 with suppress_known_deprecation(): 161 entry_points()['entries'] == entry_points(group='entries') 162 163 with self.assertRaises(KeyError): 164 entry_points()['missing'] 165 166 def test_entry_points_groups_get(self): 167 # Prior versions of entry_points() returned a dict. Ensure 168 # that callers using '.get()' are supported but warned to 169 # migrate. 170 with suppress_known_deprecation(): 171 entry_points().get('missing', 'default') == 'default' 172 entry_points().get('entries', 'default') == entry_points()['entries'] 173 entry_points().get('missing', ()) == () 174 175 def test_entry_points_allows_no_attributes(self): 176 ep = entry_points().select(group='entries', name='main') 177 with self.assertRaises(AttributeError): 178 ep.foo = 4 179 180 def test_metadata_for_this_package(self): 181 md = metadata('egginfo-pkg') 182 assert md['author'] == 'Steven Ma' 183 assert md['LICENSE'] == 'Unknown' 184 assert md['Name'] == 'egginfo-pkg' 185 classifiers = md.get_all('Classifier') 186 assert 'Topic :: Software Development :: Libraries' in classifiers 187 188 @staticmethod 189 def _test_files(files): 190 root = files[0].root 191 for file in files: 192 assert file.root == root 193 assert not file.hash or file.hash.value 194 assert not file.hash or file.hash.mode == 'sha256' 195 assert not file.size or file.size >= 0 196 assert file.locate().exists() 197 assert isinstance(file.read_binary(), bytes) 198 if file.name.endswith('.py'): 199 file.read_text() 200 201 def test_file_hash_repr(self): 202 assertRegex = self.assertRegex 203 204 util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] 205 assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>') 206 207 def test_files_dist_info(self): 208 self._test_files(files('distinfo-pkg')) 209 210 def test_files_egg_info(self): 211 self._test_files(files('egginfo-pkg')) 212 213 def test_version_egg_info_file(self): 214 self.assertEqual(version('egginfo-file'), '0.1') 215 216 def test_requires_egg_info_file(self): 217 requirements = requires('egginfo-file') 218 self.assertIsNone(requirements) 219 220 def test_requires_egg_info(self): 221 deps = requires('egginfo-pkg') 222 assert len(deps) == 2 223 assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps) 224 225 def test_requires_egg_info_empty(self): 226 fixtures.build_files( 227 { 228 'requires.txt': '', 229 }, 230 self.site_dir.joinpath('egginfo_pkg.egg-info'), 231 ) 232 deps = requires('egginfo-pkg') 233 assert deps == [] 234 235 def test_requires_dist_info(self): 236 deps = requires('distinfo-pkg') 237 assert len(deps) == 2 238 assert all(deps) 239 assert 'wheel >= 1.0' in deps 240 assert "pytest; extra == 'test'" in deps 241 242 def test_more_complex_deps_requires_text(self): 243 requires = textwrap.dedent( 244 """ 245 dep1 246 dep2 247 248 [:python_version < "3"] 249 dep3 250 251 [extra1] 252 dep4 253 dep6@ git+https://example.com/python/dep.git@v1.0.0 254 255 [extra2:python_version < "3"] 256 dep5 257 """ 258 ) 259 deps = sorted(Distribution._deps_from_requires_text(requires)) 260 expected = [ 261 'dep1', 262 'dep2', 263 'dep3; python_version < "3"', 264 'dep4; extra == "extra1"', 265 'dep5; (python_version < "3") and extra == "extra2"', 266 'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"', 267 ] 268 # It's important that the environment marker expression be 269 # wrapped in parentheses to avoid the following 'and' binding more 270 # tightly than some other part of the environment expression. 271 272 assert deps == expected 273 274 def test_as_json(self): 275 md = metadata('distinfo-pkg').json 276 assert 'name' in md 277 assert md['keywords'] == ['sample', 'package'] 278 desc = md['description'] 279 assert desc.startswith('Once upon a time\nThere was') 280 assert len(md['requires_dist']) == 2 281 282 def test_as_json_egg_info(self): 283 md = metadata('egginfo-pkg').json 284 assert 'name' in md 285 assert md['keywords'] == ['sample', 'package'] 286 desc = md['description'] 287 assert desc.startswith('Once upon a time\nThere was') 288 assert len(md['classifier']) == 2 289 290 def test_as_json_odd_case(self): 291 self.make_uppercase() 292 md = metadata('distinfo-pkg').json 293 assert 'name' in md 294 assert len(md['requires_dist']) == 2 295 assert md['keywords'] == ['SAMPLE', 'PACKAGE'] 296 297 298class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): 299 def test_name_normalization(self): 300 names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' 301 for name in names: 302 with self.subTest(name): 303 assert distribution(name).metadata['Name'] == 'pkg.dot' 304 305 def test_name_normalization_versionless_egg_info(self): 306 names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' 307 for name in names: 308 with self.subTest(name): 309 assert distribution(name).metadata['Name'] == 'pkg.lot' 310 311 312class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): 313 def test_find_distributions_specified_path(self): 314 dists = Distribution.discover(path=[str(self.site_dir)]) 315 assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) 316 317 def test_distribution_at_pathlib(self): 318 # Demonstrate how to load metadata direct from a directory. 319 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 320 dist = Distribution.at(dist_info_path) 321 assert dist.version == '1.0.0' 322 323 def test_distribution_at_str(self): 324 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 325 dist = Distribution.at(str(dist_info_path)) 326 assert dist.version == '1.0.0' 327 328 329class InvalidateCache(unittest.TestCase): 330 def test_invalidate_cache(self): 331 # No externally observable behavior, but ensures test coverage... 332 importlib.invalidate_caches() 333