~nchohan/appscale/zk3.3.4

« back to all changes in this revision

Viewing changes to AppServer/lib/webob/webob/__init__.py

  • Committer: Navraj Chohan
  • Date: 2009-03-28 01:14:04 UTC
  • Revision ID: nchohan@cs.ucsb.edu-20090328011404-42m1w6yt60m6yfg3
Initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from cStringIO import StringIO
 
2
import sys
 
3
import cgi
 
4
import urllib
 
5
import urlparse
 
6
import re
 
7
import textwrap
 
8
from Cookie import BaseCookie
 
9
from rfc822 import parsedate_tz, mktime_tz, formatdate
 
10
from datetime import datetime, date, timedelta, tzinfo
 
11
import time
 
12
import calendar
 
13
import tempfile
 
14
import warnings
 
15
from webob.datastruct import EnvironHeaders
 
16
from webob.multidict import MultiDict, UnicodeMultiDict, NestedMultiDict, NoVars
 
17
from webob.etag import AnyETag, NoETag, ETagMatcher, IfRange, NoIfRange
 
18
from webob.headerdict import HeaderDict
 
19
from webob.statusreasons import status_reasons
 
20
from webob.cachecontrol import CacheControl, serialize_cache_control
 
21
from webob.acceptparse import Accept, MIMEAccept, NilAccept, MIMENilAccept, NoAccept
 
22
from webob.byterange import Range, ContentRange
 
23
 
 
24
_CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
 
25
_SCHEME_RE = re.compile(r'^[a-z]+:', re.I)
 
26
_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
 
27
_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
 
28
 
 
29
__all__ = ['Request', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape']
 
30
 
 
31
class _UTC(tzinfo):
 
32
    def dst(self, dt):
 
33
        return timedelta(0)
 
34
    def utcoffset(self, dt):
 
35
        return timedelta(0)
 
36
    def tzname(self, dt):
 
37
        return 'UTC'
 
38
    def __repr__(self):
 
39
        return 'UTC'
 
40
 
 
41
UTC = _UTC()
 
42
 
 
43
def html_escape(s):
 
44
    """HTML-escape a string or object
 
45
    
 
46
    This converts any non-string objects passed into it to strings
 
47
    (actually, using ``unicode()``).  All values returned are
 
48
    non-unicode strings (using ``&#num;`` entities for all non-ASCII
 
49
    characters).
 
50
    
 
51
    None is treated specially, and returns the empty string.
 
52
    """
 
53
    if s is None:
 
54
        return ''
 
55
    if not isinstance(s, basestring):
 
56
        if hasattr(s, '__unicode__'):
 
57
            s = unicode(s)
 
58
        else:
 
59
            s = str(s)
 
60
    s = cgi.escape(s, True)
 
61
    if isinstance(s, unicode):
 
62
        s = s.encode('ascii', 'xmlcharrefreplace')
 
63
    return s
 
64
 
 
65
def timedelta_to_seconds(td):
 
66
    """
 
67
    Converts a timedelta instance to seconds.
 
68
    """
 
69
    return td.seconds + (td.days*24*60*60)
 
70
 
 
71
day = timedelta(days=1)
 
72
week = timedelta(weeks=1)
 
73
hour = timedelta(hours=1)
 
74
minute = timedelta(minutes=1)
 
75
second = timedelta(seconds=1)
 
76
# Estimate, I know; good enough for expirations
 
77
month = timedelta(days=30)
 
78
year = timedelta(days=365)
 
79
 
 
80
class _NoDefault:
 
81
    def __repr__(self):
 
82
        return '(No Default)'
 
83
NoDefault = _NoDefault()
 
84
 
 
85
class environ_getter(object):
 
86
    """For delegating an attribute to a key in self.environ."""
 
87
 
 
88
    def __init__(self, key, default='', default_factory=None,
 
89
                 settable=True, deletable=True, doc=None,
 
90
                 rfc_section=None):
 
91
        self.key = key
 
92
        self.default = default
 
93
        self.default_factory = default_factory
 
94
        self.settable = settable
 
95
        self.deletable = deletable
 
96
        docstring = "Gets"
 
97
        if self.settable:
 
98
            docstring += " and sets"
 
99
        if self.deletable:
 
100
            docstring += " and deletes"
 
101
        docstring += " the %r key from the environment." % self.key
 
102
        docstring += _rfc_reference(self.key, rfc_section)
 
103
        if doc:
 
104
            docstring += '\n\n' + textwrap.dedent(doc)
 
105
        self.__doc__ = docstring
 
106
 
 
107
    def __get__(self, obj, type=None):
 
108
        if obj is None:
 
109
            return self
 
110
        if self.key not in obj.environ:
 
111
            if self.default_factory:
 
112
                val = obj.environ[self.key] = self.default_factory()
 
113
                return val
 
114
            else:
 
115
                return self.default
 
116
        return obj.environ[self.key]
 
117
 
 
118
    def __set__(self, obj, value):
 
119
        if not self.settable:
 
120
            raise AttributeError("Read-only attribute (key %r)" % self.key)
 
121
        if value is None:
 
122
            if self.key in obj.environ:
 
123
                del obj.environ[self.key]
 
124
        else:
 
125
            obj.environ[self.key] = value
 
126
 
 
127
    def __delete__(self, obj):
 
128
        if not self.deletable:
 
129
            raise AttributeError("You cannot delete the key %r" % self.key)
 
130
        del obj.environ[self.key]
 
131
 
 
132
    def __repr__(self):
 
133
        return '<Proxy for WSGI environ %r key>' % self.key
 
134
 
 
135
class header_getter(object):
 
136
    """For delegating an attribute to a header in self.headers"""
 
137
 
 
138
    def __init__(self, header, default=None,
 
139
                 settable=True, deletable=True, doc=None, rfc_section=None):
 
140
        self.header = header
 
141
        self.default = default
 
142
        self.settable = settable
 
143
        self.deletable = deletable
 
144
        docstring = "Gets"
 
145
        if self.settable:
 
146
            docstring += " and sets"
 
147
        if self.deletable:
 
148
            docstring += " and deletes"
 
149
        docstring += " they header %s from the headers" % self.header
 
150
        docstring += _rfc_reference(self.header, rfc_section)
 
151
        if doc:
 
152
            docstring += '\n\n' + textwrap.dedent(doc)
 
153
        self.__doc__ = docstring
 
154
 
 
155
    def __get__(self, obj, type=None):
 
156
        if obj is None:
 
157
            return self
 
158
        if self.header not in obj.headers:
 
159
            return self.default
 
160
        else:
 
161
            return obj.headers[self.header]
 
162
 
 
163
    def __set__(self, obj, value):
 
164
        if not self.settable:
 
165
            raise AttributeError("Read-only attribute (header %s)" % self.header)
 
166
        if value is None:
 
167
            if self.header in obj.headers:
 
168
                del obj.headers[self.header]
 
169
        else:
 
170
            obj.headers[self.header] = value
 
171
 
 
172
    def __delete__(self, obj):
 
173
        if not self.deletable:
 
174
            raise AttributeError("You cannot delete the header %s" % self.header)
 
175
        del obj.headers[self.header]
 
176
 
 
177
    def __repr__(self):
 
178
        return '<Proxy for header %s>' % self.header
 
179
 
 
180
class converter(object):
 
181
    """
 
182
    Wraps a decorator, and applies conversion for that decorator
 
183
    """
 
184
    def __init__(self, decorator, getter_converter, setter_converter, convert_name=None, doc=None, converter_args=()):
 
185
        self.decorator = decorator
 
186
        self.getter_converter = getter_converter
 
187
        self.setter_converter = setter_converter
 
188
        self.convert_name = convert_name
 
189
        self.converter_args = converter_args
 
190
        docstring = decorator.__doc__ or ''
 
191
        docstring += "  Converts it as a "
 
192
        if convert_name:
 
193
            docstring += convert_name + '.'
 
194
        else:
 
195
            docstring += "%r and %r." % (getter_converter, setter_converter)
 
196
        if doc:
 
197
            docstring += '\n\n' + textwrap.dedent(doc)
 
198
        self.__doc__ = docstring
 
199
 
 
200
    def __get__(self, obj, type=None):
 
201
        if obj is None:
 
202
            return self
 
203
        value = self.decorator.__get__(obj, type)
 
204
        return self.getter_converter(value, *self.converter_args)
 
205
 
 
206
    def __set__(self, obj, value):
 
207
        value = self.setter_converter(value, *self.converter_args)
 
208
        self.decorator.__set__(obj, value)
 
209
 
 
210
    def __delete__(self, obj):
 
211
        self.decorator.__delete__(obj)
 
212
 
 
213
    def __repr__(self):
 
214
        if self.convert_name:
 
215
            name = ' %s' % self.convert_name
 
216
        else:
 
217
            name = ''
 
218
        return '<Converted %r%s>' % (self.decorator, name)
 
219
 
 
220
def _rfc_reference(header, section):
 
221
    if not section:
 
222
        return ''
 
223
    major_section = section.split('.')[0]
 
224
    link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % (
 
225
        major_section, section)
 
226
    if header.startswith('HTTP_'):
 
227
        header = header[5:].title().replace('_', '-')
 
228
    return "  For more information on %s see `section %s <%s>`_." % (
 
229
        header, section, link)
 
230
 
 
231
class deprecated_property(object):
 
232
    """
 
233
    Wraps a decorator, with a deprecation warning or error
 
234
    """
 
235
    def __init__(self, decorator, attr, message, warning=True):
 
236
        self.decorator = decorator
 
237
        self.attr = attr
 
238
        self.message = message
 
239
        self.warning = warning
 
240
 
 
241
    def __get__(self, obj, type=None):
 
242
        if obj is None:
 
243
            return self
 
244
        self.warn()
 
245
        return self.decorator.__get__(obj, type)
 
246
 
 
247
    def __set__(self, obj, value):
 
248
        self.warn()
 
249
        self.decorator.__set__(obj, value)
 
250
 
 
251
    def __delete__(self, obj):
 
252
        self.warn()
 
253
        self.decorator.__delete__(obj)
 
254
 
 
255
    def __repr__(self):
 
256
        return '<Deprecated attribute %s: %r>' % (
 
257
            self.attr,
 
258
            self.decorator)
 
259
 
 
260
    def warn(self):
 
261
        if not self.warning:
 
262
            raise DeprecationWarning(
 
263
                'The attribute %s is deprecated: %s' % (self.attr, self.message))
 
264
        else:
 
265
            warnings.warn(
 
266
                'The attribute %s is deprecated: %s' % (self.attr, self.message),
 
267
                DeprecationWarning,
 
268
                stacklevel=3)
 
269
 
 
270
def _parse_date(value):
 
271
    if not value:
 
272
        return None
 
273
    t = parsedate_tz(value)
 
274
    if t is None:
 
275
        # Could not parse
 
276
        return None
 
277
    t = mktime_tz(t)
 
278
    return datetime.fromtimestamp(t, UTC)
 
279
 
 
280
def _serialize_date(dt):
 
281
    if dt is None:
 
282
        return None
 
283
    if isinstance(dt, unicode):
 
284
        dt = dt.encode('ascii')
 
285
    if isinstance(dt, str):
 
286
        return dt
 
287
    if isinstance(dt, timedelta):
 
288
        dt = datetime.now() + dt
 
289
    if isinstance(dt, (datetime, date)):
 
290
        dt = dt.timetuple()
 
291
    if isinstance(dt, (tuple, time.struct_time)):
 
292
        dt = calendar.timegm(dt)
 
293
    if not isinstance(dt, (float, int)):
 
294
        raise ValueError(
 
295
            "You must pass in a datetime, date, time tuple, or integer object, not %r" % dt)
 
296
    return formatdate(dt)
 
297
 
 
298
def _parse_date_delta(value):
 
299
    """
 
300
    like _parse_date, but also handle delta seconds
 
301
    """
 
302
    if not value:
 
303
        return None
 
304
    try:
 
305
        value = int(value)
 
306
    except ValueError:
 
307
        pass
 
308
    else:
 
309
        delta = timedelta(seconds=value)
 
310
        return datetime.now() + delta
 
311
    return _parse_date(value)
 
312
 
 
313
def _serialize_date_delta(value):
 
314
    if not value and value != 0:
 
315
        return None
 
316
    if isinstance(value, (float, int)):
 
317
        return str(int(value))
 
318
    return _serialize_date(value)
 
319
 
 
320
def _parse_etag(value, default=True):
 
321
    if value is None:
 
322
        value = ''
 
323
    value = value.strip()
 
324
    if not value:
 
325
        if default:
 
326
            return AnyETag
 
327
        else:
 
328
            return NoETag
 
329
    if value == '*':
 
330
        return AnyETag
 
331
    else:
 
332
        return ETagMatcher.parse(value)
 
333
 
 
334
def _serialize_etag(value, default=True):
 
335
    if value is None:
 
336
        return None
 
337
    if value is AnyETag:
 
338
        if default:
 
339
            return None
 
340
        else:
 
341
            return '*'
 
342
    return str(value)
 
343
 
 
344
def _parse_if_range(value):
 
345
    if not value:
 
346
        return NoIfRange
 
347
    else:
 
348
        return IfRange.parse(value)
 
349
 
 
350
def _serialize_if_range(value):
 
351
    if value is None:
 
352
        return value
 
353
    if isinstance(value, (datetime, date)):
 
354
        return _serialize_date(value)
 
355
    if not isinstance(value, str):
 
356
        value = str(value)
 
357
    return value or None
 
358
 
 
359
def _parse_range(value):
 
360
    if not value:
 
361
        return None
 
362
    # Might return None too:
 
363
    return Range.parse(value)
 
364
 
 
365
def _serialize_range(value):
 
366
    if isinstance(value, (list, tuple)):
 
367
        if len(value) != 2:
 
368
            raise ValueError(
 
369
                "If setting .range to a list or tuple, it must be of length 2 (not %r)"
 
370
                % value)
 
371
        value = Range([value])
 
372
    if value is None:
 
373
        return None
 
374
    value = str(value)
 
375
    return value or None
 
376
 
 
377
def _parse_int(value):
 
378
    if value is None or value == '':
 
379
        return None
 
380
    return int(value)
 
381
 
 
382
def _parse_int_safe(value):
 
383
    if value is None or value == '':
 
384
        return None
 
385
    try:
 
386
        return int(value)
 
387
    except ValueError:
 
388
        return None
 
389
 
 
390
def _serialize_int(value):
 
391
    if value is None:
 
392
        return None
 
393
    return str(value)
 
394
 
 
395
def _parse_content_range(value):
 
396
    if not value or not value.strip():
 
397
        return None
 
398
    # May still return None
 
399
    return ContentRange.parse(value)
 
400
 
 
401
def _serialize_content_range(value):
 
402
    if value is None:
 
403
        return None
 
404
    if isinstance(value, (tuple, list)):
 
405
        if len(value) not in (2, 3):
 
406
            raise ValueError(
 
407
                "When setting content_range to a list/tuple, it must "
 
408
                "be length 2 or 3 (not %r)" % value)
 
409
        if len(value) == 2:
 
410
            begin, end = value
 
411
            length = None
 
412
        else:
 
413
            begin, end, length = value
 
414
        value = ContentRange(begin, end, length)
 
415
    value = str(value).strip()
 
416
    if not value:
 
417
        return None
 
418
    return value
 
419
 
 
420
def _parse_list(value):
 
421
    if value is None:
 
422
        return None
 
423
    value = value.strip()
 
424
    if not value:
 
425
        return None
 
426
    return [v.strip() for v in value.split(',')
 
427
            if v.strip()]
 
428
 
 
429
def _serialize_list(value):
 
430
    if not value:
 
431
        return None
 
432
    if isinstance(value, unicode):
 
433
        value = str(value)
 
434
    if isinstance(value, str):
 
435
        return value
 
436
    return ', '.join(map(str, value))
 
437
 
 
438
def _parse_accept(value, header_name, AcceptClass, NilClass):
 
439
    if not value:
 
440
        return NilClass(header_name)
 
441
    return AcceptClass(header_name, value)
 
442
 
 
443
def _serialize_accept(value, header_name, AcceptClass, NilClass):
 
444
    if not value or isinstance(value, NilClass):
 
445
        return None
 
446
    if isinstance(value, (list, tuple, dict)):
 
447
        value = NilClass(header_name) + value
 
448
    value = str(value).strip()
 
449
    if not value:
 
450
        return None
 
451
    return value
 
452
 
 
453
class Request(object):
 
454
 
 
455
    ## Options:
 
456
    charset = None
 
457
    unicode_errors = 'strict'
 
458
    decode_param_names = False
 
459
    ## The limit after which request bodies should be stored on disk
 
460
    ## if they are read in (under this, and the request body is stored
 
461
    ## in memory):
 
462
    request_body_tempfile_limit = 10*1024
 
463
 
 
464
    def __init__(self, environ=None, environ_getter=None, charset=NoDefault, unicode_errors=NoDefault,
 
465
                 decode_param_names=NoDefault):
 
466
        if environ is None and environ_getter is None:
 
467
            raise TypeError(
 
468
                "You must provide one of environ or environ_getter")
 
469
        if environ is not None and environ_getter is not None:
 
470
            raise TypeError(
 
471
                "You can only provide one of the environ and environ_getter arguments")
 
472
        if environ is None:
 
473
            self._environ_getter = environ_getter
 
474
        else:
 
475
            if not isinstance(environ, dict):
 
476
                raise TypeError(
 
477
                    "Bad type for environ: %s" % type(environ))
 
478
            self._environ = environ
 
479
        if charset is not NoDefault:
 
480
            self.__dict__['charset'] = charset
 
481
        if unicode_errors is not NoDefault:
 
482
            self.__dict__['unicode_errors'] = unicode_errors
 
483
        if decode_param_names is not NoDefault:
 
484
            self.__dict__['decode_param_names'] = decode_param_names
 
485
 
 
486
    def __setattr__(self, attr, value, DEFAULT=[]):
 
487
        ## FIXME: I don't know why I need this guard (though experimentation says I do)
 
488
        if getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_'):
 
489
            object.__setattr__(self, attr, value)
 
490
        else:
 
491
            self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
 
492
 
 
493
    def __getattr__(self, attr):
 
494
        ## FIXME: I don't know why I need this guard (though experimentation says I do)
 
495
        if attr in self.__class__.__dict__:
 
496
            return object.__getattribute__(self, attr)
 
497
        try:
 
498
            return self.environ['webob.adhoc_attrs'][attr]
 
499
        except KeyError:
 
500
            raise AttributeError(attr)
 
501
 
 
502
    def __delattr__(self, attr):
 
503
        ## FIXME: I don't know why I need this guard (though experimentation says I do)
 
504
        if attr in self.__class__.__dict__:
 
505
            return object.__delattr__(self, attr)
 
506
        try:
 
507
            del self.environ['webob.adhoc_attrs'][attr]
 
508
        except KeyError:
 
509
            raise AttributeError(attr)
 
510
 
 
511
    def environ(self):
 
512
        """
 
513
        The WSGI environment dictionary for this request
 
514
        """
 
515
        return self._environ_getter()
 
516
    environ = property(environ, doc=environ.__doc__)
 
517
 
 
518
    def _environ_getter(self):
 
519
        return self._environ
 
520
 
 
521
    def _body_file__get(self):
 
522
        """
 
523
        Access the body of the request (wsgi.input) as a file-like
 
524
        object.
 
525
 
 
526
        If you set this value, CONTENT_LENGTH will also be updated
 
527
        (either set to -1, 0 if you delete the attribute, or if you
 
528
        set the attribute to a string then the length of the string).
 
529
        """
 
530
        return self.environ['wsgi.input']
 
531
    def _body_file__set(self, value):
 
532
        if isinstance(value, str):
 
533
            length = len(value)
 
534
            value = StringIO(value)
 
535
        else:
 
536
            length = -1
 
537
        self.environ['wsgi.input'] = value
 
538
        self.environ['CONTENT_LENGTH'] = str(length)
 
539
    def _body_file__del(self):
 
540
        self.environ['wsgi.input'] = StringIO('')
 
541
        self.environ['CONTENT_LENGTH'] = '0'
 
542
    body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__)
 
