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