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