diff options
author | Eric V. Smith <eric@trueblade.com> | 2012-05-25 00:21:04 (GMT) |
---|---|---|
committer | Eric V. Smith <eric@trueblade.com> | 2012-05-25 00:21:04 (GMT) |
commit | 984b11f88fcace98e30decc19bbf9e281355e607 (patch) | |
tree | 613a0fb564da71c5fc84e9343813f87619591732 /Lib | |
parent | fa52cbd5e6210f257de40aab12d55d84d64bdb91 (diff) | |
download | cpython-984b11f88fcace98e30decc19bbf9e281355e607.zip cpython-984b11f88fcace98e30decc19bbf9e281355e607.tar.gz cpython-984b11f88fcace98e30decc19bbf9e281355e607.tar.bz2 |
issue 14660: Implement PEP 420, namespace packages.
Diffstat (limited to 'Lib')
21 files changed, 557 insertions, 68 deletions
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 3069bd8..3dcd05a 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -468,6 +468,10 @@ class BuiltinImporter: """ @classmethod + def module_repr(cls, module): + return "<module '{}' (built-in)>".format(module.__name__) + + @classmethod def find_module(cls, fullname, path=None): """Find the built-in module. @@ -521,6 +525,10 @@ class FrozenImporter: """ @classmethod + def module_repr(cls, m): + return "<module '{}' (frozen)>".format(m.__name__) + + @classmethod def find_module(cls, fullname, path=None): """Find a frozen module.""" return cls if _imp.is_frozen(fullname) else None @@ -533,7 +541,10 @@ class FrozenImporter: """Load a frozen module.""" is_reload = fullname in sys.modules try: - return _imp.init_frozen(fullname) + m = _imp.init_frozen(fullname) + # Let our own module_repr() method produce a suitable repr. + del m.__file__ + return m except: if not is_reload and fullname in sys.modules: del sys.modules[fullname] @@ -875,6 +886,79 @@ class ExtensionFileLoader: return None +class _NamespacePath: + """Represents a namespace package's path. It uses the module name + to find its parent module, and from there it looks up the parent's + __path__. When this changes, the module's own path is recomputed, + using path_finder. For top-leve modules, the parent module's path + is sys.path.""" + + def __init__(self, name, path, path_finder): + self._name = name + self._path = path + self._last_parent_path = tuple(self._get_parent_path()) + self._path_finder = path_finder + + def _find_parent_path_names(self): + """Returns a tuple of (parent-module-name, parent-path-attr-name)""" + parent, dot, me = self._name.rpartition('.') + if dot == '': + # This is a top-level module. sys.path contains the parent path. + return 'sys', 'path' + # Not a top-level module. parent-module.__path__ contains the + # parent path. + return parent, '__path__' + + def _get_parent_path(self): + parent_module_name, path_attr_name = self._find_parent_path_names() + return getattr(sys.modules[parent_module_name], path_attr_name) + + def _recalculate(self): + # If the parent's path has changed, recalculate _path + parent_path = tuple(self._get_parent_path()) # Make a copy + if parent_path != self._last_parent_path: + loader, new_path = self._path_finder(self._name, parent_path) + # Note that no changes are made if a loader is returned, but we + # do remember the new parent path + if loader is None: + self._path = new_path + self._last_parent_path = parent_path # Save the copy + return self._path + + def __iter__(self): + return iter(self._recalculate()) + + def __len__(self): + return len(self._recalculate()) + + def __repr__(self): + return "_NamespacePath({0!r})".format(self._path) + + def __contains__(self, item): + return item in self._recalculate() + + def append(self, item): + self._path.append(item) + + +class NamespaceLoader: + def __init__(self, name, path, path_finder): + self._path = _NamespacePath(name, path, path_finder) + + @classmethod + def module_repr(cls, module): + return "<module '{}' (namespace)>".format(module.__name__) + + @set_package + @set_loader + @module_for_loader + def load_module(self, module): + """Load a namespace module.""" + _verbose_message('namespace module loaded with path {!r}', self._path) + module.__path__ = self._path + return module + + # Finders ##################################################################### class PathFinder: @@ -916,19 +1000,46 @@ class PathFinder: return finder @classmethod + def _get_loader(cls, fullname, path): + """Find the loader or namespace_path for this module/package name.""" + # If this ends up being a namespace package, namespace_path is + # the list of paths that will become its __path__ + namespace_path = [] + for entry in path: + finder = cls._path_importer_cache(entry) + if finder is not None: + if hasattr(finder, 'find_loader'): + loader, portions = finder.find_loader(fullname) + else: + loader = finder.find_module(fullname) + portions = [] + if loader is not None: + # We found a loader: return it immediately. + return (loader, namespace_path) + # This is possibly part of a namespace package. + # Remember these path entries (if any) for when we + # create a namespace package, and continue iterating + # on path. + namespace_path.extend(portions) + else: + return (None, namespace_path) + + @classmethod def find_module(cls, fullname, path=None): """Find the module on sys.path or 'path' based on sys.path_hooks and sys.path_importer_cache.""" if path is None: path = sys.path - for entry in path: - finder = cls._path_importer_cache(entry) - if finder is not None: - loader = finder.find_module(fullname) - if loader: - return loader + loader, namespace_path = cls._get_loader(fullname, path) + if loader is not None: + return loader else: - return None + if namespace_path: + # We found at least one namespace path. Return a + # loader which can create the namespace package. + return NamespaceLoader(fullname, namespace_path, cls._get_loader) + else: + return None class FileFinder: @@ -942,8 +1053,8 @@ class FileFinder: def __init__(self, path, *details): """Initialize with the path to search on and a variable number of - 3-tuples containing the loader, file suffixes the loader recognizes, and - a boolean of whether the loader handles packages.""" + 3-tuples containing the loader, file suffixes the loader recognizes, + and a boolean of whether the loader handles packages.""" packages = [] modules = [] for loader, suffixes, supports_packages in details: @@ -964,6 +1075,19 @@ class FileFinder: def find_module(self, fullname): """Try to find a loader for the specified module.""" + # Call find_loader(). If it returns a string (indicating this + # is a namespace package portion), generate a warning and + # return None. + loader, portions = self.find_loader(fullname) + assert len(portions) in [0, 1] + if loader is None and len(portions): + msg = "Not importing directory {}: missing __init__" + _warnings.warn(msg.format(portions[0]), ImportWarning) + return loader + + def find_loader(self, fullname): + """Try to find a loader for the specified module, or the namespace + package portions. Returns (loader, list-of-portions).""" tail_module = fullname.rpartition('.')[2] try: mtime = _os.stat(self.path).st_mtime @@ -987,17 +1111,17 @@ class FileFinder: init_filename = '__init__' + suffix full_path = _path_join(base_path, init_filename) if _path_isfile(full_path): - return loader(fullname, full_path) + return (loader(fullname, full_path), [base_path]) else: - msg = "Not importing directory {}: missing __init__" - _warnings.warn(msg.format(base_path), ImportWarning) + # A namespace package, return the path + return (None, [base_path]) # Check for a file w/ a proper suffix exists. for suffix, loader in self.modules: if cache_module + suffix in cache: full_path = _path_join(self.path, tail_module + suffix) if _path_isfile(full_path): - return loader(fullname, full_path) - return None + return (loader(fullname, full_path), []) + return (None, []) def _fill_cache(self): """Fill the cache of potential modules and packages for this directory.""" diff --git a/Lib/importlib/test/frozen/test_loader.py b/Lib/importlib/test/frozen/test_loader.py index 91d73fa..ba512d9 100644 --- a/Lib/importlib/test/frozen/test_loader.py +++ b/Lib/importlib/test/frozen/test_loader.py @@ -10,38 +10,46 @@ class LoaderTests(abc.LoaderTests): def test_module(self): with util.uncache('__hello__'), captured_stdout() as stdout: module = machinery.FrozenImporter.load_module('__hello__') - check = {'__name__': '__hello__', '__file__': '<frozen>', - '__package__': '', '__loader__': machinery.FrozenImporter} + check = {'__name__': '__hello__', + '__package__': '', + '__loader__': machinery.FrozenImporter, + } for attr, value in check.items(): self.assertEqual(getattr(module, attr), value) self.assertEqual(stdout.getvalue(), 'Hello world!\n') + self.assertFalse(hasattr(module, '__file__')) def test_package(self): with util.uncache('__phello__'), captured_stdout() as stdout: module = machinery.FrozenImporter.load_module('__phello__') - check = {'__name__': '__phello__', '__file__': '<frozen>', - '__package__': '__phello__', '__path__': ['__phello__'], - '__loader__': machinery.FrozenImporter} + check = {'__name__': '__phello__', + '__package__': '__phello__', + '__path__': ['__phello__'], + '__loader__': machinery.FrozenImporter, + } for attr, value in check.items(): attr_value = getattr(module, attr) self.assertEqual(attr_value, value, "for __phello__.%s, %r != %r" % (attr, attr_value, value)) self.assertEqual(stdout.getvalue(), 'Hello world!\n') + self.assertFalse(hasattr(module, '__file__')) def test_lacking_parent(self): with util.uncache('__phello__', '__phello__.spam'), \ captured_stdout() as stdout: module = machinery.FrozenImporter.load_module('__phello__.spam') - check = {'__name__': '__phello__.spam', '__file__': '<frozen>', + check = {'__name__': '__phello__.spam', '__package__': '__phello__', - '__loader__': machinery.FrozenImporter} + '__loader__': machinery.FrozenImporter, + } for attr, value in check.items(): attr_value = getattr(module, attr) self.assertEqual(attr_value, value, "for __phello__.spam.%s, %r != %r" % (attr, attr_value, value)) self.assertEqual(stdout.getvalue(), 'Hello world!\n') + self.assertFalse(hasattr(module, '__file__')) def test_module_reuse(self): with util.uncache('__hello__'), captured_stdout() as stdout: @@ -51,6 +59,12 @@ class LoaderTests(abc.LoaderTests): self.assertEqual(stdout.getvalue(), 'Hello world!\nHello world!\n') + def test_module_repr(self): + with util.uncache('__hello__'), captured_stdout(): + module = machinery.FrozenImporter.load_module('__hello__') + self.assertEqual(repr(module), + "<module '__hello__' (frozen)>") + def test_state_after_failure(self): # No way to trigger an error in a frozen module. pass diff --git a/Lib/importlib/test/source/test_finder.py b/Lib/importlib/test/source/test_finder.py index bbe0163..a3fa21d 100644 --- a/Lib/importlib/test/source/test_finder.py +++ b/Lib/importlib/test/source/test_finder.py @@ -106,36 +106,17 @@ class FinderTests(abc.FinderTests): loader = self.import_(pkg_dir, 'pkg.sub') self.assertTrue(hasattr(loader, 'load_module')) - # [sub empty] - def test_empty_sub_directory(self): - context = source_util.create_modules('pkg.__init__', 'pkg.sub.__init__') - with warnings.catch_warnings(): - warnings.simplefilter("error", ImportWarning) - with context as mapping: - os.unlink(mapping['pkg.sub.__init__']) - pkg_dir = os.path.dirname(mapping['pkg.__init__']) - with self.assertRaises(ImportWarning): - self.import_(pkg_dir, 'pkg.sub') - # [package over modules] def test_package_over_module(self): name = '_temp' loader = self.run_test(name, {'{0}.__init__'.format(name), name}) self.assertTrue('__init__' in loader.get_filename(name)) - def test_failure(self): with source_util.create_modules('blah') as mapping: nothing = self.import_(mapping['.root'], 'sdfsadsadf') self.assertTrue(nothing is None) - # [empty dir] - def test_empty_dir(self): - with warnings.catch_warnings(): - warnings.simplefilter("error", ImportWarning) - with self.assertRaises(ImportWarning): - self.run_test('pkg', {'pkg.__init__'}, unlink={'pkg.__init__'}) - def test_empty_string_for_dir(self): # The empty string from sys.path means to search in the cwd. finder = machinery.FileFinder('', (machinery.SourceFileLoader, diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py index 8e062a4..c5b0c4d 100644 --- a/Lib/pkgutil.py +++ b/Lib/pkgutil.py @@ -515,19 +515,29 @@ def extend_path(path, name): pname = os.path.join(*name.split('.')) # Reconstitute as relative path sname_pkg = name + ".pkg" - init_py = "__init__.py" path = path[:] # Start with a copy of the existing path for dir in sys.path: - if not isinstance(dir, str) or not os.path.isdir(dir): + if not isinstance(dir, str): continue - subdir = os.path.join(dir, pname) - # XXX This may still add duplicate entries to path on - # case-insensitive filesystems - initfile = os.path.join(subdir, init_py) - if subdir not in path and os.path.isfile(initfile): - path.append(subdir) + + finder = get_importer(dir) + if finder is not None: + # Is this finder PEP 420 compliant? + if hasattr(finder, 'find_loader'): + loader, portions = finder.find_loader(name) + else: + # No, no need to call it + loader = None + portions = [] + + for portion in portions: + # XXX This may still add duplicate entries to path on + # case-insensitive filesystems + if portion not in path: + path.append(portion) + # XXX Is this the right thing for subpackages like zope.app? # It looks for a file named "zope.app.pkg" pkgfile = os.path.join(dir, sname_pkg) diff --git a/Lib/test/namespace_pkgs/both_portions/foo/one.py b/Lib/test/namespace_pkgs/both_portions/foo/one.py new file mode 100644 index 0000000..3080f6f --- /dev/null +++ b/Lib/test/namespace_pkgs/both_portions/foo/one.py @@ -0,0 +1 @@ +attr = 'both_portions foo one' diff --git a/Lib/test/namespace_pkgs/both_portions/foo/two.py b/Lib/test/namespace_pkgs/both_portions/foo/two.py new file mode 100644 index 0000000..4131d3d --- /dev/null +++ b/Lib/test/namespace_pkgs/both_portions/foo/two.py @@ -0,0 +1 @@ +attr = 'both_portions foo two' diff --git a/Lib/test/namespace_pkgs/missing_directory.zip b/Lib/test/namespace_pkgs/missing_directory.zip Binary files differnew file mode 100644 index 0000000..836a910 --- /dev/null +++ b/Lib/test/namespace_pkgs/missing_directory.zip diff --git a/Lib/test/namespace_pkgs/nested_portion1.zip b/Lib/test/namespace_pkgs/nested_portion1.zip Binary files differnew file mode 100644 index 0000000..8d22406 --- /dev/null +++ b/Lib/test/namespace_pkgs/nested_portion1.zip diff --git a/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py diff --git a/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py new file mode 100644 index 0000000..d8f5c83 --- /dev/null +++ b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py @@ -0,0 +1 @@ +attr = 'portion1 foo one' diff --git a/Lib/test/namespace_pkgs/portion1/foo/one.py b/Lib/test/namespace_pkgs/portion1/foo/one.py new file mode 100644 index 0000000..d8f5c83 --- /dev/null +++ b/Lib/test/namespace_pkgs/portion1/foo/one.py @@ -0,0 +1 @@ +attr = 'portion1 foo one' diff --git a/Lib/test/namespace_pkgs/portion2/foo/two.py b/Lib/test/namespace_pkgs/portion2/foo/two.py new file mode 100644 index 0000000..d092e1e --- /dev/null +++ b/Lib/test/namespace_pkgs/portion2/foo/two.py @@ -0,0 +1 @@ +attr = 'portion2 foo two' diff --git a/Lib/test/namespace_pkgs/project1/parent/child/one.py b/Lib/test/namespace_pkgs/project1/parent/child/one.py new file mode 100644 index 0000000..2776fcd --- /dev/null +++ b/Lib/test/namespace_pkgs/project1/parent/child/one.py @@ -0,0 +1 @@ +attr = 'parent child one' diff --git a/Lib/test/namespace_pkgs/project2/parent/child/two.py b/Lib/test/namespace_pkgs/project2/parent/child/two.py new file mode 100644 index 0000000..8b037bc --- /dev/null +++ b/Lib/test/namespace_pkgs/project2/parent/child/two.py @@ -0,0 +1 @@ +attr = 'parent child two' diff --git a/Lib/test/namespace_pkgs/project3/parent/child/three.py b/Lib/test/namespace_pkgs/project3/parent/child/three.py new file mode 100644 index 0000000..f8abfe1 --- /dev/null +++ b/Lib/test/namespace_pkgs/project3/parent/child/three.py @@ -0,0 +1 @@ +attr = 'parent child three' diff --git a/Lib/test/namespace_pkgs/top_level_portion1.zip b/Lib/test/namespace_pkgs/top_level_portion1.zip Binary files differnew file mode 100644 index 0000000..3b866c9 --- /dev/null +++ b/Lib/test/namespace_pkgs/top_level_portion1.zip diff --git a/Lib/test/test_frozen.py b/Lib/test/test_frozen.py index dbd229b..fd6761c 100644 --- a/Lib/test/test_frozen.py +++ b/Lib/test/test_frozen.py @@ -7,7 +7,7 @@ import sys class FrozenTests(unittest.TestCase): module_attrs = frozenset(['__builtins__', '__cached__', '__doc__', - '__file__', '__loader__', '__name__', + '__loader__', '__name__', '__package__']) package_attrs = frozenset(list(module_attrs) + ['__path__']) diff --git a/Lib/test/test_import.py b/Lib/test/test_import.py index 890041f..a90e627 100644 --- a/Lib/test/test_import.py +++ b/Lib/test/test_import.py @@ -286,12 +286,6 @@ class ImportTests(unittest.TestCase): import test.support as y self.assertIs(y, test.support, y.__name__) - def test_import_initless_directory_warning(self): - with check_warnings(('', ImportWarning)): - # Just a random non-package directory we always expect to be - # somewhere in sys.path... - self.assertRaises(ImportError, __import__, "site-packages") - def test_import_by_filename(self): path = os.path.abspath(TESTFN) encoding = sys.getfilesystemencoding() diff --git a/Lib/test/test_module.py b/Lib/test/test_module.py index 5617789..e5a2525 100644 --- a/Lib/test/test_module.py +++ b/Lib/test/test_module.py @@ -5,6 +5,15 @@ from test.support import run_unittest, gc_collect import sys ModuleType = type(sys) +class FullLoader: + @classmethod + def module_repr(cls, m): + return "<module '{}' (crafted)>".format(m.__name__) + +class BareLoader: + pass + + class ModuleTests(unittest.TestCase): def test_uninitialized(self): # An uninitialized module has no __dict__ or __name__, @@ -80,8 +89,90 @@ a = A(destroyed)""" gc_collect() self.assertEqual(destroyed, [1]) + def test_module_repr_minimal(self): + # reprs when modules have no __file__, __name__, or __loader__ + m = ModuleType('foo') + del m.__name__ + self.assertEqual(repr(m), "<module '?'>") + + def test_module_repr_with_name(self): + m = ModuleType('foo') + self.assertEqual(repr(m), "<module 'foo'>") + + def test_module_repr_with_name_and_filename(self): + m = ModuleType('foo') + m.__file__ = '/tmp/foo.py' + self.assertEqual(repr(m), "<module 'foo' from '/tmp/foo.py'>") + + def test_module_repr_with_filename_only(self): + m = ModuleType('foo') + del m.__name__ + m.__file__ = '/tmp/foo.py' + self.assertEqual(repr(m), "<module '?' from '/tmp/foo.py'>") + + def test_module_repr_with_bare_loader_but_no_name(self): + m = ModuleType('foo') + del m.__name__ + # Yes, a class not an instance. + m.__loader__ = BareLoader + self.assertEqual( + repr(m), "<module '?' (<class 'test.test_module.BareLoader'>)>") + + def test_module_repr_with_full_loader_but_no_name(self): + # m.__loader__.module_repr() will fail because the module has no + # m.__name__. This exception will get suppressed and instead the + # loader's repr will be used. + m = ModuleType('foo') + del m.__name__ + # Yes, a class not an instance. + m.__loader__ = FullLoader + self.assertEqual( + repr(m), "<module '?' (<class 'test.test_module.FullLoader'>)>") + + def test_module_repr_with_bare_loader(self): + m = ModuleType('foo') + # Yes, a class not an instance. + m.__loader__ = BareLoader + self.assertEqual( + repr(m), "<module 'foo' (<class 'test.test_module.BareLoader'>)>") + + def test_module_repr_with_full_loader(self): + m = ModuleType('foo') + # Yes, a class not an instance. + m.__loader__ = FullLoader + self.assertEqual( + repr(m), "<module 'foo' (crafted)>") + + def test_module_repr_with_bare_loader_and_filename(self): + # Because the loader has no module_repr(), use the file name. + m = ModuleType('foo') + # Yes, a class not an instance. + m.__loader__ = BareLoader + m.__file__ = '/tmp/foo.py' + self.assertEqual(repr(m), "<module 'foo' from '/tmp/foo.py'>") + + def test_module_repr_with_full_loader_and_filename(self): + # Even though the module has an __file__, use __loader__.module_repr() + m = ModuleType('foo') + # Yes, a class not an instance. + m.__loader__ = FullLoader + m.__file__ = '/tmp/foo.py' + self.assertEqual(repr(m), "<module 'foo' (crafted)>") + + def test_module_repr_builtin(self): + self.assertEqual(repr(sys), "<module 'sys' (built-in)>") + + def test_module_repr_source(self): + r = repr(unittest) + self.assertEqual(r[:25], "<module 'unittest' from '") + self.assertEqual(r[-13:], "__init__.py'>") + + # frozen and namespace module reprs are tested in importlib. + + def test_main(): run_unittest(ModuleTests) + if __name__ == '__main__': test_main() diff --git a/Lib/test/test_namespace_pkgs.py b/Lib/test/test_namespace_pkgs.py new file mode 100644 index 0000000..176ddd3 --- /dev/null +++ b/Lib/test/test_namespace_pkgs.py @@ -0,0 +1,239 @@ +import sys +import contextlib +import unittest +import os + +import importlib.test.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 = importlib.test.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), "<module 'foo' (namespace)>") + + +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 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') + + +def test_main(): + run_unittest(*NamespacePackageTest.__subclasses__()) + + +if __name__ == "__main__": + test_main() diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py index 6025bcd..a41b5f5 100644 --- a/Lib/test/test_pkgutil.py +++ b/Lib/test/test_pkgutil.py @@ -138,10 +138,11 @@ class PkgutilPEP302Tests(unittest.TestCase): del sys.modules['foo'] +# These tests, especially the setup and cleanup, are hideous. They +# need to be cleaned up once issue 14715 is addressed. class ExtendPathTests(unittest.TestCase): def create_init(self, pkgname): dirname = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, dirname) sys.path.insert(0, dirname) pkgdir = os.path.join(dirname, pkgname) @@ -156,22 +157,40 @@ class ExtendPathTests(unittest.TestCase): with open(module_name, 'w') as fl: print('value={}'.format(value), file=fl) - def setUp(self): - # Create 2 directories on sys.path - self.pkgname = 'foo' - self.dirname_0 = self.create_init(self.pkgname) - self.dirname_1 = self.create_init(self.pkgname) + def test_simple(self): + pkgname = 'foo' + dirname_0 = self.create_init(pkgname) + dirname_1 = self.create_init(pkgname) + self.create_submodule(dirname_0, pkgname, 'bar', 0) + self.create_submodule(dirname_1, pkgname, 'baz', 1) + import foo.bar + import foo.baz + # Ensure we read the expected values + self.assertEqual(foo.bar.value, 0) + self.assertEqual(foo.baz.value, 1) - def tearDown(self): + # Ensure the path is set up correctly + self.assertEqual(sorted(foo.__path__), + sorted([os.path.join(dirname_0, pkgname), + os.path.join(dirname_1, pkgname)])) + + # Cleanup + shutil.rmtree(dirname_0) + shutil.rmtree(dirname_1) del sys.path[0] del sys.path[0] del sys.modules['foo'] del sys.modules['foo.bar'] del sys.modules['foo.baz'] - def test_simple(self): - self.create_submodule(self.dirname_0, self.pkgname, 'bar', 0) - self.create_submodule(self.dirname_1, self.pkgname, 'baz', 1) + def test_mixed_namespace(self): + pkgname = 'foo' + dirname_0 = self.create_init(pkgname) + dirname_1 = self.create_init(pkgname) + self.create_submodule(dirname_0, pkgname, 'bar', 0) + # Turn this into a PEP 420 namespace package + os.unlink(os.path.join(dirname_0, pkgname, '__init__.py')) + self.create_submodule(dirname_1, pkgname, 'baz', 1) import foo.bar import foo.baz # Ensure we read the expected values @@ -180,8 +199,17 @@ class ExtendPathTests(unittest.TestCase): # Ensure the path is set up correctly self.assertEqual(sorted(foo.__path__), - sorted([os.path.join(self.dirname_0, self.pkgname), - os.path.join(self.dirname_1, self.pkgname)])) + sorted([os.path.join(dirname_0, pkgname), + os.path.join(dirname_1, pkgname)])) + + # Cleanup + shutil.rmtree(dirname_0) + shutil.rmtree(dirname_1) + del sys.path[0] + del sys.path[0] + del sys.modules['foo'] + del sys.modules['foo.bar'] + del sys.modules['foo.baz'] # XXX: test .pkg files |