~ubuntu-branches/ubuntu/natty/flup/natty

« back to all changes in this revision

Viewing changes to flup/publisher.py

  • Committer: Bazaar Package Importer
  • Author(s): Kai Hendry
  • Date: 2007-09-12 20:22:04 UTC
  • mfrom: (1.2.1 upstream) (4 gutsy)
  • mto: This revision was merged to the branch mainline in revision 5.
  • Revision ID: james.westby@ubuntu.com-20070912202204-fg63etr9vzaf8hea
* New upstream release
* http://www.saddi.com/software/news/archives/58-flup-1.0-released.html
* Added a note in the description that people should probably start thinking
  of moving to modwsgi.org

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (c) 2002, 2005, 2006 Allan Saddi <allan@saddi.com>
2
 
# All rights reserved.
3
 
#
4
 
# Redistribution and use in source and binary forms, with or without
5
 
# modification, are permitted provided that the following conditions
6
 
# are met:
7
 
# 1. Redistributions of source code must retain the above copyright
8
 
#    notice, this list of conditions and the following disclaimer.
9
 
# 2. Redistributions in binary form must reproduce the above copyright
10
 
#    notice, this list of conditions and the following disclaimer in the
11
 
#    documentation and/or other materials provided with the distribution.
12
 
#
13
 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
 
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
 
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17
 
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
 
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
 
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
 
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
 
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
 
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23
 
# SUCH DAMAGE.
24
 
#
25
 
# $Id: publisher.py 1853 2006-04-06 23:05:58Z asaddi $
26
 
 
27
 
__author__ = 'Allan Saddi <allan@saddi.com>'
28
 
__version__ = '$Revision: 1853 $'
29
 
 
30
 
import os
31
 
import inspect
32
 
import cgi
33
 
import types
34
 
from Cookie import SimpleCookie
35
 
 
36
 
__all__ = ['Request',
37
 
           'Response',
38
 
           'Transaction',
39
 
           'Publisher',
40
 
           'File',
41
 
           'Action',
42
 
           'Redirect',
43
 
           'InternalRedirect',
44
 
           'getexpected',
45
 
           'trimkw']
46
 
 
47
 
class NoDefault(object):
48
 
    """Sentinel object so we can distinguish None in keyword arguments."""
49
 
    pass
50
 
 
51
 
class Request(object):
52
 
    """
53
 
    Encapsulates data about the HTTP request.
54
 
 
55
 
    Supported attributes that originate from here: (The attributes are
56
 
    read-only, however you may modify the contents of environ.)
57
 
      transaction - Enclosing Transaction object.
58
 
      environ - Environment variables, as passed from WSGI adapter.
59
 
      method - Request method.
60
 
      publisherScriptName - SCRIPT_NAME of Publisher.
61
 
      scriptName - SCRIPT_NAME for request.
62
 
      pathInfo - PATH_INFO for request.
63
 
      form - Dictionary containing data from query string and/or POST request.
64
 
      cookie - Cookie from request.
65
 
    """
66
 
    def __init__(self, transaction, environ):
67
 
        self._transaction = transaction
68
 
 
69
 
        self._environ = environ
70
 
        self._publisherScriptName = environ.get('SCRIPT_NAME', '')
71
 
 
72
 
        self._form = {}
73
 
        self._parseFormData()
74
 
 
75
 
        # Is this safe? Will it ever raise exceptions?
76
 
        self._cookie = SimpleCookie(environ.get('HTTP_COOKIE', None))
77
 
 
78
 
    def _parseFormData(self):
79
 
        """
80
 
        Fills self._form with data from a FieldStorage. May be overidden to
81
 
        provide custom form processing. (Like no processing at all!)
82
 
        """
83
 
        # Parse query string and/or POST data.
84
 
        form = FieldStorage(fp=self._environ['wsgi.input'],
85
 
                            environ=self._environ, keep_blank_values=1)
86
 
 
87
 
        # Collapse FieldStorage into a simple dict.
88
 
        if form.list is not None:
89
 
            for field in form.list:
90
 
                # Wrap uploaded files
91
 
                if field.filename:
92
 
                    val = File(field)
93
 
                else:
94
 
                    val = field.value
95
 
 
96
 
                # Add File/value to args, constructing a list if there are
97
 
                # multiple values.
98
 
                if self._form.has_key(field.name):
99
 
                    self._form[field.name].append(val)
100
 
                else:
