1# coding: utf-8
2
3import re
4import json
5import pickle
6import textwrap
7import unittest
8import importlib.metadata
9
10try:
11    import pyfakefs.fake_filesystem_unittest as ffs
12except ImportError:
13    from .stubs import fake_filesystem_unittest as ffs
14
15from . import fixtures
16from importlib.metadata import (
17    Distribution, EntryPoint,
18    PackageNotFoundError, distributions,
19    entry_points, metadata, version,
20    )
21
22
23class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
24    version_pattern = r'\d+\.\d+(\.\d)?'
25
26    def test_retrieves_version_of_self(self):
27        dist = Distribution.from_name('distinfo-pkg')
28        assert isinstance(dist.version, str)
29        assert re.match(self.version_pattern, dist.version)
30
31    def test_for_name_does_not_exist(self):
32        with self.assertRaises(PackageNotFoundError):
33            Distribution.from_name('does-not-exist')
34
35    def test_new_style_classes(self):
36        self.assertIsInstance(Distribution, type)
37
38
39class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
40    def test_import_nonexistent_module(self):
41        # Ensure that the MetadataPathFinder does not crash an import of a
42        # non-existent module.
43        with self.assertRaises(ImportError):
44            importlib.import_module('does_not_exist')
45
46    def test_resolve(self):
47        entries = dict(entry_points()['entries'])
48        ep = entries['main']
49        self.assertEqual(ep.load().__name__, "main")
50
51    def test_entrypoint_with_colon_in_name(self):
52        entries = dict(entry_points()['entries'])
53        ep = entries['ns:sub']
54        self.assertEqual(ep.value, 'mod:main')
55
56    def test_resolve_without_attr(self):
57        ep = EntryPoint(
58            name='ep',
59            value='importlib.metadata',
60            group='grp',
61            )
62        assert ep.load() is importlib.metadata
63
64
65class NameNormalizationTests(
66        fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
67    @staticmethod
68    def pkg_with_dashes(site_dir):
69        """
70        Create minimal metadata for a package with dashes
71        in the name (and thus underscores in the filename).
72        """
73        metadata_dir = site_dir / 'my_pkg.dist-info'
74        metadata_dir.mkdir()
75        metadata = metadata_dir / 'METADATA'
76        with metadata.open('w') as strm:
77            strm.write('Version: 1.0\n')
78        return 'my-pkg'
79
80    def test_dashes_in_dist_name_found_as_underscores(self):
81        """
82        For a package with a dash in the name, the dist-info metadata
83        uses underscores in the name. Ensure the metadata loads.
84        """
85        pkg_name = self.pkg_with_dashes(self.site_dir)
86        assert version(pkg_name) == '1.0'
87
88    @staticmethod
89    def pkg_with_mixed_case(site_dir):
90        """
91        Create minimal metadata for a package with mixed case
92        in the name.
93        """
94        metadata_dir = site_dir / 'CherryPy.dist-info'
95        metadata_dir.mkdir()
96        metadata = metadata_dir / 'METADATA'
97        with metadata.open('w') as strm:
98            strm.write('Version: 1.0\n')
99        return 'CherryPy'
100
101    def test_dist_name_found_as_any_case(self):
102        """
103        Ensure the metadata loads when queried with any case.
104        """
105        pkg_name = self.pkg_with_mixed_case(self.site_dir)
106        assert version(pkg_name) == '1.0'
107        assert version(pkg_name.lower()) == '1.0'
108        assert version(pkg_name.upper()) == '1.0'
109
110
111class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
112    @staticmethod
113    def pkg_with_non_ascii_description(site_dir):
114        """
115        Create minimal metadata for a package with non-ASCII in
116        the description.
117        """
118        metadata_dir = site_dir / 'portend.dist-info'
119        metadata_dir.mkdir()
120        metadata = metadata_dir / 'METADATA'
121        with metadata.open('w', encoding='utf-8') as fp:
122            fp.write('Description: pôrˈtend\n')
123        return 'portend'
124
125    @staticmethod
126    def pkg_with_non_ascii_description_egg_info(site_dir):
127        """
128        Create minimal metadata for an egg-info package with
129        non-ASCII in the description.
130        """
131        metadata_dir = site_dir / 'portend.dist-info'
132        metadata_dir.mkdir()
133        metadata = metadata_dir / 'METADATA'
134        with metadata.open('w', encoding='utf-8') as fp:
135            fp.write(textwrap.dedent("""
136                Name: portend
137
138                pôrˈtend
139                """).lstrip())
140        return 'portend'
141
142    def test_metadata_loads(self):
143        pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
144        meta = metadata(pkg_name)
145        assert meta['Description'] == 'pôrˈtend'
146
147    def test_metadata_loads_egg_info(self):
148        pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
149        meta = metadata(pkg_name)
150        assert meta.get_payload() == 'pôrˈtend\n'
151
152
153class DiscoveryTests(fixtures.EggInfoPkg,
154                     fixtures.DistInfoPkg,
155                     unittest.TestCase):
156
157    def test_package_discovery(self):
158        dists = list(distributions())
159        assert all(
160            isinstance(dist, Distribution)
161            for dist in dists
162            )
163        assert any(
164            dist.metadata['Name'] == 'egginfo-pkg'
165            for dist in dists
166            )
167        assert any(
168            dist.metadata['Name'] == 'distinfo-pkg'
169            for dist in dists
170            )
171
172    def test_invalid_usage(self):
173        with self.assertRaises(ValueError):
174            list(distributions(context='something', name='else'))
175
176
177class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
178    def test_egg_info(self):
179        # make an `EGG-INFO` directory that's unrelated
180        self.site_dir.joinpath('EGG-INFO').mkdir()
181        # used to crash with `IsADirectoryError`
182        with self.assertRaises(PackageNotFoundError):
183            version('unknown-package')
184
185    def test_egg(self):
186        egg = self.site_dir.joinpath('foo-3.6.egg')
187        egg.mkdir()
188        with self.add_sys_path(egg):
189            with self.assertRaises(PackageNotFoundError):
190                version('foo')
191
192
193class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
194    site_dir = '/does-not-exist'
195
196    def test_discovery(self):
197        """
198        Discovering distributions should succeed even if
199        there is an invalid path on sys.path.
200        """
201        importlib.metadata.distributions()
202
203
204class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
205    site_dir = '/access-denied'
206
207    def setUp(self):
208        super(InaccessibleSysPath, self).setUp()
209        self.setUpPyfakefs()
210        self.fs.create_dir(self.site_dir, perm_bits=000)
211
212    def test_discovery(self):
213        """
214        Discovering distributions should succeed even if
215        there is an invalid path on sys.path.
216        """
217        list(importlib.metadata.distributions())
218
219
220class TestEntryPoints(unittest.TestCase):
221    def __init__(self, *args):
222        super(TestEntryPoints, self).__init__(*args)
223        self.ep = importlib.metadata.EntryPoint('name', 'value', 'group')
224
225    def test_entry_point_pickleable(self):
226        revived = pickle.loads(pickle.dumps(self.ep))
227        assert revived == self.ep
228
229    def test_immutable(self):
230        """EntryPoints should be immutable"""
231        with self.assertRaises(AttributeError):
232            self.ep.name = 'badactor'
233
234    def test_repr(self):
235        assert 'EntryPoint' in repr(self.ep)
236        assert 'name=' in repr(self.ep)
237        assert "'name'" in repr(self.ep)
238
239    def test_hashable(self):
240        """EntryPoints should be hashable"""
241        hash(self.ep)
242
243    def test_json_dump(self):
244        """
245        json should not expect to be able to dump an EntryPoint
246        """
247        with self.assertRaises(Exception):
248            json.dumps(self.ep)
249
250    def test_module(self):
251        assert self.ep.module == 'value'
252
253    def test_attr(self):
254        assert self.ep.attr is None
255
256
257class FileSystem(
258        fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder,
259        unittest.TestCase):
260    def test_unicode_dir_on_sys_path(self):
261        """
262        Ensure a Unicode subdirectory of a directory on sys.path
263        does not crash.
264        """
265        fixtures.build_files(
266            {self.unicode_filename(): {}},
267            prefix=self.site_dir,
268            )
269        list(distributions())
270