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