SP/web2py/gluon/custom_import.py

209 lines
7.7 KiB
Python
Raw Permalink Normal View History

2018-10-25 15:33:07 +00:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
| This file is part of the web2py Web Framework
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
Support for smart import syntax for web2py applications
-------------------------------------------------------
"""
from gluon._compat import builtin, unicodeT, PY2, to_native, reload
import os
import sys
import threading
from gluon import current
NATIVE_IMPORTER = builtin.__import__
INVALID_MODULES = set(('', 'gluon', 'applications', 'custom_import'))
# backward compatibility API
def custom_import_install():
if builtin.__import__ == NATIVE_IMPORTER:
INVALID_MODULES.update(sys.modules.keys())
builtin.__import__ = custom_importer
def track_changes(track=True):
assert track in (True, False), "must be True or False"
current.request._custom_import_track_changes = track
def is_tracking_changes():
return current.request._custom_import_track_changes
class CustomImportException(ImportError):
pass
def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
"""
web2py's custom importer. It behaves like the standard Python importer but
it tries to transform import statements as something like
"import applications.app_name.modules.x".
If the import fails, it falls back on naive_importer
"""
if isinstance(name, unicodeT):
name = to_native(name)
globals = globals or {}
locals = locals or {}
fromlist = fromlist or []
try:
if current.request._custom_import_track_changes:
base_importer = TRACK_IMPORTER
else:
base_importer = NATIVE_IMPORTER
except: # there is no current.request (should never happen)
base_importer = NATIVE_IMPORTER
if not(PY2) and level < 0:
level = 0
# if not relative and not from applications:
if hasattr(current, 'request') \
and level <= 0 \
and not name.partition('.')[0] in INVALID_MODULES \
and isinstance(globals, dict):
import_tb = None
try:
try:
oname = name if not name.startswith('.') else '.'+name
return NATIVE_IMPORTER(oname, globals, locals, fromlist, level)
except (ImportError, KeyError):
items = current.request.folder.split(os.path.sep)
if not items[-1]:
items = items[:-1]
modules_prefix = '.'.join(items[-2:]) + '.modules'
if not fromlist:
# import like "import x" or "import x.y"
result = None
for itemname in name.split("."):
new_mod = base_importer(
modules_prefix, globals, locals, [itemname], level)
try:
result = result or sys.modules[modules_prefix+'.'+itemname]
except KeyError as e:
raise ImportError('Cannot import module %s' % str(e))
modules_prefix += "." + itemname
return result
else:
# import like "from x import a, b, ..."
pname = modules_prefix + "." + name
return base_importer(pname, globals, locals, fromlist, level)
except ImportError as e1:
import_tb = sys.exc_info()[2]
try:
return NATIVE_IMPORTER(name, globals, locals, fromlist, level)
except (ImportError, KeyError) as e3:
raise ImportError(e1, import_tb) # there an import error in the module
except Exception as e2:
raise # there is an error in the module
finally:
if import_tb:
import_tb = None
return NATIVE_IMPORTER(name, globals, locals, fromlist, level)
class TrackImporter(object):
"""
An importer tracking the date of the module files and reloading them when
they are changed.
"""
THREAD_LOCAL = threading.local()
PACKAGE_PATH_SUFFIX = os.path.sep + "__init__.py"
def __init__(self):
self._import_dates = {} # Import dates of the files of the modules
def __call__(self, name, globals=None, locals=None, fromlist=None, level=-1):
"""
The import method itself.
"""
globals = globals or {}
locals = locals or {}
fromlist = fromlist or []
try:
# Check the date and reload if needed:
self._update_dates(name, globals, locals, fromlist, level)
# Try to load the module and update the dates if it works:
result = NATIVE_IMPORTER(name, globals, locals, fromlist, level)
# Module maybe loaded for the 1st time so we need to set the date
self._update_dates(name, globals, locals, fromlist, level)
return result
except Exception as e:
raise # Don't hide something that went wrong
def _update_dates(self, name, globals, locals, fromlist, level):
"""
Update all the dates associated to the statement import. A single
import statement may import many modules.
"""
self._reload_check(name, globals, locals, level)
for fromlist_name in fromlist or []:
pname = "%s.%s" % (name, fromlist_name)
self._reload_check(pname, globals, locals, level)
def _reload_check(self, name, globals, locals, level):
"""
Update the date associated to the module and reload the module if
the file changed.
"""
module = sys.modules.get(name)
file = self._get_module_file(module)
if file:
date = self._import_dates.get(file)
new_date = None
reload_mod = False
mod_to_pack = False # Module turning into a package? (special case)
try:
new_date = os.path.getmtime(file)
except:
self._import_dates.pop(file, None) # Clean up
# Handle module changing in package and
#package changing in module:
if file.endswith(".py"):
# Get path without file ext:
file = os.path.splitext(file)[0]
reload_mod = os.path.isdir(file) \
and os.path.isfile(file + self.PACKAGE_PATH_SUFFIX)
mod_to_pack = reload_mod
else: # Package turning into module?
file += ".py"
reload_mod = os.path.isfile(file)
if reload_mod:
new_date = os.path.getmtime(file) # Refresh file date
if reload_mod or not date or new_date > date:
self._import_dates[file] = new_date
if reload_mod or (date and new_date > date):
if mod_to_pack:
# Module turning into a package:
mod_name = module.__name__
del sys.modules[mod_name] # Delete the module
# Reload the module:
NATIVE_IMPORTER(mod_name, globals, locals, [], level)
else:
reload(module)
def _get_module_file(self, module):
"""
Get the absolute path file associated to the module or None.
"""
file = getattr(module, "__file__", None)
if file:
# Make path absolute if not:
file = os.path.splitext(file)[0] + ".py" # Change .pyc for .py
if file.endswith(self.PACKAGE_PATH_SUFFIX):
file = os.path.dirname(file) # Track dir for packages
return file
TRACK_IMPORTER = TrackImporter()