1import os
2import errno
3import importlib.machinery
4import py_compile
5import shutil
6import unittest
7import tempfile
8
9from test import support
10
11import modulefinder
12
13# Each test description is a list of 5 items:
14#
15# 1. a module name that will be imported by modulefinder
16# 2. a list of module names that modulefinder is required to find
17# 3. a list of module names that modulefinder should complain
18#    about because they are not found
19# 4. a list of module names that modulefinder should complain
20#    about because they MAY be not found
21# 5. a string specifying packages to create; the format is obvious imo.
22#
23# Each package will be created in test_dir, and test_dir will be
24# removed after the tests again.
25# Modulefinder searches in a path that contains test_dir, plus
26# the standard Lib directory.
27
28maybe_test = [
29    "a.module",
30    ["a", "a.module", "sys",
31     "b"],
32    ["c"], ["b.something"],
33    """\
34a/__init__.py
35a/module.py
36                                from b import something
37                                from c import something
38b/__init__.py
39                                from sys import *
40""",
41]
42
43maybe_test_new = [
44    "a.module",
45    ["a", "a.module", "sys",
46     "b", "__future__"],
47    ["c"], ["b.something"],
48    """\
49a/__init__.py
50a/module.py
51                                from b import something
52                                from c import something
53b/__init__.py
54                                from __future__ import absolute_import
55                                from sys import *
56"""]
57
58package_test = [
59    "a.module",
60    ["a", "a.b", "a.c", "a.module", "mymodule", "sys"],
61    ["blahblah", "c"], [],
62    """\
63mymodule.py
64a/__init__.py
65                                import blahblah
66                                from a import b
67                                import c
68a/module.py
69                                import sys
70                                from a import b as x
71                                from a.c import sillyname
72a/b.py
73a/c.py
74                                from a.module import x
75                                import mymodule as sillyname
76                                from sys import version_info
77"""]
78
79absolute_import_test = [
80    "a.module",
81    ["a", "a.module",
82     "b", "b.x", "b.y", "b.z",
83     "__future__", "sys", "gc"],
84    ["blahblah", "z"], [],
85    """\
86mymodule.py
87a/__init__.py
88a/module.py
89                                from __future__ import absolute_import
90                                import sys # sys
91                                import blahblah # fails
92                                import gc # gc
93                                import b.x # b.x
94                                from b import y # b.y
95                                from b.z import * # b.z.*
96a/gc.py
97a/sys.py
98                                import mymodule
99a/b/__init__.py
100a/b/x.py
101a/b/y.py
102a/b/z.py
103b/__init__.py
104                                import z
105b/unused.py
106b/x.py
107b/y.py
108b/z.py
109"""]
110
111relative_import_test = [
112    "a.module",
113    ["__future__",
114     "a", "a.module",
115     "a.b", "a.b.y", "a.b.z",
116     "a.b.c", "a.b.c.moduleC",
117     "a.b.c.d", "a.b.c.e",
118     "a.b.x",
119     "gc"],
120    [], [],
121    """\
122mymodule.py
123a/__init__.py
124                                from .b import y, z # a.b.y, a.b.z
125a/module.py
126                                from __future__ import absolute_import # __future__
127                                import gc # gc
128a/gc.py
129a/sys.py
130a/b/__init__.py
131                                from ..b import x # a.b.x
132                                #from a.b.c import moduleC
133                                from .c import moduleC # a.b.moduleC
134a/b/x.py
135a/b/y.py
136a/b/z.py
137a/b/g.py
138a/b/c/__init__.py
139                                from ..c import e # a.b.c.e
140a/b/c/moduleC.py
141                                from ..c import d # a.b.c.d
142a/b/c/d.py
143a/b/c/e.py
144a/b/c/x.py
145"""]
146
147relative_import_test_2 = [
148    "a.module",
149    ["a", "a.module",
150     "a.sys",
151     "a.b", "a.b.y", "a.b.z",
152     "a.b.c", "a.b.c.d",
153     "a.b.c.e",
154     "a.b.c.moduleC",
155     "a.b.c.f",
156     "a.b.x",
157     "a.another"],
158    [], [],
159    """\
160mymodule.py
161a/__init__.py
162                                from . import sys # a.sys
163a/another.py
164a/module.py
165                                from .b import y, z # a.b.y, a.b.z
166a/gc.py
167a/sys.py
168a/b/__init__.py
169                                from .c import moduleC # a.b.c.moduleC
170                                from .c import d # a.b.c.d
171a/b/x.py
172a/b/y.py
173a/b/z.py
174a/b/c/__init__.py
175                                from . import e # a.b.c.e
176a/b/c/moduleC.py
177                                #
178                                from . import f   # a.b.c.f
179                                from .. import x  # a.b.x
180                                from ... import another # a.another
181a/b/c/d.py
182a/b/c/e.py
183a/b/c/f.py
184"""]
185
186relative_import_test_3 = [
187    "a.module",
188    ["a", "a.module"],
189    ["a.bar"],
190    [],
191    """\
192a/__init__.py
193                                def foo(): pass
194a/module.py
195                                from . import foo
196                                from . import bar
197"""]
198
199relative_import_test_4 = [
200    "a.module",
201    ["a", "a.module"],
202    [],
203    [],
204    """\
205a/__init__.py
206                                def foo(): pass
207a/module.py
208                                from . import *
209"""]
210
211bytecode_test = [
212    "a",
213    ["a"],
214    [],
215    [],
216    ""
217]
218
219syntax_error_test = [
220    "a.module",
221    ["a", "a.module", "b"],
222    ["b.module"], [],
223    """\
224a/__init__.py
225a/module.py
226                                import b.module
227b/__init__.py
228b/module.py
229                                ?  # SyntaxError: invalid syntax
230"""]
231
232
233same_name_as_bad_test = [
234    "a.module",
235    ["a", "a.module", "b", "b.c"],
236    ["c"], [],
237    """\
238a/__init__.py
239a/module.py
240                                import c
241                                from b import c
242b/__init__.py
243b/c.py
244"""]
245
246coding_default_utf8_test = [
247    "a_utf8",
248    ["a_utf8", "b_utf8"],
249    [], [],
250    """\
251a_utf8.py
252                                # use the default of utf8
253                                print('Unicode test A code point 2090 \u2090 that is not valid in cp1252')
254                                import b_utf8
255b_utf8.py
256                                # use the default of utf8
257                                print('Unicode test B code point 2090 \u2090 that is not valid in cp1252')
258"""]
259
260coding_explicit_utf8_test = [
261    "a_utf8",
262    ["a_utf8", "b_utf8"],
263    [], [],
264    """\
265a_utf8.py
266                                # coding=utf8
267                                print('Unicode test A code point 2090 \u2090 that is not valid in cp1252')
268                                import b_utf8
269b_utf8.py
270                                # use the default of utf8
271                                print('Unicode test B code point 2090 \u2090 that is not valid in cp1252')
272"""]
273
274coding_explicit_cp1252_test = [
275    "a_cp1252",
276    ["a_cp1252", "b_utf8"],
277    [], [],
278    b"""\
279a_cp1252.py
280                                # coding=cp1252
281                                # 0xe2 is not allowed in utf8
282                                print('CP1252 test P\xe2t\xe9')
283                                import b_utf8
284""" + """\
285b_utf8.py
286                                # use the default of utf8
287                                print('Unicode test A code point 2090 \u2090 that is not valid in cp1252')
288""".encode('utf-8')]
289
290def open_file(path):
291    dirname = os.path.dirname(path)
292    try:
293        os.makedirs(dirname)
294    except OSError as e:
295        if e.errno != errno.EEXIST:
296            raise
297    return open(path, 'wb')
298
299
300def create_package(test_dir, source):
301    ofi = None
302    try:
303        for line in source.splitlines():
304            if type(line) != bytes:
305                line = line.encode('utf-8')
306            if line.startswith(b' ') or line.startswith(b'\t'):
307                ofi.write(line.strip() + b'\n')
308            else:
309                if ofi:
310                    ofi.close()
311                if type(line) == bytes:
312                    line = line.decode('utf-8')
313                ofi = open_file(os.path.join(test_dir, line.strip()))
314    finally:
315        if ofi:
316            ofi.close()
317
318class ModuleFinderTest(unittest.TestCase):
319    def setUp(self):
320        self.test_dir = tempfile.mkdtemp()
321        self.test_path = [self.test_dir, os.path.dirname(tempfile.__file__)]
322
323    def tearDown(self):
324        shutil.rmtree(self.test_dir)
325
326    def _do_test(self, info, report=False, debug=0, replace_paths=[], modulefinder_class=modulefinder.ModuleFinder):
327        import_this, modules, missing, maybe_missing, source = info
328        create_package(self.test_dir, source)
329        mf = modulefinder_class(path=self.test_path, debug=debug,
330                                        replace_paths=replace_paths)
331        mf.import_hook(import_this)
332        if report:
333            mf.report()
334##            # This wouldn't work in general when executed several times:
335##            opath = sys.path[:]
336##            sys.path = self.test_path
337##            try:
338##                __import__(import_this)
339##            except:
340##                import traceback; traceback.print_exc()
341##            sys.path = opath
342##            return
343        modules = sorted(set(modules))
344        found = sorted(mf.modules)
345        # check if we found what we expected, not more, not less
346        self.assertEqual(found, modules)
347
348        # check for missing and maybe missing modules
349        bad, maybe = mf.any_missing_maybe()
350        self.assertEqual(bad, missing)
351        self.assertEqual(maybe, maybe_missing)
352
353    def test_package(self):
354        self._do_test(package_test)
355
356    def test_maybe(self):
357        self._do_test(maybe_test)
358
359    def test_maybe_new(self):
360        self._do_test(maybe_test_new)
361
362    def test_absolute_imports(self):
363        self._do_test(absolute_import_test)
364
365    def test_relative_imports(self):
366        self._do_test(relative_import_test)
367
368    def test_relative_imports_2(self):
369        self._do_test(relative_import_test_2)
370
371    def test_relative_imports_3(self):
372        self._do_test(relative_import_test_3)
373
374    def test_relative_imports_4(self):
375        self._do_test(relative_import_test_4)
376
377    def test_syntax_error(self):
378        self._do_test(syntax_error_test)
379
380    def test_same_name_as_bad(self):
381        self._do_test(same_name_as_bad_test)
382
383    def test_bytecode(self):
384        base_path = os.path.join(self.test_dir, 'a')
385        source_path = base_path + importlib.machinery.SOURCE_SUFFIXES[0]
386        bytecode_path = base_path + importlib.machinery.BYTECODE_SUFFIXES[0]
387        with open_file(source_path) as file:
388            file.write('testing_modulefinder = True\n'.encode('utf-8'))
389        py_compile.compile(source_path, cfile=bytecode_path)
390        os.remove(source_path)
391        self._do_test(bytecode_test)
392
393    def test_replace_paths(self):
394        old_path = os.path.join(self.test_dir, 'a', 'module.py')
395        new_path = os.path.join(self.test_dir, 'a', 'spam.py')
396        with support.captured_stdout() as output:
397            self._do_test(maybe_test, debug=2,
398                          replace_paths=[(old_path, new_path)])
399        output = output.getvalue()
400        expected = "co_filename %r changed to %r" % (old_path, new_path)
401        self.assertIn(expected, output)
402
403    def test_extended_opargs(self):
404        extended_opargs_test = [
405            "a",
406            ["a", "b"],
407            [], [],
408            """\
409a.py
410                                %r
411                                import b
412b.py
413""" % list(range(2**16))]  # 2**16 constants
414        self._do_test(extended_opargs_test)
415
416    def test_coding_default_utf8(self):
417        self._do_test(coding_default_utf8_test)
418
419    def test_coding_explicit_utf8(self):
420        self._do_test(coding_explicit_utf8_test)
421
422    def test_coding_explicit_cp1252(self):
423        self._do_test(coding_explicit_cp1252_test)
424
425    def test_load_module_api(self):
426        class CheckLoadModuleApi(modulefinder.ModuleFinder):
427            def __init__(self, *args, **kwds):
428                super().__init__(*args, **kwds)
429
430            def load_module(self, fqname, fp, pathname, file_info):
431                # confirm that the fileinfo is a tuple of 3 elements
432                suffix, mode, type = file_info
433                return super().load_module(fqname, fp, pathname, file_info)
434
435        self._do_test(absolute_import_test, modulefinder_class=CheckLoadModuleApi)
436
437if __name__ == "__main__":
438    unittest.main()
439