#####################################################################
# #
# double_import_denier.py #
# #
# Copyright 2018, Chris Billington #
# #
# This file is part of the labscript suite (see #
# http://labscriptsuite.org) and is licensed under the Simplified #
# BSD License. See the license.txt file in the root of the project #
# for the full license. #
# #
#####################################################################
import sys
import os
import traceback
import re
import importlib.util
DEBUG = False
# Tensorflow contains true double imports. This is arguably a bug in tensorflow,
# (reported here: https://github.com/tensorflow/tensorflow/issues/35369), but let's work
# around it since tensorflow is not our problem:
WHITELIST = ['tensorflow', 'tensorflow_core']
[docs]class DoubleImportDenier(object):
"""A module finder that tracks what's been imported and disallows multiple
imports of the same module under different names, raising an exception
upon detecting that this has occured"""
[docs] def __init__(self):
self.enabled = False
self.names_by_filepath = {}
self.tracebacks = {}
UNKNOWN = ('<unknown: imported prior to double_import_denier.enable()>\n')
for name, module in list(sys.modules.items()):
if getattr(module, '__file__', None) is not None:
path = os.path.realpath(module.__file__)
if os.path.splitext(os.path.basename(path))[0] == '__init__':
# Import path for __init__.py is actually the folder they're in, so
# use that instead
path = os.path.dirname(path)
self.names_by_filepath[path] = name
self.tracebacks[path] = [UNKNOWN, '']
self.stack = set()
[docs] def find_spec(self, fullname, path=None, target=None):
# Prevent recursion. If importlib.util.find_spec was called by us and is looking
# through sys.meta_path for finders, return None so it moves on to the other
# loaders.
dict_key = (fullname, tuple(path) if path is not None else None)
if dict_key in self.stack:
return
self.stack.add(dict_key)
try:
spec = importlib.util.find_spec(fullname, path)
except Exception as e:
if DEBUG: print('Exception in importlib.util.find_spec ' + str(e))
return
finally:
self.stack.remove(dict_key)
if spec is not None and spec.origin is not None and spec.origin != "built-in":
path = os.path.realpath(spec.origin)
if DEBUG: print('loading', fullname, 'from', path)
tb = traceback.format_stack()
other_name = self.names_by_filepath.get(path, None)
if fullname.split('.', 1)[0] not in WHITELIST:
if other_name is not None and other_name != fullname:
other_tb = self.tracebacks[path]
self._raise_error(path, fullname, tb, other_name, other_tb)
self.names_by_filepath[path] = fullname
self.tracebacks[path] = tb
return spec
def _format_tb(self, tb):
"""Take a formatted traceback as returned by traceback.format_stack()
and remove lines that are solely about us and the Python machinery,
leaving only lines pertaining to the user's code"""
frames = [frame for frame in tb[:-1]
if 'importlib._bootstrap' not in frame
and 'imp.load_module' not in frame
and not ('imp.py' in frame
and ('load_module' in frame
or 'load_source' in frame
or 'load_package' in frame))]
return ''.join(frames)
def _restore_tracebacklimit_after_exception(self):
"""Record the current value of sys.tracebacklimit, if any, and set a
temporary sys.excepthook to restore it to that value (or delete it)
after the next exception."""
orig_excepthook = sys.excepthook
exists = hasattr(sys, 'tracebacklimit')
orig_tracebacklimit = getattr(sys, 'tracebacklimit', None)
def excepthook(*args, **kwargs):
# Raise the error normally
orig_excepthook(*args, **kwargs)
# Restore sys.tracebacklimit
if exists:
sys.tracebacklimit = orig_tracebacklimit
else:
del sys.tracebacklimit
# Restore sys.excepthook:
sys.excepthook = orig_excepthook
sys.excepthook = excepthook
def _raise_error(self, path, name, tb, other_name, other_tb):
msg = """Double import! The same file has been imported under two
different names, resulting in two copies of the module. This is almost
certainly a mistake. If you are running a script from within a package
and want to import another submodule of that package, import it by its
full path: 'import module.submodule' instead of just 'import
submodule.'"""
msg = re.sub(' +',' ', ' '.join(msg.splitlines()))
tb = self._format_tb(tb)
other_tb = self._format_tb(other_tb)
msg += "\n\nPath imported: %s\n\n" % path
msg += "Traceback (first time imported, as %s):\n" % other_name
msg += "------------\n%s------------\n\n" % other_tb
msg += "Traceback (second time imported, as %s):\n" % name
msg += "------------\n%s------------" % tb
# We set sys.tracebacklimit to a small number to not print all the
# nonsense from the import machinary in the traceback, it is not
# useful to the user in reporting this exception. But we have to jump
# through this hoop to make sure sys.tracebacklimit is restored after
# our exception is raised, since putting it in a finally: block
# doesn't work:
self._restore_tracebacklimit_after_exception()
sys.tracebacklimit = 2
raise RuntimeError(msg) from None
_denier = None
[docs]def enable():
if '--allow-double-imports' in sys.argv:
# Calls to enable/disable the double import denier are ignored if this
# command line argument is present.
return
global _denier
if _denier is None:
_denier = DoubleImportDenier()
if _denier.enabled:
raise RuntimeError('already enabled')
# This is here because it actually happened:
for importer in sys.meta_path:
if importer.__class__.__name__ == DoubleImportDenier.__name__:
msg = 'Two DoubleImportDenier instances in sys.meta_path!'
raise AssertionError(msg)
sys.meta_path.insert(0, _denier)
_denier.enabled = True
[docs]def disable():
if '--allow-double-imports' in sys.argv:
# Calls to enable/disable the double import denier are ignored if this
# command line argument is present.
return
if not _denier.enabled:
raise RuntimeError('not enabled')
sys.meta_path.remove(_denier)
_denier.enabled = False
if __name__ == '__main__':
# Run from this directory as __main__:
enable()
def test1():
# Import numpy.linalg twice under different names:
import numpy as np
np.linalg.__file__ = None
# Add the numpy folder to the search path:
sys.path.append(os.path.dirname(np.__file__))
import linalg
def test2():
# This also gets detected, since this module already exists as
# __main__ but this line would import it as double_import_denier.
import double_import_denier
test1()
test2()