diff options
| author | Bert JW Regeer <bertjw@regeer.org> | 2019-05-17 23:52:43 -0600 |
|---|---|---|
| committer | Bert JW Regeer <bertjw@regeer.org> | 2020-11-28 12:19:52 -0800 |
| commit | e5fda8fe078f28131c5495fadcb4478db9f345b0 (patch) | |
| tree | 222b2736d213a8a94d66fd7369a0dd9a5c7ca7bb /src | |
| parent | 1704e1cb1351449b503a6fd0c990c2c15f133e08 (diff) | |
| download | webob-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.py | 22 | ||||
| -rw-r--r-- | src/webob/cookies.py | 12 | ||||
| -rw-r--r-- | src/webob/datetime_utils.py | 23 | ||||
| -rw-r--r-- | src/webob/dec.py | 33 | ||||
| -rw-r--r-- | src/webob/exc.py | 6 | ||||
| -rw-r--r-- | src/webob/request.py | 201 | ||||
| -rw-r--r-- | src/webob/response.py | 141 | ||||
| -rw-r--r-- | src/webob/util.py | 27 |
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 |
