[Checkins] SVN: grok/branches/philikon-reload/src/grok/ First shot at module refresh w/o server restarts.

Philipp von Weitershausen philikon at philikon.de
Mon Jan 29 18:03:53 EST 2007


Log message for revision 72257:
  First shot at module refresh w/o server restarts.
  
  Outline of the approach:
  * when grokking a module or package, all grokked modules and their filenames
    will be remembered
  * when a new request is started, all the modification dates of those filenames
    is compared to the time of the last (re)start. A reload is initiated if
    appropriate.
  * reload is performed in three steps:
    1. ungrokking everything that has been grokked.  Grokkers now should implement
       an ungrok() method in which they take back everything they've done
       (e.g. unregister an adapter, undefine a security checker, etc.)
    2. tossing all modules that were grokked including those that were grokked
       because they're modules inside a grokked package.
    3. regrokking the modules that were initially grokked.
  
  Todo:
  * implement ungrok() in the remaining grokkers (currently only a few are equipped with ungrok())
  * decide on an off-switch for autorefresh (developer-mode?)
  * decide what to do when the refreshed code contains syntax errors and can't be regrokked.
  * fix a bug in the interaction with the ZODB that currently leaves persistent objects as instances
    of the old classes, not the new refreshed ones.  See http://mail.zope.org/pipermail/zodb-dev/2007-January/010636.html
    for details.
  

Changed:
  U   grok/branches/philikon-reload/src/grok/_grok.py
  U   grok/branches/philikon-reload/src/grok/components.py
  U   grok/branches/philikon-reload/src/grok/grokker.py
  U   grok/branches/philikon-reload/src/grok/meta.py
  U   grok/branches/philikon-reload/src/grok/publication.py

-=-
Modified: grok/branches/philikon-reload/src/grok/_grok.py
===================================================================
--- grok/branches/philikon-reload/src/grok/_grok.py	2007-01-29 22:49:12 UTC (rev 72256)
+++ grok/branches/philikon-reload/src/grok/_grok.py	2007-01-29 23:03:52 UTC (rev 72257)
@@ -13,18 +13,16 @@
 ##############################################################################
 """Grok
 """
-import os
+import os.path
 import sys
+import time
 
-from zope import component
-from zope import interface
-
+from zope import component, interface
 from zope.component.interfaces import IDefaultViewName
 from zope.publisher.interfaces.browser import IBrowserRequest
 from zope.app.component.site import LocalSiteManager
 
 import grok
-
 from grok import util, scan, components, grokker, meta
 from grok.error import GrokError, GrokImportError
 from grok.directive import frame_is_module
@@ -35,19 +33,19 @@
     component.provideAdapter(components.ModelTraverser)
     component.provideAdapter(components.ContainerTraverser)
 
-    # register the name 'index' as the default view name
+    # Register the name 'index' as the default view name.
     component.provideAdapter('index',
                              adapts=(grok.Model, IBrowserRequest),
                              provides=IDefaultViewName)
     component.provideAdapter('index',
                              adapts=(grok.Container, IBrowserRequest),
                              provides=IDefaultViewName)
-    # register a subscriber for when grok.Sites are added to make them
-    # into Zope 3 sites
+    # Register a subscriber for when grok.Sites are added to make them
+    # into Zope 3 sites.
     component.provideHandler(
         addSiteHandler, adapts=(grok.Site, grok.IObjectAddedEvent))
 
-    # now grok the grokkers
+    # Now grok the grokkers.
     grokker.grokkerRegistry.grok(scan.module_info_from_module(meta))
 
 def addSiteHandler(site, event):
@@ -58,7 +56,7 @@
     del sitemanager['default']
     site.setSiteManager(sitemanager)
 
-# add a cleanup hook so that grok will bootstrap itself again whenever
+# Add a cleanup hook so that grok will bootstrap itself again whenever
 # the Component Architecture is torn down.
 def resetBootstrap():
     global _bootstrapped
@@ -68,7 +66,7 @@
 from zope.testing.cleanup import addCleanUp
 addCleanUp(resetBootstrap)
 
-
+_grokked_modules = []  # This is deliberately a list as grokking order matters.
 def do_grok(dotted_name):
     global _bootstrapped
     if not _bootstrapped:
@@ -77,14 +75,58 @@
 
     module_info = scan.module_info_from_dotted_name(dotted_name)
     grok_tree(module_info)
+    _grokked_modules.append(dotted_name)
 
-
+_grokked_tree = set()  # all modules that were ever looked at for grokking
+_grokked_paths = set() # paths to watch for reload
 def grok_tree(module_info):
     grokker.grokkerRegistry.grok(module_info)
+    _grokked_tree.add(module_info.dotted_name)
 
