Back to index

plone3  3.1.7
actionwrapper.py
Go to the documentation of this file.
00001 # Copyright (c) 2005-2007
00002 # Authors: KSS Project Contributors (see docs/CREDITS.txt)
00003 #
00004 # This program is free software; you can redistribute it and/or modify
00005 # it under the terms of the GNU General Public License version 2 as published
00006 # by the Free Software Foundation.
00007 #
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 #
00013 # You should have received a copy of the GNU General Public License
00014 # along with this program; if not, write to the Free Software
00015 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
00016 # 02111-1307, USA.
00017 
00018 from textwrap import dedent
00019 from inspect import  formatargspec, getargspec, getargvalues, \
00020                      formatargvalues, currentframe
00021 from zope.interface import implements
00022 
00023 class KSSExplicitError(Exception):
00024     'Explicit error to be raised'
00025 
00026 class kssaction(object):
00027     '''Descriptor to bundle kss server actions.
00028 
00029     - render() will be called automatically if there is no
00030       return value
00031 
00032     - if KSSExplicitError is raised, a normal response is returned,
00033       containing a single command:error KSS command.
00034 
00035     Let's say we have a class here - that is supposed to be a kss view.
00036 
00037         >>> from kss.core import kssaction, KSSExplicitError, KSSView
00038 
00039         >>> class MyView(KSSView):
00040         ...     def ok(self, a, b, c=0):
00041         ...         return 'OK %s %s %s' % (a, b, c)
00042         ...     def notok(self, a, b, c=0):
00043         ...         pass
00044         ...     def error(self, a, b, c=0):
00045         ...         raise KSSExplicitError, 'The error'
00046         ...     def exception(self, a, b, c=0):
00047         ...         raise Exception, 'Unknown exception'
00048          
00049     Now we try qualifying with kssaction. We overwrite render too, 
00050     just to enable sensible testing of the output:
00051 
00052         >>> class MyView(KSSView):
00053         ...     def render(self):
00054         ...         return 'Rendered'
00055         ...     @kssaction
00056         ...     def ok(self, a, b, c=3):
00057         ...         return 'OK %s %s %s' % (a, b, c)
00058         ...     @kssaction
00059         ...     def notok(self, a, b, c=3):
00060         ...         pass
00061         ...     @kssaction
00062         ...     def error(self, a, b, c=3):
00063         ...         raise KSSExplicitError, 'The error'
00064         ...     @kssaction
00065         ...     def exception(self, a, b, c=3):
00066         ...         raise Exception, 'Unknown exception'
00067  
00068     Instantiate a view.
00069 
00070         >>> view = MyView(None, None)
00071 
00072     Now, of course ok renders well.
00073 
00074         >>> view.ok(1, b=2)
00075         'OK 1 2 3'
00076 
00077     Not ok will have implicit rendering.
00078 
00079         >>> view.notok(1, b=2)
00080         'Rendered'
00081 
00082     The third type will return an error action. But it will render
00083     instead of an error.
00084 
00085         >>> view.error(1, b=2)
00086         'Rendered'
00087 
00088     The fourth type will be a real error.
00089 
00090         >>> view.exception(1, b=2)
00091         Traceback (most recent call last):
00092         ...
00093         Exception: Unknown exception
00094 
00095     Now for the sake of it, let's test the rendered kukit response.
00096     So, we don't overwrite render like as we did in the previous
00097     tests.
00098 
00099         >>> from zope.publisher.browser import TestRequest
00100 
00101         >>> class MyView(KSSView):
00102         ...     @kssaction
00103         ...     def error(self, a, b, c=3):
00104         ...         raise KSSExplicitError, 'The error'
00105         ...     @kssaction
00106         ...     def with_docstring(self, a, b, c=3):
00107         ...         "Docstring"
00108         ...         raise KSSExplicitError, 'The error'
00109  
00110         >>> request = TestRequest()
00111         >>> view = MyView(None, request)
00112 
00113     Set debug-mode command rendering so we can see the results in a
00114     more structured form.
00115 
00116         >>> from zope import interface as iapi
00117         >>> from kss.core.tests.base import IDebugRequest
00118         >>> iapi.directlyProvides(request, iapi.directlyProvidedBy(request) + IDebugRequest)
00119 
00120     See the results:
00121 
00122         >>> view.error(1, b=2)
00123         [{'selectorType': None, 'params': {'message': u'The error'}, 'name': 'error', 'selector': None}]
00124 
00125     Usage of the method wrapped in browser view
00126     -------------------------------------------
00127 
00128     Finally, let's check if the method appears if defined on a browser view.
00129     Since there could be a thousand reasons why Five's magic could fail,
00130     it's good to check this. (XXX Note that this must be adjusted to run on Zope3.)
00131 
00132         >>> try:
00133         ...     import Products.Five
00134         ... except ImportError:
00135         ...     # probably zope 3, not supported
00136         ...     raise 'Zope3 not supported in this test'
00137         ... else:
00138         ...     from Products.Five.zcml import load_string, load_config
00139 
00140         >>> import kss.core.tests
00141         >>> kss.core.tests.MyView = MyView
00142 
00143     We check for two basic types of declaration. The first one declares
00144     a view with different attributes. The second one declares a dedicated
00145     view with the method as the view default method. This is how we use
00146     it in several places.
00147 
00148         >>> load_string("""
00149         ...      <configure xmlns="http://namespaces.zope.org/zope"
00150         ...      xmlns:browser="http://namespaces.zope.org/browser"
00151         ...      xmlns:five="http://namespaces.zope.org/five"
00152         ...      xmlns:zcml="http://namespaces.zope.org/zcml"
00153         ...      >
00154         ...
00155         ...      <browser:page
00156         ...          for="*"
00157         ...          class="kss.core.tests.MyView"
00158         ...          allowed_attributes="error with_docstring"
00159         ...          name="my_view"
00160         ...          permission="zope.Public"
00161         ...          />
00162         ...
00163         ...      <browser:page
00164         ...          for="*"
00165         ...          class="kss.core.tests.MyView"
00166         ...          attribute="error"
00167         ...          name="my_view2"
00168         ...          permission="zope.Public"
00169         ...          />
00170         ...
00171         ...  </configure>""")
00172 
00173     Let's check it now:
00174     
00175         >>> self.folder.restrictedTraverse('/@@my_view/error')
00176         <bound method MyView.wrapper...
00177 
00178     It must also work as a default method of a view since that is
00179     main usage for us:
00180     
00181         >>> v = self.folder.restrictedTraverse('/my_view2')
00182         >>> isinstance(v, MyView)
00183         True
00184         >>> hasattr(v, 'error')
00185         True
00186         >>> v(1, b=2)
00187         [{'selectorType': None, 'params': {'message': u'The error'}, 'name': 'error', 'selector': None}]
00188 
00189     In addition, to be publishable, the docstring must exist. Let's
00190     see if the wrapper actually does this. If the method had a docstring,
00191     it will be reused, but a docstring is provided in any case.
00192 
00193         >>> v = self.folder.restrictedTraverse('/@@my_view')
00194         >>> bool(v.error.__doc__)
00195         True
00196 
00197         >>> v.with_docstring.__doc__
00198         'Docstring'
00199 
00200     '''
00201     def __init__(self, f):
00202         self.f = f
00203         # Now this is a solution I don't like, but we need the same
00204         # function signature, otherwise the ZPublisher won't marshall
00205         # the parameters. *arg, **kw would not suffice since no parameters
00206         # would be marshalled at all.
00207         argspec = getargspec(f)
00208         orig_args = formatargspec(*argspec)[1:-1]
00209         if argspec[3] is None:
00210             fixed_args_num = len(argspec[0])
00211         else:
00212             fixed_args_num = len(argspec[0]) - len(argspec[3])
00213         values_list = [v for v in argspec[0][:fixed_args_num]]
00214         values_list.extend(['%s=%s' % (v, v) for v in argspec[0][fixed_args_num:]])
00215         values_args = ', '.join(values_list)
00216         # provide a docstring in any case.
00217         if self.f.__doc__ is not None:
00218             docstring = repr(f.__doc__)
00219         else:
00220             docstring = '"XXX"'
00221         # orig_args: "a, b, c=2"
00222         # values_args: "a, b, c=c"
00223         code = dedent('''\n
00224                 def wrapper(%s):
00225                     %s
00226                     return descr.apply(%s)
00227                 ''' % (orig_args, docstring, values_args))
00228         self.wrapper_code = compile(code, '<wrapper>', 'exec')
00229 
00230     def __get__(self, obj, cls=None):
00231         d =  {'descr': self, 'self': obj}
00232         exec(self.wrapper_code, d)
00233         wrapper = d['wrapper'].__get__(obj, cls)
00234         return wrapper
00235 
00236     def apply(self, obj, *arg, **kw):
00237         try:
00238             result = self.f(obj, *arg, **kw)
00239         except KSSExplicitError, exc:
00240             # Clear all the commands, and emit an error command
00241             obj._initcommands()
00242             obj.commands.addCommand('error', message=str(exc))
00243             result = None
00244         if result is None:
00245             # render not returned - so we do it.
00246             result = obj.render()
00247         return result
00248 
00249 # backward compatibility
00250 class KssExplicitError(KSSExplicitError):
00251     def __init__(self, *args, **kw):
00252         message = "'KssExplicitError' is deprecated," \
00253             "use 'KSSExplicitError'- KSS uppercase instead."
00254         warnings.warn(message, DeprecationWarning, 2)
00255         KSSExplicitError.__init__(self, *args, **kw)
00256