• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import contextlib
2import importlib
3import os
4import sys
5import unittest
6import warnings
7
8from test.test_importlib import util
9
10# needed tests:
11#
12# need to test when nested, so that the top-level path isn't sys.path
13# need to test dynamic path detection, both at top-level and nested
14# with dynamic path, check when a loader is returned on path reload (that is,
15#  trying to switch from a namespace package to a regular package)
16
17
18@contextlib.contextmanager
19def sys_modules_context():
20    """
21    Make sure sys.modules is the same object and has the same content
22    when exiting the context as when entering.
23
24    Similar to importlib.test.util.uncache, but doesn't require explicit
25    names.
26    """
27    sys_modules_saved = sys.modules
28    sys_modules_copy = sys.modules.copy()
29    try:
30        yield
31    finally:
32        sys.modules = sys_modules_saved
33        sys.modules.clear()
34        sys.modules.update(sys_modules_copy)
35
36
37@contextlib.contextmanager
38def namespace_tree_context(**kwargs):
39    """
40    Save import state and sys.modules cache and restore it on exit.
41    Typical usage:
42
43    >>> with namespace_tree_context(path=['/tmp/xxyy/portion1',
44    ...         '/tmp/xxyy/portion2']):
45    ...     pass
46    """
47    # use default meta_path and path_hooks unless specified otherwise
48    kwargs.setdefault('meta_path', sys.meta_path)
49    kwargs.setdefault('path_hooks', sys.path_hooks)
50    import_context = util.import_state(**kwargs)
51    with import_context, sys_modules_context():
52        yield
53
54class NamespacePackageTest(unittest.TestCase):
55    """
56    Subclasses should define self.root and self.paths (under that root)
57    to be added to sys.path.
58    """
59    root = os.path.join(os.path.dirname(__file__), 'namespace_pkgs')
60
61    def setUp(self):
62        self.resolved_paths = [
63            os.path.join(self.root, path) for path in self.paths
64        ]
65        self.ctx = namespace_tree_context(path=self.resolved_paths)
66        self.ctx.__enter__()
67
68    def tearDown(self):
69        # TODO: will we ever want to pass exc_info to __exit__?
70        self.ctx.__exit__(None, None, None)
71
72
73class SingleNamespacePackage(NamespacePackageTest):
74    paths = ['portion1']
75
76    def test_simple_package(self):
77        import foo.one
78        self.assertEqual(foo.one.attr, 'portion1 foo one')
79
80    def test_cant_import_other(self):
81        with self.assertRaises(ImportError):
82            import foo.two
83
84    def test_module_repr(self):
85        import foo.one
86        with warnings.catch_warnings():
87            warnings.simplefilter("ignore")
88            self.assertEqual(foo.__spec__.loader.module_repr(foo),
89                            "<module 'foo' (namespace)>")
90
91
92class DynamicPathNamespacePackage(NamespacePackageTest):
93    paths = ['portion1']
94
95    def test_dynamic_path(self):
96        # Make sure only 'foo.one' can be imported
97        import foo.one
98        self.assertEqual(foo.one.attr, 'portion1 foo one')
99
100        with self.assertRaises(ImportError):
101            import foo.two
102
103        # Now modify sys.path
104        sys.path.append(os.path.join(self.root, 'portion2'))
105
106        # And make sure foo.two is now importable
107        import foo.two
108        self.assertEqual(foo.two.attr, 'portion2 foo two')
109
110
111class CombinedNamespacePackages(NamespacePackageTest):
112    paths = ['both_portions']
113
114    def test_imports(self):
115        import foo.one
116        import foo.two
117        self.assertEqual(foo.one.attr, 'both_portions foo one')
118        self.assertEqual(foo.two.attr, 'both_portions foo two')
119
120
121class SeparatedNamespacePackages(NamespacePackageTest):
122    paths = ['portion1', 'portion2']
123
124    def test_imports(self):
125        import foo.one
126        import foo.two
127        self.assertEqual(foo.one.attr, 'portion1 foo one')
128        self.assertEqual(foo.two.attr, 'portion2 foo two')
129
130
131class SeparatedOverlappingNamespacePackages(NamespacePackageTest):
132    paths = ['portion1', 'both_portions']
133
134    def test_first_path_wins(self):
135        import foo.one
136        import foo.two
137        self.assertEqual(foo.one.attr, 'portion1 foo one')
138        self.assertEqual(foo.two.attr, 'both_portions foo two')
139
140    def test_first_path_wins_again(self):
141        sys.path.reverse()
142        import foo.one
143        import foo.two
144        self.assertEqual(foo.one.attr, 'both_portions foo one')
145        self.assertEqual(foo.two.attr, 'both_portions foo two')
146
147    def test_first_path_wins_importing_second_first(self):
148        import foo.two
149        import foo.one
150        self.assertEqual(foo.one.attr, 'portion1 foo one')
151        self.assertEqual(foo.two.attr, 'both_portions foo two')
152
153
154class SingleZipNamespacePackage(NamespacePackageTest):
155    paths = ['top_level_portion1.zip']
156
157    def test_simple_package(self):
158        import foo.one
159        self.assertEqual(foo.one.attr, 'portion1 foo one')
160
161    def test_cant_import_other(self):
162        with self.assertRaises(ImportError):
163            import foo.two
164
165
166class SeparatedZipNamespacePackages(NamespacePackageTest):
167    paths = ['top_level_portion1.zip', 'portion2']
168
169    def test_imports(self):
170        import foo.one
171        import foo.two
172        self.assertEqual(foo.one.attr, 'portion1 foo one')
173        self.assertEqual(foo.two.attr, 'portion2 foo two')
174        self.assertIn('top_level_portion1.zip', foo.one.__file__)
175        self.assertNotIn('.zip', foo.two.__file__)
176
177
178class SingleNestedZipNamespacePackage(NamespacePackageTest):
179    paths = ['nested_portion1.zip/nested_portion1']
180
181    def test_simple_package(self):
182        import foo.one
183        self.assertEqual(foo.one.attr, 'portion1 foo one')
184
185    def test_cant_import_other(self):
186        with self.assertRaises(ImportError):
187            import foo.two
188
189
190class SeparatedNestedZipNamespacePackages(NamespacePackageTest):
191    paths = ['nested_portion1.zip/nested_portion1', 'portion2']
192
193    def test_imports(self):
194        import foo.one
195        import foo.two
196        self.assertEqual(foo.one.attr, 'portion1 foo one')
197        self.assertEqual(foo.two.attr, 'portion2 foo two')
198        fn = os.path.join('nested_portion1.zip', 'nested_portion1')
199        self.assertIn(fn, foo.one.__file__)
200        self.assertNotIn('.zip', foo.two.__file__)
201
202
203class LegacySupport(NamespacePackageTest):
204    paths = ['not_a_namespace_pkg', 'portion1', 'portion2', 'both_portions']
205
206    def test_non_namespace_package_takes_precedence(self):
207        import foo.one
208        with self.assertRaises(ImportError):
209            import foo.two
210        self.assertIn('__init__', foo.__file__)
211        self.assertNotIn('namespace', str(foo.__loader__).lower())
212
213
214class DynamicPathCalculation(NamespacePackageTest):
215    paths = ['project1', 'project2']
216
217    def test_project3_fails(self):
218        import parent.child.one
219        self.assertEqual(len(parent.__path__), 2)
220        self.assertEqual(len(parent.child.__path__), 2)
221        import parent.child.two
222        self.assertEqual(len(parent.__path__), 2)
223        self.assertEqual(len(parent.child.__path__), 2)
224
225        self.assertEqual(parent.child.one.attr, 'parent child one')
226        self.assertEqual(parent.child.two.attr, 'parent child two')
227
228        with self.assertRaises(ImportError):
229            import parent.child.three
230
231        self.assertEqual(len(parent.__path__), 2)
232        self.assertEqual(len(parent.child.__path__), 2)
233
234    def test_project3_succeeds(self):
235        import parent.child.one
236        self.assertEqual(len(parent.__path__), 2)
237        self.assertEqual(len(parent.child.__path__), 2)
238        import parent.child.two
239        self.assertEqual(len(parent.__path__), 2)
240        self.assertEqual(len(parent.child.__path__), 2)
241
242        self.assertEqual(parent.child.one.attr, 'parent child one')
243        self.assertEqual(parent.child.two.attr, 'parent child two')
244
245        with self.assertRaises(ImportError):
246            import parent.child.three
247
248        # now add project3
249        sys.path.append(os.path.join(self.root, 'project3'))
250        import parent.child.three
251
252        # the paths dynamically get longer, to include the new directories
253        self.assertEqual(len(parent.__path__), 3)
254        self.assertEqual(len(parent.child.__path__), 3)
255
256        self.assertEqual(parent.child.three.attr, 'parent child three')
257
258
259class ZipWithMissingDirectory(NamespacePackageTest):
260    paths = ['missing_directory.zip']
261
262    @unittest.expectedFailure
263    def test_missing_directory(self):
264        # This will fail because missing_directory.zip contains:
265        #   Length      Date    Time    Name
266        # ---------  ---------- -----   ----
267        #        29  2012-05-03 18:13   foo/one.py
268        #         0  2012-05-03 20:57   bar/
269        #        38  2012-05-03 20:57   bar/two.py
270        # ---------                     -------
271        #        67                     3 files
272
273        # Because there is no 'foo/', the zipimporter currently doesn't
274        #  know that foo is a namespace package
275
276        import foo.one
277
278    def test_present_directory(self):
279        # This succeeds because there is a "bar/" in the zip file
280        import bar.two
281        self.assertEqual(bar.two.attr, 'missing_directory foo two')
282
283
284class ModuleAndNamespacePackageInSameDir(NamespacePackageTest):
285    paths = ['module_and_namespace_package']
286
287    def test_module_before_namespace_package(self):
288        # Make sure we find the module in preference to the
289        #  namespace package.
290        import a_test
291        self.assertEqual(a_test.attr, 'in module')
292
293
294class ReloadTests(NamespacePackageTest):
295    paths = ['portion1']
296
297    def test_simple_package(self):
298        import foo.one
299        foo = importlib.reload(foo)
300        self.assertEqual(foo.one.attr, 'portion1 foo one')
301
302    def test_cant_import_other(self):
303        import foo
304        with self.assertRaises(ImportError):
305            import foo.two
306        foo = importlib.reload(foo)
307        with self.assertRaises(ImportError):
308            import foo.two
309
310    def test_dynamic_path(self):
311        import foo.one
312        with self.assertRaises(ImportError):
313            import foo.two
314
315        # Now modify sys.path and reload.
316        sys.path.append(os.path.join(self.root, 'portion2'))
317        foo = importlib.reload(foo)
318
319        # And make sure foo.two is now importable
320        import foo.two
321        self.assertEqual(foo.two.attr, 'portion2 foo two')
322
323
324class LoaderTests(NamespacePackageTest):
325    paths = ['portion1']
326
327    def test_namespace_loader_consistency(self):
328        # bpo-32303
329        import foo
330        self.assertEqual(foo.__loader__, foo.__spec__.loader)
331        self.assertIsNotNone(foo.__loader__)
332
333    def test_namespace_origin_consistency(self):
334        # bpo-32305
335        import foo
336        self.assertIsNone(foo.__spec__.origin)
337        self.assertIsNone(foo.__file__)
338
339    def test_path_indexable(self):
340        # bpo-35843
341        import foo
342        expected_path = os.path.join(self.root, 'portion1', 'foo')
343        self.assertEqual(foo.__path__[0], expected_path)
344
345
346if __name__ == "__main__":
347    unittest.main()
348