+    # Remember the file path to look at for reload; also remember the
+    # directory it's in, in case a new module is placed next to it or
+    # it's removed.
+    _grokked_paths.add(module_info.path)
+    _grokked_paths.add(os.path.dirname(module_info.path))
+
     for sub_module_info in module_info.getSubModuleInfos():
         grok_tree(sub_module_info)
 
+_last_reload = time.time()
+def reload_grokked_modules():
+    # Throw away the registrations and reinitialize the grokker
+    # registry (including registering grok's default grokkers again).
+    grokker.grokkerRegistry.ungrok()
+    grokker.grokkerRegistry.clear()
+    grokker.grokkerRegistry.grok(scan.module_info_from_module(meta))
+
+    # Throw away the modules so they will have to be reimported.
+    for dotted_name in _grokked_tree:
+        del sys.modules[dotted_name]
+    _grokked_tree.clear()
+    _grokked_paths.clear()
+    # TODO what if the new modules contain errors?
+
+    # re-grok all the modules that were grokked before (not the tree,
+    # just the ones that were actually passed to do_grok, e.g. the
+    # packages)
+    grok_again = _grokked_modules[:]
+    _grokked_modules[:] = []
+    for dotted_name in grok_again:
+        do_grok(dotted_name)
+
+    global _last_reload
+    _last_reload = time.time()
+
+def check_reload():
+    """Determines whether reload is necessary"""
+    for path in _grokked_paths:
+        if not os.path.exists(path):
+            # The file was probably deleted, so reload.
+            return True
+        if os.stat(path).st_mtime > _last_reload:
+            return True
+
 # decorators
 class SubscribeDecorator:
 

Modified: grok/branches/philikon-reload/src/grok/components.py
===================================================================
--- grok/branches/philikon-reload/src/grok/components.py	2007-01-29 22:49:12 UTC (rev 72256)
+++ grok/branches/philikon-reload/src/grok/components.py	2007-01-29 23:03:52 UTC (rev 72257)
@@ -71,7 +71,10 @@
     def register(self, context, name, factory, module_info, templates):
         raise NotImplementedError
 
+    def ungrok(self):
+        raise NotImplementedError
 
+
 class InstanceGrokker(GrokkerBase):
     """Grokker for particular instances in a module.
     """
@@ -83,7 +86,10 @@
     def register(self, context, name, instance, module_info, templates):
         raise NotImplementedError
 
+    def ungrok(self):
+        raise NotImplementedError
 
+
 class ModuleGrokker(GrokkerBase):
     """Grokker that gets executed once for a module.
     """
@@ -95,7 +101,10 @@
     def register(self, context, module_info, templates):
         raise NotImplementedError
 
+    def ungrok(self):
+        raise NotImplementedError
 
+
 class Model(Contained, persistent.Persistent):
     # XXX Inheritance order is important here. If we reverse this,
     # then containers can't be models anymore because no unambigous MRO

Modified: grok/branches/philikon-reload/src/grok/grokker.py
===================================================================
--- grok/branches/philikon-reload/src/grok/grokker.py	2007-01-29 22:49:12 UTC (rev 72256)
+++ grok/branches/philikon-reload/src/grok/grokker.py	2007-01-29 23:03:52 UTC (rev 72257)
@@ -87,7 +87,14 @@
 
         templates.registerUnassociated(context, module_info)
 
+    def ungrok(self):
+        for grokker in self._grokkers:
+            try:
+                grokker.ungrok()
+            except NotImplementedError:
+                pass
 
+
 # deep meta mode here - we define grokkers for grok.ClassGrokker,
 # grok.InstanceGrokker, and grokker.ModuleGrokker.
 

Modified: grok/branches/philikon-reload/src/grok/meta.py
===================================================================
--- grok/branches/philikon-reload/src/grok/meta.py	2007-01-29 22:49:12 UTC (rev 72256)
+++ grok/branches/philikon-reload/src/grok/meta.py	2007-01-29 23:03:52 UTC (rev 72257)
@@ -2,11 +2,12 @@
 
 import zope.component.interface
 from zope import interface, component
+from zope.component.globalregistry import globalSiteManager
 from zope.publisher.interfaces.browser import (IDefaultBrowserLayer,
                                                IBrowserRequest,
                                                IBrowserPublisher)
 from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
-from zope.security.checker import NamesChecker, defineChecker
+from zope.security.checker import NamesChecker, defineChecker, undefineChecker
 from zope.security.permission import Permission
 from zope.security.interfaces import IPermission
 from zope.annotation.interfaces import IAnnotations
@@ -37,6 +38,9 @@
 class AdapterGrokker(grok.ClassGrokker):
     component_class = grok.Adapter
 
+    def __init__(self):
+        self.adapters = []
+
     def register(self, context, name, factory, module_info, templates):
         adapter_context = util.determine_class_context(factory, context)
         provides = util.class_annotation(factory, 'grok.provides', None)