543
 
 
544
    scheme = environ_getter('wsgi.url_scheme')
 
545
    method = environ_getter('REQUEST_METHOD')
 
546
    script_name = environ_getter('SCRIPT_NAME')
 
547
    path_info = environ_getter('PATH_INFO')
 
548
    ## FIXME: should I strip out parameters?:
 
549
    content_type = environ_getter('CONTENT_TYPE', rfc_section='14.17')
 
550
    content_length = converter(
 
551
        environ_getter('CONTENT_LENGTH', rfc_section='14.13'),
 
552
        _parse_int_safe, _serialize_int, 'int')
 
553
    remote_user = environ_getter('REMOTE_USER', default=None)
 
554
    remote_addr = environ_getter('REMOTE_ADDR', default=None)
 
555
    query_string = environ_getter('QUERY_STRING')
 
556
    server_name = environ_getter('SERVER_NAME')
 
557
    server_port = converter(
 
558
        environ_getter('SERVER_PORT'),
 
559
        _parse_int, _serialize_int, 'int')
 
560
 
 
561
    _headers = None
 
562
 
 
563
    def _headers__get(self):
 
564
        """
 
565
        All the request headers as a case-insensitive dictionary-like
 
566
        object.
 
567
        """
 
568
        if self._headers is None:
 
569
            self._headers = EnvironHeaders(self.environ)
 
570
        return self._headers
 
571
 
 
572
    def _headers__set(self, value):
 
573
        self.headers.clear()
 
574
        self.headers.update(value)
 
575
 
 
576
    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
 
577
 
 
578
    def host_url(self):
 
579
        """
 
580
        The URL through the host (no path)
 
581
        """
 
582
        e = self.environ
 
583
        url = e['wsgi.url_scheme'] + '://'
 
584
        if e.get('HTTP_HOST'):
 
585
            host = e['HTTP_HOST']
 
586
            if ':' in host:
 
587
                host, port = host.split(':', 1)
 
588
            else:
 
589
 
 
590
                port = None
 
591
        else:
 
592
            host = e['SERVER_NAME']
 
593
            port = e['SERVER_PORT']
 
594
        if self.environ['wsgi.url_scheme'] == 'https':
 
595
            if port == '443':
 
596
                port = None
 
597
        elif self.environ['wsgi.url_scheme'] == 'http':
 
598
            if port == '80':
 
