Commit 69f31b9e authored by Dave Cunningham's avatar Dave Cunningham

Merge pull request #444 from sparkprime/expandybird_imports

Fix expandybird Python importing to be lazy
parents 8ec10d6e f553dc80
...@@ -21,127 +21,39 @@ import os.path ...@@ -21,127 +21,39 @@ import os.path
import sys import sys
class AllowedImportsLoader(object): _IMPORTS = {}
# Dictionary with modules loaded from user provided imports
user_modules = {}
@staticmethod
def get_filename(name): class AllowedImportsLoader(object):
return '%s.py' % name.replace('.', '/')
def load_module(self, name, etc=None): # pylint: disable=unused-argument def load_module(self, name, etc=None): # pylint: disable=unused-argument
"""Implements loader.load_module() """Implements loader.load_module()
for loading user provided imports.""" for loading user provided imports."""
if name in AllowedImportsLoader.user_modules:
return AllowedImportsLoader.user_modules[name]
module = imp.new_module(name) module = imp.new_module(name)
content = _IMPORTS[name]
try: if content is None:
data = \ module.__path__ = [name.replace('.', '/')]
FileAccessRedirector.allowed_imports[self.get_filename(name)] else:
except Exception: # pylint: disable=broad-except
return None
# Run the module code. # Run the module code.
exec data in module.__dict__ # pylint: disable=exec-used exec content in module.__dict__ # pylint: disable=exec-used
AllowedImportsLoader.user_modules[name] = module
# We need to register the module in module registry, since new_module # Register the module so Python code will find it.
# doesn't do this, but we need it for hierarchical references.
sys.modules[name] = module sys.modules[name] = module
# If this module has children load them recursively.
if name in FileAccessRedirector.parents:
for child in FileAccessRedirector.parents[name]:
full_name = name + '.' + child
self.load_module(full_name)
# If we have helpers/common.py package,
# then for it to be successfully resolved helpers.common name
# must resolvable, hence, once we load
# child package we attach it to parent module immeadiately.
module.__dict__[child] = \
AllowedImportsLoader.user_modules[full_name]
return module return module
class AllowedImportsHandler(object): class AllowedImportsHandler(object):
def find_module(self, name, path=None): # pylint: disable=unused-argument def find_module(self, name, path=None): # pylint: disable=unused-argument
filename = AllowedImportsLoader.get_filename(name) if name in _IMPORTS:
if filename in FileAccessRedirector.allowed_imports:
return AllowedImportsLoader() return AllowedImportsLoader()
else: else:
return None return None # Delegate to system handlers.
def process_imports(imports):
"""Processes the imports by copying them and adding necessary parent packages.
Copies the imports and then for all the hierarchical packages creates
dummy entries for those parent packages, so that hierarchical imports
can be resolved. In the process parent child relationship map is built.
For example: helpers/extra/common.py will generate helpers,
helpers.extra and helpers.extra.common packages
along with related .py files.
Args:
imports: map of files to their relative paths.
Returns:
dictionary of imports to their contents and parent-child pacakge
relationship map.
"""
# First clone all the existing ones.
ret = {}
parents = {}
for k in imports:
ret[k] = imports[k]
# Now build the hierarchical modules.
for k in imports.keys():
path = imports[k]['path']
if path.endswith('.jinja'):
continue
# Normalize paths and trim .py extension, if any.
normalized = os.path.splitext(os.path.normpath(path))[0]
# If this is actually a path and not an absolute name,
# split it and process the hierarchical packages.
if sep in normalized:
parts = normalized.split(sep)
# Create dummy file entries for package levels and also retain
# parent-child relationships.
for i in xrange(0, len(parts)-1):
# Generate the partial package path.
path = os.path.join(parts[0], *parts[1:i+1])
# __init__.py file might have been provided and
# non-empty by the user.
if path not in ret:
# exec requires at least new line to be present
# to successfully compile the file.
ret[path + '.py'] = '\n'
else:
# To simplify our code, we'll store both versions
# in that case, since loader code expects files
# with .py extension.
ret[path + '.py'] = ret[path]
# Generate fully qualified package name.
fqpn = '.'.join(parts[0:i+1])
if fqpn in parents:
parents[fqpn].append(parts[i+1])
else:
parents[fqpn] = [parts[i+1]]
return ret, parents
class FileAccessRedirector(object): class FileAccessRedirector(object):
# Dictionary with user provided imports.
allowed_imports = {}
# Dictionary that shows parent child relationships,
# key is the parent, value is the list of child packages.
parents = {}
@staticmethod @staticmethod
def redirect(imports): def redirect(imports):
...@@ -150,13 +62,27 @@ class FileAccessRedirector(object): ...@@ -150,13 +62,27 @@ class FileAccessRedirector(object):
Imports already available in sys.modules will continue to be available. Imports already available in sys.modules will continue to be available.
Args: Args:
imports: map from string to string, the map of imported files names imports: map from string to dict, the map of files from names.
and contents.
""" """
if imports is not None: if imports is not None:
imps, parents = process_imports(imports) # Build map of fully qualified module names to either the content
FileAccessRedirector.allowed_imports = imps # of that module (if it is a file within a package) or just None if
FileAccessRedirector.parents = parents # the module is a package (i.e. a directory).
for name, entry in imports.iteritems():
path = entry['path']
content = entry['content']
prefix, ext = os.path.splitext(os.path.normpath(path))
if ext not in {'.py', '.pyc'}:
continue
if '.' in prefix:
# Python modules cannot contain '.', ignore these files.
continue
parts = prefix.split(sep)
dirs = ('.'.join(parts[0:i]) for i in xrange(0, len(parts)))
for d in dirs:
if d not in _IMPORTS:
_IMPORTS[d] = None
_IMPORTS['.'.join(parts)] = content
# Prepend our module handler before standard ones. # Prepend our module handler before standard ones.
sys.meta_path = [AllowedImportsHandler()] + sys.meta_path sys.meta_path = [AllowedImportsHandler()] + sys.meta_path
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment