constance

Scripts for generating (an earlier obsolete version of) my personal web site
git clone https://code.djc.id.au/git/constance/
commit 44bf4a60c35f425f40429bded509c9aef9509b57
parent 14d1e9ceb79f294300144e9a0cb1851db3ceedcf
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sat, 22 Nov 2008 13:52:55 +1000

WebOb-0.9.4 from http://pypi.python.org/packages/source/W/WebOb/WebOb-0.9.4.tar.gz

Diffstat:
Alib/WebOb.egg-info/PKG-INFO | 23+++++++++++++++++++++++
Alib/WebOb.egg-info/SOURCES.txt | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/WebOb.egg-info/dependency_links.txt | 1+
Alib/WebOb.egg-info/top_level.txt | 1+
Alib/WebOb.egg-info/zip-safe | 1+
Alib/webob/__init__.py | 2383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/acceptparse.py | 297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/byterange.py | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/cachecontrol.py | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/compat.py | 23+++++++++++++++++++++++
Alib/webob/datastruct.py | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/etag.py | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/exc.py | 660+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/headerdict.py | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/multidict.py | 606+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/statusreasons.py | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/updatedict.py | 41+++++++++++++++++++++++++++++++++++++++++
Alib/webob/util/__init__.py | 1+
Alib/webob/util/dictmixin.py | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/webob/util/reversed.py | 4++++
Alib/webob/util/safegzip.py | 21+++++++++++++++++++++
Alib/webob/util/stringtemplate.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
22 files changed, 5256 insertions(+), 0 deletions(-)
diff --git a/lib/WebOb.egg-info/PKG-INFO b/lib/WebOb.egg-info/PKG-INFO
@@ -0,0 +1,23 @@
+Metadata-Version: 1.0
+Name: WebOb
+Version: 0.9.4
+Summary: WSGI request and response object
+Home-page: http://pythonpaste.org/webob/
+Author: Ian Bicking
+Author-email: ianb@colorstudy.com
+License: MIT
+Description: WebOb provides wrappers around the WSGI request environment, and an
+        object to help create WSGI responses.
+        
+        The objects map much of the specified behavior of HTTP, including
+        header parsing and accessors for other standard parts of the
+        environment.
+        
+Keywords: wsgi request web http
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Framework :: Paste
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
diff --git a/lib/WebOb.egg-info/SOURCES.txt b/lib/WebOb.egg-info/SOURCES.txt
@@ -0,0 +1,50 @@
+regen-docs
+setup.cfg
+setup.py
+test
+WebOb.egg-info/PKG-INFO
+WebOb.egg-info/SOURCES.txt
+WebOb.egg-info/dependency_links.txt
+WebOb.egg-info/top_level.txt
+WebOb.egg-info/zip-safe
+docs/comment-example.txt
+docs/conf.py
+docs/differences.txt
+docs/do-it-yourself.txt
+docs/file-example.txt
+docs/index.txt
+docs/jsonrpc-example.txt
+docs/license.txt
+docs/news.txt
+docs/reference.txt
+docs/test-file.txt
+docs/wiki-example.txt
+docs/comment-example-code/example.py
+docs/jsonrpc-example-code/jsonrpc.py
+docs/jsonrpc-example-code/test_jsonrpc.py
+docs/jsonrpc-example-code/test_jsonrpc.txt
+docs/modules/webob.txt
+docs/wiki-example-code/example.py
+tests/__init__.py
+tests/conftest.py
+tests/test_request.py
+tests/test_request.txt
+tests/test_response.py
+tests/test_response.txt
+webob/__init__.py
+webob/acceptparse.py
+webob/byterange.py
+webob/cachecontrol.py
+webob/compat.py
+webob/datastruct.py
+webob/etag.py
+webob/exc.py
+webob/headerdict.py
+webob/multidict.py
+webob/statusreasons.py
+webob/updatedict.py
+webob/util/__init__.py
+webob/util/dictmixin.py
+webob/util/reversed.py
+webob/util/safegzip.py
+webob/util/stringtemplate.py
+\ No newline at end of file
diff --git a/lib/WebOb.egg-info/dependency_links.txt b/lib/WebOb.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/lib/WebOb.egg-info/top_level.txt b/lib/WebOb.egg-info/top_level.txt
@@ -0,0 +1 @@
+webob
diff --git a/lib/WebOb.egg-info/zip-safe b/lib/WebOb.egg-info/zip-safe
@@ -0,0 +1 @@
+
diff --git a/lib/webob/__init__.py b/lib/webob/__init__.py
@@ -0,0 +1,2383 @@
+from cStringIO import StringIO
+import sys
+import cgi
+import urllib
+import urlparse
+import re
+import textwrap
+from Cookie import BaseCookie
+from rfc822 import parsedate_tz, mktime_tz, formatdate
+from datetime import datetime, date, timedelta, tzinfo
+import time
+import calendar
+import tempfile
+import warnings
+from webob.datastruct import EnvironHeaders
+from webob.multidict import MultiDict, UnicodeMultiDict, NestedMultiDict, NoVars
+from webob.etag import AnyETag, NoETag, ETagMatcher, IfRange, NoIfRange
+from webob.headerdict import HeaderDict
+from webob.statusreasons import status_reasons
+from webob.cachecontrol import CacheControl, serialize_cache_control
+from webob.acceptparse import Accept, MIMEAccept, NilAccept, MIMENilAccept, NoAccept
+from webob.byterange import Range, ContentRange
+try:
+    sorted
+except NameError:
+    from webob.compat import sorted
+
+_CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I)
+_SCHEME_RE = re.compile(r'^[a-z]+:', re.I)
+_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
+_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
+
+__all__ = ['Request', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape']
+
+class _UTC(tzinfo):
+    def dst(self, dt):
+        return timedelta(0)
+    def utcoffset(self, dt):
+        return timedelta(0)
+    def tzname(self, dt):
+        return 'UTC'
+    def __repr__(self):
+        return 'UTC'
+
+UTC = _UTC()
+
+def html_escape(s):
+    """HTML-escape a string or object
+
+    This converts any non-string objects passed into it to strings
+    (actually, using ``unicode()``).  All values returned are
+    non-unicode strings (using ``&#num;`` entities for all non-ASCII
+    characters).
+
+    None is treated specially, and returns the empty string.
+    """
+    if s is None:
+        return ''
+    if not isinstance(s, basestring):
+        if hasattr(s, '__unicode__'):
+            s = unicode(s)
+        else:
+            s = str(s)
+    s = cgi.escape(s, True)
+    if isinstance(s, unicode):
+        s = s.encode('ascii', 'xmlcharrefreplace')
+    return s
+
+def timedelta_to_seconds(td):
+    """
+    Converts a timedelta instance to seconds.
+    """
+    return td.seconds + (td.days*24*60*60)
+
+day = timedelta(days=1)
+week = timedelta(weeks=1)
+hour = timedelta(hours=1)
+minute = timedelta(minutes=1)
+second = timedelta(seconds=1)
+# Estimate, I know; good enough for expirations
+month = timedelta(days=30)
+year = timedelta(days=365)
+
+class _NoDefault:
+    def __repr__(self):
+        return '(No Default)'
+NoDefault = _NoDefault()
+
+class environ_getter(object):
+    """For delegating an attribute to a key in self.environ."""
+
+    def __init__(self, key, default='', default_factory=None,
+                 settable=True, deletable=True, doc=None,
+                 rfc_section=None):
+        self.key = key
+        self.default = default
+        self.default_factory = default_factory
+        self.settable = settable
+        self.deletable = deletable
+        docstring = "Gets"
+        if self.settable:
+            docstring += " and sets"
+        if self.deletable:
+            docstring += " and deletes"
+        docstring += " the %r key from the environment." % self.key
+        docstring += _rfc_reference(self.key, rfc_section)
+        if doc:
+            docstring += '\n\n' + textwrap.dedent(doc)
+        self.__doc__ = docstring
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        if self.key not in obj.environ:
+            if self.default_factory:
+                val = obj.environ[self.key] = self.default_factory()
+                return val
+            else:
+                return self.default
+        return obj.environ[self.key]
+
+    def __set__(self, obj, value):
+        if not self.settable:
+            raise AttributeError("Read-only attribute (key %r)" % self.key)
+        if value is None:
+            if self.key in obj.environ:
+                del obj.environ[self.key]
+        else:
+            obj.environ[self.key] = value
+
+    def __delete__(self, obj):
+        if not self.deletable:
+            raise AttributeError("You cannot delete the key %r" % self.key)
+        del obj.environ[self.key]
+
+    def __repr__(self):
+        return '<Proxy for WSGI environ %r key>' % self.key
+
+class header_getter(object):
+    """For delegating an attribute to a header in self.headers"""
+
+    def __init__(self, header, default=None,
+                 settable=True, deletable=True, doc=None, rfc_section=None):
+        self.header = header
+        self.default = default
+        self.settable = settable
+        self.deletable = deletable
+        docstring = "Gets"
+        if self.settable:
+            docstring += " and sets"
+        if self.deletable:
+            docstring += " and deletes"
+        docstring += " they header %s from the headers" % self.header
+        docstring += _rfc_reference(self.header, rfc_section)
+        if doc:
+            docstring += '\n\n' + textwrap.dedent(doc)
+        self.__doc__ = docstring
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        if self.header not in obj.headers:
+            return self.default
+        else:
+            return obj.headers[self.header]
+
+    def __set__(self, obj, value):
+        if not self.settable:
+            raise AttributeError("Read-only attribute (header %s)" % self.header)
+        if value is None:
+            if self.header in obj.headers:
+                del obj.headers[self.header]
+        else:
+            if isinstance(value, unicode):
+                # This is the standard encoding for headers:
+                value = value.encode('ISO-8859-1')
+            obj.headers[self.header] = value
+
+    def __delete__(self, obj):
+        if not self.deletable:
+            raise AttributeError("You cannot delete the header %s" % self.header)
+        del obj.headers[self.header]
+
+    def __repr__(self):
+        return '<Proxy for header %s>' % self.header
+
+class converter(object):
+    """
+    Wraps a decorator, and applies conversion for that decorator
+    """
+    def __init__(self, decorator, getter_converter, setter_converter, convert_name=None, doc=None, converter_args=()):
+        self.decorator = decorator
+        self.getter_converter = getter_converter
+        self.setter_converter = setter_converter
+        self.convert_name = convert_name
+        self.converter_args = converter_args
+        docstring = decorator.__doc__ or ''
+        docstring += "  Converts it as a "
+        if convert_name:
+            docstring += convert_name + '.'
+        else:
+            docstring += "%r and %r." % (getter_converter, setter_converter)
+        if doc:
+            docstring += '\n\n' + textwrap.dedent(doc)
+        self.__doc__ = docstring
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        value = self.decorator.__get__(obj, type)
+        return self.getter_converter(value, *self.converter_args)
+
+    def __set__(self, obj, value):
+        value = self.setter_converter(value, *self.converter_args)
+        self.decorator.__set__(obj, value)
+
+    def __delete__(self, obj):
+        self.decorator.__delete__(obj)
+
+    def __repr__(self):
+        if self.convert_name:
+            name = ' %s' % self.convert_name
+        else:
+            name = ''
+        return '<Converted %r%s>' % (self.decorator, name)
+
+def _rfc_reference(header, section):
+    if not section:
+        return ''
+    major_section = section.split('.')[0]
+    link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % (
+        major_section, section)
+    if header.startswith('HTTP_'):
+        header = header[5:].title().replace('_', '-')
+    return "  For more information on %s see `section %s <%s>`_." % (
+        header, section, link)
+
+class deprecated_property(object):
+    """
+    Wraps a decorator, with a deprecation warning or error
+    """
+    def __init__(self, decorator, attr, message, warning=True):
+        self.decorator = decorator
+        self.attr = attr
+        self.message = message
+        self.warning = warning
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        self.warn()
+        return self.decorator.__get__(obj, type)
+
+    def __set__(self, obj, value):
+        self.warn()
+        self.decorator.__set__(obj, value)
+
+    def __delete__(self, obj):
+        self.warn()
+        self.decorator.__delete__(obj)
+
+    def __repr__(self):
+        return '<Deprecated attribute %s: %r>' % (
+            self.attr,
+            self.decorator)
+
+    def warn(self):
+        if not self.warning:
+            raise DeprecationWarning(
+                'The attribute %s is deprecated: %s' % (self.attr, self.message))
+        else:
+            warnings.warn(
+                'The attribute %s is deprecated: %s' % (self.attr, self.message),
+                DeprecationWarning,
+                stacklevel=3)
+
+def _parse_date(value):
+    if not value:
+        return None
+    t = parsedate_tz(value)
+    if t is None:
+        # Could not parse
+        return None
+    if t[-1] is None:
+        # No timezone given.  None would mean local time, but we'll force UTC
+        t = t[:9] + (0,)
+    t = mktime_tz(t)
+    return datetime.fromtimestamp(t, UTC)
+
+def _serialize_date(dt):
+    if dt is None:
+        return None
+    if isinstance(dt, unicode):
+        dt = dt.encode('ascii')
+    if isinstance(dt, str):
+        return dt
+    if isinstance(dt, timedelta):
+        dt = datetime.now() + dt
+    if isinstance(dt, (datetime, date)):
+        dt = dt.timetuple()
+    if isinstance(dt, (tuple, time.struct_time)):
+        dt = calendar.timegm(dt)
+    if not isinstance(dt, (float, int, long)):
+        raise ValueError(
+            "You must pass in a datetime, date, time tuple, or integer object, not %r" % dt)
+    return formatdate(dt)
+
+def _serialize_cookie_date(dt):
+    if dt is None:
+        return None
+    if isinstance(dt, unicode):
+        dt = dt.encode('ascii')
+    if isinstance(dt, timedelta):
+        dt = datetime.now() + dt
+    if isinstance(dt, (datetime, date)):
+        dt = dt.timetuple()
+    return time.strftime('%a, %d-%b-%Y %H:%M:%S GMT', dt)
+
+def _parse_date_delta(value):
+    """
+    like _parse_date, but also handle delta seconds
+    """
+    if not value:
+        return None
+    try:
+        value = int(value)
+    except ValueError:
+        pass
+    else:
+        delta = timedelta(seconds=value)
+        return datetime.now() + delta
+    return _parse_date(value)
+
+def _serialize_date_delta(value):
+    if not value and value != 0:
+        return None
+    if isinstance(value, (float, int)):
+        return str(int(value))
+    return _serialize_date(value)
+
+def _parse_etag(value, default=True):
+    if value is None:
+        value = ''
+    value = value.strip()
+    if not value:
+        if default:
+            return AnyETag
+        else:
+            return NoETag
+    if value == '*':
+        return AnyETag
+    else:
+        return ETagMatcher.parse(value)
+
+def _serialize_etag(value, default=True):
+    if value is None:
+        return None
+    if value is AnyETag:
+        if default:
+            return None
+        else:
+            return '*'
+    return str(value)
+
+def _parse_if_range(value):
+    if not value:
+        return NoIfRange
+    else:
+        return IfRange.parse(value)
+
+def _serialize_if_range(value):
+    if value is None:
+        return value
+    if isinstance(value, (datetime, date)):
+        return _serialize_date(value)
+    if not isinstance(value, str):
+        value = str(value)
+    return value or None
+
+def _parse_range(value):
+    if not value:
+        return None
+    # Might return None too:
+    return Range.parse(value)
+
+def _serialize_range(value):
+    if isinstance(value, (list, tuple)):
+        if len(value) != 2:
+            raise ValueError(
+                "If setting .range to a list or tuple, it must be of length 2 (not %r)"
+                % value)
+        value = Range([value])
+    if value is None:
+        return None
+    value = str(value)
+    return value or None
+
+def _parse_int(value):
+    if value is None or value == '':
+        return None
+    return int(value)
+
+def _parse_int_safe(value):
+    if value is None or value == '':
+        return None
+    try:
+        return int(value)
+    except ValueError:
+        return None
+
+def _serialize_int(value):
+    if value is None:
+        return None
+    return str(value)
+
+def _parse_content_range(value):
+    if not value or not value.strip():
+        return None
+    # May still return None
+    return ContentRange.parse(value)
+
+def _serialize_content_range(value):
+    if value is None:
+        return None
+    if isinstance(value, (tuple, list)):
+        if len(value) not in (2, 3):
+            raise ValueError(
+                "When setting content_range to a list/tuple, it must "
+                "be length 2 or 3 (not %r)" % value)
+        if len(value) == 2:
+            begin, end = value
+            length = None
+        else:
+            begin, end, length = value
+        value = ContentRange(begin, end, length)
+    value = str(value).strip()
+    if not value:
+        return None
+    return value
+
+def _parse_list(value):
+    if value is None:
+        return None
+    value = value.strip()
+    if not value:
+        return None
+    return [v.strip() for v in value.split(',')
+            if v.strip()]
+
+def _serialize_list(value):
+    if not value:
+        return None
+    if isinstance(value, unicode):
+        value = str(value)
+    if isinstance(value, str):
+        return value
+    return ', '.join(map(str, value))
+
+def _parse_accept(value, header_name, AcceptClass, NilClass):
+    if not value:
+        return NilClass(header_name)
+    return AcceptClass(header_name, value)
+
+def _serialize_accept(value, header_name, AcceptClass, NilClass):
+    if not value or isinstance(value, NilClass):
+        return None
+    if isinstance(value, (list, tuple, dict)):
+        value = NilClass(header_name) + value
+    value = str(value).strip()
+    if not value:
+        return None
+    return value
+
+class Request(object):
+
+    ## Options:
+    charset = None
+    unicode_errors = 'strict'
+    decode_param_names = False
+    ## The limit after which request bodies should be stored on disk
+    ## if they are read in (under this, and the request body is stored
+    ## in memory):
+    request_body_tempfile_limit = 10*1024
+
+    def __init__(self, environ=None, environ_getter=None, charset=NoDefault, unicode_errors=NoDefault,
+                 decode_param_names=NoDefault, **kw):
+        if environ is None and environ_getter is None:
+            raise TypeError(
+                "You must provide one of environ or environ_getter")
+        if environ is not None and environ_getter is not None:
+            raise TypeError(
+                "You can only provide one of the environ and environ_getter arguments")
+        if environ is None:
+            self._environ_getter = environ_getter
+        else:
+            if not isinstance(environ, dict):
+                raise TypeError(
+                    "Bad type for environ: %s" % type(environ))
+            self._environ = environ
+        if charset is not NoDefault:
+            self.__dict__['charset'] = charset
+        if unicode_errors is not NoDefault:
+            self.__dict__['unicode_errors'] = unicode_errors
+        if decode_param_names is not NoDefault:
+            self.__dict__['decode_param_names'] = decode_param_names
+        for name, value in kw.items():
+            if not hasattr(self.__class__, name):
+                raise TypeError(
+                    "Unexpected keyword: %s=%r" % name, value)
+            setattr(self, name, value)
+
+    def __setattr__(self, attr, value, DEFAULT=[]):
+        ## FIXME: I don't know why I need this guard (though experimentation says I do)
+        if getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_'):
+            object.__setattr__(self, attr, value)
+        else:
+            self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
+
+    def __getattr__(self, attr):
+        ## FIXME: I don't know why I need this guard (though experimentation says I do)
+        if attr in self.__class__.__dict__:
+            return object.__getattribute__(self, attr)
+        try:
+            return self.environ['webob.adhoc_attrs'][attr]
+        except KeyError:
+            raise AttributeError(attr)
+
+    def __delattr__(self, attr):
+        ## FIXME: I don't know why I need this guard (though experimentation says I do)
+        if attr in self.__class__.__dict__:
+            return object.__delattr__(self, attr)
+        try:
+            del self.environ['webob.adhoc_attrs'][attr]
+        except KeyError:
+            raise AttributeError(attr)
+
+    def environ(self):
+        """
+        The WSGI environment dictionary for this request
+        """
+        return self._environ_getter()
+    environ = property(environ, doc=environ.__doc__)
+
+    def _environ_getter(self):
+        return self._environ
+
+    def _body_file__get(self):
+        """
+        Access the body of the request (wsgi.input) as a file-like
+        object.
+
+        If you set this value, CONTENT_LENGTH will also be updated
+        (either set to -1, 0 if you delete the attribute, or if you
+        set the attribute to a string then the length of the string).
+        """
+        return self.environ['wsgi.input']
+    def _body_file__set(self, value):
+        if isinstance(value, str):
+            length = len(value)
+            value = StringIO(value)
+        else:
+            length = -1
+        self.environ['wsgi.input'] = value
+        self.environ['CONTENT_LENGTH'] = str(length)
+    def _body_file__del(self):
+        self.environ['wsgi.input'] = StringIO('')
+        self.environ['CONTENT_LENGTH'] = '0'
+    body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__)
+
+    scheme = environ_getter('wsgi.url_scheme')
+    method = environ_getter('REQUEST_METHOD')
+    script_name = environ_getter('SCRIPT_NAME')
+    path_info = environ_getter('PATH_INFO')
+    ## FIXME: should I strip out parameters?:
+    content_type = environ_getter('CONTENT_TYPE', rfc_section='14.17')
+    content_length = converter(
+        environ_getter('CONTENT_LENGTH', rfc_section='14.13'),
+        _parse_int_safe, _serialize_int, 'int')
+    remote_user = environ_getter('REMOTE_USER', default=None)
+    remote_addr = environ_getter('REMOTE_ADDR', default=None)
+    query_string = environ_getter('QUERY_STRING')
+    server_name = environ_getter('SERVER_NAME')
+    server_port = converter(
+        environ_getter('SERVER_PORT'),
+        _parse_int, _serialize_int, 'int')
+
+    _headers = None
+
+    def _headers__get(self):
+        """
+        All the request headers as a case-insensitive dictionary-like
+        object.
+        """
+        if self._headers is None:
+            self._headers = EnvironHeaders(self.environ)
+        return self._headers
+
+    def _headers__set(self, value):
+        self.headers.clear()
+        self.headers.update(value)
+
+    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
+
+    def host_url(self):
+        """
+        The URL through the host (no path)
+        """
+        e = self.environ
+        url = e['wsgi.url_scheme'] + '://'
+        if e.get('HTTP_HOST'):
+            host = e['HTTP_HOST']
+            if ':' in host:
+                host, port = host.split(':', 1)
+            else:
+
+                port = None
+        else:
+            host = e['SERVER_NAME']
+            port = e['SERVER_PORT']
+        if self.environ['wsgi.url_scheme'] == 'https':
+            if port == '443':
+                port = None
+        elif self.environ['wsgi.url_scheme'] == 'http':
+            if port == '80':
+                port = None
+        url += host
+        if port:
+            url += ':%s' % port
+        return url
+    host_url = property(host_url, doc=host_url.__doc__)
+
+    def application_url(self):
+        """
+        The URL including SCRIPT_NAME (no PATH_INFO or query string)
+        """
+        return self.host_url + urllib.quote(self.environ.get('SCRIPT_NAME', ''))
+    application_url = property(application_url, doc=application_url.__doc__)
+
+    def path_url(self):
+        """
+        The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
+        """
+        return self.application_url + urllib.quote(self.environ.get('PATH_INFO', ''))
+    path_url = property(path_url, doc=path_url.__doc__)
+
+    def path(self):
+        """
+        The path of the request, without host or query string
+        """
+        return urllib.quote(self.script_name) + urllib.quote(self.path_info)
+    path = property(path, doc=path.__doc__)
+
+    def path_qs(self):
+        """
+        The path of the request, without host but with query string
+        """
+        path = self.path
+        qs = self.environ.get('QUERY_STRING')
+        if qs:
+            path += '?' + qs
+        return path
+    path_qs = property(path_qs, doc=path_qs.__doc__)
+
+    def url(self):
+        """
+        The full request URL, including QUERY_STRING
+        """
+        url = self.path_url
+        if self.environ.get('QUERY_STRING'):
+            url += '?' + self.environ['QUERY_STRING']
+        return url
+    url = property(url, doc=url.__doc__)
+
+    def relative_url(self, other_url, to_application=False):
+        """
+        Resolve other_url relative to the request URL.
+
+        If ``to_application`` is True, then resolve it relative to the
+        URL with only SCRIPT_NAME
+        """
+        if to_application:
+            url = self.application_url
+            if not url.endswith('/'):
+                url += '/'
+        else:
+            url = self.path_url
+        return urlparse.urljoin(url, other_url)
+
+    def path_info_pop(self):
+        """
+        'Pops' off the next segment of PATH_INFO, pushing it onto
+        SCRIPT_NAME, and returning the popped segment.  Returns None if
+        there is nothing left on PATH_INFO.
+
+        Does not return ``''`` when there's an empty segment (like
+        ``/path//path``); these segments are just ignored.
+        """
+        path = self.path_info
+        if not path:
+            return None
+        while path.startswith('/'):
+            self.script_name += '/'
+            path = path[1:]
+        if '/' not in path:
+            self.script_name += path
+            self.path_info = ''
+            return path
+        else:
+            segment, path = path.split('/', 1)
+            self.path_info = '/' + path
+            self.script_name += segment
+            return segment
+
+    def path_info_peek(self):
+        """
+        Returns the next segment on PATH_INFO, or None if there is no
+        next segment.  Doesn't modify the environment.
+        """
+        path = self.path_info
+        if not path:
+            return None
+        path = path.lstrip('/')
+        return path.split('/', 1)[0]
+
+    def _urlvars__get(self):
+        """
+        Return any *named* variables matched in the URL.
+
+        Takes values from ``environ['wsgiorg.routing_args']``.
+        Systems like ``routes`` set this value.
+        """
+        if 'paste.urlvars' in self.environ:
+            return self.environ['paste.urlvars']
+        elif 'wsgiorg.routing_args' in self.environ:
+            return self.environ['wsgiorg.routing_args'][1]
+        else:
+            result = {}
+            self.environ['wsgiorg.routing_args'] = ((), result)
+            return result
+
+    def _urlvars__set(self, value):
+        environ = self.environ
+        if 'wsgiorg.routing_args' in environ:
+            environ['wsgiorg.routing_args'] = (environ['wsgiorg.routing_args'][0], value)
+            if 'paste.urlvars' in environ:
+                del environ['paste.urlvars']
+        elif 'paste.urlvars' in environ:
+            environ['paste.urlvars'] = value
+        else:
+            environ['wsgiorg.routing_args'] = ((), value)
+
+    def _urlvars__del(self):
+        if 'paste.urlvars' in self.environ:
+            del self.environ['paste.urlvars']
+        if 'wsgiorg.routing_args' in self.environ:
+            if not self.environ['wsgiorg.routing_args'][0]:
+                del self.environ['wsgiorg.routing_args']
+            else:
+                self.environ['wsgiorg.routing_args'] = (self.environ['wsgiorg.routing_args'][0], {})
+
+    urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__)
+
+    def _urlargs__get(self):
+        """
+        Return any *positional* variables matched in the URL.
+
+        Takes values from ``environ['wsgiorg.routing_args']``.
+        Systems like ``routes`` set this value.
+        """
+        if 'wsgiorg.routing_args' in self.environ:
+            return self.environ['wsgiorg.routing_args'][0]
+        else:
+            # Since you can't update this value in-place, we don't need
+            # to set the key in the environment
+            return ()
+
+    def _urlargs__set(self, value):
+        environ = self.environ
+        if 'paste.urlvars' in environ:
+            # Some overlap between this and wsgiorg.routing_args; we need
+            # wsgiorg.routing_args to make this work
+            routing_args = (value, environ.pop('paste.urlvars'))
+        elif 'wsgiorg.routing_args' in environ:
+            routing_args = (value, environ['wsgiorg.routing_args'][1])
+        else:
+            routing_args = (value, {})
+        environ['wsgiorg.routing_args'] = routing_args
+
+    def _urlargs__del(self):
+        if 'wsgiorg.routing_args' in self.environ:
+            if not self.environ['wsgiorg.routing_args'][1]:
+                del self.environ['wsgiorg.routing_args']
+            else:
+                self.environ['wsgiorg.routing_args'] = ((), self.environ['wsgiorg.routing_args'][1])
+
+    urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__)
+
+    def is_xhr(self):
+        """Returns a boolean if X-Requested-With is present and ``XMLHttpRequest``
+
+        Note: this isn't set by every XMLHttpRequest request, it is
+        only set if you are using a Javascript library that sets it
+        (or you set the header yourself manually).  Currently
+        Prototype and jQuery are known to set this header."""
+        return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
+    is_xhr = property(is_xhr, doc=is_xhr.__doc__)
+
+    def _host__get(self):
+        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
+        if 'HTTP_HOST' in self.environ:
+            return self.environ['HTTP_HOST']
+        else:
+            return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
+    def _host__set(self, value):
+        self.environ['HTTP_HOST'] = value
+    def _host__del(self):
+        if 'HTTP_HOST' in self.environ:
+            del self.environ['HTTP_HOST']
+    host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
+
+    def _body__get(self):
+        """
+        Return the content of the request body.
+        """
+        try:
+            length = int(self.environ.get('CONTENT_LENGTH', '0'))
+        except ValueError:
+            return ''
+        c = self.body_file.read(length)
+        tempfile_limit = self.request_body_tempfile_limit
+        if tempfile_limit and len(c) > tempfile_limit:
+            fileobj = tempfile.TemporaryFile()
+            fileobj.write(c)
+            fileobj.seek(0)
+        else:
+            fileobj = StringIO(c)
+        # We don't want/need to lose CONTENT_LENGTH here (as setting
+        # self.body_file would do):
+        self.environ['wsgi.input'] = fileobj
+        return c
+
+    def _body__set(self, value):
+        if value is None:
+            del self.body
+            return
+        if not isinstance(value, str):
+            raise TypeError(
+                "You can only set Request.body to a str (not %r)" % type(value))
+        body_file = StringIO(value)
+        self.body_file = body_file
+        self.environ['CONTENT_LENGTH'] = str(len(value))
+
+    def _body__del(self, value):
+        del self.body_file
+
+    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
+
+    def str_POST(self):
+        """
+        Return a MultiDict containing all the variables from a form
+        request. Returns an empty dict-like object for non-form
+        requests.
+
+        Form requests are typically POST requests, however PUT requests
+        with an appropriate Content-Type are also supported.
+        """
+        env = self.environ
+        if self.method not in ('POST', 'PUT'):
+            return NoVars('Not a form request')
+        if 'webob._parsed_post_vars' in env:
+            vars, body_file = env['webob._parsed_post_vars']
+            if body_file is self.body_file:
+                return vars
+        # Paste compatibility:
+        if 'paste.parsed_formvars' in env:
+            # from paste.request.parse_formvars
+            vars, body_file = env['paste.parsed_formvars']
+            if body_file is self.body_file:
+                # FIXME: is it okay that this isn't *our* MultiDict?
+                return vars
+        content_type = self.content_type
+        if ';' in content_type:
+            content_type = content_type.split(';', 1)[0]
+        if (self.method == 'PUT' and not content_type) or \
+                content_type not in ('', 'application/x-www-form-urlencoded',
+                                     'multipart/form-data'):
+            # Not an HTML form submission
+            return NoVars('Not an HTML form submission (Content-Type: %s)'
+                          % content_type)
+        if 'CONTENT_LENGTH' not in env:
+            # FieldStorage assumes a default CONTENT_LENGTH of -1, but a
+            # default of 0 is better:
+            env['CONTENT_TYPE'] = '0'
+        fs_environ = env.copy()
+        fs_environ['QUERY_STRING'] = ''
+        fs = cgi.FieldStorage(fp=self.body_file,
+                              environ=fs_environ,
+                              keep_blank_values=True)
+        vars = MultiDict.from_fieldstorage(fs)
+        FakeCGIBody.update_environ(env, vars)
+        env['webob._parsed_post_vars'] = (vars, self.body_file)
+        return vars
+
+    str_POST = property(str_POST, doc=str_POST.__doc__)
+
+    str_postvars = deprecated_property(str_POST, 'str_postvars',
+                                       'use str_POST instead')
+
+    def POST(self):
+        """
+        Like ``.str_POST``, but may decode values and keys
+        """
+        vars = self.str_POST
+        if self.charset:
+            vars = UnicodeMultiDict(vars, encoding=self.charset,
+                                    errors=self.unicode_errors,
+                                    decode_keys=self.decode_param_names)
+        return vars
+
+    POST = property(POST, doc=POST.__doc__)
+
+    postvars = deprecated_property(POST, 'postvars',
+                                   'use POST instead')
+
+    def str_GET(self):
+        """
+        Return a MultiDict containing all the variables from the
+        QUERY_STRING.
+        """
+        env = self.environ
+        source = env.get('QUERY_STRING', '')
+        if 'webob._parsed_query_vars' in env:
+            vars, qs = env['webob._parsed_query_vars']
+            if qs == source:
+                return vars
+        if not source:
+            vars = MultiDict()
+        else:
+            vars = MultiDict(cgi.parse_qsl(
+                source, keep_blank_values=True,
+                strict_parsing=False))
+        env['webob._parsed_query_vars'] = (vars, source)
+        return vars
+
+    str_GET = property(str_GET, doc=str_GET.__doc__)
+
+    str_queryvars = deprecated_property(str_GET, 'str_queryvars',
+                                        'use str_GET instead')
+
+
+    def GET(self):
+        """
+        Like ``.str_GET``, but may decode values and keys
+        """
+        vars = self.str_GET
+        if self.charset:
+            vars = UnicodeMultiDict(vars, encoding=self.charset,
+                                    errors=self.unicode_errors,
+                                    decode_keys=self.decode_param_names)
+        return vars
+
+    GET = property(GET, doc=GET.__doc__)
+
+    queryvars = deprecated_property(GET, 'queryvars',
+                                    'use GET instead')
+
+    def str_params(self):
+        """
+        A dictionary-like object containing both the parameters from
+        the query string and request body.
+        """
+        return NestedMultiDict(self.str_GET, self.str_POST)
+
+    str_params = property(str_params, doc=str_params.__doc__)
+
+    def params(self):
+        """
+        Like ``.str_params``, but may decode values and keys
+        """
+        params = self.str_params
+        if self.charset:
+            params = UnicodeMultiDict(params, encoding=self.charset,
+                                      errors=self.unicode_errors,
+                                      decode_keys=self.decode_param_names)
+        return params
+
+    params = property(params, doc=params.__doc__)
+
+    _rx_quotes = re.compile('"(.*)"')
+
+    def str_cookies(self):
+        """
+        Return a *plain* dictionary of cookies as found in the request.
+        """
+        env = self.environ
+        source = env.get('HTTP_COOKIE', '')
+        if 'webob._parsed_cookies' in env:
+            vars, var_source = env['webob._parsed_cookies']
+            if var_source == source:
+                return vars
+        vars = {}
+        if source:
+            cookies = BaseCookie()
+            cookies.load(source)
+            for name in cookies:
+                value = cookies[name].value
+                unquote_match = self._rx_quotes.match(value)
+                if unquote_match is not None:
+                    value = unquote_match.group(1)
+                vars[name] = value
+        env['webob._parsed_cookies'] = (vars, source)
+        return vars
+
+    str_cookies = property(str_cookies, doc=str_cookies.__doc__)
+
+    def cookies(self):
+        """
+        Like ``.str_cookies``, but may decode values and keys
+        """
+        vars = self.str_cookies
+        if self.charset:
+            vars = UnicodeMultiDict(vars, encoding=self.charset,
+                                    errors=self.unicode_errors,
+                                    decode_keys=self.decode_param_names)
+        return vars
+
+    cookies = property(cookies, doc=cookies.__doc__)
+
+    def copy(self):
+        """
+        Copy the request and environment object.
+
+        This only does a shallow copy, except of wsgi.input
+        """
+        env = self.environ.copy()
+        new_req = self.__class__(env)
+        new_req.copy_body()
+        return new_req
+
+    def copy_get(self):
+        """
+        Copies the request and environment object, but turning this request
+        into a GET along the way.  If this was a POST request (or any other verb)
+        then it becomes GET, and the request body is thrown away.
+        """
+        env = self.environ.copy()
+        env['wsgi.input'] = StringIO('')
+        env['CONTENT_LENGTH'] = '0'
+        if 'CONTENT_TYPE' in env:
+            del env['CONTENT_TYPE']
+        env['REQUEST_METHOD'] = 'GET'
+        return self.__class__(env)
+
+    def make_body_seekable(self):
+        """
+        This forces ``environ['wsgi.input']`` to be seekable.  That
+        is, if it doesn't have a `seek` method already, the content is
+        copied into a StringIO or temporary file.
+
+        The choice to copy to StringIO is made from
+        ``self.request_body_tempfile_limit``
+        """
+        input = self.body_file
+        if hasattr(input, 'seek'):
+            # It has a seek method, so we don't need to do anything
+            return
+        self.copy_body()
+
+    def copy_body(self):
+        """
+        Copies the body, in cases where it might be shared with
+        another request object and that is not desired.
+
+        This copies the body in-place, either into a StringIO object
+        or a temporary file.
+        """
+        length = self.content_length
+        if length == 0:
+            # No real need to copy this, but of course it is free
+            self.body_file = StringIO('')
+            return
+        tempfile_limit = self.request_body_tempfile_limit
+        body = None
+        input = self.body_file
+        if hasattr(input, 'seek'):
+            # Just in case someone has read parts of the body already
+            ## FIXME: Should we use .tell() to try to put the body
+            ## back to its previous position?
+            input.seek(0)
+        if length == -1:
+            body = self.body
+            length = len(body)
+            self.content_length = length
+        if tempfile_limit and length > tempfile_limit:
+            fileobj = tempfile.TemporaryFile()
+            if body is None:
+                while length:
+                    data = input.read(min(length, 4096))
+                    fileobj.write(data)
+                    length -= len(data)
+            else:
+                fileobj.write(body)
+            fileobj.seek(0)
+        else:
+            if body is None:
+                body = input.read(length)
+            fileobj = StringIO(body)
+        self.body_file = fileobj
+
+    def remove_conditional_headers(self, remove_encoding=True, remove_range=True,
+                                        remove_match=True, remove_modified=True):
+        """
+        Remove headers that make the request conditional.
+
+        These headers can cause the response to be 304 Not Modified,
+        which in some cases you may not want to be possible.
+
+        This does not remove headers like If-Match, which are used for
+        conflict detection.
+        """
+        check_keys = []
+        if remove_range:
+            check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE']
+        if remove_match:
+            check_keys.append('HTTP_IF_NONE_MATCH')
+        if remove_modified:
+            check_keys.append('HTTP_IF_MODIFIED_SINCE')
+        if remove_encoding:
+            check_keys.append('HTTP_ACCEPT_ENCODING')
+
+        for key in check_keys:
+            if key in self.environ:
+                del self.environ[key]
+
+    accept = converter(
+        environ_getter('HTTP_ACCEPT', rfc_section='14.1'),
+        _parse_accept, _serialize_accept, 'MIME Accept',
+        converter_args=('Accept', MIMEAccept, MIMENilAccept))
+
+    accept_charset = converter(
+        environ_getter('HTTP_ACCEPT_CHARSET', rfc_section='14.2'),
+        _parse_accept, _serialize_accept, 'accept header',
+        converter_args=('Accept-Charset', Accept, NilAccept))
+
+    accept_encoding = converter(
+        environ_getter('HTTP_ACCEPT_ENCODING', rfc_section='14.3'),
+        _parse_accept, _serialize_accept, 'accept header',
+        converter_args=('Accept-Encoding', Accept, NoAccept))
+
+    accept_language = converter(
+        environ_getter('HTTP_ACCEPT_LANGUAGE', rfc_section='14.4'),
+        _parse_accept, _serialize_accept, 'accept header',
+        converter_args=('Accept-Language', Accept, NilAccept))
+
+    ## FIXME: 14.8 Authorization
+    ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8
+
+    def _cache_control__get(self):
+        """
+        Get/set/modify the Cache-Control header (section `14.9
+        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
+        """
+        env = self.environ
+        value = env.get('HTTP_CACHE_CONTROL', '')
+        cache_header, cache_obj = env.get('webob._cache_control', (None, None))
+        if cache_obj is not None and cache_header == value:
+            return cache_obj
+        cache_obj = CacheControl.parse(value, type='request')
+        env['webob._cache_control'] = (value, cache_obj)
+        return cache_obj
+
+    def _cache_control__set(self, value):
+        env = self.environ
+        if not value:
+            value = ""
+        if isinstance(value, dict):
+            value = CacheControl(value, type='request')
+        elif isinstance(value, CacheControl):
+            str_value = str(value)
+            env['HTTP_CACHE_CONTROL'] = str_value
+            env['webob._cache_control'] = (str_value, value)
+        else:
+            env['HTTP_CACHE_CONTROL'] = str(value)
+            if 'webob._cache_control' in env:
+                del env['webob._cache_control']
+
+    def _cache_control__del(self, value):
+        env = self.environ
+        if 'HTTP_CACHE_CONTROL' in env:
+            del env['HTTP_CACHE_CONTROL']
+        if 'webob._cache_control' in env:
+            del env['webob._cache_control']
+
+    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
+
+    date = converter(
+        environ_getter('HTTP_DATE', rfc_section='14.8'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    if_match = converter(
+        environ_getter('HTTP_IF_MATCH', rfc_section='14.24'),
+        _parse_etag, _serialize_etag, 'ETag', converter_args=(True,))
+
+    if_modified_since = converter(
+        environ_getter('HTTP_IF_MODIFIED_SINCE', rfc_section='14.25'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    if_none_match = converter(
+        environ_getter('HTTP_IF_NONE_MATCH', rfc_section='14.26'),
+        _parse_etag, _serialize_etag, 'ETag', converter_args=(False,))
+
+    if_range = converter(
+        environ_getter('HTTP_IF_RANGE', rfc_section='14.27'),
+        _parse_if_range, _serialize_if_range, 'IfRange object')
+
+    if_unmodified_since = converter(
+        environ_getter('HTTP_IF_UNMODIFIED_SINCE', rfc_section='14.28'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    max_forwards = converter(
+        environ_getter('HTTP_MAX_FORWARDS', rfc_section='14.31'),
+        _parse_int, _serialize_int, 'int')
+
+    pragma = environ_getter('HTTP_PRAGMA', rfc_section='14.32')
+
+    range = converter(
+        environ_getter('HTTP_RANGE', rfc_section='14.35'),
+        _parse_range, _serialize_range, 'Range object')
+
+    referer = environ_getter('HTTP_REFERER', rfc_section='14.36')
+    referrer = referer
+
+    user_agent = environ_getter('HTTP_USER_AGENT', rfc_section='14.43')
+
+    def __repr__(self):
+        msg = '<%s at %x %s %s>' % (
+            self.__class__.__name__,
+            abs(id(self)), self.method, self.url)
+        return msg
+
+    def __str__(self):
+        url = self.url
+        host = self.host_url
+        assert url.startswith(host)
+        url = url[len(host):]
+        if 'Host' not in self.headers:
+            self.headers['Host'] = self.host
+        parts = ['%s %s' % (self.method, url)]
+        for name, value in sorted(self.headers.items()):
+            parts.append('%s: %s' % (name, value))
+        parts.append('')
+        parts.append(self.body)
+        return '\r\n'.join(parts)
+
+    def call_application(self, application, catch_exc_info=False):
+        """
+        Call the given WSGI application, returning ``(status_string,
+        headerlist, app_iter)``
+
+        Be sure to call ``app_iter.close()`` if it's there.
+
+        If catch_exc_info is true, then returns ``(status_string,
+        headerlist, app_iter, exc_info)``, where the fourth item may
+        be None, but won't be if there was an exception.  If you don't
+        do this and there was an exception, the exception will be
+        raised directly.
+        """
+        captured = []
+        output = []
+        def start_response(status, headers, exc_info=None):
+            if exc_info is not None and not catch_exc_info:
+                raise exc_info[0], exc_info[1], exc_info[2]
+            captured[:] = [status, headers, exc_info]
+            return output.append
+        app_iter = application(self.environ, start_response)
+        if (not captured
+            or output):
+            try:
+                output.extend(app_iter)
+            finally:
+                if hasattr(app_iter, 'close'):
+                    app_iter.close()
+            app_iter = output
+        if catch_exc_info:
+            return (captured[0], captured[1], app_iter, captured[2])
+        else:
+            return (captured[0], captured[1], app_iter)
+
+    # Will be filled in later:
+    ResponseClass = None
+
+    def get_response(self, application, catch_exc_info=False):
+        """
+        Like ``.call_application(application)``, except returns a
+        response object with ``.status``, ``.headers``, and ``.body``
+        attributes.
+
+        This will use ``self.ResponseClass`` to figure out the class
+        of the response object to return.
+        """
+        if catch_exc_info:
+            status, headers, app_iter, exc_info = self.call_application(
+                application, catch_exc_info=True)
+            del exc_info
+        else:
+            status, headers, app_iter = self.call_application(
+                application, catch_exc_info=False)
+        return self.ResponseClass(
+            status=status, headerlist=headers, app_iter=app_iter,
+            request=self)
+
+    #@classmethod
+    def blank(cls, path, environ=None, base_url=None, headers=None, **kw):
+        """
+        Create a blank request environ (and Request wrapper) with the
+        given path (path should be urlencoded), and any keys from
+        environ.
+
+        The path will become path_info, with any query string split
+        off and used.
+
+        All necessary keys will be added to the environ, but the
+        values you pass in will take precedence.  If you pass in
+        base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
+        be filled in from that value.
+
+        Any extra keyword will be passed to ``__init__`` (e.g.,
+        ``decode_param_names``).
+        """
+        if _SCHEME_RE.search(path):
+            scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
+            if fragment:
+                raise TypeError(
+                    "Path cannot contain a fragment (%r)" % fragment)
+            if qs:
+                path += '?' + qs
+            if ':' not in netloc:
+                if scheme == 'http':
+                    netloc += ':80'
+                elif scheme == 'https':
+                    netloc += ':443'
+                else:
+                    raise TypeError("Unknown scheme: %r" % scheme)
+        else:
+            scheme = 'http'
+            netloc = 'localhost:80'
+        if path and '?' in path:
+            path_info, query_string = path.split('?', 1)
+            path_info = urllib.unquote(path_info)
+        else:
+            path_info = urllib.unquote(path)
+            query_string = ''
+        env = {
+            'REQUEST_METHOD': 'GET',
+            'SCRIPT_NAME': '',
+            'PATH_INFO': path_info or '',
+            'QUERY_STRING': query_string,
+            'SERVER_NAME': netloc.split(':')[0],
+            'SERVER_PORT': netloc.split(':')[1],
+            'HTTP_HOST': netloc,
+            'SERVER_PROTOCOL': 'HTTP/1.0',
+            'wsgi.version': (1, 0),
+            'wsgi.url_scheme': scheme,
+            'wsgi.input': StringIO(''),
+            'wsgi.errors': sys.stderr,
+            'wsgi.multithread': False,
+            'wsgi.multiprocess': False,
+            'wsgi.run_once': False,
+            }
+        if base_url:
+            scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
+            if query or fragment:
+                raise ValueError(
+                    "base_url (%r) cannot have a query or fragment"
+                    % base_url)
+            if scheme:
+                env['wsgi.url_scheme'] = scheme
+            if netloc:
+                if ':' not in netloc:
+                    if scheme == 'http':
+                        netloc += ':80'
+                    elif scheme == 'https':
+                        netloc += ':443'
+                    else:
+                        raise ValueError(
+                            "Unknown scheme: %r" % scheme)
+                host, port = netloc.split(':', 1)
+                env['SERVER_PORT'] = port
+                env['SERVER_NAME'] = host
+                env['HTTP_HOST'] = netloc
+            if path:
+                env['SCRIPT_NAME'] = urllib.unquote(path)
+        if environ:
+            env.update(environ)
+        obj = cls(env, **kw)
+        if headers is not None:
+            obj.headers.update(headers)
+        return obj
+
+    blank = classmethod(blank)
+
+class Response(object):
+
+    """
+    Represents a WSGI response
+    """
+
+    default_content_type = 'text/html'
+    default_charset = 'UTF-8'
+    unicode_errors = 'strict'
+    default_conditional_response = False
+
+    def __init__(self, body=None, status='200 OK', headerlist=None, app_iter=None,
+                 request=None, content_type=None, conditional_response=NoDefault,
+                 **kw):
+        if app_iter is None:
+            if body is None:
+                body = ''
+        elif body is not None:
+            raise TypeError(
+                "You may only give one of the body and app_iter arguments")
+        self.status = status
+        if headerlist is None:
+            self._headerlist = []
+        else:
+            self._headerlist = headerlist
+        self._headers = None
+        if request is not None:
+            if hasattr(request, 'environ'):
+                self._environ = request.environ
+                self._request = request
+            else:
+                self._environ = request
+                self._request = None
+        else:
+            self._environ = self._request = None
+        if content_type is not None:
+            self.content_type = content_type
+        elif self.default_content_type is not None and headerlist is None:
+            self.content_type = self.default_content_type
+        if conditional_response is NoDefault:
+            self.conditional_response = self.default_conditional_response
+        else:
+            self.conditional_response = conditional_response
+        if 'charset' in kw:
+            # We set this early, so something like unicode_body works later
+            value = kw.pop('charset')
+            if value:
+                self.charset = value
+        elif self.default_charset and not self.charset and headerlist is None:
+            ct = self.content_type
+            if ct and (ct.startswith('text/') or ct.startswith('application/xml')
+                       or (ct.startswith('application/') and ct.endswith('+xml'))):
+                self.charset = self.default_charset
+        if app_iter is not None:
+            self._app_iter = app_iter
+            self._body = None
+        else:
+            if isinstance(body, unicode):
+                self.unicode_body = body
+            else:
+                self.body = body
+            self._app_iter = None
+        for name, value in kw.items():
+            if not hasattr(self.__class__, name):
+                # Not a basic attribute
+                raise TypeError(
+                    "Unexpected keyword: %s=%r" % (name, value))
+            setattr(self, name, value)
+
+    def __repr__(self):
+        return '<%s %x %s>' % (
+            self.__class__.__name__,
+            abs(id(self)),
+            self.status)
+
+    def __str__(self):
+        return (self.status + '\n'
+                + '\n'.join(['%s: %s' % (name, value)
+                             for name, value in self.headerlist])
+                + '\n\n'
+                + self.body)
+
+    def _status__get(self):
+        """
+        The status string
+        """
+        return self._status
+
+    def _status__set(self, value):
+        if isinstance(value, int):
+            value = str(value)
+        if not isinstance(value, str):
+            raise TypeError(
+                "You must set status to a string or integer (not %s)"
+                % type(value))
+        if ' ' not in value:
+            # Need to add a reason:
+            code = int(value)
+            reason = status_reasons[code]
+            value += ' ' + reason
+        self._status = value
+
+    status = property(_status__get, _status__set, doc=_status__get.__doc__)
+
+    def _status_int__get(self):
+        """
+        The status as an integer
+        """
+        return int(self.status.split()[0])
+    def _status_int__set(self, value):
+        self.status = value
+    status_int = property(_status_int__get, _status_int__set, doc=_status_int__get.__doc__)
+
+    def _headerlist__get(self):
+        """
+        The list of response headers
+        """
+        return self._headerlist
+
+    def _headerlist__set(self, value):
+        self._headers = None
+        if not isinstance(value, list):
+            if hasattr(value, 'items'):
+                value = value.items()
+            value = list(value)
+        self._headerlist = value
+
+    def _headerlist__del(self):
+        self.headerlist = []
+
+    headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__)
+
+    def _charset__get(self):
+        """
+        Get/set the charset (in the Content-Type)
+        """
+        header = self.headers.get('content-type')
+        if not header:
+            return None
+        match = _CHARSET_RE.search(header)
+        if match:
+            return match.group(1)
+        return None
+
+    def _charset__set(self, charset):
+        if charset is None:
+            del self.charset
+            return
+        try:
+            header = self.headers.pop('content-type')
+        except KeyError:
+            raise AttributeError(
+                "You cannot set the charset when no content-type is defined")
+        match = _CHARSET_RE.search(header)
+        if match:
+            header = header[:match.start()] + header[match.end():]
+        header += '; charset=%s' % charset
+        self.headers['content-type'] = header
+
+    def _charset__del(self):
+        try:
+            header = self.headers.pop('content-type')
+        except KeyError:
+            # Don't need to remove anything
+            return
+        match = _CHARSET_RE.search(header)
+        if match:
+            header = header[:match.start()] + header[match.end():]
+        self.headers['content-type'] = header
+
+    charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__)
+
+    def _content_type__get(self):
+        """
+        Get/set the Content-Type header (or None), *without* the
+        charset or any parameters.
+
+        If you include parameters (or ``;`` at all) when setting the
+        content_type, any existing parameters will be deleted;
+        otherwise they will be preserved.
+        """
+        header = self.headers.get('content-type')
+        if not header:
+            return None
+        return header.split(';', 1)[0]
+
+    def _content_type__set(self, value):
+        if ';' not in value:
+            header = self.headers.get('content-type', '')
+            if ';' in header:
+                params = header.split(';', 1)[1]
+                value += ';' + params
+        self.headers['content-type'] = value
+
+    def _content_type__del(self):
+        try:
+            del self.headers['content-type']
+        except KeyError:
+            pass
+
+    content_type = property(_content_type__get, _content_type__set,
+                            _content_type__del, doc=_content_type__get.__doc__)
+
+    def _content_type_params__get(self):
+        """
+        Returns a dictionary of all the parameters in the content type.
+        """
+        params = self.headers.get('content-type', '')
+        if ';' not in params:
+            return {}
+        params = params.split(';', 1)[1]
+        result = {}
+        for match in _PARAM_RE.finditer(params):
+            result[match.group(1)] = match.group(2) or match.group(3) or ''
+        return result
+
+    def _content_type_params__set(self, value_dict):
+        if not value_dict:
+            del self.content_type_params
+            return
+        params = []
+        for k, v in sorted(value_dict.items()):
+            if not _OK_PARAM_RE.search(v):
+                ## FIXME: I'm not sure what to do with "'s in the parameter value
+                ## I think it might be simply illegal
+                v = '"%s"' % v.replace('"', '\\"')
+            params.append('; %s=%s' % (k, v))
+        ct = self.headers.pop('content-type', '').split(';', 1)[0]
+        ct += ''.join(params)
+        self.headers['content-type'] = ct
+
+    def _content_type_params__del(self, value):
+        self.headers['content-type'] = self.headers.get('content-type', '').split(';', 1)[0]
+
+    content_type_params = property(_content_type_params__get, _content_type_params__set, _content_type_params__del, doc=_content_type_params__get.__doc__)
+
+    def _headers__get(self):
+        """
+        The headers in a dictionary-like object
+        """
+        if self._headers is None:
+            self._headers = HeaderDict.view_list(self.headerlist)
+        return self._headers
+
+    def _headers__set(self, value):
+        if hasattr(value, 'items'):
+            value = value.items()
+        self.headerlist = value
+        self._headers = None
+
+    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
+
+    def _body__get(self):
+        """
+        The body of the response, as a ``str``.  This will read in the
+        entire app_iter if necessary.
+        """
+        if self._body is None:
+            if self._app_iter is None:
+                raise AttributeError(
+                    "No body has been set")
+            try:
+                self._body = ''.join(self._app_iter)
+            finally:
+                if hasattr(self._app_iter, 'close'):
+                    self._app_iter.close()
+            self._app_iter = None
+            self.content_length = len(self._body)
+        return self._body
+
+    def _body__set(self, value):
+        if isinstance(value, unicode):
+            raise TypeError(
+                "You cannot set Response.body to a unicode object (use Response.unicode_body)")
+        if not isinstance(value, str):
+            raise TypeError(
+                "You can only set the body to a str (not %s)"
+                % type(value))
+        try:
+            if self._body or self._app_iter:
+                self.content_md5 = None
+        except AttributeError:
+            # if setting body early in initialization _body and _app_iter don't exist yet
+            pass
+        self._body = value
+        self.content_length = len(value)
+        self._app_iter = None
+
+    def _body__del(self):
+        self._body = None
+        self.content_length = None
+        self._app_iter = None
+
+    body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__)
+
+    def _body_file__get(self):
+        """
+        Returns a file-like object that can be used to write to the
+        body.  If you passed in a list app_iter, that app_iter will be
+        modified by writes.
+        """
+        return ResponseBodyFile(self)
+
+    def _body_file__del(self):
+        del self.body
+
+    body_file = property(_body_file__get, fdel=_body_file__del, doc=_body_file__get.__doc__)
+
+    def write(self, text):
+        if isinstance(text, unicode):
+            self.unicode_body += text
+        else:
+            self.body += text
+
+    def _unicode_body__get(self):
+        """
+        Get/set the unicode value of the body (using the charset of the Content-Type)
+        """
+        if not self.charset:
+            raise AttributeError(
+                "You cannot access Response.unicode_body unless charset is set")
+        body = self.body
+        return body.decode(self.charset, self.unicode_errors)
+
+    def _unicode_body__set(self, value):
+        if not self.charset:
+            raise AttributeError(
+                "You cannot access Response.unicode_body unless charset is set")
+        if not isinstance(value, unicode):
+            raise TypeError(
+                "You can only set Response.unicode_body to a unicode string (not %s)" % type(value))
+        self.body = value.encode(self.charset)
+
+    def _unicode_body__del(self):
+        del self.body
+
+    unicode_body = property(_unicode_body__get, _unicode_body__set, _unicode_body__del, doc=_unicode_body__get.__doc__)
+
+    def _app_iter__get(self):
+        """
+        Returns the app_iter of the response.
+
+        If body was set, this will create an app_iter from that body
+        (a single-item list)
+        """
+        if self._app_iter is None:
+            if self._body is None:
+                raise AttributeError(
+                    "No body or app_iter has been set")
+            return [self._body]
+        else:
+            return self._app_iter
+
+    def _app_iter__set(self, value):
+        if self._body is not None:
+            # Undo the automatically-set content-length
+            self.content_length = None
+        self._app_iter = value
+        self._body = None
+
+    def _app_iter__del(self):
+        self.content_length = None
+        self._app_iter = self._body = None
+
+    app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__)
+
+    def set_cookie(self, key, value='', max_age=None,
+                   path='/', domain=None, secure=None, httponly=False,
+                   version=None, comment=None, expires=None):
+        """
+        Set (add) a cookie for the response
+        """
+        if isinstance(value, unicode) and self.charset is not None:
+            value = '"%s"' % value.encode(self.charset)
+        cookies = BaseCookie()
+        cookies[key] = value
+        if isinstance(max_age, timedelta):
+            max_age = max_age.seconds + max_age.days*24*60*60
+        if max_age is not None and expires is None:
+            expires = datetime.utcnow() + timedelta(seconds=max_age)
+        if isinstance(expires, timedelta):
+            expires = datetime.utcnow() + expires
+        if isinstance(expires, datetime):
+            expires = '"'+_serialize_cookie_date(expires)+'"'
+        for var_name, var_value in [
+            ('max_age', max_age),
+            ('path', path),
+            ('domain', domain),
+            ('secure', secure),
+            ('HttpOnly', httponly),
+            ('version', version),
+            ('comment', comment),
+            ('expires', expires),
+            ]:
+            if var_value is not None and var_value is not False:
+                cookies[key][var_name.replace('_', '-')] = str(var_value)
+        header_value = cookies[key].output(header='').lstrip()
+        if header_value.endswith(';'):
+            # Python 2.4 adds a trailing ; to the end, strip it to be
+            # consistent with 2.5
+            header_value = header_value[:-1]
+        self.headerlist.append(('Set-Cookie', header_value))
+
+    def delete_cookie(self, key, path='/', domain=None):
+        """
+        Delete a cookie from the client.  Note that path and domain must match
+        how the cookie was originally set.
+
+        This sets the cookie to the empty string, and max_age=0 so
+        that it should expire immediately.
+        """
+        self.set_cookie(key, '', path=path, domain=domain,
+                        max_age=0, expires=timedelta(days=-5))
+
+    def unset_cookie(self, key):
+        """
+        Unset a cookie with the given name (remove it from the
+        response).  If there are multiple cookies (e.g., two cookies
+        with the same name and different paths or domains), all such
+        cookies will be deleted.
+        """
+        existing = self.headers.getall('Set-Cookie')
+        if not existing:
+            raise KeyError(
+                "No cookies at all have been set")
+        del self.headers['Set-Cookie']
+        found = False
+        for header in existing:
+            cookies = BaseCookie()
+            cookies.load(header)
+            if key in cookies:
+                found = True
+                del cookies[key]
+                header = cookies.output(header='').lstrip()
+            if header:
+                if header.endswith(';'):
+                    # Python 2.4 adds a trailing ; to the end, strip it
+                    # to be consistent with 2.5
+                    header = header[:-1]
+                self.headers.add('Set-Cookie', header)
+        if not found:
+            raise KeyError(
+                "No cookie has been set with the name %r" % key)
+
+    def _location__get(self):
+        """
+        Retrieve the Location header of the response, or None if there
+        is no header.  If the header is not absolute and this response
+        is associated with a request, make the header absolute.
+
+        For more information see `section 14.30
+        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30>`_.
+        """
+        if 'location' not in self.headers:
+            return None
+        location = self.headers['location']
+        if _SCHEME_RE.search(location):
+            # Absolute
+            return location
+        if self.request is not None:
+            base_uri = self.request.url
+            location = urlparse.urljoin(base_uri, location)
+        return location
+
+    def _location__set(self, value):
+        if not _SCHEME_RE.search(value):
+            # Not absolute, see if we can make it absolute
+            if self.request is not None:
+                value = urlparse.urljoin(self.request.url, value)
+        self.headers['location'] = value
+
+    def _location__del(self):
+        if 'location' in self.headers:
+            del self.headers['location']
+
+    location = property(_location__get, _location__set, _location__del, doc=_location__get.__doc__)
+
+    accept_ranges = header_getter('Accept-Ranges', rfc_section='14.5')
+
+    age = converter(
+        header_getter('Age', rfc_section='14.6'),
+        _parse_int_safe, _serialize_int, 'int')
+
+    allow = converter(
+        header_getter('Allow', rfc_section='14.7'),
+        _parse_list, _serialize_list, 'list')
+
+    _cache_control_obj = None
+
+    def _cache_control__get(self):
+        """
+        Get/set/modify the Cache-Control header (section `14.9
+        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
+        """
+        value = self.headers.get('cache-control', '')
+        if self._cache_control_obj is None:
+            self._cache_control_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='response')
+            self._cache_control_obj.header_value = value
+        if self._cache_control_obj.header_value != value:
+            new_obj = CacheControl.parse(value, type='response')
+            self._cache_control_obj.properties.clear()
+            self._cache_control_obj.properties.update(new_obj.properties)
+            self._cache_control_obj.header_value = value
+        return self._cache_control_obj
+
+    def _cache_control__set(self, value):
+        # This actually becomes a copy
+        if not value:
+            value = ""
+        if isinstance(value, dict):
+            value = CacheControl(value, 'response')
+        if isinstance(value, unicode):
+            value = str(value)
+        if isinstance(value, str):
+            if self._cache_control_obj is None:
+                self.headers['Cache-Control'] = value
+                return
+            value = CacheControl.parse(value, 'response')
+        cache = self.cache_control
+        cache.properties.clear()
+        cache.properties.update(value.properties)
+
+    def _cache_control__del(self):
+        self.cache_control = {}
+
+    def _update_cache_control(self, prop_dict):
+        value = serialize_cache_control(prop_dict)
+        if not value:
+            if 'Cache-Control' in self.headers:
+                del self.headers['Cache-Control']
+        else:
+            self.headers['Cache-Control'] = value
+
+    cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__)
+
+    def cache_expires(self, seconds=0, **kw):
+        """
+        Set expiration on this request.  This sets the response to
+        expire in the given seconds, and any other attributes are used
+        for cache_control (e.g., private=True, etc).
+        """
+        cache_control = self.cache_control
+        if isinstance(seconds, timedelta):
+            seconds = timedelta_to_seconds(seconds)
+        if not seconds:
+            # To really expire something, you have to force a
+            # bunch of these cache control attributes, and IE may
+            # not pay attention to those still so we also set
+            # Expires.
+            cache_control.no_store = True
+            cache_control.no_cache = True
+            cache_control.must_revalidate = True
+            cache_control.max_age = 0
+            cache_control.post_check = 0
+            cache_control.pre_check = 0
+            self.expires = datetime.utcnow()
+            if 'last-modified' not in self.headers:
+                self.last_modified = datetime.utcnow()
+            self.pragma = 'no-cache'
+        else:
+            cache_control.max_age = seconds
+            self.expires = datetime.utcnow() + timedelta(seconds=seconds)
+        for name, value in kw.items():
+            setattr(cache_control, name, value)
+
+    content_encoding = header_getter('Content-Encoding', rfc_section='14.11')
+
+    def encode_content(self, encoding='gzip'):
+        """
+        Encode the content with the given encoding (only gzip and
+        identity are supported).
+        """
+        if encoding == 'identity':
+            self.decode_content()
+            return
+        if encoding != 'gzip':
+            raise ValueError(
+                "Unknown encoding: %r" % encoding)
+        if self.content_encoding:
+            if self.content_encoding == encoding:
+                return
+            self.decode_content()
+        from webob.util.safegzip import GzipFile
+        f = StringIO()
+        gzip_f = GzipFile(filename='', mode='w', fileobj=f)
+        gzip_f.write(self.body)
+        gzip_f.close()
+        new_body = f.getvalue()
+        f.close()
+        self.content_encoding = 'gzip'
+        self.body = new_body
+
+    def decode_content(self):
+        content_encoding = self.content_encoding
+        if not content_encoding or content_encoding == 'identity':
+            return
+        if content_encoding != 'gzip':
+            raise ValueError(
+                "I don't know how to decode the content %s" % content_encoding)
+        from webob.util.safegzip import GzipFile
+        f = StringIO(self.body)
+        gzip_f = GzipFile(filename='', mode='r', fileobj=f)
+        new_body = gzip_f.read()
+        gzip_f.close()
+        f.close()
+        self.content_encoding = None
+        self.body = new_body
+
+    content_language = converter(
+        header_getter('Content-Language', rfc_section='14.12'),
+        _parse_list, _serialize_list, 'list')
+
+    content_location = header_getter(
+        'Content-Location', rfc_section='14.14')
+
+    content_md5 = header_getter(
+        'Content-MD5', rfc_section='14.14')
+
+    content_range = converter(
+        header_getter('Content-Range', rfc_section='14.16'),
+        _parse_content_range, _serialize_content_range, 'ContentRange object')
+
+    content_length = converter(
+        header_getter('Content-Length', rfc_section='14.17'),
+        _parse_int, _serialize_int, 'int')
+
+    date = converter(
+        header_getter('Date', rfc_section='14.18'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    etag = header_getter('ETag', rfc_section='14.19')
+
+    def md5_etag(self, body=None, set_content_md5=False, set_conditional_response=False):
+        """
+        Generate an etag for the response object using an MD5 hash of
+        the body (the body parameter, or ``self.body`` if not given)
+
+        Sets ``self.etag``
+        If ``set_content_md5`` is True sets ``self.content_md5`` as well
+        If ``set_conditional_response`` is True sets ``self.conditional_response`` to True
+        """
+        if body is None:
+            body = self.body
+        try:
+            from hashlib import md5
+        except ImportError:
+            from md5 import md5
+        h = md5(body)
+        md5_digest = h.digest().encode('base64').replace('\n', '').strip('=')
+        self.etag = md5_digest
+        if set_content_md5:
+            self.content_md5 = md5_digest
+        if set_conditional_response:
+            self.conditional_response = True
+
+    expires = converter(
+        header_getter('Expires', rfc_section='14.21'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    last_modified = converter(
+        header_getter('Last-Modified', rfc_section='14.29'),
+        _parse_date, _serialize_date, 'HTTP date')
+
+    pragma = header_getter('Pragma', rfc_section='14.32')
+
+    retry_after = converter(
+        header_getter('Retry-After', rfc_section='14.37'),
+        _parse_date_delta, _serialize_date_delta, 'HTTP date or delta seconds')
+
+    server = header_getter('Server', rfc_section='14.38')
+
+    ## FIXME: I realize response.vary += 'something' won't work.  It should.
+    ## Maybe for all listy headers.
+    vary = converter(
+        header_getter('Vary', rfc_section='14.44'),
+        _parse_list, _serialize_list, 'list')
+
+    ## FIXME: 14.47 WWW-Authenticate
+    ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.47
+
+
+    def _request__get(self):
+        """
+        Return the request associated with this response if any.
+        """
+        if self._request is None and self._environ is not None:
+            self._request = self.RequestClass(self._environ)
+        return self._request
+
+    def _request__set(self, value):
+        if value is None:
+            del self.request
+            return
+        if isinstance(value, dict):
+            self._environ = value
+            self._request = None
+        else:
+            self._request = value
+            self._environ = value.environ
+
+    def _request__del(self):
+        self._request = self._environ = None
+
+    request = property(_request__get, _request__set, _request__del, doc=_request__get.__doc__)
+
+    def _environ__get(self):
+        """
+        Get/set the request environ associated with this response, if
+        any.
+        """
+        return self._environ
+
+    def _environ__set(self, value):
+        if value is None:
+            del self.environ
+        self._environ = value
+        self._request = None
+
+    def _environ__del(self):
+        self._request = self._environ = None
+
+    environ = property(_environ__get, _environ__set, _environ__del, doc=_environ__get.__doc__)
+
+    def __call__(self, environ, start_response):
+        """
+        WSGI application interface
+        """
+        if self.conditional_response:
+            return self.conditional_response_app(environ, start_response)
+        start_response(self.status, self.headerlist)
+        if environ['REQUEST_METHOD'] == 'HEAD':
+            # Special case here...
+            return []
+        return self.app_iter
+
+    _safe_methods = ('GET', 'HEAD')
+
+    def conditional_response_app(self, environ, start_response):
+        """
+        Like the normal __call__ interface, but checks conditional headers:
+
+        * If-Modified-Since   (304 Not Modified; only on GET, HEAD)
+        * If-None-Match       (304 Not Modified; only on GET, HEAD)
+        * Range               (406 Partial Content; only on GET, HEAD)
+        """
+        req = self.RequestClass(environ)
+        status304 = False
+        if req.method in self._safe_methods:
+            if req.if_modified_since and self.last_modified and self.last_modified <= req.if_modified_since:
+                status304 = True
+            if req.if_none_match and self.etag:
+                ## FIXME: should a weak match be okay?
+                if self.etag in req.if_none_match:
+                    status304 = True
+                else:
+                    # Even if If-Modified-Since matched, if ETag doesn't then reject it
+                    status304 = False
+        if status304:
+            start_response('304 Not Modified', self.headerlist)
+            return []
+        if req.method == 'HEAD':
+            start_response(self.status, self.headerlist)
+            return []
+        if (req.range and req.if_range.match_response(self)
+            and self.content_range is None
+            and req.method == 'GET'
+            and self.status_int == 200):
+            content_range = req.range.content_range(self.content_length)
+            if content_range is not None:
+                app_iter = self.app_iter_range(content_range.start, content_range.stop)
+                if app_iter is not None:
+                    headers = list(self.headerlist)
+                    headers.append(('Content-Range', str(content_range)))
+                    start_response('206 Partial Content', headers)
+                    return app_iter
+        start_response(self.status, self.headerlist)
+        return self.app_iter
+
+    def app_iter_range(self, start, stop):
+        """
+        Return a new app_iter built from the response app_iter, that
+        serves up only the given ``start:stop`` range.
+        """
+        if self._app_iter is None:
+            return [self.body[start:stop]]
+        app_iter = self.app_iter
+        if hasattr(app_iter, 'app_iter_range'):
+            return app_iter.app_iter_range(start, stop)
+        return AppIterRange(app_iter, start, stop)
+
+
+Request.ResponseClass = Response
+Response.RequestClass = Request
+
+def _cgi_FieldStorage__repr__patch(self):
+    """ monkey patch for FieldStorage.__repr__
+
+    Unbelievely, the default __repr__ on FieldStorage reads
+    the entire file content instead of being sane about it.
+    This is a simple replacement that doesn't do that
+    """
+    if self.file:
+        return "FieldStorage(%r, %r)" % (
+                self.name, self.filename)
+    return "FieldStorage(%r, %r, %r)" % (
+             self.name, self.filename, self.value)
+
+cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
+
+class FakeCGIBody(object):
+
+    def __init__(self, vars):
+        self.vars = vars
+        self._body = None
+        self.position = 0
+
+    def read(self, size=-1):
+        body = self._get_body()
+        if size == -1:
+            v = body[self.position:]
+            self.position = len(body)
+            return v
+        else:
+            v = body[self.position:self.position+size]
+            self.position = min(len(body), self.position+size)
+            return v
+
+    def _get_body(self):
+        if self._body is None:
+            self._body = urllib.urlencode(self.vars.items())
+        return self._body
+
+    def readline(self, size=None):
+        # We ignore size, but allow it to be hinted
+        rest = self._get_body()[self.position:]
+        next = rest.find('\r\n')
+        if next == -1:
+            return self.read()
+        self.position += next+2
+        return rest[:next+2]
+
+    def readlines(self, hint=None):
+        # Again, allow hint but ignore
+        body = self._get_body()
+        rest = body[self.position:]
+        self.position = len(body)
+        result = []
+        while 1:
+            next = rest.find('\r\n')
+            if next == -1:
+                result.append(rest)
+                break
+            result.append(rest[:next+2])
+            rest = rest[next+2:]
+        return result
+
+    def __iter__(self):
+        return iter(self.readlines())
+
+    def __repr__(self):
+        inner = repr(self.vars)
+        if len(inner) > 20:
+            inner = inner[:15] + '...' + inner[-5:]
+        return '<%s at %x viewing %s>' % (
+            self.__class__.__name__,
+            abs(id(self)), inner)
+
+    #@classmethod
+    def update_environ(cls, environ, vars):
+        obj = cls(vars)
+        environ['CONTENT_LENGTH'] = '-1'
+        environ['wsgi.input'] = obj
+
+    update_environ = classmethod(update_environ)
+
+class ResponseBodyFile(object):
+
+    def __init__(self, response):
+        self.response = response
+
+    def __repr__(self):
+        return '<body_file for %r>' % (
+            self.response)
+
+    def close(self):
+        raise NotImplementedError(
+            "Response bodies cannot be closed")
+
+    def flush(self):
+        pass
+
+    def write(self, s):
+        if isinstance(s, unicode):
+            if self.response.charset is not None:
+                s = s.encode(self.response.charset)
+            else:
+                raise TypeError(
+                    "You can only write unicode to Response.body_file "
+                    "if charset has been set")
+        if not isinstance(s, str):
+            raise TypeError(
+                "You can only write str to a Response.body_file, not %s"
+                % type(s))
+        if not isinstance(self.response._app_iter, list):
+            body = self.response.body
+            if body:
+                self.response.app_iter = [body]
+            else:
+                self.response.app_iter = []
+        self.response.app_iter.append(s)
+
+    def writelines(self, seq):
+        for item in seq:
+            self.write(item)
+
+    closed = False
+
+    def encoding(self):
+        """
+        The encoding of the file (inherited from response.charset)
+        """
+        return self.response.charset
+
+    encoding = property(encoding, doc=encoding.__doc__)
+
+    mode = 'wb'
+
+class AppIterRange(object):
+    """
+    Wraps an app_iter, returning just a range of bytes
+    """
+
+    def __init__(self, app_iter, start, stop):
+        assert start >= 0, "Bad start: %r" % start
+        assert stop is None or (stop >= 0 and stop >= start), (
+            "Bad stop: %r" % stop)
+        self.app_iter = app_iter
+        self.app_iterator = iter(app_iter)
+        self.start = start
+        if stop is None:
+            self.length = -1
+        else:
+            self.length = stop - start
+        if start:
+            self._served = None
+        else:
+            self._served = 0
+        if hasattr(app_iter, 'close'):
+            self.close = app_iter.close
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        if self._served is None:
+            # Haven't served anything; need to skip some leading bytes
+            skipped = 0
+            start = self.start
+            while 1:
+                chunk = self.app_iterator.next()
+                skipped += len(chunk)
+                extra = skipped - start
+                if extra == 0:
+                    self._served = 0
+                    break
+                elif extra > 0:
+                    self._served = extra
+                    return chunk[-extra:]
+        length = self.length
+        if length is None:
+            # Spent
+            raise StopIteration
+        chunk = self.app_iterator.next()
+        if length == -1:
+            return chunk
+        if self._served + len(chunk) > length:
+            extra = self._served + len(chunk) - length
+            self.length = None
+            return chunk[:-extra]
+        self._served += len(chunk)
+        return chunk
+
diff --git a/lib/webob/acceptparse.py b/lib/webob/acceptparse.py
@@ -0,0 +1,297 @@
+"""
+Parses a variety of ``Accept-*`` headers.
+
+These headers generally take the form of::
+
+    value1; q=0.5, value2; q=0
+
+Where the ``q`` parameter is optional.  In theory other parameters
+exists, but this ignores them.
+"""
+
+import re
+try:
+    sorted
+except NameError:
+    from webob.compat import sorted
+
+part_re = re.compile(
+    r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')
+
+def parse_accept(value):
+    """
+    Parses an ``Accept-*`` style header.
+
+    A list of ``[(value, quality), ...]`` is returned.  ``quality``
+    will be 1 if it was not given.
+    """
+    result = []
+    for match in part_re.finditer(','+value):
+        name = match.group(1)
+        if name == 'q':
+            continue
+        quality = match.group(2) or ''
+        if not quality:
+            quality = 1
+        else:
+            try:
+                quality = max(min(float(quality), 1), 0)
+            except ValueError:
+                quality = 1
+        result.append((name, quality))
+    return result
+
+class Accept(object):
+    """
+    Represents a generic ``Accept-*`` style header.
+
+    This object should not be modified.  To add items you can use
+    ``accept_obj + 'accept_thing'`` to get a new object
+    """
+
+    def __init__(self, header_name, header_value):
+        self.header_name = header_name
+        self.header_value = header_value
+        self._parsed = parse_accept(header_value)
+
+    def __repr__(self):
+        return '<%s at %x %s: %s>' % (
+            self.__class__.__name__,
+            abs(id(self)),
+            self.header_name, str(self))
+
+    def __str__(self):
+        result = []
+        for match, quality in self._parsed:
+            if quality != 1:
+                match = '%s;q=%0.1f' % (match, quality)
+            result.append(match)
+        return ', '.join(result)
+
+    # FIXME: should subtraction be allowed?
+    def __add__(self, other, reversed=False):
+        if isinstance(other, Accept):
+            other = other.header_value
+        if hasattr(other, 'items'):
+            other = sorted(other.items(), key=lambda item: -item[1])
+        if isinstance(other, (list, tuple)):
+            result = []
+            for item in other:
+                if isinstance(item, (list, tuple)):
+                    name, quality = item
+                    result.append('%s; q=%s' % (name, quality))
+                else:
+                    result.append(item)
+            other = ', '.join(result)
+        other = str(other)
+        my_value = self.header_value
+        if reversed:
+            other, my_value = my_value, other
+        if not other:
+            new_value = my_value
+        elif not my_value:
+            new_value = other
+        else:
+            new_value = my_value + ', ' + other
+        return self.__class__(self.header_name, new_value)
+
+    def __radd__(self, other):
+        return self.__add__(other, True)
+
+    def __contains__(self, match):
+        """
+        Returns true if the given object is listed in the accepted
+        types.
+        """
+        for item, quality in self._parsed:
+            if self._match(item, match):
+                return True
+
+    def quality(self, match):
+        """
+        Return the quality of the given match.  Returns None if there
+        is no match (not 0).
+        """
+        for item, quality in self._parsed:
+            if self._match(item, match):
+                return quality
+        return None
+    
+    def first_match(self, matches):
+        """
+        Returns the first match in the sequences of matches that is
+        allowed.  Ignores quality.  Returns the first item if nothing
+        else matches; or if you include None at the end of the match
+        list then that will be returned.
+        """
+        if not matches:
+            raise ValueError(
+                "You must pass in a non-empty list")
+        for match in matches:
+            for item, quality in self._parsed:
+                if self._match(item, match):
+                    return match
+            if match is None:
+                return None
+        return matches[0]
+    
+    def best_match(self, matches, default_match=None):
+        """
+        Returns the best match in the sequence of matches.
+
+        The sequence can be a simple sequence, or you can have
+        ``(match, server_quality)`` items in the sequence.  If you
+        have these tuples then the client quality is multiplied by the
+        server_quality to get a total.
+
+        default_match (default None) is returned if there is no intersection.
+        """
+        best_quality = -1
+        best_match = default_match
+        for match_item in matches:
+            if isinstance(match_item, (tuple, list)):
+                match, server_quality = match_item
+            else:
+                match = match_item
+                server_quality = 1
+            for item, quality in self._parsed:
+                possible_quality = server_quality * quality
+                if possible_quality < best_quality:
+                    continue
+                if self._match(item, match):
+                    best_quality = possible_quality
+                    best_match = match
+        return best_match
+
+    def best_matches(self, fallback=None):
+        """
+        Return all the matches in order of quality, with fallback (if
+        given) at the end.
+        """
+        items = [
+            i for i, q in sorted(self._parsed, key=lambda iq: -iq[1])]
+        if fallback:
+            for index, item in enumerate(items):
+                if self._match(item, fallback):
+                    items[index+1:] = []
+                    break
+            else:
+                items.append(fallback)
+        return items
+
+    def _match(self, item, match):
+        return item.lower() == match.lower() or item == '*'
+
+class NilAccept(object):
+
+    """
+    Represents an Accept header with no value.
+    """
+
+    MasterClass = Accept
+
+    def __init__(self, header_name):
+        self.header_name = header_name
+
+    def __repr__(self):
+        return '<%s for %s: %s>' % (
+            self.__class__.__name__, self.header_name, self.MasterClass)
+
+    def __str__(self):
+        return ''
+
+    def __add__(self, item):
+        if isinstance(item, self.MasterClass):
+            return item
+        else:
+            return self.MasterClass(self.header_name, '') + item
+
+    def __radd__(self, item):
+        if isinstance(item, self.MasterClass):
+            return item
+        else:
+            return item + self.MasterClass(self.header_name, '')
+
+    def __contains__(self, item):
+        return True
+
+    def quality(self, match, default_quality=1):
+        return 0
+
+    def first_match(self, matches):
+        return matches[0]
+
+    def best_match(self, matches, default_match=None):
+        best_quality = -1
+        best_match = default_match
+        for match_item in matches:
+            if isinstance(match_item, (list, tuple)):
+                match, quality = match_item
+            else:
+                match = match_item
+                quality = 1
+            if quality > best_quality:
+                best_match = match
+                best_quality = quality
+        return best_match
+
+    def best_matches(self, fallback=None):
+        if fallback:
+            return [fallback]
+        else:
+            return []
+
+class NoAccept(NilAccept):
+
+    def __contains__(self, item):
+        return False
+
+class MIMEAccept(Accept):
+
+    """
+    Represents the ``Accept`` header, which is a list of mimetypes.
+
+    This class knows about mime wildcards, like ``image/*``
+    """
+
+    def _match(self, item, match):
+        item = item.lower()
+        if item == '*':
+            item = '*/*'
+        match = match.lower()
+        if match == '*':
+            match = '*/*'
+        if '/' not in item:
+            # Bad, but we ignore
+            return False
+        if '/' not in match:
+            raise ValueError(
+                "MIME matches must include / (bad: %r)" % match)
+        item_major, item_minor = item.split('/', 1)
+        match_major, match_minor = match.split('/', 1)
+        if match_major == '*' and match_minor != '*':
+            raise ValueError(
+                "A MIME type of %r doesn't make sense" % match)
+        if item_major == '*' and item_minor != '*':
+            # Bad, but we ignore
+            return False
+        if ((item_major == '*' and item_minor == '*')
+            or (match_major == '*' and match_minor == '*')):
+            return True
+        if (item_major == match_major
+            and ((item_minor == '*' or match_minor == '*')
+                 or item_minor == match_minor)):
+            return True
+        return False
+
+    def accept_html(self):
+        """
+        Returns true if any HTML-like type is accepted
+        """
+        return ('text/html' in self
+                or 'application/xhtml+xml' in self
+                or 'application/xml' in self
+                or 'text/xml' in self)
+
+class MIMENilAccept(NilAccept):
+    MasterClass = MIMEAccept
diff --git a/lib/webob/byterange.py b/lib/webob/byterange.py
@@ -0,0 +1,295 @@
+class Range(object):
+
+    """
+    Represents the Range header.
+
+    This only represents ``bytes`` ranges, which are the only kind
+    specified in HTTP.  This can represent multiple sets of ranges,
+    but no place else is this multi-range facility supported.
+    """
+
+    def __init__(self, ranges):
+        for begin, end in ranges:
+            assert end is None or end >= 0, "Bad ranges: %r" % ranges
+        self.ranges = ranges
+
+    def satisfiable(self, length):
+        """
+        Returns true if this range can be satisfied by the resource
+        with the given byte length.
+        """
+        for begin, end in self.ranges:
+            if end is not None and end >= length:
+                return False
+        return True
+
+    def range_for_length(self, length):
+        """
+        *If* there is only one range, and *if* it is satisfiable by
+        the given length, then return a (begin, end) non-inclusive range
+        of bytes to serve.  Otherwise return None
+
+        If length is None (unknown length), then the resulting range
+        may be (begin, None), meaning it should be served from that
+        point.  If it's a range with a fixed endpoint we won't know if
+        it is satisfiable, so this will return None.
+        """
+        if len(self.ranges) != 1:
+            return None
+        begin, end = self.ranges[0]
+        if length is None:
+            # Unknown; only works with ranges with no end-point
+            if end is None:
+                return (begin, end)
+            return None
+        if end >= length:
+            # Overshoots the end
+            return None
+        return (begin, end)
+
+    def content_range(self, length):
+        """
+        Works like range_for_length; returns None or a ContentRange object
+
+        You can use it like::
+
+            response.content_range = req.range.content_range(response.content_length)
+
+        Though it's still up to you to actually serve that content range!
+        """
+        range = self.range_for_length(length)
+        if range is None:
+            return None
+        return ContentRange(range[0], range[1], length)
+
+    def __str__(self):
+        return self.serialize_bytes('bytes', self.python_ranges_to_bytes(self.ranges))
+
+    def __repr__(self):
+        return '<%s ranges=%s>' % (
+            self.__class__.__name__,
+            ', '.join(map(repr, self.ranges)))
+
+    #@classmethod
+    def parse(cls, header):
+        """
+        Parse the header; may return None if header is invalid
+        """
+        bytes = cls.parse_bytes(header)
+        if bytes is None:
+            return None
+        units, ranges = bytes
+        if units.lower() != 'bytes':
+            return None
+        ranges = cls.bytes_to_python_ranges(ranges)
+        if ranges is None:
+            return None
+        return cls(ranges)
+    parse = classmethod(parse)
+
+    #@staticmethod
+    def parse_bytes(header):
+        """
+        Parse a Range header into (bytes, list_of_ranges).  Note that the
+        ranges are *inclusive* (like in HTTP, not like in Python
+        typically).
+
+        Will return None if the header is invalid
+        """
+        if not header:
+            raise TypeError(
+                "The header must not be empty")
+        ranges = []
+        last_end = 0
+        try:
+            (units, range) = header.split("=", 1)
+            units = units.strip().lower()
+            for item in range.split(","):
+                if '-' not in item:
+                    raise ValueError()
+                if item.startswith('-'):
+                    # This is a range asking for a trailing chunk
+                    if last_end < 0:
+                        raise ValueError('too many end ranges')
+                    begin = int(item)
+                    end = None
+                    last_end = -1
+                else:
+                    (begin, end) = item.split("-", 1)
+                    begin = int(begin)
+                    if begin < last_end or last_end < 0:
+                        print begin, last_end
+                        raise ValueError('begin<last_end, or last_end<0')
+                    if not end.strip():
+                        end = None
+                    else:
+                        end = int(end)
+                    if end is not None and begin > end:
+                        raise ValueError('begin>end')
+                    last_end = end
+                ranges.append((begin, end))
+        except ValueError, e:
+            # In this case where the Range header is malformed,
+            # section 14.16 says to treat the request as if the
+            # Range header was not present.  How do I log this?
+            print e
+            return None
+        return (units, ranges)
+    parse_bytes = staticmethod(parse_bytes)
+
+    #@staticmethod
+    def serialize_bytes(units, ranges):
+        """
+        Takes the output of parse_bytes and turns it into a header
+        """
+        parts = []
+        for begin, end in ranges:
+            if end is None:
+                if begin >= 0:
+                    parts.append('%s-' % begin)
+                else:
+                    parts.append(str(begin))
+            else:
+                if begin < 0:
+                    raise ValueError(
+                        "(%r, %r) should have a non-negative first value" % (begin, end))
+                if end < 0:
+                    raise ValueError(
+                        "(%r, %r) should have a non-negative second value" % (begin, end))
+                parts.append('%s-%s' % (begin, end))
+        return '%s=%s' % (units, ','.join(parts))
+    serialize_bytes = staticmethod(serialize_bytes)
+
+    #@staticmethod
+    def bytes_to_python_ranges(ranges, length=None):
+        """
+        Converts the list-of-ranges from parse_bytes() to a Python-style
+        list of ranges (non-inclusive end points)
+
+        In the list of ranges, the last item can be None to indicate that
+        it should go to the end of the file, and the first item can be
+        negative to indicate that it should start from an offset from the
+        end.  If you give a length then this will not occur (negative
+        numbers and offsets will be resolved).
+
+        If length is given, and any range is not value, then None is
+        returned.
+        """
+        result = []
+        for begin, end in ranges:
+            if begin < 0:
+                if length is None:
+                    result.append((begin, None))
+                    continue
+                else:
+                    begin = length - begin
+                    end = length
+            if begin is None:
+                begin = 0
+            if end is None and length is not None:
+                end = length
+            if length is not None and end is not None and end > length:
+                return None
+            if end is not None:
+                end -= 1
+            result.append((begin, end))
+        return result
+    bytes_to_python_ranges = staticmethod(bytes_to_python_ranges)
+    
+    #@staticmethod
+    def python_ranges_to_bytes(ranges):
+        """
+        Converts a Python-style list of ranges to what serialize_bytes
+        expects.
+
+        This is the inverse of bytes_to_python_ranges
+        """
+        result = []
+        for begin, end in ranges:
+            if end is None:
+                result.append((begin, None))
+            else:
+                result.append((begin, end+1))
+        return result
+    python_ranges_to_bytes = staticmethod(python_ranges_to_bytes)
+
+class ContentRange(object):
+
+    """
+    Represents the Content-Range header
+
+    This header is ``start-stop/length``, where stop and length can be
+    ``*`` (represented as None in the attributes).
+    """
+
+    def __init__(self, start, stop, length):
+        assert start >= 0, "Bad start: %r" % start
+        assert stop is None or (stop >= 0 and stop >= start), (
+            "Bad stop: %r" % stop)
+        self.start = start
+        self.stop = stop
+        self.length = length
+
+    def __repr__(self):
+        return '<%s %s>' % (
+            self.__class__.__name__,
+            self)
+
+    def __str__(self):
+        if self.stop is None:
+            stop = '*'
+        else:
+            stop = self.stop + 1
+        if self.length is None:
+            length = '*'
+        else:
+            length = self.length
+        return 'bytes %s-%s/%s' % (self.start, stop, length)
+
+    def __iter__(self):
+        """
+        Mostly so you can unpack this, like:
+
+            start, stop, length = res.content_range
+        """
+        return iter([self.start, self.stop, self.length])
+
+    #@classmethod
+    def parse(cls, value):
+        """
+        Parse the header.  May return None if it cannot parse.
+        """
+        if value is None:
+            return None
+        value = value.strip()
+        if not value.startswith('bytes '):
+            # Unparseable
+            return None
+        value = value[len('bytes '):].strip()
+        if '/' not in value:
+            # Invalid, no length given
+            return None
+        range, length = value.split('/', 1)
+        if '-' not in range:
+            # Invalid, no range
+            return None
+        start, end = range.split('-', 1)
+        try:
+            start = int(start)
+            if end == '*':
+                end = None
+            else:
+                end = int(end)
+            if length == '*':
+                length = None
+            else:
+                length = int(length)
+        except ValueError:
+            # Parse problem
+            return None
+        if end is None:
+            return cls(start, None, length)
+        else:
+            return cls(start, end-1, length)
+    parse = classmethod(parse)
+    
diff --git a/lib/webob/cachecontrol.py b/lib/webob/cachecontrol.py
@@ -0,0 +1,169 @@
+"""
+Represents the Cache-Control header
+"""
+
+import re
+from webob.updatedict import UpdateDict
+try:
+    sorted
+except NameError:
+    from webob.compat import sorted
+
+token_re = re.compile(
+    r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?')
+need_quote_re = re.compile(r'[^a-zA-Z0-9._-]')
+
+class exists_property(object):
+    """
+    Represents a property that either is listed in the Cache-Control
+    header, or is not listed (has no value)
+    """
+    def __init__(self, prop, type=None):
+        self.prop = prop
+        self.type = type
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        return self.prop in obj.properties
+    def __set__(self, obj, value):
+        if (self.type is not None
+            and self.type != obj.type):
+            raise AttributeError(
+                "The property %s only applies to %s Cache-Control" % (self.prop, self.type))
+        if value:
+            obj.properties[self.prop] = None
+        else:
+            if self.prop in obj.properties:
+                del obj.properties[self.prop]
+    def __delete__(self, obj):
+        self.__set__(obj, False)
+
+class value_property(object):
+    """
+    Represents a property that has a value in the Cache-Control header.
+
+    When no value is actually given, the value of self.none is returned.
+    """
+    def __init__(self, prop, default=None, none=None, type=None):
+        self.prop = prop
+        self.default = default
+        self.none = none
+        self.type = type
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        if self.prop in obj.properties:
+            value = obj.properties[self.prop]
+            if value is None:
+                return self.none
+            else:
+                return value
+        else:
+            return self.default
+    def __set__(self, obj, value):
+        if (self.type is not None
+            and self.type != obj.type):
+            raise AttributeError(
+                "The property %s only applies to %s Cache-Control" % (self.prop, self.type))
+        if value == self.default:
+            if self.prop in obj.properties:
+                del obj.properties[self.prop]
+        elif value is True:
+            obj.properties[self.prop] = None # Empty value, but present
+        else:
+            obj.properties[self.prop] = value
+    def __delete__(self, obj):
+        if self.prop in obj.properties:
+            del obj.properties[self.prop]
+
+class CacheControl(object):
+
+    """
+    Represents the Cache-Control header.
+
+    By giving a type of ``'request'`` or ``'response'`` you can
+    control what attributes are allowed (some Cache-Control values
+    only apply to requests or responses).
+    """
+
+    def __init__(self, properties, type):
+        self.properties = properties
+        self.type = type
+
+    #@classmethod
+    def parse(cls, header, updates_to=None, type=None):
+        """
+        Parse the header, returning a CacheControl object.
+
+        The object is bound to the request or response object
+        ``updates_to``, if that is given.
+        """
+        if updates_to:
+            props = UpdateDict()
+            props.updated = updates_to
+        else:
+            props = {}
+        for match in token_re.finditer(header):
+            name = match.group(1)
+            value = match.group(2) or match.group(3) or None
+            if value:
+                try:
+                    value = int(value)
+                except ValueError:
+                    pass
+            props[name] = value
+        obj = cls(props, type=type)
+        if updates_to:
+            props.updated_args = (obj,)
+        return obj
+
+    parse = classmethod(parse)
+
+    def __repr__(self):
+        return '<CacheControl %r>' % str(self)
+
+    # Request values:
+    # no-cache shared (below)
+    # no-store shared (below)
+    # max-age shared  (below)
+    max_stale = value_property('max-stale', none='*', type='request')
+    min_fresh = value_property('min-fresh', type='request')
+    # no-transform shared (below)
+    only_if_cached = exists_property('only-if-cached', type='request')
+
+    # Response values:
+    public = exists_property('public', type='response')
+    private = value_property('private', none='*', type='response')
+    no_cache = value_property('no-cache', none='*')
+    no_store = exists_property('no-store')
+    no_transform = exists_property('no-transform')
+    must_revalidate = exists_property('must-revalidate', type='response')
+    proxy_revalidate = exists_property('proxy-revalidate', type='response')
+    max_age = value_property('max-age', none=-1)
+    s_maxage = value_property('s-maxage', type='response')
+    s_max_age = s_maxage
+
+    def __str__(self):
+        return serialize_cache_control(self.properties)
+
+    def copy(self):
+        """
+        Returns a copy of this object.
+        """
+        return self.__class__(self.properties.copy(), type=self.type)
+
+def serialize_cache_control(properties):
+    if isinstance(properties, CacheControl):
+        properties = properties.properties
+    parts = []
+    for name, value in sorted(properties.items()):
+        if value is None:
+            parts.append(name)
+            continue
+        value = str(value)
+        if need_quote_re.search(value):
+            value = '"%s"' % value
+        parts.append('%s=%s' % (name, value))
+    return ', '.join(parts)
+    
diff --git a/lib/webob/compat.py b/lib/webob/compat.py
@@ -0,0 +1,23 @@
+try:
+    # This will succeed on Python 2.4, and fail on Python 2.3.
+
+    [].sort(key=lambda: None)
+
+    def sorted(iterable, cmp=None, key=None, reverse=False):
+        l = list(iterable)
+        l.sort(cmp=cmp, key=key, reverse=reverse)
+        return l
+
+except TypeError:
+    # Implementation for Python 2.3.
+    
+    def sorted(iterable, key=None, reverse=False):
+        l = list(iterable)
+        if key:
+            l = [(key(i), i) for i in l]
+        l.sort()
+        if key:
+            l = [i[1] for i in l]
+        if reverse:
+            l.reverse()
+        return l
diff --git a/lib/webob/datastruct.py b/lib/webob/datastruct.py
@@ -0,0 +1,58 @@
+"""
+Contains some data structures.
+"""
+
+from webob.util.dictmixin import DictMixin
+
+class EnvironHeaders(DictMixin):
+    """An object that represents the headers as present in a
+    WSGI environment.
+
+    This object is a wrapper (with no internal state) for a WSGI
+    request object, representing the CGI-style HTTP_* keys as a
+    dictionary.  Because a CGI environment can only hold one value for
+    each key, this dictionary is single-valued (unlike outgoing
+    headers).
+    """
+
+    def __init__(self, environ):
+        self.environ = environ
+
+    def _trans_name(self, name):
+        key = 'HTTP_'+name.replace('-', '_').upper()
+        if key == 'HTTP_CONTENT_LENGTH':
+            key = 'CONTENT_LENGTH'
+        elif key == 'HTTP_CONTENT_TYPE':
+            key = 'CONTENT_TYPE'
+        return key
+
+    def _trans_key(self, key):
+        if key == 'CONTENT_TYPE':
+            return 'Content-Type'
+        elif key == 'CONTENT_LENGTH':
+            return 'Content-Length'
+        elif key.startswith('HTTP_'):
+            return key[5:].replace('_', '-').title()
+        else:
+            return None
+        
+    def __getitem__(self, item):
+        return self.environ[self._trans_name(item)]
+
+    def __setitem__(self, item, value):
+        self.environ[self._trans_name(item)] = value
+
+    def __delitem__(self, item):
+        del self.environ[self._trans_name(item)]
+
+    def __iter__(self):
+        for key in self.environ:
+            name = self._trans_key(key)
+            if name is not None:
+                yield name
+
+    def keys(self):
+        return list(iter(self))
+
+    def __contains__(self, item):
+        return self._trans_name(item) in self.environ
diff --git a/lib/webob/etag.py b/lib/webob/etag.py
@@ -0,0 +1,214 @@
+"""
+Does parsing of ETag-related headers: If-None-Matches, If-Matches
+
+Also If-Range parsing
+"""
+
+import webob
+
+__all__ = ['AnyETag', 'NoETag', 'ETagMatcher', 'IfRange', 'NoIfRange']
+
+class _AnyETag(object):
+    """
+    Represents an ETag of *, or a missing ETag when matching is 'safe'
+    """
+
+    def __repr__(self):
+        return '<ETag *>'
+
+    def __nonzero__(self):
+        return False
+
+    def __contains__(self, other):
+        return True
+
+    def weak_match(self, other):
+        return True
+
+    def __str__(self):
+        return '*'
+
+AnyETag = _AnyETag()
+
+class _NoETag(object):
+    """
+    Represents a missing ETag when matching is unsafe
+    """
+
+    def __repr__(self):
+        return '<No ETag>'
+
+    def __nonzero__(self):
+        return False
+
+    def __contains__(self, other):
+        return False
+
+    def weak_match(self, other):
+        return False
+
+    def __str__(self):
+        return ''
+
+NoETag = _NoETag()
+
+class ETagMatcher(object):
+
+    """
+    Represents an ETag request.  Supports containment to see if an
+    ETag matches.  You can also use
+    ``etag_matcher.weak_contains(etag)`` to allow weak ETags to match
+    (allowable for conditional GET requests, but not ranges or other
+    methods).
+    """
+
+    def __init__(self, etags, weak_etags=()):
+        self.etags = etags
+        self.weak_etags = weak_etags
+
+    def __contains__(self, other):
+        return other in self.etags
+
+    def weak_match(self, other):
+        if other.lower().startswith('w/'):
+            other = other[2:]
+        return other in self.etags or other in self.weak_etags
+
+    def __repr__(self):
+        return '<ETag %s>' % (
+            ' or '.join(self.etags))
+
+    def parse(cls, value):
+        """
+        Parse this from a header value
+        """
+        results = []
+        weak_results = []
+        while value:
+            if value.lower().startswith('w/'):
+                # Next item is weak
+                weak = True
+                value = value[2:]
+            else:
+                weak = False
+            if value.startswith('"'):
+                try:
+                    etag, rest = value[1:].split('"', 1)
+                except ValueError:
+                    etag = value.strip(' ",')
+                    rest = ''
+                else:
+                    rest = rest.strip(', ')
+            else:
+                if ',' in value:
+                    etag, rest = value.split(',', 1)
+                    rest = rest.strip()
+                else:
+                    etag = value
+                    rest = ''
+            if etag == '*':
+                return AnyETag
+            if etag:
+                if weak:
+                    weak_results.append(etag)
+                else:
+                    results.append(etag)
+            value = rest
+        return cls(results, weak_results)
+    parse = classmethod(parse)
+                    
+    def __str__(self):
+        # FIXME: should I quote these?
+        items = list(self.etags)
+        for weak in self.weak_etags:
+            items.append('W/%s' % weak)
+        return ', '.join(items)
+
+class IfRange(object):
+    """
+    Parses and represents the If-Range header, which can be
+    an ETag *or* a date
+    """
+    def __init__(self, etag=None, date=None):
+        self.etag = etag
+        self.date = date
+
+    def __repr__(self):
+        if self.etag is None:
+            etag = '*'
+        else:
+            etag = str(self.etag)
+        if self.date is None:
+            date = '*'
+        else:
+            date = webob._serialize_date(self.date)
+        return '<%s etag=%s, date=%s>' % (
+            self.__class__.__name__,
+            etag, date)
+
+    def __str__(self):
+        if self.etag is not None:
+            return str(self.etag)
+        elif self.date:
+            return webob._serialize_date(self.date)
+        else:
+            return ''
+
+    def match(self, etag=None, last_modified=None):
+        """
+        Return True if the If-Range header matches the given etag or last_modified
+        """
+        if self.date is not None:
+            if last_modified is None:
+                # Conditional with nothing to base the condition won't work
+                return False
+            return last_modified <= self.date
+        elif self.etag is not None:
+            if not etag:
+                return False
+            return etag in self.etag
+        return True
+
+    def match_response(self, response):
+        """
+        Return True if this matches the given ``webob.Response`` instance.
+        """
+        return self.match(etag=response.etag, last_modified=response.last_modified)
+
+    #@classmethod
+    def parse(cls, value):
+        """
+        Parse this from a header value.
+        """
+        date = etag = None
+        if not value:
+            etag = NoETag()
+        elif value and value.endswith(' GMT'):
+            # Must be a date
+            date = webob._parse_date(value)
+        else:
+            etag = ETagMatcher.parse(value)
+        return cls(etag=etag, date=date)
+    parse = classmethod(parse)
+
+class _NoIfRange(object):
+    """
+    Represents a missing If-Range header
+    """
+    
+    def __repr__(self):
+        return '<Empty If-Range>'
+
+    def __str__(self):
+        return ''
+
+    def __nonzero__(self):
+        return False
+
+    def match(self, etag=None, last_modified=None):
+        return True
+
+    def match_response(self, response):
+        return True
+
+NoIfRange = _NoIfRange()
diff --git a/lib/webob/exc.py b/lib/webob/exc.py
@@ -0,0 +1,660 @@
+"""
+HTTP Exception
+
+This module processes Python exceptions that relate to HTTP exceptions
+by defining a set of exceptions, all subclasses of HTTPException.
+Each exception, in addition to being a Python exception that can be
+raised and caught, is also a WSGI application and ``webob.Response``
+object.
+
+This module defines exceptions according to RFC 2068 [1]_ : codes with
+100-300 are not really errors; 400's are client errors, and 500's are
+server errors.  According to the WSGI specification [2]_ , the application
+can call ``start_response`` more then once only under two conditions:
+(a) the response has not yet been sent, or (b) if the second and
+subsequent invocations of ``start_response`` have a valid ``exc_info``
+argument obtained from ``sys.exc_info()``.  The WSGI specification then
+requires the server or gateway to handle the case where content has been
+sent and then an exception was encountered.
+
+Exception
+  HTTPException
+    HTTPOk
+      * 200 - HTTPOk
+      * 201 - HTTPCreated
+      * 202 - HTTPAccepted
+      * 203 - HTTPNonAuthoritativeInformation
+      * 204 - HTTPNoContent
+      * 205 - HTTPResetContent
+      * 206 - HTTPPartialContent
+    HTTPRedirection
+      * 300 - HTTPMultipleChoices
+      * 301 - HTTPMovedPermanently
+      * 302 - HTTPFound
+      * 303 - HTTPSeeOther
+      * 304 - HTTPNotModified
+      * 305 - HTTPUseProxy
+      * 306 - Unused (not implemented, obviously)
+      * 307 - HTTPTemporaryRedirect
+    HTTPError
+      HTTPClientError
+        * 400 - HTTPBadRequest
+        * 401 - HTTPUnauthorized
+        * 402 - HTTPPaymentRequired
+        * 403 - HTTPForbidden
+        * 404 - HTTPNotFound
+        * 405 - HTTPMethodNotAllowed
+        * 406 - HTTPNotAcceptable
+        * 407 - HTTPProxyAuthenticationRequired
+        * 408 - HTTPRequestTimeout
+        * 409 - HTTPConfict
+        * 410 - HTTPGone
+        * 411 - HTTPLengthRequired
+        * 412 - HTTPPreconditionFailed
+        * 413 - HTTPRequestEntityTooLarge
+        * 414 - HTTPRequestURITooLong
+        * 415 - HTTPUnsupportedMediaType
+        * 416 - HTTPRequestRangeNotSatisfiable
+        * 417 - HTTPExpectationFailed
+      HTTPServerError
+        * 500 - HTTPInternalServerError
+        * 501 - HTTPNotImplemented
+        * 502 - HTTPBadGateway
+        * 503 - HTTPServiceUnavailable
+        * 504 - HTTPGatewayTimeout
+        * 505 - HTTPVersionNotSupported
+
+References:
+
+.. [1] http://www.python.org/peps/pep-0333.html#error-handling
+.. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
+
+
+"""
+
+import re
+import urlparse
+import sys
+try:
+    from string import Template
+except ImportError:
+    from webob.util.stringtemplate import Template
+import types
+from webob import Response, Request, html_escape
+
+newstyle_exceptions = issubclass(Exception, object)
+
+tag_re = re.compile(r'<.*?>', re.S)
+br_re = re.compile(r'<br.*?>', re.I|re.S)
+comment_re = re.compile(r'<!--|-->')
+
+def no_escape(value):
+    if value is None:
+        return ''
+    if not isinstance(value, basestring):
+        if hasattr(value, '__unicode__'):
+            value = unicode(value)
+        else:
+            value = str(value)
+    return value
+
+def strip_tags(value):
+    value = value.replace('\n', ' ')
+    value = value.replace('\r', '')
+    value = br_re.sub('\n', value)
+    value = comment_re.sub('', value)
+    value = tag_re.sub('', value)
+    return value
+
+class HTTPException(Exception):
+    """
+    Exception used on pre-Python-2.5, where new-style classes cannot be used as
+    an exception.
+    """
+
+    def __init__(self, message, wsgi_response):
+        Exception.__init__(self, message)
+        self.__dict__['wsgi_response'] = wsgi_response
+
+    def __call__(self, environ, start_response):
+        return self.wsgi_response(environ, start_response)
+
+    def exception(self):
+        return self
+    
+    exception = property(exception)
+
+    # for old style exceptions
+    if not newstyle_exceptions:
+        def __getattr__(self, attr):
+            if not attr.startswith('_'):
+                return getattr(self.wsgi_response, attr)
+            else:
+                raise AttributeError(attr)
+
+        def __setattr__(self, attr, value):
+            if attr.startswith('_') or attr in ('args',):
+                self.__dict__[attr] = value
+            else:
+                setattr(self.wsgi_response, attr, value)
+
+class WSGIHTTPException(Response, HTTPException):
+
+    ## You should set in subclasses:
+    # code = 200
+    # title = 'OK'
+    # explanation = 'why this happens'
+    # body_template_obj = Template('response template')
+    code = None
+    title = None
+    explanation = ''
+    body_template_obj = Template('''\
+${explanation}<br /><br />
+${detail}
+${html_comment}
+''')
+
+    plain_template_obj = Template('''\
+${status}
+
+${body}''')
+
+    html_template_obj = Template('''\
+<html>
+ <head>
+  <title>${status}</title>
+ </head>
+ <body>
+  <h1>${status}</h1>
+  ${body}
+ </body>
+</html>''')
+
+    ## Set this to True for responses that should have no request body
+    empty_body = False
+
+    def __init__(self, detail=None, headers=None, comment=None,
+                 body_template=None):
+        Response.__init__(self,
+                          status='%s %s' % (self.code, self.title),
+                          content_type='text/html')
+        Exception.__init__(self, detail)
+        if headers:
+            self.headers.update(headers)
+        self.detail = detail
+        self.comment = comment
+        if body_template is not None:
+            self.body_template = body_template
+            self.body_template_obj = Template(body_template)
+        if self.empty_body:
+            del self.content_type
+            del self.content_length
+
+    def _make_body(self, environ, escape):
+        args = {
+            'explanation': escape(self.explanation),
+            'detail': escape(self.detail or ''),
+            'comment': escape(self.comment or ''),
+            }
+        if self.comment:
+            args['html_comment'] = '<!-- %s -->' % escape(self.comment)
+        else:
+            args['html_comment'] = ''
+        body_tmpl = self.body_template_obj
+        if WSGIHTTPException.body_template_obj is not self.body_template_obj:
+            # Custom template; add headers to args
+            for k, v in environ.items():
+                args[k] = escape(v)
+            for k, v in self.headers.items():
+                args[k.lower()] = escape(v)
+        t_obj = self.body_template_obj
+        return t_obj.substitute(args)
+
+    def plain_body(self, environ):
+        body = self._make_body(environ, no_escape)
+        body = strip_tags(body)
+        return self.plain_template_obj.substitute(status=self.status,
+                                                  title=self.title,
+                                                  body=body)
+
+    def html_body(self, environ):
+        body = self._make_body(environ, html_escape)
+        return self.html_template_obj.substitute(status=self.status,
+                                                 body=body)
+
+    def generate_response(self, environ, start_response):
+        if self.content_length is not None:
+            del self.content_length
+        headerlist = list(self.headerlist)
+        accept = environ.get('HTTP_ACCEPT', '')
+        if accept and 'html' in accept or '*/*' in accept:
+            body = self.html_body(environ)
+            if not self.content_type:
+                headerlist.append('text/html; charset=utf8')
+        else:
+            body = self.plain_body(environ)
+            if not self.content_type:
+                headerlist.append('text/plain; charset=utf8')
+        headerlist.append(('Content-Length', str(len(body))))
+        start_response(self.status, headerlist)
+        return [body]
+
+    def __call__(self, environ, start_response):
+        if environ['REQUEST_METHOD'] == 'HEAD':
+            start_response(self.status, self.headerlist)
+            return []
+        if not self.body and not self.empty_body:
+            return self.generate_response(environ, start_response)
+        return Response.__call__(self, environ, start_response)
+
+    def wsgi_response(self):
+        return self
+    
+    wsgi_response = property(wsgi_response)
+
+    def exception(self):
+        if newstyle_exceptions:
+            return self
+        else:
+            return HTTPException(self.detail, self)
+
+    exception = property(exception)
+
+class HTTPError(WSGIHTTPException):
+    """
+    base class for status codes in the 400's and 500's
+
+    This is an exception which indicates that an error has occurred,
+    and that any work in progress should not be committed.  These are
+    typically results in the 400's and 500's.
+    """
+
+class HTTPRedirection(WSGIHTTPException):
+    """
+    base class for 300's status code (redirections)
+
+    This is an abstract base class for 3xx redirection.  It indicates
+    that further action needs to be taken by the user agent in order
+    to fulfill the request.  It does not necessarly signal an error
+    condition.
+    """
+
+class HTTPOk(WSGIHTTPException):
+    """
+    Base class for the 200's status code (successful responses)
+    """
+    code = 200
+    title = 'OK'
+
+############################################################
+## 2xx success
+############################################################
+
+class HTTPCreated(HTTPOk):
+    code = 201
+    title = 'Created'
+
+class HTTPAccepted(HTTPOk):
+    code = 202
+    title = 'Accepted'
+    explanation = 'The request is accepted for processing.'
+
+class HTTPNonAuthoritativeInformation(HTTPOk):
+    code = 203
+    title = 'Non-Authoritative Information'
+
+class HTTPNoContent(HTTPOk):
+    code = 204
+    title = 'No Content'
+    empty_body = True
+
+class HTTPResetContent(HTTPOk):
+    code = 205
+    title = 'Reset Content'
+    empty_body = True
+
+class HTTPPartialContent(HTTPOk):
+    code = 206
+    title = 'Partial Content'
+
+## FIXME: add 207 Multi-Status (but it's complicated)
+
+############################################################
+## 3xx redirection
+############################################################
+
+class _HTTPMove(HTTPRedirection):
+    """
+    redirections which require a Location field
+
+    Since a 'Location' header is a required attribute of 301, 302, 303,
+    305 and 307 (but not 304), this base class provides the mechanics to
+    make this easy.
+
+    You can provide a location keyword argument to set the location
+    immediately.  You may also give ``add_slash=True`` if you want to
+    redirect to the same URL as the request, except with a ``/`` added
+    to the end.
+
+    Relative URLs in the location will be resolved to absolute.
+    """
+    explanation = 'The resource has been moved to'
+    body_template_obj = Template('''\
+${explanation} <a href="${location}">${location}</a>;
+you should be redirected automatically.
+${detail}
+${html_comment}''')
+
+    def __init__(self, detail=None, headers=None, comment=None,
+                 body_template=None, location=None, add_slash=False):
+        super(_HTTPMove, self).__init__(
+            detail=detail, headers=headers, comment=comment,
+            body_template=body_template)
+        if location is not None:
+            self.location = location
+            if add_slash:
+                raise TypeError(
+                    "You can only provide one of the arguments location and add_slash")
+        self.add_slash = add_slash
+
+    def __call__(self, environ, start_response):
+        req = Request(environ)
+        if self.add_slash:
+            url = req.path_url
+            url += '/'
+            if req.environ.get('QUERY_STRING'):
+                url += '?' + req.environ['QUERY_STRING']
+            self.location = url
+        self.location = urlparse.urljoin(req.path_url, self.location)
+        return super(_HTTPMove, self).__call__(
+            environ, start_response)
+    
+class HTTPMultipleChoices(_HTTPMove):
+    code = 300
+    title = 'Multiple Choices'
+
+class HTTPMovedPermanently(_HTTPMove):
+    code = 301
+    title = 'Moved Permanently'
+
+class HTTPFound(_HTTPMove):
+    code = 302
+    title = 'Found'
+    explanation = 'The resource was found at'
+
+# This one is safe after a POST (the redirected location will be
+# retrieved with GET):
+class HTTPSeeOther(_HTTPMove):
+    code = 303
+    title = 'See Other'
+
+class HTTPNotModified(HTTPRedirection):
+    # FIXME: this should include a date or etag header
+    code = 304
+    title = 'Not Modified'
+    empty_body = True
+
+class HTTPUseProxy(_HTTPMove):
+    # Not a move, but looks a little like one
+    code = 305
+    title = 'Use Proxy'
+    explanation = (
+        'The resource must be accessed through a proxy located at')
+
+class HTTPTemporaryRedirect(_HTTPMove):
+    code = 307
+    title = 'Temporary Redirect'
+
+############################################################
+## 4xx client error
+############################################################
+
+class HTTPClientError(HTTPError):
+    """
+    base class for the 400's, where the client is in error
+
+    This is an error condition in which the client is presumed to be
+    in-error.  This is an expected problem, and thus is not considered
+    a bug.  A server-side traceback is not warranted.  Unless specialized,
+    this is a '400 Bad Request'
+    """
+    code = 400
+    title = 'Bad Request'
+    explanation = ('The server could not comply with the request since\r\n'
+                   'it is either malformed or otherwise incorrect.\r\n')
+
+class HTTPBadRequest(HTTPClientError):
+    pass
+
+class HTTPUnauthorized(HTTPClientError):
+    code = 401
+    title = 'Unauthorized'
+    explanation = (
+        'This server could not verify that you are authorized to\r\n'
+        'access the document you requested.  Either you supplied the\r\n'
+        'wrong credentials (e.g., bad password), or your browser\r\n'
+        'does not understand how to supply the credentials required.\r\n')
+
+class HTTPPaymentRequired(HTTPClientError):
+    code = 402
+    title = 'Payment Required'
+    explanation = ('Access was denied for financial reasons.')
+
+class HTTPForbidden(HTTPClientError):
+    code = 403
+    title = 'Forbidden'
+    explanation = ('Access was denied to this resource.')
+
+class HTTPNotFound(HTTPClientError):
+    code = 404
+    title = 'Not Found'
+    explanation = ('The resource could not be found.')
+
+class HTTPMethodNotAllowed(HTTPClientError):
+    code = 405
+    title = 'Method Not Allowed'
+    # override template since we need an environment variable
+    body_template_obj = Template('''\
+The method ${REQUEST_METHOD} is not allowed for this resource. <br /><br />
+${detail}''')
+
+class HTTPNotAcceptable(HTTPClientError):
+    code = 406
+    title = 'Not Acceptable'
+    # override template since we need an environment variable
+    template = Template('''\
+The resource could not be generated that was acceptable to your browser
+(content of type ${HTTP_ACCEPT}. <br /><br />
+${detail}''')
+
+class HTTPProxyAuthenticationRequired(HTTPClientError):
+    code = 407
+    title = 'Proxy Authentication Required'
+    explanation = ('Authentication with a local proxy is needed.')
+
+class HTTPRequestTimeout(HTTPClientError):
+    code = 408
+    title = 'Request Timeout'
+    explanation = ('The server has waited too long for the request to '
+                   'be sent by the client.')
+
+class HTTPConflict(HTTPClientError):
+    code = 409
+    title = 'Conflict'
+    explanation = ('There was a conflict when trying to complete '
+                   'your request.')
+
+class HTTPGone(HTTPClientError):
+    code = 410
+    title = 'Gone'
+    explanation = ('This resource is no longer available.  No forwarding '
+                   'address is given.')
+
+class HTTPLengthRequired(HTTPClientError):
+    code = 411
+    title = 'Length Required'
+    explanation = ('Content-Length header required.')
+
+class HTTPPreconditionFailed(HTTPClientError):
+    code = 412
+    title = 'Precondition Failed'
+    explanation = ('Request precondition failed.')
+
+class HTTPRequestEntityTooLarge(HTTPClientError):
+    code = 413
+    title = 'Request Entity Too Large'
+    explanation = ('The body of your request was too large for this server.')
+
+class HTTPRequestURITooLong(HTTPClientError):
+    code = 414
+    title = 'Request-URI Too Long'
+    explanation = ('The request URI was too long for this server.')
+
+class HTTPUnsupportedMediaType(HTTPClientError):
+    code = 415
+    title = 'Unsupported Media Type'
+    # override template since we need an environment variable
+    template_obj = Template('''\
+The request media type ${CONTENT_TYPE} is not supported by this server.
+<br /><br />
+${detail}''')
+
+class HTTPRequestRangeNotSatisfiable(HTTPClientError):
+    code = 416
+    title = 'Request Range Not Satisfiable'
+    explanation = ('The Range requested is not available.')
+
+class HTTPExpectationFailed(HTTPClientError):
+    code = 417
+    title = 'Expectation Failed'
+    explanation = ('Expectation failed.')
+
+class HTTPUnprocessableEntity(HTTPClientError):
+    ## Note: from WebDAV
+    code = 422
+    title = 'Unprocessable Entity'
+    explanation = 'Unable to process the contained instructions'
+
+class HTTPLocked(HTTPClientError):
+    ## Note: from WebDAV
+    code = 423
+    title = 'Locked'
+    explanation = ('The resource is locked')
+
+class HTTPFailedDependency(HTTPClientError):
+    ## Note: from WebDAV
+    code = 424
+    title = 'Failed Dependency'
+    explanation = ('The method could not be performed because the requested '
+                   'action dependended on another action and that action failed')
+
+############################################################
+## 5xx Server Error
+############################################################
+#  Response status codes beginning with the digit "5" indicate cases in
+#  which the server is aware that it has erred or is incapable of
+#  performing the request. Except when responding to a HEAD request, the
+#  server SHOULD include an entity containing an explanation of the error
+#  situation, and whether it is a temporary or permanent condition. User
+#  agents SHOULD display any included entity to the user. These response
+#  codes are applicable to any request method.
+
+class HTTPServerError(HTTPError):
+    """
+    base class for the 500's, where the server is in-error
+
+    This is an error condition in which the server is presumed to be
+    in-error.  This is usually unexpected, and thus requires a traceback;
+    ideally, opening a support ticket for the customer. Unless specialized,
+    this is a '500 Internal Server Error'
+    """
+    code = 500
+    title = 'Internal Server Error'
+    explanation = (
+      'The server has either erred or is incapable of performing\r\n'
+      'the requested operation.\r\n')
+
+class HTTPInternalServerError(HTTPServerError):
+    pass
+
+class HTTPNotImplemented(HTTPServerError):
+    code = 501
+    title = 'Not Implemented'
+    template = Template('''
+The request method ${REQUEST_METHOD} is not implemented for this server. <br /><br />
+${detail}''')
+
+class HTTPBadGateway(HTTPServerError):
+    code = 502
+    title = 'Bad Gateway'
+    explanation = ('Bad gateway.')
+
+class HTTPServiceUnavailable(HTTPServerError):
+    code = 503
+    title = 'Service Unavailable'
+    explanation = ('The server is currently unavailable. '
+                   'Please try again at a later time.')
+
+class HTTPGatewayTimeout(HTTPServerError):
+    code = 504
+    title = 'Gateway Timeout'
+    explanation = ('The gateway has timed out.')
+
+class HTTPVersionNotSupported(HTTPServerError):
+    code = 505
+    title = 'HTTP Version Not Supported'
+    explanation = ('The HTTP version is not supported.')
+
+class HTTPInsufficientStorage(HTTPServerError):
+    code = 507
+    title = 'Insufficient Storage'
+    explanation = ('There was not enough space to save the resource')
+
+class HTTPExceptionMiddleware(object):
+    """
+    Middleware that catches exceptions in the sub-application.  This
+    does not catch exceptions in the app_iter; only during the initial
+    calling of the application.
+
+    This should be put *very close* to applications that might raise
+    these exceptions.  This should not be applied globally; letting
+    *expected* exceptions raise through the WSGI stack is dangerous.
+    """
+
+    def __init__(self, application):
+        self.application = application
+    def __call__(self, environ, start_response):
+        try:
+            return self.application(environ, start_response)
+        except HTTPException, exc:
+            parent_exc_info = sys.exc_info()
+            def repl_start_response(status, headers, exc_info=None):
+                if exc_info is None:
+                    exc_info = parent_exc_info
+                return start_response(status, headers, exc_info)
+            return exc(environ, repl_start_response)
+
+try:
+    from paste import httpexceptions
+except ImportError:
+    # Without Paste we don't need to do this fixup
+    pass
+else:
+    for name in dir(httpexceptions):
+        obj = globals().get(name)
+        if (obj and isinstance(obj, type) and issubclass(obj, HTTPException)
+            and obj is not HTTPException
+            and obj is not WSGIHTTPException):
+            obj.__bases__ = obj.__bases__ + (getattr(httpexceptions, name),)
+    del name, obj, httpexceptions
+
+__all__ = ['HTTPExceptionMiddleware', 'status_map']
+status_map={}
+for name, value in globals().items():
+    if (isinstance(value, (type, types.ClassType)) and issubclass(value, HTTPException)
+        and not name.startswith('_')):
+        __all__.append(name)
+        if getattr(value, 'code', None):
+            status_map[value.code]=value
+del name, value
+
diff --git a/lib/webob/headerdict.py b/lib/webob/headerdict.py
@@ -0,0 +1,110 @@
+"""
+Represents the response header list as a dictionary-like object.
+"""
+
+from webob.multidict import MultiDict
+try:
+    reversed
+except NameError:
+    from webob.util.reversed import reversed
+
+class HeaderDict(MultiDict):
+
+    """
+    Like a MultiDict, this wraps a list.  Keys are normalized
+    for case and whitespace.
+    """
+
+    def normalize(self, key):
+        return str(key).lower().strip()
+
+    def __getitem__(self, key):
+        normalize = self.normalize
+        key = normalize(key)
+        for k, v in reversed(self._items):
+            if normalize(k) == key:
+                return v
+        raise KeyError(key)
+
+    def getall(self, key):
+        normalize = self.normalize
+        key = normalize(key)
+        result = []
+        for k, v in self._items:
+            if normalize(k) == key:
+                result.append(v)
+        return result
+
+    def mixed(self):
+        result = {}
+        multi = {}
+        normalize = self.normalize
+        for key, value in self.iteritems():
+            key = normalize(key)
+            if key in result:
+                if key in multi:
+                    result[key].append(value)
+                else:
+                    result[key] = [result[key], value]
+                    multi[key] = None
+            else:
+                result[key] = value
+        return result
+
+    def dict_of_lists(self):
+        result = {}
+        normalize = self.normalize
+        for key, value in self.iteritems():
+            key = normalize(key)
+            if key in result:
+                result[key].append(value)
+            else:
+                result[key] = [value]
+        return result
+
+    def __delitem__(self, key):
+        normalize = self.normalize
+        key = normalize(key)
+        items = self._items
+        found = False
+        for i in range(len(items)-1, -1, -1):
+            if normalize(items[i][0]) == key:
+                del items[i]
+                found = True
+        if not found:
+            raise KeyError(key)
+
+    def __contains__(self, key):
+        normalize = self.normalize
+        key = normalize(key)
+        for k, v in self._items:
+            if normalize(k) == key:
+                return True
+        return False
+
+    has_key = __contains__
+
+    def setdefault(self, key, default=None):
+        normalize = self.normalize
+        c_key = normalize(key)
+        for k, v in self._items:
+            if normalize(k) == c_key:
+                return v
+        self._items.append((key, default))
+        return default
+
+    def pop(self, key, *args):
+        if len(args) > 1:
+            raise TypeError, "pop expected at most 2 arguments, got "\
+                              + repr(1 + len(args))
+        key = self.normalize(key)
+        for i in range(len(self._items)):
+            if self.normalize(self._items[i][0]) == key:
+                v = self._items[i][1]
+                del self._items[i]
+                return v
+        if args:
+            return args[0]
+        else:
+            raise KeyError(key)
+
diff --git a/lib/webob/multidict.py b/lib/webob/multidict.py
@@ -0,0 +1,606 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+"""
+Gives a multi-value dictionary object (MultiDict) plus several wrappers
+"""
+import cgi
+import copy
+import sys
+from webob.util.dictmixin import DictMixin
+try:
+    reversed
+except NameError:
+    from webob.util.reversed import reversed
+
+__all__ = ['MultiDict', 'UnicodeMultiDict', 'NestedMultiDict', 'NoVars']
+
+class MultiDict(DictMixin):
+
+    """
+    An ordered dictionary that can have multiple values for each key.
+    Adds the methods getall, getone, mixed, and add to the normal
+    dictionary interface.
+    """
+
+    def __init__(self, *args, **kw):
+        if len(args) > 1:
+            raise TypeError(
+                "MultiDict can only be called with one positional argument")
+        if args:
+            if hasattr(args[0], 'iteritems'):
+                items = list(args[0].iteritems())
+            elif hasattr(args[0], 'items'):
+                items = args[0].items()
+            else:
+                items = list(args[0])
+            self._items = items
+        else:
+            self._items = []
+        self._items.extend(kw.iteritems())
+
+    #@classmethod
+    def view_list(cls, lst):
+        """
+        Create a dict that is a view on the given list
+        """
+        if not isinstance(lst, list):
+            raise TypeError(
+                "%s.view_list(obj) takes only actual list objects, not %r"
+                % (cls.__name__, lst))
+        obj = cls()
+        obj._items = lst
+        return obj
+
+    view_list = classmethod(view_list)
+
+    #@classmethod
+    def from_fieldstorage(cls, fs):
+        """
+        Create a dict from a cgi.FieldStorage instance
+        """
+        obj = cls()
+        if fs.list:
+            # fs.list can be None when there's nothing to parse
+            for field in fs.list:
+                if field.filename:
+                    obj.add(field.name, field)
+                else:
+                    obj.add(field.name, field.value)
+        return obj
+
+    from_fieldstorage = classmethod(from_fieldstorage)
+
+    def __getitem__(self, key):
+        for k, v in reversed(self._items):
+            if k == key:
+                return v
+        raise KeyError(key)
+
+    def __setitem__(self, key, value):
+        try:
+            del self[key]
+        except KeyError:
+            pass
+        self._items.append((key, value))
+
+    def add(self, key, value):
+        """
+        Add the key and value, not overwriting any previous value.
+        """
+        self._items.append((key, value))
+
+    def getall(self, key):
+        """
+        Return a list of all values matching the key (may be an empty list)
+        """
+        result = []
+        for k, v in self._items:
+            if key == k:
+                result.append(v)
+        return result
+
+    def getone(self, key):
+        """
+        Get one value matching the key, raising a KeyError if multiple
+        values were found.
+        """
+        v = self.getall(key)
+        if not v:
+            raise KeyError('Key not found: %r' % key)
+        if len(v) > 1:
+            raise KeyError('Multiple values match %r: %r' % (key, v))
+        return v[0]
+
+    def mixed(self):
+        """
+        Returns a dictionary where the values are either single
+        values, or a list of values when a key/value appears more than
+        once in this dictionary.  This is similar to the kind of
+        dictionary often used to represent the variables in a web
+        request.
+        """
+        result = {}
+        multi = {}
+        for key, value in self.iteritems():
+            if key in result:
+                # We do this to not clobber any lists that are
+                # *actual* values in this dictionary:
+                if key in multi:
+                    result[key].append(value)
+                else:
+                    result[key] = [result[key], value]
+                    multi[key] = None
+            else:
+                result[key] = value
+        return result
+
+    def dict_of_lists(self):
+        """
+        Returns a dictionary where each key is associated with a
+        list of values.
+        """
+        result = {}
+        for key, value in self.iteritems():
+            if key in result:
+                result[key].append(value)
+            else:
+                result[key] = [value]
+        return result
+
+    def __delitem__(self, key):
+        items = self._items
+        found = False
+        for i in range(len(items)-1, -1, -1):
+            if items[i][0] == key:
+                del items[i]
+                found = True
+        if not found:
+            raise KeyError(key)
+
+    def __contains__(self, key):
+        for k, v in self._items:
+            if k == key:
+                return True
+        return False
+
+    has_key = __contains__
+
+    def clear(self):
+        self._items = []
+
+    def copy(self):
+        return self.__class__(self)
+
+    def setdefault(self, key, default=None):
+        for k, v in self._items:
+            if key == k:
+                return v
+        self._items.append((key, default))
+        return default
+
+    def pop(self, key, *args):
+        if len(args) > 1:
+            raise TypeError, "pop expected at most 2 arguments, got "\
+                              + repr(1 + len(args))
+        for i in range(len(self._items)):
+            if self._items[i][0] == key:
+                v = self._items[i][1]
+                del self._items[i]
+                return v
+        if args:
+            return args[0]
+        else:
+            raise KeyError(key)
+
+    def popitem(self):
+        return self._items.pop()
+
+    def update(self, other=None, **kwargs):
+        if other is None:
+            pass
+        elif hasattr(other, 'items'):
+            self._items.extend(other.items())
+        elif hasattr(other, 'keys'):
+            for k in other.keys():
+                self._items.append((k, other[k]))
+        else:
+            for k, v in other:
+                self._items.append((k, v))
+        if kwargs:
+            self.update(kwargs)
+
+    def __repr__(self):
+        items = ', '.join(['(%r, %r)' % v for v in self.iteritems()])
+        return '%s([%s])' % (self.__class__.__name__, items)
+
+    def __len__(self):
+        return len(self._items)
+
+    ##
+    ## All the iteration:
+    ##
+
+    def keys(self):
+        return [k for k, v in self._items]
+
+    def iterkeys(self):
+        for k, v in self._items:
+            yield k
+
+    __iter__ = iterkeys
+
+    def items(self):
+        return self._items[:]
+
+    def iteritems(self):
+        return iter(self._items)
+
+    def values(self):
+        return [v for k, v in self._items]
+
+    def itervalues(self):
+        for k, v in self._items:
+            yield v
+
+class UnicodeMultiDict(DictMixin):
+    """
+    A MultiDict wrapper that decodes returned values to unicode on the
+    fly. Decoding is not applied to assigned values.
+
+    The key/value contents are assumed to be ``str``/``strs`` or
+    ``str``/``FieldStorages`` (as is returned by the ``paste.request.parse_``
+    functions).
+
+    Can optionally also decode keys when the ``decode_keys`` argument is
+    True.
+
+    ``FieldStorage`` instances are cloned, and the clone's ``filename``
+    variable is decoded. Its ``name`` variable is decoded when ``decode_keys``
+    is enabled.
+
+    """
+    def __init__(self, multi=None, encoding=None, errors='strict',
+                 decode_keys=False):
+        self.multi = multi
+        if encoding is None:
+            encoding = sys.getdefaultencoding()
+        self.encoding = encoding
+        self.errors = errors
+        self.decode_keys = decode_keys
+
+    def _decode_key(self, key):
+        if self.decode_keys:
+            try:
+                key = key.decode(self.encoding, self.errors)
+            except AttributeError:
+                pass
+        return key
+
+    def _encode_key(self, key):
+        if self.decode_keys and isinstance(key, unicode):
+            return key.encode(self.encoding, self.errors)
+        return key
+
+    def _decode_value(self, value):
+        """
+        Decode the specified value to unicode. Assumes value is a ``str`` or
+        `FieldStorage`` object.
+
+        ``FieldStorage`` objects are specially handled.
+        """
+        if isinstance(value, cgi.FieldStorage):
+            # decode FieldStorage's field name and filename
+            value = copy.copy(value)
+            if self.decode_keys:
+                value.name = value.name.decode(self.encoding, self.errors)
+            if value.filename:
+                value.filename = value.filename.decode(self.encoding,
+                                                       self.errors)
+        elif not isinstance(value, unicode):
+            try:
+                value = value.decode(self.encoding, self.errors)
+            except AttributeError:
+                pass
+        return value
+
+    def _encode_value(self, value):
+        # FIXME: should this do the FieldStorage stuff too?
+        if isinstance(value, unicode):
+            value = value.encode(self.encoding, self.errors)
+        return value
+
+    def __getitem__(self, key):
+        return self._decode_value(self.multi.__getitem__(self._encode_key(key)))
+
+    def __setitem__(self, key, value):
+        self.multi.__setitem__(self._encode_key(key), self._encode_value(value))
+
+    def add(self, key, value):
+        """
+        Add the key and value, not overwriting any previous value.
+        """
+        self.multi.add(self._encode_key(key), self._encode_value(value))
+
+    def getall(self, key):
+        """
+        Return a list of all values matching the key (may be an empty list)
+        """
+        return [self._decode_value(v) for v in self.multi.getall(self._encode_key(key))]
+
+    def getone(self, key):
+        """
+        Get one value matching the key, raising a KeyError if multiple
+        values were found.
+        """
+        return self._decode_value(self.multi.getone(self._encode_key(key)))
+
+    def mixed(self):
+        """
+        Returns a dictionary where the values are either single
+        values, or a list of values when a key/value appears more than
+        once in this dictionary.  This is similar to the kind of
+        dictionary often used to represent the variables in a web
+        request.
+        """
+        unicode_mixed = {}
+        for key, value in self.multi.mixed().iteritems():
+            if isinstance(value, list):
+                value = [self._decode_value(value) for value in value]
+            else:
+                value = self._decode_value(value)
+            unicode_mixed[self._decode_key(key)] = value
+        return unicode_mixed
+
+    def dict_of_lists(self):
+        """
+        Returns a dictionary where each key is associated with a
+        list of values.
+        """
+        unicode_dict = {}
+        for key, value in self.multi.dict_of_lists().iteritems():
+            value = [self._decode_value(value) for value in value]
+            unicode_dict[self._decode_key(key)] = value
+        return unicode_dict
+
+    def __delitem__(self, key):
+        self.multi.__delitem__(self._encode_key(key))
+
+    def __contains__(self, key):
+        return self.multi.__contains__(self._encode_key(key))
+
+    has_key = __contains__
+
+    def clear(self):
+        self.multi.clear()
+
+    def copy(self):
+        return UnicodeMultiDict(self.multi.copy(), self.encoding, self.errors)
+
+    def setdefault(self, key, default=None):
+        return self._decode_value(self.multi.setdefault(self._encode_key(key), self._encode_value(default)))
+
+    def pop(self, key, *args):
+        return self._decode_value(self.multi.pop(self._encode_key(key), *args))
+
+    def popitem(self):
+        k, v = self.multi.popitem()
+        return (self._decode_key(k), self._decode_value(v))
+
+    def __repr__(self):
+        items = ', '.join(['(%r, %r)' % v for v in self.items()])
+        return '%s([%s])' % (self.__class__.__name__, items)
+
+    def __len__(self):
+        return self.multi.__len__()
+
+    ##
+    ## All the iteration:
+    ##
+
+    def keys(self):
+        return [self._decode_key(k) for k in self.multi.iterkeys()]
+
+    def iterkeys(self):
+        for k in self.multi.iterkeys():
+            yield self._decode_key(k)
+
+    __iter__ = iterkeys
+
+    def items(self):
+        return [(self._decode_key(k), self._decode_value(v))
+                for k, v in self.multi.iteritems()]
+
+    def iteritems(self):
+        for k, v in self.multi.iteritems():
+            yield (self._decode_key(k), self._decode_value(v))
+
+    def values(self):
+        return [self._decode_value(v) for v in self.multi.itervalues()]
+
+    def itervalues(self):
+        for v in self.multi.itervalues():
+            yield self._decode_value(v)
+
+_dummy = object()
+
+class NestedMultiDict(MultiDict):
+    """
+    Wraps several MultiDict objects, treating it as one large MultiDict
+    """
+    
+    def __init__(self, *dicts):
+        self.dicts = dicts
+
+    def __getitem__(self, key):
+        for d in self.dicts:
+            value = d.get(key, _dummy)
+            if value is not _dummy:
+                return value
+        raise KeyError(key)
+
+    def _readonly(self, *args, **kw):
+        raise KeyError("NestedMultiDict objects are read-only")
+    __setitem__ = _readonly
+    add = _readonly
+    __delitem__ = _readonly
+    clear = _readonly
+    setdefault = _readonly
+    pop = _readonly
+    popitem = _readonly
+    update = _readonly
+
+    def getall(self, key):
+        result = []
+        for d in self.dicts:
+            result.extend(d.getall(key))
+        return result
+
+    # Inherited:
+    # getone
+    # mixed
+    # dict_of_lists
+
+    def copy(self):
+        return MultiDict(self)
+
+    def __contains__(self, key):
+        for d in self.dicts:
+            if key in d:
+                return True
+        return False
+
+    has_key = __contains__
+
+    def __len__(self):
+        v = 0
+        for d in self.dicts:
+            v += len(d)
+        return v
+
+    def __nonzero__(self):
+        for d in self.dicts:
+            if d:
+                return True
+        return False
+
+    def items(self):
+        return list(self.iteritems())
+
+    def iteritems(self):
+        for d in self.dicts:
+            for item in d.iteritems():
+                yield item
+
+    def values(self):
+        return list(self.itervalues())
+
+    def itervalues(self):
+        for d in self.dicts:
+            for value in d.itervalues():
+                yield value
+
+    def keys(self):
+        return list(self.iterkeys())
+
+    def __iter__(self):
+        for d in self.dicts:
+            for key in d:
+                yield key
+
+    iterkeys = __iter__
+    
+class NoVars(object):
+    """
+    Represents no variables; used when no variables
+    are applicable.
+
+    This is read-only
+    """
+    
+    def __init__(self, reason=None):
+        self.reason = reason or 'N/A'
+
+    def __getitem__(self, key):
+        raise KeyError("No key %r: %s" % (key, self.reason))
+
+    def __setitem__(self, *args, **kw):
+        raise KeyError("Cannot add variables: %s" % self.reason)
+
+    add = __setitem__
+    setdefault = __setitem__
+    update = __setitem__
+
+    def __delitem__(self, *args, **kw):
+        raise KeyError("No keys to delete: %s" % self.reason)
+    clear = __delitem__
+    pop = __delitem__
+    popitem = __delitem__
+
+    def get(self, key, default=None):
+        return default
+
+    def getall(self, key):
+        return []
+
+    def getone(self, key):
+        return self[key]
+
+    def mixed(self):
+        return {}
+    dict_of_lists = mixed
+
+    def __contains__(self, key):
+        return False
+    has_key = __contains__
+
+    def copy(self):
+        return self
+
+    def __repr__(self):
+        return '<%s: %s>' % (self.__class__.__name__,
+                             self.reason)
+
+    def __len__(self):
+        return 0
+
+    def __cmp__(self, other):
+        return cmp({}, other)
+
+    def keys(self):
+        return []
+    def iterkeys(self):
+        return iter([])
+    __iter__ = iterkeys
+    items = keys
+    iteritems = iterkeys
+    values = keys
+    itervalues = iterkeys
+
+__test__ = {
+    'general': """
+    >>> d = MultiDict(a=1, b=2)
+    >>> d['a']
+    1
+    >>> d.getall('c')
+    []
+    >>> d.add('a', 2)
+    >>> d['a']
+    2
+    >>> d.getall('a')
+    [1, 2]
+    >>> d['b'] = 4
+    >>> d.getall('b')
+    [4]
+    >>> d.keys()
+    ['a', 'a', 'b']
+    >>> d.items()
+    [('a', 1), ('a', 2), ('b', 4)]
+    >>> d.mixed() == {'a': [1, 2], 'b': 4}
+    True
+    >>> MultiDict([('a', 'b')], c=2)
+    MultiDict([('a', 'b'), ('c', 2)])
+    """}
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
diff --git a/lib/webob/statusreasons.py b/lib/webob/statusreasons.py
@@ -0,0 +1,67 @@
+"""
+Gives ``status_reasons``, a dictionary of HTTP reasons for integer status codes
+"""
+
+__all__ = ['status_reasons']
+
+status_reasons = {
+    # Status Codes
+    # Informational
+    100: 'Continue',
+    101: 'Switching Protocols',
+    102: 'Processing',
+
+    # Successful
+    200: 'OK',
+    201: 'Created',
+    202: 'Accepted',
+    203: 'Non Authoritative Information',
+    204: 'No Content',
+    205: 'Reset Content',
+    206: 'Partial Content',
+    207: 'Multi Status',
+    226: 'IM Used',
+
+    # Redirection
+    300: 'Multiple Choices',
+    301: 'Moved Permanently',
+    302: 'Found',
+    303: 'See Other',
+    304: 'Not Modified',
+    305: 'Use Proxy',
+    307: 'Temporary Redirect',
+
+    # Client Error
+    400: 'Bad Request',
+    401: 'Unauthorized',
+    402: 'Payment Required',
+    403: 'Forbidden',
+    404: 'Not Found',
+    405: 'Method Not Allowed',
+    406: 'Not Acceptable',
+    407: 'Proxy Authentication Required',
+    408: 'Request Timeout',
+    409: 'Conflict',
+    410: 'Gone',
+    411: 'Length Required',
+    412: 'Precondition Failed',
+    413: 'Request Entity Too Large',
+    414: 'Request URI Too Long',
+    415: 'Unsupported Media Type',
+    416: 'Requested Range Not Satisfiable',
+    417: 'Expectation Failed',
+    422: 'Unprocessable Entity',
+    423: 'Locked',
+    424: 'Failed Dependency',
+    426: 'Upgrade Required',
+
+    # Server Error
+    500: 'Internal Server Error',
+    501: 'Not Implemented',
+    502: 'Bad Gateway',
+    503: 'Service Unavailable',
+    504: 'Gateway Timeout',
+    505: 'HTTP Version Not Supported',
+    507: 'Insufficient Storage',
+    510: 'Not Extended',
+    }
diff --git a/lib/webob/updatedict.py b/lib/webob/updatedict.py
@@ -0,0 +1,41 @@
+"""
+Dict that has a callback on all updates
+"""
+
+class UpdateDict(dict):
+    updated = None
+    updated_args = None
+    def _updated(self):
+        """
+        Assign to new_dict.updated to track updates
+        """
+        updated = self.updated
+        if updated is not None:
+            args = self.updated_args
+            if args is None:
+                args = (self,)
+            updated(*args)
+    def __setitem__(self, key, item):
+        dict.__setitem__(self, key, item)
+        self._updated()
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._updated()
+    def clear(self):
+        dict.clear(self)
+        self._updated()
+    def update(self, *args, **kw):
+        dict.update(self, *args, **kw)
+        self._updated()
+    def setdefault(self, key, failobj=None):
+        dict.setdefault(self, key, failobj)
+        self._updated()
+    def pop(self):
+        v = dict.pop(self)
+        self._updated()
+        return v
+    def popitem(self):
+        v = dict.popitem(self)
+        self._updated()
+        return v
+
diff --git a/lib/webob/util/__init__.py b/lib/webob/util/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/lib/webob/util/dictmixin.py b/lib/webob/util/dictmixin.py
@@ -0,0 +1,102 @@
+"""
+A backport of UserDict.DictMixin for pre-python-2.4
+"""
+__all__ = ['DictMixin']
+
+try:
+    from UserDict import DictMixin
+except ImportError:
+    class DictMixin:
+        # Mixin defining all dictionary methods for classes that already have
+        # a minimum dictionary interface including getitem, setitem, delitem,
+        # and keys. Without knowledge of the subclass constructor, the mixin
+        # does not define __init__() or copy().  In addition to the four base
+        # methods, progressively more efficiency comes with defining
+        # __contains__(), __iter__(), and iteritems().
+
+        # second level definitions support higher levels
+        def __iter__(self):
+            for k in self.keys():
+                yield k
+        def has_key(self, key):
+            try:
+                value = self[key]
+            except KeyError:
+                return False
+            return True
+        def __contains__(self, key):
+            return self.has_key(key)
+
+        # third level takes advantage of second level definitions
+        def iteritems(self):
+            for k in self:
+                yield (k, self[k])
+        def iterkeys(self):
+            return self.__iter__()
+
+        # fourth level uses definitions from lower levels
+        def itervalues(self):
+            for _, v in self.iteritems():
+                yield v
+        def values(self):
+            return [v for _, v in self.iteritems()]
+        def items(self):
+            return list(self.iteritems())
+        def clear(self):
+            for key in self.keys():
+                del self[key]
+        def setdefault(self, key, default=None):
+            try:
+                return self[key]
+            except KeyError:
+                self[key] = default
+            return default
+        def pop(self, key, *args):
+            if len(args) > 1:
+                raise TypeError, "pop expected at most 2 arguments, got "\
+                                  + repr(1 + len(args))
+            try:
+                value = self[key]
+            except KeyError:
+                if args:
+                    return args[0]
+                raise
+            del self[key]
+            return value
+        def popitem(self):
+            try:
+                k, v = self.iteritems().next()
+            except StopIteration:
+                raise KeyError, 'container is empty'
+            del self[k]
+            return (k, v)
+        def update(self, other=None, **kwargs):
+            # Make progressively weaker assumptions about "other"
+            if other is None:
+                pass
+            elif hasattr(other, 'iteritems'):  # iteritems saves memory and lookups
+                for k, v in other.iteritems():
+                    self[k] = v
+            elif hasattr(other, 'keys'):
+                for k in other.keys():
+                    self[k] = other[k]
+            else:
+                for k, v in other:
+                    self[k] = v
+            if kwargs:
+                self.update(kwargs)
+        def get(self, key, default=None):
+            try:
+                return self[key]
+            except KeyError:
+                return default
+        def __repr__(self):
+            return repr(dict(self.iteritems()))
+        def __cmp__(self, other):
+            if other is None:
+                return 1
+            if isinstance(other, DictMixin):
+                other = dict(other.iteritems())
+            return cmp(dict(self.iteritems()), other)
+        def __len__(self):
+            return len(self.keys())
diff --git a/lib/webob/util/reversed.py b/lib/webob/util/reversed.py
@@ -0,0 +1,4 @@
+## Backport of reversed
+
+def reversed(seq):
+    return iter(list(seq)[::-1])
diff --git a/lib/webob/util/safegzip.py b/lib/webob/util/safegzip.py
@@ -0,0 +1,21 @@
+"""
+GZip that doesn't include the timestamp
+"""
+import gzip
+
+class GzipFile(gzip.GzipFile):
+
+    def _write_gzip_header(self):
+        self.fileobj.write('\037\213')             # magic header
+        self.fileobj.write('\010')                 # compression method
+        fname = self.filename[:-3]
+        flags = 0
+        if fname:
+            flags = gzip.FNAME
+        self.fileobj.write(chr(flags))
+        ## This is what WebOb patches:
+        gzip.write32u(self.fileobj, long(0))
+        self.fileobj.write('\002')
+        self.fileobj.write('\377')
+        if fname:
+            self.fileobj.write(fname + '\000')
diff --git a/lib/webob/util/stringtemplate.py b/lib/webob/util/stringtemplate.py
@@ -0,0 +1,128 @@
+"""
+Just string.Template, backported for use with Python 2.3
+"""
+####################################################################
+import re as _re
+
+class _multimap:
+    """Helper class for combining multiple mappings.
+
+    Used by .{safe_,}substitute() to combine the mapping and keyword
+    arguments.
+    """
+    def __init__(self, primary, secondary):
+        self._primary = primary
+        self._secondary = secondary
+
+    def __getitem__(self, key):
+        try:
+            return self._primary[key]
+        except KeyError:
+            return self._secondary[key]
+
+
+class _TemplateMetaclass(type):
+    pattern = r"""
+    %(delim)s(?:
+      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
+      (?P<named>%(id)s)      |   # delimiter and a Python identifier
+      {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
+      (?P<invalid>)              # Other ill-formed delimiter exprs
+    )
+    """
+
+    def __init__(cls, name, bases, dct):
+        super(_TemplateMetaclass, cls).__init__(name, bases, dct)
+        if 'pattern' in dct:
+            pattern = cls.pattern
+        else:
+            pattern = _TemplateMetaclass.pattern % {
+                'delim' : _re.escape(cls.delimiter),
+                'id'    : cls.idpattern,
+                }
+        cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE)
+
+
+class Template:
+    """A string class for supporting $-substitutions."""
+    __metaclass__ = _TemplateMetaclass
+
+    delimiter = '$'
+    idpattern = r'[_a-z][_a-z0-9]*'
+
+    def __init__(self, template):
+        self.template = template
+
+    # Search for $$, $identifier, ${identifier}, and any bare $'s
+
+    def _invalid(self, mo):
+        i = mo.start('invalid')
+        lines = self.template[:i].splitlines(True)
+        if not lines:
+            colno = 1
+            lineno = 1
+        else:
+            colno = i - len(''.join(lines[:-1]))
+            lineno = len(lines)
+        raise ValueError('Invalid placeholder in string: line %d, col %d' %
+                         (lineno, colno))
+
+    def substitute(self, *args, **kws):
+        if len(args) > 1:
+            raise TypeError('Too many positional arguments')
+        if not args:
+            mapping = kws
+        elif kws:
+            mapping = _multimap(kws, args[0])
+        else:
+            mapping = args[0]
+        # Helper function for .sub()
+        def convert(mo):
+            # Check the most common path first.
+            named = mo.group('named') or mo.group('braced')
+            if named is not None:
+                val = mapping[named]
+                # We use this idiom instead of str() because the latter will
+                # fail if val is a Unicode containing non-ASCII characters.
+                return '%s' % val
+            if mo.group('escaped') is not None:
+                return self.delimiter
+            if mo.group('invalid') is not None:
+                self._invalid(mo)
+            raise ValueError('Unrecognized named group in pattern',
+                             self.pattern)
+        return self.pattern.sub(convert, self.template)
+
+    def safe_substitute(self, *args, **kws):
+        if len(args) > 1:
+            raise TypeError('Too many positional arguments')
+        if not args:
+            mapping = kws
+        elif kws:
+            mapping = _multimap(kws, args[0])
+        else:
+            mapping = args[0]
+        # Helper function for .sub()
+        def convert(mo):
+            named = mo.group('named')
+            if named is not None:
+                try:
+                    # We use this idiom instead of str() because the latter
+                    # will fail if val is a Unicode containing non-ASCII
+                    return '%s' % mapping[named]
+                except KeyError:
+                    return self.delimiter + named
+            braced = mo.group('braced')
+            if braced is not None:
+                try:
+                    return '%s' % mapping[braced]
+                except KeyError:
+                    return self.delimiter + '{' + braced + '}'
+            if mo.group('escaped') is not None:
+                return self.delimiter
+            if mo.group('invalid') is not None:
+                return self.delimiter
+            raise ValueError('Unrecognized named group in pattern',
+                             self.pattern)
+        return self.pattern.sub(convert, self.template)
+