• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import sys
3import copy
4import json
5import shutil
6import pathlib
7import tempfile
8import textwrap
9import functools
10import contextlib
11
12from test.support import import_helper
13from test.support import os_helper
14from test.support import requires_zlib
15
16from . import _path
17from ._path import FilesSpec
18
19
20try:
21    from importlib import resources  # type: ignore
22
23    getattr(resources, 'files')
24    getattr(resources, 'as_file')
25except (ImportError, AttributeError):
26    import importlib_resources as resources  # type: ignore
27
28
29@contextlib.contextmanager
30def tempdir():
31    tmpdir = tempfile.mkdtemp()
32    try:
33        yield pathlib.Path(tmpdir)
34    finally:
35        shutil.rmtree(tmpdir)
36
37
38@contextlib.contextmanager
39def save_cwd():
40    orig = os.getcwd()
41    try:
42        yield
43    finally:
44        os.chdir(orig)
45
46
47@contextlib.contextmanager
48def tempdir_as_cwd():
49    with tempdir() as tmp:
50        with save_cwd():
51            os.chdir(str(tmp))
52            yield tmp
53
54
55@contextlib.contextmanager
56def install_finder(finder):
57    sys.meta_path.append(finder)
58    try:
59        yield
60    finally:
61        sys.meta_path.remove(finder)
62
63
64class Fixtures:
65    def setUp(self):
66        self.fixtures = contextlib.ExitStack()
67        self.addCleanup(self.fixtures.close)
68
69
70class SiteDir(Fixtures):
71    def setUp(self):
72        super().setUp()
73        self.site_dir = self.fixtures.enter_context(tempdir())
74
75
76class OnSysPath(Fixtures):
77    @staticmethod
78    @contextlib.contextmanager
79    def add_sys_path(dir):
80        sys.path[:0] = [str(dir)]
81        try:
82            yield
83        finally:
84            sys.path.remove(str(dir))
85
86    def setUp(self):
87        super().setUp()
88        self.fixtures.enter_context(self.add_sys_path(self.site_dir))
89        self.fixtures.enter_context(import_helper.isolated_modules())
90
91
92class SiteBuilder(SiteDir):
93    def setUp(self):
94        super().setUp()
95        for cls in self.__class__.mro():
96            with contextlib.suppress(AttributeError):
97                build_files(cls.files, prefix=self.site_dir)
98
99
100class DistInfoPkg(OnSysPath, SiteBuilder):
101    files: FilesSpec = {
102        "distinfo_pkg-1.0.0.dist-info": {
103            "METADATA": """
104                Name: distinfo-pkg
105                Author: Steven Ma
106                Version: 1.0.0
107                Requires-Dist: wheel >= 1.0
108                Requires-Dist: pytest; extra == 'test'
109                Keywords: sample package
110
111                Once upon a time
112                There was a distinfo pkg
113                """,
114            "RECORD": "mod.py,sha256=abc,20\n",
115            "entry_points.txt": """
116                [entries]
117                main = mod:main
118                ns:sub = mod:main
119            """,
120        },
121        "mod.py": """
122            def main():
123                print("hello world")
124            """,
125    }
126
127    def make_uppercase(self):
128        """
129        Rewrite metadata with everything uppercase.
130        """
131        shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info")
132        files = copy.deepcopy(DistInfoPkg.files)
133        info = files["distinfo_pkg-1.0.0.dist-info"]
134        info["METADATA"] = info["METADATA"].upper()
135        build_files(files, self.site_dir)
136
137
138class DistInfoPkgEditable(DistInfoPkg):
139    """
140    Package with a PEP 660 direct_url.json.
141    """
142
143    some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc'
144    files: FilesSpec = {
145        'distinfo_pkg-1.0.0.dist-info': {
146            'direct_url.json': json.dumps({
147                "archive_info": {
148                    "hash": f"sha256={some_hash}",
149                    "hashes": {"sha256": f"{some_hash}"},
150                },
151                "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl",
152            })
153        },
154    }
155
156
157class DistInfoPkgWithDot(OnSysPath, SiteBuilder):
158    files: FilesSpec = {
159        "pkg_dot-1.0.0.dist-info": {
160            "METADATA": """
161                Name: pkg.dot
162                Version: 1.0.0
163                """,
164        },
165    }
166
167
168class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder):
169    files: FilesSpec = {
170        "pkg.dot-1.0.0.dist-info": {
171            "METADATA": """
172                Name: pkg.dot
173                Version: 1.0.0
174                """,
175        },
176        "pkg.lot.egg-info": {
177            "METADATA": """
178                Name: pkg.lot
179                Version: 1.0.0
180                """,
181        },
182    }
183
184
185class DistInfoPkgOffPath(SiteBuilder):
186    files = DistInfoPkg.files
187
188
189class EggInfoPkg(OnSysPath, SiteBuilder):
190    files: FilesSpec = {
191        "egginfo_pkg.egg-info": {
192            "PKG-INFO": """
193                Name: egginfo-pkg
194                Author: Steven Ma
195                License: Unknown
196                Version: 1.0.0
197                Classifier: Intended Audience :: Developers
198                Classifier: Topic :: Software Development :: Libraries
199                Keywords: sample package
200                Description: Once upon a time
201                        There was an egginfo package
202                """,
203            "SOURCES.txt": """
204                mod.py
205                egginfo_pkg.egg-info/top_level.txt
206            """,
207            "entry_points.txt": """
208                [entries]
209                main = mod:main
210            """,
211            "requires.txt": """
212                wheel >= 1.0; python_version >= "2.7"
213                [test]
214                pytest
215            """,
216            "top_level.txt": "mod\n",
217        },
218        "mod.py": """
219            def main():
220                print("hello world")
221            """,
222    }
223
224
225class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder):
226    files: FilesSpec = {
227        "egg_with_module_pkg.egg-info": {
228            "PKG-INFO": "Name: egg_with_module-pkg",
229            # SOURCES.txt is made from the source archive, and contains files
230            # (setup.py) that are not present after installation.
231            "SOURCES.txt": """
232                egg_with_module.py
233                setup.py
234                egg_with_module_pkg.egg-info/PKG-INFO
235                egg_with_module_pkg.egg-info/SOURCES.txt
236                egg_with_module_pkg.egg-info/top_level.txt
237            """,
238            # installed-files.txt is written by pip, and is a strictly more
239            # accurate source than SOURCES.txt as to the installed contents of
240            # the package.
241            "installed-files.txt": """
242                ../egg_with_module.py
243                PKG-INFO
244                SOURCES.txt
245                top_level.txt
246            """,
247            # missing top_level.txt (to trigger fallback to installed-files.txt)
248        },
249        "egg_with_module.py": """
250            def main():
251                print("hello world")
252            """,
253    }
254
255
256class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder):
257    files: FilesSpec = {
258        "egg_with_module_pkg.egg-info": {
259            "PKG-INFO": "Name: egg_with_module-pkg",
260            # SOURCES.txt is made from the source archive, and contains files
261            # (setup.py) that are not present after installation.
262            "SOURCES.txt": """
263                egg_with_module.py
264                setup.py
265                egg_with_module.json
266                egg_with_module_pkg.egg-info/PKG-INFO
267                egg_with_module_pkg.egg-info/SOURCES.txt
268                egg_with_module_pkg.egg-info/top_level.txt
269            """,
270            # installed-files.txt is written by pip, and is a strictly more
271            # accurate source than SOURCES.txt as to the installed contents of
272            # the package.
273            "installed-files.txt": """
274                ../../../etc/jupyter/jupyter_notebook_config.d/relative.json
275                /etc/jupyter/jupyter_notebook_config.d/absolute.json
276                ../egg_with_module.py
277                PKG-INFO
278                SOURCES.txt
279                top_level.txt
280            """,
281            # missing top_level.txt (to trigger fallback to installed-files.txt)
282        },
283        "egg_with_module.py": """
284            def main():
285                print("hello world")
286            """,
287    }
288
289
290class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder):
291    files: FilesSpec = {
292        "egg_with_no_modules_pkg.egg-info": {
293            "PKG-INFO": "Name: egg_with_no_modules-pkg",
294            # SOURCES.txt is made from the source archive, and contains files
295            # (setup.py) that are not present after installation.
296            "SOURCES.txt": """
297                setup.py
298                egg_with_no_modules_pkg.egg-info/PKG-INFO
299                egg_with_no_modules_pkg.egg-info/SOURCES.txt
300                egg_with_no_modules_pkg.egg-info/top_level.txt
301            """,
302            # installed-files.txt is written by pip, and is a strictly more
303            # accurate source than SOURCES.txt as to the installed contents of
304            # the package.
305            "installed-files.txt": """
306                PKG-INFO
307                SOURCES.txt
308                top_level.txt
309            """,
310            # top_level.txt correctly reflects that no modules are installed
311            "top_level.txt": b"\n",
312        },
313    }
314
315
316class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder):
317    files: FilesSpec = {
318        "sources_fallback_pkg.egg-info": {
319            "PKG-INFO": "Name: sources_fallback-pkg",
320            # SOURCES.txt is made from the source archive, and contains files
321            # (setup.py) that are not present after installation.
322            "SOURCES.txt": """
323                sources_fallback.py
324                setup.py
325                sources_fallback_pkg.egg-info/PKG-INFO
326                sources_fallback_pkg.egg-info/SOURCES.txt
327            """,
328            # missing installed-files.txt (i.e. not installed by pip) and
329            # missing top_level.txt (to trigger fallback to SOURCES.txt)
330        },
331        "sources_fallback.py": """
332            def main():
333                print("hello world")
334            """,
335    }
336
337
338class EggInfoFile(OnSysPath, SiteBuilder):
339    files: FilesSpec = {
340        "egginfo_file.egg-info": """
341            Metadata-Version: 1.0
342            Name: egginfo_file
343            Version: 0.1
344            Summary: An example package
345            Home-page: www.example.com
346            Author: Eric Haffa-Vee
347            Author-email: eric@example.coms
348            License: UNKNOWN
349            Description: UNKNOWN
350            Platform: UNKNOWN
351            """,
352    }
353
354
355# dedent all text strings before writing
356orig = _path.create.registry[str]
357_path.create.register(str, lambda content, path: orig(DALS(content), path))
358
359
360build_files = _path.build
361
362
363def build_record(file_defs):
364    return ''.join(f'{name},,\n' for name in record_names(file_defs))
365
366
367def record_names(file_defs):
368    recording = _path.Recording()
369    _path.build(file_defs, recording)
370    return recording.record
371
372
373class FileBuilder:
374    def unicode_filename(self):
375        return os_helper.FS_NONASCII or self.skip(
376            "File system does not support non-ascii."
377        )
378
379
380def DALS(str):
381    "Dedent and left-strip"
382    return textwrap.dedent(str).lstrip()
383
384
385@requires_zlib()
386class ZipFixtures:
387    root = 'test.test_importlib.metadata.data'
388
389    def _fixture_on_path(self, filename):
390        pkg_file = resources.files(self.root).joinpath(filename)
391        file = self.resources.enter_context(resources.as_file(pkg_file))
392        assert file.name.startswith('example'), file.name
393        sys.path.insert(0, str(file))
394        self.resources.callback(sys.path.pop, 0)
395
396    def setUp(self):
397        # Add self.zip_name to the front of sys.path.
398        self.resources = contextlib.ExitStack()
399        self.addCleanup(self.resources.close)
400
401
402def parameterize(*args_set):
403    """Run test method with a series of parameters."""
404
405    def wrapper(func):
406        @functools.wraps(func)
407        def _inner(self):
408            for args in args_set:
409                with self.subTest(**args):
410                    func(self, **args)
411
412        return _inner
413
414    return wrapper
415