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