pylons.controllers.core
Covered: 190 lines
Missed: 28 lines
Skipped 54 lines
Percent: 87 %

      1

    """The core WSGIController"""

      2

    import inspect

      3

    import logging

      4

    import types

      5

      6

    from webob.exc import HTTPException, HTTPNotFound

      7

      8

    import pylons

      9

     10

    __all__ = ['WSGIController']

     11

     12

    log = logging.getLogger(__name__)

     13

     14

     15

    class WSGIController(object):

     16

        """WSGI Controller that follows WSGI spec for calling and return

     17

        values

     18

     19

        The Pylons WSGI Controller handles incoming web requests that are

     20

        dispatched from the PylonsBaseWSGIApp. These requests result in a

     21

        new instance of the WSGIController being created, which is then

     22

        called with the dict options from the Routes match. The standard

     23

        WSGI response is then returned with start_response called as per

     24

        the WSGI spec.

     25

     26

        Special WSGIController methods you may define:

     27

     28

        ``__before__``

     29

            This method is called before your action is, and should be used

     30

            for setting up variables/objects, restricting access to other

     31

            actions, or other tasks which should be executed before the

     32

            action is called.

     33

     34

        ``__after__``

     35

            This method is called after the action is, unless an unexpected

     36

            exception was raised. Subclasses of

     37

            :class:`~webob.exc.HTTPException` (such as those raised by

     38

            ``redirect_to`` and ``abort``) are expected; e.g. ``__after__``

     39

            will be called on redirects.

     40

     41

        Each action to be called is inspected with :meth:`_inspect_call` so

     42

        that it is only passed the arguments in the Routes match dict that

     43

        it asks for. The arguments passed into the action can be customized

     44

        by overriding the :meth:`_get_method_args` function which is

     45

        expected to return a dict.

     46

     47

        In the event that an action is not found to handle the request, the

     48

        Controller will raise an "Action Not Found" error if in debug mode,

     49

        otherwise a ``404 Not Found`` error will be returned.

     50

     51

        """

     52

        _pylons_log_debug = False

     53

     54

        def _perform_call(self, func, args):

     55

            """Hide the traceback for everything above this method"""

     56

            __traceback_hide__ = 'before_and_this'

     57

            return func(**args)

     58

     59

        def _inspect_call(self, func):

     60

            """Calls a function with arguments from

     61

            :meth:`_get_method_args`

     62

     63

            Given a function, inspect_call will inspect the function args

     64

            and call it with no further keyword args than it asked for.

     65

     66

            If the function has been decorated, it is assumed that the

     67

            decorator preserved the function signature.

     68

     69

            """

     70

            # Check to see if the class has a cache of argspecs yet

     71

            try:

     72

                cached_argspecs = self.__class__._cached_argspecs

     73

            except AttributeError:

     74

                self.__class__._cached_argspecs = cached_argspecs = {}

     75

     76

            try:

     77

                argspec = cached_argspecs[func.im_func]

     78

            except KeyError:

     79

                argspec = cached_argspecs[func.im_func] = inspect.getargspec(func)

     80

            kargs = self._get_method_args()

     81

     82

            log_debug = self._pylons_log_debug

     83

            c = self._py_object.tmpl_context

     84

            environ = self._py_object.request.environ

     85

            args = None

     86

     87

            if argspec[2]:

     88

                if self._py_object.config['pylons.tmpl_context_attach_args']:

     89

                    for k, val in kargs.iteritems():

     90

                        setattr(c, k, val)

     91

                args = kargs

     92

            else:

     93

                args = {}

     94

                argnames = argspec[0][isinstance(func, types.MethodType)

     95

                                      and 1 or 0:]

     96

                for name in argnames:

     97

                    if name in kargs:

     98

                        if self._py_object.config['pylons.tmpl_context_attach_args']:

     99

                            setattr(c, name, kargs[name])

    100

                        args[name] = kargs[name]

    101

            if log_debug:

    102

                log.debug("Calling %r method with keyword args: **%r",

    103

                          func.__name__, args)

    104

            try:

    105

                result = self._perform_call(func, args)

    106

            except HTTPException, httpe:

    107

                if log_debug:

    108

                    log.debug("%r method raised HTTPException: %s (code: %s)",

    109

                              func.__name__, httpe.__class__.__name__,

    110

                              httpe.wsgi_response.code, exc_info=True)

    111

                result = httpe

    112

    113

                # Store the exception in the environ

    114

                environ['pylons.controller.exception'] = httpe

    115

    116

                # 304 Not Modified's shouldn't have a content-type set

    117

                if result.wsgi_response.status_int == 304:

    118

                    result.wsgi_response.headers.pop('Content-Type', None)

    119

                result._exception = True

    120

    121

            return result

    122

    123

        def _get_method_args(self):

    124

            """Retrieve the method arguments to use with inspect call

    125

    126

            By default, this uses Routes to retrieve the arguments,

    127

            override this method to customize the arguments your controller

    128

            actions are called with.

    129

    130

            This method should return a dict.

    131

    132

            """

    133

            req = self._py_object.request

    134

            kargs = req.environ['pylons.routes_dict'].copy()

    135

            kargs['environ'] = req.environ

    136

            kargs['start_response'] = self.start_response

    137

            kargs['pylons'] = self._py_object

    138

            return kargs

    139

    140

        def _dispatch_call(self):

    141

            """Handles dispatching the request to the function using

    142

            Routes"""

    143

            log_debug = self._pylons_log_debug

    144

            req = self._py_object.request

    145

            try:

    146

                action = req.environ['pylons.routes_dict']['action']

    147

            except KeyError:

    148

                raise Exception("No action matched from Routes, unable to"

    149

                                "determine action dispatch.")

    150

            action_method = action.replace('-', '_')

    151

            if log_debug:

    152

                log.debug("Looking for %r method to handle the request",

    153

                          action_method)

    154

            try:

    155

                func = getattr(self, action_method, None)

    156

            except UnicodeEncodeError:

    157

                func = None

    158

            if action_method != 'start_response' and callable(func):

    159

                # Store function used to handle request

    160

                req.environ['pylons.action_method'] = func

    161

    162

                response = self._inspect_call(func)

    163

            else:

    164

                if log_debug:

    165

                    log.debug("Couldn't find %r method to handle response", action)

    166

                if pylons.config['debug']:

    167

                    raise NotImplementedError('Action %r is not implemented' %

    168

                                              action)

    169

                else:

    170

                    response = HTTPNotFound()

    171

            return response

    172

    173

        def __call__(self, environ, start_response):

    174

            """The main call handler that is called to return a response"""

    175

            log_debug = self._pylons_log_debug

    176

    177

            # Keep a local reference to the req/response objects

    178

            self._py_object = environ['pylons.pylons']

    179

    180

            # Keep private methods private

    181

            try:

    182

                if environ['pylons.routes_dict']['action'][:1] in ('_', '-'):

    183

                    if log_debug:

    184

                        log.debug("Action starts with _, private action not "

    185

                                  "allowed. Returning a 404 response")

    186

                    return HTTPNotFound()(environ, start_response)

    187

            except KeyError:

    188

                # The check later will notice that there's no action

    189

                pass

    190

    191

            start_response_called = []

    192

            def repl_start_response(status, headers, exc_info=None):

    193

                response = self._py_object.response

    194

                start_response_called.append(None)

    195

    196

                # Copy the headers from the global response

    197

                if log_debug:

    198

                    log.debug("Merging pylons.response headers into "

    199

                              "start_response call, status: %s", status)

    200

                headers.extend(header for header in response.headerlist

    201

                               if header[0] == 'Set-Cookie' or

    202

                               header[0].startswith('X-'))

    203

                return start_response(status, headers, exc_info)

    204

            self.start_response = repl_start_response

    205

    206

            if hasattr(self, '__before__'):

    207

                response = self._inspect_call(self.__before__)

    208

                if hasattr(response, '_exception'):

    209

                    return response(environ, self.start_response)

    210

    211

            response = self._dispatch_call()

    212

            if not start_response_called:

    213

                self.start_response = start_response

    214

                py_response = self._py_object.response

    215

                # If its not a WSGI response, and we have content, it needs to

    216

                # be wrapped in the response object

    217

                if isinstance(response, str):

    218

                    if log_debug:

    219

                        log.debug("Controller returned a string "

    220

                                  ", writing it to pylons.response")

    221

                    py_response.body = py_response.body + response

    222

                elif isinstance(response, unicode):

    223

                    if log_debug:

    224

                        log.debug("Controller returned a unicode string "

    225

                                  ", writing it to pylons.response")

    226

                    py_response.unicode_body = py_response.unicode_body + \

    227

                            response

    228

                elif hasattr(response, 'wsgi_response'):

    229

                    # It's an exception that got tossed.

    230

                    if log_debug:

    231

                        log.debug("Controller returned a Response object, merging "

    232

                                  "it with pylons.response")

    233

                    for name, value in py_response.headers.items():

    234

                        if name.lower() == 'set-cookie':

    235

                            response.headers.add(name, value)

    236

                        else:

    237

                            response.headers.setdefault(name, value)

    238

                    try:

    239

                        registry = environ['paste.registry']

    240

                        registry.replace(pylons.response, response)

    241

                    except KeyError:

    242

                        # Ignore the case when someone removes the registry

    243

                        pass

    244

                    py_response = response

    245

                elif response is None:

    246

                    if log_debug:

    247

                        log.debug("Controller returned None")

    248

                else:

    249

                    if log_debug:

    250

                        log.debug("Assuming controller returned an iterable, "

    251

                                  "setting it as pylons.response.app_iter")

    252

                    py_response.app_iter = response

    253

                response = py_response

    254

    255

            if hasattr(self, '__after__'):

    256

                after = self._inspect_call(self.__after__)

    257

                if hasattr(after, '_exception'):

    258

                    return after(environ, self.start_response)

    259

    260

            if hasattr(response, 'wsgi_response'):

    261

                # Copy the response object into the testing vars if we're testing

    262

                if 'paste.testing_variables' in environ:

    263

                    environ['paste.testing_variables']['response'] = response

    264

                if log_debug:

    265

                    log.debug("Calling Response object to return WSGI data")

    266

                return response(environ, self.start_response)

    267

    268

            if log_debug:

    269

                log.debug("Response assumed to be WSGI content, returning "

    270

                          "un-touched")

    271

            return response

    272