test_namespace_pkgs.py 9.8 KB
Newer Older
1
import contextlib
2 3 4
from importlib._bootstrap import NamespaceLoader
import importlib.abc
import importlib.machinery
5
import os
6 7 8
import sys
import types
import unittest
9

Brett Cannon's avatar
Brett Cannon committed
10
from test.test_importlib import util
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
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)
Brett Cannon's avatar
Brett Cannon committed
53
    import_context = util.import_state(**kwargs)
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    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())
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255


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')
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282


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')


283 284
class ModuleAndNamespacePackageInSameDir(NamespacePackageTest):
    paths = ['module_and_namespace_package']
285 286

    def test_module_before_namespace_package(self):
287 288
        # Make sure we find the module in preference to the
        #  namespace package.
289 290 291 292
        import a_test
        self.assertEqual(a_test.attr, 'in module')


293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
class ABCTests(unittest.TestCase):

    def setUp(self):
        self.loader = NamespaceLoader('foo', ['pkg'],
                                      importlib.machinery.PathFinder)

    def test_is_package(self):
        self.assertTrue(self.loader.is_package('foo'))

    def test_get_code(self):
        self.assertTrue(isinstance(self.loader.get_code('foo'), types.CodeType))

    def test_get_source(self):
        self.assertEqual(self.loader.get_source('foo'), '')

    def test_abc_isinstance(self):
        self.assertTrue(isinstance(self.loader, importlib.abc.InspectLoader))
310 311 312


if __name__ == "__main__":
313
    unittest.main()