1import os 2import sys 3import copy 4import shutil 5import pathlib 6import tempfile 7import textwrap 8import functools 9import contextlib 10 11from test.support.os_helper import FS_NONASCII 12from test.support import requires_zlib 13from typing import Dict, Union 14 15try: 16 from importlib import resources # type: ignore 17 18 getattr(resources, 'files') 19 getattr(resources, 'as_file') 20except (ImportError, AttributeError): 21 import importlib_resources as resources # type: ignore 22 23 24@contextlib.contextmanager 25def tempdir(): 26 tmpdir = tempfile.mkdtemp() 27 try: 28 yield pathlib.Path(tmpdir) 29 finally: 30 shutil.rmtree(tmpdir) 31 32 33@contextlib.contextmanager 34def save_cwd(): 35 orig = os.getcwd() 36 try: 37 yield 38 finally: 39 os.chdir(orig) 40 41 42@contextlib.contextmanager 43def tempdir_as_cwd(): 44 with tempdir() as tmp: 45 with save_cwd(): 46 os.chdir(str(tmp)) 47 yield tmp 48 49 50@contextlib.contextmanager 51def install_finder(finder): 52 sys.meta_path.append(finder) 53 try: 54 yield 55 finally: 56 sys.meta_path.remove(finder) 57 58 59class Fixtures: 60 def setUp(self): 61 self.fixtures = contextlib.ExitStack() 62 self.addCleanup(self.fixtures.close) 63 64 65class SiteDir(Fixtures): 66 def setUp(self): 67 super().setUp() 68 self.site_dir = self.fixtures.enter_context(tempdir()) 69 70 71class OnSysPath(Fixtures): 72 @staticmethod 73 @contextlib.contextmanager 74 def add_sys_path(dir): 75 sys.path[:0] = [str(dir)] 76 try: 77 yield 78 finally: 79 sys.path.remove(str(dir)) 80 81 def setUp(self): 82 super().setUp() 83 self.fixtures.enter_context(self.add_sys_path(self.site_dir)) 84 85 86# Except for python/mypy#731, prefer to define 87# FilesDef = Dict[str, Union['FilesDef', str]] 88FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]] 89 90 91class DistInfoPkg(OnSysPath, SiteDir): 92 files: FilesDef = { 93 "distinfo_pkg-1.0.0.dist-info": { 94 "METADATA": """ 95 Name: distinfo-pkg 96 Author: Steven Ma 97 Version: 1.0.0 98 Requires-Dist: wheel >= 1.0 99 Requires-Dist: pytest; extra == 'test' 100 Keywords: sample package 101 102 Once upon a time 103 There was a distinfo pkg 104 """, 105 "RECORD": "mod.py,sha256=abc,20\n", 106 "entry_points.txt": """ 107 [entries] 108 main = mod:main 109 ns:sub = mod:main 110 """, 111 }, 112 "mod.py": """ 113 def main(): 114 print("hello world") 115 """, 116 } 117 118 def setUp(self): 119 super().setUp() 120 build_files(DistInfoPkg.files, self.site_dir) 121 122 def make_uppercase(self): 123 """ 124 Rewrite metadata with everything uppercase. 125 """ 126 shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info") 127 files = copy.deepcopy(DistInfoPkg.files) 128 info = files["distinfo_pkg-1.0.0.dist-info"] 129 info["METADATA"] = info["METADATA"].upper() 130 build_files(files, self.site_dir) 131 132 133class DistInfoPkgWithDot(OnSysPath, SiteDir): 134 files: FilesDef = { 135 "pkg_dot-1.0.0.dist-info": { 136 "METADATA": """ 137 Name: pkg.dot 138 Version: 1.0.0 139 """, 140 }, 141 } 142 143 def setUp(self): 144 super().setUp() 145 build_files(DistInfoPkgWithDot.files, self.site_dir) 146 147 148class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir): 149 files: FilesDef = { 150 "pkg.dot-1.0.0.dist-info": { 151 "METADATA": """ 152 Name: pkg.dot 153 Version: 1.0.0 154 """, 155 }, 156 "pkg.lot.egg-info": { 157 "METADATA": """ 158 Name: pkg.lot 159 Version: 1.0.0 160 """, 161 }, 162 } 163 164 def setUp(self): 165 super().setUp() 166 build_files(DistInfoPkgWithDotLegacy.files, self.site_dir) 167 168 169class DistInfoPkgOffPath(SiteDir): 170 def setUp(self): 171 super().setUp() 172 build_files(DistInfoPkg.files, self.site_dir) 173 174 175class EggInfoPkg(OnSysPath, SiteDir): 176 files: FilesDef = { 177 "egginfo_pkg.egg-info": { 178 "PKG-INFO": """ 179 Name: egginfo-pkg 180 Author: Steven Ma 181 License: Unknown 182 Version: 1.0.0 183 Classifier: Intended Audience :: Developers 184 Classifier: Topic :: Software Development :: Libraries 185 Keywords: sample package 186 Description: Once upon a time 187 There was an egginfo package 188 """, 189 "SOURCES.txt": """ 190 mod.py 191 egginfo_pkg.egg-info/top_level.txt 192 """, 193 "entry_points.txt": """ 194 [entries] 195 main = mod:main 196 """, 197 "requires.txt": """ 198 wheel >= 1.0; python_version >= "2.7" 199 [test] 200 pytest 201 """, 202 "top_level.txt": "mod\n", 203 }, 204 "mod.py": """ 205 def main(): 206 print("hello world") 207 """, 208 } 209 210 def setUp(self): 211 super().setUp() 212 build_files(EggInfoPkg.files, prefix=self.site_dir) 213 214 215class EggInfoFile(OnSysPath, SiteDir): 216 files: FilesDef = { 217 "egginfo_file.egg-info": """ 218 Metadata-Version: 1.0 219 Name: egginfo_file 220 Version: 0.1 221 Summary: An example package 222 Home-page: www.example.com 223 Author: Eric Haffa-Vee 224 Author-email: [email protected] 225 License: UNKNOWN 226 Description: UNKNOWN 227 Platform: UNKNOWN 228 """, 229 } 230 231 def setUp(self): 232 super().setUp() 233 build_files(EggInfoFile.files, prefix=self.site_dir) 234 235 236def build_files(file_defs, prefix=pathlib.Path()): 237 """Build a set of files/directories, as described by the 238 239 file_defs dictionary. Each key/value pair in the dictionary is 240 interpreted as a filename/contents pair. If the contents value is a 241 dictionary, a directory is created, and the dictionary interpreted 242 as the files within it, recursively. 243 244 For example: 245 246 {"README.txt": "A README file", 247 "foo": { 248 "__init__.py": "", 249 "bar": { 250 "__init__.py": "", 251 }, 252 "baz.py": "# Some code", 253 } 254 } 255 """ 256 for name, contents in file_defs.items(): 257 full_name = prefix / name 258 if isinstance(contents, dict): 259 full_name.mkdir() 260 build_files(contents, prefix=full_name) 261 else: 262 if isinstance(contents, bytes): 263 with full_name.open('wb') as f: 264 f.write(contents) 265 else: 266 with full_name.open('w', encoding='utf-8') as f: 267 f.write(DALS(contents)) 268 269 270class FileBuilder: 271 def unicode_filename(self): 272 return FS_NONASCII or self.skip("File system does not support non-ascii.") 273 274 275def DALS(str): 276 "Dedent and left-strip" 277 return textwrap.dedent(str).lstrip() 278 279 280class NullFinder: 281 def find_module(self, name): 282 pass 283 284 285@requires_zlib() 286class ZipFixtures: 287 root = 'test.test_importlib.data' 288 289 def _fixture_on_path(self, filename): 290 pkg_file = resources.files(self.root).joinpath(filename) 291 file = self.resources.enter_context(resources.as_file(pkg_file)) 292 assert file.name.startswith('example'), file.name 293 sys.path.insert(0, str(file)) 294 self.resources.callback(sys.path.pop, 0) 295 296 def setUp(self): 297 # Add self.zip_name to the front of sys.path. 298 self.resources = contextlib.ExitStack() 299 self.addCleanup(self.resources.close) 300 301 302def parameterize(*args_set): 303 """Run test method with a series of parameters.""" 304 305 def wrapper(func): 306 @functools.wraps(func) 307 def _inner(self): 308 for args in args_set: 309 with self.subTest(**args): 310 func(self, **args) 311 312 return _inner 313 314 return wrapper 315