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:
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)
+