# Copyright (c) 2005-2007 # Authors: KSS Project Contributors (see docs/CREDITS.txt) # # 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 KSS command. Let's say we have a class here - that is supposed to be a kss view. >>> from kss.core import kssaction, KSSExplicitError, KSSView >>> class MyView(KSSView): ... 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(KSSView): ... 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(KSSView): ... @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.) # BBB Zope 2.12 >>> try: ... from Zope2.App.zcml import load_string, load_config ... except ImportError: ... 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(""" ... ... ... ... ... ... ... """) Let's check it now: >>> self.folder.restrictedTraverse('/@@my_view/error') >> 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, '', '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 # backward compatibility class KssExplicitError(KSSExplicitError): def __init__(self, *args, **kw): message = "'KssExplicitError' is deprecated," \ "use 'KSSExplicitError'- KSS uppercase instead." warnings.warn(message, DeprecationWarning, 2) KSSExplicitError.__init__(self, *args, **kw)