[vreg] refactor the [re]loading process so things get clearer and fix spurious errors on edge cases stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 15 Apr 2010 12:47:29 +0200
branchstable
changeset 5273 c4caef6f09c9
parent 5269 2e5bc78d05f3
child 5274 16461f675734
child 5275 32fbb13c06d3
[vreg] refactor the [re]loading process so things get clearer and fix spurious errors on edge cases
_exceptions.py
cwvreg.py
etwist/server.py
vregistry.py
--- a/_exceptions.py	Thu Apr 15 12:04:48 2010 +0200
+++ b/_exceptions.py	Thu Apr 15 12:47:29 2010 +0200
@@ -125,8 +125,6 @@
 class UnknownProperty(RegistryException):
     """property found in database but unknown in registry"""
 
-class RegistryOutOfDate(RegistryException):
-    """raised when a source file modification is detected"""
 
 # query exception #############################################################
 
--- a/cwvreg.py	Thu Apr 15 12:04:48 2010 +0200
+++ b/cwvreg.py	Thu Apr 15 12:47:29 2010 +0200
@@ -185,7 +185,7 @@
 
 from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
                       ObjectNotFound, NoSelectableObject, RegistryNotFound,
-                      RegistryOutOfDate, CW_EVENT_MANAGER, onevent)
+                      CW_EVENT_MANAGER, onevent)
 from cubicweb.utils import dump_class
 from cubicweb.vregistry import VRegistry, Registry, class_regid
 from cubicweb.rtags import RTAGS
@@ -456,8 +456,8 @@
     def itervalues(self):
         return (value for key, value in self.items())
 
-    def reset(self, path=None, force_reload=None):
-        super(CubicWebVRegistry, self).reset(path, force_reload)
+    def reset(self):
+        super(CubicWebVRegistry, self).reset()
         self._needs_iface = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
@@ -467,23 +467,12 @@
             self['propertyvalues'] = self.eprop_values = {}
             for key, propdef in self.config.eproperty_definitions():
                 self.register_property(key, **propdef)
-        if path is not None and force_reload:
-            cleanup_sys_modules(path)
-            cubes = self.config.cubes()
-            # if the fs code use some cubes not yet registered into the instance
-            # we should cleanup sys.modules for those as well to avoid potential
-            # bad class reference pb after reloading
-            cfg = self.config
-            for cube in cfg.expand_cubes(cubes, with_recommends=True):
-                if not cube in cubes:
-                    cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
-                    cleanup_sys_modules(cpath)
 
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
         self._set_schema(schema)
         # now we can load application's web objects
-        self._reload(self.config.vregistry_path(), force_reload=False)
+        self.reload(self.config.vregistry_path())
         # map lowered entity type names to their actual name
         self.case_insensitive_etypes = {}
         for eschema in self.schema.entities():
@@ -492,12 +481,26 @@
             clear_cache(eschema, 'ordered_relations')
             clear_cache(eschema, 'meta_attributes')
 
-    def _reload(self, path, force_reload):
+    def reload_if_needed(self):
+        path = self.config.vregistry_path()
+        if self.is_reload_needed(path):
+            self.reload(path)
+
+    def reload(self, path):
+        """modification detected, reset and reload the vreg"""
         CW_EVENT_MANAGER.emit('before-registry-reload')
-        # modification detected, reset and reload
-        self.reset(path, force_reload)
-        super(CubicWebVRegistry, self).register_objects(
-            path, force_reload, self.config.extrapath)
+        cleanup_sys_modules(path)
+        cubes = self.config.cubes()
+        # if the fs code use some cubes not yet registered into the instance we
+        # should cleanup sys.modules for those as well to avoid potential bad
+        # class reference pb after reloading
+        cfg = self.config
+        for cube in cfg.expand_cubes(cubes, with_recommends=True):
+            if not cube in cubes:
+                cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
+                cleanup_sys_modules(cpath)
+        self.reset()
+        self.register_objects(path, True)
         CW_EVENT_MANAGER.emit('after-registry-reload')
 
     def _set_schema(self, schema):
@@ -542,15 +545,10 @@
         if ifaces:
             self._needs_iface[obj] = ifaces
 
-    def register_objects(self, path, force_reload=None):
+    def register_objects(self, path, force_reload=False):
         """overriden to remove objects requiring a missing interface"""
