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