issue 14660: Implement PEP 420, namespace packages.
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
new file mode 100644
index 0000000..836a910
--- /dev/null
+++ b/Lib/test/namespace_pkgs/missing_directory.zip
Binary files differ
diff --git a/Lib/test/namespace_pkgs/nested_portion1.zip b/Lib/test/namespace_pkgs/nested_portion1.zip
new file mode 100644
index 0000000..8d22406
--- /dev/null
+++ b/Lib/test/namespace_pkgs/nested_portion1.zip
Binary files differ
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
new file mode 100644
index 0000000..3b866c9
--- /dev/null
+++ b/Lib/test/namespace_pkgs/top_level_portion1.zip
Binary files differ
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 @@
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 @@
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 @@
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 @@
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 @@
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,12 @@
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 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):
- self.create_submodule(self.dirname_0, self.pkgname, 'bar', 0)
- self.create_submodule(self.dirname_1, self.pkgname, 'baz', 1)
+ 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
@@ -180,8 +171,45 @@
# 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']
+
+ 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