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