101
 
                    self._form[field.name] = [val]
102
 
 
103
 
        # Unwrap lists with a single item.
104
 
        for name,val in self._form.items():
105
 
            if len(val) == 1:
106
 
                self._form[name] = val[0]
107
 
 
108
 
    def _get_transaction(self):
109
 
        return self._transaction
110
 
    transaction = property(_get_transaction, None, None,
111
 
                           "Transaction associated with this Request")
112
 
    
113
 
    def _get_environ(self):
114
 
        return self._environ
115
 
    environ = property(_get_environ, None, None,
116
 
                       "Environment variables passed from adapter")
117
 
    
118
 
    def _get_method(self):
119
 
        return self._environ['REQUEST_METHOD']
120
 
    method = property(_get_method, None, None,
121
 
                      "Request method")
122
 
    
123
 
    def _get_publisherScriptName(self):
124
 
        return self._publisherScriptName
125
 
    publisherScriptName = property(_get_publisherScriptName, None, None,
126
 
                                   'SCRIPT_NAME of Publisher')
127
 
 
128
 
    def _get_scriptName(self):
129
 
        return self._environ.get('SCRIPT_NAME', '')
130
 
    scriptName = property(_get_scriptName, None, None,
131
 
                        "SCRIPT_NAME of request")
132
 
 
133
 
    def _get_pathInfo(self):
134
 
        return self._environ.get('PATH_INFO', '')
135
 
    pathInfo = property(_get_pathInfo, None, None,
136
 
                        "PATH_INFO of request")
137
 
 
138
 
    def _get_form(self):
139
 
        return self._form
140
 
    form = property(_get_form, None, None,
141
 
                    "Parsed GET/POST data")
142
 
 
143
 
    def _get_cookie(self):
144
 
        return self._cookie
145
 
    cookie = property(_get_cookie, None, None,
146
 
                      "Cookie received from client")
147
 
 
148
 
class Response(object):
149
 
    """
150
 
    Encapsulates response-related data.
151
 
 
152
 
    Supported attributes:
153
 
      transaction - Enclosing Transaction object.
154
 
      status - Response status code (and message).
155
 
      headers - Response headers.
156
 
      cookie - Response cookie.
157
 
      contentType - Content type of body.
158
 
      body - Response body. Must be an iterable that yields strings.
159
 
 
160
 
    Since there are multiple ways of passing response info back to
161
 
    Publisher, here is their defined precedence:
162
 
      status - headers['Status'] first, then status
163
 
      cookie - Any values set in this cookie are added to the headers in
164
 
        addition to existing Set-Cookie headers.
165
 
      contentType - headers['Content-Type'] first, then contentType. If
166
 
        neither are specified, defaults to 'text/html'.
167
 
      body - Return of function takes precedence. If function returns None,
168
 
        body is used instead.
169
 
    """
170
 
    _initialHeaders = {
171
 
        'Pragma': 'no-cache',
172
 
        'Cache-Control': 'no-cache'
173
 
        }
174
 
 
175
 
    def __init__(self, transaction):
176
 
        self._transaction = transaction
177
 
 
178
 
        self._status = '200 OK'
179
 
 
180
 
        # Initial response headers.
181
 
        self._headers = header_dict(self._initialHeaders)
182
 
 
183
 
        self._cookie = SimpleCookie()
184
 
 
185
 
        self.body = []
186
 
 
187
 
    def _get_transaction(self):
188
 
        return self._transaction
189
 
    transaction = property(_get_transaction, None, None,
190
 
                           "Transaction associated with this Response")
191
 
    
192
 
    def _get_status(self):
193
 
        status = self._headers.get('Status')
194
 
        if status is not None:
195
 
            return status
196
 
        return self._status
197
 
    def _set_status(self, value):
198
 
        if self._headers.has_key('Status'):
199
 
            self._headers['Status'] = value
200
 
        else:
201
 
            self._status = value
202
 
    status = property(_get_status, _set_status, None,
203
 
                      'Response status')
204
 
 
205
 
    def _get_headers(self):
206
 
        return self._headers
207
 
    headers = property(_get_headers, None, None,
208
 
                       "Headers to send in response")
209
 
 
210
 
    def _get_cookie(self):
211
 
        return self._cookie
212
 
    cookie = property(_get_cookie, None, None,
213
 
                      "Cookie to send in response")
214
 
 
215
 
    def _get_contentType(self):
