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