0001from cStringIO import StringIO
0002import sys
0003import cgi
0004import urllib
0005import urlparse
0006import re
0007import textwrap
0008from Cookie import BaseCookie
0009from rfc822 import parsedate_tz, mktime_tz, formatdate
0010from datetime import datetime, date, timedelta, tzinfo
0011import time
0012import calendar
0013import tempfile
0014import warnings
0015from webob.datastruct import EnvironHeaders
0016from webob.multidict import MultiDict, UnicodeMultiDict, NestedMultiDict, NoVars
0017from webob.etag import AnyETag, NoETag, ETagMatcher, IfRange, NoIfRange
0018from webob.headerdict import HeaderDict
0019from webob.statusreasons import status_reasons
0020from webob.cachecontrol import CacheControl, serialize_cache_control
0021from webob.acceptparse import Accept, MIMEAccept, NilAccept, MIMENilAccept, NoAccept
0022from webob.byterange import Range, ContentRange
0023try:
0024    sorted
0025except NameError:
0026    from webob.compat import sorted
0027
0028_CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
0029_SCHEME_RE = re.compile(r'^[a-z]+:', re.I)
0030_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
0031_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
0032
0033__all__ = ['Request', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape']
0034
0035class _UTC(tzinfo):
0036    def dst(self, dt):
0037        return timedelta(0)
0038    def utcoffset(self, dt):
0039        return timedelta(0)
0040    def tzname(self, dt):
0041        return 'UTC'
0042    def __repr__(self):
0043        return 'UTC'
0044
0045UTC = _UTC()
0046
0047def html_escape(s):
0048    """HTML-escape a string or object
0049
0050    This converts any non-string objects passed into it to strings
0051    (actually, using ``unicode()``).  All values returned are
0052    non-unicode strings (using ``&#num;`` entities for all non-ASCII
0053    characters).
0054
0055    None is treated specially, and returns the empty string.
0056    """
0057    if s is None:
0058        return ''
0059    if not isinstance(s, basestring):
0060        if hasattr(s, '__unicode__'):
0061            s = unicode(s)
0062        else:
0063            s = str(s)
0064    s = cgi.escape(s, True)
0065    if isinstance(s, unicode):
0066        s = s.encode('ascii', 'xmlcharrefreplace')
0067    return s
0068
0069def timedelta_to_seconds(td):
0070    """
0071    Converts a timedelta instance to seconds.
0072    """
0073    return td.seconds + (td.days*24*60*60)
0074
0075day = timedelta(days=1)
0076week = timedelta(weeks=1)
0077hour = timedelta(hours=1)
0078minute = timedelta(minutes=1)
0079second = timedelta(seconds=1)
0080# Estimate, I know; good enough for expirations
0081month = timedelta(days=30)
0082year = timedelta(days=365)
0083
0084class _NoDefault:
0085    def __repr__(self):
0086        return '(No Default)'
0087NoDefault = _NoDefault()
0088
0089class environ_getter(object):
0090    """For delegating an attribute to a key in self.environ."""
0091
0092    def __init__(self, key, default='', default_factory=None,
0093                 settable=True, deletable=True, doc=None,
0094                 rfc_section=None):
0095        self.key = key
0096        self.default = default
0097        self.default_factory = default_factory
0098        self.settable = settable
0099        self.deletable = deletable
0100        docstring = "Gets"
0101        if self.settable:
0102            docstring += " and sets"
0103        if self.deletable:
0104            docstring += " and deletes"
0105        docstring += " the %r key from the environment." % self.key
0106        docstring += _rfc_reference(self.key, rfc_section)
0107        if doc:
0108            docstring += '\n\n' + textwrap.dedent(doc)
0109        self.__doc__ = docstring
0110
0111    def __get__(self, obj, type=None):
0112        if obj is None:
0113            return self
0114        if self.key not in obj.environ:
0115            if self.default_factory:
0116                val = obj.environ[self.key] = self.default_factory()
0117                return val
0118            else:
0119                return self.default
0120        return obj.environ[self.key]
0121
0122    def __set__(self, obj, value):
0123        if not self.settable:
0124            raise AttributeError("Read-only attribute (key %r)" % self.key)
0125        if value is None:
0126            if self.key in obj.environ:
0127                del obj.environ[self.key]
0128        else:
0129            obj.environ[self.key] = value
0130
0131    def __delete__(self, obj):
0132        if not self.deletable:
0133            raise AttributeError("You cannot delete the key %r" % self.key)
0134        del obj.environ[self.key]
0135
0136    def __repr__(self):
0137        return '<Proxy for WSGI environ %r key>' % self.key
0138
0139class header_getter(object):
0140    """For delegating an attribute to a header in self.headers"""
0141
0142    def __init__(self, header, default=None,
0143                 settable=True, deletable=True, doc=None, rfc_section=None):
0144        self.header = header
0145        self.default = default
0146        self.settable = settable
0147        self.deletable = deletable
0148        docstring = "Gets"
0149        if self.settable:
0150            docstring += " and sets"
0151        if self.deletable:
0152            docstring += " and deletes"
0153        docstring += " they header %s from the headers" % self.header
0154        docstring += _rfc_reference(self.header, rfc_section)
0155        if doc:
0156            docstring += '\n\n' + textwrap.dedent(doc)
0157        self.__doc__ = docstring
0158
0159    def __get__(self, obj, type=None):
0160        if obj is None:
0161            return self
0162        if self.header not in obj.headers:
0163            return self.default
0164        else:
0165            return obj.headers[self.header]
0166
0167    def __set__(self, obj, value):
0168        if not self.settable:
0169            raise AttributeError("Read-only attribute (header %s)" % self.header)
0170        if value is None:
0171            if self.header in obj.headers:
0172                del obj.headers[self.header]
0173        else:
0174            if isinstance(value, unicode):
0175                # This is the standard encoding for headers:
0176                value = value.encode('ISO-8859-1')
0177            obj.headers[self.header] = value
0178
0179    def __delete__(self, obj):
0180        if not self.deletable:
0181            raise AttributeError("You cannot delete the header %s" % self.header)
0182        del obj.headers[self.header]
0183
0184    def __repr__(self):
0185        return '<Proxy for header %s>' % self.header
0186
0187class converter(object):
0188    """
0189    Wraps a decorator, and applies conversion for that decorator
0190    """
0191    def __init__(self, decorator, getter_converter, setter_converter, convert_name=None, doc=None, converter_args=()):
0192        self.decorator = decorator
0193        self.getter_converter = getter_converter
0194        self.setter_converter = setter_converter
0195        self.convert_name = convert_name
0196        self.converter_args = converter_args
0197        docstring = decorator.__doc__ or ''
0198        docstring += "  Converts it as a "
0199        if convert_name:
0200            docstring += convert_name + '.'
0201        else:
0202            docstring += "%r and %r." % (getter_converter, setter_converter)
0203        if doc:
0204            docstring += '\n\n' + textwrap.dedent(doc)
0205        self.__doc__ = docstring
0206
0207    def __get__(self, obj, type=None):
0208        if obj is None:
0209            return self
0210        value = self.decorator.__get__(obj, type)
0211        return self.getter_converter(value, *self.converter_args)
0212
0213    def __set__(self, obj, value):
0214        value = self.setter_converter(value, *self.converter_args)
0215        self.decorator.__set__(obj, value)
0216
0217    def __delete__(self, obj):
0218        self.decorator.__delete__(obj)
0219
0220    def __repr__(self):
0221        if self.convert_name:
0222            name = ' %s' % self.convert_name
0223        else:
0224            name = ''
0225        return '<Converted %r%s>' % (self.decorator, name)
0226
0227def _rfc_reference(header, section):
0228    if not section:
0229        return ''
0230    major_section = section.split('.')[0]
0231    link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % (
0232        major_section, section)
0233    if header.startswith('HTTP_'):
0234        header = header[5:].title().replace('_', '-')
0235    return "  For more information on %s see `section %s <%s>`_." % (
0236        header, section, link)
0237
0238class deprecated_property(object):
0239    """
0240    Wraps a decorator, with a deprecation warning or error
0241    """
0242    def __init__(self, decorator, attr, message, warning=True):
0243        self.decorator = decorator
0244        self.attr = attr
0245        self.message = message
0246        self.warning = warning
0247
0248    def __get__(self, obj, type=None):
0249        if obj is None:
0250            return self
0251        self.warn()
0252        return self.decorator.__get__(obj, type)
0253
0254    def __set__(self, obj, value):
0255        self.warn()
0256        self.decorator.__set__(obj, value)
0257
0258    def __delete__(self, obj):
0259        self.warn()
0260        self.decorator.__delete__(obj)
0261
0262    def __repr__(self):
0263        return '<Deprecated attribute %s: %r>' % (
0264            self.attr,
0265            self.decorator)
0266
0267    def warn(self):
0268        if not self.warning:
0269            raise DeprecationWarning(
0270                'The attribute %s is deprecated: %s' % (self.attr, self.message))
0271        else:
0272            warnings.warn(
0273                'The attribute %s is deprecated: %s' % (self.attr, self.message),
0274                DeprecationWarning,
0275                stacklevel=3)
0276
0277def _parse_date(value):
0278    if not value:
0279        return None
0280    t = parsedate_tz(value)
0281    if t is None:
0282        # Could not parse
0283        return None
0284    if t[-1] is None:
0285        # No timezone given.  None would mean local time, but we'll force UTC
0286        t = t[:9] + (0,)
0287    t = mktime_tz(t)
0288    return datetime.fromtimestamp(t, UTC)
0289
0290def _serialize_date(dt):
0291    if dt is None:
0292        return None
0293    if isinstance(dt, unicode):
0294        dt = dt.encode('ascii')
0295    if isinstance(dt, str):
0296        return dt
0297    if isinstance(dt, timedelta):
0298        dt = datetime.now() + dt
0299    if isinstance(dt, (datetime, date)):
0300        dt = dt.timetuple()
0301    if isinstance(dt, (tuple, time.struct_time)):
0302        dt = calendar.timegm(dt)
0303    if not isinstance(dt, (float, int)):
0304        raise ValueError(
0305            "You must pass in a datetime, date, time tuple, or integer object, not %r" % dt)
0306    return formatdate(dt)
0307
0308def _serialize_cookie_date(dt):
0309    if dt is None:
0310        return None
0311    if isinstance(dt, unicode):
0312        dt = dt.encode('ascii')
0313    if isinstance(dt, timedelta):
0314        dt = datetime.now() + dt
0315    if isinstance(dt, (datetime, date)):
0316        dt = dt.timetuple()
0317    return time.strftime('%a, %d-%b-%Y %H:%M:%S GMT', dt)
0318
0319def _parse_date_delta(value):
0320    """
0321    like _parse_date, but also handle delta seconds
0322    """
0323    if not value:
0324        return None
0325    try:
0326        value = int(value)
0327    except ValueError:
0328        pass
0329    else:
0330        delta = timedelta(seconds=value)
0331        return datetime.now() + delta
0332    return _parse_date(value)
0333
0334def _serialize_date_delta(value):
0335    if not value and value != 0:
0336        return None
0337    if isinstance(value, (float, int)):
0338        return str(int(value))
0339    return _serialize_date(value)
0340
0341def _parse_etag(value, default=True):
0342    if value is None:
0343        value = ''
0344    value = value.strip()
0345    if not value:
0346        if default:
0347            return AnyETag
0348        else:
0349            return NoETag
0350    if value == '*':
0351        return AnyETag
0352    else:
0353        return ETagMatcher.parse(value)
0354
0355def _serialize_etag(value, default=True):
0356    if value is None:
0357        return None
0358    if value is AnyETag:
0359        if default:
0360            return None
0361        else:
0362            return '*'
0363    return str(value)
0364
0365def _parse_if_range(value):
0366    if not value:
0367        return NoIfRange
0368    else:
0369        return IfRange.parse(value)
0370
0371def _serialize_if_range(value):
0372    if value is None:
0373        return value
0374    if isinstance(value, (datetime, date)):
0375        return _serialize_date(value)
0376    if not isinstance(value, str):
0377        value = str(value)
0378    return value or None
0379
0380def _parse_range(value):
0381    if not value:
0382        return None
0383    # Might return None too:
0384    return Range.parse(value)
0385
0386def _serialize_range(value):
0387    if isinstance(value, (list, tuple)):
0388        if len(value) != 2:
0389            raise ValueError(
0390                "If setting .range to a list or tuple, it must be of length 2 (not %r)"
0391                % value)
0392        value = Range([value])
0393    if value is None:
0394        return None
0395    value = str(value)
0396    return value or None
0397
0398def _parse_int(value):
0399    if value is None or value == '':
0400        return None
0401    return int(value)
0402
0403def _parse_int_safe(value):
0404    if value is None or value == '':
0405        return None
0406    try:
0407        return int(value)
0408    except ValueError:
0409        return None
0410
0411def _serialize_int(value):
0412    if value is None:
0413        return None
0414    return str(value)
0415
0416def _parse_content_range(value):
0417    if not value or not value.strip():
0418        return None
0419    # May still return None
0420    return ContentRange.parse(value)
0421
0422def _serialize_content_range(value):
0423    if value is None:
0424        return None
0425    if isinstance(value, (tuple, list)):
0426        if len(value) not in (2, 3):
0427            raise ValueError(
0428                "When setting content_range to a list/tuple, it must "
0429                "be length 2 or 3 (not %r)" % value)
0430        if len(value) == 2:
0431            begin, end = value
0432            length = None
0433        else:
0434            begin, end, length = value
0435        value = ContentRange(begin, end, length)
0436    value = str(value).strip()
0437    if not value:
0438        return None
0439    return value
0440
0441def _parse_list(value):
0442    if value is None:
0443        return None
0444    value = value.strip()
0445    if not value:
0446        return None
0447    return [v.strip() for v in value.split(',')
0448            if v.strip()]
0449
0450def _serialize_list(value):
0451    if not value:
0452        return None
0453    if isinstance(value, unicode):
0454        value = str(value)
0455    if isinstance(value, str):
0456        return value
0457    return ', '.join(map(str, value))
0458
0459def _parse_accept(value, header_name, AcceptClass, NilClass):
0460    if not value:
0461        return NilClass(header_name)
0462    return AcceptClass(header_name, value)
0463
0464def _serialize_accept(value, header_name, AcceptClass, NilClass):
0465    if not value or isinstance(value, NilClass):
0466        return None
0467    if isinstance(value, (list, tuple, dict)):
0468        value = NilClass(header_name) + value
0469    value = str(value).strip()
0470    if not value:
0471        return None
0472    return value
0473
0474class Request(object):
0475
0476    ## Options:
0477    charset = None
0478    unicode_errors = 'strict'
0479    decode_param_names = False
0480    ## The limit after which request bodies should be stored on disk
0481    ## if they are read in (under this, and the request body is stored
0482    ## in memory):
0483    request_body_tempfile_limit = 10*1024
0484
0485    def __init__(self, environ=None, environ_getter=None, charset=NoDefault, unicode_errors=NoDefault,
0486                 decode_param_names=NoDefault):
0487        if environ is None and environ_getter is None:
0488            raise TypeError(
0489                "You must provide one of environ or environ_getter")
0490        if environ is not None and environ_getter is not None:
0491            raise TypeError(
0492                "You can only provide one of the environ and environ_getter arguments")
0493        if environ is None:
0494            self._environ_getter = environ_getter
0495        else:
0496            if not isinstance(environ, dict):
0497                raise TypeError(
0498                    "Bad type for environ: %s" % type(environ))
0499            self._environ = environ
0500        if charset is not NoDefault:
0501            self.__dict__['charset'] = charset
0502        if unicode_errors is not NoDefault:
0503            self.__dict__['unicode_errors'] = unicode_errors
0504        if decode_param_names is not NoDefault:
0505            self.__dict__['decode_param_names'] = decode_param_names
0506
0507    def __setattr__(self, attr, value, DEFAULT=[]):
0508        ## FIXME: I don't know why I need this guard (though experimentation says I do)
0509        if getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_'):
0510            object.__setattr__(self, attr, value)
0511        else:
0512            self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
0513
0514    def __getattr__(self, attr):
0515        ## FIXME: I don't know why I need this guard (though experimentation says I do)
0516        if attr in self.__class__.__dict__:
0517            return object.__getattribute__(self, attr)
0518        try:
0519            return self.environ['webob.adhoc_attrs'][attr]
0520        except KeyError:
0521            raise AttributeError(attr)
0522
0523    def __delattr__(self, attr):
0524        ## FIXME: I don't know why I need this guard (though experimentation says I do)
0525        if attr in self.__class__.__dict__:
0526            return object.__delattr__(self, attr)
0527        try:
0528            del self.environ['webob.adhoc_attrs'][attr]
0529        except KeyError:
0530            raise AttributeError(attr)
0531
0532    def environ(self):
0533        """
0534        The WSGI environment dictionary for this request
0535        """
0536        return self._environ_getter()
0537    environ = property(environ, doc=environ.__doc__)
0538
0539    def _environ_getter(self):
0540        return self._environ
0541
0542    def _body_file__get(self):
0543        """
0544        Access the body of the request (wsgi.input) as a file-like
0545        object.
0546
0547        If you set this value, CONTENT_LENGTH will also be updated
0548        (either set to -1, 0 if you delete the attribute, or if you
0549        set the attribute to a string then the length of the string).
0550        """
0551        return self.environ['wsgi.input']
0552    def _body_file__set(self, value):
0553        if isinstance(value, str):
0554            length = len(value)
0555            value = StringIO(value)
0556        else:
0557            length = -1
0558        self.environ['wsgi.input'] = value
0559        self.environ['CONTENT_LENGTH'] = str(length)
0560    def _body_file__del(self):
0561        self.environ['wsgi.input'] = StringIO('')
0562        self.environ['CONTENT_LENGTH'] = '0'
0563    body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__)
0564
0565    scheme = environ_getter('wsgi.url_scheme')
0566    method = environ_getter('REQUEST_METHOD')
0567    script_name = environ_getter('SCRIPT_NAME')
0568    path_info = environ_getter('PATH_INFO')
0569    ## FIXME: should I strip out parameters?:
0570    content_type = environ_getter('CONTENT_TYPE', rfc_section='14.17')
0571    content_length = converter(
0572        environ_getter('CONTENT_LENGTH', rfc_section='14.13'),
0573        _parse_int_safe, _serialize_int, 'int')
0574    remote_user = environ_getter('REMOTE_USER', default=None)
0575    remote_addr = environ_getter('REMOTE_ADDR', default=None)
0576    query_string = environ_getter('QUERY_STRING')
0577    server_name = environ_getter('SERVER_NAME')
0578    server_port = converter(
0579        environ_getter('SERVER_PORT'),
0580        _parse_int, _serialize_int, 'int')
0581
0582    _headers = None
0583
0584    def _headers__get(self):
0585        """
0586        All the request headers as a case-insensitive dictionary-like
0587        object.
0588        """
0589        if self._headers is None:
0590            self._headers = EnvironHeaders(self.environ)
0591        return self._headers
0592
0593    def _headers__set(self, value):
0594        self.headers.clear()
0595        self.headers.update(value)
0596
0597    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
0598
0599    def host_url(self):
0600        """
0601        The URL through the host (no path)
0602        """
0603        e = self.environ
0604        url = e['wsgi.url_scheme'] + '://'
0605        if e.get('HTTP_HOST'):
0606            host = e['HTTP_HOST']
0607            if ':' in host:
0608                host, port = host.split(':', 1)
0609            else:
0610
0611                port = None
0612        else:
0613            host = e['SERVER_NAME']
0614            port = e['SERVER_PORT']
0615        if self.environ['wsgi.url_scheme'] == 'https':
0616            if port == '443':
0617                port = None
0618        elif self.environ['wsgi.url_scheme'] == 'http':
0619            if port == '80':
0620                port = None
0621        url += host
0622        if port:
0623            url += ':%s' % port
0624        return url
0625    host_url = property(host_url, doc=host_url.__doc__)
0626
0627    def application_url(self):
0628        """
0629        The URL including SCRIPT_NAME (no PATH_INFO or query string)
0630        """
0631        return self.host_url + urllib.quote(self.environ.get('SCRIPT_NAME', ''))
0632    application_url = property(application_url, doc=application_url.__doc__)
0633
0634    def path_url(self):
0635        """
0636        The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
0637        """
0638        return self.application_url + urllib.quote(self.environ.get('PATH_INFO', ''))
0639    path_url = property(path_url, doc=path_url.__doc__)
0640
0641    def path(self):
0642        """
0643        The path of the request, without host or query string
0644        """
0645        return urllib.quote(self.script_name) + urllib.quote(self.path_info)
0646    path = property(path, doc=path.__doc__)
0647
0648    def path_qs(self):
0649        """
0650        The path of the request, without host but with query string
0651        """
0652        path = self.path
0653        qs = self.environ.get('QUERY_STRING')
0654        if qs:
0655            path += '?' + qs
0656        return path
0657    path_qs = property(path_qs, doc=path_qs.__doc__)
0658
0659    def url(self):
0660        """
0661        The full request URL, including QUERY_STRING
0662        """
0663        url = self.path_url
0664        if self.environ.get('QUERY_STRING'):
0665            url += '?' + self.environ['QUERY_STRING']
0666        return url
0667    url = property(url, doc=url.__doc__)
0668
0669    def relative_url(self, other_url, to_application=False):
0670        """
0671        Resolve other_url relative to the request URL.
0672
0673        If ``to_application`` is True, then resolve it relative to the
0674        URL with only SCRIPT_NAME
0675        """
0676        if to_application:
0677            url = self.application_url
0678            if not url.endswith('/'):
0679                url += '/'
0680        else:
0681            url = self.path_url
0682        return urlparse.urljoin(url, other_url)
0683
0684    def path_info_pop(self):
0685        """
0686        'Pops' off the next segment of PATH_INFO, pushing it onto
0687        SCRIPT_NAME, and returning the popped segment.  Returns None if
0688        there is nothing left on PATH_INFO.
0689
0690        Does not return ``''`` when there's an empty segment (like
0691        ``/path//path``); these segments are just ignored.
0692        """
0693        path = self.path_info
0694        if not path:
0695            return None
0696        while path.startswith('/'):
0697            self.script_name += '/'
0698            path = path[1:]
0699        if '/' not in path:
0700            self.script_name += path
0701            self.path_info = ''
0702            return path
0703        else:
0704            segment, path = path.split('/', 1)
0705            self.path_info = '/' + path
0706            self.script_name += segment
0707            return segment
0708
0709    def path_info_peek(self):
0710        """
0711        Returns the next segment on PATH_INFO, or None if there is no
0712        next segment.  Doesn't modify the environment.
0713        """
0714        path = self.path_info
0715        if not path:
0716            return None
0717        path = path.lstrip('/')
0718        return path.split('/', 1)[0]
0719
0720    def _urlvars__get(self):
0721        """
0722        Return any *named* variables matched in the URL.
0723
0724        Takes values from ``environ['wsgiorg.routing_args']``.
0725        Systems like ``routes`` set this value.
0726        """
0727        if 'paste.urlvars' in self.environ:
0728            return self.environ['paste.urlvars']
0729        elif 'wsgiorg.routing_args' in self.environ:
0730            return self.environ['wsgiorg.routing_args'][1]
0731        else:
0732            result = {}
0733            self.environ['wsgiorg.routing_args'] = ((), result)
0734            return result
0735
0736    def _urlvars__set(self, value):
0737        environ = self.environ
0738        if 'wsgiorg.routing_args' in environ:
0739            environ['wsgiorg.routing_args'] = (environ['wsgiorg.routing_args'][0], value)
0740            if 'paste.urlvars' in environ:
0741                del environ['paste.urlvars']
0742        elif 'paste.urlvars' in environ:
0743            environ['paste.urlvars'] = value
0744        else:
0745            environ['wsgiorg.routing_args'] = ((), value)
0746
0747    def _urlvars__del(self):
0748        if 'paste.urlvars' in self.environ:
0749            del self.environ['paste.urlvars']
0750        if 'wsgiorg.routing_args' in self.environ:
0751            if not self.environ['wsgiorg.routing_args'][0]:
0752                del self.environ['wsgiorg.routing_args']
0753            else:
0754                self.environ['wsgiorg.routing_args'] = (self.environ['wsgiorg.routing_args'][0], {})
0755
0756    urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__)
0757
0758    def _urlargs__get(self):
0759        """
0760        Return any *positional* variables matched in the URL.
0761
0762        Takes values from ``environ['wsgiorg.routing_args']``.
0763        Systems like ``routes`` set this value.
0764        """
0765        if 'wsgiorg.routing_args' in self.environ:
0766            return self.environ['wsgiorg.routing_args'][0]
0767        else:
0768            # Since you can't update this value in-place, we don't need
0769            # to set the key in the environment
0770            return ()
0771
0772    def _urlargs__set(self, value):
0773        environ = self.environ
0774        if 'paste.urlvars' in environ:
0775            # Some overlap between this and wsgiorg.routing_args; we need
0776            # wsgiorg.routing_args to make this work
0777            routing_args = (value, environ.pop('paste.urlvars'))
0778        elif 'wsgiorg.routing_args' in environ:
0779            routing_args = (value, environ['wsgiorg.routing_args'][1])
0780        else:
0781            routing_args = (value, {})
0782        environ['wsgiorg.routing_args'] = routing_args
0783
0784    def _urlargs__del(self):
0785        if 'wsgiorg.routing_args' in self.environ:
0786            if not self.environ['wsgiorg.routing_args'][1]:
0787                del self.environ['wsgiorg.routing_args']
0788            else:
0789                self.environ['wsgiorg.routing_args'] = ((), self.environ['wsgiorg.routing_args'][1])
0790
0791    urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__)
0792
0793    def is_xhr(self):
0794        """Returns a boolean if X-Requested-With is present and ``XMLHttpRequest``
0795
0796        Note: this isn't set by every XMLHttpRequest request, it is
0797        only set if you are using a Javascript library that sets it
0798        (or you set the header yourself manually).  Currently
0799        Prototype and jQuery are known to set this header."""
0800        return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
0801    is_xhr = property(is_xhr, doc=is_xhr.__doc__)
0802
0803    def _host__get(self):
0804        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
0805        if 'HTTP_HOST' in self.environ:
0806            return self.environ['HTTP_HOST']
0807        else:
0808            return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
0809    def _host__set(self, value):
0810        self.environ['HTTP_HOST'] = value
0811    def _host__del(self):
0812        if 'HTTP_HOST' in self.environ:
0813            del self.environ['HTTP_HOST']
0814    host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
0815
0816    def _body__get(self):
0817        """
0818        Return the content of the request body.
0819        """
0820        try:
0821            length = int(self.environ.get('CONTENT_LENGTH', '0'))
0822        except ValueError:
0823            return ''
0824        c = self.body_file.read(length)
0825        tempfile_limit = self.request_body_tempfile_limit
0826        if tempfile_limit and len(c) > tempfile_limit:
0827            fileobj = tempfile.TemporaryFile()
0828            fileobj.write(c)
0829            fileobj.seek(0)
0830        else:
0831            fileobj = StringIO(c)
0832        # We don't want/need to lose CONTENT_LENGTH here (as setting
0833        # self.body_file would do):
0834        self.environ['wsgi.input'] = fileobj
0835        return c
0836
0837    def _body__set(self, value):
0838        if value is None:
0839            del self.body
0840            return
0841        if not isinstance(value, str):
0842            raise TypeError(
0843                "You can only set Request.body to a str (not %r)" % type(value))
0844        body_file = StringIO(value)
0845        self.body_file = body_file
0846        self.environ['CONTENT_LENGTH'] = str(len(value))
0847
0848    def _body__del(self, value):
0849        del self.body_file
0850
0851    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
0852
0853    def str_POST(self):
0854        """
0855        Return a MultiDict containing all the variables from a POST
0856        form request.  Does *not* return anything for non-POST
0857        requests or for non-form requests (returns empty dict-like
0858        object in that case).
0859        """
0860        env = self.environ
0861        if self.method != 'POST':
0862            return NoVars('Not a POST request')
0863        if 'webob._parsed_post_vars' in env:
0864            vars, body_file = env['webob._parsed_post_vars']
0865            if body_file is self.body_file:
0866                return vars
0867        # Paste compatibility:
0868        if 'paste.parsed_formvars' in env:
0869            # from paste.request.parse_formvars
0870            vars, body_file = env['paste.parsed_formvars']
0871            if body_file is self.body_file:
0872                # FIXME: is it okay that this isn't *our* MultiDict?
0873                return vars
0874        content_type = self.content_type
0875        if ';' in content_type:
0876            content_type = content_type.split(';', 1)[0]
0877        if content_type not in ('', 'application/x-www-form-urlencoded',
0878                                'multipart/form-data'):
0879            # Not an HTML form submission
0880            return NoVars('Not an HTML form submission (Content-Type: %s)'
0881                          % content_type)
0882        if 'CONTENT_LENGTH' not in env:
0883            # FieldStorage assumes a default CONTENT_LENGTH of -1, but a
0884            # default of 0 is better:
0885            env['CONTENT_TYPE'] = '0'
0886        fs_environ = env.copy()
0887        fs_environ['QUERY_STRING'] = ''
0888        fs = cgi.FieldStorage(fp=self.body_file,
0889                              environ=fs_environ,
0890                              keep_blank_values=True)
0891        vars = MultiDict.from_fieldstorage(fs)
0892        FakeCGIBody.update_environ(env, vars)
0893        env['webob._parsed_post_vars'] = (vars, self.body_file)
0894        return vars
0895
0896    str_POST = property(str_POST, doc=str_POST.__doc__)
0897
0898    str_postvars = deprecated_property(str_POST, 'str_postvars',
0899                                       'use str_POST instead')
0900
0901    def POST(self):
0902        """
0903        Like ``.str_POST``, but may decode values and keys
0904        """
0905        vars = self.str_POST
0906        if self.charset:
0907            vars = UnicodeMultiDict(vars, encoding=self.charset,
0908                                    errors=self.unicode_errors,
0909                                    decode_keys=self.decode_param_names)
0910        return vars
0911
0912    POST = property(POST, doc=POST.__doc__)
0913
0914    postvars = deprecated_property(POST, 'postvars',
0915                                   'use POST instead')
0916
0917    def str_GET(self):
0918        """
0919        Return a MultiDict containing all the variables from the
0920        QUERY_STRING.
0921        """
0922        env = self.environ
0923        source = env.get('QUERY_STRING', '')
0924        if 'webob._parsed_query_vars' in env:
0925            vars, qs = env['webob._parsed_query_vars']
0926            if qs == source:
0927                return vars
0928        if not source:
0929            vars = MultiDict()
0930        else:
0931            vars = MultiDict(cgi.parse_qsl(
0932                source, keep_blank_values=True,
0933                strict_parsing=False))
0934        env['webob._parsed_query_vars'] = (vars, source)
0935        return vars
0936
0937    str_GET = property(str_GET, doc=str_GET.__doc__)
0938
0939    str_queryvars = deprecated_property(str_GET, 'str_queryvars',
0940                                        'use str_GET instead')
0941
0942
0943    def GET(self):
0944        """
0945        Like ``.str_GET``, but may decode values and keys
0946        """
0947        vars = self.str_GET
0948        if self.charset:
0949            vars = UnicodeMultiDict(vars, encoding=self.charset,
0950                                    errors=self.unicode_errors,
0951                                    decode_keys=self.decode_param_names)
0952        return vars
0953
0954    GET = property(GET, doc=GET.__doc__)
0955
0956    queryvars = deprecated_property(GET, 'queryvars',
0957                                    'use GET instead')
0958
0959    def str_params(self):
0960        """
0961        A dictionary-like object containing both the parameters from
0962        the query string and request body.
0963        """
0964        return NestedMultiDict(self.str_GET, self.str_POST)
0965
0966    str_params = property(str_params, doc=str_params.__doc__)
0967
0968    def params(self):
0969        """
0970        Like ``.str_params``, but may decode values and keys
0971        """
0972        params = self.str_params
0973        if self.charset:
0974            params = UnicodeMultiDict(params, encoding=self.charset,
0975                                      errors=self.unicode_errors,
0976                                      decode_keys=self.decode_param_names)
0977        return params
0978
0979    params = property(params, doc=params.__doc__)
0980
0981    _rx_quotes = re.compile('"(.*)"')
0982
0983    def str_cookies(self):
0984        """
0985        Return a *plain* dictionary of cookies as found in the request.
0986        """
0987        env = self.environ
0988        source = env.get('HTTP_COOKIE', '')
0989        if 'webob._parsed_cookies' in env:
0990            vars, var_source = env['webob._parsed_cookies']
0991            if var_source == source:
0992                return vars
0993        vars = {}
0994        if source:
0995            cookies = BaseCookie()
0996            cookies.load(source)
0997            for name in cookies:
0998                value = cookies[name].value
0999                unquote_match = self._rx_quotes.match(value)
1000                if unquote_match is not None:
1001                    value = unquote_match.group(1)
1002                vars[name] = value
1003        env['webob._parsed_cookies'] = (vars, source)
1004        return vars
1005
1006    str_cookies = property(str_cookies, doc=str_cookies.__doc__)
1007
1008    def cookies(self):
1009        """
1010        Like ``.str_cookies``, but may decode values and keys
1011        """
1012        vars = self.str_cookies
1013        if self.charset:
1014            vars = UnicodeMultiDict(vars, encoding=self.charset,
1015                                    errors=self.unicode_errors,
1016                                    decode_keys=self.decode_param_names)
1017        return vars
1018
1019    cookies = property(cookies, doc=cookies.__doc__)
1020
1021    def copy(self):
1022        """
1023        Copy the request and environment object.
1024
1025        This only does a shallow copy, except of wsgi.input
1026        """
1027        env = self.environ.copy()
1028        data = self.body
1029        tempfile_limit = self.request_body_tempfile_limit
1030        if tempfile_limit and len(data) > tempfile_limit:
1031            fileobj = tempfile.TemporaryFile()
1032            fileobj.write(data)
1033            fileobj.seek(0)
1034        else:
1035            fileobj = StringIO(data)
1036        env['wsgi.input'] = fileobj
1037        return self.__class__(env)
1038
1039    def copy_get(self):
1040        """
1041        Copies the request and environment object, but turning this request
1042        into a GET along the way.  If this was a POST request (or any other verb)
1043        then it becomes GET, and the request body is thrown away.
1044        """
1045        env = self.environ.copy()
1046        env['wsgi.input'] = StringIO('')
1047        env['CONTENT_LENGTH'] = '0'
1048        if 'CONTENT_TYPE' in env:
1049            del env['CONTENT_TYPE']
1050        env['REQUEST_METHOD'] = 'GET'
1051        return self.__class__(env)
1052
1053    def remove_conditional_headers(self, remove_encoding=True):
1054        """
1055        Remove headers that make the request conditional.
1056
1057        These headers can cause the response to be 304 Not Modified,
1058        which in some cases you may not want to be possible.
1059
1060        This does not remove headers like If-Match, which are used for
1061        conflict detection.
1062        """
1063        for key in ['HTTP_IF_MATCH', 'HTTP_IF_MODIFIED_SINCE',
1064                    'HTTP_IF_RANGE', 'HTTP_RANGE']:
1065            if key in self.environ:
1066                del self.environ[key]
1067        if remove_encoding:
1068            if 'HTTP_ACCEPT_ENCODING' in self.environ:
1069                del self.environ['HTTP_ACCEPT_ENCODING']
1070
1071    accept = converter(
1072        environ_getter('HTTP_ACCEPT', rfc_section='14.1'),
1073        _parse_accept, _serialize_accept, 'MIME Accept',
1074        converter_args=('Accept', MIMEAccept, MIMENilAccept))
1075
1076    accept_charset = converter(
1077        environ_getter('HTTP_ACCEPT_CHARSET', rfc_section='14.2'),
1078        _parse_accept, _serialize_accept, 'accept header',
1079        converter_args=('Accept-Charset', Accept, NilAccept))
1080
1081    accept_encoding = converter(
1082        environ_getter('HTTP_ACCEPT_ENCODING', rfc_section='14.3'),
1083        _parse_accept, _serialize_accept, 'accept header',
1084        converter_args=('Accept-Encoding', Accept, NoAccept))
1085
1086    accept_language = converter(
1087        environ_getter('HTTP_ACCEPT_LANGUAGE', rfc_section='14.4'),
1088        _parse_accept, _serialize_accept, 'accept header',
1089        converter_args=('Accept-Language', Accept, NilAccept))
1090
1091    ## FIXME: 14.8 Authorization
1092    ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8
1093
1094    def _cache_control__get(self):
1095        """
1096        Get/set/modify the Cache-Control header (section `14.9
1097        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
1098        """
1099        env = self.environ
1100        value = env.get('HTTP_CACHE_CONTROL', '')
1101        cache_header, cache_obj = env.get('webob._cache_control', (None, None))
1102        if cache_obj is not None and cache_header == value:
1103            return cache_obj
1104        cache_obj = CacheControl.parse(value, type='request')
1105        env['webob._cache_control'] = (value, cache_obj)
1106        return cache_obj
1107
1108    def _cache_control__set(self, value):
1109        env = self.environ
1110        if not value:
1111            value = ""
1112        if isinstance(value, dict):
1113            value = CacheControl(value, type='request')
1114        elif isinstance(value, CacheControl):
1115            str_value = str(value)
1116            env['HTTP_CACHE_CONTROL'] = str_value
1117            env['webob._cache_control'] = (str_value, value)
1118        else:
1119            env['HTTP_CACHE_CONTROL'] = str(value)
1120            if 'webob._cache_control' in env:
1121                del env['webob._cache_control']
1122
1123    def _cache_control__del(self, value):
1124        env = self.environ
1125        if 'HTTP_CACHE_CONTROL' in env:
1126            del env['HTTP_CACHE_CONTROL']
1127        if 'webob._cache_control' in env:
1128            del env['webob._cache_control']
1129
1130    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
1131
1132    date = converter(
1133        environ_getter('HTTP_DATE', rfc_section='14.8'),
1134        _parse_date, _serialize_date, 'HTTP date')
1135
1136    if_match = converter(
1137        environ_getter('HTTP_IF_MATCH', rfc_section='14.24'),
1138        _parse_etag, _serialize_etag, 'ETag', converter_args=(True,))
1139
1140    if_modified_since = converter(
1141        environ_getter('HTTP_IF_MODIFIED_SINCE', rfc_section='14.25'),
1142        _parse_date, _serialize_date, 'HTTP date')
1143
1144    if_none_match = converter(
1145        environ_getter('HTTP_IF_NONE_MATCH', rfc_section='14.26'),
1146        _parse_etag, _serialize_etag, 'ETag', converter_args=(False,))
1147
1148    if_range = converter(
1149        environ_getter('HTTP_IF_RANGE', rfc_section='14.27'),
1150        _parse_if_range, _serialize_if_range, 'IfRange object')
1151
1152    if_unmodified_since = converter(
1153        environ_getter('HTTP_IF_UNMODIFIED_SINCE', rfc_section='14.28'),
1154        _parse_date, _serialize_date, 'HTTP date')
1155
1156    max_forwards = converter(
1157        environ_getter('HTTP_MAX_FORWARDS', rfc_section='14.31'),
1158        _parse_int, _serialize_int, 'int')
1159
1160    pragma = environ_getter('HTTP_PRAGMA', rfc_section='14.32')
1161
1162    range = converter(
1163        environ_getter('HTTP_RANGE', rfc_section='14.35'),
1164        _parse_range, _serialize_range, 'Range object')
1165
1166    referer = environ_getter('HTTP_REFERER', rfc_section='14.36')
1167    referrer = referer
1168
1169    user_agent = environ_getter('HTTP_USER_AGENT', rfc_section='14.43')
1170
1171    def __repr__(self):
1172        msg = '<%s at %x %s %s>' % (
1173            self.__class__.__name__,
1174            abs(id(self)), self.method, self.url)
1175        return msg
1176
1177    def __str__(self):
1178        url = self.url
1179        host = self.host_url
1180        assert url.startswith(host)
1181        url = url[len(host):]
1182        if 'Host' not in self.headers:
1183            self.headers['Host'] = self.host
1184        parts = ['%s %s' % (self.method, url)]
1185        for name, value in sorted(self.headers.items()):
1186            parts.append('%s: %s' % (name, value))
1187        parts.append('')
1188        parts.append(self.body)
1189        return '\r\n'.join(parts)
1190
1191    def call_application(self, application, catch_exc_info=False):
1192        """
1193        Call the given WSGI application, returning ``(status_string,
1194        headerlist, app_iter)``
1195
1196        Be sure to call ``app_iter.close()`` if it's there.
1197
1198        If catch_exc_info is true, then returns ``(status_string,
1199        headerlist, app_iter, exc_info)``, where the fourth item may
1200        be None, but won't be if there was an exception.  If you don't
1201        do this and there was an exception, the exception will be
1202        raised directly.
1203        """
1204        captured = []
1205        output = []
1206        def start_response(status, headers, exc_info=None):
1207            if exc_info is not None and not catch_exc_info:
1208                raise exc_info[0], exc_info[1], exc_info[2]
1209            captured[:] = [status, headers, exc_info]
1210            return output.append
1211        app_iter = application(self.environ, start_response)
1212        if (not captured
1213            or output):
1214            try:
1215                output.extend(app_iter)
1216            finally:
1217                if hasattr(app_iter, 'close'):
1218                    app_iter.close()
1219            app_iter = output
1220        if catch_exc_info:
1221            return (captured[0], captured[1], app_iter, captured[2])
1222        else:
1223            return (captured[0], captured[1], app_iter)
1224
1225    # Will be filled in later:
1226    ResponseClass = None
1227
1228    def get_response(self, application, catch_exc_info=False):
1229        """
1230        Like ``.call_application(application)``, except returns a
1231        response object with ``.status``, ``.headers``, and ``.body``
1232        attributes.
1233
1234        This will use ``self.ResponseClass`` to figure out the class
1235        of the response object to return.
1236        """
1237        if catch_exc_info:
1238            status, headers, app_iter, exc_info = self.call_application(
1239                application, catch_exc_info=True)
1240            del exc_info
1241        else:
1242            status, headers, app_iter = self.call_application(
1243                application, catch_exc_info=False)
1244        return self.ResponseClass(
1245            status=status, headerlist=headers, app_iter=app_iter,
1246            request=self)
1247
1248    #@classmethod
1249    def blank(cls, path, environ=None, base_url=None, headers=None, **kw):
1250        """
1251        Create a blank request environ (and Request wrapper) with the
1252        given path (path should be urlencoded), and any keys from
1253        environ.
1254
1255        The path will become path_info, with any query string split
1256        off and used.
1257
1258        All necessary keys will be added to the environ, but the
1259        values you pass in will take precedence.  If you pass in
1260        base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
1261        be filled in from that value.
1262
1263        Any extra keyword will be passed to ``__init__`` (e.g.,
1264        ``decode_param_names``).
1265        """
1266        if _SCHEME_RE.search(path):
1267            scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
1268            if fragment:
1269                raise TypeError(
1270                    "Path cannot contain a fragment (%r)" % fragment)
1271            if qs:
1272                path += '?' + qs
1273            if ':' not in netloc:
1274                if scheme == 'http':
1275                    netloc += ':80'
1276                elif scheme == 'https':
1277                    netloc += ':443'
1278                else:
1279                    raise TypeError("Unknown scheme: %r" % scheme)
1280        else:
1281            scheme = 'http'
1282            netloc = 'localhost:80'
1283        if path and '?' in path:
1284            path_info, query_string = path.split('?', 1)
1285            path_info = urllib.unquote(path_info)
1286        else:
1287            path_info = urllib.unquote(path)
1288            query_string = ''
1289        env = {
1290            'REQUEST_METHOD': 'GET',
1291            'SCRIPT_NAME': '',
1292            'PATH_INFO': path_info or '',
1293            'QUERY_STRING': query_string,
1294            'SERVER_NAME': netloc.split(':')[0],
1295            'SERVER_PORT': netloc.split(':')[1],
1296            'HTTP_HOST': netloc,
1297            'SERVER_PROTOCOL': 'HTTP/1.0',
1298            'wsgi.version': (1, 0),
1299            'wsgi.url_scheme': scheme,
1300            'wsgi.input': StringIO(''),
1301            'wsgi.errors': sys.stderr,
1302            'wsgi.multithread': False,
1303            'wsgi.multiprocess': False,
1304            'wsgi.run_once': False,
1305            }
1306        if base_url:
1307            scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
1308            if query or fragment:
1309                raise ValueError(
1310                    "base_url (%r) cannot have a query or fragment"
1311                    % base_url)
1312            if scheme:
1313                env['wsgi.url_scheme'] = scheme
1314            if netloc:
1315                if ':' not in netloc:
1316                    if scheme == 'http':
1317                        netloc += ':80'
1318                    elif scheme == 'https':
1319                        netloc += ':443'
1320                    else:
1321                        raise ValueError(
1322                            "Unknown scheme: %r" % scheme)
1323                host, port = netloc.split(':', 1)
1324                env['SERVER_PORT'] = port
1325                env['SERVER_NAME'] = host
1326                env['HTTP_HOST'] = netloc
1327            if path:
1328                env['SCRIPT_NAME'] = urllib.unquote(path)
1329        if environ:
1330            env.update(environ)
1331        obj = cls(env, **kw)
1332        if headers is not None:
1333            obj.headers.update(headers)
1334        return obj
1335
1336    blank = classmethod(blank)
1337
1338class Response(object):
1339
1340    """
1341    Represents a WSGI response
1342    """
1343
1344    default_content_type = 'text/html'
1345    default_charset = 'UTF-8'
1346    default_conditional_response = False
1347
1348    def __init__(self, body=None, status='200 OK', headerlist=None, app_iter=None,
1349                 request=None, content_type=None, conditional_response=NoDefault,
1350                 **kw):
1351        if app_iter is None:
1352            if body is None:
1353                body = ''
1354        elif body is not None:
1355            raise TypeError(
1356                "You may only give one of the body and app_iter arguments")
1357        self.status = status
1358        if headerlist is None:
1359            self._headerlist = []
1360        else:
1361            self._headerlist = headerlist
1362        self._headers = None
1363        if request is not None:
1364            if hasattr(request, 'environ'):
1365                self._environ = request.environ
1366                self._request = request
1367            else:
1368                self._environ = request
1369                self._request = None
1370        else:
1371            self._environ = self._request = None
1372        if content_type is not None:
1373            self.content_type = content_type
1374        elif self.default_content_type is not None and headerlist is None:
1375            self.content_type = self.default_content_type
1376        if conditional_response is NoDefault:
1377            self.conditional_response = self.default_conditional_response
1378        else:
1379            self.conditional_response = conditional_response
1380        if 'charset' in kw:
1381            # We set this early, so something like unicode_body works later
1382            value = kw.pop('charset')
1383            if value:
1384                self.charset = value
1385        elif self.default_charset and not self.charset and headerlist is None:
1386            ct = self.content_type
1387            if ct and (ct.startswith('text/') or ct.startswith('application/xml')
1388                       or (ct.startswith('application/') and ct.endswith('+xml'))):
1389                self.charset = self.default_charset
1390        if app_iter is not None:
1391            self._app_iter = app_iter
1392            self._body = None
1393        else:
1394            if isinstance(body, unicode):
1395                self.unicode_body = body
1396            else:
1397                self.body = body
1398            self._app_iter = None
1399        for name, value in kw.items():
1400            if not hasattr(self.__class__, name):
1401                # Not a basic attribute
1402                raise TypeError(
1403                    "Unexpected keyword: %s=%r in %r" % (name, value))
1404            setattr(self, name, value)
1405
1406    def __repr__(self):
1407        return '<%s %x %s>' % (
1408            self.__class__.__name__,
1409            abs(id(self)),
1410            self.status)
1411
1412    def __str__(self):
1413        return (self.status + '\n'
1414                + '\n'.join(['%s: %s' % (name, value)
1415                             for name, value in self.headerlist])
1416                + '\n\n'
1417                + self.body)
1418
1419    def _status__get(self):
1420        """
1421        The status string
1422        """
1423        return self._status
1424
1425    def _status__set(self, value):
1426        if isinstance(value, int):
1427            value = str(value)
1428        if not isinstance(value, str):
1429            raise TypeError(
1430                "You must set status to a string or integer (not %s)"
1431                % type(value))
1432        if ' ' not in value:
1433            # Need to add a reason:
1434            code = int(value)
1435            reason = status_reasons[code]
1436            value += ' ' + reason
1437        self._status = value
1438
1439    status = property(_status__get, _status__set, doc=_status__get.__doc__)
1440
1441    def _status_int__get(self):
1442        """
1443        The status as an integer
1444        """
1445        return int(self.status.split()[0])
1446    def _status_int__set(self, value):
1447        self.status = value
1448    status_int = property(_status_int__get, _status_int__set, doc=_status_int__get.__doc__)
1449
1450    def _headerlist__get(self):
1451        """
1452        The list of response headers
1453        """
1454        return self._headerlist
1455
1456    def _headerlist__set(self, value):
1457        self._headers = None
1458        if not isinstance(value, list):
1459            if hasattr(value, 'items'):
1460                value = value.items()
1461            value = list(value)
1462        self._headerlist = value
1463
1464    def _headerlist__del(self):
1465        self.headerlist = []
1466
1467    headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__)
1468
1469    def _charset__get(self):
1470        """
1471        Get/set the charset (in the Content-Type)
1472        """
1473        header = self.headers.get('content-type')
1474        if not header:
1475            return None
1476        match = _CHARSET_RE.search(header)
1477        if match:
1478            return match.group(1)
1479        return None
1480
1481    def _charset__set(self, charset):
1482        if charset is None:
1483            del self.charset
1484            return
1485        try:
1486            header = self.headers.pop('content-type')
1487        except KeyError:
1488            raise AttributeError(
1489                "You cannot set the charset when no content-type is defined")
1490        match = _CHARSET_RE.search(header)
1491        if match:
1492            header = header[:match.start()] + header[match.end():]
1493        header += '; charset=%s' % charset
1494        self.headers['content-type'] = header
1495
1496    def _charset__del(self):
1497        try:
1498            header = self.headers.pop('content-type')
1499        except KeyError:
1500            # Don't need to remove anything
1501            return
1502        match = _CHARSET_RE.search(header)
1503        if match:
1504            header = header[:match.start()] + header[match.end():]
1505        self.headers['content-type'] = header
1506
1507    charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__)
1508
1509    def _content_type__get(self):
1510        """
1511        Get/set the Content-Type header (or None), *without* the
1512        charset or any parameters.
1513
1514        If you include parameters (or ``;`` at all) when setting the
1515        content_type, any existing parameters will be deleted;
1516        otherwise they will be preserved.
1517        """
1518        header = self.headers.get('content-type')
1519        if not header:
1520            return None
1521        return header.split(';', 1)[0]
1522
1523    def _content_type__set(self, value):
1524        if ';' not in value:
1525            header = self.headers.get('content-type', '')
1526            if ';' in header:
1527                params = header.split(';', 1)[1]
1528                value += ';' + params
1529        self.headers['content-type'] = value
1530
1531    def _content_type__del(self):
1532        try:
1533            del self.headers['content-type']
1534        except KeyError:
1535            pass
1536
1537    content_type = property(_content_type__get, _content_type__set,
1538                            _content_type__del, doc=_content_type__get.__doc__)
1539
1540    def _content_type_params__get(self):
1541        """
1542        Returns a dictionary of all the parameters in the content type.
1543        """
1544        params = self.headers.get('content-type', '')
1545        if ';' not in params:
1546            return {}
1547        params = params.split(';', 1)[1]
1548        result = {}
1549        for match in _PARAM_RE.finditer(params):
1550            result[match.group(1)] = match.group(2) or match.group(3) or ''
1551        return result
1552
1553    def _content_type_params__set(self, value_dict):
1554        if not value_dict:
1555            del self.content_type_params
1556            return
1557        params = []
1558        for k, v in sorted(value_dict.items()):
1559            if not _OK_PARAM_RE.search(v):
1560                ## FIXME: I'm not sure what to do with "'s in the parameter value
1561                ## I think it might be simply illegal
1562                v = '"%s"' % v.replace('"', '\\"')
1563            params.append('; %s=%s' % (k, v))
1564        ct = self.headers.pop('content-type', '').split(';', 1)[0]
1565        ct += ''.join(params)
1566        self.headers['content-type'] = ct
1567
1568    def _content_type_params__del(self, value):
1569        self.headers['content-type'] = self.headers.get('content-type', '').split(';', 1)[0]
1570
1571    content_type_params = property(_content_type_params__get, _content_type_params__set, _content_type_params__del, doc=_content_type_params__get.__doc__)
1572
1573    def _headers__get(self):
1574        """
1575        The headers in a dictionary-like object
1576        """
1577        if self._headers is None:
1578            self._headers = HeaderDict.view_list(self.headerlist)
1579        return self._headers
1580
1581    def _headers__set(self, value):
1582        if hasattr(value, 'items'):
1583            value = value.items()
1584        self.headerlist = value
1585        self._headers = None
1586
1587    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
1588
1589    def _body__get(self):
1590        """
1591        The body of the response, as a ``str``.  This will read in the
1592        entire app_iter if necessary.
1593        """
1594        if self._body is None:
1595            if self._app_iter is None:
1596                raise AttributeError(
1597                    "No body has been set")
1598            try:
1599                self._body = ''.join(self._app_iter)
1600            finally:
1601                if hasattr(self._app_iter, 'close'):
1602                    self._app_iter.close()
1603            self._app_iter = None
1604            self.content_length = len(self._body)
1605        return self._body
1606
1607    def _body__set(self, value):
1608        if isinstance(value, unicode):
1609            raise TypeError(
1610                "You cannot set Response.body to a unicode object (use Response.unicode_body)")
1611        if not isinstance(value, str):
1612            raise TypeError(
1613                "You can only set the body to a str (not %s)"
1614                % type(value))
1615        self._body = value
1616        self.content_length = len(value)
1617        self._app_iter = None
1618
1619    def _body__del(self):
1620        self._body = None
1621        self.content_length = None
1622        self._app_iter = None
1623
1624    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
1625
1626    def _body_file__get(self):
1627        """
1628        Returns a file-like object that can be used to write to the
1629        body.  If you passed in a list app_iter, that app_iter will be
1630        modified by writes.
1631        """
1632        return ResponseBodyFile(self)
1633
1634    def _body_file__del(self):
1635        del self.body
1636
1637    body_file = property(_body_file__get, fdel=_body_file__del, doc=_body_file__get.__doc__)
1638
1639    def write(self, text):
1640        if isinstance(text, unicode):
1641            self.unicode_body += text
1642        else:
1643            self.body += text
1644
1645    def _unicode_body__get(self):
1646        """
1647        Get/set the unicode value of the body (using the charset of the Content-Type)
1648        """
1649        if not self.charset:
1650            raise AttributeError(
1651                "You cannot access Response.unicode_body unless charset is set")
1652        body = self.body
1653        return body.decode(self.charset)
1654
1655    def _unicode_body__set(self, value):
1656        if not self.charset:
1657            raise AttributeError(
1658                "You cannot access Response.unicode_body unless charset is set")
1659        if not isinstance(value, unicode):
1660            raise TypeError(
1661                "You can only set Response.unicode_body to a unicode string (not %s)" % type(value))
1662        self.body = value.encode(self.charset)
1663
1664    def _unicode_body__del(self):
1665        del self.body
1666
1667    unicode_body = property(_unicode_body__get, _unicode_body__set, _unicode_body__del, doc=_unicode_body__get.__doc__)
1668
1669    def _app_iter__get(self):
1670        """
1671        Returns the app_iter of the response.
1672
1673        If body was set, this will create an app_iter from that body
1674        (a single-item list)
1675        """
1676        if self._app_iter is None:
1677            if self._body is None:
1678                raise AttributeError(
1679                    "No body or app_iter has been set")
1680            return [self._body]
1681        else:
1682            return self._app_iter
1683
1684    def _app_iter__set(self, value):
1685        if self._body is not None:
1686            # Undo the automatically-set content-length
1687            self.content_length = None
1688        self._app_iter = value
1689        self._body = None
1690
1691    def _app_iter__del(self):
1692        self.content_length = None
1693        self._app_iter = self._body = None
1694
1695    app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__)
1696
1697    def set_cookie(self, key, value='', max_age=None,
1698                   path='/', domain=None, secure=None, httponly=False,
1699                   version=None, comment=None, expires=None):
1700        """
1701        Set (add) a cookie for the response
1702        """
1703        if isinstance(value, unicode) and self.charset is not None:
1704            value = '"%s"' % value.encode(self.charset)
1705        cookies = BaseCookie()
1706        cookies[key] = value
1707        if isinstance(max_age, timedelta):
1708            max_age = timedelta.seconds + timedelta.days*24*60*60
1709        if max_age is not None and expires is None:
1710            expires = datetime.utcnow() + timedelta(seconds=max_age)
1711        if isinstance(expires, timedelta):
1712            expires = datetime.utcnow() + expires
1713        if isinstance(expires, datetime):
1714            expires = '"'+_serialize_cookie_date(expires)+'"'
1715        for var_name, var_value in [
1716            ('max_age', max_age),
1717            ('path', path),
1718            ('domain', domain),
1719            ('secure', secure),
1720            ('HttpOnly', httponly),
1721            ('version', version),
1722            ('comment', comment),
1723            ('expires', expires),
1724            ]:
1725            if var_value is not None and var_value is not False:
1726                cookies[key][var_name.replace('_', '-')] = str(var_value)
1727        header_value = cookies[key].output(header='').lstrip()
1728        self.headerlist.append(('Set-Cookie', header_value))
1729
1730    def delete_cookie(self, key, path='/', domain=None):
1731        """
1732        Delete a cookie from the client.  Note that path and domain must match
1733        how the cookie was originally set.
1734
1735        This sets the cookie to the empty string, and max_age=0 so
1736        that it should expire immediately.
1737        """
1738        self.set_cookie(key, '', path=path, domain=domain,
1739                        max_age=0, expires=timedelta(days=-5))
1740
1741    def unset_cookie(self, key):
1742        """
1743        Unset a cookie with the given name (remove it from the
1744        response).  If there are multiple cookies (e.g., two cookies
1745        with the same name and different paths or domains), all such
1746        cookies will be deleted.
1747        """
1748        existing = self.headers.getall('Set-Cookie')
1749        if not existing:
1750            raise KeyError(
1751                "No cookies at all have been set")
1752        del self.headers['Set-Cookie']
1753        found = False
1754        for header in existing:
1755            cookies = BaseCookie()
1756            cookies.load(header)
1757            if key in cookies:
1758                found = True
1759                del cookies[key]
1760                header = cookies.output(header='').lstrip()
1761            if header:
1762                self.headers.add('Set-Cookie', header)
1763        if not found:
1764            raise KeyError(
1765                "No cookie has been set with the name %r" % key)
1766
1767    def _location__get(self):
1768        """
1769        Retrieve the Location header of the response, or None if there
1770        is no header.  If the header is not absolute and this response
1771        is associated with a request, make the header absolute.
1772
1773        For more information see `section 14.30
1774        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30>`_.
1775        """
1776        if 'location' not in self.headers:
1777            return None
1778        location = self.headers['location']
1779        if _SCHEME_RE.search(location):
1780            # Absolute
1781            return location
1782        if self.request is not None:
1783            base_uri = self.request.url
1784            location = urlparse.urljoin(base_uri, location)
1785        return location
1786
1787    def _location__set(self, value):
1788        if not _SCHEME_RE.search(value):
1789            # Not absolute, see if we can make it absolute
1790            if self.request is not None:
1791                value = urlparse.urljoin(self.request.url, value)
1792        self.headers['location'] = value
1793
1794    def _location__del(self):
1795        if 'location' in self.headers:
1796            del self.headers['location']
1797
1798    location = property(_location__get, _location__set, _location__del, doc=_location__get.__doc__)
1799
1800    accept_ranges = header_getter('Accept-Ranges', rfc_section='14.5')
1801
1802    age = converter(
1803        header_getter('Age', rfc_section='14.6'),
1804        _parse_int_safe, _serialize_int, 'int')
1805
1806    allow = converter(
1807        header_getter('Allow', rfc_section='14.7'),
1808        _parse_list, _serialize_list, 'list')
1809
1810    _cache_control_obj = None
1811
1812    def _cache_control__get(self):
1813        """
1814        Get/set/modify the Cache-Control header (section `14.9
1815        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
1816        """
1817        value = self.headers.get('cache-control', '')
1818        if self._cache_control_obj is None:
1819            self._cache_control_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='response')
1820            self._cache_control_obj.header_value = value
1821        if self._cache_control_obj.header_value != value:
1822            new_obj = CacheControl.parse(value, type='response')
1823            self._cache_control_obj.properties.clear()
1824            self._cache_control_obj.properties.update(new_obj.properties)
1825            self._cache_control_obj.header_value = value
1826        return self._cache_control_obj
1827
1828    def _cache_control__set(self, value):
1829        # This actually becomes a copy
1830        if not value:
1831            value = ""
1832        if isinstance(value, dict):
1833            value = CacheControl(value, 'response')
1834        if isinstance(value, unicode):
1835            value = str(value)
1836        if isinstance(value, str):
1837            if self._cache_control_obj is None:
1838                self.headers['Cache-Control'] = value
1839                return
1840            value = CacheControl.parse(value, 'response')
1841        cache = self.cache_control
1842        cache.properties.clear()
1843        cache.properties.update(value.properties)
1844
1845    def _cache_control__del(self):
1846        self.cache_control = {}
1847
1848    def _update_cache_control(self, prop_dict):
1849        value = serialize_cache_control(prop_dict)
1850        if not value:
1851            if 'Cache-Control' in self.headers:
1852                del self.headers['Cache-Control']
1853        else:
1854            self.headers['Cache-Control'] = value
1855
1856    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
1857
1858    def cache_expires(self, seconds=0, **kw):
1859        """
1860        Set expiration on this request.  This sets the response to
1861        expire in the given seconds, and any other attributes are used
1862        for cache_control (e.g., private=True, etc).
1863        """
1864        cache_control = self.cache_control
1865        if isinstance(seconds, timedelta):
1866            seconds = timedelta_to_seconds(seconds)
1867        if not seconds:
1868            # To really expire something, you have to force a
1869            # bunch of these cache control attributes, and IE may
1870            # not pay attention to those still so we also set
1871            # Expires.
1872            cache_control.no_store = True
1873            cache_control.no_cache = True
1874            cache_control.must_revalidate = True
1875            cache_control.max_age = 0
1876            cache_control.post_check = 0
1877            cache_control.pre_check = 0