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.EggInfoPkgPipInstalledNoToplevel, 31 fixtures.EggInfoPkgPipInstalledNoModules, 32 fixtures.EggInfoPkgPipInstalledExternalDataFiles, 33 fixtures.EggInfoPkgSourcesFallback, 34 fixtures.DistInfoPkg, 35 fixtures.DistInfoPkgWithDot, 36 fixtures.EggInfoFile, 37 unittest.TestCase, 38): 39 version_pattern = r'\d+\.\d+(\.\d)?' 40 41 def test_retrieves_version_of_self(self): 42 pkg_version = version('egginfo-pkg') 43 assert isinstance(pkg_version, str) 44 assert re.match(self.version_pattern, pkg_version) 45 46 def test_retrieves_version_of_distinfo_pkg(self): 47 pkg_version = version('distinfo-pkg') 48 assert isinstance(pkg_version, str) 49 assert re.match(self.version_pattern, pkg_version) 50 51 def test_for_name_does_not_exist(self): 52 with self.assertRaises(PackageNotFoundError): 53 distribution('does-not-exist') 54 55 def test_name_normalization(self): 56 names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' 57 for name in names: 58 with self.subTest(name): 59 assert distribution(name).metadata['Name'] == 'pkg.dot' 60 61 def test_prefix_not_matched(self): 62 prefixes = 'p', 'pkg', 'pkg.' 63 for prefix in prefixes: 64 with self.subTest(prefix): 65 with self.assertRaises(PackageNotFoundError): 66 distribution(prefix) 67 68 def test_for_top_level(self): 69 tests = [ 70 ('egginfo-pkg', 'mod'), 71 ('egg_with_no_modules-pkg', ''), 72 ] 73 for pkg_name, expect_content in tests: 74 with self.subTest(pkg_name): 75 self.assertEqual( 76 distribution(pkg_name).read_text('top_level.txt').strip(), 77 expect_content, 78 ) 79 80 def test_read_text(self): 81 tests = [ 82 ('egginfo-pkg', 'mod\n'), 83 ('egg_with_no_modules-pkg', '\n'), 84 ] 85 for pkg_name, expect_content in tests: 86 with self.subTest(pkg_name): 87 top_level = [ 88 path for path in files(pkg_name) if path.name == 'top_level.txt' 89 ][0] 90 self.assertEqual(top_level.read_text(), expect_content) 91 92 def test_entry_points(self): 93 eps = entry_points() 94 assert 'entries' in eps.groups 95 entries = eps.select(group='entries') 96 assert 'main' in entries.names 97 ep = entries['main'] 98 self.assertEqual(ep.value, 'mod:main') 99 self.assertEqual(ep.extras, []) 100 101 def test_entry_points_distribution(self): 102 entries = entry_points(group='entries') 103 for entry in ("main", "ns:sub"): 104 ep = entries[entry] 105 self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg')) 106 self.assertEqual(ep.dist.version, "1.0.0") 107 108 def test_entry_points_unique_packages_normalized(self): 109 """ 110 Entry points should only be exposed for the first package 111 on sys.path with a given name (even when normalized). 112 """ 113 alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) 114 self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) 115 alt_pkg = { 116 "DistInfo_pkg-1.1.0.dist-info": { 117 "METADATA": """ 118 Name: distinfo-pkg 119 Version: 1.1.0 120 """, 121 "entry_points.txt": """ 122 [entries] 123 main = mod:altmain 124 """, 125 }, 126 } 127 fixtures.build_files(alt_pkg, alt_site_dir) 128 entries = entry_points(group='entries') 129 assert not any( 130 ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0' 131 for ep in entries 132 ) 133 # ns:sub doesn't exist in alt_pkg 134 assert 'ns:sub' not in entries.names 135 136 def test_entry_points_missing_name(self): 137 with self.assertRaises(KeyError): 138 entry_points(group='entries')['missing'] 139 140 def test_entry_points_missing_group(self): 141 assert entry_points(group='missing') == () 142 143 def test_entry_points_allows_no_attributes(self): 144 ep = entry_points().select(group='entries', name='main') 145 with self.assertRaises(AttributeError): 146 ep.foo = 4 147 148 def test_metadata_for_this_package(self): 149 md = metadata('egginfo-pkg') 150 assert md['author'] == 'Steven Ma' 151 assert md['LICENSE'] == 'Unknown' 152 assert md['Name'] == 'egginfo-pkg' 153 classifiers = md.get_all('Classifier') 154 assert 'Topic :: Software Development :: Libraries' in classifiers 155 156 def test_missing_key_legacy(self): 157 """ 158 Requesting a missing key will still return None, but warn. 159 """ 160 md = metadata('distinfo-pkg') 161 with suppress_known_deprecation(): 162 assert md['does-not-exist'] is None 163 164 def test_get_key(self): 165 """ 166 Getting a key gets the key. 167 """ 168 md = metadata('egginfo-pkg') 169 assert md.get('Name') == 'egginfo-pkg' 170 171 def test_get_missing_key(self): 172 """ 173 Requesting a missing key will return None. 174 """ 175 md = metadata('distinfo-pkg') 176 assert md.get('does-not-exist') is None 177 178 @staticmethod 179 def _test_files(files): 180 root = files[0].root 181 for file in files: 182 assert file.root == root 183 assert not file.hash or file.hash.value 184 assert not file.hash or file.hash.mode == 'sha256' 185 assert not file.size or file.size >= 0 186 assert file.locate().exists() 187 assert isinstance(file.read_binary(), bytes) 188 if file.name.endswith('.py'): 189 file.read_text() 190 191 def test_file_hash_repr(self): 192 util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] 193 self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>') 194 195 def test_files_dist_info(self): 196 self._test_files(files('distinfo-pkg')) 197 198 def test_files_egg_info(self): 199 self._test_files(files('egginfo-pkg')) 200 self._test_files(files('egg_with_module-pkg')) 201 self._test_files(files('egg_with_no_modules-pkg')) 202 self._test_files(files('sources_fallback-pkg')) 203 204 def test_version_egg_info_file(self): 205 self.assertEqual(version('egginfo-file'), '0.1') 206 207 def test_requires_egg_info_file(self): 208 requirements = requires('egginfo-file') 209 self.assertIsNone(requirements) 210 211 def test_requires_egg_info(self): 212 deps = requires('egginfo-pkg') 213 assert len(deps) == 2 214 assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps) 215 216 def test_requires_egg_info_empty(self): 217 fixtures.build_files( 218 { 219 'requires.txt': '', 220 }, 221 self.site_dir.joinpath('egginfo_pkg.egg-info'), 222 ) 223 deps = requires('egginfo-pkg') 224 assert deps == [] 225 226 def test_requires_dist_info(self): 227 deps = requires('distinfo-pkg') 228 assert len(deps) == 2 229 assert all(deps) 230 assert 'wheel >= 1.0' in deps 231 assert "pytest; extra == 'test'" in deps 232 233 def test_more_complex_deps_requires_text(self): 234 requires = textwrap.dedent( 235 """ 236 dep1 237 dep2 238 239 [:python_version < "3"] 240 dep3 241 242 [extra1] 243 dep4 244 dep6@ git+https://example.com/python/dep.git@v1.0.0 245 246 [extra2:python_version < "3"] 247 dep5 248 """ 249 ) 250 deps = sorted(Distribution._deps_from_requires_text(requires)) 251 expected = [ 252 'dep1', 253 'dep2', 254 'dep3; python_version < "3"', 255 'dep4; extra == "extra1"', 256 'dep5; (python_version < "3") and extra == "extra2"', 257 'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"', 258 ] 259 # It's important that the environment marker expression be 260 # wrapped in parentheses to avoid the following 'and' binding more 261 # tightly than some other part of the environment expression. 262 263 assert deps == expected 264 265 def test_as_json(self): 266 md = metadata('distinfo-pkg').json 267 assert 'name' in md 268 assert md['keywords'] == ['sample', 'package'] 269 desc = md['description'] 270 assert desc.startswith('Once upon a time\nThere was') 271 assert len(md['requires_dist']) == 2 272 273 def test_as_json_egg_info(self): 274 md = metadata('egginfo-pkg').json 275 assert 'name' in md 276 assert md['keywords'] == ['sample', 'package'] 277 desc = md['description'] 278 assert desc.startswith('Once upon a time\nThere was') 279 assert len(md['classifier']) == 2 280 281 def test_as_json_odd_case(self): 282 self.make_uppercase() 283 md = metadata('distinfo-pkg').json 284 assert 'name' in md 285 assert len(md['requires_dist']) == 2 286 assert md['keywords'] == ['SAMPLE', 'PACKAGE'] 287 288 289class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): 290 def test_name_normalization(self): 291 names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' 292 for name in names: 293 with self.subTest(name): 294 assert distribution(name).metadata['Name'] == 'pkg.dot' 295 296 def test_name_normalization_versionless_egg_info(self): 297 names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' 298 for name in names: 299 with self.subTest(name): 300 assert distribution(name).metadata['Name'] == 'pkg.lot' 301 302 303class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): 304 def test_find_distributions_specified_path(self): 305 dists = Distribution.discover(path=[str(self.site_dir)]) 306 assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) 307 308 def test_distribution_at_pathlib(self): 309 """Demonstrate how to load metadata direct from a directory.""" 310 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 311 dist = Distribution.at(dist_info_path) 312 assert dist.version == '1.0.0' 313 314 def test_distribution_at_str(self): 315 dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' 316 dist = Distribution.at(str(dist_info_path)) 317 assert dist.version == '1.0.0' 318 319 320class InvalidateCache(unittest.TestCase): 321 def test_invalidate_cache(self): 322 # No externally observable behavior, but ensures test coverage... 323 importlib.invalidate_caches() 324