216
 
        return self._headers.get('Content-Type', 'text/html')
217
 
    def _set_contentType(self, value):
218
 
        self._headers['Content-Type'] = value
219
 
    contentType = property(_get_contentType, _set_contentType, None,
220
 
                           'Content-Type of the response body')
221
 
 
222
 
class Transaction(object):
223
 
    """
224
 
    Encapsulates objects associated with a single transaction (Request,
225
 
    Response, and possibly a Session).
226
 
 
227
 
    Public attributes: (all read-only)
228
 
      request - Request object.
229
 
      response - Response object.
230
 
 
231
 
    If Publisher sits on top of SessionMiddleware, the public API of
232
 
    SessionService is also available through the Transaction object.
233
 
    """
234
 
    _requestClass = Request
235
 
    _responseClass = Response
236
 
 
237
 
    def __init__(self, publisher, environ):
238
 
        self._publisher = publisher
239
 
        self._request = self._requestClass(self, environ)
240
 
        self._response = self._responseClass(self)
241
 
 
242
 
        # Session support.
243
 
        self._sessionService = environ.get('com.saddi.service.session')
244
 
        if self._sessionService is not None:
245
 
            self.encodeURL = self._sessionService.encodeURL
246
 
 
247
 
    def _get_request(self):
248
 
        return self._request
249
 
    request = property(_get_request, None, None,
250
 
                       "Request associated with this Transaction")
251
 
 
252
 
    def _get_response(self):
253
 
        return self._response
254
 
    response = property(_get_response, None, None,
255
 
                        "Response associated with this Transaction")
256
 
 
257
 
    # Export SessionService API
258
 
 
259
 
    def _get_session(self):
260
 
        assert self._sessionService is not None, 'No session service found'
261
 
        return self._sessionService.session
262
 
    session = property(_get_session, None, None,
263
 
                       'Returns the Session object associated with this '
264
 
                       'client')
265
 
 
266
 
    def _get_hasSession(self):
267
 
        assert self._sessionService is not None, 'No session service found'
268
 
        return self._sessionService.hasSession
269
 
    hasSession = property(_get_hasSession, None, None,
270
 
                          'True if a Session currently exists for this client')
271
 
 
272
 
    def _get_isSessionNew(self):
273
 
        assert self._sessionService is not None, 'No session service found'
274
 
        return self._sessionService.isSessionNew
275
 
    isSessionNew = property(_get_isSessionNew, None, None,
276
 
                            'True if the Session was created in this '
277
 
                            'transaction')
278
 
 
279
 
    def _get_hasSessionExpired(self):
280
 
        assert self._sessionService is not None, 'No session service found'
281
 
        return self._sessionService.hasSessionExpired
282
 
    hasSessionExpired = property(_get_hasSessionExpired, None, None,
283
 
                                 'True if the client was associated with a '
284
 
                                 'non-existent Session')
285
 
 
286
 
    def _get_encodesSessionInURL(self):
287
 
        assert self._sessionService is not None, 'No session service found'
288
 
        return self._sessionService.encodesSessionInURL
289
 
    def _set_encodesSessionInURL(self, value):
290
 
        assert self._sessionService is not None, 'No session service found'
291
 
        self._sessionService.encodesSessionInURL = value
292
 
    encodesSessionInURL = property(_get_encodesSessionInURL,
293
 
                                   _set_encodesSessionInURL, None,
294
 
                                   'True if the Session ID should be encoded '
295
 
                                   'in the URL')
296
 
 
297
 
    def prepare(self):
298
 
        """
299
 
        Called before resolved function is invoked. If overridden,
300
 
        super's prepare() MUST be called and it must be called first.
301
 
        """
302
 
        # Pass form values as keyword arguments.
303
 
        args = dict(self._request.form)
304
 
 
305
 
        # Pass Transaction to function if it wants it.
306
 
        args['transaction'] = args['trans'] = self
307
 
 
308
 
        self._args = args
309
 
 
310
 
    def call(self, func, args):
311
 
        """
312
 
        May be overridden to provide custom exception handling and/or
313
 
        per-request additions, e.g. opening a database connection,
314
 
        starting a transaction, etc.
315
 
        """
316
 
        # Trim down keywords to only what the callable expects.
317
 
        expected, varkw = getexpected(func)
318
 
        trimkw(args, expected, varkw)
319
 
 
320
 
        return func(**args)
321
 
 
322
 
    def run(self, func):
