issue 14660: Implement PEP 420, namespace packages.

This commit is contained in:
Eric V. Smith 2012-05-24 20:21:04 -04:00
parent fa52cbd5e6
commit 984b11f88f
25 changed files with 4455 additions and 3412 deletions

View File

@ -467,6 +467,10 @@ class BuiltinImporter:
""" """
@classmethod
def module_repr(cls, module):
return "<module '{}' (built-in)>".format(module.__name__)
@classmethod @classmethod
def find_module(cls, fullname, path=None): def find_module(cls, fullname, path=None):
"""Find the built-in module. """Find the built-in module.
@ -520,6 +524,10 @@ class FrozenImporter:
""" """
@classmethod
def module_repr(cls, m):
return "<module '{}' (frozen)>".format(m.__name__)
@classmethod @classmethod
def find_module(cls, fullname, path=None): def find_module(cls, fullname, path=None):
"""Find a frozen module.""" """Find a frozen module."""
@ -533,7 +541,10 @@ class FrozenImporter:
"""Load a frozen module.""" """Load a frozen module."""
is_reload = fullname in sys.modules is_reload = fullname in sys.modules
try: 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: except:
if not is_reload and fullname in sys.modules: if not is_reload and fullname in sys.modules:
del sys.modules[fullname] del sys.modules[fullname]
@ -875,6 +886,79 @@ class ExtensionFileLoader:
return None 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 ##################################################################### # Finders #####################################################################
class PathFinder: class PathFinder:
@ -915,20 +999,47 @@ class PathFinder:
sys.path_importer_cache[path] = finder sys.path_importer_cache[path] = finder
return finder 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 @classmethod
def find_module(cls, fullname, path=None): def find_module(cls, fullname, path=None):
"""Find the module on sys.path or 'path' based on sys.path_hooks and """Find the module on sys.path or 'path' based on sys.path_hooks and
sys.path_importer_cache.""" sys.path_importer_cache."""
if path is None: if path is None:
path = sys.path path = sys.path
for entry in path: loader, namespace_path = cls._get_loader(fullname, path)
finder = cls._path_importer_cache(entry) if loader is not None:
if finder is not None: return loader
loader = finder.find_module(fullname)
if loader:
return loader
else: 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: class FileFinder:
@ -942,8 +1053,8 @@ class FileFinder:
def __init__(self, path, *details): def __init__(self, path, *details):
"""Initialize with the path to search on and a variable number of """Initialize with the path to search on and a variable number of
3-tuples containing the loader, file suffixes the loader recognizes, and 3-tuples containing the loader, file suffixes the loader recognizes,
a boolean of whether the loader handles packages.""" and a boolean of whether the loader handles packages."""
packages = [] packages = []
modules = [] modules = []
for loader, suffixes, supports_packages in details: for loader, suffixes, supports_packages in details:
@ -964,6 +1075,19 @@ class FileFinder:
def find_module(self, fullname): def find_module(self, fullname):
"""Try to find a loader for the specified module.""" """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] tail_module = fullname.rpartition('.')[2]
try: try:
mtime = _os.stat(self.path).st_mtime mtime = _os.stat(self.path).st_mtime
@ -987,17 +1111,17 @@ class FileFinder:
init_filename = '__init__' + suffix init_filename = '__init__' + suffix
full_path = _path_join(base_path, init_filename) full_path = _path_join(base_path, init_filename)
if _path_isfile(full_path): if _path_isfile(full_path):
return loader(fullname, full_path) return (loader(fullname, full_path), [base_path])
else: else:
msg = "Not importing directory {}: missing __init__" # A namespace package, return the path
_warnings.warn(msg.format(base_path), ImportWarning) return (None, [base_path])
# Check for a file w/ a proper suffix exists. # Check for a file w/ a proper suffix exists.
for suffix, loader in self.modules: for suffix, loader in self.modules:
if cache_module + suffix in cache: if cache_module + suffix in cache:
full_path = _path_join(self.path, tail_module + suffix) full_path = _path_join(self.path, tail_module + suffix)
if _path_isfile(full_path): if _path_isfile(full_path):
return loader(fullname, full_path) return (loader(fullname, full_path), [])
return None return (None, [])
def _fill_cache(self): def _fill_cache(self):
"""Fill the cache of potential modules and packages for this directory.""" """Fill the cache of potential modules and packages for this directory."""

View File

@ -10,38 +10,46 @@ class LoaderTests(abc.LoaderTests):
def test_module(self): def test_module(self):
with util.uncache('__hello__'), captured_stdout() as stdout: with util.uncache('__hello__'), captured_stdout() as stdout:
module = machinery.FrozenImporter.load_module('__hello__') module = machinery.FrozenImporter.load_module('__hello__')
check = {'__name__': '__hello__', '__file__': '<frozen>', check = {'__name__': '__hello__',
'__package__': '', '__loader__': machinery.FrozenImporter} '__package__': '',
'__loader__': machinery.FrozenImporter,
}
for attr, value in check.items(): for attr, value in check.items():
self.assertEqual(getattr(module, attr), value) self.assertEqual(getattr(module, attr), value)
self.assertEqual(stdout.getvalue(), 'Hello world!\n') self.assertEqual(stdout.getvalue(), 'Hello world!\n')
self.assertFalse(hasattr(module, '__file__'))
def test_package(self): def test_package(self):
with util.uncache('__phello__'), captured_stdout() as stdout: with util.uncache('__phello__'), captured_stdout() as stdout:
module = machinery.FrozenImporter.load_module('__phello__') module = machinery.FrozenImporter.load_module('__phello__')
check = {'__name__': '__phello__', '__file__': '<frozen>', check = {'__name__': '__phello__',
'__package__': '__phello__', '__path__': ['__phello__'], '__package__': '__phello__',
'__loader__': machinery.FrozenImporter} '__path__': ['__phello__'],
'__loader__': machinery.FrozenImporter,
}
for attr, value in check.items(): for attr, value in check.items():
attr_value = getattr(module, attr) attr_value = getattr(module, attr)
self.assertEqual(attr_value, value, self.assertEqual(attr_value, value,
"for __phello__.%s, %r != %r" % "for __phello__.%s, %r != %r" %
(attr, attr_value, value)) (attr, attr_value, value))
self.assertEqual(stdout.getvalue(), 'Hello world!\n') self.assertEqual(stdout.getvalue(), 'Hello world!\n')
self.assertFalse(hasattr(module, '__file__'))
def test_lacking_parent(self): def test_lacking_parent(self):
with util.uncache('__phello__', '__phello__.spam'), \ with util.uncache('__phello__', '__phello__.spam'), \
captured_stdout() as stdout: captured_stdout() as stdout:
module = machinery.FrozenImporter.load_module('__phello__.spam') module = machinery.FrozenImporter.load_module('__phello__.spam')
check = {'__name__': '__phello__.spam', '__file__': '<frozen>', check = {'__name__': '__phello__.spam',
'__package__': '__phello__', '__package__': '__phello__',
'__loader__': machinery.FrozenImporter} '__loader__': machinery.FrozenImporter,
}
for attr, value in check.items(): for attr, value in check.items():
attr_value = getattr(module, attr) attr_value = getattr(module, attr)
self.assertEqual(attr_value, value, self.assertEqual(attr_value, value,
"for __phello__.spam.%s, %r != %r" % "for __phello__.spam.%s, %r != %r" %
(attr, attr_value, value)) (attr, attr_value, value))
self.assertEqual(stdout.getvalue(), 'Hello world!\n') self.assertEqual(stdout.getvalue(), 'Hello world!\n')
self.assertFalse(hasattr(module, '__file__'))
def test_module_reuse(self): def test_module_reuse(self):
with util.uncache('__hello__'), captured_stdout() as stdout: with util.uncache('__hello__'), captured_stdout() as stdout:
@ -51,6 +59,12 @@ class LoaderTests(abc.LoaderTests):
self.assertEqual(stdout.getvalue(), self.assertEqual(stdout.getvalue(),
'Hello world!\nHello world!\n') '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): def test_state_after_failure(self):
# No way to trigger an error in a frozen module. # No way to trigger an error in a frozen module.
pass pass

View File

@ -106,36 +106,17 @@ class FinderTests(abc.FinderTests):
loader = self.import_(pkg_dir, 'pkg.sub') loader = self.import_(pkg_dir, 'pkg.sub')
self.assertTrue(hasattr(loader, 'load_module')) 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] # [package over modules]
def test_package_over_module(self): def test_package_over_module(self):
name = '_temp' name = '_temp'
loader = self.run_test(name, {'{0}.__init__'.format(name), name}) loader = self.run_test(name, {'{0}.__init__'.format(name), name})
self.assertTrue('__init__' in loader.get_filename(name)) self.assertTrue('__init__' in loader.get_filename(name))
def test_failure(self): def test_failure(self):
with source_util.create_modules('blah') as mapping: with source_util.create_modules('blah') as mapping:
nothing = self.import_(mapping['.root'], 'sdfsadsadf') nothing = self.import_(mapping['.root'], 'sdfsadsadf')
self.assertTrue(nothing is None) 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): def test_empty_string_for_dir(self):
# The empty string from sys.path means to search in the cwd. # The empty string from sys.path means to search in the cwd.
finder = machinery.FileFinder('', (machinery.SourceFileLoader, finder = machinery.FileFinder('', (machinery.SourceFileLoader,

View File

@ -515,19 +515,29 @@ def extend_path(path, name):
pname = os.path.join(*name.split('.')) # Reconstitute as relative path pname = os.path.join(*name.split('.')) # Reconstitute as relative path
sname_pkg = name + ".pkg" sname_pkg = name + ".pkg"
init_py = "__init__.py"
path = path[:] # Start with a copy of the existing path path = path[:] # Start with a copy of the existing path
for dir in sys.path: for dir in sys.path:
if not isinstance(dir, str) or not os.path.isdir(dir): if not isinstance(dir, str):
continue continue
subdir = os.path.join(dir, pname)
# XXX This may still add duplicate entries to path on finder = get_importer(dir)
# case-insensitive filesystems if finder is not None:
initfile = os.path.join(subdir, init_py) # Is this finder PEP 420 compliant?
if subdir not in path and os.path.isfile(initfile): if hasattr(finder, 'find_loader'):
path.append(subdir) 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? # XXX Is this the right thing for subpackages like zope.app?
# It looks for a file named "zope.app.pkg" # It looks for a file named "zope.app.pkg"
pkgfile = os.path.join(dir, sname_pkg) pkgfile = os.path.join(dir, sname_pkg)

View File

@ -0,0 +1 @@
attr = 'both_portions foo one'

View File

@ -0,0 +1 @@
attr = 'both_portions foo two'

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
attr = 'portion1 foo one'

View File

@ -0,0 +1 @@
attr = 'portion1 foo one'

View File

@ -0,0 +1 @@
attr = 'portion2 foo two'

View File

@ -0,0 +1 @@
attr = 'parent child one'

View File

@ -0,0 +1 @@
attr = 'parent child two'

View File

@ -0,0 +1 @@
attr = 'parent child three'

Binary file not shown.

View File

@ -7,7 +7,7 @@ import sys
class FrozenTests(unittest.TestCase): class FrozenTests(unittest.TestCase):
module_attrs = frozenset(['__builtins__', '__cached__', '__doc__', module_attrs = frozenset(['__builtins__', '__cached__', '__doc__',
'__file__', '__loader__', '__name__', '__loader__', '__name__',
'__package__']) '__package__'])
package_attrs = frozenset(list(module_attrs) + ['__path__']) package_attrs = frozenset(list(module_attrs) + ['__path__'])

View File

@ -286,12 +286,6 @@ class ImportTests(unittest.TestCase):
import test.support as y import test.support as y
self.assertIs(y, test.support, y.__name__) 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): def test_import_by_filename(self):
path = os.path.abspath(TESTFN) path = os.path.abspath(TESTFN)
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()

View File

@ -5,6 +5,15 @@ from test.support import run_unittest, gc_collect
import sys import sys
ModuleType = type(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): class ModuleTests(unittest.TestCase):
def test_uninitialized(self): def test_uninitialized(self):
# An uninitialized module has no __dict__ or __name__, # An uninitialized module has no __dict__ or __name__,
@ -80,8 +89,90 @@ a = A(destroyed)"""
gc_collect() gc_collect()
self.assertEqual(destroyed, [1]) 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(): def test_main():
run_unittest(ModuleTests) run_unittest(ModuleTests)
if __name__ == '__main__': if __name__ == '__main__':
test_main() test_main()

