summaryrefslogtreecommitdiff
path: root/pecan/decorators.py
blob: 68195a9d569696a3fd4dcc4fc1bf5e4ad6086c6d (plain)
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
from inspect import getmembers, isclass, ismethod, isfunction

import six

from .util import _cfg, getargspec

__all__ = [
    'expose', 'transactional', 'accept_noncanonical', 'after_commit',
    'after_rollback'
]


def when_for(controller):
    def when(method=None, **kw):
        def decorate(f):
            _cfg(f)['generic_handler'] = True
            controller._pecan['generic_handlers'][method.upper()] = f
            controller._pecan['allowed_methods'].append(method.upper())
            expose(**kw)(f)
            return f
        return decorate
    return when


def expose(template=None,
           content_type='text/html',
           generic=False,
           route=None):

    '''
    Decorator used to flag controller methods as being "exposed" for
    access via HTTP, and to configure that access.

    :param template: The path to a template, relative to the base template
                     directory.
    :param content_type: The content-type to use for this template.
    :param generic: A boolean which flags this as a "generic" controller,
                    which uses generic functions based upon
                    ``functools.singledispatch`` generic functions.  Allows you
                    to split a single controller into multiple paths based upon
                    HTTP method.
    :param route: The name of the path segment to match (excluding
                  separator characters, like `/`).  Defaults to the name of
                  the function itself, but this can be used to resolve paths
                  which are not valid Python function names, e.g., if you
                  wanted to route a function to `some-special-path'.
    '''

    if template == 'json':
        content_type = 'application/json'

    def decorate(f):
        # flag the method as exposed
        f.exposed = True

        cfg = _cfg(f)

        if route:
            # This import is here to avoid a circular import issue
            from pecan import routing
            if cfg.get('generic_handler'):
                raise ValueError(
                    'Path segments cannot be overridden for generic '
                    'controllers.'
                )
            routing.route(route, f)

        # set a "pecan" attribute, where we will store details
        cfg['content_type'] = content_type
        cfg.setdefault('template', []).append(template)
        cfg.setdefault('content_types', {})[content_type] = template

        # handle generic controllers
        if generic:
            if f.__name__ in ('_default', '_lookup', '_route'):
                raise ValueError(
                    'The special method %s cannot be used as a generic '
                    'controller' % f.__name__
                )
            cfg['generic'] = True
            cfg['generic_handlers'] = dict(DEFAULT=f)
            cfg['allowed_methods'] = []
            f.when = when_for(f)

        # store the arguments for this controller method
        cfg['argspec'] = getargspec(f)

        return f

    return decorate


def transactional(ignore_redirects=True):
    '''
    If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
    to flag a controller method or class as being wrapped in a transaction,
    regardless of HTTP method.

    :param ignore_redirects: Indicates if the hook should ignore redirects
                             for this controller or not.
    '''

    def deco(f):
        if isclass(f):
            for meth in [
                m[1] for m in getmembers(f)
                if (isfunction if six.PY3 else ismethod)(m[1])
            ]:
                if getattr(meth, 'exposed', False):
                    _cfg(meth)['transactional'] = True
                    _cfg(meth)['transactional_ignore_redirects'] = _cfg(
                        meth
                    ).get(
                        'transactional_ignore_redirects',
                        ignore_redirects
                    )
        else:
            _cfg(f)['transactional'] = True
            _cfg(f)['transactional_ignore_redirects'] = ignore_redirects
        return f
    return deco


def after_action(action_type, action):
    '''
    If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
    to flag a controller method to perform a callable action after the
    action_type is successfully issued.

    :param action: The callable to call after the commit is successfully
    issued.  '''

    if action_type not in ('commit', 'rollback'):
        raise Exception('action_type (%s) is not valid' % action_type)

    def deco(func):
        _cfg(func).setdefault('after_%s' % action_type, []).append(action)
        return func
    return deco


def after_commit(action):
    '''
    If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
    to flag a controller method to perform a callable action after the
    commit is successfully issued.

    :param action: The callable to call after the commit is successfully
                   issued.
    '''
    return after_action('commit', action)


def after_rollback(action):
    '''
    If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
    to flag a controller method to perform a callable action after the
    rollback is successfully issued.

    :param action: The callable to call after the rollback is successfully
                   issued.
    '''
    return after_action('rollback', action)


def accept_noncanonical(func):
    '''
    Flags a controller method as accepting non-canoncial URLs.
    '''

    _cfg(func)['accept_noncanonical'] = True
    return func