File: actionwrapper.py

package info (click to toggle)
plone3 3.1.3-1
  • links: PTS
  • area: main
  • in suites: lenny
  • size: 73,972 kB
  • ctags: 39,149
  • sloc: python: 209,481; xml: 25,065; sh: 1,395; makefile: 585; php: 129; lisp: 54
file content (256 lines) | stat: -rw-r--r-- 9,058 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# 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.)

        >>> 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

# 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)