323
 
        """
324
 
        Execute the function, doing any Action processing and also
325
 
        post-processing the function/Action's return value.
326
 
        """
327
 
        try:
328
 
            # Call the function.
329
 
            result = self.call(func, self._args)
330
 
        except Action, a:
331
 
            # Caught an Action, have it do whatever it does
332
 
            result = a.run(self)
333
 
 
334
 
        response = self._response
335
 
        headers = response.headers
336
 
 
337
 
        if result is not None:
338
 
            if type(result) in types.StringTypes:
339
 
                assert type(result) is str, 'result cannot be unicode!'
340
 
 
341
 
                # Set Content-Length, if needed
342
 
                if not headers.has_key('Content-Length'):
343
 
                    headers['Content-Length'] = str(len(result))
344
 
 
345
 
                # Wrap in list for WSGI
346
 
                response.body = [result]
347
 
            else:
348
 
                if __debug__:
349
 
                    try:
350
 
                        iter(result)
351
 
                    except TypeError:
352
 
                        raise AssertionError, 'result not iterable!'
353
 
                response.body = result
354
 
 
355
 
        # If result was None, assume response.body was appropriately set.
356
 
 
357
 
    def finish(self):
358
 
        """
359
 
        Called after resolved function returns, but before response is
360
 
        sent. If overridden, super's finish() should be called last.
361
 
        """
362
 
        response = self._response
363
 
        headers = response.headers
364
 
 
365
 
        # Copy cookie to headers.
366
 
        items = response.cookie.items()
367
 
        items.sort()
368
 
        for name,morsel in items:
369
 
            headers.add('Set-Cookie', morsel.OutputString())
370
 
 
371
 
        # If there is a Status header, transfer its value to response.status.
372
 
        # It must not remain in the headers!
373
 
        status = headers.get('Status', NoDefault)
374
 
        if status is not NoDefault:
375
 
            del headers['Status']
376
 
            response.status = status
377
 
 
378
 
        code = int(response.status[:3])
379
 
        # Check if it's a response that must not include a body.
380
 
        # (See 4.3 in RFC2068.)
381
 
        if code / 100 == 1 or code in (204, 304):
382
 
            # Remove any trace of the response body, if that's the case.
383
 
            for header,value in headers.items():
384
 
                if header.lower().startswith('content-'):
385
 
                    del headers[header]
386
 
            response.body = []
387
 
        else:
388
 
            if self._request.method == 'HEAD':
389
 
                # HEAD reponse must not return a body (but the headers must be
390
 
                # kept intact).
391
 
                response.body = []
392
 
 
393
 
            # Add Content-Type, if missing.
394
 
            if not headers.has_key('Content-Type'):
395
 
                headers['Content-Type'] = response.contentType
396
 
 
397
 
        # If we have a close() method, ensure that it is called.
398
 
        if hasattr(self, 'close'):
399
 
            response.body = _addClose(response.body, self.close)
400
 
 
401
 
class Publisher(object):
402
 
    """
403
 
    WSGI application that publishes Python functions as web pages. Constructor
404
 
    takes an instance of a concrete subclass of Resolver, which is responsible
405
 
    for mapping requests to functions.
406
 
 
407
 
    Query string/POST data values are passed to the function as keyword
408
 
    arguments. If the function does not support variable keywords (e.g.
409
 
    does not have a ** parameter), the function will only be passed
410
 
    keywords which it expects. It is recommended that all keyword parameters
411
 
    have defaults so that missing form data does not raise an exception.
412
 
 
413
 
    A Transaction instance is always passed to the function via the
414
 
    "transaction" or "trans" keywords. See the Transaction, Request, and
415
 
    Response classes.
416
 
 
417
 
    Valid return types for the function are: a string, None, or an iterable
418
 
    that yields strings. If returning None, it is expected that Response.body
419
 
    has been appropriately set. (It must be an iterable that yields strings.)
420
 
 
421
 
    An instance of Publisher itself is the WSGI application.
422
 
    """
423
 
    _transactionClass = Transaction
424
 
 
425
 
    def __init__(self, resolver, transactionClass=None, error404=None):
426
 
        self._resolver = resolver
427
 
 
428
 
        if transactionClass is not None:
429
 
            self._transactionClass = transactionClass
430
 
 
431
 
        if error404 is not None:
432
 
            self._error404 = error404
433
 
 
434
 
    def _get_resolver(self):
