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