[KSS-checkins] r35984 - in kukit/kss.core/trunk: . plugins/core tests

reebalazs at codespeak.net reebalazs at codespeak.net
Tue Dec 26 12:24:56 CET 2006


Author: reebalazs
Date: Tue Dec 26 12:24:54 2006
New Revision: 35984

Added:
   kukit/kss.core/trunk/actionwrapper.py
Modified:
   kukit/kss.core/trunk/__init__.py
   kukit/kss.core/trunk/azaxview.py
   kukit/kss.core/trunk/plugins/core/configure.zcml
   kukit/kss.core/trunk/tests/test_azaxview.py
Log:
Implement kssaction wrapper for action view methods

This is an alternate way of making view methods. First, self.render()
is not needed to be returned at the end. Second, if KssExplicitError
is raised, that will not cause an error log on the server, but
instead it sends the error command to the client, which in return
displays the fallback reason and executes its error action.

Modified: kukit/kss.core/trunk/__init__.py
==============================================================================
--- kukit/kss.core/trunk/__init__.py	(original)
+++ kukit/kss.core/trunk/__init__.py	Tue Dec 26 12:24:54 2006
@@ -18,16 +18,18 @@
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 # 02111-1307, USA.
 #
-__all__ = ('AzaxBaseView', 'force_unicode', 'AzaxUnicodeError', 'AzaxCommands') 
+__all__ = ('AzaxBaseView', 'force_unicode', 'AzaxUnicodeError', 
+           'IKssExplicitError', 'KssExplicitError', 'kssaction',
+        ) 
 
 import mimetypes
 
 mimetypes.types_map['.kkt'] = 'text/xml'    # XXX legacy!
 mimetypes.types_map['.kukit'] = 'text/xml'
 
-from azaxview import AzaxBaseView
+from azaxview import AzaxBaseView 
+from actionwrapper import KssExplicitError, kssaction 
 from unicode_quirks import force_unicode, AzaxUnicodeError
-from commands import AzaxCommands, AzaxCommand, AzaxParam
 
 try:
     import Products.Five
@@ -36,6 +38,7 @@
 else:
 # Allow to build commands from restricted code
     from AccessControl import allow_class
+    from commands import AzaxCommands, AzaxCommand, AzaxParam
     allow_class(AzaxCommands)
     allow_class(AzaxCommand)
     allow_class(AzaxParam)