435
 
        return self._resolver
436
 
    resolver = property(_get_resolver, None, None,
437
 
                        'Associated Resolver for this Publisher')
438
 
 
439
 
    def __call__(self, environ, start_response):
440
 
        """
441
 
        WSGI application interface. Creates a Transaction (which does most
442
 
        of the work) and sends the response.
443
 
        """
444
 
        # Set up a Transaction.
445
 
        transaction = self._transactionClass(self, environ)
446
 
 
447
 
        # Make any needed preparations.
448
 
        transaction.prepare()
449
 
 
450
 
        redirect = False
451
 
 
452
 
        while True:
453
 
            # Attempt to resolve the function.
454
 
            func = self._resolver.resolve(transaction.request,
455
 
                                          redirect=redirect)
456
 
            if func is None:
457
 
                func = self._error404
458
 
 
459
 
            try:
460
 
                # Call the function.
461
 
                transaction.run(func)
462
 
            except InternalRedirect, r:
463
 
                # Internal redirect. Set up environment and resolve again.
464
 
                environ['SCRIPT_NAME'] = transaction.request.publisherScriptName
465
 
                environ['PATH_INFO'] = r.pathInfo
466
 
                redirect = True
467
 
            else:
468
 
                break
469
 
 
470
 
        # Give Transaction a chance to do modify/add to the response.
471
 
        transaction.finish()
472
 
 
473
 
        # Transform headers into a list. (Need to pay attention to
474
 
        # multiple values.)
475
 
        responseHeaders = []
476
 
        for key,value in transaction.response.headers.items():
477
 
            if type(value) is list:
478
 
                for v in value:
479
 
                    responseHeaders.append((key, v))
480
 
            else:
481
 
                responseHeaders.append((key, value))
482
 
 
483
 
        start_response(transaction.response.status, responseHeaders)
484
 
        return transaction.response.body
485
 
 
486
 
    def _error404(self, transaction):
487
 
        """Error page to display when resolver fails."""
488
 
        transaction.response.status = '404 Not Found'
489
 
        request_uri = transaction.request.environ.get('REQUEST_URI')
490
 
        if request_uri is None:
491
 
            request_uri = transaction.request.environ.get('SCRIPT_NAME', '') + \
492
 
                          transaction.request.environ.get('PATH_INFO', '')
493
 
        return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
494
 
<html><head>
495
 
<title>404 Not Found</title>
496
 
</head><body>
497
 
<h1>Not Found</h1>
498
 
The requested URL %s was not found on this server.<p>
499
 
<hr>
500
 
%s</body></html>
501
 
""" % (request_uri, transaction.request.environ.get('SERVER_SIGNATURE', ''))]
502
 
 
503
 
class File(object):
504
 
    """
505
 
    Wrapper so we can identify uploaded files.
506
 
    """
507
 
    def __init__(self, field):
508
 
        self.filename = field.filename
509
 
        self.file = field.file
510
 
        self.type = field.type
511
 
        self.type_options = field.type_options
512
 
        self.headers = field.headers
513
 
        
514
 
class Action(Exception):
515
 
    """
516
 
    Abstract base class for 'Actions', which are just exceptions.
517
 
    Within Publisher, Actions have no inherent meaning and are used
518
 
    as a shortcut to perform specific actions where it's ok for a
519
 
    function to abruptly halt. (Like redirects or displaying an
520
 
    error page.)
521
 
 
522
 
    I don't know if using exceptions this way is good form (something
523
 
    tells me no ;) Their usage isn't really required, but they're
524
 
    convenient in some situations.
525
 
    """
526
 
    def run(self, transaction):
527
 
        """Override to perform your action."""
528
 
        raise NotImplementedError, self.__class__.__name__ + '.run'
529
 
 
530
 
class Redirect(Action):
531
 
    """
532
 
    Redirect to the given URL.
533
 
    """
534
 
    def __init__(self, url, permanent=False):
535
 
        self._url = url
536
 
        self._permanent = permanent
537
 
 
538
 
    def run(self, transaction):
539
 
        response = transaction.response
540
 
        response.status = self._permanent and '301 Moved Permanently' or \
541
 
                          '302 Moved Temporarily'
542
 
        response.headers.reset()
543
 
        response.headers['Location'] = self._url
544
 
        response.contentType = 'text/html'
545
 
        return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
546
 
<html><head>
547
 
<title>%s</title>
548
 
</head><body>
549
 
<h1>Found</h1>
550
 
<p>The document has moved <a href="%s">here</a>.</p>
551
 
<hr>
552
 
%s</body></html>
553
 
""" % (response.status, self._url,
554
 
       transaction.request.environ.get('SERVER_SIGNATURE', ''))]
