1import os 2import sys 3import copy 4import json 5import shutil 6import pathlib 7import tempfile 8import textwrap 9import functools 10import contextlib 11 12from test.support import import_helper 13from test.support import os_helper 14from test.support import requires_zlib 15 16from . import _path 17from ._path import FilesSpec 18 19 20try: 21 from importlib import resources # type: ignore 22 23 getattr(resources, 'files') 24 getattr(resources, 'as_file') 25except (ImportError, AttributeError): 26 import importlib_resources as resources # type: ignore 27 28 29@contextlib.contextmanager 30def tempdir(): 31 tmpdir = tempfile.mkdtemp() 32 try: 33 yield pathlib.Path(tmpdir) 34 finally: 35 shutil.rmtree(tmpdir) 36 37 38@contextlib.contextmanager 39def save_cwd(): 40 orig = os.getcwd() 41 try: 42 yield 43 finally: 44 os.chdir(orig) 45 46 47@contextlib.contextmanager 48def tempdir_as_cwd(): 49 with tempdir() as tmp: 50 with save_cwd(): 51 os.chdir(str(tmp)) 52 yield tmp 53 54 55@contextlib.contextmanager 56def install_finder(finder): 57 sys.meta_path.append(finder) 58 try: 59 yield 60 finally: 61 sys.meta_path.remove(finder) 62 63 64class Fixtures: 65 def setUp(self): 66 self.fixtures = contextlib.ExitStack() 67 self.addCleanup(self.fixtures.close) 68 69 70class SiteDir(Fixtures): 71 def setUp(self): 72 super().setUp() 73 self.site_dir = self.fixtures.enter_context(tempdir()) 74 75 76class OnSysPath(Fixtures): 77 @staticmethod 78 @contextlib.contextmanager 79 def add_sys_path(dir): 80 sys.path[:0] = [str(dir)] 81 try: 82 yield 83 finally: 84 sys.path.remove(str(dir)) 85 86 def setUp(self): 87 super().setUp() 88 self.fixtures.enter_context(self.add_sys_path(self.site_dir)) 89 self.fixtures.enter_context(import_helper.isolated_modules()) 90 91 92class SiteBuilder(SiteDir): 93 def setUp(self): 94 super().setUp() 95 for cls in self.__class__.mro(): 96 with contextlib.suppress(AttributeError): 97 build_files(cls.files, prefix=self.site_dir) 98 99 100class DistInfoPkg(OnSysPath, SiteBuilder): 101 files: FilesSpec = { 102 "distinfo_pkg-1.0.0.dist-info": { 103 "METADATA": """ 104 Name: distinfo-pkg 105 Author: Steven Ma 106 Version: 1.0.0 107 Requires-Dist: wheel >= 1.0 108 Requires-Dist: pytest; extra == 'test' 109 Keywords: sample package 110 111 Once upon a time 112 There was a distinfo pkg 113 """, 114 "RECORD": "mod.py,sha256=abc,20\n", 115 "entry_points.txt": """ 116 [entries] 117 main = mod:main 118 ns:sub = mod:main 119 """, 120 }, 121 "mod.py": """ 122 def main(): 123 print("hello world") 124 """, 125 } 126 127 def make_uppercase(self): 128 """ 129 Rewrite metadata with everything uppercase. 130 """ 131 shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info") 132 files = copy.deepcopy(DistInfoPkg.files) 133 info = files["distinfo_pkg-1.0.0.dist-info"] 134 info["METADATA"] = info["METADATA"].upper() 135 build_files(files, self.site_dir) 136 137 138class DistInfoPkgEditable(DistInfoPkg): 139 """ 140 Package with a PEP 660 direct_url.json. 141 """ 142 143 some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' 144 files: FilesSpec = { 145 'distinfo_pkg-1.0.0.dist-info': { 146 'direct_url.json': json.dumps({ 147 "archive_info": { 148 "hash": f"sha256={some_hash}", 149 "hashes": {"sha256": f"{some_hash}"}, 150 }, 151 "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", 152 }) 153 }, 154 } 155 156 157class DistInfoPkgWithDot(OnSysPath, SiteBuilder): 158 files: FilesSpec = { 159 "pkg_dot-1.0.0.dist-info": { 160 "METADATA": """ 161 Name: pkg.dot 162 Version: 1.0.0 163 """, 164 }, 165 } 166 167 168class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): 169 files: FilesSpec = { 170 "pkg.dot-1.0.0.dist-info": { 171 "METADATA": """ 172 Name: pkg.dot 173 Version: 1.0.0 174 """, 175 }, 176 "pkg.lot.egg-info": { 177 "METADATA": """ 178 Name: pkg.lot 179 Version: 1.0.0 180 """, 181 }, 182 } 183 184 185class DistInfoPkgOffPath(SiteBuilder): 186 files = DistInfoPkg.files 187 188 189class EggInfoPkg(OnSysPath, SiteBuilder): 190 files: FilesSpec = { 191 "egginfo_pkg.egg-info": { 192 "PKG-INFO": """ 193 Name: egginfo-pkg 194 Author: Steven Ma 195 License: Unknown 196 Version: 1.0.0 197 Classifier: Intended Audience :: Developers 198 Classifier: Topic :: Software Development :: Libraries 199 Keywords: sample package 200 Description: Once upon a time 201 There was an egginfo package 202 """, 203 "SOURCES.txt": """ 204 mod.py 205 egginfo_pkg.egg-info/top_level.txt 206 """, 207 "entry_points.txt": """ 208 [entries] 209 main = mod:main 210 """, 211 "requires.txt": """ 212 wheel >= 1.0; python_version >= "2.7" 213 [test] 214 pytest 215 """, 216 "top_level.txt": "mod\n", 217 }, 218 "mod.py": """ 219 def main(): 220 print("hello world") 221 """, 222 } 223 224 225class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): 226 files: FilesSpec = { 227 "egg_with_module_pkg.egg-info": { 228 "PKG-INFO": "Name: egg_with_module-pkg", 229 # SOURCES.txt is made from the source archive, and contains files 230 # (setup.py) that are not present after installation. 231 "SOURCES.txt": """ 232 egg_with_module.py 233 setup.py 234 egg_with_module_pkg.egg-info/PKG-INFO 235 egg_with_module_pkg.egg-info/SOURCES.txt 236 egg_with_module_pkg.egg-info/top_level.txt 237 """, 238 # installed-files.txt is written by pip, and is a strictly more 239 # accurate source than SOURCES.txt as to the installed contents of 240 # the package. 241 "installed-files.txt": """ 242 ../egg_with_module.py 243 PKG-INFO 244 SOURCES.txt 245 top_level.txt 246 """, 247 # missing top_level.txt (to trigger fallback to installed-files.txt) 248 }, 249 "egg_with_module.py": """ 250 def main(): 251 print("hello world") 252 """, 253 } 254 255 256class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder): 257 files: FilesSpec = { 258 "egg_with_module_pkg.egg-info": { 259 "PKG-INFO": "Name: egg_with_module-pkg", 260 # SOURCES.txt is made from the source archive, and contains files 261 # (setup.py) that are not present after installation. 262 "SOURCES.txt": """ 263 egg_with_module.py 264 setup.py 265 egg_with_module.json 266 egg_with_module_pkg.egg-info/PKG-INFO 267 egg_with_module_pkg.egg-info/SOURCES.txt 268 egg_with_module_pkg.egg-info/top_level.txt 269 """, 270 # installed-files.txt is written by pip, and is a strictly more 271 # accurate source than SOURCES.txt as to the installed contents of 272 # the package. 273 "installed-files.txt": """ 274 ../../../etc/jupyter/jupyter_notebook_config.d/relative.json 275 /etc/jupyter/jupyter_notebook_config.d/absolute.json 276 ../egg_with_module.py 277 PKG-INFO 278 SOURCES.txt 279 top_level.txt 280 """, 281 # missing top_level.txt (to trigger fallback to installed-files.txt) 282 }, 283 "egg_with_module.py": """ 284 def main(): 285 print("hello world") 286 """, 287 } 288 289 290class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): 291 files: FilesSpec = { 292 "egg_with_no_modules_pkg.egg-info": { 293 "PKG-INFO": "Name: egg_with_no_modules-pkg", 294 # SOURCES.txt is made from the source archive, and contains files 295 # (setup.py) that are not present after installation. 296 "SOURCES.txt": """ 297 setup.py 298 egg_with_no_modules_pkg.egg-info/PKG-INFO 299 egg_with_no_modules_pkg.egg-info/SOURCES.txt 300 egg_with_no_modules_pkg.egg-info/top_level.txt 301 """, 302 # installed-files.txt is written by pip, and is a strictly more 303 # accurate source than SOURCES.txt as to the installed contents of 304 # the package. 305 "installed-files.txt": """ 306 PKG-INFO 307 SOURCES.txt 308 top_level.txt 309 """, 310 # top_level.txt correctly reflects that no modules are installed 311 "top_level.txt": b"\n", 312 }, 313 } 314 315 316class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): 317 files: FilesSpec = { 318 "sources_fallback_pkg.egg-info": { 319 "PKG-INFO": "Name: sources_fallback-pkg", 320 # SOURCES.txt is made from the source archive, and contains files 321 # (setup.py) that are not present after installation. 322 "SOURCES.txt": """ 323 sources_fallback.py 324 setup.py 325 sources_fallback_pkg.egg-info/PKG-INFO 326 sources_fallback_pkg.egg-info/SOURCES.txt 327 """, 328 # missing installed-files.txt (i.e. not installed by pip) and 329 # missing top_level.txt (to trigger fallback to SOURCES.txt) 330 }, 331 "sources_fallback.py": """ 332 def main(): 333 print("hello world") 334 """, 335 } 336 337 338class EggInfoFile(OnSysPath, SiteBuilder): 339 files: FilesSpec = { 340 "egginfo_file.egg-info": """ 341 Metadata-Version: 1.0 342 Name: egginfo_file 343 Version: 0.1 344 Summary: An example package 345 Home-page: www.example.com 346 Author: Eric Haffa-Vee 347 Author-email: eric@example.coms 348 License: UNKNOWN 349 Description: UNKNOWN 350 Platform: UNKNOWN 351 """, 352 } 353 354 355# dedent all text strings before writing 356orig = _path.create.registry[str] 357_path.create.register(str, lambda content, path: orig(DALS(content), path)) 358 359 360build_files = _path.build 361 362 363def build_record(file_defs): 364 return ''.join(f'{name},,\n' for name in record_names(file_defs)) 365 366 367def record_names(file_defs): 368 recording = _path.Recording() 369 _path.build(file_defs, recording) 370 return recording.record 371 372 373class FileBuilder: 374 def unicode_filename(self): 375 return os_helper.FS_NONASCII or self.skip( 376 "File system does not support non-ascii." 377 ) 378 379 380def DALS(str): 381 "Dedent and left-strip" 382 return textwrap.dedent(str).lstrip() 383 384 385@requires_zlib() 386class ZipFixtures: 387 root = 'test.test_importlib.metadata.data' 388 389 def _fixture_on_path(self, filename): 390 pkg_file = resources.files(self.root).joinpath(filename) 391 file = self.resources.enter_context(resources.as_file(pkg_file)) 392 assert file.name.startswith('example'), file.name 393 sys.path.insert(0, str(file)) 394 self.resources.callback(sys.path.pop, 0) 395 396 def setUp(self): 397 # Add self.zip_name to the front of sys.path. 398 self.resources = contextlib.ExitStack() 399 self.addCleanup(self.resources.close) 400 401 402def parameterize(*args_set): 403 """Run test method with a series of parameters.""" 404 405 def wrapper(func): 406 @functools.wraps(func) 407 def _inner(self): 408 for args in args_set: 409 with self.subTest(**args): 410 func(self, **args) 411 412 return _inner 413 414 return wrapper 415