599
                port = None
 
600
        url += host
 
601
        if port:
 
602
            url += ':%s' % port
 
603
        return url
 
604
    host_url = property(host_url, doc=host_url.__doc__)
 
605
 
 
606
    def application_url(self):
 
607
        """
 
608
        The URL including SCRIPT_NAME (no PATH_INFO or query string)
 
609
        """
 
610
        return self.host_url + urllib.quote(self.environ.get('SCRIPT_NAME', ''))
 
611
    application_url = property(application_url, doc=application_url.__doc__)
 
612
 
 
613
    def path_url(self):
 
614
        """
 
615
        The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
 
616
        """
 
617
        return self.application_url + urllib.quote(self.environ.get('PATH_INFO', ''))
 
618
    path_url = property(path_url, doc=path_url.__doc__)
 
619
 
 
620
    def path(self):
 
621
        """
 
622
        The path of the request, without host or query string
 
623
        """
 
624
        return urllib.quote(self.script_name) + urllib.quote(self.path_info)
 
625
    path = property(path, doc=path.__doc__)
 
626
 
 
627
    def path_qs(self):
 
628
        """
 
629
        The path of the request, without host but with query string
 
630
        """
 
631
        path = self.path
 
632
        qs = self.environ.get('QUERY_STRING')
 
633
        if qs:
 
634
            path += '?' + qs
 
635
        return path
 
636
    path_qs = property(path_qs, doc=path_qs.__doc__)
 
637
 
 
638
    def url(self):
 
639
        """
 
640
        The full request URL, including QUERY_STRING
 
641
        """
 
642
        url = self.path_url
 
643
        if self.environ.get('QUERY_STRING'):
 
644
            url += '?' + self.environ['QUERY_STRING']
 
645
        return url
 
646
    url = property(url, doc=url.__doc__)
 
647
 
 
648
    def relative_url(self, other_url, to_application=False):
 
649
        """
 
650
        Resolve other_url relative to the request URL.
 
651
 
 
652
        If ``to_application`` is True, then resolve it relative to the
 
653
        URL with only SCRIPT_NAME
 
654
        """
 
655
        if to_application:
 
656
            url = self.application_url
 
657
            if not url.endswith('/'):
 
658
                url += '/'
 
659
        else:
 
660
            url = self.path_url
 
661
        return urlparse.urljoin(url, other_url)
 
662
 
 
663
    def path_info_pop(self):
 
664
        """
 
665
        'Pops' off the next segment of PATH_INFO, pushing it onto
 
666
        SCRIPT_NAME, and returning the popped segment.  Returns None if
 
667
        there is nothing left on PATH_INFO.
 
668
 
 
669
        Does not return ``''`` when there's an empty segment (like
 
670
        ``/path//path``); these segments are just ignored.
 
671
        """
 
672
        path = self.path_info
 
673
        if not path:
 
674
            return None
 
675
        while path.startswith('/'):
 
676
            self.script_name += '/'
 
677
            path = path[1:]
 
678
        if '/' not in path:
 
679
            self.script_name += path
 
680
            self.path_info = ''
 
681
            return path
 
682
        else:
 
683
            segment, path = path.split('/', 1)
 
684
            self.path_info = '/' + path
 
685
            self.script_name += segment
 
686
            return segment
 
687
 
 
688
    def path_info_peek(self):
 
689
        """
 
690
        Returns the next segment on PATH_INFO, or None if there is no
 
691
        next segment.  Doesn't modify the environment.
 
692
        """
 
693
        path = self.path_info
 
694
        if not path:
 
695
            return None
 
696
        path = path.lstrip('/')
 
697
        return path.split('/', 1)[0]
 
698
 
 
699
    def _urlvars__get(self):
 
700
        """
 
701
        Return any *named* variables matched in the URL.
 
702
 
 
703
        Takes values from ``environ['wsgiorg.routing_args']``.
 
704
        Systems like ``routes`` set this value.
 
705
        """
 
706
        if 'paste.urlvars' in self.environ:
 
707
            return self.environ['paste.urlvars']
 
708
        elif 'wsgiorg.routing_args' in self.environ:
 
709
            return self.environ['wsgiorg.routing_args'][1]
 
710
        else:
 
711
            result = {}
 
712
            self.environ['wsgiorg.routing_args'] = ((), result)
 
713
            return result
 
714
 
 
715
    def _urlvars__set(self, value):
 
716
        environ = self.environ
 
717
        if 'wsgiorg.routing_args' in environ:
 
718
            environ['wsgiorg.routing_args'] = (environ['wsgiorg.routing_args'][0], value)
 
719
            if 'paste.urlvars' in environ:
 
720
                del environ['paste.urlvars']
 
721
        elif 'paste.urlvars' in environ:
 
722
            environ['paste.urlvars'] = value
 
723
        else:
 
724
            environ['wsgiorg.routing_args'] = ((), value)
 
725
 
 
726
    def _urlvars__del(self):
 
727
        if 'paste.urlvars' in self.environ:
 
728
            del self.environ['paste.urlvars']
 
729
        if 'wsgiorg.routing_args' in self.environ:
 
730
            if not self.environ['wsgiorg.routing_args'][0]:
 
731
                del self.environ['wsgiorg.routing_args']
 
732
            else:
 
733
                self.environ['wsgiorg.routing_args'] = (self.environ['wsgiorg.routing_args'][0], {})
 
734
            
 
735
    urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__)
 
736
 
 
737
    def _urlargs__get(self):
 
738
        """
 
739
        Return any *positional* variables matched in the URL.
 
740
 
 
741
        Takes values from ``environ['wsgiorg.routing_args']``.
 
742
        Systems like ``routes`` set this value.
 
743
        """
 
744
        if 'wsgiorg.routing_args' in self.environ:
 
745
            return self.environ['wsgiorg.routing_args'][0]
 
746
        else:
 
747
            # Since you can't update this value in-place, we don't need
 
748
            # to set the key in the environment
 
749
            return ()
 
750
 
 
751
    def _urlargs__set(self, value):
 
752
        environ = self.environ
 
753
        if 'paste.urlvars' in environ:
 
754
            # Some overlap between this and wsgiorg.routing_args; we need
 
755
            # wsgiorg.routing_args to make this work
 
756
            routing_args = (value, environ.pop('paste.urlvars'))
 
757
        elif 'wsgiorg.routing_args' in environ:
 
758
            routing_args = (value, environ['wsgiorg.routing_args'][1])
 
759
        else:
 
760
            routing_args = (value, {})
 
761
        environ['wsgiorg.routing_args'] = routing_args
 
762
 
 
763
    def _urlargs__del(self):
 
764
        if 'wsgiorg.routing_args' in self.environ:
 
765
            if not self.environ['wsgiorg.routing_args'][1]:
 
766
                del self.environ['wsgiorg.routing_args']
 
767
            else:
 
768
                self.environ['wsgiorg.routing_args'] = ((), self.environ['wsgiorg.routing_args'][1])
 
769
 
 
770
    urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__)
 
771
 
 
772
    def is_xhr(self):
 
773
        """Returns a boolean if X-Requested-With is present and ``XMLHttpRequest``
 
774
 
 
775
        Note: this isn't set by every XMLHttpRequest request, it is
 
776
        only set if you are using a Javascript library that sets it
 
777
        (or you set the header yourself manually).  Currently
 
778
        Prototype and jQuery are known to set this header."""
 
779
        return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
 
780
    is_xhr = property(is_xhr, doc=is_xhr.__doc__)
 
781
 
 
782
    def _host__get(self):
 
783
        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
 
784
        if 'HTTP_HOST' in self.environ:
 
785
            return self.environ['HTTP_HOST']
 
786
        else:
 
787
            return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
 
788
    def _host__set(self, value):
 
789
        self.environ['HTTP_HOST'] = value
 
790
    def _host__del(self):
 
791
        if 'HTTP_HOST' in self.environ:
 
792
            del self.environ['HTTP_HOST']
 
793
    host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
 
794
 
 
795
    def _body__get(self):
 
796
        """
 
797
        Return the content of the request body.
 
798
        """
 
799
        try:
 
800
            length = int(self.environ.get('CONTENT_LENGTH', '0'))
 
801
        except ValueError:
 
802
            return ''
 
803
        c = self.body_file.read(length)
 
804
        tempfile_limit = self.request_body_tempfile_limit
 
805
        if tempfile_limit and len(c) > tempfile_limit:
 
806
            fileobj = tempfile.TemporaryFile()
 
807
            fileobj.write(c)
 
808
            fileobj.seek(0)
 
809
        else:
 
810
            fileobj = StringIO(c)
 
811
        # We don't want/need to lose CONTENT_LENGTH here (as setting
 
812
        # self.body_file would do):
 
813
        self.environ['wsgi.input'] = fileobj
 
814
        return c
 
815
 
 
816
    def _body__set(self, value):
 
817
        if value is None:
 
818
            del self.body
 
819
            return
 
820
        if not isinstance(value, str):
 
821
            raise TypeError(
 
822
                "You can only set Request.body to a str (not %r)" % type(value))
 
823
        body_file = StringIO(value)
 
824
        self.body_file = body_file
 
825
        self.environ['CONTENT_LENGTH'] = str(len(value))
 
826
 
 
827
    def _body__del(self, value):
 
828
        del self.body_file
 
829
 
 
830
    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
 
831
 
 
832
    def str_POST(self):
 
833
        """
 
834
        Return a MultiDict containing all the variables from a POST
 
835
        form request.  Does *not* return anything for non-POST
 
836
        requests or for non-form requests (returns empty dict-like
 
837
        object in that case).
 
838
        """
 
839
        env = self.environ
 
840
        if self.method != 'POST':
 
841
            return NoVars('Not a POST request')
 
842
        if 'webob._parsed_post_vars' in env:
 
843
            vars, body_file = env['webob._parsed_post_vars']
 
844
            if body_file is self.body_file:
 
845
                return vars
 
846
        # Paste compatibility:
 
847
        if 'paste.parsed_formvars' in env:
 
848
            # from paste.request.parse_formvars
 
849
            vars, body_file = env['paste.parsed_formvars']
 
850
            if body_file is self.body_file:
 
851
                # FIXME: is it okay that this isn't *our* MultiDict?
 
852
                return vars
 
853
        content_type = self.content_type
 
854
        if ';' in content_type:
 
855
            content_type = content_type.split(';', 1)[0]
 
856
        if content_type not in ('', 'application/x-www-form-urlencoded',
 
857
                                'multipart/form-data'):
 
858
            # Not an HTML form submission
 
859
            return NoVars('Not an HTML form submission (Content-Type: %s)'
 
860
                          % content_type)
 
861
        if 'CONTENT_LENGTH' not in env:
 
862
            # FieldStorage assumes a default CONTENT_LENGTH of -1, but a
 
863
            # default of 0 is better:
 
864
            env['CONTENT_TYPE'] = '0'
 
865
        fs_environ = env.copy()
 
866
        fs_environ['QUERY_STRING'] = ''
 
867
        fs = cgi.FieldStorage(fp=self.body_file,
 
868
                              environ=fs_environ,
 
869
                              keep_blank_values=True)
 
870
        vars = MultiDict.from_fieldstorage(fs)
 
871
        FakeCGIBody.update_environ(env, vars)
 
872
        env['webob._parsed_post_vars'] = (vars, self.body_file)
 