555
 
 
556
 
class InternalRedirect(Exception):
557
 
    """
558
 
    An internal redirect using a new PATH_INFO (relative to Publisher's
559
 
    SCRIPT_NAME).
560
 
 
561
 
    When handling an InternalRedirect, the behavior of all included
562
 
    Resolvers is to expose a larger set of callables (that would normally
563
 
    be hidden). Therefore, it is important that you set pathInfo securely -
564
 
    preferably, it should not depend on any data from the request. Ideally,
565
 
    pathInfo should be a constant string.
566
 
    """
567
 
    def __init__(self, pathInfo):
568
 
        self.pathInfo = pathInfo
569
 
 
570
 
class FieldStorage(cgi.FieldStorage):
571
 
    def __init__(self, *args, **kw):
572
 
        """
573
 
        cgi.FieldStorage only parses the query string during a GET or HEAD
574
 
        request. Fix this.
575
 
        """
576
 
        cgi.FieldStorage.__init__(self, *args, **kw)
577
 
 
578
 
        environ = kw.get('environ') or os.environ
579
 
        method = environ.get('REQUEST_METHOD', 'GET').upper()
580
 
        if method not in ('GET', 'HEAD'): # cgi.FieldStorage already parsed?
581
 
            qs = environ.get('QUERY_STRING')
582
 
            if qs:
583
 
                if self.list is None:
584
 
                    self.list = []
585
 
                for key,value in cgi.parse_qsl(qs, self.keep_blank_values,
586
 
                                               self.strict_parsing):
587
 
                    self.list.append(cgi.MiniFieldStorage(key, value))
588
 
 
589
 
class header_dict(dict):
590
 
    """
591
 
    This is essentially a case-insensitive dictionary, with some additions
592
 
    geared towards supporting HTTP headers (like __str__(), add(), and
593
 
    reset()).
594
 
    """
595
 
    def __init__(self, val=None):
596
 
        """
597
 
        If initialized with an existing dictionary, calling reset() will
598
 
        reset our contents back to that initial dictionary.
599
 
        """
600
 
        dict.__init__(self)
601
 
        self._keymap = {}
602
 
        if val is None:
603
 
            val = {}
604
 
        self.update(val)
605
 
        self._reset_state = dict(val)
606
 
        
607
 
    def __contains__(self, key):
608
 
        return key.lower() in self._keymap
609
 
 
610
 
    def __delitem__(self, key):
611
 
        lower_key = key.lower()
612
 
        real_key = self._keymap.get(lower_key)
613
 
        if real_key is None:
614
 
            raise KeyError, key
615
 
        del self._keymap[lower_key]
616
 
        dict.__delitem__(self, real_key)
617
 
 
618
 
    def __getitem__(self, key):
619
 
        lower_key = key.lower()
620
 
        real_key = self._keymap.get(lower_key)
621
 
        if real_key is None:
622
 
            raise KeyError, key
623
 
        return dict.__getitem__(self, real_key)
624
 
 
625
 
    def __str__(self):
626
 
        """Output as HTTP headers."""
627
 
        s = ''
628
 
        for k,v in self.items():
629
 
            if type(v) is list:
630
 
                for i in v:
631
 
                    s += '%s: %s\n' % (k, i)
632
 
            else:
633
 
                s += '%s: %s\n' % (k, v)
634
 
        return s
635
 
    
636
 
    def __setitem__(self, key, value):
637
 
        lower_key = key.lower()
638
 
        real_key = self._keymap.get(lower_key)
639
 
        if real_key is None:
640
 
            self._keymap[lower_key] = key
641
 
            dict.__setitem__(self, key, value)
642
 
        else:
643
 
            dict.__setitem__(self, real_key, value)
644
 
 
645
 
    def clear(self):
646
 
        self._keymap.clear()
647
 
        dict.clear(self)
648
 
 
649
 
    def copy(self):
650
 
        c = self.__class__(self)
651
 
        c._reset_state = self._reset_state
652
 
        return c
653
 
 
654
 
    def get(self, key, failobj=None):
655
 
        lower_key = key.lower()