View File

@ -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()

View File

@ -138,10 +138,11 @@ class PkgutilPEP302Tests(unittest.TestCase):
del sys.modules['foo'] 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): class ExtendPathTests(unittest.TestCase):
def create_init(self, pkgname): def create_init(self, pkgname):
dirname = tempfile.mkdtemp() dirname = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, dirname)
sys.path.insert(0, dirname) sys.path.insert(0, dirname)
pkgdir = os.path.join(dirname, pkgname) pkgdir = os.path.join(dirname, pkgname)
@ -156,22 +157,12 @@ class ExtendPathTests(unittest.TestCase):
with open(module_name, 'w') as fl: with open(module_name, 'w') as fl:
print('value={}'.format(value), file=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 tearDown(self):
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): def test_simple(self):
self.create_submodule(self.dirname_0, self.pkgname, 'bar', 0) pkgname = 'foo'
self.create_submodule(self.dirname_1, self.pkgname, 'baz', 1) 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.bar
import foo.baz import foo.baz
# Ensure we read the expected values # Ensure we read the expected values
@ -180,8 +171,45 @@ class ExtendPathTests(unittest.TestCase):
# Ensure the path is set up correctly # Ensure the path is set up correctly
self.assertEqual(sorted(foo.__path__), self.assertEqual(sorted(foo.__path__),
sorted([os.path.join(self.dirname_0, self.pkgname), sorted([os.path.join(dirname_0, pkgname),
os.path.join(self.dirname_1, self.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_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
self.assertEqual(foo.bar.value, 0)
self.assertEqual(foo.baz.value, 1)
# 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']
# XXX: test .pkg files # XXX: test .pkg files

View File

@ -10,6 +10,8 @@ What's New in Python 3.3.0 Alpha 4?
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #14660 (PEP 420): Namespace packages. Implemented by Eric Smith.
- Issue #14494: Fix __future__.py and its documentation to note that - Issue #14494: Fix __future__.py and its documentation to note that
absolute imports are the default behavior in 3.0 instead of 2.7. absolute imports are the default behavior in 3.0 instead of 2.7.
Patch by Sven Marnach. Patch by Sven Marnach.

View File

@ -259,6 +259,29 @@ enum zi_module_info {
MI_PACKAGE MI_PACKAGE
}; };
/* Does this path represent a directory?
on error, return < 0
if not a dir, return 0
if a dir, return 1
*/
static int
check_is_directory(ZipImporter *self, PyObject* prefix, PyObject *path)
{
PyObject *dirpath;
PyObject *item;
/* See if this is a "directory". If so, it's eligible to be part
of a namespace package. We test by seeing if the name, with an
appended path separator, exists. */
dirpath = PyUnicode_FromFormat("%U%U%c", prefix, path, SEP);
if (dirpath == NULL)
return -1;
/* If dirpath is present in self->files, we have a directory. */
item = PyDict_GetItem(self->files, dirpath);
Py_DECREF(dirpath);
return item != NULL;
}
/* Return some information about a module. */ /* Return some information about a module. */
static enum zi_module_info static enum zi_module_info
get_module_info(ZipImporter *self, PyObject *fullname) get_module_info(ZipImporter *self, PyObject *fullname)
@ -296,6 +319,46 @@ get_module_info(ZipImporter *self, PyObject *fullname)
return MI_NOT_FOUND; return MI_NOT_FOUND;
} }
/* The guts of "find_loader" and "find_module". Return values:
-1: error
0: no loader or namespace portions found
1: module/package found
2: namespace portion found: *namespace_portion will point to the name
*/
static int
find_loader(ZipImporter *self, PyObject *fullname, PyObject **namespace_portion)
{
enum zi_module_info mi;
*namespace_portion = NULL;
mi = get_module_info(self, fullname);
if (mi == MI_ERROR)
return -1;
if (mi == MI_NOT_FOUND) {
/* Not a module or regular package. See if this is a directory, and
therefore possibly a portion of a namespace package. */
int is_dir = check_is_directory(self, self->prefix, fullname);
if (is_dir < 0)
return -1;
if (is_dir) {
/* This is possibly a portion of a namespace
package. Return the string representing its path,
without a trailing separator. */
*namespace_portion = PyUnicode_FromFormat("%U%c%U%U",
self->archive, SEP,
self->prefix, fullname);
if (*namespace_portion == NULL)
return -1;
return 2;
}
return 0;
}
/* This is a module or package. */
return 1;
}
/* Check whether we can satisfy the import of the module named by /* Check whether we can satisfy the import of the module named by
'fullname'. Return self if we can, None if we can't. */ 'fullname'. Return self if we can, None if we can't. */
static PyObject * static PyObject *
@ -304,21 +367,78 @@ zipimporter_find_module(PyObject *obj, PyObject *args)
ZipImporter *self = (ZipImporter *)obj; ZipImporter *self = (ZipImporter *)obj;
PyObject *path = NULL; PyObject *path = NULL;
PyObject *fullname; PyObject *fullname;
enum zi_module_info mi; PyObject* namespace_portion = NULL;
if (!PyArg_ParseTuple(args, "U|O:zipimporter.find_module", if (!PyArg_ParseTuple(args, "U|O:zipimporter.find_module",
&fullname, &path)) &fullname, &path))
return NULL; goto error;
mi = get_module_info(self, fullname); switch (find_loader(self, fullname, &namespace_portion)) {
if (mi == MI_ERROR) case -1: /* Error */
return NULL; goto error;
if (mi == MI_NOT_FOUND) { case 0: /* Not found, return None */
Py_INCREF(Py_None);
return Py_None;
case 1: /* Return self */
Py_INCREF(self);
return (PyObject *)self;
case 2: /* A namespace portion, but not allowed via
find_module, so return None */
Py_DECREF(namespace_portion);
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
} }
Py_INCREF(self); /* Can't get here. */
return (PyObject *)self; assert(0);
return NULL;
error:
Py_XDECREF(namespace_portion);
return NULL;
}
/* Check whether we can satisfy the import of the module named by
'fullname', or whether it could be a portion of a namespace
package. Return self if we can load it, a string containing the
full path if it's a possible namespace portion, None if we
can't load it. */
static PyObject *
zipimporter_find_loader(PyObject *obj, PyObject *args)
{
ZipImporter *self = (ZipImporter *)obj;
PyObject *path = NULL;
PyObject *fullname;
PyObject *result = NULL;
PyObject *namespace_portion = NULL;
if (!PyArg_ParseTuple(args, "U|O:zipimporter.find_module",
&fullname, &path))
goto error;
switch (find_loader(self, fullname, &namespace_portion)) {
case -1: /* Error */
goto error;
case 0: /* Not found, return (None, []) */
if (!(result = Py_BuildValue("O[]", Py_None)))
goto error;
return result;
case 1: /* Return (self, []) */
if (!(result = Py_BuildValue("O[]", self)))
goto error;
return result;
case 2: /* Return (None, [namespace_portion]) */
if (!(result = Py_BuildValue("O[O]", Py_None, namespace_portion)))
goto error;
return result;
}
/* Can't get here. */
assert(0);
return NULL;
error:
Py_XDECREF(namespace_portion);
Py_XDECREF(result);
return NULL;
} }
/* Load and return the module named by 'fullname'. */ /* Load and return the module named by 'fullname'. */
@ -558,6 +678,16 @@ instance itself if the module was found, or None if it wasn't.\n\
The optional 'path' argument is ignored -- it's there for compatibility\n\ The optional 'path' argument is ignored -- it's there for compatibility\n\
with the importer protocol."); with the importer protocol.");
PyDoc_STRVAR(doc_find_loader,
"find_loader(fullname, path=None) -> self, str or None.\n\
\n\
Search for a module specified by 'fullname'. 'fullname' must be the\n\
fully qualified (dotted) module name. It returns the zipimporter\n\
instance itself if the module was found, a string containing the\n\
full path name if it's possibly a portion of a namespace package,\n\
or None otherwise. The optional 'path' argument is ignored -- it's\n\
there for compatibility with the importer protocol.");
PyDoc_STRVAR(doc_load_module, PyDoc_STRVAR(doc_load_module,
"load_module(fullname) -> module.\n\ "load_module(fullname) -> module.\n\
\n\ \n\
@ -599,6 +729,8 @@ Return the filename for the specified module.");
static PyMethodDef zipimporter_methods[] = { static PyMethodDef zipimporter_methods[] = {
{"find_module", zipimporter_find_module, METH_VARARGS, {"find_module", zipimporter_find_module, METH_VARARGS,
doc_find_module}, doc_find_module},
{"find_loader", zipimporter_find_loader, METH_VARARGS,
doc_find_loader},
{"load_module", zipimporter_load_module, METH_VARARGS, {"load_module", zipimporter_load_module, METH_VARARGS,
doc_load_module}, doc_load_module},
{"get_data", zipimporter_get_data, METH_VARARGS, {"get_data", zipimporter_get_data, METH_VARARGS,

View File

@ -366,8 +366,28 @@ module_dealloc(PyModuleObject *m)
static PyObject * static PyObject *
module_repr(PyModuleObject *m) module_repr(PyModuleObject *m)
{ {
PyObject *name, *filename, *repr; PyObject *name, *filename, *repr, *loader = NULL;
/* See if the module has an __loader__. If it does, give the loader the
* first shot at producing a repr for the module.
*/
if (m->md_dict != NULL) {
loader = PyDict_GetItemString(m->md_dict, "__loader__");
}
if (loader != NULL) {
repr = PyObject_CallMethod(loader, "module_repr", "(O)",
(PyObject *)m, NULL);
if (repr == NULL) {
PyErr_Clear();
}
else {
return repr;
}
}
/* __loader__.module_repr(m) did not provide us with a repr. Next, see if
* the module has an __file__. If it doesn't then use repr(__loader__) if
* it exists, otherwise, just use module.__name__.
*/
name = PyModule_GetNameObject((PyObject *)m); name = PyModule_GetNameObject((PyObject *)m);
if (name == NULL) { if (name == NULL) {
PyErr_Clear(); PyErr_Clear();
@ -378,8 +398,17 @@ module_repr(PyModuleObject *m)
filename = PyModule_GetFilenameObject((PyObject *)m); filename = PyModule_GetFilenameObject((PyObject *)m);
if (filename == NULL) { if (filename == NULL) {
PyErr_Clear(); PyErr_Clear();
repr = PyUnicode_FromFormat("<module %R (built-in)>", name); /* There's no m.__file__, so if there was an __loader__, use that in
* the repr, otherwise, the only thing you can use is m.__name__
*/
if (loader == NULL) {
repr = PyUnicode_FromFormat("<module %R>", name);
}
else {
repr = PyUnicode_FromFormat("<module %R (%R)>", name, loader);
}
} }
/* Finally, use m.__file__ */
else { else {
repr = PyUnicode_FromFormat("<module %R from %R>", name, filename); repr = PyUnicode_FromFormat("<module %R from %R>", name, filename);
Py_DECREF(filename); Py_DECREF(filename);

File diff suppressed because it is too large Load Diff