873
        return vars
 
874
 
 
875
    str_POST = property(str_POST, doc=str_POST.__doc__)
 
876
 
 
877
    str_postvars = deprecated_property(str_POST, 'str_postvars',
 
878
                                       'use str_POST instead')
 
879
 
 
880
    def POST(self):
 
881
        """
 
882
        Like ``.str_POST``, but may decode values and keys
 
883
        """
 
884
        vars = self.str_POST
 
885
        if self.charset:
 
886
            vars = UnicodeMultiDict(vars, encoding=self.charset,
 
887
                                    errors=self.unicode_errors,
 
888
                                    decode_keys=self.decode_param_names)
 
889
        return vars
 
890
 
 
891
    POST = property(POST, doc=POST.__doc__)
 
892
 
 
893
    postvars = deprecated_property(POST, 'postvars',
 
894
                                   'use POST instead')
 
895
 
 
896
    def str_GET(self):
 
897
        """
 
898
        Return a MultiDict containing all the variables from the
 
899
        QUERY_STRING.
 
900
        """
 
901
        env = self.environ
 
902
        source = env.get('QUERY_STRING', '')
 
903
        if 'webob._parsed_query_vars' in env:
 
904
            vars, qs = env['webob._parsed_query_vars']
 
905
            if qs == source:
 
906
                return vars
 
907
        if not source:
 
908
            vars = MultiDict()
 
909
        else:
 
910
            vars = MultiDict(cgi.parse_qsl(
 
911
                source, keep_blank_values=True,
 
912
                strict_parsing=False))
 
913
        env['webob._parsed_query_vars'] = (vars, source)
 
914
        return vars
 
915
 
 
916
    str_GET = property(str_GET, doc=str_GET.__doc__)
 
917
 
 
918
    str_queryvars = deprecated_property(str_GET, 'str_queryvars',
 
919
                                        'use str_GET instead')
 
920
                                        
 
921
 
 
922
    def GET(self):
 
923
        """
 
924
        Like ``.str_GET``, but may decode values and keys
 
925
        """
 
926
        vars = self.str_GET
 
927
        if self.charset:
 
928
            vars = UnicodeMultiDict(vars, encoding=self.charset,
 
929
                                    errors=self.unicode_errors,
 
930
                                    decode_keys=self.decode_param_names)
 
931
        return vars
 
932
 
 
933
    GET = property(GET, doc=GET.__doc__)
 
934
 
 
935
    queryvars = deprecated_property(GET, 'queryvars',
 
936
                                    'use GET instead')
 
937
 
 
938
    def str_params(self):
 
939
        """
 
940
        A dictionary-like object containing both the parameters from
 
941
        the query string and request body.
 
942
        """
 
943
        return NestedMultiDict(self.str_GET, self.str_POST)
 
944
 
 
945
    str_params = property(str_params, doc=str_params.__doc__)
 
946
 
 
947
    def params(self):
 
948
        """
 
949
        Like ``.str_params``, but may decode values and keys
 
950
        """
 
951
        params = self.str_params
 
952
        if self.charset:
 
953
            params = UnicodeMultiDict(params, encoding=self.charset,
 
954
                                      errors=self.unicode_errors,
 
955
                                      decode_keys=self.decode_param_names)
 
956
        return params
 
957
 
 
958
    params = property(params, doc=params.__doc__)
 
959
 
 
960
    def str_cookies(self):
 
961
        """
 
962
        Return a *plain* dictionary of cookies as found in the request.
 
963
        """
 
964
        env = self.environ
 
965
        source = env.get('HTTP_COOKIE', '')
 
966
        if 'webob._parsed_cookies' in env:
 
967
            vars, var_source = env['webob._parsed_cookies']
 
968
            if var_source == source:
 
969
                return vars
 
970
        vars = {}
 
971
        if source:
 
972
            cookies = BaseCookie()
 
973
            cookies.load(source)
 
974
            for name in cookies:
 
975
                vars[name] = cookies[name].value
 
976
        env['webob._parsed_cookies'] = (vars, source)
 
977
        return vars
 
978
 
 
979
    str_cookies = property(str_cookies, doc=str_cookies.__doc__)
 
980
 
 
981
    def cookies(self):
 
982
        """
 
983
        Like ``.str_cookies``, but may decode values and keys
 
984
        """
 
985
        vars = self.str_cookies
 
986
        if self.charset:
 
987
            vars = UnicodeMultiDict(vars, encoding=self.charset,
 
988
                                    errors=self.unicode_errors,
 
989
                                    decode_keys=self.decode_param_names)
 
990
        return vars
 
991
 
 
992
    cookies = property(cookies, doc=cookies.__doc__)
 
993
 
 
994
    def copy(self):
 
995
        """
 
996
        Copy the request and environment object.
 
997
 
 
998
        This only does a shallow copy, except of wsgi.input
 
999
        """
 
1000
        env = self.environ.copy()
 
1001
        data = self.body
 
1002
        tempfile_limit = self.request_body_tempfile_limit
 
1003
        if tempfile_limit and len(data) > tempfile_limit:
 
1004
            fileobj = tempfile.TemporaryFile()
 
1005
            fileobj.write(data)
 
1006
            fileobj.seek(0)
 
1007
        else:
 
1008
            fileobj = StringIO(data)
 
1009
        env['wsgi.input'] = fileobj
 
1010
        return self.__class__(env)
 
1011
 
 
1012
    def copy_get(self):
 
1013
        """
 
1014
        Copies the request and environment object, but turning this request
 
1015
        into a GET along the way.  If this was a POST request (or any other verb)
 
1016
        then it becomes GET, and the request body is thrown away.
 
1017
        """
 
1018
        env = self.environ.copy()
 
1019
        env['wsgi.input'] = StringIO('')
 
1020
        env['CONTENT_LENGTH'] = '0'
 
1021
        if 'CONTENT_TYPE' in env:
 
1022
            del env['CONTENT_TYPE']
 
1023
        env['REQUEST_METHOD'] = 'GET'
 
1024
        return self.__class__(env)
 
1025
 
 
1026
    def remove_conditional_headers(self, remove_encoding=True):
 
1027
        """
 
1028
        Remove headers that make the request conditional.
 
1029
 
 
1030
        These headers can cause the response to be 304 Not Modified,
 
1031
        which in some cases you may not want to be possible.
 
1032
 
 
1033
        This does not remove headers like If-Match, which are used for
 
1034
        conflict detection.
 
1035
        """
 
1036
        for key in ['HTTP_IF_MATCH', 'HTTP_IF_MODIFIED_SINCE',
 
1037
                    'HTTP_IF_RANGE', 'HTTP_RANGE']:
 
1038
            if key in self.environ:
 
1039
                del self.environ[key]
 
1040
        if remove_encoding:
 
1041
            if 'HTTP_ACCEPT_ENCODING' in self.environ:
 
1042
                del self.environ['HTTP_ACCEPT_ENCODING']
 
1043
 
 
1044
    accept = converter(
 
1045
        environ_getter('HTTP_ACCEPT', rfc_section='14.1'),
 
1046
        _parse_accept, _serialize_accept, 'MIME Accept',
 
1047
        converter_args=('Accept', MIMEAccept, MIMENilAccept))
 
1048
 
 
1049
    accept_charset = converter(
 
1050
        environ_getter('HTTP_ACCEPT_CHARSET', rfc_section='14.2'),
 
1051
        _parse_accept, _serialize_accept, 'accept header',
 
1052
        converter_args=('Accept-Charset', Accept, NilAccept))
 
1053
 
 
1054
    accept_encoding = converter(
 
1055
        environ_getter('HTTP_ACCEPT_ENCODING', rfc_section='14.3'),
 
1056
        _parse_accept, _serialize_accept, 'accept header',
 
1057
        converter_args=('Accept-Encoding', Accept, NoAccept))
 
1058
 
 
1059
    accept_language = converter(
 
1060
        environ_getter('HTTP_ACCEPT_LANGUAGE', rfc_section='14.4'),
 
1061
        _parse_accept, _serialize_accept, 'accept header',
 
1062
        converter_args=('Accept-Language', Accept, NilAccept))
 
1063
 
 
1064
    ## FIXME: 14.8 Authorization
 
1065
    ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8
 
1066
 
 
1067
    def _cache_control__get(self):
 
1068
        """
 
1069
        Get/set/modify the Cache-Control header (section `14.9
 
1070
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
 
1071
        """
 
1072
        env = self.environ
 
1073
        value = env.get('HTTP_CACHE_CONTROL', '')
 
1074
        cache_header, cache_obj = env.get('webob._cache_control', (None, None))
 
1075
        if cache_obj is not None and cache_header == value:
 
1076
            return cache_obj
 
1077
        cache_obj = CacheControl.parse(value, type='request')
 
1078
        env['webob._cache_control'] = (value, cache_obj)
 
1079
        return cache_obj
 
1080
 
 
1081
    def _cache_control__set(self, value):
 
1082
        env = self.environ
 
1083
        if not value:
 
1084
            value = ""
 
1085
        if isinstance(value, dict):
 
1086
            value = CacheControl(value, type='request')
 
1087
        elif isinstance(value, CacheControl):
 
1088
            str_value = str(value)
 
1089
            env['HTTP_CACHE_CONTROL'] = str_value
 
1090
            env['webob._cache_control'] = (str_value, value)
 
1091
        else:
 
1092
            env['HTTP_CACHE_CONTROL'] = str(value)
 
1093
            if 'webob._cache_control' in env:
 
1094
                del env['webob._cache_control']
 
1095
 
 
1096
    def _cache_control__del(self, value):
 
1097
        env = self.environ
 
1098
        if 'HTTP_CACHE_CONTROL' in env:
 
1099
            del env['HTTP_CACHE_CONTROL']
 
1100
        if 'webob._cache_control' in env:
 
1101
            del env['webob._cache_control']
 
1102
 
 
1103
    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
 
1104
 
 
1105
    date = converter(
 
1106
        environ_getter('HTTP_DATE', rfc_section='14.8'),
 
1107
        _parse_date, _serialize_date, 'HTTP date')
 
1108
 
 
1109
    if_match = converter(
 
1110
        environ_getter('HTTP_IF_MATCH', rfc_section='14.24'),
 
1111
        _parse_etag, _serialize_etag, 'ETag', converter_args=(True,))
 
1112
 
 
1113
    if_modified_since = converter(
 
1114
        environ_getter('HTTP_IF_MODIFIED_SINCE', rfc_section='14.25'),
 
1115
        _parse_date, _serialize_date, 'HTTP date')
 
1116
 
 
1117
    if_none_match = converter(
 
1118
        environ_getter('HTTP_IF_NONE_MATCH', rfc_section='14.26'),
 
1119
        _parse_etag, _serialize_etag, 'ETag', converter_args=(False,))
 
1120
 
 
1121
    if_range = converter(
 
1122
        environ_getter('HTTP_IF_RANGE', rfc_section='14.27'),
 
1123
        _parse_if_range, _serialize_if_range, 'IfRange object')
 
1124
 
 
1125
    if_unmodified_since = converter(
 
1126
        environ_getter('HTTP_IF_UNMODIFIED_SINCE', rfc_section='14.28'),
 
1127
        _parse_date, _serialize_date, 'HTTP date')
 
