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