summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBert JW Regeer <bertjw@regeer.org>2019-05-17 23:52:43 -0600
committerBert JW Regeer <bertjw@regeer.org>2020-11-28 12:19:52 -0800
commite5fda8fe078f28131c5495fadcb4478db9f345b0 (patch)
tree222b2736d213a8a94d66fd7369a0dd9a5c7ca7bb /src
parent1704e1cb1351449b503a6fd0c990c2c15f133e08 (diff)
downloadwebob-e5fda8fe078f28131c5495fadcb4478db9f345b0.tar.gz
Remove native_ and move bytes_, text_ to webob.util
This removes any calls to `native_`, most of which should have been calls to `text_` in the first place. Also moves `bytes_` and `text_` to `webob.util` where they are more at home as we continue to remove items from `webob.compat`
Diffstat (limited to 'src')
-rw-r--r--src/webob/compat.py22
-rw-r--r--src/webob/cookies.py12
-rw-r--r--src/webob/datetime_utils.py23
-rw-r--r--src/webob/dec.py33
-rw-r--r--src/webob/exc.py6
-rw-r--r--src/webob/request.py201
-rw-r--r--src/webob/response.py141
-rw-r--r--src/webob/util.py27
8 files changed, 388 insertions, 77 deletions
diff --git a/src/webob/compat.py b/src/webob/compat.py
index 692b5b3..fc5c049 100644
--- a/src/webob/compat.py
+++ b/src/webob/compat.py
@@ -1,5 +1,3 @@
-# code stolen from "six"
-
# flake8: noqa
import cgi
@@ -17,26 +15,6 @@ from urllib.parse import quote_plus
from urllib.parse import urlencode as url_encode
from urllib.request import urlopen as url_open
-def text_(s, encoding="latin-1", errors="strict"):
- if isinstance(s, bytes):
- return s.decode(encoding, errors)
-
- return s
-
-
-def bytes_(s, encoding="latin-1", errors="strict"):
- if isinstance(s, str):
- return s.encode(encoding, errors)
-
- return s
-
-
-def native_(s, encoding="latin-1", errors="strict"):
- if isinstance(s, str):
- return s
-
- return str(s, encoding, errors)
-
urlparse = parse
diff --git a/src/webob/cookies.py b/src/webob/cookies.py
index 16d6072..3351fa1 100644
--- a/src/webob/cookies.py
+++ b/src/webob/cookies.py
@@ -9,9 +9,9 @@ import string
import time
import warnings
-from webob.compat import MutableMapping, bytes_, text_, native_
+from webob.compat import MutableMapping
-from webob.util import strings_differ
+from webob.util import strings_differ, bytes_, text_
__all__ = [
"Cookie",
@@ -91,7 +91,7 @@ class RequestCookies(MutableMapping):
header = replacement
if header:
- self._environ["HTTP_COOKIE"] = native_(header, "latin-1")
+ self._environ["HTTP_COOKIE"] = text_(header, "latin-1")
elif had_header:
self._environ["HTTP_COOKIE"] = ""
@@ -324,15 +324,15 @@ class Morsel(dict):
)
add(b"SameSite=" + self.samesite)
- return native_(b"; ".join(result), "ascii")
+ return text_(b"; ".join(result), "ascii")
__str__ = serialize
def __repr__(self):
return "<%s: %s=%r>" % (
self.__class__.__name__,
- native_(self.name),
- native_(self.value),
+ text_(self.name),
+ text_(self.value),
)
diff --git a/src/webob/datetime_utils.py b/src/webob/datetime_utils.py
index 43eebc0..86bb247 100644
--- a/src/webob/datetime_utils.py
+++ b/src/webob/datetime_utils.py
@@ -1,12 +1,9 @@
import calendar
-
+import time
from datetime import date, datetime, timedelta, tzinfo
-
from email.utils import formatdate, mktime_tz, parsedate_tz
-import time
-
-from webob.compat import native_
+from webob.util import text_
__all__ = [
"UTC",
@@ -48,6 +45,7 @@ def timedelta_to_seconds(td):
"""
Converts a timedelta instance to seconds.
"""
+
return td.seconds + (td.days * 24 * 60 * 60)
@@ -65,34 +63,44 @@ def parse_date(value):
if not value:
return None
try:
- value = native_(value)
+ if not isinstance(value, str):
+ value = str(value, "latin-1")
except Exception:
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 isinstance(dt, (bytes, str)):
- return native_(dt)
+ return text_(dt)
+
if isinstance(dt, timedelta):
dt = _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) or isinstance(dt, int)):
raise ValueError(
"You must pass in a datetime, date, time tuple, or integer object, "
"not %r" % dt
)
+
return formatdate(dt, usegmt=True)
@@ -100,6 +108,7 @@ def parse_date_delta(value):
"""
like parse_date, but also handle delta seconds
"""
+
if not value:
return None
try:
diff --git a/src/webob/dec.py b/src/webob/dec.py
index f28de79..237266a 100644
--- a/src/webob/dec.py
+++ b/src/webob/dec.py
@@ -6,10 +6,9 @@ application (while also allowing normal calling of the method with an
instantiated request).
"""
-from webob.compat import bytes_
-
-from webob.request import Request
from webob.exc import HTTPException
+from webob.request import Request
+from webob.util import bytes_
__all__ = ["wsgify"]
@@ -84,9 +83,11 @@ class wsgify(object):
self, func=None, RequestClass=None, args=(), kwargs=None, middleware_wraps=None
):
self.func = func
+
if RequestClass is not None and RequestClass is not self.RequestClass:
self.RequestClass = RequestClass
self.args = tuple(args)
+
if kwargs is None:
kwargs = {}
self.kwargs = kwargs
@@ -97,6 +98,7 @@ class wsgify(object):
def __get__(self, obj, type=None):
# This handles wrapping methods
+
if hasattr(self.func, "__get__"):
return self.clone(self.func.__get__(obj, type))
else:
@@ -105,6 +107,7 @@ class wsgify(object):
def __call__(self, req, *args, **kw):
"""Call this as a WSGI application or with a request"""
func = self.func
+
if func is None:
if args or kw:
raise TypeError(
@@ -112,7 +115,9 @@ class wsgify(object):
"will wrap" % self.__class__.__name__
)
func = req
+
return self.clone(func)
+
if isinstance(req, dict):
if len(args) != 1 or kw:
raise TypeError(
@@ -127,20 +132,26 @@ class wsgify(object):
resp = self.call_func(req, *args, **kw)
except HTTPException as exc:
resp = exc
+
if resp is None:
# FIXME: I'm not sure what this should be?
resp = req.response
+
if isinstance(resp, str):
resp = bytes_(resp, req.charset)
+
if isinstance(resp, bytes):
body = resp
resp = req.response
resp.write(body)
+
if resp is not req.response:
resp = req.response.merge_cookies(resp)
+
return resp(environ, start_response)
else:
args, kw = self._prepare_args(args, kw)
+
return self.call_func(req, *args, **kw)
def get(self, url, **kw):
@@ -156,6 +167,7 @@ class wsgify(object):
"""
kw.setdefault("method", "GET")
req = self.RequestClass.blank(url, **kw)
+
return self(req)
def post(self, url, POST=None, **kw):
@@ -173,6 +185,7 @@ class wsgify(object):
"""
kw.setdefault("method", "POST")
req = self.RequestClass.blank(url, POST=POST, **kw)
+
return self(req)
def request(self, url, **kw):
@@ -183,11 +196,13 @@ class wsgify(object):
resp = myapp.request('/article/1', method='PUT', body='New article')
"""
req = self.RequestClass.blank(url, **kw)
+
return self(req)
def call_func(self, req, *args, **kwargs):
"""Call the wrapped function; override this in a subclass to
change how the function is called."""
+
return self.func(req, *args, **kwargs)
def clone(self, func=None, **kw):
@@ -195,15 +210,20 @@ class wsgify(object):
parameters rebound
"""
kwargs = {}
+
if func is not None:
kwargs["func"] = func
+
if self.RequestClass is not self.__class__.RequestClass:
kwargs["RequestClass"] = self.RequestClass
+
if self.args:
kwargs["args"] = self.args
+
if self.kwargs:
kwargs["kwargs"] = self.kwargs
kwargs.update(kw)
+
return self.__class__(**kwargs)
# To match @decorator:
@@ -256,17 +276,22 @@ class wsgify(object):
binding the application).
"""
+
if middle_func is None:
return _UnboundMiddleware(cls, app, kw)
+
if app is None:
return _MiddlewareFactory(cls, middle_func, kw)
+
return cls(middle_func, middleware_wraps=app, kwargs=kw)
def _prepare_args(self, args, kwargs):
args = args or self.args
kwargs = kwargs or self.kwargs
+
if self.middleware_wraps:
args = (self.middleware_wraps,) + args
+
return args, kwargs
@@ -287,6 +312,7 @@ class _UnboundMiddleware(object):
def __call__(self, func, app=None):
if app is None:
app = self.app
+
return self.wrapper_class.middleware(func, app=app, **self.kw)
@@ -310,4 +336,5 @@ class _MiddlewareFactory(object):
def __call__(self, app=None, **config):
kw = self.kw.copy()
kw.update(config)
+
return self.wrapper_class.middleware(self.middleware, app, **kw)
diff --git a/src/webob/exc.py b/src/webob/exc.py
index f5719c3..48d372b 100644
--- a/src/webob/exc.py
+++ b/src/webob/exc.py
@@ -166,15 +166,15 @@ References:
"""
import json
-from string import Template
import re
import sys
+from string import Template
from webob.acceptparse import create_accept_header
-from webob.compat import text_, urlparse
+from webob.compat import urlparse
from webob.request import Request
from webob.response import Response
-from webob.util import html_escape
+from webob.util import html_escape, text_
tag_re = re.compile(r"<.*?>", re.S)
br_re = re.compile(r"<br.*?>", re.I | re.S)
diff --git a/src/webob/request.py b/src/webob/request.py
index b69c2db..43593f9 100644
--- a/src/webob/request.py
+++ b/src/webob/request.py
@@ -1,15 +1,10 @@
import binascii
import io
+import mimetypes
import os
import re
import sys
import tempfile
-import mimetypes
-
-try:
- import simplejson as json
-except ImportError:
- import json
import warnings
from webob.acceptparse import (
@@ -18,30 +13,24 @@ from webob.acceptparse import (
accept_language_property,
accept_property,
)
-
from webob.cachecontrol import CacheControl, serialize_cache_control
-
from webob.compat import (
- bytes_,
- native_,
+ cgi_FieldStorage,
parse_qsl_text,
+ quote_plus,
url_encode,
url_quote,
url_unquote,
- quote_plus,
urlparse,
- cgi_FieldStorage,
)
-
from webob.cookies import RequestCookies
-
from webob.descriptors import (
CHARSET_RE,
SCHEME_RE,
converter,
converter_date,
- environ_getter,
environ_decoder,
+ environ_getter,
parse_auth,
parse_int,
parse_int_safe,
@@ -52,12 +41,16 @@ from webob.descriptors import (
serialize_range,
upath_property,
)
-
-from webob.etag import IfRange, AnyETag, NoETag, etag_property
-
+from webob.etag import AnyETag, IfRange, NoETag, etag_property
from webob.headers import EnvironHeaders
+from webob.multidict import GetDict, MultiDict, NestedMultiDict, NoVars
+from webob.util import text_, bytes_
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
-from webob.multidict import NestedMultiDict, MultiDict, NoVars, GetDict
__all__ = ["BaseRequest", "Request"]
@@ -117,16 +110,20 @@ class BaseRequest(object):
def encget(self, key, default=NoDefault, encattr=None):
val = self.environ.get(key, default)
+
if val is NoDefault:
raise KeyError(key)
+
if val is default:
return default
+
if not encattr:
return val
encoding = getattr(self, encattr)
if encoding in _LATIN_ENCODINGS: # shortcut
return val
+
return bytes_(val, "latin-1").decode(encoding)
def encset(self, key, val, encattr=None):
@@ -140,20 +137,24 @@ class BaseRequest(object):
def charset(self):
if self._charset is None:
charset = detect_charset(self._content_type_raw)
+
if _is_utf8(charset):
charset = "UTF-8"
self._charset = charset
+
return self._charset
@charset.setter
def charset(self, charset):
if _is_utf8(charset):
charset = "UTF-8"
+
if charset != self.charset:
raise DeprecationWarning("Use req = req.decode(%r)" % charset)
def decode(self, charset=None, errors="strict"):
charset = charset or self.charset
+
if charset == "UTF-8":
return self
# cookies and path are always utf-8
@@ -168,7 +169,8 @@ class BaseRequest(object):
)
if content_type == "application/x-www-form-urlencoded":
- r.body = bytes_(t.transcode_query(native_(self.body)))
+ r.body = bytes_(t.transcode_query(text_(self.body)))
+
return r
elif content_type != "multipart/form-data":
return r
@@ -191,6 +193,7 @@ class BaseRequest(object):
r.body_file = fout
r.content_length = fout.tell()
fout.seek(0)
+
return r
# this is necessary for correct warnings depth for both
@@ -254,8 +257,10 @@ class BaseRequest(object):
If you access this value, CONTENT_LENGTH will also be updated.
"""
+
if not self.is_body_seekable:
self.make_body_seekable()
+
return self.body_file_raw
url_encoding = environ_getter("webob.url_encoding", "UTF-8")
@@ -295,13 +300,16 @@ class BaseRequest(object):
you don't include any parameters in the value then existing
parameters will be preserved.
"""
+
return self._content_type_raw.split(";", 1)[0]
def _content_type__set(self, value=None):
if value is not None:
value = str(value)
+
if ";" not in value:
content_type = self._content_type_raw
+
if ";" in content_type:
value += ";" + content_type.split(";", 1)[1]
self._content_type_raw = value
@@ -320,8 +328,10 @@ class BaseRequest(object):
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):
@@ -354,10 +364,12 @@ class BaseRequest(object):
"""
e = self.environ
xff = e.get("HTTP_X_FORWARDED_FOR")
+
if xff is not None:
addr = xff.split(",")[0].strip()
else:
addr = e.get("REMOTE_ADDR")
+
return addr
@property
@@ -374,17 +386,20 @@ class BaseRequest(object):
"""
e = self.environ
host = e.get("HTTP_HOST")
+
if host is not None:
if ":" in host and host[-1] != "]":
host, port = host.rsplit(":", 1)
else:
url_scheme = e["wsgi.url_scheme"]
+
if url_scheme == "https":
port = "443"
else:
port = "80"
else:
port = e["SERVER_PORT"]
+
return port
@property
@@ -396,6 +411,7 @@ class BaseRequest(object):
scheme = e.get("wsgi.url_scheme")
url = scheme + "://"
host = e.get("HTTP_HOST")
+
if host is not None:
if ":" in host and host[-1] != "]":
host, port = host.rsplit(":", 1)
@@ -404,6 +420,7 @@ class BaseRequest(object):
else:
host = e.get("SERVER_NAME")
port = e.get("SERVER_PORT")
+
if scheme == "https":
if port == "443":
port = None
@@ -411,8 +428,10 @@ class BaseRequest(object):
if port == "80":
port = None
url += host
+
if port:
url += ":%s" % port
+
return url
@property
@@ -421,6 +440,7 @@ class BaseRequest(object):
The URL including SCRIPT_NAME (no PATH_INFO or query string)
"""
bscript_name = bytes_(self.script_name, self.url_encoding)
+
return self.host_url + url_quote(bscript_name, PATH_SAFE)
@property
@@ -429,6 +449,7 @@ class BaseRequest(object):
The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
"""
bpath_info = bytes_(self.path_info, self.url_encoding)
+
return self.application_url + url_quote(bpath_info, PATH_SAFE)
@property
@@ -438,6 +459,7 @@ class BaseRequest(object):
"""
bscript = bytes_(self.script_name, self.url_encoding)
bpath = bytes_(self.path_info, self.url_encoding)
+
return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE)
@property
@@ -447,8 +469,10 @@ class BaseRequest(object):
"""
path = self.path
qs = self.environ.get("QUERY_STRING")
+
if qs:
path += "?" + qs
+
return path
@property
@@ -458,8 +482,10 @@ class BaseRequest(object):
"""
url = self.path_url
qs = self.environ.get("QUERY_STRING")
+
if qs:
url += "?" + qs
+
return url
def relative_url(self, other_url, to_application=False):
@@ -469,12 +495,15 @@ class BaseRequest(object):
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, pattern=None):
@@ -491,19 +520,24 @@ class BaseRequest(object):
request and None is returned.
"""
path = self.path_info
+
if not path:
return None
slashes = ""
+
while path.startswith("/"):
slashes += "/"
path = path[1:]
idx = path.find("/")
+
if idx == -1:
idx = len(path)
r = path[:idx]
+
if pattern is None or re.match(pattern, r):
self.script_name += slashes + r
self.path_info = path[idx:]
+
return r
def path_info_peek(self):
@@ -512,9 +546,11 @@ class BaseRequest(object):
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):
@@ -524,6 +560,7 @@ class BaseRequest(object):
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:
@@ -531,15 +568,18 @@ class BaseRequest(object):
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:
@@ -550,6 +590,7 @@ class BaseRequest(object):
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"]
@@ -570,15 +611,18 @@ class BaseRequest(object):
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
@@ -611,10 +655,12 @@ class BaseRequest(object):
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"
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:
@@ -649,8 +695,10 @@ class BaseRequest(object):
value use :meth:`webob.request.Request.host` instead.
"""
domain = self.host
+
if ":" in domain and domain[-1] != "]":
domain = domain.rsplit(":", 1)[0]
+
return domain
@property
@@ -658,18 +706,21 @@ class BaseRequest(object):
"""
Return the content of the request body.
"""
+
if not self.is_body_readable:
return b""
self.make_body_seekable() # we need this to have content_length
r = self.body_file.read(self.content_length)
self.body_file_raw.seek(0)
+
return r
@body.setter
def body(self, value):
if value is None:
value = b""
+
if not isinstance(value, bytes):
raise TypeError(
"You can only set Request.body to bytes (not %r)" % type(value)
@@ -684,6 +735,7 @@ class BaseRequest(object):
def _json_body__get(self):
"""Access the body of the request as JSON"""
+
return json.loads(self.body.decode(self.charset))
def _json_body__set(self, value):
@@ -698,9 +750,11 @@ class BaseRequest(object):
"""
Get/set the text value of the body
"""
+
if not self.charset:
raise AttributeError("You cannot access Request.text unless charset is set")
body = self.body
+
return body.decode(self.charset)
def _text__set(self, value):
@@ -708,6 +762,7 @@ class BaseRequest(object):
raise AttributeError(
"You cannot access Response.text unless charset is set"
)
+
if not isinstance(value, str):
raise TypeError(
"You can only set Request.text to a unicode string "
@@ -730,17 +785,21 @@ class BaseRequest(object):
requests with an appropriate Content-Type are also supported.
"""
env = self.environ
+
if "webob._parsed_post_vars" in env:
vars, body_file = env["webob._parsed_post_vars"]
+
if body_file is self.body_file_raw:
return vars
content_type = self.content_type
+
if (self.method != "POST" 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
)
@@ -764,6 +823,7 @@ class BaseRequest(object):
self.body_file_raw.seek(0)
vars = MultiDict.from_fieldstorage(fs)
env["webob._parsed_post_vars"] = (vars, self.body_file_raw)
+
return vars
@property
@@ -774,12 +834,15 @@ class BaseRequest(object):
"""
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
data = []
+
if source:
# this is disabled because we want to access req.GET
# for text/plain; charset=ascii uploads for example
@@ -789,6 +852,7 @@ class BaseRequest(object):
# data = [(d(k), d(v)) for k,v in data]
vars = GetDict(data, env)
env["webob._parsed_query_vars"] = (vars, source)
+
return vars
def _check_charset(self):
@@ -806,6 +870,7 @@ class BaseRequest(object):
the query string and request body.
"""
params = NestedMultiDict(self.GET, self.POST)
+
return params
@property
@@ -813,6 +878,7 @@ class BaseRequest(object):
"""
Return a dictionary of cookies as found in the request.
"""
+
return RequestCookies(self.environ)
@cookies.setter
@@ -831,6 +897,7 @@ class BaseRequest(object):
env = self.environ.copy()
new_req = self.__class__(env)
new_req.copy_body()
+
return new_req
def copy_get(self):
@@ -840,6 +907,7 @@ class BaseRequest(object):
verb) then it becomes GET, and the request body is thrown away.
"""
env = self.environ.copy()
+
return self.__class__(env, method="GET", content_type=None, body=b"")
# webob.is_body_seekable marks input streams that are seekable
@@ -862,6 +930,7 @@ class BaseRequest(object):
# Encoding is allowed (and works) or we have replaced
# self.body_file with something that is readable and EOF's
# correctly.
+
return self.environ.get(
"wsgi.input_terminated",
# For backwards compatibility, we fall back to checking if
@@ -888,6 +957,7 @@ class BaseRequest(object):
The choice to copy to BytesIO is made from
``self.request_body_tempfile_limit``
"""
+
if self.is_body_seekable:
self.body_file_raw.seek(0)
else:
@@ -904,6 +974,7 @@ class BaseRequest(object):
if self.is_body_readable:
# Before we copy, if we can, rewind the body file
+
if self.is_body_seekable:
self.body_file_raw.seek(0)
@@ -921,6 +992,7 @@ class BaseRequest(object):
# We attempted to read more data, but got none, break.
# This can happen if for instance we are reading as much as
# we can because we don't have a Content-Length...
+
break
elif not data:
# We have a Content-Length and we attempted to read, but
@@ -940,12 +1012,14 @@ class BaseRequest(object):
# When we have enough data that we need a tempfile, let's
# create one, then clear the temporary variable we were
# using
+
if len(newbody) > tempfile_limit:
fileobj = self.make_tempfile()
fileobj.write(newbody)
newbody = b""
# Only decrement todo if Content-Length is set
+
if self.content_length is not None:
todo -= len(data)
@@ -981,6 +1055,7 @@ class BaseRequest(object):
Create a tempfile to store big request body.
This API is not stable yet. A 'size' argument might be added.
"""
+
return tempfile.TemporaryFile()
def remove_conditional_headers(
@@ -1000,12 +1075,16 @@ class BaseRequest(object):
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")
@@ -1030,19 +1109,23 @@ class BaseRequest(object):
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, updates_to=self._update_cache_control, type="request"
)
env["webob._cache_control"] = (value, cache_obj)
+
return cache_obj
def _cache_control__set(self, value):
env = self.environ
value = value or ""
+
if isinstance(value, dict):
value = CacheControl(value, type="request")
+
if isinstance(value, CacheControl):
str_value = str(value)
env["HTTP_CACHE_CONTROL"] = str_value
@@ -1053,8 +1136,10 @@ class BaseRequest(object):
def _cache_control__del(self):
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"]
@@ -1112,6 +1197,7 @@ class BaseRequest(object):
except KeyError:
name = "(invalid WSGI environ)"
msg = "<%s at 0x%x %s>" % (self.__class__.__name__, abs(id(self)), name)
+
return msg
def as_bytes(self, skip_body=False):
@@ -1130,12 +1216,14 @@ class BaseRequest(object):
# acquire body before we handle headers so that
# content-length will be set
body = None
+
if self.is_body_readable:
if skip_body > 1:
if len(self.body) > skip_body:
body = bytes_("<body skipped (len=%s)>" % len(self.body))
else:
skip_body = False
+
if not skip_body:
body = self.body
@@ -1146,10 +1234,12 @@ class BaseRequest(object):
if body:
parts.extend([b"", body])
# HTTP clearly specifies CRLF
+
return b"\r\n".join(parts)
def as_text(self, skip_body=False):
bytes = self.as_bytes(skip_body)
+
return bytes.decode(self.charset)
__str__ = as_text
@@ -1162,13 +1252,16 @@ class BaseRequest(object):
"""
f = io.BytesIO(b)
r = cls.from_file(f)
+
if f.tell() != len(b):
raise ValueError("The string contains more data than expected")
+
return r
@classmethod
def from_text(cls, s):
b = bytes_(s, "utf-8")
+
return cls.from_bytes(b)
@classmethod
@@ -1185,6 +1278,7 @@ class BaseRequest(object):
"""
start_line = fp.readline()
is_text = isinstance(start_line, str)
+
if is_text:
crlf = "\r\n"
colon = ":"
@@ -1194,32 +1288,38 @@ class BaseRequest(object):
try:
header = start_line.rstrip(crlf)
method, resource, http_version = header.split(None, 2)
- method = native_(method, "utf-8")
- resource = native_(resource, "utf-8")
- http_version = native_(http_version, "utf-8")
+ method = text_(method, "utf-8")
+ resource = text_(resource, "utf-8")
+ http_version = text_(http_version, "utf-8")
except ValueError:
raise ValueError("Bad HTTP request line: %r" % start_line)
r = cls(
environ_from_url(resource), http_version=http_version, method=method.upper()
)
del r.environ["HTTP_HOST"]
+
while 1:
line = fp.readline()
+
if not line.strip():
# end of headers
+
break
hname, hval = line.split(colon, 1)
- hname = native_(hname, "utf-8")
- hval = native_(hval, "utf-8").strip()
+ hname = text_(hname, "utf-8")
+ hval = text_(hval, "utf-8").strip()
+
if hname in r.headers:
hval = r.headers[hname] + ", " + hval
r.headers[hname] = hval
clen = r.content_length
+
if clen is None:
body = fp.read()
else:
body = fp.read(clen)
+
if is_text:
body = bytes_(body, "utf-8")
r.body = body
@@ -1239,6 +1339,7 @@ class BaseRequest(object):
do this and there was an exception, the exception will be
raised directly.
"""
+
if self.is_body_seekable:
self.body_file_raw.seek(0)
captured = []
@@ -1249,9 +1350,11 @@ class BaseRequest(object):
etype, exc, tb = exc_info
raise etype(exc).with_traceback(tb)
captured[:] = [status, headers, exc_info]
+
return output.append
app_iter = application(self.environ, start_response)
+
if output or not captured:
try:
output.extend(app_iter)
@@ -1259,6 +1362,7 @@ class BaseRequest(object):
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:
@@ -1279,8 +1383,10 @@ class BaseRequest(object):
If ``application`` is not given, this will send the request to
``self.make_default_send_app()``
"""
+
if application is None:
application = self.make_default_send_app()
+
if catch_exc_info:
status, headers, app_iter, exc_info = self.call_application(
application, catch_exc_info=True
@@ -1290,6 +1396,7 @@ class BaseRequest(object):
status, headers, app_iter = self.call_application(
application, catch_exc_info=False
)
+
return self.ResponseClass(
status=status, headerlist=list(headers), app_iter=app_iter
)
@@ -1304,6 +1411,7 @@ class BaseRequest(object):
from webob import client
_client = client
+
return client.send_request_app
@classmethod
@@ -1324,14 +1432,18 @@ class BaseRequest(object):
Any extra keyword will be passed to ``__init__``.
"""
env = environ_from_url(path)
+
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":
@@ -1344,17 +1456,22 @@ class BaseRequest(object):
env["SERVER_PORT"] = port
env["SERVER_NAME"] = host
env["HTTP_HOST"] = netloc
+
if path:
env["SCRIPT_NAME"] = url_unquote(path)
+
if environ:
env.update(environ)
content_type = kw.get("content_type", env.get("CONTENT_TYPE"))
+
if headers and "Content-Type" in headers:
content_type = headers["Content-Type"]
+
if content_type is not None:
kw["content_type"] = content_type
environ_add_POST(env, POST, content_type=content_type)
obj = cls(env, **kw)
+
if headers is not None:
obj.headers.update(headers)
@@ -1394,10 +1511,13 @@ class Request(AdhocAttrMixin, BaseRequest):
def environ_from_url(path):
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"
@@ -1408,6 +1528,7 @@ def environ_from_url(path):
else:
scheme = "http"
netloc = "localhost:80"
+
if path and "?" in path:
path_info, query_string = path.split("?", 1)
path_info = url_unquote(path_info)
@@ -1432,6 +1553,7 @@ def environ_from_url(path):
"wsgi.run_once": False,
"webob.is_body_seekable": True,
}
+
return env
@@ -1440,20 +1562,26 @@ def environ_add_POST(env, data, content_type=None):
return
elif isinstance(data, str):
data = data.encode("ascii")
+
if env["REQUEST_METHOD"] not in ("POST", "PUT"):
env["REQUEST_METHOD"] = "POST"
has_files = False
+
if hasattr(data, "items"):
data = list(data.items())
+
for _, v in data:
if isinstance(v, (tuple, list)):
has_files = True
+
break
+
if content_type is None:
if has_files:
content_type = "multipart/form-data"
else:
content_type = "application/x-www-form-urlencoded"
+
if content_type.startswith("multipart/form-data"):
if not isinstance(data, bytes):
content_type, data = _encode_multipart(data, content_type)
@@ -1462,6 +1590,7 @@ def environ_add_POST(env, data, content_type=None):
raise ValueError(
"Submiting files is not allowed for" " content type `%s`" % content_type
)
+
if not isinstance(data, bytes):
data = url_encode(data)
else:
@@ -1509,6 +1638,7 @@ class LimitedLengthFile(io.RawIOBase):
data = self.file.read(sz0)
sz = len(data)
self.remaining -= sz
+
if sz < sz0 and self.remaining:
raise DisconnectionError(
"The client disconnected while sending the body "
@@ -1521,8 +1651,9 @@ class LimitedLengthFile(io.RawIOBase):
def _get_multipart_boundary(ctype):
m = re.search(r"boundary=([^ ]+)", ctype, re.I)
+
if m:
- return native_(m.group(1).strip('"'))
+ return text_(m.group(1).strip('"'))
def _encode_multipart(vars, content_type, fout=None):
@@ -1535,21 +1666,26 @@ def _encode_multipart(vars, content_type, fout=None):
CRLF = b"\r\n"
boundary = _get_multipart_boundary(content_type)
+
if not boundary:
- boundary = native_(binascii.hexlify(os.urandom(10)))
+ boundary = text_(binascii.hexlify(os.urandom(10)))
content_type += "; boundary=%s" % boundary
+
for name, value in vars:
w(b"--")
wt(boundary)
w(CRLF)
wt("Content-Disposition: form-data")
+
if name is not None:
wt('; name="%s"' % name)
filename = None
+
if getattr(value, "filename", None):
filename = value.filename
elif isinstance(value, (list, tuple)):
filename, value = value
+
if hasattr(value, "read"):
value = value.read()
@@ -1562,8 +1698,10 @@ def _encode_multipart(vars, content_type, fout=None):
w(CRLF)
# TODO: should handle value.disposition_options
+
if getattr(value, "type", None):
wt("Content-type: %s" % value.type)
+
if value.type_options:
for ct_name, ct_value in sorted(value.type_options.items()):
wt('; %s="%s"' % (ct_name, ct_value))
@@ -1572,14 +1710,17 @@ def _encode_multipart(vars, content_type, fout=None):
wt("Content-type: %s" % mime_type)
w(CRLF)
w(CRLF)
+
if hasattr(value, "value"):
value = value.value
+
if isinstance(value, bytes):
w(value)
else:
wt(value)
w(CRLF)
wt("--%s--" % boundary)
+
if fout:
return content_type, fout
else:
@@ -1588,6 +1729,7 @@ def _encode_multipart(vars, content_type, fout=None):
def detect_charset(ctype):
m = CHARSET_RE.search(ctype)
+
if m:
return m.group(1).strip('"').strip()
@@ -1607,8 +1749,10 @@ class Transcoder(object):
def transcode_query(self, q):
q_orig = q
+
if "=" not in q:
# this doesn't look like a form submission
+
return q_orig
q = list(parse_qsl_text(q, self.charset))
@@ -1621,8 +1765,10 @@ class Transcoder(object):
return b
data = []
+
for field in fs.list or ():
field.name = decode(field.name)
+
if field.filename:
field.filename = decode(field.filename)
data.append((field.name, field))
@@ -1631,4 +1777,5 @@ class Transcoder(object):
# TODO: transcode big requests to temp file
content_type, fout = _encode_multipart(data, content_type, fout=io.BytesIO())
+
return fout
diff --git a/src/webob/response.py b/src/webob/response.py
index 8c485e0..85ac0eb 100644
--- a/src/webob/response.py
+++ b/src/webob/response.py
@@ -7,7 +7,7 @@ from hashlib import md5
from webob.byterange import ContentRange
from webob.cachecontrol import CacheControl, serialize_cache_control
-from webob.compat import bytes_, native_, url_quote, urlparse
+from webob.compat import url_quote, urlparse
from webob.cookies import Cookie, make_cookie
from webob.datetime_utils import (
parse_date_delta,
@@ -33,7 +33,13 @@ from webob.descriptors import (
)
from webob.headers import ResponseHeaders
from webob.request import BaseRequest
-from webob.util import status_generic_reasons, status_reasons, warn_deprecation
+from webob.util import (
+ bytes_,
+ status_generic_reasons,
+ status_reasons,
+ text_,
+ warn_deprecation,
+)
try:
import simplejson as json
@@ -170,6 +176,7 @@ class Response(object):
**kw
):
# Do some sanity checking, and turn json_body into an actual body
+
if app_iter is None and body is None and ("json_body" in kw or "json" in kw):
if "json_body" in kw:
json_body = kw.pop("json_body")
@@ -187,6 +194,7 @@ class Response(object):
raise TypeError("You may only give one of the body and app_iter arguments")
# Set up Response.status
+
if status is None:
self._status = "200 OK"
else:
@@ -194,6 +202,7 @@ class Response(object):
# Initialize headers
self._headers = None
+
if headerlist is None:
self._headerlist = []
else:
@@ -210,6 +219,7 @@ class Response(object):
# Content-Type: application/foo with no charset on it.
encoding = None
+
if charset is not _marker:
encoding = charset
@@ -245,6 +255,7 @@ class Response(object):
# If the Content-Type already has a charset, we don't set the user
# provided charset on the Content-Type, so we shouldn't use it as
# the encoding for text_type based body's.
+
if has_charset:
encoding = None
@@ -275,17 +286,20 @@ class Response(object):
self._headerlist.append(("Content-Type", content_type))
# Set up conditional response
+
if conditional_response is None:
self.conditional_response = self.default_conditional_response
else:
self.conditional_response = bool(conditional_response)
# Set up app_iter if the HTTP Status code has a body
+
if app_iter is None and code_has_body:
if isinstance(body, str):
# Fall back to trying self.charset if encoding is not set. In
# most cases encoding will be set to the default value.
encoding = encoding or self.charset
+
if encoding is None:
raise TypeError(
"You cannot set the body to a text value without a " "charset"
@@ -306,6 +320,7 @@ class Response(object):
self._app_iter = app_iter
# Loop through all the remaining keyword arguments
+
for name, value in kw.items():
if not hasattr(self.__class__, name):
# Not a basic attribute
@@ -336,27 +351,29 @@ class Response(object):
if status.startswith(_http):
(http_ver, status_num, status_text) = status.split(None, 2)
- status = "%s %s" % (native_(status_num), native_(status_text))
+ status = "%s %s" % (text_(status_num), text_(status_text))
while 1:
line = fp.readline().strip()
+
if not line:
# end of headers
+
break
try:
header_name, value = line.split(_colon, 1)
except ValueError:
raise ValueError("Bad header line: %r" % line)
value = value.strip()
- headerlist.append(
- (native_(header_name, "latin-1"), native_(value, "latin-1"))
- )
+ headerlist.append((text_(header_name, "latin-1"), text_(value, "latin-1")))
r = cls(status=status, headerlist=headerlist, app_iter=())
body = fp.read(r.content_length or 0)
+
if is_text:
r.text = body
else:
r.body = body
+
return r
def copy(self):
@@ -366,6 +383,7 @@ class Response(object):
iter_close(self._app_iter)
# and this to make sure app_iter instances are different
self._app_iter = list(app_iter)
+
return self.__class__(
status=self._status,
headerlist=self._headerlist[:],
@@ -382,10 +400,12 @@ class Response(object):
def __str__(self, skip_body=False):
parts = [self.status]
+
if not skip_body:
# Force enumeration of the body (to set content-length)
self.body
parts += map("%s: %s".__mod__, self.headerlist)
+
if not skip_body and self.body:
parts += ["", self.text]
@@ -399,6 +419,7 @@ class Response(object):
"""
The status string.
"""
+
return self._status
def _status__set(self, value):
@@ -408,6 +429,7 @@ class Response(object):
pass
else:
self.status_code = code
+
return
if isinstance(value, bytes):
@@ -434,6 +456,7 @@ class Response(object):
"""
The status as an integer.
"""
+
return int(self._status.split()[0])
def _status_code__set(self, code):
@@ -454,10 +477,12 @@ class Response(object):
"""
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()
@@ -478,8 +503,10 @@ class Response(object):
"""
The headers in a dictionary-like object.
"""
+
if self._headers is None:
self._headers = ResponseHeaders.view_list(self._headerlist)
+
return self._headers
def _headers__set(self, value):
@@ -505,17 +532,21 @@ class Response(object):
# return app_iter[0]
# except:
# pass
+
if isinstance(app_iter, list) and len(app_iter) == 1:
return app_iter[0]
+
if app_iter is None:
raise AttributeError("No body has been set")
try:
body = b"".join(app_iter)
finally:
iter_close(app_iter)
+
if isinstance(body, str):
raise _error_unicode_in_app_iter(app_iter, body)
self._app_iter = [body]
+
if len(body) == 0:
# if body-length is zero, we assume it's a HEAD response and
# leave content_length alone
@@ -527,6 +558,7 @@ class Response(object):
"Content-Length is different from actual app_iter length "
"(%r!=%r)" % (self.content_length, len(body))
)
+
return body
def _body__set(self, value=b""):
@@ -541,6 +573,7 @@ class Response(object):
value
)
raise TypeError(msg)
+
if self._app_iter is not None:
self.content_md5 = None
self._app_iter = [value]
@@ -565,6 +598,7 @@ class Response(object):
"""
# Note: UTF-8 is a content-type specific default for JSON
+
return json.loads(self.body.decode("UTF-8"))
def _json_body__set(self, value):
@@ -606,6 +640,7 @@ class Response(object):
Get/set the text value of the body using the ``charset`` of the
``Content-Type`` or the ``default_body_encoding``.
"""
+
if not self.charset and not self.default_body_encoding:
raise AttributeError(
"You cannot access Response.text unless charset or "
@@ -613,6 +648,7 @@ class Response(object):
)
decoding = self.charset or self.default_body_encoding
body = self.body
+
return body.decode(decoding, self.unicode_errors)
def _text__set(self, value):
@@ -621,6 +657,7 @@ class Response(object):
"You cannot access Response.text unless charset or "
"default_body_encoding is set"
)
+
if not isinstance(value, str):
raise TypeError(
"You can only set Response.text to a unicode string "
@@ -648,6 +685,7 @@ class Response(object):
body. If you passed in a list ``app_iter``, that ``app_iter`` will be
modified by writes.
"""
+
return ResponseBodyFile(self)
def _body_file__set(self, file):
@@ -665,12 +703,14 @@ class Response(object):
if not isinstance(text, str):
msg = "You can only write str to a Response.body_file, not %s"
raise TypeError(msg % type(text))
+
if not self.charset:
msg = "You can only write text to Response if charset has " "been set"
raise TypeError(msg)
text = text.encode(self.charset)
text_len = len(text)
app_iter = self._app_iter
+
if not isinstance(app_iter, list):
try:
new_app_iter = self._app_iter = list(app_iter)
@@ -679,6 +719,7 @@ class Response(object):
app_iter = new_app_iter
self.content_length = sum(len(chunk) for chunk in app_iter)
app_iter.append(text)
+
if self.content_length is not None:
self.content_length += text_len
return text_len
@@ -694,6 +735,7 @@ class Response(object):
If ``body`` was set, this will create an ``app_iter`` from that
``body`` (a single-item list).
"""
+
return self._app_iter
def _app_iter__set(self, value):
@@ -780,23 +822,29 @@ class Response(object):
allows for a ``charset`` parameter.
"""
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:
self._charset__del()
+
return
header = self.headers.get("Content-Type", None)
+
if header is None:
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
@@ -804,10 +852,13 @@ class Response(object):
def _charset__del(self):
header = self.headers.pop("Content-Type", None)
+
if header is None:
# 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
@@ -843,13 +894,16 @@ class Response(object):
resp.content_type_params = params
"""
header = self.headers.get("Content-Type")
+
if not header:
return None
+
return header.split(";", 1)[0]
def _content_type__set(self, value):
if not value:
self._content_type__del()
+
return
else:
if not isinstance(value, str):
@@ -871,6 +925,7 @@ class Response(object):
# otherwise add a charset if the content_type has a charset.
#
# We add the default charset if the content-type is "texty".
+
if new_charset and (
content_type == "text/html" or _content_type_has_charset(content_type)
):
@@ -900,20 +955,25 @@ class Response(object):
be applied otherwise.)
"""
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:
self._content_type_params__del()
+
return
params = []
+
for k, v in sorted(value_dict.items()):
if not _OK_PARAM_RE.search(v):
v = '"%s"' % v.replace('"', '\\"')
@@ -1052,16 +1112,21 @@ class Response(object):
Unset a cookie with the given name (remove it from the response).
"""
existing = self.headers.getall("Set-Cookie")
+
if not existing and not strict:
return
cookies = Cookie()
+
for header in existing:
cookies.load(header)
+
if isinstance(name, str):
name = name.encode("utf8")
+
if name in cookies:
del cookies[name]
del self.headers["Set-Cookie"]
+
for m in cookies.values():
self.headerlist.append(("Set-Cookie", m.serialize()))
elif strict:
@@ -1074,11 +1139,14 @@ class Response(object):
If the ``resp`` is a :class:`webob.Response` object, then the
other object will be modified in-place.
"""
+
if not self.headers.get("Set-Cookie"):
return resp
+
if isinstance(resp, Response):
for header in self.headers.getall("Set-Cookie"):
resp.headers.add("Set-Cookie", header)
+
return resp
else:
c_headers = [h for h in self.headerlist if h[0].lower() == "set-cookie"]
@@ -1105,29 +1173,37 @@ class Response(object):
<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, str):
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
@@ -1139,6 +1215,7 @@ class Response(object):
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"]
@@ -1162,11 +1239,13 @@ class Response(object):
expire in the given seconds, and any other attributes are used
for ``cache_control`` (e.g., ``private=True``).
"""
+
if seconds is True:
seconds = 0
elif isinstance(seconds, timedelta):
seconds = timedelta_to_seconds(seconds)
cache_control = self.cache_control
+
if seconds is None:
pass
elif not seconds:
@@ -1181,6 +1260,7 @@ class Response(object):
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"
@@ -1189,6 +1269,7 @@ class Response(object):
cache_control.max_age = seconds
self.expires = datetime.utcnow() + timedelta(seconds=seconds)
self.pragma = None
+
for name, value in kw.items():
setattr(cache_control, name, value)
@@ -1204,11 +1285,15 @@ class Response(object):
``identity`` are supported).
"""
assert encoding in ("identity", "gzip"), "Unknown encoding: %r" % encoding
+
if encoding == "identity":
self.decode_content()
+
return
+
if self.content_encoding == "gzip":
return
+
if lazy:
self.app_iter = gzip_app_iter(self._app_iter)
self.content_length = None
@@ -1219,12 +1304,15 @@ class Response(object):
def decode_content(self):
content_encoding = self.content_encoding or "identity"
+
if content_encoding == "identity":
return
+
if content_encoding not in ("gzip", "deflate"):
raise ValueError(
"I don't know how to decode the content %s" % content_encoding
)
+
if content_encoding == "gzip":
from gzip import GzipFile
from io import BytesIO
@@ -1247,13 +1335,15 @@ class Response(object):
If ``set_content_md5`` is ``True``, sets ``self.content_md5`` as well.
"""
+
if body is None:
body = self.body
md5_digest = md5(body).digest()
md5_digest = b64encode(md5_digest)
md5_digest = md5_digest.replace(b"\n", b"")
- md5_digest = native_(md5_digest)
+ md5_digest = text_(md5_digest)
self.etag = md5_digest.strip("=")
+
if set_content_md5:
self.content_md5 = md5_digest
@@ -1263,10 +1353,12 @@ class Response(object):
return value
new_location = urlparse.urljoin(_request_uri(environ), value)
+
return new_location
def _abs_headerlist(self, environ):
# Build the headerlist, if we have a Location header, make it absolute
+
return [
(k, v)
if k.lower() != "location"
@@ -1282,15 +1374,19 @@ class Response(object):
"""
WSGI application interface
"""
+
if self.conditional_response:
return self.conditional_response_app(environ, start_response)
headerlist = self._abs_headerlist(environ)
start_response(self.status, headerlist)
+
if environ["REQUEST_METHOD"] == "HEAD":
# Special case here...
+
return EmptyResponse(self._app_iter)
+
return self._app_iter
_safe_methods = ("GET", "HEAD")
@@ -1311,15 +1407,20 @@ class Response(object):
headerlist = self._abs_headerlist(environ)
method = environ.get("REQUEST_METHOD", "GET")
+
if method in self._safe_methods:
status304 = False
+
if req.if_none_match and self.etag:
status304 = self.etag in req.if_none_match
elif req.if_modified_since and self.last_modified:
status304 = self.last_modified <= req.if_modified_since
+
if status304:
start_response("304 Not Modified", filter_headers(headerlist))
+
return EmptyResponse(self._app_iter)
+
if (
req.range
and self in req.if_range
@@ -1329,6 +1430,7 @@ class Response(object):
and self.content_length is not None
):
content_range = req.range.content_range(self.content_length)
+
if content_range is None:
iter_close(self._app_iter)
body = bytes_("Requested range not satisfiable: %s" % req.range)
@@ -1341,11 +1443,14 @@ class Response(object):
("Content-Type", "text/plain"),
] + filter_headers(headerlist)
start_response("416 Requested Range Not Satisfiable", headerlist)
+
if method == "HEAD":
return ()
+
return [body]
else:
app_iter = self.app_iter_range(content_range.start, content_range.stop)
+
if app_iter is not None:
# the following should be guaranteed by
# Range.range_for_length(length)
@@ -1358,13 +1463,17 @@ class Response(object):
("Content-Range", str(content_range)),
] + filter_headers(headerlist, ("content-length",))
start_response("206 Partial Content", headerlist)
+
if method == "HEAD":
return EmptyResponse(app_iter)
+
return app_iter
start_response(self.status, headerlist)
+
if method == "HEAD":
return EmptyResponse(self._app_iter)
+
return self._app_iter
def app_iter_range(self, start, stop):
@@ -1373,8 +1482,10 @@ class Response(object):
serves up only the given ``start:stop`` range.
"""
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)
@@ -1385,6 +1496,7 @@ def filter_headers(hlist, remove_headers=("content-length", "content-type")):
def iter_file(file, block_size=1 << 18): # 256Kb
while True:
data = file.read(block_size)
+
if not data:
break
yield data
@@ -1413,6 +1525,7 @@ class ResponseBodyFile(object):
"""
Write a sequence of lines to the response.
"""
+
for item in seq:
self.write(item)
@@ -1426,6 +1539,7 @@ class ResponseBodyFile(object):
"""
Provide the current location where we are going to start writing.
"""
+
if not self.response.has_body:
return 0
@@ -1450,17 +1564,21 @@ class AppIterRange(object):
def _skip_start(self):
start, stop = self.start, self.stop
+
for chunk in self.app_iter:
self._pos += len(chunk)
+
if self._pos < start:
continue
elif self._pos == start:
return b""
else:
chunk = chunk[start - self._pos :]
+
if stop is not None and self._pos > stop:
chunk = chunk[: stop - self._pos]
assert len(chunk) == stop - start
+
return chunk
else:
raise StopIteration()
@@ -1468,8 +1586,10 @@ class AppIterRange(object):
def next(self):
if self._pos < self.start:
# need to skip some leading bytes
+
return self._skip_start()
stop = self.stop
+
if stop is not None and self._pos >= stop:
raise StopIteration
@@ -1533,6 +1653,7 @@ def _request_uri(environ):
url += environ["HTTP_HOST"]
else:
url += environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]
+
if url.endswith(":80") and environ["wsgi.url_scheme"] == "http":
url = url[:-3]
elif url.endswith(":443") and environ["wsgi.url_scheme"] == "https":
@@ -1543,10 +1664,12 @@ def _request_uri(environ):
url += url_quote(script_name)
qpath_info = url_quote(path_info)
+
if "SCRIPT_NAME" not in environ:
url += qpath_info[1:]
else:
url += qpath_info
+
return url
@@ -1563,6 +1686,7 @@ def gzip_app_iter(app_iter):
)
yield _gzip_header
+
for item in app_iter:
size += len(item)
crc = zlib.crc32(item, crc) & 0xFFFFFFFF
@@ -1571,11 +1695,13 @@ def gzip_app_iter(app_iter):
# small enough; it buffers the input for the next iteration or for a
# flush.
result = compress.compress(item)
+
if result:
yield result
# Similarly, flush may also not yield a value.
result = compress.flush()
+
if result:
yield result
yield struct.pack("<2L", crc, size & 0xFFFFFFFF)
@@ -1583,6 +1709,7 @@ def gzip_app_iter(app_iter):
def _error_unicode_in_app_iter(app_iter, body):
app_iter_repr = repr(app_iter)
+
if len(app_iter_repr) > 50:
app_iter_repr = app_iter_repr[:30] + "..." + app_iter_repr[-10:]
raise TypeError(
diff --git a/src/webob/util.py b/src/webob/util.py
index 7eb826b..7a3331f 100644
--- a/src/webob/util.py
+++ b/src/webob/util.py
@@ -1,10 +1,23 @@
import warnings
-from webob.compat import escape, text_
-
+from webob.compat import escape
from webob.headers import _trans_key
+def text_(s, encoding="latin-1", errors="strict"):
+ if isinstance(s, bytes):
+ return str(s, encoding, errors)
+
+ return s
+
+
+def bytes_(s, encoding="latin-1", errors="strict"):
+ if isinstance(s, str):
+ return s.encode(encoding, errors)
+
+ return s
+
+
def html_escape(s):
"""HTML-escape a string or object
@@ -15,20 +28,26 @@ def html_escape(s):
None is treated specially, and returns the empty string.
"""
+
if s is None:
return ""
__html__ = getattr(s, "__html__", None)
+
if __html__ is not None and callable(__html__):
return s.__html__()
+
if not isinstance(s, str):
__unicode__ = getattr(s, "__unicode__", None)
+
if __unicode__ is not None and callable(__unicode__):
s = s.__unicode__()
else:
s = str(s)
s = escape(s, True)
+
if isinstance(s, str):
s = s.encode("ascii", "xmlcharrefreplace")
+
return text_(s)
@@ -40,6 +59,7 @@ def header_docstring(header, rfc_section):
major_section,
rfc_section,
)
+
return "Gets and sets the ``%s`` header (`HTTP spec section %s <%s>`_)." % (
header,
rfc_section,
@@ -49,6 +69,7 @@ def header_docstring(header, rfc_section):
def warn_deprecation(text, version, stacklevel):
# version specifies when to start raising exceptions instead of warnings
+
if version in ("1.2", "1.3", "1.4", "1.5", "1.6", "1.7"):
raise DeprecationWarning(text)
else:
@@ -153,6 +174,7 @@ def strings_differ(string1, string2, compare_digest=compare_digest):
"""
len_eq = len(string1) == len(string2)
+
if len_eq:
invalid_bits = 0
left = string1
@@ -166,4 +188,5 @@ def strings_differ(string1, string2, compare_digest=compare_digest):
else:
for a, b in zip(left, right):
invalid_bits += a != b
+
return invalid_bits != 0