1128
 
 
1129
    max_forwards = converter(
 
1130
        environ_getter('HTTP_MAX_FORWARDS', rfc_section='14.31'),
 
1131
        _parse_int, _serialize_int, 'int')
 
1132
 
 
1133
    pragma = environ_getter('HTTP_PRAGMA', rfc_section='14.32')
 
1134
 
 
1135
    range = converter(
 
1136
        environ_getter('HTTP_RANGE', rfc_section='14.35'),
 
1137
        _parse_range, _serialize_range, 'Range object')
 
1138
 
 
1139
    referer = environ_getter('HTTP_REFERER', rfc_section='14.36')
 
1140
    referrer = referer
 
1141
 
 
1142
    user_agent = environ_getter('HTTP_USER_AGENT', rfc_section='14.43')
 
1143
 
 
1144
    def __repr__(self):
 
1145
        msg = '<%s at %x %s %s>' % (
 
1146
            self.__class__.__name__,
 
1147
            abs(id(self)), self.method, self.url)
 
1148
        return msg
 
1149
 
 
1150
    def __str__(self):
 
1151
        url = self.url
 
1152
        host = self.host_url
 
1153
        assert url.startswith(host)
 
1154
        url = url[len(host):]
 
1155
        if 'Host' not in self.headers:
 
1156
            self.headers['Host'] = self.host
 
1157
        parts = ['%s %s' % (self.method, url)]
 
1158
        for name, value in sorted(self.headers.items()):
 
1159
            parts.append('%s: %s' % (name, value))
 
1160
        parts.append('')
 
1161
        parts.append(self.body)
 
1162
        return '\r\n'.join(parts)
 
1163
 
 
1164
    def call_application(self, application, catch_exc_info=False):
 
1165
        """
 
1166
        Call the given WSGI application, returning ``(status_string,
 
1167
        headerlist, app_iter)``
 
1168
 
 
1169
        Be sure to call ``app_iter.close()`` if it's there.
 
1170
 
 
1171
        If catch_exc_info is true, then returns ``(status_string,
 
1172
        headerlist, app_iter, exc_info)``, where the fourth item may
 
1173
        be None, but won't be if there was an exception.  If you don't
 
1174
        do this and there was an exception, the exception will be
 
1175
        raised directly.
 
1176
        """
 
1177
        captured = []
 
1178
        output = []
 
1179
        def start_response(status, headers, exc_info=None):
 
1180
            if exc_info is not None and not catch_exc_info:
 
1181
                raise exc_info[0], exc_info[1], exc_info[2]
 
1182
            captured[:] = [status, headers, exc_info]
 
1183
            return output.append
 
1184
        app_iter = application(self.environ, start_response)
 
1185
        if (not captured
 
1186
            or output):
 
1187
            try:
 
1188
                output.extend(app_iter)
 
1189
            finally:
 
1190
                if hasattr(app_iter, 'close'):
 
1191
                    app_iter.close()
 
1192
            app_iter = output
 
1193
        if catch_exc_info:
 
1194
            return (captured[0], captured[1], app_iter, captured[2])
 
1195
        else:
 
1196
            return (captured[0], captured[1], app_iter)
 
1197
 
 
1198
    # Will be filled in later:
 
1199
    ResponseClass = None
 
1200
 
 
1201
    def get_response(self, application, catch_exc_info=False):
 
1202
        """
 
1203
        Like ``.call_application(application)``, except returns a
 
1204
        response object with ``.status``, ``.headers``, and ``.body``
 
1205
        attributes.
 
1206
 
 
1207
        This will use ``self.ResponseClass`` to figure out the class
 
1208
        of the response object to return.
 
1209
        """
 
1210
        if catch_exc_info:
 
1211
            status, headers, app_iter, exc_info = self.call_application(
 
1212
                application, catch_exc_info=True)
 
1213
            del exc_info
 
1214
        else:
 
1215
            status, headers, app_iter = self.call_application(
 
1216
                application, catch_exc_info=False)
 
1217
        return self.ResponseClass(
 
1218
            status=status, headerlist=headers, app_iter=app_iter,
 
1219
            request=self)
 
1220
 
 
1221
    #@classmethod
 
1222
    def blank(cls, path, environ=None, base_url=None, headers=None):
 
1223
        """
 
1224
        Create a blank request environ (and Request wrapper) with the
 
1225
        given path (path should be urlencoded), and any keys from
 
1226
        environ.
 
1227
 
 
1228
        The path will become path_info, with any query string split
 
1229
        off and used.
 
1230
 
 
1231
        All necessary keys will be added to the environ, but the
 
1232
        values you pass in will take precedence.  If you pass in
 
1233
        base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
 
1234
        be filled in from that value.
 
1235
        """
 
1236
        if _SCHEME_RE.search(path):
 
1237
            scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
 
1238
            if fragment:
 
1239
                raise TypeError(
 
1240
                    "Path cannot contain a fragment (%r)" % fragment)
 
1241
            if qs:
 
1242
                path += '?' + qs
 
1243
            if ':' not in netloc:
 
1244
                if scheme == 'http':
 
1245
                    netloc += ':80'
 
1246
                elif scheme == 'https':
 
1247
                    netloc += ':443'
 
1248
                else:
 
1249
                    raise TypeError("Unknown scheme: %r" % scheme)
 
1250
        else:
 
1251
            scheme = 'http'
 
1252
            netloc = 'localhost:80'
 
1253
        if path and '?' in path:
 
1254
            path_info, query_string = path.split('?', 1)
 
1255
            path_info = urllib.unquote(path_info)
 
1256
        else:
 
1257
            path_info = urllib.unquote(path)
 
1258
            query_string = ''
 
1259
        env = {
 
1260
            'REQUEST_METHOD': 'GET',
 
1261
            'SCRIPT_NAME': '',
 
1262
            'PATH_INFO': path_info or '',
 
1263
            'QUERY_STRING': query_string,
 
1264
            'SERVER_NAME': netloc.split(':')[0],
 
1265
            'SERVER_PORT': netloc.split(':')[1],
 
1266
            'HTTP_HOST': netloc,
 
1267
            'SERVER_PROTOCOL': 'HTTP/1.0',
 
1268
            'wsgi.version': (1, 0),
 
1269
            'wsgi.url_scheme': scheme,
 
1270
            'wsgi.input': StringIO(''),
 
1271
            'wsgi.errors': sys.stderr,
 
1272
            'wsgi.multithread': False,
 
1273
            'wsgi.multiprocess': False,
 
1274
            'wsgi.run_once': False,
 
1275
            }
 
1276
        if base_url:
 
1277
            scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
 
1278
            if query or fragment:
 
1279
                raise ValueError(
 
1280
                    "base_url (%r) cannot have a query or fragment"
 
1281
                    % base_url)
 
1282
            if scheme:
 
1283
                env['wsgi.url_scheme'] = scheme
 
1284
            if netloc:
 
1285
                if ':' not in netloc:
 
1286
                    if scheme == 'http':
 
1287
                        netloc += ':80'
 
1288
                    elif scheme == 'https':
 
1289
                        netloc += ':443'
 
1290
                    else:
 
1291
                        raise ValueError(
 
1292
                            "Unknown scheme: %r" % scheme)
 
1293
                host, port = netloc.split(':', 1)
 
1294
                env['SERVER_PORT'] = port
 
1295
                env['SERVER_NAME'] = host
 
1296
                env['HTTP_HOST'] = netloc
 
1297
            if path:
 
1298
                env['SCRIPT_NAME'] = urllib.unquote(path)
 
1299
        if environ:
 
1300
            env.update(environ)
 
1301
        obj = cls(env)
 
1302
        if headers is not None:
 
1303
            obj.headers.update(headers)
 
1304
        return obj
 
1305
 
 
1306
    blank = classmethod(blank)
 
1307
 
 
1308
class Response(object):
 
1309
 
 
1310
    """
 
1311
    Represents a WSGI response
 
1312
    """
 
1313
 
 
1314
    default_content_type = 'text/html'
 
1315
    default_charset = 'utf8'
 
1316
    default_conditional_response = False
 
1317
 
 
1318
    def __init__(self, body=None, status='200 OK', headerlist=None, app_iter=None,
 
1319
                 request=None, content_type=None, conditional_response=NoDefault,
 
1320
                 **kw):
 
1321
        if app_iter is None:
 
1322
            if body is None:
 
1323
                body = ''
 
1324
        elif body is not None:
 
1325
            raise TypeError(
 
1326
                "You may only give one of the body and app_iter arguments")
 
1327
        self.status = status
 
1328
        if headerlist is None:
 
1329
            self._headerlist = []
 
1330
        else:
 
1331
            self._headerlist = headerlist
 
1332
        self._headers = None
 
1333
        if request is not None:
 
1334
            if hasattr(request, 'environ'):
 
1335
                self._environ = request.environ
 
1336
                self._request = request
 
1337
            else:
 
1338
                self._environ = request
 
1339
                self._request = None
 
1340
        else:
 
1341
            self._environ = self._request = None
 
1342
        if content_type is not None:
 
1343
            self.content_type = content_type
 
1344
        elif self.default_content_type is not None and headerlist is None:
 
1345
            self.content_type = self.default_content_type
 
1346
        if conditional_response is NoDefault:
 
1347
            self.conditional_response = self.default_conditional_response
 
1348
        else:
 
1349
            self.conditional_response = conditional_response
 
1350
        if 'charset' in kw:
 
1351
            # We set this early, so something like unicode_body works later
 
1352
            value = kw.pop('charset')
 
1353
            if value:
 
1354
                self.charset = value
 
1355
        elif self.default_charset and not self.charset and headerlist is None:
 
1356
            ct = self.content_type
 
1357
            if ct and (ct.startswith('text/') or ct.startswith('application/xml')
 
1358
                       or (ct.startswith('application/') and ct.endswith('+xml'))):
 
1359
                self.charset = self.default_charset
 
1360
        if app_iter is not None:
 
1361
            self._app_iter = app_iter
 
1362
            self._body = None
 
1363
        else:
 
1364
            if isinstance(body, unicode):
 
1365
                self.unicode_body = body
 
1366
            else:
 
1367
                self.body = body
 
1368
            self._app_iter = None
 
1369
        for name, value in kw.items():
 
1370
            if not hasattr(self.__class__, name):
 
1371
                # Not a basic attribute
 
1372
                raise TypeError(
 
1373
                    "Unexpected keyword: %s=%r in %r" % (name, value))
 
1374
            setattr(self, name, value)
 
1375
 
 
1376
    def __repr__(self):
 
1377
        return '<%s %x %s>' % (
 
1378
            self.__class__.__name__,
 
1379
            abs(id(self)),
 
1380
            self.status)
 
1381
 
 
1382
    def __str__(self):
 
1383
        return (self.status + '\n'
 
1384
                + '\n'.join(['%s: %s' % (name, value)
 
1385
                             for name, value in self.headerlist])
 
1386
                + '\n\n'
 
1387
                + self.body)
 
1388
 
 
1389
    def _status__get(self):
 
1390
        """
 
1391
        The status string
 
1392
        """
 
1393
        return self._status
 
1394
 
 
1395
    def _status__set(self, value):
 
1396
        if isinstance(value, int):
 
1397
            value = str(value)
 
1398
        if not isinstance(value, str):
 
1399
            raise TypeError(
 
1400
                "You must set status to a string or integer (not %s)"
 
1401
                % type(value))
 
