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