summaryrefslogtreecommitdiff
path: root/src/werkzeug/http.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/werkzeug/http.py')
-rw-r--r--src/werkzeug/http.py513
1 files changed, 288 insertions, 225 deletions
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
index 9e18ea97..af320075 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -16,54 +16,68 @@
:copyright: 2007 Pallets
:license: BSD-3-Clause
"""
+import base64
import re
import warnings
-from time import time, gmtime
+from datetime import datetime
+from datetime import timedelta
+from hashlib import md5
+from time import gmtime
+from time import time
+
+from ._compat import integer_types
+from ._compat import iteritems
+from ._compat import PY2
+from ._compat import string_types
+from ._compat import text_type
+from ._compat import to_bytes
+from ._compat import to_unicode
+from ._compat import try_coerce_native
+from ._internal import _cookie_parse_impl
+from ._internal import _cookie_quote
+from ._internal import _make_cookie_domain
+
try:
from email.utils import parsedate_tz
-except ImportError: # pragma: no cover
+except ImportError:
from email.Utils import parsedate_tz
+
try:
from urllib.request import parse_http_list as _parse_list_header
from urllib.parse import unquote_to_bytes as _unquote
-except ImportError: # pragma: no cover
- from urllib2 import parse_http_list as _parse_list_header, \
- unquote as _unquote
-from datetime import datetime, timedelta
-from hashlib import md5
-import base64
+except ImportError:
+ from urllib2 import parse_http_list as _parse_list_header
+ from urllib2 import unquote as _unquote
-from werkzeug._internal import _cookie_quote, _make_cookie_domain, \
- _cookie_parse_impl
-from werkzeug._compat import to_unicode, iteritems, text_type, \
- string_types, try_coerce_native, to_bytes, PY2, \
- integer_types
-
-
-_cookie_charset = 'latin1'
-_basic_auth_charset = 'utf-8'
+_cookie_charset = "latin1"
+_basic_auth_charset = "utf-8"
# for explanation of "media-range", etc. see Sections 5.3.{1,2} of RFC 7231
_accept_re = re.compile(
- r'''( # media-range capturing-parenthesis
- [^\s;,]+ # type/subtype
- (?:[ \t]*;[ \t]* # ";"
- (?: # parameter non-capturing-parenthesis
- [^\s;,q][^\s;,]* # token that doesn't start with "q"
- | # or
- q[^\s;,=][^\s;,]* # token that is more than just "q"
- )
- )* # zero or more parameters
- ) # end of media-range
- (?:[ \t]*;[ \t]*q= # weight is a "q" parameter
- (\d*(?:\.\d+)?) # qvalue capturing-parentheses
- [^,]* # "extension" accept params: who cares?
- )? # accept params are optional
- ''', re.VERBOSE)
-_token_chars = frozenset("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- '^_`abcdefghijklmnopqrstuvwxyz|~')
+ r"""
+ ( # media-range capturing-parenthesis
+ [^\s;,]+ # type/subtype
+ (?:[ \t]*;[ \t]* # ";"
+ (?: # parameter non-capturing-parenthesis
+ [^\s;,q][^\s;,]* # token that doesn't start with "q"
+ | # or
+ q[^\s;,=][^\s;,]* # token that is more than just "q"
+ )
+ )* # zero or more parameters
+ ) # end of media-range
+ (?:[ \t]*;[ \t]*q= # weight is a "q" parameter
+ (\d*(?:\.\d+)?) # qvalue capturing-parentheses
+ [^,]* # "extension" accept params: who cares?
+ )? # accept params are optional
+ """,
+ re.VERBOSE,
+)
+_token_chars = frozenset(
+ "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
+)
_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
-_unsafe_header_chars = set('()<>@,;:\"/[]?={} \t')
-_option_header_piece_re = re.compile(r'''
+_unsafe_header_chars = set('()<>@,;:"/[]?={} \t')
+_option_header_piece_re = re.compile(
+ r"""
;\s*,?\s* # newlines were replaced with commas
(?P<key>
"[^"\\]*(?:\\.[^"\\]*)*" # quoted string
@@ -89,100 +103,116 @@ _option_header_piece_re = re.compile(r'''
)?
)?
\s*
-''', flags=re.VERBOSE)
-_option_header_start_mime_type = re.compile(r',\s*([^;,\s]+)([;,]\s*.+)?')
-
-_entity_headers = frozenset([
- 'allow', 'content-encoding', 'content-language', 'content-length',
- 'content-location', 'content-md5', 'content-range', 'content-type',
- 'expires', 'last-modified'
-])
-_hop_by_hop_headers = frozenset([
- 'connection', 'keep-alive', 'proxy-authenticate',
- 'proxy-authorization', 'te', 'trailer', 'transfer-encoding',
- 'upgrade'
-])
+ """,
+ flags=re.VERBOSE,
+)
+_option_header_start_mime_type = re.compile(r",\s*([^;,\s]+)([;,]\s*.+)?")
+
+_entity_headers = frozenset(
+ [
+ "allow",
+ "content-encoding",
+ "content-language",
+ "content-length",
+ "content-location",
+ "content-md5",
+ "content-range",
+ "content-type",
+ "expires",
+ "last-modified",
+ ]
+)
+_hop_by_hop_headers = frozenset(
+ [
+ "connection",
+ "keep-alive",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade",
+ ]
+)
HTTP_STATUS_CODES = {
- 100: 'Continue',
- 101: 'Switching Protocols',
- 102: 'Processing',
- 200: 'OK',
- 201: 'Created',
- 202: 'Accepted',
- 203: 'Non Authoritative Information',
- 204: 'No Content',
- 205: 'Reset Content',
- 206: 'Partial Content',
- 207: 'Multi Status',
- 226: 'IM Used', # see RFC 3229
- 300: 'Multiple Choices',
- 301: 'Moved Permanently',
- 302: 'Found',
- 303: 'See Other',
- 304: 'Not Modified',
- 305: 'Use Proxy',
- 307: 'Temporary Redirect',
- 308: 'Permanent Redirect',
- 400: 'Bad Request',
- 401: 'Unauthorized',
- 402: 'Payment Required', # unused
- 403: 'Forbidden',
- 404: 'Not Found',
- 405: 'Method Not Allowed',
- 406: 'Not Acceptable',
- 407: 'Proxy Authentication Required',
- 408: 'Request Timeout',
- 409: 'Conflict',
- 410: 'Gone',
- 411: 'Length Required',
- 412: 'Precondition Failed',
- 413: 'Request Entity Too Large',
- 414: 'Request URI Too Long',
- 415: 'Unsupported Media Type',
- 416: 'Requested Range Not Satisfiable',
- 417: 'Expectation Failed',
- 418: 'I\'m a teapot', # see RFC 2324
- 421: 'Misdirected Request', # see RFC 7540
- 422: 'Unprocessable Entity',
- 423: 'Locked',
- 424: 'Failed Dependency',
- 426: 'Upgrade Required',
- 428: 'Precondition Required', # see RFC 6585
- 429: 'Too Many Requests',
- 431: 'Request Header Fields Too Large',
- 449: 'Retry With', # proprietary MS extension
- 451: 'Unavailable For Legal Reasons',
- 500: 'Internal Server Error',
- 501: 'Not Implemented',
- 502: 'Bad Gateway',
- 503: 'Service Unavailable',
- 504: 'Gateway Timeout',
- 505: 'HTTP Version Not Supported',
- 507: 'Insufficient Storage',
- 510: 'Not Extended'
+ 100: "Continue",
+ 101: "Switching Protocols",
+ 102: "Processing",
+ 200: "OK",
+ 201: "Created",
+ 202: "Accepted",
+ 203: "Non Authoritative Information",
+ 204: "No Content",
+ 205: "Reset Content",
+ 206: "Partial Content",
+ 207: "Multi Status",
+ 226: "IM Used", # see RFC 3229
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Found",
+ 303: "See Other",
+ 304: "Not Modified",
+ 305: "Use Proxy",
+ 307: "Temporary Redirect",
+ 308: "Permanent Redirect",
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required", # unused
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict",
+ 410: "Gone",
+ 411: "Length Required",
+ 412: "Precondition Failed",
+ 413: "Request Entity Too Large",
+ 414: "Request URI Too Long",
+ 415: "Unsupported Media Type",
+ 416: "Requested Range Not Satisfiable",
+ 417: "Expectation Failed",
+ 418: "I'm a teapot", # see RFC 2324
+ 421: "Misdirected Request", # see RFC 7540
+ 422: "Unprocessable Entity",
+ 423: "Locked",
+ 424: "Failed Dependency",
+ 426: "Upgrade Required",
+ 428: "Precondition Required", # see RFC 6585
+ 429: "Too Many Requests",
+ 431: "Request Header Fields Too Large",
+ 449: "Retry With", # proprietary MS extension
+ 451: "Unavailable For Legal Reasons",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway",
+ 503: "Service Unavailable",
+ 504: "Gateway Timeout",
+ 505: "HTTP Version Not Supported",
+ 507: "Insufficient Storage",
+ 510: "Not Extended",
}
def wsgi_to_bytes(data):
- """coerce wsgi unicode represented bytes to real ones
-
- """
+ """coerce wsgi unicode represented bytes to real ones"""
if isinstance(data, bytes):
return data
- return data.encode('latin1') # XXX: utf8 fallback?
+ return data.encode("latin1") # XXX: utf8 fallback?
def bytes_to_wsgi(data):
- assert isinstance(data, bytes), 'data must be bytes'
+ assert isinstance(data, bytes), "data must be bytes"
if isinstance(data, str):
return data
else:
- return data.decode('latin1')
+ return data.decode("latin1")
-def quote_header_value(value, extra_chars='', allow_token=True):
+def quote_header_value(value, extra_chars="", allow_token=True):
"""Quote a header value if necessary.
.. versionadded:: 0.5
@@ -199,7 +229,7 @@ def quote_header_value(value, extra_chars='', allow_token=True):
token_chars = _token_chars | set(extra_chars)
if set(value).issubset(token_chars):
return value
- return '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"')
+ return '"%s"' % value.replace("\\", "\\\\").replace('"', '\\"')
def unquote_header_value(value, is_filename=False):
@@ -223,8 +253,8 @@ def unquote_header_value(value, is_filename=False):
# replace sequence below on a UNC path has the effect of turning
# the leading double slash into a single slash and then
# _fix_ie_filename() doesn't work correctly. See #458.
- if not is_filename or value[:2] != '\\\\':
- return value.replace('\\\\', '\\').replace('\\"', '"')
+ if not is_filename or value[:2] != "\\\\":
+ return value.replace("\\\\", "\\").replace('\\"', '"')
return value
@@ -241,8 +271,8 @@ def dump_options_header(header, options):
if value is None:
segments.append(key)
else:
- segments.append('%s=%s' % (key, quote_header_value(value)))
- return '; '.join(segments)
+ segments.append("%s=%s" % (key, quote_header_value(value)))
+ return "; ".join(segments)
def dump_header(iterable, allow_token=True):
@@ -266,14 +296,12 @@ def dump_header(iterable, allow_token=True):
if value is None:
items.append(key)
else:
- items.append('%s=%s' % (
- key,
- quote_header_value(value, allow_token=allow_token)
- ))
+ items.append(
+ "%s=%s" % (key, quote_header_value(value, allow_token=allow_token))
+ )
else:
- items = [quote_header_value(x, allow_token=allow_token)
- for x in iterable]
- return ', '.join(items)
+ items = [quote_header_value(x, allow_token=allow_token) for x in iterable]
+ return ", ".join(items)
def parse_list_header(value):
@@ -337,10 +365,10 @@ def parse_dict_header(value, cls=dict):
# XXX: validate
value = bytes_to_wsgi(value)
for item in _parse_list_header(value):
- if '=' not in item:
+ if "=" not in item:
result[item] = None
continue
- name, value = item.split('=', 1)
+ name, value = item.split("=", 1)
if value[:1] == value[-1:] == '"':
value = unquote_header_value(value[1:-1])
result[name] = value
@@ -369,7 +397,7 @@ def parse_options_header(value, multiple=False):
if multiple=True
"""
if not value:
- return '', {}
+ return "", {}
result = []
@@ -400,9 +428,7 @@ def parse_options_header(value, multiple=False):
continued_encoding = encoding
option = unquote_header_value(option)
if option_value is not None:
- option_value = unquote_header_value(
- option_value,
- option == 'filename')
+ option_value = unquote_header_value(option_value, option == "filename")
if encoding is not None:
option_value = _unquote(option_value).decode(encoding)
if count:
@@ -412,13 +438,13 @@ def parse_options_header(value, multiple=False):
options[option] = options.get(option, "") + option_value
else:
options[option] = option_value
- rest = rest[optmatch.end():]
+ rest = rest[optmatch.end() :]
result.append(options)
if multiple is False:
return tuple(result)
value = rest
- return tuple(result) if result else ('', {})
+ return tuple(result) if result else ("", {})
def parse_accept_header(value, cls=None):
@@ -525,26 +551,27 @@ def parse_authorization_header(value):
auth_type = auth_type.lower()
except ValueError:
return
- if auth_type == b'basic':
+ if auth_type == b"basic":
try:
- username, password = base64.b64decode(auth_info).split(b':', 1)
+ username, password = base64.b64decode(auth_info).split(b":", 1)
except Exception:
return
return Authorization(
- 'basic', {
- 'username': to_unicode(username, _basic_auth_charset),
- 'password': to_unicode(password, _basic_auth_charset)
- }
+ "basic",
+ {
+ "username": to_unicode(username, _basic_auth_charset),
+ "password": to_unicode(password, _basic_auth_charset),
+ },
)
- elif auth_type == b'digest':
+ elif auth_type == b"digest":
auth_map = parse_dict_header(auth_info)
- for key in 'username', 'realm', 'nonce', 'uri', 'response':
+ for key in "username", "realm", "nonce", "uri", "response":
if key not in auth_map:
return
- if 'qop' in auth_map:
- if not auth_map.get('nc') or not auth_map.get('cnonce'):
+ if "qop" in auth_map:
+ if not auth_map.get("nc") or not auth_map.get("cnonce"):
return
- return Authorization('digest', auth_map)
+ return Authorization("digest", auth_map)
def parse_www_authenticate_header(value, on_update=None):
@@ -564,8 +591,7 @@ def parse_www_authenticate_header(value, on_update=None):
auth_type = auth_type.lower()
except (ValueError, AttributeError):
return WWWAuthenticate(value.strip().lower(), on_update=on_update)
- return WWWAuthenticate(auth_type, parse_dict_header(auth_info),
- on_update)
+ return WWWAuthenticate(auth_type, parse_dict_header(auth_info), on_update)
def parse_if_range_header(value):
@@ -591,19 +617,19 @@ def parse_range_header(value, make_inclusive=True):
.. versionadded:: 0.7
"""
- if not value or '=' not in value:
+ if not value or "=" not in value:
return None
ranges = []
last_end = 0
- units, rng = value.split('=', 1)
+ units, rng = value.split("=", 1)
units = units.strip().lower()
- for item in rng.split(','):
+ for item in rng.split(","):
item = item.strip()
- if '-' not in item:
+ if "-" not in item:
return None
- if item.startswith('-'):
+ if item.startswith("-"):
if last_end < 0:
return None
try:
@@ -612,8 +638,8 @@ def parse_range_header(value, make_inclusive=True):
return None
end = None
last_end = -1
- elif '-' in item:
- begin, end = item.split('-', 1)
+ elif "-" in item:
+ begin, end = item.split("-", 1)
begin = begin.strip()
end = end.strip()
if not begin.isdigit():
@@ -650,26 +676,26 @@ def parse_content_range_header(value, on_update=None):
if value is None:
return None
try:
- units, rangedef = (value or '').strip().split(None, 1)
+ units, rangedef = (value or "").strip().split(None, 1)
except ValueError:
return None
- if '/' not in rangedef:
+ if "/" not in rangedef:
return None
- rng, length = rangedef.split('/', 1)
- if length == '*':
+ rng, length = rangedef.split("/", 1)
+ if length == "*":
length = None
elif length.isdigit():
length = int(length)
else:
return None
- if rng == '*':
+ if rng == "*":
return ContentRange(units, None, None, length, on_update=on_update)
- elif '-' not in rng:
+ elif "-" not in rng:
return None
- start, stop = rng.split('-', 1)
+ start, stop = rng.split("-", 1)
try:
start = int(start)
stop = int(stop) + 1
@@ -687,10 +713,10 @@ def quote_etag(etag, weak=False):
:param weak: set to `True` to tag it "weak".
"""
if '"' in etag:
- raise ValueError('invalid etag')
+ raise ValueError("invalid etag")
etag = '"%s"' % etag
if weak:
- etag = 'W/' + etag
+ etag = "W/" + etag
return etag
@@ -709,7 +735,7 @@ def unquote_etag(etag):
return None, None
etag = etag.strip()
weak = False
- if etag.startswith(('W/', 'w/')):
+ if etag.startswith(("W/", "w/")):
weak = True
etag = etag[2:]
if etag[:1] == etag[-1:] == '"':
@@ -734,7 +760,7 @@ def parse_etags(value):
if match is None:
break
is_weak, quoted, raw = match.groups()
- if raw == '*':
+ if raw == "*":
return ETags(star_tag=True)
elif quoted:
raw = quoted
@@ -778,8 +804,7 @@ def parse_date(value):
year += 2000
elif year >= 69 and year <= 99:
year += 1900
- return datetime(*((year,) + t[1:7])) - \
- timedelta(seconds=t[-1] or 0)
+ return datetime(*((year,) + t[1:7])) - timedelta(seconds=t[-1] or 0)
except (ValueError, OverflowError):
return None
@@ -792,12 +817,29 @@ def _dump_date(d, delim):
d = d.utctimetuple()
elif isinstance(d, (integer_types, float)):
d = gmtime(d)
- return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % (
- ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[d.tm_wday],
- d.tm_mday, delim,
- ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
- 'Oct', 'Nov', 'Dec')[d.tm_mon - 1],
- delim, str(d.tm_year), d.tm_hour, d.tm_min, d.tm_sec
+ return "%s, %02d%s%s%s%s %02d:%02d:%02d GMT" % (
+ ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[d.tm_wday],
+ d.tm_mday,
+ delim,
+ (
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ )[d.tm_mon - 1],
+ delim,
+ str(d.tm_year),
+ d.tm_hour,
+ d.tm_min,
+ d.tm_sec,
)
@@ -813,7 +855,7 @@ def cookie_date(expires=None):
:param expires: If provided that date is used, otherwise the current.
"""
- return _dump_date(expires, '-')
+ return _dump_date(expires, "-")
def http_date(timestamp=None):
@@ -827,7 +869,7 @@ def http_date(timestamp=None):
:param timestamp: If provided that date is used, otherwise the current.
"""
- return _dump_date(timestamp, ' ')
+ return _dump_date(timestamp, " ")
def parse_age(value=None):
@@ -868,13 +910,14 @@ def dump_age(age=None):
age = int(age)
if age < 0:
- raise ValueError('age cannot be negative')
+ raise ValueError("age cannot be negative")
return str(age)
-def is_resource_modified(environ, etag=None, data=None, last_modified=None,
- ignore_if_range=True):
+def is_resource_modified(
+ environ, etag=None, data=None, last_modified=None, ignore_if_range=True
+):
"""Convenience method for conditional requests.
:param environ: the WSGI environment of the request to be checked.
@@ -889,8 +932,8 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None,
if etag is None and data is not None:
etag = generate_etag(data)
elif data is not None:
- raise TypeError('both data and etag given')
- if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'):
+ raise TypeError("both data and etag given")
+ if environ["REQUEST_METHOD"] not in ("GET", "HEAD"):
return False
unmodified = False
@@ -903,16 +946,16 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None,
last_modified = last_modified.replace(microsecond=0)
if_range = None
- if not ignore_if_range and 'HTTP_RANGE' in environ:
+ if not ignore_if_range and "HTTP_RANGE" in environ:
# https://tools.ietf.org/html/rfc7233#section-3.2
# A server MUST ignore an If-Range header field received in a request
# that does not contain a Range header field.
- if_range = parse_if_range_header(environ.get('HTTP_IF_RANGE'))
+ if_range = parse_if_range_header(environ.get("HTTP_IF_RANGE"))
if if_range is not None and if_range.date is not None:
modified_since = if_range.date
else:
- modified_since = parse_date(environ.get('HTTP_IF_MODIFIED_SINCE'))
+ modified_since = parse_date(environ.get("HTTP_IF_MODIFIED_SINCE"))
if modified_since and last_modified and last_modified <= modified_since:
unmodified = True
@@ -922,7 +965,7 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None,
if if_range is not None and if_range.etag is not None:
unmodified = parse_etags(if_range.etag).contains(etag)
else:
- if_none_match = parse_etags(environ.get('HTTP_IF_NONE_MATCH'))
+ if_none_match = parse_etags(environ.get("HTTP_IF_NONE_MATCH"))
if if_none_match:
# https://tools.ietf.org/html/rfc7232#section-3.2
# "A recipient MUST use the weak comparison function when comparing
@@ -932,14 +975,14 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None,
# https://tools.ietf.org/html/rfc7232#section-3.1
# "Origin server MUST use the strong comparison function when
# comparing entity-tags for If-Match"
- if_match = parse_etags(environ.get('HTTP_IF_MATCH'))
+ if_match = parse_etags(environ.get("HTTP_IF_MATCH"))
if if_match:
unmodified = not if_match.is_strong(etag)
return not unmodified
-def remove_entity_headers(headers, allowed=('expires', 'content-location')):
+def remove_entity_headers(headers, allowed=("expires", "content-location")):
"""Remove all entity headers from a list or :class:`Headers` object. This
operation works in-place. `Expires` and `Content-Location` headers are
by default not removed. The reason for this is :rfc:`2616` section
@@ -953,8 +996,11 @@ def remove_entity_headers(headers, allowed=('expires', 'content-location')):
they are entity headers.
"""
allowed = set(x.lower() for x in allowed)
- headers[:] = [(key, value) for key, value in headers if
- not is_entity_header(key) or key.lower() in allowed]
+ headers[:] = [
+ (key, value)
+ for key, value in headers
+ if not is_entity_header(key) or key.lower() in allowed
+ ]
def remove_hop_by_hop_headers(headers):
@@ -965,8 +1011,9 @@ def remove_hop_by_hop_headers(headers):
:param headers: a list or :class:`Headers` object.
"""
- headers[:] = [(key, value) for key, value in headers if
- not is_hop_by_hop_header(key)]
+ headers[:] = [
+ (key, value) for key, value in headers if not is_hop_by_hop_header(key)
+ ]
def is_entity_header(header):
@@ -991,7 +1038,7 @@ def is_hop_by_hop_header(header):
return header.lower() in _hop_by_hop_headers
-def parse_cookie(header, charset='utf-8', errors='replace', cls=None):
+def parse_cookie(header, charset="utf-8", errors="replace", cls=None):
"""Parse a cookie. Either from a string or WSGI environ.
Per default encoding errors are ignored. If you want a different behavior
@@ -1011,16 +1058,16 @@ def parse_cookie(header, charset='utf-8', errors='replace', cls=None):
used.
"""
if isinstance(header, dict):
- header = header.get('HTTP_COOKIE', '')
+ header = header.get("HTTP_COOKIE", "")
elif header is None:
- header = ''
+ header = ""
# If the value is an unicode string it's mangled through latin1. This
# is done because on PEP 3333 on Python 3 all headers are assumed latin1
# which however is incorrect for cookies, which are sent in page encoding.
# As a result we
if isinstance(header, text_type):
- header = header.encode('latin1', 'replace')
+ header = header.encode("latin1", "replace")
if cls is None:
cls = TypeConversionDict
@@ -1036,10 +1083,20 @@ def parse_cookie(header, charset='utf-8', errors='replace', cls=None):
return cls(_parse_pairs())
-def dump_cookie(key, value='', max_age=None, expires=None, path='/',
- domain=None, secure=False, httponly=False,
- charset='utf-8', sync_expires=True, max_size=4093,
- samesite=None):
+def dump_cookie(
+ key,
+ value="",
+ max_age=None,
+ expires=None,
+ path="/",
+ domain=None,
+ secure=False,
+ httponly=False,
+ charset="utf-8",
+ sync_expires=True,
+ max_size=4093,
+ samesite=None,
+):
"""Creates a new Set-Cookie header without the ``Set-Cookie`` prefix
The parameters are the same as in the cookie Morsel object in the
Python standard library but it accepts unicode data, too.
@@ -1098,21 +1155,23 @@ def dump_cookie(key, value='', max_age=None, expires=None, path='/',
expires = to_bytes(cookie_date(time() + max_age))
samesite = samesite.title() if samesite else None
- if samesite not in ('Strict', 'Lax', None):
+ if samesite not in ("Strict", "Lax", None):
raise ValueError("invalid SameSite value; must be 'Strict', 'Lax' or None")
- buf = [key + b'=' + _cookie_quote(value)]
+ buf = [key + b"=" + _cookie_quote(value)]
# XXX: In theory all of these parameters that are not marked with `None`
# should be quoted. Because stdlib did not quote it before I did not
# want to introduce quoting there now.
- for k, v, q in ((b'Domain', domain, True),
- (b'Expires', expires, False,),
- (b'Max-Age', max_age, False),
- (b'Secure', secure, None),
- (b'HttpOnly', httponly, None),
- (b'Path', path, False),
- (b'SameSite', samesite, False)):
+ for k, v, q in (
+ (b"Domain", domain, True),
+ (b"Expires", expires, False),
+ (b"Max-Age", max_age, False),
+ (b"Secure", secure, None),
+ (b"HttpOnly", httponly, None),
+ (b"Path", path, False),
+ (b"SameSite", samesite, False),
+ ):
if q is None:
if v:
buf.append(k)
@@ -1126,15 +1185,15 @@ def dump_cookie(key, value='', max_age=None, expires=None, path='/',
v = to_bytes(text_type(v), charset)
if q:
v = _cookie_quote(v)
- tmp += b'=' + v
+ tmp += b"=" + v
buf.append(bytes(tmp))
# The return value will be an incorrectly encoded latin1 header on
# Python 3 for consistency with the headers object and a bytestring
# on Python 2 because that's how the API makes more sense.
- rv = b'; '.join(buf)
+ rv = b"; ".join(buf)
if not PY2:
- rv = rv.decode('latin1')
+ rv = rv.decode("latin1")
# Warn if the final value of the cookie is less than the limit. If the
# cookie is too large, then it may be silently ignored, which can be quite
@@ -1145,16 +1204,16 @@ def dump_cookie(key, value='', max_age=None, expires=None, path='/',
value_size = len(value)
warnings.warn(
'The "{key}" cookie is too large: the value was {value_size} bytes'
- ' but the header required {extra_size} extra bytes. The final size'
- ' was {cookie_size} bytes but the limit is {max_size} bytes.'
- ' Browsers may silently ignore cookies larger than this.'.format(
+ " but the header required {extra_size} extra bytes. The final size"
+ " was {cookie_size} bytes but the limit is {max_size} bytes."
+ " Browsers may silently ignore cookies larger than this.".format(
key=key,
value_size=value_size,
extra_size=cookie_size - value_size,
cookie_size=cookie_size,
- max_size=max_size
+ max_size=max_size,
),
- stacklevel=2
+ stacklevel=2,
)
return rv
@@ -1177,19 +1236,23 @@ def is_byte_range_valid(start, stop, length):
# circular dependency fun
-from werkzeug.datastructures import Accept, HeaderSet, ETags, Authorization, \
- WWWAuthenticate, TypeConversionDict, IfRange, Range, ContentRange, \
- RequestCacheControl
-from werkzeug.urls import iri_to_uri
-
+from .datastructures import Accept
+from .datastructures import Authorization
+from .datastructures import ContentRange
+from .datastructures import ETags
+from .datastructures import HeaderSet
+from .datastructures import IfRange
+from .datastructures import Range
+from .datastructures import RequestCacheControl
+from .datastructures import TypeConversionDict
+from .datastructures import WWWAuthenticate
+from .urls import iri_to_uri
# DEPRECATED
-from werkzeug.datastructures import (
- MIMEAccept as _MIMEAccept,
- CharsetAccept as _CharsetAccept,
- LanguageAccept as _LanguageAccept,
- Headers as _Headers,
-)
+from .datastructures import CharsetAccept as _CharsetAccept
+from .datastructures import Headers as _Headers
+from .datastructures import LanguageAccept as _LanguageAccept
+from .datastructures import MIMEAccept as _MIMEAccept
class MIMEAccept(_MIMEAccept):