1402
        if ' ' not in value:
 
1403
            # Need to add a reason:
 
1404
            code = int(value)
 
1405
            reason = status_reasons[code]
 
1406
            value += ' ' + reason
 
1407
        self._status = value
 
1408
 
 
1409
    status = property(_status__get, _status__set, doc=_status__get.__doc__)
 
1410
 
 
1411
    def _status_int__get(self):
 
1412
        """
 
1413
        The status as an integer
 
1414
        """
 
1415
        return int(self.status.split()[0])
 
1416
    def _status_int__set(self, value):
 
1417
        self.status = value
 
1418
    status_int = property(_status_int__get, _status_int__set, doc=_status_int__get.__doc__)
 
1419
 
 
1420
    def _headerlist__get(self):
 
1421
        """
 
1422
        The list of response headers
 
1423
        """
 
1424
        return self._headerlist
 
1425
 
 
1426
    def _headerlist__set(self, value):
 
1427
        self._headers = None
 
1428
        if not isinstance(value, list):
 
1429
            if hasattr(value, 'items'):
 
1430
                value = value.items()
 
1431
            value = list(value)
 
1432
        self._headerlist = value
 
1433
 
 
1434
    def _headerlist__del(self):
 
1435
        self.headerlist = []
 
1436
 
 
1437
    headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__)
 
1438
 
 
1439
    def _charset__get(self):
 
1440
        """
 
1441
        Get/set the charset (in the Content-Type)
 
1442
        """
 
1443
        header = self.headers.get('content-type')
 
1444
        if not header:
 
1445
            return None
 
1446
        match = _CHARSET_RE.search(header)
 
1447
        if match:
 
1448
            return match.group(1)
 
1449
        return None
 
1450
 
 
1451
    def _charset__set(self, charset):
 
1452
        if charset is None:
 
1453
            del self.charset
 
1454
            return
 
1455
        try:
 
1456
            header = self.headers.pop('content-type')
 
1457
        except KeyError:
 
1458
            raise AttributeError(
 
1459
                "You cannot set the charset when no content-type is defined")
 
1460
        match = _CHARSET_RE.search(header)
 
1461
        if match:
 
1462
            header = header[:match.start()] + header[match.end():]
 
1463
        header += '; charset=%s' % charset
 
1464
        self.headers['content-type'] = header
 
1465
 
 
1466
    def _charset__del(self):
 
1467
        try:
 
1468
            header = self.headers.pop('content-type')
 
1469
        except KeyError:
 
1470
            # Don't need to remove anything
 
1471
            return
 
1472
        match = _CHARSET_RE.search(header)
 
1473
        if match:
 
1474
            header = header[:match.start()] + header[match.end():]
 
1475
        self.headers['content-type'] = header
 
1476
 
 
1477
    charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__)
 
1478
 
 
1479
    def _content_type__get(self):
 
1480
        """
 
1481
        Get/set the Content-Type header (or None), *without* the
 
1482
        charset or any parameters.
 
1483
 
 
1484
        If you include parameters (or ``;`` at all) when setting the
 
1485
        content_type, any existing parameters will be deleted;
 
1486
        otherwise they will be preserved.
 
1487
        """
 
1488
        header = self.headers.get('content-type')
 
1489
        if not header:
 
1490
            return None
 
1491
        return header.split(';', 1)[0]
 
1492
 
 
1493
    def _content_type__set(self, value):
 
1494
        if ';' not in value:
 
1495
            header = self.headers.get('content-type', '')
 
1496
            if ';' in header:
 
1497
                params = header.split(';', 1)[1]
 
1498
                value += ';' + params
 
1499
        self.headers['content-type'] = value
 
1500
 
 
1501
    def _content_type__del(self):
 
1502
        try:
 
1503
            del self.headers['content-type']
 
1504
        except KeyError:
 
1505
            pass
 
1506
 
 
1507
    content_type = property(_content_type__get, _content_type__set,
 
1508
                            _content_type__del, doc=_content_type__get.__doc__)
 
1509
 
 
1510
    def _content_type_params__get(self):
 
1511
        """
 
1512
        Returns a dictionary of all the parameters in the content type.
 
1513
        """
 
1514
        params = self.headers.get('content-type', '')
 
1515
        if ';' not in params:
 
1516
            return {}
 
1517
        params = params.split(';', 1)[1]
 
1518
        result = {}
 
1519
        for match in _PARAM_RE.finditer(params):
 
1520
            result[match.group(1)] = match.group(2) or match.group(3) or ''
 
1521
        return result
 
1522
        
 
1523
    def _content_type_params__set(self, value_dict):
 
1524
        if not value_dict:
 
1525
            del self.content_type_params
 
1526
            return
 
1527
        params = []
 
1528
        for k, v in sorted(value_dict.items()):
 
1529
            if not _OK_PARAM_RE.search(v):
 
1530
                ## FIXME: I'm not sure what to do with "'s in the parameter value
 
1531
                ## I think it might be simply illegal
 
1532
                v = '"%s"' % v.replace('"', '\\"')
 
1533
            params.append('; %s=%s' % (k, v))
 
1534
        ct = self.headers.pop('content-type', '').split(';', 1)[0]
 
1535
        ct += ''.join(params)
 
1536
        self.headers['content-type'] = ct
 
1537
 
 
1538
    def _content_type_params__del(self, value):
 
1539
        self.headers['content-type'] = self.headers.get('content-type', '').split(';', 1)[0]
 
1540
 
 
1541
    content_type_params = property(_content_type_params__get, _content_type_params__set, _content_type_params__del, doc=_content_type_params__get.__doc__)
 
1542
 
 
1543
    def _headers__get(self):
 
1544
        """
 
1545
        The headers in a dictionary-like object
 
1546
        """
 
1547
        if self._headers is None:
 
1548
            self._headers = HeaderDict.view_list(self.headerlist)
 
1549
        return self._headers
 
1550
 
 
1551
    def _headers__set(self, value):
 
1552
        if hasattr(value, 'items'):
 
1553
            value = value.items()
 
1554
        self.headerlist = value
 
1555
        self._headers = None
 
1556
 
 
1557
    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
 
1558
 
 
1559
    def _body__get(self):
 
1560
        """
 
1561
        The body of the response, as a ``str``.  This will read in the
 
1562
        entire app_iter if necessary.
 
1563
        """
 
1564
        if self._body is None:
 
1565
            if self._app_iter is None:
 
1566
                raise AttributeError(
 
1567
                    "No body has been set")
 
1568
            try:
 
1569
                self._body = ''.join(self._app_iter)
 
1570
            finally:
 
1571
                if hasattr(self._app_iter, 'close'):
 
1572
                    self._app_iter.close()
 
1573
            self._app_iter = None
 
1574
            self.content_length = len(self._body)
 
1575
        return self._body
 
1576
 
 
1577
    def _body__set(self, value):
 
1578
        if isinstance(value, unicode):
 
1579
            raise TypeError(
 
1580
                "You cannot set Response.body to a unicode object (use Response.unicode_body)")
 
1581
        if not isinstance(value, str):
 
1582
            raise TypeError(
 
1583
                "You can only set the body to a str (not %s)"
 
1584
                % type(value))
 
1585
        self._body = value
 
1586
        self.content_length = len(value)
 
1587
        self._app_iter = None
 
1588
 
 
1589
    def _body__del(self):
 
1590
        self._body = None
 
1591
        self.content_length = None
 
1592
        self._app_iter = None
 
1593
 
 
1594
    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
 
1595
 
 
1596
    def _body_file__get(self):
 
1597
        """
 
1598
        Returns a file-like object that can be used to write to the
 
1599
        body.  If you passed in a list app_iter, that app_iter will be
 
1600
        modified by writes.
 
1601
        """
 
1602
        return ResponseBodyFile(self)
 
1603
 
 
1604
    def _body_file__del(self):
 
1605
        del self.body
 
1606
 
 
1607
    body_file = property(_body_file__get, fdel=_body_file__del, doc=_body_file__get.__doc__)
 
1608
 
 
1609
    def write(self, text):
 
1610
        if isinstance(text, unicode):
 
1611
            self.unicode_body += text
 
1612
        else:
 
1613
            self.body += text
 
1614
 
 
1615
    def _unicode_body__get(self):
 
1616
        """
 
1617
        Get/set the unicode value of the body (using the charset of the Content-Type)
 
1618
        """
 
1619
        if not self.charset:
 
1620
            raise AttributeError(
 
1621
                "You cannot access Response.unicode_body unless charset is set")
 
1622
        body = self.body
 
1623
        return body.decode(self.charset)
 
1624
 
 
1625
    def _unicode_body__set(self, value):
 
1626
        if not self.charset:
 
1627
            raise AttributeError(
 
1628
                "You cannot access Response.unicode_body unless charset is set")
 
1629
        if not isinstance(value, unicode):
 
1630
            raise TypeError(
 
1631
                "You can only set Response.unicode_body to a unicode string (not %s)" % type(value))
 
1632
        self.body = value.encode(self.charset)
 
1633
 
 
1634
    def _unicode_body__del(self):
 
1635
        del self.body
 
1636
 
 
1637
    unicode_body = property(_unicode_body__get, _unicode_body__set, _unicode_body__del, doc=_unicode_body__get.__doc__)
 
1638
 
 
1639
    def _app_iter__get(self):
 
1640
        """
 
1641
        Returns the app_iter of the response.
 
1642
 
 
1643
        If body was set, this will create an app_iter from that body
 
1644
        (a single-item list)
 
1645
        """
 
1646
        if self._app_iter is None:
 
1647
            if self._body is None:
 
1648
                raise AttributeError(
 
1649
                    "No body or app_iter has been set")
 
1650
            return [self._body]
 
1651
        else:
 
1652
            return self._app_iter
 
1653
 
 
1654
    def _app_iter__set(self, value):
 
1655
        if self._body is not None:
 
1656
            # Undo the automatically-set content-length
 
1657
            self.content_length = None
 
1658
        self._app_iter = value
 
1659
        self._body = None
 
1660
 
 
1661
    def _app_iter__del(self):
 
1662
        self.content_length = None
 
1663
        self._app_iter = self._body = None
 
1664
 
 
1665
    app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__)
 
1666
 
 
1667
    def set_cookie(self, key, value='', max_age=None,
 
1668
                   path='/', domain=None, secure=None, httponly=False,
 
1669
                   version=None, comment=None):
 
1670
        """
 
1671
        Set (add) a cookie for the response
 
1672
        """
 
1673
        cookies = BaseCookie()
 
1674
        cookies[key] = value
 
1675
        for var_name, var_value in [
 
1676
            ('max_age', max_age),
 
1677
            ('path', path),
 
1678
            ('domain', domain),
 
1679
            ('secure', secure),
 
1680
            ('HttpOnly', httponly),
 
1681
            ('version', version),
 
1682
            ('comment', comment),
 
1683
            ]:
 
1684
            if var_value is not None and var_value is not False:
 
1685
                cookies[key][var_name.replace('_', '-')] = str(var_value)
 
1686
        header_value = cookies[key].output(header='').lstrip()
 
1687
        self.headerlist.append(('Set-Cookie', header_value))
 
1688
 
 
1689
    def delete_cookie(self, key, path='/', domain=None):
 