656
 
        real_key = self._keymap.get(lower_key)
657
 
        if real_key is None:
658
 
            return failobj
659
 
        return dict.__getitem__(self, real_key)
660
 
    
661
 
    def has_key(self, key):
662
 
        return key.lower() in self._keymap
663
 
 
664
 
    def setdefault(self, key, failobj=None):
665
 
        lower_key = key.lower()
666
 
        real_key = self._keymap.get(lower_key)
667
 
        if real_key is None:
668
 
            self._keymap[lower_key] = key
669
 
            dict.__setitem__(self, key, failobj)
670
 
            return failobj
671
 
        else:
672
 
            return dict.__getitem__(self, real_key)
673
 
        
674
 
    def update(self, d):
675
 
        for k,v in d.items():
676
 
            self[k] = v
677
 
            
678
 
    def add(self, key, value):
679
 
        """
680
 
        Add a new header value. Does not overwrite previous value of header
681
 
        (in contrast to __setitem__()).
682
 
        """
683
 
        if self.has_key(key):
684
 
            if type(self[key]) is list:
685
 
                self[key].append(value)
686
 
            else:
687
 
                self[key] = [self[key], value]
688
 
        else:
689
 
            self[key] = value
690
 
 
691
 
    def reset(self):
692
 
        """Reset contents to that at the time this instance was created."""
693
 
        self.clear()
694
 
        self.update(self._reset_state)
695
 
 
696
 
def _addClose(appIter, closeFunc):
697
 
    """
698
 
    Wraps an iterator so that its close() method calls closeFunc. Respects
699
 
    the existence of __len__ and the iterator's own close() method.
700
 
 
701
 
    Need to use metaclass magic because __len__ and next are not
702
 
    recognized unless they're part of the class. (Can't assign at
703
 
    __init__ time.)
704
 
    """
705
 
    class metaIterWrapper(type):
706
 
        def __init__(cls, name, bases, clsdict):
707
 
            super(metaIterWrapper, cls).__init__(name, bases, clsdict)
708
 
            if hasattr(appIter, '__len__'):
709
 
                cls.__len__ = appIter.__len__
710
 
            cls.next = iter(appIter).next
711
 
            if hasattr(appIter, 'close'):
712
 
                def _close(self):
713
 
                    appIter.close()
714
 
                    closeFunc()
715
 
                cls.close = _close
716
 
            else:
717
 
                cls.close = closeFunc
718
 
 
719
 
    class iterWrapper(object):
720
 
        __metaclass__ = metaIterWrapper
721
 
        def __iter__(self):
722
 
            return self
723
 
 
724
 
    return iterWrapper()
725
 
 
726
 
# Utilities which may be useful outside of Publisher? Perhaps for decorators...
727
 
 
728
 
def getexpected(func):
729
 
    """
730
 
    Returns as a 2-tuple the passed in object's expected arguments and
731
 
    whether or not it accepts variable keywords.
732
 
    """
733
 
    assert callable(func), 'object not callable'
734
 
 
735
 
    if not inspect.isclass(func):
736
 
        # At this point, we assume func is either a function, method, or
737
 
        # callable instance.
738
 
        if not inspect.isfunction(func) and not inspect.ismethod(func):
739
 
            func = getattr(func, '__call__') # When would this fail?
740
 
 
741
 
        argspec = inspect.getargspec(func)
742
 
        expected, varkw = argspec[0], argspec[2] is not None
743
 
        if inspect.ismethod(func):
744
 
            expected = expected[1:]
745
 
    else:
746
 
        # A class. Try to figure out the calling conventions of the
747
 
        # constructor.
748
 
        init = getattr(func, '__init__', None)
749
 
        # Sigh, this is getting into the realm of black magic...
750
 
        if init is not None and inspect.ismethod(init):
751
 
            argspec = inspect.getargspec(init)
752
 
            expected, varkw = argspec[0], argspec[2] is not None
753
 
            expected = expected[1:]
754
 
        else:
755
 
            expected, varkw = [], False
756
 
 
757
 
    return expected, varkw
758
 
 
759
 
def trimkw(kw, expected, varkw):
760
 
    """
761
 
    If necessary, trims down a dictionary of keyword arguments to only
762
 
    what's expected.
763
 
    """
764
 
    if not varkw: # Trimming only necessary if it doesn't accept variable kw
765
 
        for name in kw.keys():
766
 
            if name not in expected:
767
 
                del kw[name]