much better custom_import, smaller, faster, and finally works with shell

This commit is contained in:
mdipierro
2012-10-11 22:03:20 -05:00
parent ca5efcc032
commit aadde5a791
6 changed files with 80 additions and 241 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.1.0 (2012-10-11 16:48:00) dev Version 2.1.0 (2012-10-11 22:03:15) dev
+2
View File
@@ -40,6 +40,7 @@ import imp
import logging import logging
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
import rewrite import rewrite
from custom_import import custom_import_install
try: try:
import py_compile import py_compile
@@ -411,6 +412,7 @@ def build_environment(request, response, session, store_current=True):
local_import_aux(name,reload,app) local_import_aux(name,reload,app)
BaseAdapter.set_folder(pjoin(request.folder, 'databases')) BaseAdapter.set_folder(pjoin(request.folder, 'databases'))
response._view_environment = copy.copy(environment) response._view_environment = copy.copy(environment)
custom_import_install()
return environment return environment
def save_pyc(filename): def save_pyc(filename):
+77 -232
View File
@@ -6,149 +6,119 @@ import os
import re import re
import sys import sys
import threading import threading
import traceback
from gluon import current
# Install the new import function: NAIVE_IMPORTER = __builtin__.__import__
def custom_import_install(web2py_path): TRACK_CHANGES = False
global _web2py_importer INVALID_MODULES = set(sys.modules.keys()).union(('','gluon','applications','custom_import'))
global _web2py_path
if isinstance(__builtin__.__import__, _Web2pyImporter):
return #aready installed
_web2py_path = web2py_path
_web2py_importer = _Web2pyImporter(web2py_path)
__builtin__.__import__ = _web2py_importer
def is_tracking_changes(): # backward compatibility API
""" def custom_import_install():
@return: True: neo_importer is tracking changes made to Python source __builtin__.__import__ = custom_importer
files. False: neo_import does not reload Python modules.
"""
global _is_tracking_changes
return _is_tracking_changes
def track_changes(track=True): def track_changes(track=True):
assert track in (True,False), "must be True or False"
global TRACK_CHANGES
TRACK_CHANGES = track
def is_tracking_changes():
return TRACK_CHANGES
def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
""" """
Tell neo_importer to start/stop tracking changes made to Python modules. The web2py custom importer. Like the standard Python importer but it
@param track: True: Start tracking changes. False: Stop tracking changes. tries to transform import statements as something like
"import applications.app_name.modules.x".
If the import failed, fall back on naive_importer
""" """
global _is_tracking_changes globals = globals or {}
global _web2py_importer locals = locals or {}
global _web2py_date_tracker_importer fromlist = fromlist or []
assert track is True or track is False, "Boolean expected."
if track == _is_tracking_changes:
return
if track:
if not _web2py_date_tracker_importer:
_web2py_date_tracker_importer = \
_Web2pyDateTrackerImporter(_web2py_path)
__builtin__.__import__ = _web2py_date_tracker_importer
else:
__builtin__.__import__ = _web2py_importer
_is_tracking_changes = track
_STANDARD_PYTHON_IMPORTER = __builtin__.__import__ # Keep standard importer base_importer = TRACK_IMPORTER if TRACK_CHANGES else NAIVE_IMPORTER
_web2py_importer = None # The standard web2py importer
_web2py_date_tracker_importer = None # The web2py importer with date tracking
_web2py_path = None # Absolute path of the web2py directory
_is_tracking_changes = False # The tracking mode # if not relative and not from applications:
if hasattr(current,'request') \
class _BaseImporter(object): and level<=0 \
""" and not name.split('.')[0] in INVALID_MODULES \
The base importer. Dispatch the import the call to the standard Python and isinstance(globals, dict):
importer. try:
""" items = current.request.folder.split(os.path.sep)
if not items[-1]: items = items[:-1]
def begin(self): modules_prefix = '.'.join(items[-2:])+'.modules'
""" if not fromlist:
Many imports can be made for a single import statement. This method # import like "import x" or "import x.y"
help the management of this aspect. result = None
""" for itemname in name.split("."):
new_mod = base_importer(
def __init__(self): modules_prefix, globals,locals, [itemname], level)
self._STANDARD_PYTHON_IMPORTER = _STANDARD_PYTHON_IMPORTER try:
def __call__(self, name, globals=None, locals=None, result = result or new_mod.__dict__[itemname]
fromlist=None, level=-1): except KeyError, e:
""" raise ImportError, 'Cannot import module %s' % str(e)
The import method itself. modules_prefix += "." + itemname
""" return result
return self._STANDARD_PYTHON_IMPORTER( else:
name,globals,locals,fromlist,level) # import like "from x import a, b, ..."
pname = modules_prefix + "." + name
def end(self): return base_importer(pname, globals, locals, fromlist, level)
""" except ImportError, e1:
Needed for clean up. pass # the module does not exist
""" except Exception, e2:
raise e2 # there is an error in the module
return NAIVE_IMPORTER(name,globals,locals,fromlist,level)
class _DateTrackerImporter(_BaseImporter): class TrackImporter(object):
""" """
An importer tracking the date of the module files and reloading them when An importer tracking the date of the module files and reloading them when
they have changed. they have changed.
""" """
_PACKAGE_PATH_SUFFIX = os.path.sep+"__init__.py" THREAD_LOCAL = threading.local()
PACKAGE_PATH_SUFFIX = os.path.sep+"__init__.py"
def __init__(self): def __init__(self):
super(_DateTrackerImporter, self).__init__()
self._import_dates = {} # Import dates of the files of the modules self._import_dates = {} # Import dates of the files of the modules
# Avoid reloading cause by file modifications of reload:
self._tl = threading.local()
self._tl._modules_loaded = None
def begin(self): def __call__(self,name,globals=None,locals=None,fromlist=None,level=-1):
self._tl._modules_loaded = set()
def __call__(self, name, globals=None, locals=None,
fromlist=None, level=-1):
""" """
The import method itself. The import method itself.
""" """
globals = globals or {} globals = globals or {}
locals = locals or {} locals = locals or {}
fromlist = fromlist or [] fromlist = fromlist or []
if not hasattr(self.THREAD_LOCAL,'_modules_loaded'):
call_begin_end = self._tl._modules_loaded is None self.THREAD_LOCAL._modules_loaded = set()
if call_begin_end:
self.begin()
try: try:
self._tl.globals = globals
self._tl.locals = locals
self._tl.level = level
# Check the date and reload if needed: # Check the date and reload if needed:
self._update_dates(name, fromlist) self._update_dates(name, globals, locals, fromlist, level)
# Try to load the module and update the dates if it works: # Try to load the module and update the dates if it works:
result = super(_DateTrackerImporter, self) \ result = NAIVE_IMPORTER(name, globals, locals, fromlist, level)
.__call__(name, globals, locals, fromlist, level)
# Module maybe loaded for the 1st time so we need to set the date # Module maybe loaded for the 1st time so we need to set the date
self._update_dates(name, fromlist) self._update_dates(name, globals, locals, fromlist, level)
return result return result
except Exception: except Exception, e:
raise # Don't hide something that went wrong raise # Don't hide something that went wrong
finally:
if call_begin_end:
self.end()
def _update_dates(self, name, fromlist): def _update_dates(self, name, globals, locals, fromlist, level):
""" """
Update all the dates associated to the statement import. A single Update all the dates associated to the statement import. A single
import statement may import many modules. import statement may import many modules.
""" """
self._reload_check(name) self._reload_check(name, globals, locals, level)
if fromlist: for fromlist_name in fromlist or []:
for fromlist_name in fromlist: pname = "%s.%s" % (name, fromlist_name)
self._reload_check("%s.%s" % (name, fromlist_name)) self._reload_check(pname, globals, locals, level)
def _reload_check(self, name): def _reload_check(self, name, globals, locals, level):
""" """
Update the date associated to the module and reload the module if Update the date associated to the module and reload the module if
the file has changed. the file has changed.
""" """
module = sys.modules.get(name) module = sys.modules.get(name)
file = self._get_module_file(module) file = self._get_module_file(module)
if file: if file:
@@ -166,7 +136,7 @@ class _DateTrackerImporter(_BaseImporter):
# Get path without file ext: # Get path without file ext:
file = os.path.splitext(file)[0] file = os.path.splitext(file)[0]
reload_mod = os.path.isdir(file) \ reload_mod = os.path.isdir(file) \
and os.path.isfile(file+self._PACKAGE_PATH_SUFFIX) and os.path.isfile(file+self.PACKAGE_PATH_SUFFIX)
mod_to_pack = reload_mod mod_to_pack = reload_mod
else: # Package turning into module? else: # Package turning into module?
file += ".py" file += ".py"
@@ -176,152 +146,27 @@ class _DateTrackerImporter(_BaseImporter):
if reload_mod or not date or new_date > date: if reload_mod or not date or new_date > date:
self._import_dates[file] = new_date self._import_dates[file] = new_date
if reload_mod or (date and new_date > date): if reload_mod or (date and new_date > date):
if module not in self._tl._modules_loaded: if module not in self.THREAD_LOCAL._modules_loaded:
if mod_to_pack: if mod_to_pack:
# Module turning into a package: # Module turning into a package:
mod_name = module.__name__ mod_name = module.__name__
del sys.modules[mod_name] # Delete the module del sys.modules[mod_name] # Delete the module
# Reload the module: # Reload the module:
super(_DateTrackerImporter, self).__call__ \ NAIVE_IMPORTER(mod_name, globals, locals, [], level)
(mod_name, self._tl.globals, self._tl.locals, [],
self._tl.level)
else: else:
reload(module) reload(module)
self._tl._modules_loaded.add(module) self.THREAD_LOCAL._modules_loaded.add(module)
def end(self): def _get_module_file(self, module):
self._tl._modules_loaded = None
@classmethod
def _get_module_file(cls, module):
""" """
Get the absolute path file associated to the module or None. Get the absolute path file associated to the module or None.
""" """
file = getattr(module, "__file__", None) file = getattr(module, "__file__", None)
if file: if file:
# Make path absolute if not: # Make path absolute if not:
#file = os.path.join(cls.web2py_path, file)
file = os.path.splitext(file)[0]+".py" # Change .pyc for .py file = os.path.splitext(file)[0]+".py" # Change .pyc for .py
if file.endswith(cls._PACKAGE_PATH_SUFFIX): if file.endswith(self.PACKAGE_PATH_SUFFIX):
file = os.path.dirname(file) # Track dir for packages file = os.path.dirname(file) # Track dir for packages
return file return file
class _Web2pyImporter(_BaseImporter): TRACK_IMPORTER = TrackImporter()
"""
The standard web2py importer. Like the standard Python importer but it
tries to transform import statements as something like
"import applications.app_name.modules.x". If the import failed, fall back
on _BaseImporter.
"""
_RE_ESCAPED_PATH_SEP = re.escape(os.path.sep) # os.path.sep escaped for re
def __init__(self, web2py_path):
"""
@param web2py_path: The absolute path of the web2py installation.
"""
global DEBUG
self.super_class = super(_Web2pyImporter, self)
self.super_class.__init__()
self.web2py_path = web2py_path
self.__web2py_path_os_path_sep = self.web2py_path+os.path.sep
self.__web2py_path_os_path_sep_len = \
len(self.__web2py_path_os_path_sep)
self.__RE_APP_DIR = re.compile(
self._RE_ESCAPED_PATH_SEP.join(( \
#"^" + re.escape(web2py_path),# Not working with Python 2.5
"^(" + "applications","[^","]+)","")))
def _matchAppDir(self, file_path):
"""
Does the file in a directory inside the "applications" directory?
"""
if file_path.startswith(self.__web2py_path_os_path_sep):
file_path = file_path[self.__web2py_path_os_path_sep_len:]
return self.__RE_APP_DIR.match(file_path)
return False
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 []
self.begin()
#try:
# if not relative and not from applications:
if not name.startswith(".") and level <= 0 \
and not name.startswith("applications.") \
and isinstance(globals, dict):
# Get the name of the file do the import
caller_file_name = os.path.join(
self.web2py_path, globals.get("__file__", ""))
# Is the path in an application directory?
match_app_dir = self._matchAppDir(caller_file_name)
if match_app_dir:
try:
# Get the prefix to add for the import
# (like applications.app_name.modules):
modules_prefix = \
".".join((match_app_dir.group(1). \
replace(os.path.sep, "."), "modules"))
if not fromlist:
# import like "import x" or "import x.y"
return self.__import__dot(modules_prefix, name,
globals, locals, fromlist, level)
else:
# import like "from x import a, b, ..."
return self.super_class \
.__call__(modules_prefix+"."+name,
globals, locals, fromlist, level)
except ImportError, e:
try:
return self.super_class.__call__(name, globals, locals,
fromlist, level)
except ImportError, e1:
raise e
return self.super_class.__call__(name, globals, locals,
fromlist, level)
def __import__dot(self, prefix, name, globals, locals, fromlist,
level):
"""
Here we will import x.y.z as many imports like:
from applications.app_name.modules import x
from applications.app_name.modules.x import y
from applications.app_name.modules.x.y import z.
x will be the module returned.
"""
result = None
for name in name.split("."):
new_mod = super(_Web2pyImporter, self).__call__(
prefix, globals,locals, [name], level)
try:
result = result or new_mod.__dict__[name]
except KeyError, e:
raise ImportError, 'Cannot import module %s' % str(e)
prefix += "." + name
return result
class _Web2pyDateTrackerImporter(_Web2pyImporter, _DateTrackerImporter):
"""
Like _Web2pyImporter but using a _DateTrackerImporter.
"""
-5
View File
@@ -35,8 +35,6 @@ from settings import global_settings
from admin import add_path_first, create_missing_folders, create_missing_app_folders from admin import add_path_first, create_missing_folders, create_missing_app_folders
from globals import current from globals import current
from custom_import import custom_import_install
# Remarks: # Remarks:
# calling script has inserted path to script directory into sys.path # calling script has inserted path to script directory into sys.path
# applications_parent (path to applications/, site-packages/ etc) # applications_parent (path to applications/, site-packages/ etc)
@@ -55,8 +53,6 @@ from custom_import import custom_import_install
web2py_path = global_settings.applications_parent # backward compatibility web2py_path = global_settings.applications_parent # backward compatibility
custom_import_install(web2py_path)
create_missing_folders() create_missing_folders()
# set up logging for subsequent imports # set up logging for subsequent imports
@@ -799,7 +795,6 @@ class HttpServer(object):
global_settings.applications_parent = path global_settings.applications_parent = path
os.chdir(path) os.chdir(path)
[add_path_first(p) for p in (path, abspath('site-packages'), "")] [add_path_first(p) for p in (path, abspath('site-packages'), "")]
custom_import_install(web2py_path)
if exists("logging.conf"): if exists("logging.conf"):
logging.config.fileConfig("logging.conf") logging.config.fileConfig("logging.conf")
-1
View File
@@ -27,7 +27,6 @@ from globals import Request, Response, Session
from storage import Storage from storage import Storage
from admin import w2p_unpack from admin import w2p_unpack
from dal import BaseAdapter from dal import BaseAdapter
from custom_import import custom_import_install
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
-2
View File
@@ -94,8 +94,6 @@ class Web2pyService(Service):
os.chdir(dir) os.chdir(dir)
from gluon.settings import global_settings from gluon.settings import global_settings
global_settings.gluon_parent = dir global_settings.gluon_parent = dir
from gluon.custom_import import custom_import_install
custom_import_install(dir)
return True return True
except: except:
self.log("Can't change to web2py working path; server is stopped") self.log("Can't change to web2py working path; server is stopped")