1690
        """
 
1691
        Delete a cookie from the client.  Note that path and domain must match
 
1692
        how the cookie was originally set.
 
1693
 
 
1694
        This sets the cookie to the empty string, and max_age=0 so
 
1695
        that it should expire immediately.
 
1696
        """
 
1697
        self.set_cookie(key, '', path=path, domain=domain,
 
1698
                        max_age=0)
 
1699
 
 
1700
    def unset_cookie(self, key):
 
1701
        """
 
1702
        Unset a cookie with the given name (remove it from the
 
1703
        response).  If there are multiple cookies (e.g., two cookies
 
1704
        with the same name and different paths or domains), all such
 
1705
        cookies will be deleted.
 
1706
        """
 
1707
        existing = self.headers.getall('Set-Cookie')
 
1708
        if not existing:
 
1709
            raise KeyError(
 
1710
                "No cookies at all have been set")
 
1711
        del self.headers['Set-Cookie']
 
1712
        found = False
 
1713
        for header in existing:
 
1714
            cookies = BaseCookie()
 
1715
            cookies.load(header)
 
1716
            if key in cookies:
 
1717
                found = True
 
1718
                del cookies[key]
 
1719
            header = cookies.output(header='').lstrip()
 
1720
            if header:
 
1721
                self.headers.add('Set-Cookie', header)
 
1722
        if not found:
 
1723
            raise KeyError(
 
1724
                "No cookie has been set with the name %r" % key)
 
1725
 
 
1726
    def _location__get(self):
 
1727
        """
 
1728
        Retrieve the Location header of the response, or None if there
 
1729
        is no header.  If the header is not absolute and this response
 
1730
        is associated with a request, make the header absolute.
 
1731
 
 
1732
        For more information see `section 14.30
 
1733
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30>`_.
 
1734
        """
 
1735
        if 'location' not in self.headers:
 
1736
            return None
 
1737
        location = self.headers['location']
 
1738
        if _SCHEME_RE.search(location):
 
1739
            # Absolute
 
1740
            return location
 
1741
        if self.request is not None:
 
1742
            base_uri = self.request.url
 
1743
            location = urlparse.urljoin(base_uri, location)
 
1744
        return location
 
1745
 
 
1746
    def _location__set(self, value):
 
1747
        if not _SCHEME_RE.search(value):
 
1748
            # Not absolute, see if we can make it absolute
 
1749
            if self.request is not None:
 
1750
                value = urlparse.urljoin(self.request.url, value)
 
1751
        self.headers['location'] = value
 
1752
 
 
1753
    def _location__del(self):
 
1754
        if 'location' in self.headers:
 
1755
            del self.headers['location']
 
1756
 
 
1757
    location = property(_location__get, _location__set, _location__del, doc=_location__get.__doc__)
 
1758
 
 
1759
    accept_ranges = header_getter('Accept-Ranges', rfc_section='14.5')
 
1760
 
 
1761
    age = converter(
 
1762
        header_getter('Age', rfc_section='14.6'),
 
1763
        _parse_int_safe, _serialize_int, 'int')
 
1764
 
 
1765
    allow = converter(
 
1766
        header_getter('Allow', rfc_section='14.7'),
 
1767
        _parse_list, _serialize_list, 'list')
 
1768
 
 
1769
    _cache_control_obj = None
 
1770
 
 
1771
    def _cache_control__get(self):
 
1772
        """
 
1773
        Get/set/modify the Cache-Control header (section `14.9
 
1774
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
 
1775
        """
 
1776
        value = self.headers.get('cache-control', '')
 
1777
        if self._cache_control_obj is None:
 
1778
            self._cache_control_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='response')
 
1779
            self._cache_control_obj.header_value = value
 
1780
        if self._cache_control_obj.header_value != value:
 
1781
            new_obj = CacheControl.parse(value, type='response')
 
1782
            self._cache_control_obj.properties.clear()
 
1783
            self._cache_control_obj.properties.update(new_obj.properties)
 
1784
            self._cache_control_obj.header_value = value
 
1785
        return self._cache_control_obj
 
1786
 
 
1787
    def _cache_control__set(self, value):
 
1788
        # This actually becomes a copy
 
1789
        if not value:
 
1790
            value = ""
 
1791
        if isinstance(value, dict):
 
1792
            value = CacheControl(value, 'response')
 
1793
        if isinstance(value, unicode):
 
1794
            value = str(value)
 
1795
        if isinstance(value, str):
 
1796
            if self._cache_control_obj is None:
 
1797
                self.headers['Cache-Control'] = value
 
1798
                return
 
1799
            value = CacheControl.parse(value, 'response')
 
1800
        cache = self.cache_control
 
1801
        cache.properties.clear()
 
1802
        cache.properties.update(value.properties)
 
1803
 
 
1804
    def _cache_control__del(self):
 
1805
        self.cache_control = {}
 
1806
 
 
1807
    def _update_cache_control(self, prop_dict):
 
1808
        value = serialize_cache_control(prop_dict)
 
1809
        if not value:
 
1810
            if 'Cache-Control' in self.headers:
 
1811
                del self.headers['Cache-Control']
 
1812
        else:
 
1813
            self.headers['Cache-Control'] = value
 
1814
 
 
1815
    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
 
1816
 
 
1817
    def cache_expires(self, seconds=0, **kw):
 
1818
        """
 
1819
        Set expiration on this request.  This sets the response to
 
1820
        expire in the given seconds, and any other attributes are used
 
1821
        for cache_control (e.g., private=True, etc).
 
1822
        """
 
1823
        cache_control = self.cache_control
 
1824
        if isinstance(seconds, timedelta):
 
1825
            seconds = timedelta_to_seconds(seconds)
 
1826
        if not seconds:
 
1827
            # To really expire something, you have to force a
 
1828
            # bunch of these cache control attributes, and IE may
 
1829
            # not pay attention to those still so we also set
 
1830
            # Expires.
 
1831
            cache_control.no_store = True
 
1832
            cache_control.no_cache = True
 
1833
            cache_control.must_revalidate = True
 
1834
            cache_control.max_age = 0
 
1835
            cache_control.post_check = 0
 
1836
            cache_control.pre_check = 0
 
1837
            self.expires = datetime.utcnow()
 
1838
            if 'last-modified' not in self.headers:
 
1839
                self.last_modified = datetime.utcnow()
 
1840
            self.pragma = 'no-cache'
 
1841
        else:
 
1842
            cache_control.max_age = seconds
 
1843
            self.expires = datetime.utcnow() + timedelta(seconds=seconds)
 
1844
        for name, value in kw.items():
 
1845
            setattr(cache_control, name, value)
 
1846
 
 
1847
    content_encoding = header_getter('Content-Encoding', rfc_section='14.11')
 
1848
 
 
1849
    def encode_content(self, encoding='gzip'):
 
1850
        """
 
1851
        Encode the content with the given encoding (only gzip and
 
1852
        identity are supported).
 
1853
        """
 
1854
        if encoding == 'identity':
 
1855
            return
 
1856
        if encoding != 'gzip':
 
1857
            raise ValueError(
 
1858
                "Unknown encoding: %r" % encoding)
 
1859
        if self.content_encoding:
 
1860
            if self.content_encoding == encoding:
 
1861
                return
 
1862
            self.decode_content()
 
1863
        from webob.util.safegzip import GzipFile
 
1864
        f = StringIO()
 
1865
        gzip_f = GzipFile(filename='', mode='w', fileobj=f)
 
1866
        gzip_f.write(self.body)
 
1867
        gzip_f.close()
 
1868
        new_body = f.getvalue()
 
1869
        f.close()
 
1870
        self.content_encoding = 'gzip'
 
1871
        self.body = new_body
 
1872
 
 
1873
    def decode_content(self):
 
1874
        content_encoding = self.content_encoding
 
1875
        if not content_encoding or content_encoding == 'identity':
 
1876
            return
 
1877
        if content_encoding != 'gzip':
 
1878
            raise ValueError(
 
1879
                "I don't know how to decode the content %s" % content_encoding)
 
1880
        from webob.util.safegzip import GzipFile
 
1881
        f = StringIO(self.body)
 
1882
        gzip_f = GzipFile(filename='', mode='r', fileobj=f)
 
1883
        new_body = gzip_f.read()
 
1884
        gzip_f.close()
 
1885
        f.close()
 
1886
        self.content_encoding = None
 
1887
        self.body = new_body
 
1888
 
 
1889
    content_language = converter(
 
1890
        header_getter('Content-Language', rfc_section='14.12'),
 
1891
        _parse_list, _serialize_list, 'list')
 
1892
 
 
1893
    content_location = header_getter(
 
1894
        'Content-Location', rfc_section='14.14')
 
1895
 
 
1896
    content_md5 = header_getter(
 
1897
        'Content-MD5', rfc_section='14.14')
 
1898
 
 
1899
    content_range = converter(
 
1900
        header_getter('Content-Range', rfc_section='14.16'),
 
1901
        _parse_content_range, _serialize_content_range, 'ContentRange object')
 
1902
 
 
1903
    content_length = converter(
 
1904
        header_getter('Content-Length', rfc_section='14.17'),
 
1905
        _parse_int, _serialize_int, 'int')
 
1906
 
 
1907
    date = converter(
 
1908
        header_getter('Date', rfc_section='14.18'),
 
1909
        _parse_date, _serialize_date, 'HTTP date')
 
1910
 
 
1911
    etag = header_getter('ETag', rfc_section='14.19')
 
1912
 
 
1913
    def md5_etag(self, body=None):
 
1914
        """
 
1915
        Generate an etag for the response object using an MD5 hash of
 
1916
        the body (the body parameter, or ``self.body`` if not given)
 
1917
 
 
1918
        Sets ``self.etag``
 
1919
        """
 
1920
        if body is None:
 
1921
            body = self.body
 
1922
        import md5
 
1923
        h = md5.new(body)
 
1924
        self.etag = h.digest().encode('base64').replace('\n', '').strip('=')
 
1925
 
 
1926
    expires = converter(
 
1927
        header_getter('Expires', rfc_section='14.21'),
 
1928
        _parse_date, _serialize_date, 'HTTP date')
 
1929
 
 
1930
    last_modified = converter(
 
1931
        header_getter('Last-Modified', rfc_section='14.29'),
 
1932
        _parse_date, _serialize_date, 'HTTP date')
 
1933
 
 
1934
    pragma = header_getter('Pragma', rfc_section='14.32')
 
1935
 
 
1936
    retry_after = converter(
 
1937
        header_getter('Retry-After', rfc_section='14.37'),
 
1938
        _parse_date_delta, _serialize_date_delta, 'HTTP date or delta seconds')
 
1939
 
 
1940
    server = header_getter('Server', rfc_section='14.38')
 
1941
 
 
1942
    ## FIXME: I realize response.vary += 'something' won't work.  It should.
 
1943
    ## Maybe for all listy headers.
 
1944
    vary = converter(
 
1945
        header_getter('Vary', rfc_section='14.44'),
 
1946
        _parse_list, _serialize_list, 'list')
 
1947
 
 
1948
    ## FIXME: 14.47 WWW-Authenticate
 
1949
    ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.47
 
1950
 
 
1951
 
 
1952
    def _request__get(self):
 
1953
        """
 
1954
        Return the request associated with this response if any.
 
1955
        """
 
1956
        if self._request is None and self._environ is not None:
 
