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