-        if force_reload is None:
-            force_reload = self.config.debugmode
-        try:
-            super(CubicWebVRegistry, self).register_objects(
-                path, force_reload, self.config.extrapath)
-        except RegistryOutOfDate:
-            self._reload(path, force_reload)
+        super(CubicWebVRegistry, self).register_objects(
+            path, force_reload, self.config.extrapath)
 
     def initialization_completed(self):
         """cw specific code once vreg initialization is completed:
--- a/etwist/server.py	Thu Apr 15 12:04:48 2010 +0200
+++ b/etwist/server.py	Thu Apr 15 12:47:29 2010 +0200
@@ -179,7 +179,7 @@
         """Render a page from the root resource"""
         # reload modified files in debug mode
         if self.debugmode:
-            self.appli.vreg.register_objects(self.config.vregistry_path())
+            self.appli.vreg.reload_if_needed()
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
         else:
--- a/vregistry.py	Thu Apr 15 12:04:48 2010 +0200
+++ b/vregistry.py	Thu Apr 15 12:47:29 2010 +0200
@@ -31,8 +31,7 @@
 from logilab.common.logging_ext import set_log_methods
 
 from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb import (RegistryNotFound, ObjectNotFound, NoSelectableObject,
-                      RegistryOutOfDate)
+from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
 from cubicweb.appobject import AppObject
 
 def _toload_info(path, extrapath, _toload=None):
@@ -246,8 +245,18 @@
     def __init__(self, config):
         super(VRegistry, self).__init__()
         self.config = config
+        # need to clean sys.path this to avoid import confusion pb (i.e.  having
+        # the same module loaded as 'cubicweb.web.views' subpackage and as
+        # views' or 'web.views' subpackage. This is mainly for testing purpose,
+        # we should'nt need this in production environment
+        for webdir in (join(dirname(realpath(__file__)), 'web'),
+                       join(dirname(__file__), 'web')):
+            if webdir in sys.path:
+                sys.path.remove(webdir)
+        if CW_SOFTWARE_ROOT in sys.path:
+            sys.path.remove(CW_SOFTWARE_ROOT)
 
-    def reset(self, path=None, force_reload=None):
+    def reset(self):
         # don't use self.clear, we want to keep existing subdictionaries
         for subdict in self.itervalues():
             subdict.clear()
@@ -392,60 +401,61 @@
         self._loadedmods = {}
         return filemods
 
-    def register_objects(self, path, force_reload, extrapath=None):
-        # need to clean sys.path this to avoid import confusion pb (i.e.
-        # having the same module loaded as 'cubicweb.web.views' subpackage and
-        # as views'  or 'web.views' subpackage
-        # this is mainly for testing purpose, we should'nt need this in
-        # production environment
-        for webdir in (join(dirname(realpath(__file__)), 'web'),
-                       join(dirname(__file__), 'web')):
-            if webdir in sys.path:
-                sys.path.remove(webdir)
-        if CW_SOFTWARE_ROOT in sys.path:
-            sys.path.remove(CW_SOFTWARE_ROOT)
+    def register_objects(self, path, force_reload=False, extrapath=None):
         # load views from each directory in the instance's path
         filemods = self.init_registration(path, extrapath)
-        change = False
         for filepath, modname in filemods:
-            if self.load_file(filepath, modname, force_reload):
-                change = True
-        if change:
-            self.initialization_completed()
-        return change
+            self.load_file(filepath, modname, force_reload)
+        self.initialization_completed()
 
     def initialization_completed(self):
         for regname, reg in self.iteritems():
             reg.initialization_completed()
 
+    def _mdate(self, filepath):
+        try:
+            return stat(filepath)[-2]
+        except OSError:
+            # this typically happens on emacs backup files (.#foo.py)
+            self.warning('Unable to load %s. It is likely to be a backup file',
+                         filepath)
+            return None
+
+    def is_reload_needed(self, path):
+        """return True if something module changed and the registry should be
+        reloaded
+        """
+        lastmodifs = self._lastmodifs
+        for fileordir in path:
+            if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
+                if self.is_reload_needed([join(fileordir, fname)
+                                          for fname in listdir(fileordir)]):
+                    return True
+            elif fileordir[-3:] == '.py':
+                mdate = self._mdate(fileordir)
+                if mdate is None:
+                    continue # backup file, see _mdate implementation
+                if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
+                    self.info('File %s changed since last visit', fileordir)
+                    return True
+        return False
+
     def load_file(self, filepath, modname, force_reload=False):
         """load app objects from a python file"""
         from logilab.common.modutils import load_module_from_name
         if modname in self._loadedmods:
             return
         self._loadedmods[modname] = {}
-        try:
-            modified_on = stat(filepath)[-2]
-        except OSError:
-            # this typically happens on emacs backup files (.#foo.py)
-            self.warning('Unable to load %s. It is likely to be a backup file',
-                         filepath)
-            return False
-        if filepath in self._lastmodifs:
-            # only load file if it was modified
-            if modified_on <= self._lastmodifs[filepath]:
-                return
-            # if it was modified, raise RegistryOutOfDate to reload everything
-            self.info('File %s changed since last visit', filepath)
-            raise RegistryOutOfDate()
+        mdate = self._mdate(filepath)
+        if mdate is None:
+            return # backup file, see _mdate implementation
         # set update time before module loading, else we get some reloading
         # weirdness in case of syntax error or other error while importing the
         # module
-        self._lastmodifs[filepath] = modified_on
+        self._lastmodifs[filepath] = mdate
         # load the module
         module = load_module_from_name(modname, use_sys=not force_reload)
         self.load_module(module)
-        return True
 
     def load_module(self, module):
         self.info('loading %s', module)