Added: kukit/kss.core/trunk/actionwrapper.py
==============================================================================
--- (empty file)
+++ kukit/kss.core/trunk/actionwrapper.py	Tue Dec 26 12:24:54 2006
@@ -0,0 +1,248 @@
+# Copyright (c) 2005-2006
+# Authors: KSS project contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+# 02111-1307, USA.
+#
+
+from textwrap import dedent
+from inspect import  formatargspec, getargspec, getargvalues, \
+                     formatargvalues, currentframe
+from zope.interface import implements
+
+class KssExplicitError(Exception):
+    'Explicit error to be raised'
+
+class kssaction(object):
+    '''Descriptor to bundle kss server actions.
+
+    - render() will be called automatically if there is no
+      return value
+
+    - if KssExplicitError is raised, a normal response is returned,
+      containing a single command:error azax command.
+
+    Let's say we have a class here - that is supposed to be a kss view.
+
+        >>> from kss.core import kssaction, KssExplicitError, AzaxBaseView
+
+        >>> class MyView(AzaxBaseView):
+        ...     def ok(self, a, b, c=0):
+        ...         return 'OK %s %s %s' % (a, b, c)
+        ...     def notok(self, a, b, c=0):
+        ...         pass
+        ...     def error(self, a, b, c=0):
+        ...         raise KssExplicitError, 'The error'
+        ...     def exception(self, a, b, c=0):
+        ...         raise Exception, 'Unknown exception'
+         
+    Now we try qualifying with kssaction. We overwrite render too, 
+    just to enable sensible testing of the output:
+
+        >>> class MyView(AzaxBaseView):
+        ...     def render(self):
+        ...         return 'Rendered'
+        ...     @kssaction
+        ...     def ok(self, a, b, c=3):
+        ...         return 'OK %s %s %s' % (a, b, c)
+        ...     @kssaction
+        ...     def notok(self, a, b, c=3):
+        ...         pass
+        ...     @kssaction
+        ...     def error(self, a, b, c=3):
+        ...         raise KssExplicitError, 'The error'
+        ...     @kssaction
+        ...     def exception(self, a, b, c=3):
+        ...         raise Exception, 'Unknown exception'
+ 
+    Instantiate a view.
+
+        >>> view = MyView(None, None)
+
+    Now, of course ok renders well.
+
+        >>> view.ok(1, b=2)
+        'OK 1 2 3'
+
+    Not ok will have implicit rendering.
+
+        >>> view.notok(1, b=2)
+        'Rendered'
+
+    The third type will return an error action. But it will render
+    instead of an error.
+
+        >>> view.error(1, b=2)
+        'Rendered'
+
+    The fourth type will be a real error.
+
+        >>> view.exception(1, b=2)
+        Traceback (most recent call last):
+        ...
+        Exception: Unknown exception
+
+    Now for the sake of it, let's test the rendered kukit response.
+    So, we don't overwrite render like as we did in the previous
+    tests.
+
+        >>> from zope.publisher.browser import TestRequest
+
+        >>> class MyView(AzaxBaseView):
+        ...     @kssaction
+        ...     def error(self, a, b, c=3):
+        ...         raise KssExplicitError, 'The error'
+        ...     @kssaction
+        ...     def with_docstring(self, a, b, c=3):
+        ...         "Docstring"
+        ...         raise KssExplicitError, 'The error'
+ 
+        >>> request = TestRequest()
+        >>> view = MyView(None, request)
+
+    Set debug-mode command rendering so we can see the results in a
+    more structured form.
+
+        >>> from zope import interface as iapi
+        >>> from kss.core.tests.base import IDebugRequest
+        >>> iapi.directlyProvides(request, iapi.directlyProvidedBy(request) + IDebugRequest)
+
+    See the results:
+
+        >>> view.error(1, b=2)
+        [{'selectorType': None, 'params': {'message': u'The error'}, 'name': 'error', 'selector': None}]
+
+    Usage of the method wrapped in browser view
+    -------------------------------------------
+
+    Finally, let's check if the method appears if defined on a browser view.
+    Since there could be a thousand reasons why Five's magic could fail,
+    it's good to check this. (XXX Note that this must be adjusted to run on Zope3.)
+
+        >>> try:
+        ...     import Products.Five
+        ... except ImportError:
+        ...     # probably zope 3, not supported
+        ...     raise 'Zope3 not supported in this test'
+        ... else:
+        ...     from Products.Five.zcml import load_string, load_config
+
+        >>> import kss.core.tests
+        >>> kss.core.tests.MyView = MyView
+
+    We check for two basic types of declaration. The first one declares
+    a view with different attributes. The second one declares a dedicated
+    view with the method as the view default method. This is how we use
+    it in several places.
+
+        >>> load_string("""
+        ...      <configure xmlns="http://namespaces.zope.org/zope"
+        ...      xmlns:browser="http://namespaces.zope.org/browser"
+        ...      xmlns:five="http://namespaces.zope.org/five"
+        ...      xmlns:zcml="http://namespaces.zope.org/zcml"
+        ...      >
+        ...
+        ...      <browser:page
+        ...          for="*"
+        ...          class="kss.core.tests.MyView"
+        ...          allowed_attributes="error with_docstring"
+        ...          name="my_view"
+        ...          permission="zope.Public"
+        ...          />
+        ...
+        ...      <browser:page
+        ...          for="*"
+        ...          class="kss.core.tests.MyView"
+        ...          attribute="error"
+        ...          name="my_view2"
+        ...          permission="zope.Public"
+        ...          />
+        ...
+        ...  </configure>""")
+
+    Let's check it now:
+    
+        >>> self.folder.restrictedTraverse('/@@my_view/error')
+        <bound method MyView.wrapper...
+
+    It must also work as a default method of a view since that is
+    main usage for us:
+    
+        >>> v = self.folder.restrictedTraverse('/my_view2')
+        >>> isinstance(v, MyView)
+        True
+        >>> hasattr(v, 'error')
+        True
+        >>> v(1, b=2)
+        [{'selectorType': None, 'params': {'message': u'The error'}, 'name': 'error', 'selector': None}]
+
+    In addition, to be publishable, the docstring must exist. Let's
+    see if the wrapper actually does this. If the method had a docstring,
+    it will be reused, but a docstring is provided in any case.
+
+        >>> v = self.folder.restrictedTraverse('/@@my_view')
+        >>> bool(v.error.__doc__)
+        True
+
+        >>> v.with_docstring.__doc__
+        'Docstring'
+
+    '''
+    def __init__(self, f):
+        self.f = f
+        # Now this is a solution I don't like, but we need the same
+        # function signature, otherwise the ZPublisher won't marshall
+        # the parameters. *arg, **kw would not suffice since no parameters
+        # would be marshalled at all.
+        argspec = getargspec(f)
+        orig_args = formatargspec(*argspec)[1:-1]
+        if argspec[3] is None:
+            fixed_args_num = len(argspec[0])
+        else:
+            fixed_args_num = len(argspec[0]) - len(argspec[3])
+        values_list = [v for v in argspec[0][:fixed_args_num]]
+        values_list.extend(['%s=%s' % (v, v) for v in argspec[0][fixed_args_num:]])
+        values_args = ', '.join(values_list)
+        # provide a docstring in any case.
+        if self.f.__doc__ is not None:
+            docstring = repr(f.__doc__)
+        else:
+            docstring = '"XXX"'
+        # orig_args: "a, b, c=2"
+        # values_args: "a, b, c=c"
+        code = dedent('''\n
+                def wrapper(%s):
+                    %s
+                    return descr.apply(%s)
+                ''' % (orig_args, docstring, values_args))
+        self.wrapper_code = compile(code, '<wrapper>', 'exec')
+
+    def __get__(self, obj, cls=None):
+        d =  {'descr': self, 'self': obj}
+        exec(self.wrapper_code, d)
+        wrapper = d['wrapper'].__get__(obj, cls)
+        return wrapper
+
+    def apply(self, obj, *arg, **kw):
+        try:
+            result = self.f(obj, *arg, **kw)
+        except KssExplicitError, exc:
+            # Clear all the commands, and emit an error command
+            obj._initcommands()
+            obj.commands.addCommand('error', message=str(exc))
+            result = None
+        if result is None:
+            # render not returned - so we do it.
+            result = obj.render()
+        return result