1957
            self._request = self.RequestClass(self._environ)
 
1958
        return self._request
 
1959
 
 
1960
    def _request__set(self, value):
 
1961
        if value is None:
 
1962
            del self.request
 
1963
            return
 
1964
        if isinstance(value, dict):
 
1965
            self._environ = value
 
1966
            self._request = None
 
1967
        else:
 
1968
            self._request = value
 
1969
            self._environ = value.environ
 
1970
 
 
1971
    def _request__del(self):
 
1972
        self._request = self._environ = None
 
1973
 
 
1974
    request = property(_request__get, _request__set, _request__del, doc=_request__get.__doc__)
 
1975
 
 
1976
    def _environ__get(self):
 
1977
        """
 
1978
        Get/set the request environ associated with this response, if
 
1979
        any.
 
1980
        """
 
1981
        return self._environ
 
1982
 
 
1983
    def _environ__set(self, value):
 
1984
        if value is None:
 
1985
            del self.environ
 
1986
        self._environ = value
 
1987
        self._request = None
 
1988
 
 
1989
    def _environ__del(self):
 
1990
        self._request = self._environ = None
 
1991
 
 
1992
    environ = property(_environ__get, _environ__set, _environ__del, doc=_environ__get.__doc__)
 
1993
 
 
1994
    def __call__(self, environ, start_response):
 
1995
        """
 
1996
        WSGI application interface
 
1997
        """
 
1998
        if self.conditional_response:
 
1999
            return self.conditional_response_app(environ, start_response)
 
2000
        start_response(self.status, self.headerlist)
 
2001
        if environ['REQUEST_METHOD'] == 'HEAD':
 
2002
            # Special case here...
 
2003
            return []
 
2004
        return self.app_iter
 
2005
 
 
2006
    _safe_methods = ('GET', 'HEAD')
 
2007
 
 
2008
    def conditional_response_app(self, environ, start_response):
 
2009
        """
 
2010
        Like the normal __call__ interface, but checks conditional headers:
 
2011
 
 
2012
        * If-Modified-Since   (304 Not Modified; only on GET, HEAD)
 
2013
        * If-None-Match       (304 Not Modified; only on GET, HEAD)
 
2014
        * Range               (406 Partial Content; only on GET, HEAD)
 
2015
        """
 
2016
        req = self.RequestClass(environ)
 
2017
        status304 = False
 
2018
        if req.method in self._safe_methods:
 
2019
            if req.if_modified_since and self.last_modified and self.last_modified <= req.if_modified_since:
 
2020
                status304 = True
 
2021
            if req.if_none_match and self.etag:
 
2022
                ## FIXME: should a weak match be okay?
 
2023
                if self.etag in req.if_none_match:
 
2024
                    status304 = True
 
2025
                else:
 
2026
                    # Even if If-Modified-Since matched, if ETag doesn't then reject it
 
2027
                    status304 = False
 
2028
        if status304:
 
2029
            start_response('304 Not Modified', self.headerlist)
 
2030
            return []
 
2031
        if req.method == 'HEAD':
 
2032
            start_response(self.status, self.headerlist)
 
2033
            return []
 
2034
        if (req.range and req.if_range.match_response(self)
 
2035
            and self.content_range is None
 
2036
            and req.method == 'GET'
 
2037
            and self.status_int == 200):
 
2038
            content_range = req.range.content_range(self.content_length)
 
2039
            if content_range is not None:
 
2040
                app_iter = self.app_iter_range(content_range.start, content_range.stop)
 
2041
                if app_iter is not None:
 
2042
                    headers = list(self.headerlist)
 
2043
                    headers.append(('Content-Range', str(content_range)))
 
2044
                    start_response('206 Partial Content', headers)
 
2045
                    return app_iter
 
2046
        start_response(self.status, self.headerlist)
 
2047
        return self.app_iter
 
2048
 
 
2049
    def app_iter_range(self, start, stop):
 
2050
        """
 
2051
        Return a new app_iter built from the response app_iter, that
 
2052
        serves up only the given ``start:stop`` range.
 
2053
        """
 
2054
        if self._app_iter is None:
 
2055
            return [self.body[start:stop]]
 
2056
        app_iter = self.app_iter
 
2057
        if hasattr(app_iter, 'app_iter_range'):
 
2058
            return app_iter.app_iter_range(start, stop)
 
2059
        return AppIterRange(app_iter, start, stop)
 
2060
        
 
2061
        
 
2062
Request.ResponseClass = Response
 
2063
Response.RequestClass = Request
 
2064
 
 
2065
def _cgi_FieldStorage__repr__patch(self):
 
2066
    """ monkey patch for FieldStorage.__repr__
 
2067
 
 
2068
    Unbelievely, the default __repr__ on FieldStorage reads
 
2069
    the entire file content instead of being sane about it.
 
2070
    This is a simple replacement that doesn't do that
 
2071
    """
 
2072
    if self.file:
 
2073
        return "FieldStorage(%r, %r)" % (
 
2074
                self.name, self.filename)
 
2075
    return "FieldStorage(%r, %r, %r)" % (
 
2076
             self.name, self.filename, self.value)
 
2077
 
 
2078
cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
 
2079
 
 
2080
class FakeCGIBody(object):
 
2081
 
 
2082
    def __init__(self, vars):
 
2083
        self.vars = vars
 
2084
        self._body = None
 
2085
        self.position = 0
 
2086
 
 
2087
    def read(self, size=-1):
 
2088
        body = self._get_body()
 
2089
        if size == -1:
 
2090
            v = body[self.position:]
 
2091
            self.position = len(body)
 
2092
            return v
 
2093
        else:
 
2094
            v = body[self.position:self.position+size]
 
2095
            self.position = min(len(body), self.position+size)
 
2096
            return v
 
2097
 
 
2098
    def _get_body(self):
 
2099
        if self._body is None:
 
2100
            self._body = urllib.urlencode(self.vars.items())
 
2101
        return self._body
 
2102
 
 
2103
    def readline(self, size=None):
 
2104
        # We ignore size, but allow it to be hinted
 
2105
        rest = self._get_body()[self.position:]
 
2106
        next = rest.find('\r\n')
 
2107
        if next == -1:
 
2108
            return self.read()
 
2109
        self.position += next+2
 
2110
        return rest[:next+2]
 
2111
 
 
2112
    def readlines(self, hint=None):
 
2113
        # Again, allow hint but ignore
 
2114
        body = self._get_body()
 
2115
        rest = body[self.position:]
 
2116
        self.position = len(body)
 
2117
        result = []
 
2118
        while 1:
 
2119
            next = rest.find('\r\n')
 
2120
            if next == -1:
 
2121
                result.append(rest)
 
2122
                break
 
2123
            result.append(rest[:next+2])
 
2124
            rest = rest[next+2:]
 
2125
        return result
 
2126
 
 
2127
    def __iter__(self):
 
2128
        return iter(self.readlines())
 
2129
 
 
2130
    def __repr__(self):
 
2131
        inner = repr(self.vars)
 
2132
        if len(inner) > 20:
 
2133
            inner = inner[:15] + '...' + inner[-5:]
 
2134
        return '<%s at %x viewing %s>' % (
 
2135
            self.__class__.__name__,
 
2136
            abs(id(self)), inner)
 
2137
 
 
2138
    #@classmethod
 
2139
    def update_environ(cls, environ, vars):
 
2140
        obj = cls(vars)
 
2141
        environ['CONTENT_LENGTH'] = '-1'
 
2142
        environ['wsgi.input'] = obj
 
2143
 
 
2144
    update_environ = classmethod(update_environ)
 
2145
 
 
2146
class ResponseBodyFile(object):
 
2147
 
 
2148
    def __init__(self, response):
 
2149
        self.response = response
 
2150
 
 
2151
    def __repr__(self):
 
2152
        return '<body_file for %r>' % (
 
2153
            self.response)
 
2154
 
 
2155
    def close(self):
 
2156
        raise NotImplementedError(
 
2157
            "Response bodies cannot be closed")
 
2158
 
 
2159
    def flush(self):
 
2160
        pass
 
2161
 
 
2162
    def write(self, s):
 
2163
        if isinstance(s, unicode):
 
2164
            if self.response.charset is not None:
 
2165
                s = s.encode(self.response.charset)
 
2166
            else:
 
2167
                raise TypeError(
 
2168
                    "You can only write unicode to Response.body_file "
 
2169
                    "if charset has been set")
 
2170
        if not isinstance(s, str):
 
2171
            raise TypeError(
 
2172
                "You can only write str to a Response.body_file, not %s"
 
2173
                % type(s))
 
2174
        if not isinstance(self.response._app_iter, list):
 
2175
            body = self.response.body
 
2176
            if body:
 
2177
                self.response.app_iter = [body]
 
2178
            else:
 
2179
                self.response.app_iter = []
 
2180
        self.response.app_iter.append(s)
 
2181
 
 
2182
    def writelines(self, seq):
 
2183
        for item in seq:
 
2184
            self.write(item)
 
2185
        
 
2186
    closed = False
 
2187
 
 
2188
    def encoding(self):
 
2189
        """
 
2190
        The encoding of the file (inherited from response.charset)
 
2191
        """
 
2192
        return self.response.charset
 
2193
 
 
2194
    encoding = property(encoding, doc=encoding.__doc__)
 
2195
 
 
2196
    mode = 'wb'
 
2197
 
 
2198
class AppIterRange(object):
 
2199
    """
 
2200
    Wraps an app_iter, returning just a range of bytes
 
2201
    """
 
2202
 
 
2203
    def __init__(self, app_iter, start, stop):
 
2204
        assert start >= 0, "Bad start: %r" % start
 
2205
        assert stop is None or (stop >= 0 and stop >= start), (
 
2206
            "Bad stop: %r" % stop)
 
2207
        self.app_iter = app_iter
 
2208
        self.app_iterator = iter(app_iter)
 
2209
        self.start = start
 
2210
        if stop is None:
 
2211
            self.length = -1
 
2212
        else:
 
2213
            self.length = stop - start
 
2214
        if start:
 
2215
            self._served = None
 
2216
        else:
 
2217
            self._served = 0
 
2218
        if hasattr(app_iter, 'close'):
 
2219
            self.close = app_iter.close
 
2220
 
 
2221
    def __iter__(self):
 
2222
        return self
 
2223
 
 
2224
    def next(self):
 
2225
        if self._served is None:
 
2226
            # Haven't served anything; need to skip some leading bytes
 
2227
            skipped = 0
 
2228
            start = self.start
 
2229
            while 1:
 
2230
                chunk = self.app_iterator.next()
 
2231
                skipped += len(chunk)
 
2232
                extra = skipped - start
 
2233
                if extra == 0:
 
2234
                    self._served = 0
 
2235
                    break
 
2236
                elif extra > 0:
 
2237
                    self._served = extra
 
2238
                    return chunk[-extra:]
 
2239
        length = self.length
 
2240
        if length is None:
 
2241
            # Spent
 
2242
            raise StopIteration
 
2243
        chunk = self.app_iterator.next()
 
2244
        if length == -1:
 
2245
            return chunk
 
2246
        if self._served + len(chunk) > length:
 
2247
            extra = self._served + len(chunk) - length
 
2248
            self.length = None
 
2249
            return chunk[:-extra]
 
2250
        self._served += len(chunk)
 
2251
        return chunk
 
2252