1import contextlib 2import importlib 3import importlib.abc 4import importlib.machinery 5import os 6import sys 7import tempfile 8import unittest 9import warnings 10 11from test.test_importlib import util 12 13# needed tests: 14# 15# need to test when nested, so that the top-level path isn't sys.path 16# need to test dynamic path detection, both at top-level and nested 17# with dynamic path, check when a loader is returned on path reload (that is, 18# trying to switch from a namespace package to a regular package) 19 20 21@contextlib.contextmanager 22def sys_modules_context(): 23 """ 24 Make sure sys.modules is the same object and has the same content 25 when exiting the context as when entering. 26 27 Similar to importlib.test.util.uncache, but doesn't require explicit 28 names. 29 """ 30 sys_modules_saved = sys.modules 31 sys_modules_copy = sys.modules.copy() 32 try: 33 yield 34 finally: 35 sys.modules = sys_modules_saved 36 sys.modules.clear() 37 sys.modules.update(sys_modules_copy) 38 39 40@contextlib.contextmanager 41def namespace_tree_context(**kwargs): 42 """ 43 Save import state and sys.modules cache and restore it on exit. 44 Typical usage: 45 46 >>> with namespace_tree_context(path=['/tmp/xxyy/portion1', 47 ... '/tmp/xxyy/portion2']): 48 ... pass 49 """ 50 # use default meta_path and path_hooks unless specified otherwise 51 kwargs.setdefault('meta_path', sys.meta_path) 52 kwargs.setdefault('path_hooks', sys.path_hooks) 53 import_context = util.import_state(**kwargs) 54 with import_context, sys_modules_context(): 55 yield 56 57class NamespacePackageTest(unittest.TestCase): 58 """ 59 Subclasses should define self.root and self.paths (under that root) 60 to be added to sys.path. 61 """ 62 root = os.path.join(os.path.dirname(__file__), 'namespace_pkgs') 63 64 def setUp(self): 65 self.resolved_paths = [ 66 os.path.join(self.root, path) for path in self.paths 67 ] 68 self.enterContext(namespace_tree_context(path=self.resolved_paths)) 69 70 71class SingleNamespacePackage(NamespacePackageTest): 72 paths = ['portion1'] 73 74 def test_simple_package(self): 75 import foo.one 76 self.assertEqual(foo.one.attr, 'portion1 foo one') 77 78 def test_cant_import_other(self): 79 with self.assertRaises(ImportError): 80 import foo.two 81 82 def test_module_repr(self): 83 import foo.one 84 with warnings.catch_warnings(): 85 warnings.simplefilter("ignore") 86 self.assertEqual(foo.__spec__.loader.module_repr(foo), 87 "<module 'foo' (namespace)>") 88 89 90class DynamicPathNamespacePackage(NamespacePackageTest): 91 paths = ['portion1'] 92 93 def test_dynamic_path(self): 94 # Make sure only 'foo.one' can be imported 95 import foo.one 96 self.assertEqual(foo.one.attr, 'portion1 foo one') 97 98 with self.assertRaises(ImportError): 99 import foo.two 100 101 # Now modify sys.path 102 sys.path.append(os.path.join(self.root, 'portion2')) 103 104 # And make sure foo.two is now importable 105 import foo.two 106 self.assertEqual(foo.two.attr, 'portion2 foo two') 107 108 109class CombinedNamespacePackages(NamespacePackageTest): 110 paths = ['both_portions'] 111 112 def test_imports(self): 113 import foo.one 114 import foo.two 115 self.assertEqual(foo.one.attr, 'both_portions foo one') 116 self.assertEqual(foo.two.attr, 'both_portions foo two') 117 118 119class SeparatedNamespacePackages(NamespacePackageTest): 120 paths = ['portion1', 'portion2'] 121 122 def test_imports(self): 123 import foo.one 124 import foo.two 125 self.assertEqual(foo.one.attr, 'portion1 foo one') 126 self.assertEqual(foo.two.attr, 'portion2 foo two') 127 128 129class SeparatedNamespacePackagesCreatedWhileRunning(NamespacePackageTest): 130 paths = ['portion1'] 131 132 def test_invalidate_caches(self): 133 with tempfile.TemporaryDirectory() as temp_dir: 134 # we manipulate sys.path before anything is imported to avoid 135 # accidental cache invalidation when changing it 136 sys.path.append(temp_dir) 137 138 import foo.one 139 self.assertEqual(foo.one.attr, 'portion1 foo one') 140 141 # the module does not exist, so it cannot be imported 142 with self.assertRaises(ImportError): 143 import foo.just_created 144 145 # util.create_modules() manipulates sys.path 146 # so we must create the modules manually instead 147 namespace_path = os.path.join(temp_dir, 'foo') 148 os.mkdir(namespace_path) 149 module_path = os.path.join(namespace_path, 'just_created.py') 150 with open(module_path, 'w', encoding='utf-8') as file: 151 file.write('attr = "just_created foo"') 152 153 # the module is not known, so it cannot be imported yet 154 with self.assertRaises(ImportError): 155 import foo.just_created 156 157 # but after explicit cache invalidation, it is importable 158 importlib.invalidate_caches() 159 import foo.just_created 160 self.assertEqual(foo.just_created.attr, 'just_created foo') 161 162 163class SeparatedOverlappingNamespacePackages(NamespacePackageTest): 164 paths = ['portion1', 'both_portions'] 165 166 def test_first_path_wins(self): 167 import foo.one 168 import foo.two 169 self.assertEqual(foo.one.attr, 'portion1 foo one') 170 self.assertEqual(foo.two.attr, 'both_portions foo two') 171 172 def test_first_path_wins_again(self): 173 sys.path.reverse() 174 import foo.one 175 import foo.two 176 self.assertEqual(foo.one.attr, 'both_portions foo one') 177 self.assertEqual(foo.two.attr, 'both_portions foo two') 178 179 def test_first_path_wins_importing_second_first(self): 180 import foo.two 181 import foo.one 182 self.assertEqual(foo.one.attr, 'portion1 foo one') 183 self.assertEqual(foo.two.attr, 'both_portions foo two') 184 185 186class SingleZipNamespacePackage(NamespacePackageTest): 187 paths = ['top_level_portion1.zip'] 188 189 def test_simple_package(self): 190 import foo.one 191 self.assertEqual(foo.one.attr, 'portion1 foo one') 192 193 def test_cant_import_other(self): 194 with self.assertRaises(ImportError): 195 import foo.two 196 197 198class SeparatedZipNamespacePackages(NamespacePackageTest): 199 paths = ['top_level_portion1.zip', 'portion2'] 200 201 def test_imports(self): 202 import foo.one 203 import foo.two 204 self.assertEqual(foo.one.attr, 'portion1 foo one') 205 self.assertEqual(foo.two.attr, 'portion2 foo two') 206 self.assertIn('top_level_portion1.zip', foo.one.__file__) 207 self.assertNotIn('.zip', foo.two.__file__) 208 209 210class SingleNestedZipNamespacePackage(NamespacePackageTest): 211 paths = ['nested_portion1.zip/nested_portion1'] 212 213 def test_simple_package(self): 214 import foo.one 215 self.assertEqual(foo.one.attr, 'portion1 foo one') 216 217 def test_cant_import_other(self): 218 with self.assertRaises(ImportError): 219 import foo.two 220 221 222class SeparatedNestedZipNamespacePackages(NamespacePackageTest): 223 paths = ['nested_portion1.zip/nested_portion1', 'portion2'] 224 225 def test_imports(self): 226 import foo.one 227 import foo.two 228 self.assertEqual(foo.one.attr, 'portion1 foo one') 229 self.assertEqual(foo.two.attr, 'portion2 foo two') 230 fn = os.path.join('nested_portion1.zip', 'nested_portion1') 231 self.assertIn(fn, foo.one.__file__) 232 self.assertNotIn('.zip', foo.two.__file__) 233 234 235class LegacySupport(NamespacePackageTest): 236 paths = ['not_a_namespace_pkg', 'portion1', 'portion2', 'both_portions'] 237 238 def test_non_namespace_package_takes_precedence(self): 239 import foo.one 240 with self.assertRaises(ImportError): 241 import foo.two 242 self.assertIn('__init__', foo.__file__) 243 self.assertNotIn('namespace', str(foo.__loader__).lower()) 244 245 246class DynamicPathCalculation(NamespacePackageTest): 247 paths = ['project1', 'project2'] 248 249 def test_project3_fails(self): 250 import parent.child.one 251 self.assertEqual(len(parent.__path__), 2) 252 self.assertEqual(len(parent.child.__path__), 2) 253 import parent.child.two 254 self.assertEqual(len(parent.__path__), 2) 255 self.assertEqual(len(parent.child.__path__), 2) 256 257 self.assertEqual(parent.child.one.attr, 'parent child one') 258 self.assertEqual(parent.child.two.attr, 'parent child two') 259 260 with self.assertRaises(ImportError): 261 import parent.child.three 262 263 self.assertEqual(len(parent.__path__), 2) 264 self.assertEqual(len(parent.child.__path__), 2) 265 266 def test_project3_succeeds(self): 267 import parent.child.one 268 self.assertEqual(len(parent.__path__), 2) 269 self.assertEqual(len(parent.child.__path__), 2) 270 import parent.child.two 271 self.assertEqual(len(parent.__path__), 2) 272 self.assertEqual(len(parent.child.__path__), 2) 273 274 self.assertEqual(parent.child.one.attr, 'parent child one') 275 self.assertEqual(parent.child.two.attr, 'parent child two') 276 277 with self.assertRaises(ImportError): 278 import parent.child.three 279 280 # now add project3 281 sys.path.append(os.path.join(self.root, 'project3')) 282 import parent.child.three 283 284 # the paths dynamically get longer, to include the new directories 285 self.assertEqual(len(parent.__path__), 3) 286 self.assertEqual(len(parent.child.__path__), 3) 287 288 self.assertEqual(parent.child.three.attr, 'parent child three') 289 290 291class ZipWithMissingDirectory(NamespacePackageTest): 292 paths = ['missing_directory.zip'] 293 294 @unittest.expectedFailure 295 def test_missing_directory(self): 296 # This will fail because missing_directory.zip contains: 297 # Length Date Time Name 298 # --------- ---------- ----- ---- 299 # 29 2012-05-03 18:13 foo/one.py 300 # 0 2012-05-03 20:57 bar/ 301 # 38 2012-05-03 20:57 bar/two.py 302 # --------- ------- 303 # 67 3 files 304 305 # Because there is no 'foo/', the zipimporter currently doesn't 306 # know that foo is a namespace package 307 308 import foo.one 309 310 def test_present_directory(self): 311 # This succeeds because there is a "bar/" in the zip file 312 import bar.two 313 self.assertEqual(bar.two.attr, 'missing_directory foo two') 314 315 316class ModuleAndNamespacePackageInSameDir(NamespacePackageTest): 317 paths = ['module_and_namespace_package'] 318 319 def test_module_before_namespace_package(self): 320 # Make sure we find the module in preference to the 321 # namespace package. 322 import a_test 323 self.assertEqual(a_test.attr, 'in module') 324 325 326class ReloadTests(NamespacePackageTest): 327 paths = ['portion1'] 328 329 def test_simple_package(self): 330 import foo.one 331 foo = importlib.reload(foo) 332 self.assertEqual(foo.one.attr, 'portion1 foo one') 333 334 def test_cant_import_other(self): 335 import foo 336 with self.assertRaises(ImportError): 337 import foo.two 338 foo = importlib.reload(foo) 339 with self.assertRaises(ImportError): 340 import foo.two 341 342 def test_dynamic_path(self): 343 import foo.one 344 with self.assertRaises(ImportError): 345 import foo.two 346 347 # Now modify sys.path and reload. 348 sys.path.append(os.path.join(self.root, 'portion2')) 349 foo = importlib.reload(foo) 350 351 # And make sure foo.two is now importable 352 import foo.two 353 self.assertEqual(foo.two.attr, 'portion2 foo two') 354 355 356class LoaderTests(NamespacePackageTest): 357 paths = ['portion1'] 358 359 def test_namespace_loader_consistency(self): 360 # bpo-32303 361 import foo 362 self.assertEqual(foo.__loader__, foo.__spec__.loader) 363 self.assertIsNotNone(foo.__loader__) 364 365 def test_namespace_origin_consistency(self): 366 # bpo-32305 367 import foo 368 self.assertIsNone(foo.__spec__.origin) 369 self.assertIsNone(foo.__file__) 370 371 def test_path_indexable(self): 372 # bpo-35843 373 import foo 374 expected_path = os.path.join(self.root, 'portion1', 'foo') 375 self.assertEqual(foo.__path__[0], expected_path) 376 377 def test_loader_abc(self): 378 import foo 379 self.assertTrue(isinstance(foo.__loader__, importlib.abc.Loader)) 380 self.assertTrue(isinstance(foo.__loader__, importlib.machinery.NamespaceLoader)) 381 382 383if __name__ == "__main__": 384 unittest.main() 385