221 lines
9.5 KiB
Python
221 lines
9.5 KiB
Python
#!/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')
|
|
or importer_filename.endswith('__init__.pyc')
|
|
or importer_filename.endswith('__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)
|