File size: 9,588 Bytes
6a86ad5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python

"""
Import diagnostics. Run bin/diagnose_imports.py --help for details.
"""

from __future__ import annotations

if __name__ == "__main__":

    import sys
    import inspect
    import builtins

    import optparse

    from os.path import abspath, dirname, join, normpath
    this_file = abspath(__file__)
    sympy_dir = join(dirname(this_file), '..', '..', '..')
    sympy_dir = normpath(sympy_dir)
    sys.path.insert(0, sympy_dir)

    option_parser = optparse.OptionParser(
        usage=
            "Usage: %prog option [options]\n"
            "\n"
            "Import analysis for imports between SymPy modules.")
    option_group = optparse.OptionGroup(
        option_parser,
        'Analysis options',
        'Options that define what to do. Exactly one of these must be given.')
    option_group.add_option(
        '--problems',
        help=
            'Print all import problems, that is: '
            'If an import pulls in a package instead of a module '
            '(e.g. sympy.core instead of sympy.core.add); ' # see ##PACKAGE##
            'if it imports a symbol that is already present; ' # see ##DUPLICATE##
            'if it imports a symbol '
            'from somewhere other than the defining module.', # see ##ORIGIN##
        action='count')
    option_group.add_option(
        '--origins',
        help=
            'For each imported symbol in each module, '
            'print the module that defined it. '
            '(This is useful for import refactoring.)',
        action='count')
    option_parser.add_option_group(option_group)
    option_group = optparse.OptionGroup(
        option_parser,
        'Sort options',
        'These options define the sort order for output lines. '
        'At most one of these options is allowed. '
        'Unsorted output will reflect the order in which imports happened.')
    option_group.add_option(
        '--by-importer',
        help='Sort output lines by name of importing module.',
        action='count')
    option_group.add_option(
        '--by-origin',
        help='Sort output lines by name of imported module.',
        action='count')
    option_parser.add_option_group(option_group)
    (options, args) = option_parser.parse_args()
    if args:
        option_parser.error(
            'Unexpected arguments %s (try %s --help)' % (args, sys.argv[0]))
    if options.problems > 1:
        option_parser.error('--problems must not be given more than once.')
    if options.origins > 1:
        option_parser.error('--origins must not be given more than once.')
    if options.by_importer > 1:
        option_parser.error('--by-importer must not be given more than once.')
    if options.by_origin > 1:
        option_parser.error('--by-origin must not be given more than once.')
    options.problems = options.problems == 1
    options.origins = options.origins == 1
    options.by_importer = options.by_importer == 1
    options.by_origin = options.by_origin == 1
    if not options.problems and not options.origins:
        option_parser.error(
            'At least one of --problems and --origins is required')
    if options.problems and options.origins:
        option_parser.error(
            'At most one of --problems and --origins is allowed')
    if options.by_importer and options.by_origin:
        option_parser.error(
            'At most one of --by-importer and --by-origin is allowed')
    options.by_process = not options.by_importer and not options.by_origin

    builtin_import = builtins.__import__

    class Definition:
        """Information about a symbol's definition."""
        def __init__(self, name, value, definer):
            self.name = name
            self.value = value
            self.definer = definer
        def __hash__(self):
            return hash(self.name)
        def __eq__(self, other):
            return self.name == other.name and self.value == other.value
        def __ne__(self, other):
            return not (self == other)
        def __repr__(self):
            return 'Definition(%s, ..., %s)' % (
                repr(self.name), repr(self.definer))

    # Maps each function/variable to name of module to define it
    symbol_definers: dict[Definition, str] = {}

    def in_module(a, b):
        """Is a the same module as or a submodule of b?"""
        return a == b or a != None and b != None and a.startswith(b + '.')

    def relevant(module):
        """Is module relevant for import checking?

        Only imports between relevant modules will be checked."""
        return in_module(module, 'sympy')

    sorted_messages = []

    def msg(msg, *args):
        global options, sorted_messages
        if options.by_process:
            print(msg % args)
        else:
            sorted_messages.append(msg % args)

    def tracking_import(module, globals=globals(), locals=[], fromlist=None, level=-1):
        """__import__ wrapper - does not change imports at all, but tracks them.

        Default order is implemented by doing output directly.
        All other orders are implemented by collecting output information into
        a sorted list that will be emitted after all imports are processed.

        Indirect imports can only occur after the requested symbol has been
        imported directly (because the indirect import would not have a module
        to pick the symbol up from).
        So this code detects indirect imports by checking whether the symbol in
        question was already imported.

        Keeps the semantics of __import__ unchanged."""
        global options, symbol_definers
        caller_frame = inspect.getframeinfo(sys._getframe(1))
        importer_filename = caller_frame.filename
        importer_module = globals['__name__']
        if importer_filename == caller_frame.filename:
            importer_reference = '%s line %s' % (
                importer_filename, str(caller_frame.lineno))
        else:
            importer_reference = importer_filename
        result = builtin_import(module, globals, locals, fromlist, level)
        importee_module = result.__name__
        # We're only interested if importer and importee are in SymPy
        if relevant(importer_module) and relevant(importee_module):
            for symbol in result.__dict__.iterkeys():
                definition = Definition(
                    symbol, result.__dict__[symbol], importer_module)
                if definition not in symbol_definers:
                    symbol_definers[definition] = importee_module
            if hasattr(result, '__path__'):
                ##PACKAGE##
                # The existence of __path__ is documented in the tutorial on modules.
                # Python 3.3 documents this in http://docs.python.org/3.3/reference/import.html
                if options.by_origin:
                    msg('Error: %s (a package) is imported by %s',
                        module, importer_reference)
                else:
                    msg('Error: %s contains package import %s',
                        importer_reference, module)
            if fromlist != None:
                symbol_list = fromlist
                if '*' in symbol_list:
                    if (importer_filename.endswith(("__init__.py", "__init__.pyc", "__init__.pyo"))):
                        # We do not check starred imports inside __init__
                        # That's the normal "please copy over its imports to my namespace"
                        symbol_list = []
                    else:
                        symbol_list = result.__dict__.iterkeys()
                for symbol in symbol_list:
                    if symbol not in result.__dict__:
                        if options.by_origin:
                            msg('Error: %s.%s is not defined (yet), but %s tries to import it',
                                importee_module, symbol, importer_reference)
                        else:
                            msg('Error: %s tries to import %s.%s, which did not define it (yet)',
                                importer_reference, importee_module, symbol)
                    else:
                        definition = Definition(
                            symbol, result.__dict__[symbol], importer_module)
                        symbol_definer = symbol_definers[definition]
                        if symbol_definer == importee_module:
                            ##DUPLICATE##
                            if options.by_origin:
                                msg('Error: %s.%s is imported again into %s',
                                    importee_module, symbol, importer_reference)
                            else:
                                msg('Error: %s imports %s.%s again',
                                      importer_reference, importee_module, symbol)
                        else:
                            ##ORIGIN##
                            if options.by_origin:
                                msg('Error: %s.%s is imported by %s, which should import %s.%s instead',
                                      importee_module, symbol, importer_reference, symbol_definer, symbol)
                            else:
                                msg('Error: %s imports %s.%s but should import %s.%s instead',
                                      importer_reference, importee_module, symbol, symbol_definer, symbol)
        return result

    builtins.__import__ = tracking_import
    __import__('sympy')

    sorted_messages.sort()
    for message in sorted_messages:
        print(message)