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