209 lines
7.7 KiB
Python
209 lines
7.7 KiB
Python
|
#!/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()
|