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