import contextlib import importlib.abc import importlib.machinery import os import sys import types import unittest from test.test_importlib import util from test.support import run_unittest # needed tests: # # need to test when nested, so that the top-level path isn't sys.path # need to test dynamic path detection, both at top-level and nested # with dynamic path, check when a loader is returned on path reload (that is, # trying to switch from a namespace package to a regular package) @contextlib.contextmanager def sys_modules_context(): """ Make sure sys.modules is the same object and has the same content when exiting the context as when entering. Similar to importlib.test.util.uncache, but doesn't require explicit names. """ sys_modules_saved = sys.modules sys_modules_copy = sys.modules.copy() try: yield finally: sys.modules = sys_modules_saved sys.modules.clear() sys.modules.update(sys_modules_copy) @contextlib.contextmanager def namespace_tree_context(**kwargs): """ Save import state and sys.modules cache and restore it on exit. Typical usage: >>> with namespace_tree_context(path=['/tmp/xxyy/portion1', ... '/tmp/xxyy/portion2']): ... pass """ # use default meta_path and path_hooks unless specified otherwise kwargs.setdefault('meta_path', sys.meta_path) kwargs.setdefault('path_hooks', sys.path_hooks) import_context = util.import_state(**kwargs) with import_context, sys_modules_context(): yield class NamespacePackageTest(unittest.TestCase): """ Subclasses should define self.root and self.paths (under that root) to be added to sys.path. """ root = os.path.join(os.path.dirname(__file__), 'namespace_pkgs') def setUp(self): self.resolved_paths = [ os.path.join(self.root, path) for path in self.paths ] self.ctx = namespace_tree_context(path=self.resolved_paths) self.ctx.__enter__() def tearDown(self): # TODO: will we ever want to pass exc_info to __exit__? self.ctx.__exit__(None, None, None) class SingleNamespacePackage(NamespacePackageTest): paths = ['portion1'] def test_simple_package(self): import foo.one self.assertEqual(foo.one.attr, 'portion1 foo one') def test_cant_import_other(self): with self.assertRaises(ImportError): import foo.two def test_module_repr(self): import foo.one self.assertEqual(repr(foo), "") class DynamicPatheNamespacePackage(NamespacePackageTest): paths = ['portion1'] def test_dynamic_path(self): # Make sure only 'foo.one' can be imported import foo.one self.assertEqual(foo.one.attr, 'portion1 foo one') with self.assertRaises(ImportError): import foo.two # Now modify sys.path sys.path.append(os.path.join(self.root, 'portion2')) # And make sure foo.two is now importable import foo.two self.assertEqual(foo.two.attr, 'portion2 foo two') class CombinedNamespacePackages(NamespacePackageTest): paths = ['both_portions'] def test_imports(self): import foo.one import foo.two self.assertEqual(foo.one.attr, 'both_portions foo one') self.assertEqual(foo.two.attr, 'both_portions foo two') class SeparatedNamespacePackages(NamespacePackageTest): paths = ['portion1', 'portion2'] def test_imports(self): import foo.one import foo.two self.assertEqual(foo.one.attr, 'portion1 foo one') self.assertEqual(foo.two.attr, 'portion2 foo two') class SeparatedOverlappingNamespacePackages(NamespacePackageTest): paths = ['portion1', 'both_portions'] def test_first_path_wins(self): import foo.one import foo.two self.assertEqual(foo.one.attr, 'portion1 foo one') self.assertEqual(foo.two.attr, 'both_portions foo two') def test_first_path_wins_again(self): sys.path.reverse() import foo.one import foo.two self.assertEqual(foo.one.attr, 'both_portions foo one') self.assertEqual(foo.two.attr, 'both_portions foo two') def test_first_path_wins_importing_second_first(self): import foo.two import foo.one self.assertEqual(foo.one.attr, 'portion1 foo one') self.assertEqual(foo.two.attr, 'both_portions foo two') class SingleZipNamespacePackage(NamespacePackageTest): paths = ['top_level_portion1.zip'] def test_simple_package(self): import foo.one self.assertEqual(foo.one.attr, 'portion1 foo one') def test_cant_import_other(self): with self.assertRaises(ImportError): import foo.two class SeparatedZipNamespacePackages(NamespacePackageTest): paths = ['top_level_portion1.zip', 'portion2'] def test_imports(self): import foo.one import foo.two self.assertEqual(foo.one.attr, 'portion1 foo one') self.assertEqual(foo.two.attr, 'portion2 foo two') self.assertIn('top_level_portion1.zip', foo.one.__file__) self.assertNotIn('.zip', foo.two.__file__) class SingleNestedZipNamespacePackage(NamespacePackageTest): paths = ['nested_portion1.zip/nested_portion1'] def test_simple_package(self): import foo.one self.assertEqual(foo.one.attr, 'portion1 foo one') def test_cant_import_other(self): with self.assertRaises(ImportError): import foo.two class SeparatedNestedZipNamespacePackages(NamespacePackageTest): paths = ['nested_portion1.zip/nested_portion1', 'portion2'] def test_imports(self): import foo.one import foo.two self.assertEqual(foo.one.attr, 'portion1 foo one') self.assertEqual(foo.two.attr, 'portion2 foo two') fn = os.path.join('nested_portion1.zip', 'nested_portion1') self.assertIn(fn, foo.one.__file__) self.assertNotIn('.zip', foo.two.__file__) class LegacySupport(NamespacePackageTest): paths = ['not_a_namespace_pkg', 'portion1', 'portion2', 'both_portions'] def test_non_namespace_package_takes_precedence(self): import foo.one with self.assertRaises(ImportError): import foo.two self.assertIn('__init__', foo.__file__) self.assertNotIn('namespace', str(foo.__loader__).lower()) class DynamicPathCalculation(NamespacePackageTest): paths = ['project1', 'project2'] def test_project3_fails(self): import parent.child.one self.assertEqual(len(parent.__path__), 2) self.assertEqual(len(parent.child.__path__), 2) import parent.child.two self.assertEqual(len(parent.__path__), 2) self.assertEqual(len(parent.child.__path__), 2) self.assertEqual(parent.child.one.attr, 'parent child one') self.assertEqual(parent.child.two.attr, 'parent child two') with self.assertRaises(ImportError): import parent.child.three self.assertEqual(len(parent.__path__), 2) self.assertEqual(len(parent.child.__path__), 2) def test_project3_succeeds(self): import parent.child.one self.assertEqual(len(parent.__path__), 2) self.assertEqual(len(parent.child.__path__), 2) import parent.child.two self.assertEqual(len(parent.__path__), 2) self.assertEqual(len(parent.child.__path__), 2) self.assertEqual(parent.child.one.attr, 'parent child one') self.assertEqual(parent.child.two.attr, 'parent child two') with self.assertRaises(ImportError): import parent.child.three # now add project3 sys.path.append(os.path.join(self.root, 'project3')) import parent.child.three # the paths dynamically get longer, to include the new directories self.assertEqual(len(parent.__path__), 3) self.assertEqual(len(parent.child.__path__), 3) self.assertEqual(parent.child.three.attr, 'parent child three') class ZipWithMissingDirectory(NamespacePackageTest): paths = ['missing_directory.zip'] @unittest.expectedFailure def test_missing_directory(self): # This will fail because missing_directory.zip contains: # Length Date Time Name # --------- ---------- ----- ---- # 29 2012-05-03 18:13 foo/one.py # 0 2012-05-03 20:57 bar/ # 38 2012-05-03 20:57 bar/two.py # --------- ------- # 67 3 files # Because there is no 'foo/', the zipimporter currently doesn't # know that foo is a namespace package import foo.one def test_present_directory(self): # This succeeds because there is a "bar/" in the zip file import bar.two self.assertEqual(bar.two.attr, 'missing_directory foo two') class ModuleAndNamespacePackageInSameDir(NamespacePackageTest): paths = ['module_and_namespace_package'] def test_module_before_namespace_package(self): # Make sure we find the module in preference to the # namespace package. import a_test self.assertEqual(a_test.attr, 'in module') if __name__ == "__main__": unittest.main()