summaryrefslogtreecommitdiff
path: root/pecan/routing.py
blob: fc1a7d45478ab6de881d6806ce0491c3391b9e1d (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
import warnings

from webob import exc

from .secure import handle_security, cross_boundary
from .util import iscontroller

__all__ = ['lookup_controller', 'find_object']


class NonCanonicalPath(Exception):
    '''
    Exception Raised when a non-canonical path is encountered when 'walking'
    the URI.  This is typically a ``POST`` request which requires a trailing
    slash.
    '''
    def __init__(self, controller, remainder):
        self.controller = controller
        self.remainder = remainder


def lookup_controller(obj, url_path):
    '''
    Traverses the requested url path and returns the appropriate controller
    object, including default routes.

    Handles common errors gracefully.
    '''
    remainder = url_path
    notfound_handlers = []

    while True:
        try:
            obj, remainder = find_object(obj, remainder, notfound_handlers)
            handle_security(obj)
            return obj, remainder
        except exc.HTTPNotFound:
            while notfound_handlers:
                name, obj, remainder = notfound_handlers.pop()
                if name == '_default':
                    # Notfound handler is, in fact, a controller, so stop
                    #   traversal
                    return obj, remainder
                else:
                    # Notfound handler is an internal redirect, so continue
                    #   traversal
                    result = handle_lookup_traversal(obj, remainder)
                    if result:
                        # If no arguments are passed to the _lookup, yet the
                        # argspec requires at least one, raise a 404
                        if (
                            remainder == ['']
                            and len(obj._pecan['argspec'].args) > 1
                        ):
                            raise
                        return lookup_controller(*result)
            else:
                raise exc.HTTPNotFound


def handle_lookup_traversal(obj, args):
    try:
        result = obj(*args)
        if result:
            prev_obj = obj
            obj, remainder = result
            # crossing controller boundary
            cross_boundary(prev_obj, obj)
            return result
    except TypeError as te:
        msg = 'Got exception calling lookup(): %s (%s)'
        warnings.warn(
            msg % (te, te.args),
            RuntimeWarning
        )


def find_object(obj, remainder, notfound_handlers):
    '''
    'Walks' the url path in search of an action for which a controller is
    implemented and returns that controller object along with what's left
    of the remainder.
    '''
    prev_obj = None
    while True:
        if obj is None:
            raise exc.HTTPNotFound
        if iscontroller(obj):
            return obj, remainder

        # are we traversing to another controller
        cross_boundary(prev_obj, obj)

        if remainder and remainder[0] == '':
            index = getattr(obj, 'index', None)
            if iscontroller(index):
                return index, remainder[1:]
        elif not remainder:
            # the URL has hit an index method without a trailing slash
            index = getattr(obj, 'index', None)
            if iscontroller(index):
                raise NonCanonicalPath(index, remainder[1:])
        default = getattr(obj, '_default', None)
        if iscontroller(default):
            notfound_handlers.append(('_default', default, remainder))

        lookup = getattr(obj, '_lookup', None)
        if iscontroller(lookup):
            notfound_handlers.append(('_lookup', lookup, remainder))

        route = getattr(obj, '_route', None)
        if iscontroller(route):
            next, next_remainder = route(remainder)
            cross_boundary(route, next)
            return next, next_remainder

        if not remainder:
            raise exc.HTTPNotFound
        next, remainder = remainder[0], remainder[1:]
        prev_obj = obj
        obj = getattr(obj, next, None)