Modified: kukit/kss.core/trunk/azaxview.py
==============================================================================
--- kukit/kss.core/trunk/azaxview.py	(original)
+++ kukit/kss.core/trunk/azaxview.py	Tue Dec 26 12:24:54 2006
@@ -120,6 +120,9 @@
 
     def __init__(self, context, request):
         super(AzaxBaseView, self).__init__(context, request)
+        self._initcommands()
+
+    def _initcommands(self):
         self.commands = AzaxCommands()
 
     def _set_context(self, context):

Modified: kukit/kss.core/trunk/plugins/core/configure.zcml
==============================================================================
--- kukit/kss.core/trunk/plugins/core/configure.zcml	(original)
+++ kukit/kss.core/trunk/plugins/core/configure.zcml	Tue Dec 26 12:24:54 2006
@@ -112,7 +112,15 @@
     <kss:registerEventType
         name="spinneroff"
         />
-        
+
+    <!-- the special error action -->
+
+    <kss:registerAction
+        name="error"
+        command_factory="global"
+        params_optional="message"
+        />
+       
      <!-- Client actions & commands -->
 	
     <kss:registerAction

Modified: kukit/kss.core/trunk/tests/test_azaxview.py
==============================================================================
--- kukit/kss.core/trunk/tests/test_azaxview.py	(original)
+++ kukit/kss.core/trunk/tests/test_azaxview.py	Tue Dec 26 12:24:54 2006
@@ -22,6 +22,7 @@
 import unittest, os
 from textwrap import dedent
 from zope.testing import doctest
+from Testing.ZopeTestCase import FunctionalDocFileSuite
 from base import AzaxViewTestCase
 from kss.core import AzaxUnicodeError
 from kss.core.plugins.core.interfaces import IKSSCoreCommands
@@ -34,7 +35,7 @@
 from kss.core.pluginregistry.plugin import registerPlugin
 from kss.core.pluginregistry.commandset import CommandSet
 from kss.core.plugins.core.commands import KSSCoreCommands
-from kss.core.tests.base import IDebugRequest
+from kss.core.tests.base import IDebugRequest, KssViewTestCase
 from kss.core.tests.commandinspector import CommandInspectorView
 from zope import component
 from Products.Five import zcml
@@ -186,6 +187,10 @@
 '''
         self.assertCommandsEqual(result, awaited)
  
+def afterSetUp(self):
+    KssViewTestCase.afterSetUp(self)
+    self.setDebugRequest()
+
 def test_suite():
     suites = []
     suites.append(unittest.makeSuite(TestAzaxView))
@@ -194,4 +199,8 @@
     suites.append(doctest.DocFileSuite('../azaxview.txt',
                                        setUp=setUpDoctTest,
                                        tearDown=tearDownDoctTest))
+    suites.append(FunctionalDocFileSuite('../actionwrapper.py',
+                                       test_class=KssViewTestCase,
+                                       setUp=afterSetUp,
+                                       tearDown=KssViewTestCase.beforeTearDown.im_func))
     return unittest.TestSuite(suites)


More information about the Kukit-checkins mailing list