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
45maybe_test_new = [
46    "a.module",
47    ["a", "a.module", "sys",
48     "b", "__future__"],
49    ["c"], ["b.something"],
50    """\
51a/__init__.py
52a/module.py
53                                from b import something
54                                from c import something
55b/__init__.py
56                                from __future__ import absolute_import
57                                from sys import *
58"""]
59
60package_test = [
61    "a.module",
62    ["a", "a.b", "a.c", "a.module", "mymodule", "sys"],
63    ["blahblah", "c"], [],
64    """\
65mymodule.py
66a/__init__.py
67                                import blahblah
68                                from a import b
69                                import c
70a/module.py
71                                import sys
72                                from a import b as x
73                                from a.c import sillyname
74a/b.py
75a/c.py
76                                from a.module import x
77                                import mymodule as sillyname
78                                from sys import version_info
79"""]
80
81absolute_import_test = [
82    "a.module",
83    ["a", "a.module",
84     "b", "b.x", "b.y", "b.z",
85     "__future__", "sys", "gc"],
86    ["blahblah", "z"], [],
87    """\
88mymodule.py
89a/__init__.py
90a/module.py
91                                from __future__ import absolute_import
92                                import sys # sys
93                                import blahblah # fails
94                                import gc # gc
95                                import b.x # b.x
96                                from b import y # b.y
97                                from b.z import * # b.z.*
98a/gc.py
99a/sys.py
100                                import mymodule
101a/b/__init__.py
102a/b/x.py
103a/b/y.py
104a/b/z.py
105b/__init__.py
106                                import z
107b/unused.py
108b/x.py
109b/y.py
110b/z.py
111"""]
112
113relative_import_test = [
114    "a.module",
115    ["__future__",
116     "a", "a.module",
117     "a.b", "a.b.y", "a.b.z",
118     "a.b.c", "a.b.c.moduleC",
119     "a.b.c.d", "a.b.c.e",
120     "a.b.x",
121     "gc"],
122    [], [],
123    """\
124mymodule.py
125a/__init__.py
126                                from .b import y, z # a.b.y, a.b.z
127a/module.py
128                                from __future__ import absolute_import # __future__
129                                import gc # gc
130a/gc.py
131a/sys.py
132a/b/__init__.py
133                                from ..b import x # a.b.x
134                                #from a.b.c import moduleC
135                                from .c import moduleC # a.b.moduleC
136a/b/x.py
137a/b/y.py
138a/b/z.py
139a/b/g.py
140a/b/c/__init__.py
141                                from ..c import e # a.b.c.e
142a/b/c/moduleC.py
143                                from ..c import d # a.b.c.d
144a/b/c/d.py
145a/b/c/e.py
146a/b/c/x.py
147"""]
148
149relative_import_test_2 = [
150    "a.module",
151    ["a", "a.module",
152     "a.sys",
153     "a.b", "a.b.y", "a.b.z",
154     "a.b.c", "a.b.c.d",
155     "a.b.c.e",
156     "a.b.c.moduleC",
157     "a.b.c.f",
158     "a.b.x",
159     "a.another"],
160    [], [],
161    """\
162mymodule.py
163a/__init__.py
164                                from . import sys # a.sys
165a/another.py
166a/module.py
167                                from .b import y, z # a.b.y, a.b.z
168a/gc.py
169a/sys.py
170a/b/__init__.py
171                                from .c import moduleC # a.b.c.moduleC
172                                from .c import d # a.b.c.d
173a/b/x.py
174a/b/y.py
175a/b/z.py
176a/b/c/__init__.py
177                                from . import e # a.b.c.e
178a/b/c/moduleC.py
179                                #
180                                from . import f   # a.b.c.f
181                                from .. import x  # a.b.x
182                                from ... import another # a.another
183a/b/c/d.py
184a/b/c/e.py
185a/b/c/f.py
186"""]
187
188relative_import_test_3 = [
189    "a.module",
190    ["a", "a.module"],
191    ["a.bar"],
192    [],
193    """\
194a/__init__.py
195                                def foo(): pass
196a/module.py
197                                from . import foo
198                                from . import bar
199"""]
200
201relative_import_test_4 = [
202    "a.module",
203    ["a", "a.module"],
204    [],
205    [],
206    """\
207a/__init__.py
208                                def foo(): pass
209a/module.py
210                                from . import *
211"""]
212
213bytecode_test = [
214    "a",
215    ["a"],
216    [],
217    [],
218    ""
219]
220
221
222def open_file(path):
223    dirname = os.path.dirname(path)
224    try:
225        os.makedirs(dirname)
226    except OSError as e:
227        if e.errno != errno.EEXIST:
228            raise
229    return open(path, "w")
230
231
232def create_package(source):
233    ofi = None
234    try:
235        for line in source.splitlines():
236            if line.startswith(" ") or line.startswith("\t"):
237                ofi.write(line.strip() + "\n")
238            else:
239                if ofi:
240                    ofi.close()
241                ofi = open_file(os.path.join(TEST_DIR, line.strip()))
242    finally:
243        if ofi:
244            ofi.close()
245
246
247class ModuleFinderTest(unittest.TestCase):
248    def _do_test(self, info, report=False, debug=0, replace_paths=[]):
249        import_this, modules, missing, maybe_missing, source = info
250        create_package(source)
251        try:
252            mf = modulefinder.ModuleFinder(path=TEST_PATH, debug=debug,
253                                           replace_paths=replace_paths)
254            mf.import_hook(import_this)
255            if report:
256                mf.report()
257##                # This wouldn't work in general when executed several times:
258##                opath = sys.path[:]
259##                sys.path = TEST_PATH
260##                try:
261##                    __import__(import_this)
262##                except:
263##                    import traceback; traceback.print_exc()
264##                sys.path = opath
265##                return
266            modules = sorted(set(modules))
267            found = sorted(mf.modules)
268            # check if we found what we expected, not more, not less
269            self.assertEqual(found, modules)
270
271            # check for missing and maybe missing modules
272            bad, maybe = mf.any_missing_maybe()
273            self.assertEqual(bad, missing)
274            self.assertEqual(maybe, maybe_missing)
275        finally:
276            shutil.rmtree(TEST_DIR)
277
278    def test_package(self):
279        self._do_test(package_test)
280
281    def test_maybe(self):
282        self._do_test(maybe_test)
283
284    def test_maybe_new(self):
285        self._do_test(maybe_test_new)
286
287    def test_absolute_imports(self):
288        self._do_test(absolute_import_test)
289
290    def test_relative_imports(self):
291        self._do_test(relative_import_test)
292
293    def test_relative_imports_2(self):
294        self._do_test(relative_import_test_2)
295
296    def test_relative_imports_3(self):
297        self._do_test(relative_import_test_3)
298
299    def test_relative_imports_4(self):
300        self._do_test(relative_import_test_4)
301
302    def test_bytecode(self):
303        base_path = os.path.join(TEST_DIR, 'a')
304        source_path = base_path + importlib.machinery.SOURCE_SUFFIXES[0]
305        bytecode_path = base_path + importlib.machinery.BYTECODE_SUFFIXES[0]
306        with open_file(source_path) as file:
307            file.write('testing_modulefinder = True\n')
308        py_compile.compile(source_path, cfile=bytecode_path)
309        os.remove(source_path)
310        self._do_test(bytecode_test)
311
312    def test_replace_paths(self):
313        old_path = os.path.join(TEST_DIR, 'a', 'module.py')
314        new_path = os.path.join(TEST_DIR, 'a', 'spam.py')
315        with support.captured_stdout() as output:
316            self._do_test(maybe_test, debug=2,
317                          replace_paths=[(old_path, new_path)])
318        output = output.getvalue()
319        expected = "co_filename %r changed to %r" % (old_path, new_path)
320        self.assertIn(expected, output)
321
322    def test_extended_opargs(self):
323        extended_opargs_test = [
324            "a",
325            ["a", "b"],
326            [], [],
327            """\
328a.py
329                                %r
330                                import b
331b.py
332""" % list(range(2**16))]  # 2**16 constants
333        self._do_test(extended_opargs_test)
334
335
336if __name__ == "__main__":
337    unittest.main()
338