@@ -46,10 +50,19 @@
         component.provideAdapter(factory, adapts=(adapter_context,),
                                  provides=provides,
                                  name=name)
+        self.adapters.append((factory, (adapter_context,), provides, name))
 
+    def ungrok(self):
+        for factory, adapts, provides, name in self.adapters:
+            globalSiteManager.unregisterAdapter(factory, adapts, provides, name)
+        self.adapters[:] = []
+
 class MultiAdapterGrokker(grok.ClassGrokker):
     component_class = grok.MultiAdapter
-    
+
+    def __init__(self):
+        self.adapters = []
+
     def register(self, context, name, factory, module_info, templates):
         provides = util.class_annotation(factory, 'grok.provides', None)
         if provides is None:
@@ -57,17 +70,35 @@
         util.check_adapts(factory)
         name = util.class_annotation(factory, 'grok.name', '')
         component.provideAdapter(factory, provides=provides, name=name)
+        self.adapters.append((factory, provides, name))
 
+    def ungrok(self):
+        for factory, provides, name in self.adapters:
+            globalSiteManager.unregisterAdapter(factory, provided=provided,
+                                                name=name)
+        self.adapters[:] = []
+
 class GlobalUtilityGrokker(grok.ClassGrokker):
     component_class = grok.GlobalUtility
 
+    def __init__(self):
+        self.utilities = []
+
     def register(self, context, name, factory, module_info, templates):
         provides = util.class_annotation(factory, 'grok.provides', None)
         if provides is None:
             util.check_implements_one(factory)
         name = util.class_annotation(factory, 'grok.name', '')
-        component.provideUtility(factory(), provides=provides, name=name)
+        utility = factory()
+        component.provideUtility(utility, provides=provided, name=name)
+        self.utilities.append((utility, provides, name))
 
+    def ungrok(self):
+        for utility, provides, name in self.utilities:
+            globalSiteManager.unregisterUtility(utility, provided=provides,
+                                                name=name)
+        self.utilities[:] = []
+
 class XMLRPCGrokker(grok.ClassGrokker):
     component_class = grok.XMLRPC
 
@@ -114,6 +145,9 @@
 class ViewGrokker(grok.ClassGrokker):
     component_class = grok.View
 
+    def __init__(self):
+        self.adapters = []
+
     def register(self, context, name, factory, module_info, templates):
         view_context = util.determine_class_context(factory, context)
 
@@ -188,6 +222,8 @@
                                  adapts=(view_context, IDefaultBrowserLayer),
                                  provides=interface.Interface,
                                  name=view_name)
+        self.adapters.append((factory, (view_context, IDefaultBrowserLayer),
+                             interface.Interface, view_name))
 
         # protect view, public by default
         permissions = util.class_annotation(factory, 'grok.require', [])
@@ -219,6 +255,12 @@
                                 'for XML-RPC methods.'
                                 % (method.__name__, factory), factory)
 
+    def ungrok(self):
+        for factory, adapts, provides, name in self.adapters:
+            globalSiteManager.unregisterAdapter(factory, adapts, provides, name)
+            undefineChecker(factory)
+        self.adapters[:] = []
+
 class TraverserGrokker(grok.ClassGrokker):
     component_class = grok.Traverser
 

Modified: grok/branches/philikon-reload/src/grok/publication.py
===================================================================
--- grok/branches/philikon-reload/src/grok/publication.py	2007-01-29 22:49:12 UTC (rev 72256)
+++ grok/branches/philikon-reload/src/grok/publication.py	2007-01-29 23:03:52 UTC (rev 72257)
@@ -13,18 +13,34 @@
 ##############################################################################
 """Grok publication objects
 """
+import transaction
+import ZODB.Connection
 
 from zope.security.proxy import removeSecurityProxy
 from zope.security.checker import selectChecker
+from zope.publisher.interfaces import Redirect
 
 from zope.app.publication.http import BaseHTTPPublication
 from zope.app.publication.browser import BrowserPublication
 from zope.app.publication.requestpublicationfactories import \
      BrowserFactory, XMLRPCFactory
 
+from grok._grok import check_reload, reload_grokked_modules
 
 class ZopePublicationSansProxy(object):
 
+    def openedConnection(self, conn):
+        if check_reload():
+            reload_grokked_modules()
+
+            # Reset ZODB's pickle caches so that persistent objects
+            # use the new classes.
+            # XXX doesn't work?!?
+            ZODB.Connection.resetCaches()
+            transaction.commit()
+            conn._resetCache()
+            transaction.begin()
+
     def getApplication(self, request):
         result = super(ZopePublicationSansProxy, self).getApplication(request)
         return removeSecurityProxy(result)



More information about the Checkins mailing list