Made an application for supporting sustainable local businesses in San Pancho.
Never really got completed, but it has some useful Svelte components for maps that we can reuse.
http://greenspots.dctrl.space
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3059 lines
96 KiB
3059 lines
96 KiB
import base64 |
|
import codecs |
|
import mimetypes |
|
import re |
|
import warnings |
|
from collections.abc import Collection |
|
from collections.abc import MutableSet |
|
from copy import deepcopy |
|
from io import BytesIO |
|
from itertools import repeat |
|
from os import fspath |
|
|
|
from . import exceptions |
|
from ._internal import _make_encode_wrapper |
|
from ._internal import _missing |
|
from .filesystem import get_filesystem_encoding |
|
|
|
|
|
def is_immutable(self): |
|
raise TypeError(f"{type(self).__name__!r} objects are immutable") |
|
|
|
|
|
def iter_multi_items(mapping): |
|
"""Iterates over the items of a mapping yielding keys and values |
|
without dropping any from more complex structures. |
|
""" |
|
if isinstance(mapping, MultiDict): |
|
yield from mapping.items(multi=True) |
|
elif isinstance(mapping, dict): |
|
for key, value in mapping.items(): |
|
if isinstance(value, (tuple, list)): |
|
for v in value: |
|
yield key, v |
|
else: |
|
yield key, value |
|
else: |
|
yield from mapping |
|
|
|
|
|
class ImmutableListMixin: |
|
"""Makes a :class:`list` immutable. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
_hash_cache = None |
|
|
|
def __hash__(self): |
|
if self._hash_cache is not None: |
|
return self._hash_cache |
|
rv = self._hash_cache = hash(tuple(self)) |
|
return rv |
|
|
|
def __reduce_ex__(self, protocol): |
|
return type(self), (list(self),) |
|
|
|
def __delitem__(self, key): |
|
is_immutable(self) |
|
|
|
def __iadd__(self, other): |
|
is_immutable(self) |
|
|
|
def __imul__(self, other): |
|
is_immutable(self) |
|
|
|
def __setitem__(self, key, value): |
|
is_immutable(self) |
|
|
|
def append(self, item): |
|
is_immutable(self) |
|
|
|
def remove(self, item): |
|
is_immutable(self) |
|
|
|
def extend(self, iterable): |
|
is_immutable(self) |
|
|
|
def insert(self, pos, value): |
|
is_immutable(self) |
|
|
|
def pop(self, index=-1): |
|
is_immutable(self) |
|
|
|
def reverse(self): |
|
is_immutable(self) |
|
|
|
def sort(self, key=None, reverse=False): |
|
is_immutable(self) |
|
|
|
|
|
class ImmutableList(ImmutableListMixin, list): |
|
"""An immutable :class:`list`. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({list.__repr__(self)})" |
|
|
|
|
|
class ImmutableDictMixin: |
|
"""Makes a :class:`dict` immutable. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
_hash_cache = None |
|
|
|
@classmethod |
|
def fromkeys(cls, keys, value=None): |
|
instance = super().__new__(cls) |
|
instance.__init__(zip(keys, repeat(value))) |
|
return instance |
|
|
|
def __reduce_ex__(self, protocol): |
|
return type(self), (dict(self),) |
|
|
|
def _iter_hashitems(self): |
|
return self.items() |
|
|
|
def __hash__(self): |
|
if self._hash_cache is not None: |
|
return self._hash_cache |
|
rv = self._hash_cache = hash(frozenset(self._iter_hashitems())) |
|
return rv |
|
|
|
def setdefault(self, key, default=None): |
|
is_immutable(self) |
|
|
|
def update(self, *args, **kwargs): |
|
is_immutable(self) |
|
|
|
def pop(self, key, default=None): |
|
is_immutable(self) |
|
|
|
def popitem(self): |
|
is_immutable(self) |
|
|
|
def __setitem__(self, key, value): |
|
is_immutable(self) |
|
|
|
def __delitem__(self, key): |
|
is_immutable(self) |
|
|
|
def clear(self): |
|
is_immutable(self) |
|
|
|
|
|
class ImmutableMultiDictMixin(ImmutableDictMixin): |
|
"""Makes a :class:`MultiDict` immutable. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
def __reduce_ex__(self, protocol): |
|
return type(self), (list(self.items(multi=True)),) |
|
|
|
def _iter_hashitems(self): |
|
return self.items(multi=True) |
|
|
|
def add(self, key, value): |
|
is_immutable(self) |
|
|
|
def popitemlist(self): |
|
is_immutable(self) |
|
|
|
def poplist(self, key): |
|
is_immutable(self) |
|
|
|
def setlist(self, key, new_list): |
|
is_immutable(self) |
|
|
|
def setlistdefault(self, key, default_list=None): |
|
is_immutable(self) |
|
|
|
|
|
def _calls_update(name): |
|
def oncall(self, *args, **kw): |
|
rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw) |
|
|
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
return rv |
|
|
|
oncall.__name__ = name |
|
return oncall |
|
|
|
|
|
class UpdateDictMixin(dict): |
|
"""Makes dicts call `self.on_update` on modifications. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
on_update = None |
|
|
|
def setdefault(self, key, default=None): |
|
modified = key not in self |
|
rv = super().setdefault(key, default) |
|
if modified and self.on_update is not None: |
|
self.on_update(self) |
|
return rv |
|
|
|
def pop(self, key, default=_missing): |
|
modified = key in self |
|
if default is _missing: |
|
rv = super().pop(key) |
|
else: |
|
rv = super().pop(key, default) |
|
if modified and self.on_update is not None: |
|
self.on_update(self) |
|
return rv |
|
|
|
__setitem__ = _calls_update("__setitem__") |
|
__delitem__ = _calls_update("__delitem__") |
|
clear = _calls_update("clear") |
|
popitem = _calls_update("popitem") |
|
update = _calls_update("update") |
|
|
|
|
|
class TypeConversionDict(dict): |
|
"""Works like a regular dict but the :meth:`get` method can perform |
|
type conversions. :class:`MultiDict` and :class:`CombinedMultiDict` |
|
are subclasses of this class and provide the same feature. |
|
|
|
.. versionadded:: 0.5 |
|
""" |
|
|
|
def get(self, key, default=None, type=None): |
|
"""Return the default value if the requested data doesn't exist. |
|
If `type` is provided and is a callable it should convert the value, |
|
return it or raise a :exc:`ValueError` if that is not possible. In |
|
this case the function will return the default as if the value was not |
|
found: |
|
|
|
>>> d = TypeConversionDict(foo='42', bar='blub') |
|
>>> d.get('foo', type=int) |
|
42 |
|
>>> d.get('bar', -1, type=int) |
|
-1 |
|
|
|
:param key: The key to be looked up. |
|
:param default: The default value to be returned if the key can't |
|
be looked up. If not further specified `None` is |
|
returned. |
|
:param type: A callable that is used to cast the value in the |
|
:class:`MultiDict`. If a :exc:`ValueError` is raised |
|
by this callable the default value is returned. |
|
""" |
|
try: |
|
rv = self[key] |
|
except KeyError: |
|
return default |
|
if type is not None: |
|
try: |
|
rv = type(rv) |
|
except ValueError: |
|
rv = default |
|
return rv |
|
|
|
|
|
class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict): |
|
"""Works like a :class:`TypeConversionDict` but does not support |
|
modifications. |
|
|
|
.. versionadded:: 0.5 |
|
""" |
|
|
|
def copy(self): |
|
"""Return a shallow mutable copy of this object. Keep in mind that |
|
the standard library's :func:`copy` function is a no-op for this class |
|
like for any other python immutable type (eg: :class:`tuple`). |
|
""" |
|
return TypeConversionDict(self) |
|
|
|
def __copy__(self): |
|
return self |
|
|
|
|
|
class MultiDict(TypeConversionDict): |
|
"""A :class:`MultiDict` is a dictionary subclass customized to deal with |
|
multiple values for the same key which is for example used by the parsing |
|
functions in the wrappers. This is necessary because some HTML form |
|
elements pass multiple values for the same key. |
|
|
|
:class:`MultiDict` implements all standard dictionary methods. |
|
Internally, it saves all values for a key as a list, but the standard dict |
|
access methods will only return the first value for a key. If you want to |
|
gain access to the other values, too, you have to use the `list` methods as |
|
explained below. |
|
|
|
Basic Usage: |
|
|
|
>>> d = MultiDict([('a', 'b'), ('a', 'c')]) |
|
>>> d |
|
MultiDict([('a', 'b'), ('a', 'c')]) |
|
>>> d['a'] |
|
'b' |
|
>>> d.getlist('a') |
|
['b', 'c'] |
|
>>> 'a' in d |
|
True |
|
|
|
It behaves like a normal dict thus all dict functions will only return the |
|
first value when multiple values for one key are found. |
|
|
|
From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a |
|
subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will |
|
render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP |
|
exceptions. |
|
|
|
A :class:`MultiDict` can be constructed from an iterable of |
|
``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2 |
|
onwards some keyword parameters. |
|
|
|
:param mapping: the initial value for the :class:`MultiDict`. Either a |
|
regular dict, an iterable of ``(key, value)`` tuples |
|
or `None`. |
|
""" |
|
|
|
def __init__(self, mapping=None): |
|
if isinstance(mapping, MultiDict): |
|
dict.__init__(self, ((k, l[:]) for k, l in mapping.lists())) |
|
elif isinstance(mapping, dict): |
|
tmp = {} |
|
for key, value in mapping.items(): |
|
if isinstance(value, (tuple, list)): |
|
if len(value) == 0: |
|
continue |
|
value = list(value) |
|
else: |
|
value = [value] |
|
tmp[key] = value |
|
dict.__init__(self, tmp) |
|
else: |
|
tmp = {} |
|
for key, value in mapping or (): |
|
tmp.setdefault(key, []).append(value) |
|
dict.__init__(self, tmp) |
|
|
|
def __getstate__(self): |
|
return dict(self.lists()) |
|
|
|
def __setstate__(self, value): |
|
dict.clear(self) |
|
dict.update(self, value) |
|
|
|
def __iter__(self): |
|
# Work around https://bugs.python.org/issue43246. |
|
# (`return super().__iter__()` also works here, which makes this look |
|
# even more like it should be a no-op, yet it isn't.) |
|
return dict.__iter__(self) |
|
|
|
def __getitem__(self, key): |
|
"""Return the first data value for this key; |
|
raises KeyError if not found. |
|
|
|
:param key: The key to be looked up. |
|
:raise KeyError: if the key does not exist. |
|
""" |
|
|
|
if key in self: |
|
lst = dict.__getitem__(self, key) |
|
if len(lst) > 0: |
|
return lst[0] |
|
raise exceptions.BadRequestKeyError(key) |
|
|
|
def __setitem__(self, key, value): |
|
"""Like :meth:`add` but removes an existing key first. |
|
|
|
:param key: the key for the value. |
|
:param value: the value to set. |
|
""" |
|
dict.__setitem__(self, key, [value]) |
|
|
|
def add(self, key, value): |
|
"""Adds a new value for the key. |
|
|
|
.. versionadded:: 0.6 |
|
|
|
:param key: the key for the value. |
|
:param value: the value to add. |
|
""" |
|
dict.setdefault(self, key, []).append(value) |
|
|
|
def getlist(self, key, type=None): |
|
"""Return the list of items for a given key. If that key is not in the |
|
`MultiDict`, the return value will be an empty list. Just like `get`, |
|
`getlist` accepts a `type` parameter. All items will be converted |
|
with the callable defined there. |
|
|
|
:param key: The key to be looked up. |
|
:param type: A callable that is used to cast the value in the |
|
:class:`MultiDict`. If a :exc:`ValueError` is raised |
|
by this callable the value will be removed from the list. |
|
:return: a :class:`list` of all the values for the key. |
|
""" |
|
try: |
|
rv = dict.__getitem__(self, key) |
|
except KeyError: |
|
return [] |
|
if type is None: |
|
return list(rv) |
|
result = [] |
|
for item in rv: |
|
try: |
|
result.append(type(item)) |
|
except ValueError: |
|
pass |
|
return result |
|
|
|
def setlist(self, key, new_list): |
|
"""Remove the old values for a key and add new ones. Note that the list |
|
you pass the values in will be shallow-copied before it is inserted in |
|
the dictionary. |
|
|
|
>>> d = MultiDict() |
|
>>> d.setlist('foo', ['1', '2']) |
|
>>> d['foo'] |
|
'1' |
|
>>> d.getlist('foo') |
|
['1', '2'] |
|
|
|
:param key: The key for which the values are set. |
|
:param new_list: An iterable with the new values for the key. Old values |
|
are removed first. |
|
""" |
|
dict.__setitem__(self, key, list(new_list)) |
|
|
|
def setdefault(self, key, default=None): |
|
"""Returns the value for the key if it is in the dict, otherwise it |
|
returns `default` and sets that value for `key`. |
|
|
|
:param key: The key to be looked up. |
|
:param default: The default value to be returned if the key is not |
|
in the dict. If not further specified it's `None`. |
|
""" |
|
if key not in self: |
|
self[key] = default |
|
else: |
|
default = self[key] |
|
return default |
|
|
|
def setlistdefault(self, key, default_list=None): |
|
"""Like `setdefault` but sets multiple values. The list returned |
|
is not a copy, but the list that is actually used internally. This |
|
means that you can put new values into the dict by appending items |
|
to the list: |
|
|
|
>>> d = MultiDict({"foo": 1}) |
|
>>> d.setlistdefault("foo").extend([2, 3]) |
|
>>> d.getlist("foo") |
|
[1, 2, 3] |
|
|
|
:param key: The key to be looked up. |
|
:param default_list: An iterable of default values. It is either copied |
|
(in case it was a list) or converted into a list |
|
before returned. |
|
:return: a :class:`list` |
|
""" |
|
if key not in self: |
|
default_list = list(default_list or ()) |
|
dict.__setitem__(self, key, default_list) |
|
else: |
|
default_list = dict.__getitem__(self, key) |
|
return default_list |
|
|
|
def items(self, multi=False): |
|
"""Return an iterator of ``(key, value)`` pairs. |
|
|
|
:param multi: If set to `True` the iterator returned will have a pair |
|
for each value of each key. Otherwise it will only |
|
contain pairs for the first value of each key. |
|
""" |
|
for key, values in dict.items(self): |
|
if multi: |
|
for value in values: |
|
yield key, value |
|
else: |
|
yield key, values[0] |
|
|
|
def lists(self): |
|
"""Return a iterator of ``(key, values)`` pairs, where values is the list |
|
of all values associated with the key.""" |
|
for key, values in dict.items(self): |
|
yield key, list(values) |
|
|
|
def values(self): |
|
"""Returns an iterator of the first value on every key's value list.""" |
|
for values in dict.values(self): |
|
yield values[0] |
|
|
|
def listvalues(self): |
|
"""Return an iterator of all values associated with a key. Zipping |
|
:meth:`keys` and this is the same as calling :meth:`lists`: |
|
|
|
>>> d = MultiDict({"foo": [1, 2, 3]}) |
|
>>> zip(d.keys(), d.listvalues()) == d.lists() |
|
True |
|
""" |
|
return dict.values(self) |
|
|
|
def copy(self): |
|
"""Return a shallow copy of this object.""" |
|
return self.__class__(self) |
|
|
|
def deepcopy(self, memo=None): |
|
"""Return a deep copy of this object.""" |
|
return self.__class__(deepcopy(self.to_dict(flat=False), memo)) |
|
|
|
def to_dict(self, flat=True): |
|
"""Return the contents as regular dict. If `flat` is `True` the |
|
returned dict will only have the first item present, if `flat` is |
|
`False` all values will be returned as lists. |
|
|
|
:param flat: If set to `False` the dict returned will have lists |
|
with all the values in it. Otherwise it will only |
|
contain the first value for each key. |
|
:return: a :class:`dict` |
|
""" |
|
if flat: |
|
return dict(self.items()) |
|
return dict(self.lists()) |
|
|
|
def update(self, mapping): |
|
"""update() extends rather than replaces existing key lists: |
|
|
|
>>> a = MultiDict({'x': 1}) |
|
>>> b = MultiDict({'x': 2, 'y': 3}) |
|
>>> a.update(b) |
|
>>> a |
|
MultiDict([('y', 3), ('x', 1), ('x', 2)]) |
|
|
|
If the value list for a key in ``other_dict`` is empty, no new values |
|
will be added to the dict and the key will not be created: |
|
|
|
>>> x = {'empty_list': []} |
|
>>> y = MultiDict() |
|
>>> y.update(x) |
|
>>> y |
|
MultiDict([]) |
|
""" |
|
for key, value in iter_multi_items(mapping): |
|
MultiDict.add(self, key, value) |
|
|
|
def pop(self, key, default=_missing): |
|
"""Pop the first item for a list on the dict. Afterwards the |
|
key is removed from the dict, so additional values are discarded: |
|
|
|
>>> d = MultiDict({"foo": [1, 2, 3]}) |
|
>>> d.pop("foo") |
|
1 |
|
>>> "foo" in d |
|
False |
|
|
|
:param key: the key to pop. |
|
:param default: if provided the value to return if the key was |
|
not in the dictionary. |
|
""" |
|
try: |
|
lst = dict.pop(self, key) |
|
|
|
if len(lst) == 0: |
|
raise exceptions.BadRequestKeyError(key) |
|
|
|
return lst[0] |
|
except KeyError: |
|
if default is not _missing: |
|
return default |
|
|
|
raise exceptions.BadRequestKeyError(key) from None |
|
|
|
def popitem(self): |
|
"""Pop an item from the dict.""" |
|
try: |
|
item = dict.popitem(self) |
|
|
|
if len(item[1]) == 0: |
|
raise exceptions.BadRequestKeyError(item[0]) |
|
|
|
return (item[0], item[1][0]) |
|
except KeyError as e: |
|
raise exceptions.BadRequestKeyError(e.args[0]) from None |
|
|
|
def poplist(self, key): |
|
"""Pop the list for a key from the dict. If the key is not in the dict |
|
an empty list is returned. |
|
|
|
.. versionchanged:: 0.5 |
|
If the key does no longer exist a list is returned instead of |
|
raising an error. |
|
""" |
|
return dict.pop(self, key, []) |
|
|
|
def popitemlist(self): |
|
"""Pop a ``(key, list)`` tuple from the dict.""" |
|
try: |
|
return dict.popitem(self) |
|
except KeyError as e: |
|
raise exceptions.BadRequestKeyError(e.args[0]) from None |
|
|
|
def __copy__(self): |
|
return self.copy() |
|
|
|
def __deepcopy__(self, memo): |
|
return self.deepcopy(memo=memo) |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({list(self.items(multi=True))!r})" |
|
|
|
|
|
class _omd_bucket: |
|
"""Wraps values in the :class:`OrderedMultiDict`. This makes it |
|
possible to keep an order over multiple different keys. It requires |
|
a lot of extra memory and slows down access a lot, but makes it |
|
possible to access elements in O(1) and iterate in O(n). |
|
""" |
|
|
|
__slots__ = ("prev", "key", "value", "next") |
|
|
|
def __init__(self, omd, key, value): |
|
self.prev = omd._last_bucket |
|
self.key = key |
|
self.value = value |
|
self.next = None |
|
|
|
if omd._first_bucket is None: |
|
omd._first_bucket = self |
|
if omd._last_bucket is not None: |
|
omd._last_bucket.next = self |
|
omd._last_bucket = self |
|
|
|
def unlink(self, omd): |
|
if self.prev: |
|
self.prev.next = self.next |
|
if self.next: |
|
self.next.prev = self.prev |
|
if omd._first_bucket is self: |
|
omd._first_bucket = self.next |
|
if omd._last_bucket is self: |
|
omd._last_bucket = self.prev |
|
|
|
|
|
class OrderedMultiDict(MultiDict): |
|
"""Works like a regular :class:`MultiDict` but preserves the |
|
order of the fields. To convert the ordered multi dict into a |
|
list you can use the :meth:`items` method and pass it ``multi=True``. |
|
|
|
In general an :class:`OrderedMultiDict` is an order of magnitude |
|
slower than a :class:`MultiDict`. |
|
|
|
.. admonition:: note |
|
|
|
Due to a limitation in Python you cannot convert an ordered |
|
multi dict into a regular dict by using ``dict(multidict)``. |
|
Instead you have to use the :meth:`to_dict` method, otherwise |
|
the internal bucket objects are exposed. |
|
""" |
|
|
|
def __init__(self, mapping=None): |
|
dict.__init__(self) |
|
self._first_bucket = self._last_bucket = None |
|
if mapping is not None: |
|
OrderedMultiDict.update(self, mapping) |
|
|
|
def __eq__(self, other): |
|
if not isinstance(other, MultiDict): |
|
return NotImplemented |
|
if isinstance(other, OrderedMultiDict): |
|
iter1 = iter(self.items(multi=True)) |
|
iter2 = iter(other.items(multi=True)) |
|
try: |
|
for k1, v1 in iter1: |
|
k2, v2 = next(iter2) |
|
if k1 != k2 or v1 != v2: |
|
return False |
|
except StopIteration: |
|
return False |
|
try: |
|
next(iter2) |
|
except StopIteration: |
|
return True |
|
return False |
|
if len(self) != len(other): |
|
return False |
|
for key, values in self.lists(): |
|
if other.getlist(key) != values: |
|
return False |
|
return True |
|
|
|
__hash__ = None |
|
|
|
def __reduce_ex__(self, protocol): |
|
return type(self), (list(self.items(multi=True)),) |
|
|
|
def __getstate__(self): |
|
return list(self.items(multi=True)) |
|
|
|
def __setstate__(self, values): |
|
dict.clear(self) |
|
for key, value in values: |
|
self.add(key, value) |
|
|
|
def __getitem__(self, key): |
|
if key in self: |
|
return dict.__getitem__(self, key)[0].value |
|
raise exceptions.BadRequestKeyError(key) |
|
|
|
def __setitem__(self, key, value): |
|
self.poplist(key) |
|
self.add(key, value) |
|
|
|
def __delitem__(self, key): |
|
self.pop(key) |
|
|
|
def keys(self): |
|
return (key for key, value in self.items()) |
|
|
|
def __iter__(self): |
|
return iter(self.keys()) |
|
|
|
def values(self): |
|
return (value for key, value in self.items()) |
|
|
|
def items(self, multi=False): |
|
ptr = self._first_bucket |
|
if multi: |
|
while ptr is not None: |
|
yield ptr.key, ptr.value |
|
ptr = ptr.next |
|
else: |
|
returned_keys = set() |
|
while ptr is not None: |
|
if ptr.key not in returned_keys: |
|
returned_keys.add(ptr.key) |
|
yield ptr.key, ptr.value |
|
ptr = ptr.next |
|
|
|
def lists(self): |
|
returned_keys = set() |
|
ptr = self._first_bucket |
|
while ptr is not None: |
|
if ptr.key not in returned_keys: |
|
yield ptr.key, self.getlist(ptr.key) |
|
returned_keys.add(ptr.key) |
|
ptr = ptr.next |
|
|
|
def listvalues(self): |
|
for _key, values in self.lists(): |
|
yield values |
|
|
|
def add(self, key, value): |
|
dict.setdefault(self, key, []).append(_omd_bucket(self, key, value)) |
|
|
|
def getlist(self, key, type=None): |
|
try: |
|
rv = dict.__getitem__(self, key) |
|
except KeyError: |
|
return [] |
|
if type is None: |
|
return [x.value for x in rv] |
|
result = [] |
|
for item in rv: |
|
try: |
|
result.append(type(item.value)) |
|
except ValueError: |
|
pass |
|
return result |
|
|
|
def setlist(self, key, new_list): |
|
self.poplist(key) |
|
for value in new_list: |
|
self.add(key, value) |
|
|
|
def setlistdefault(self, key, default_list=None): |
|
raise TypeError("setlistdefault is unsupported for ordered multi dicts") |
|
|
|
def update(self, mapping): |
|
for key, value in iter_multi_items(mapping): |
|
OrderedMultiDict.add(self, key, value) |
|
|
|
def poplist(self, key): |
|
buckets = dict.pop(self, key, ()) |
|
for bucket in buckets: |
|
bucket.unlink(self) |
|
return [x.value for x in buckets] |
|
|
|
def pop(self, key, default=_missing): |
|
try: |
|
buckets = dict.pop(self, key) |
|
except KeyError: |
|
if default is not _missing: |
|
return default |
|
|
|
raise exceptions.BadRequestKeyError(key) from None |
|
|
|
for bucket in buckets: |
|
bucket.unlink(self) |
|
|
|
return buckets[0].value |
|
|
|
def popitem(self): |
|
try: |
|
key, buckets = dict.popitem(self) |
|
except KeyError as e: |
|
raise exceptions.BadRequestKeyError(e.args[0]) from None |
|
|
|
for bucket in buckets: |
|
bucket.unlink(self) |
|
|
|
return key, buckets[0].value |
|
|
|
def popitemlist(self): |
|
try: |
|
key, buckets = dict.popitem(self) |
|
except KeyError as e: |
|
raise exceptions.BadRequestKeyError(e.args[0]) from None |
|
|
|
for bucket in buckets: |
|
bucket.unlink(self) |
|
|
|
return key, [x.value for x in buckets] |
|
|
|
|
|
def _options_header_vkw(value, kw): |
|
return http.dump_options_header( |
|
value, {k.replace("_", "-"): v for k, v in kw.items()} |
|
) |
|
|
|
|
|
def _unicodify_header_value(value): |
|
if isinstance(value, bytes): |
|
value = value.decode("latin-1") |
|
if not isinstance(value, str): |
|
value = str(value) |
|
return value |
|
|
|
|
|
class Headers: |
|
"""An object that stores some headers. It has a dict-like interface, |
|
but is ordered, can store the same key multiple times, and iterating |
|
yields ``(key, value)`` pairs instead of only keys. |
|
|
|
This data structure is useful if you want a nicer way to handle WSGI |
|
headers which are stored as tuples in a list. |
|
|
|
From Werkzeug 0.3 onwards, the :exc:`KeyError` raised by this class is |
|
also a subclass of the :class:`~exceptions.BadRequest` HTTP exception |
|
and will render a page for a ``400 BAD REQUEST`` if caught in a |
|
catch-all for HTTP exceptions. |
|
|
|
Headers is mostly compatible with the Python :class:`wsgiref.headers.Headers` |
|
class, with the exception of `__getitem__`. :mod:`wsgiref` will return |
|
`None` for ``headers['missing']``, whereas :class:`Headers` will raise |
|
a :class:`KeyError`. |
|
|
|
To create a new :class:`Headers` object pass it a list or dict of headers |
|
which are used as default values. This does not reuse the list passed |
|
to the constructor for internal usage. |
|
|
|
:param defaults: The list of default values for the :class:`Headers`. |
|
|
|
.. versionchanged:: 0.9 |
|
This data structure now stores unicode values similar to how the |
|
multi dicts do it. The main difference is that bytes can be set as |
|
well which will automatically be latin1 decoded. |
|
|
|
.. versionchanged:: 0.9 |
|
The :meth:`linked` function was removed without replacement as it |
|
was an API that does not support the changes to the encoding model. |
|
""" |
|
|
|
def __init__(self, defaults=None): |
|
self._list = [] |
|
if defaults is not None: |
|
if isinstance(defaults, (list, Headers)): |
|
self._list.extend(defaults) |
|
else: |
|
self.extend(defaults) |
|
|
|
def __getitem__(self, key, _get_mode=False): |
|
if not _get_mode: |
|
if isinstance(key, int): |
|
return self._list[key] |
|
elif isinstance(key, slice): |
|
return self.__class__(self._list[key]) |
|
if not isinstance(key, str): |
|
raise exceptions.BadRequestKeyError(key) |
|
ikey = key.lower() |
|
for k, v in self._list: |
|
if k.lower() == ikey: |
|
return v |
|
# micro optimization: if we are in get mode we will catch that |
|
# exception one stack level down so we can raise a standard |
|
# key error instead of our special one. |
|
if _get_mode: |
|
raise KeyError() |
|
raise exceptions.BadRequestKeyError(key) |
|
|
|
def __eq__(self, other): |
|
def lowered(item): |
|
return (item[0].lower(),) + item[1:] |
|
|
|
return other.__class__ is self.__class__ and set( |
|
map(lowered, other._list) |
|
) == set(map(lowered, self._list)) |
|
|
|
__hash__ = None |
|
|
|
def get(self, key, default=None, type=None, as_bytes=False): |
|
"""Return the default value if the requested data doesn't exist. |
|
If `type` is provided and is a callable it should convert the value, |
|
return it or raise a :exc:`ValueError` if that is not possible. In |
|
this case the function will return the default as if the value was not |
|
found: |
|
|
|
>>> d = Headers([('Content-Length', '42')]) |
|
>>> d.get('Content-Length', type=int) |
|
42 |
|
|
|
.. versionadded:: 0.9 |
|
Added support for `as_bytes`. |
|
|
|
:param key: The key to be looked up. |
|
:param default: The default value to be returned if the key can't |
|
be looked up. If not further specified `None` is |
|
returned. |
|
:param type: A callable that is used to cast the value in the |
|
:class:`Headers`. If a :exc:`ValueError` is raised |
|
by this callable the default value is returned. |
|
:param as_bytes: return bytes instead of strings. |
|
""" |
|
try: |
|
rv = self.__getitem__(key, _get_mode=True) |
|
except KeyError: |
|
return default |
|
if as_bytes: |
|
rv = rv.encode("latin1") |
|
if type is None: |
|
return rv |
|
try: |
|
return type(rv) |
|
except ValueError: |
|
return default |
|
|
|
def getlist(self, key, type=None, as_bytes=False): |
|
"""Return the list of items for a given key. If that key is not in the |
|
:class:`Headers`, the return value will be an empty list. Just like |
|
:meth:`get`, :meth:`getlist` accepts a `type` parameter. All items will |
|
be converted with the callable defined there. |
|
|
|
.. versionadded:: 0.9 |
|
Added support for `as_bytes`. |
|
|
|
:param key: The key to be looked up. |
|
:param type: A callable that is used to cast the value in the |
|
:class:`Headers`. If a :exc:`ValueError` is raised |
|
by this callable the value will be removed from the list. |
|
:return: a :class:`list` of all the values for the key. |
|
:param as_bytes: return bytes instead of strings. |
|
""" |
|
ikey = key.lower() |
|
result = [] |
|
for k, v in self: |
|
if k.lower() == ikey: |
|
if as_bytes: |
|
v = v.encode("latin1") |
|
if type is not None: |
|
try: |
|
v = type(v) |
|
except ValueError: |
|
continue |
|
result.append(v) |
|
return result |
|
|
|
def get_all(self, name): |
|
"""Return a list of all the values for the named field. |
|
|
|
This method is compatible with the :mod:`wsgiref` |
|
:meth:`~wsgiref.headers.Headers.get_all` method. |
|
""" |
|
return self.getlist(name) |
|
|
|
def items(self, lower=False): |
|
for key, value in self: |
|
if lower: |
|
key = key.lower() |
|
yield key, value |
|
|
|
def keys(self, lower=False): |
|
for key, _ in self.items(lower): |
|
yield key |
|
|
|
def values(self): |
|
for _, value in self.items(): |
|
yield value |
|
|
|
def extend(self, *args, **kwargs): |
|
"""Extend headers in this object with items from another object |
|
containing header items as well as keyword arguments. |
|
|
|
To replace existing keys instead of extending, use |
|
:meth:`update` instead. |
|
|
|
If provided, the first argument can be another :class:`Headers` |
|
object, a :class:`MultiDict`, :class:`dict`, or iterable of |
|
pairs. |
|
|
|
.. versionchanged:: 1.0 |
|
Support :class:`MultiDict`. Allow passing ``kwargs``. |
|
""" |
|
if len(args) > 1: |
|
raise TypeError(f"update expected at most 1 arguments, got {len(args)}") |
|
|
|
if args: |
|
for key, value in iter_multi_items(args[0]): |
|
self.add(key, value) |
|
|
|
for key, value in iter_multi_items(kwargs): |
|
self.add(key, value) |
|
|
|
def __delitem__(self, key, _index_operation=True): |
|
if _index_operation and isinstance(key, (int, slice)): |
|
del self._list[key] |
|
return |
|
key = key.lower() |
|
new = [] |
|
for k, v in self._list: |
|
if k.lower() != key: |
|
new.append((k, v)) |
|
self._list[:] = new |
|
|
|
def remove(self, key): |
|
"""Remove a key. |
|
|
|
:param key: The key to be removed. |
|
""" |
|
return self.__delitem__(key, _index_operation=False) |
|
|
|
def pop(self, key=None, default=_missing): |
|
"""Removes and returns a key or index. |
|
|
|
:param key: The key to be popped. If this is an integer the item at |
|
that position is removed, if it's a string the value for |
|
that key is. If the key is omitted or `None` the last |
|
item is removed. |
|
:return: an item. |
|
""" |
|
if key is None: |
|
return self._list.pop() |
|
if isinstance(key, int): |
|
return self._list.pop(key) |
|
try: |
|
rv = self[key] |
|
self.remove(key) |
|
except KeyError: |
|
if default is not _missing: |
|
return default |
|
raise |
|
return rv |
|
|
|
def popitem(self): |
|
"""Removes a key or index and returns a (key, value) item.""" |
|
return self.pop() |
|
|
|
def __contains__(self, key): |
|
"""Check if a key is present.""" |
|
try: |
|
self.__getitem__(key, _get_mode=True) |
|
except KeyError: |
|
return False |
|
return True |
|
|
|
def has_key(self, key): |
|
""" |
|
.. deprecated:: 2.0 |
|
Will be removed in Werkzeug 2.1. Use ``key in data`` |
|
instead. |
|
""" |
|
warnings.warn( |
|
"'has_key' is deprecated and will be removed in Werkzeug" |
|
" 2.1. Use 'key in data' instead.", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
return key in self |
|
|
|
def __iter__(self): |
|
"""Yield ``(key, value)`` tuples.""" |
|
return iter(self._list) |
|
|
|
def __len__(self): |
|
return len(self._list) |
|
|
|
def add(self, _key, _value, **kw): |
|
"""Add a new header tuple to the list. |
|
|
|
Keyword arguments can specify additional parameters for the header |
|
value, with underscores converted to dashes:: |
|
|
|
>>> d = Headers() |
|
>>> d.add('Content-Type', 'text/plain') |
|
>>> d.add('Content-Disposition', 'attachment', filename='foo.png') |
|
|
|
The keyword argument dumping uses :func:`dump_options_header` |
|
behind the scenes. |
|
|
|
.. versionadded:: 0.4.1 |
|
keyword arguments were added for :mod:`wsgiref` compatibility. |
|
""" |
|
if kw: |
|
_value = _options_header_vkw(_value, kw) |
|
_key = _unicodify_header_value(_key) |
|
_value = _unicodify_header_value(_value) |
|
self._validate_value(_value) |
|
self._list.append((_key, _value)) |
|
|
|
def _validate_value(self, value): |
|
if not isinstance(value, str): |
|
raise TypeError("Value should be a string.") |
|
if "\n" in value or "\r" in value: |
|
raise ValueError( |
|
"Detected newline in header value. This is " |
|
"a potential security problem" |
|
) |
|
|
|
def add_header(self, _key, _value, **_kw): |
|
"""Add a new header tuple to the list. |
|
|
|
An alias for :meth:`add` for compatibility with the :mod:`wsgiref` |
|
:meth:`~wsgiref.headers.Headers.add_header` method. |
|
""" |
|
self.add(_key, _value, **_kw) |
|
|
|
def clear(self): |
|
"""Clears all headers.""" |
|
del self._list[:] |
|
|
|
def set(self, _key, _value, **kw): |
|
"""Remove all header tuples for `key` and add a new one. The newly |
|
added key either appears at the end of the list if there was no |
|
entry or replaces the first one. |
|
|
|
Keyword arguments can specify additional parameters for the header |
|
value, with underscores converted to dashes. See :meth:`add` for |
|
more information. |
|
|
|
.. versionchanged:: 0.6.1 |
|
:meth:`set` now accepts the same arguments as :meth:`add`. |
|
|
|
:param key: The key to be inserted. |
|
:param value: The value to be inserted. |
|
""" |
|
if kw: |
|
_value = _options_header_vkw(_value, kw) |
|
_key = _unicodify_header_value(_key) |
|
_value = _unicodify_header_value(_value) |
|
self._validate_value(_value) |
|
if not self._list: |
|
self._list.append((_key, _value)) |
|
return |
|
listiter = iter(self._list) |
|
ikey = _key.lower() |
|
for idx, (old_key, _old_value) in enumerate(listiter): |
|
if old_key.lower() == ikey: |
|
# replace first occurrence |
|
self._list[idx] = (_key, _value) |
|
break |
|
else: |
|
self._list.append((_key, _value)) |
|
return |
|
self._list[idx + 1 :] = [t for t in listiter if t[0].lower() != ikey] |
|
|
|
def setlist(self, key, values): |
|
"""Remove any existing values for a header and add new ones. |
|
|
|
:param key: The header key to set. |
|
:param values: An iterable of values to set for the key. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
if values: |
|
values_iter = iter(values) |
|
self.set(key, next(values_iter)) |
|
|
|
for value in values_iter: |
|
self.add(key, value) |
|
else: |
|
self.remove(key) |
|
|
|
def setdefault(self, key, default): |
|
"""Return the first value for the key if it is in the headers, |
|
otherwise set the header to the value given by ``default`` and |
|
return that. |
|
|
|
:param key: The header key to get. |
|
:param default: The value to set for the key if it is not in the |
|
headers. |
|
""" |
|
if key in self: |
|
return self[key] |
|
|
|
self.set(key, default) |
|
return default |
|
|
|
def setlistdefault(self, key, default): |
|
"""Return the list of values for the key if it is in the |
|
headers, otherwise set the header to the list of values given |
|
by ``default`` and return that. |
|
|
|
Unlike :meth:`MultiDict.setlistdefault`, modifying the returned |
|
list will not affect the headers. |
|
|
|
:param key: The header key to get. |
|
:param default: An iterable of values to set for the key if it |
|
is not in the headers. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
if key not in self: |
|
self.setlist(key, default) |
|
|
|
return self.getlist(key) |
|
|
|
def __setitem__(self, key, value): |
|
"""Like :meth:`set` but also supports index/slice based setting.""" |
|
if isinstance(key, (slice, int)): |
|
if isinstance(key, int): |
|
value = [value] |
|
value = [ |
|
(_unicodify_header_value(k), _unicodify_header_value(v)) |
|
for (k, v) in value |
|
] |
|
for (_, v) in value: |
|
self._validate_value(v) |
|
if isinstance(key, int): |
|
self._list[key] = value[0] |
|
else: |
|
self._list[key] = value |
|
else: |
|
self.set(key, value) |
|
|
|
def update(self, *args, **kwargs): |
|
"""Replace headers in this object with items from another |
|
headers object and keyword arguments. |
|
|
|
To extend existing keys instead of replacing, use :meth:`extend` |
|
instead. |
|
|
|
If provided, the first argument can be another :class:`Headers` |
|
object, a :class:`MultiDict`, :class:`dict`, or iterable of |
|
pairs. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
if len(args) > 1: |
|
raise TypeError(f"update expected at most 1 arguments, got {len(args)}") |
|
|
|
if args: |
|
mapping = args[0] |
|
|
|
if isinstance(mapping, (Headers, MultiDict)): |
|
for key in mapping.keys(): |
|
self.setlist(key, mapping.getlist(key)) |
|
elif isinstance(mapping, dict): |
|
for key, value in mapping.items(): |
|
if isinstance(value, (list, tuple)): |
|
self.setlist(key, value) |
|
else: |
|
self.set(key, value) |
|
else: |
|
for key, value in mapping: |
|
self.set(key, value) |
|
|
|
for key, value in kwargs.items(): |
|
if isinstance(value, (list, tuple)): |
|
self.setlist(key, value) |
|
else: |
|
self.set(key, value) |
|
|
|
def to_wsgi_list(self): |
|
"""Convert the headers into a list suitable for WSGI. |
|
|
|
:return: list |
|
""" |
|
return list(self) |
|
|
|
def copy(self): |
|
return self.__class__(self._list) |
|
|
|
def __copy__(self): |
|
return self.copy() |
|
|
|
def __str__(self): |
|
"""Returns formatted headers suitable for HTTP transmission.""" |
|
strs = [] |
|
for key, value in self.to_wsgi_list(): |
|
strs.append(f"{key}: {value}") |
|
strs.append("\r\n") |
|
return "\r\n".join(strs) |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({list(self)!r})" |
|
|
|
|
|
class ImmutableHeadersMixin: |
|
"""Makes a :class:`Headers` immutable. We do not mark them as |
|
hashable though since the only usecase for this datastructure |
|
in Werkzeug is a view on a mutable structure. |
|
|
|
.. versionadded:: 0.5 |
|
|
|
:private: |
|
""" |
|
|
|
def __delitem__(self, key, **kwargs): |
|
is_immutable(self) |
|
|
|
def __setitem__(self, key, value): |
|
is_immutable(self) |
|
|
|
def set(self, _key, _value, **kw): |
|
is_immutable(self) |
|
|
|
def setlist(self, key, values): |
|
is_immutable(self) |
|
|
|
def add(self, _key, _value, **kw): |
|
is_immutable(self) |
|
|
|
def add_header(self, _key, _value, **_kw): |
|
is_immutable(self) |
|
|
|
def remove(self, key): |
|
is_immutable(self) |
|
|
|
def extend(self, *args, **kwargs): |
|
is_immutable(self) |
|
|
|
def update(self, *args, **kwargs): |
|
is_immutable(self) |
|
|
|
def insert(self, pos, value): |
|
is_immutable(self) |
|
|
|
def pop(self, key=None, default=_missing): |
|
is_immutable(self) |
|
|
|
def popitem(self): |
|
is_immutable(self) |
|
|
|
def setdefault(self, key, default): |
|
is_immutable(self) |
|
|
|
def setlistdefault(self, key, default): |
|
is_immutable(self) |
|
|
|
|
|
class EnvironHeaders(ImmutableHeadersMixin, Headers): |
|
"""Read only version of the headers from a WSGI environment. This |
|
provides the same interface as `Headers` and is constructed from |
|
a WSGI environment. |
|
|
|
From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a |
|
subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will |
|
render a page for a ``400 BAD REQUEST`` if caught in a catch-all for |
|
HTTP exceptions. |
|
""" |
|
|
|
def __init__(self, environ): |
|
self.environ = environ |
|
|
|
def __eq__(self, other): |
|
return self.environ is other.environ |
|
|
|
__hash__ = None |
|
|
|
def __getitem__(self, key, _get_mode=False): |
|
# _get_mode is a no-op for this class as there is no index but |
|
# used because get() calls it. |
|
if not isinstance(key, str): |
|
raise KeyError(key) |
|
key = key.upper().replace("-", "_") |
|
if key in ("CONTENT_TYPE", "CONTENT_LENGTH"): |
|
return _unicodify_header_value(self.environ[key]) |
|
return _unicodify_header_value(self.environ[f"HTTP_{key}"]) |
|
|
|
def __len__(self): |
|
# the iter is necessary because otherwise list calls our |
|
# len which would call list again and so forth. |
|
return len(list(iter(self))) |
|
|
|
def __iter__(self): |
|
for key, value in self.environ.items(): |
|
if key.startswith("HTTP_") and key not in ( |
|
"HTTP_CONTENT_TYPE", |
|
"HTTP_CONTENT_LENGTH", |
|
): |
|
yield ( |
|
key[5:].replace("_", "-").title(), |
|
_unicodify_header_value(value), |
|
) |
|
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH") and value: |
|
yield (key.replace("_", "-").title(), _unicodify_header_value(value)) |
|
|
|
def copy(self): |
|
raise TypeError(f"cannot create {type(self).__name__!r} copies") |
|
|
|
|
|
class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict): |
|
"""A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict` |
|
instances as sequence and it will combine the return values of all wrapped |
|
dicts: |
|
|
|
>>> from werkzeug.datastructures import CombinedMultiDict, MultiDict |
|
>>> post = MultiDict([('foo', 'bar')]) |
|
>>> get = MultiDict([('blub', 'blah')]) |
|
>>> combined = CombinedMultiDict([get, post]) |
|
>>> combined['foo'] |
|
'bar' |
|
>>> combined['blub'] |
|
'blah' |
|
|
|
This works for all read operations and will raise a `TypeError` for |
|
methods that usually change data which isn't possible. |
|
|
|
From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a |
|
subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will |
|
render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP |
|
exceptions. |
|
""" |
|
|
|
def __reduce_ex__(self, protocol): |
|
return type(self), (self.dicts,) |
|
|
|
def __init__(self, dicts=None): |
|
self.dicts = list(dicts) or [] |
|
|
|
@classmethod |
|
def fromkeys(cls, keys, value=None): |
|
raise TypeError(f"cannot create {cls.__name__!r} instances by fromkeys") |
|
|
|
def __getitem__(self, key): |
|
for d in self.dicts: |
|
if key in d: |
|
return d[key] |
|
raise exceptions.BadRequestKeyError(key) |
|
|
|
def get(self, key, default=None, type=None): |
|
for d in self.dicts: |
|
if key in d: |
|
if type is not None: |
|
try: |
|
return type(d[key]) |
|
except ValueError: |
|
continue |
|
return d[key] |
|
return default |
|
|
|
def getlist(self, key, type=None): |
|
rv = [] |
|
for d in self.dicts: |
|
rv.extend(d.getlist(key, type)) |
|
return rv |
|
|
|
def _keys_impl(self): |
|
"""This function exists so __len__ can be implemented more efficiently, |
|
saving one list creation from an iterator. |
|
""" |
|
rv = set() |
|
rv.update(*self.dicts) |
|
return rv |
|
|
|
def keys(self): |
|
return self._keys_impl() |
|
|
|
def __iter__(self): |
|
return iter(self.keys()) |
|
|
|
def items(self, multi=False): |
|
found = set() |
|
for d in self.dicts: |
|
for key, value in d.items(multi): |
|
if multi: |
|
yield key, value |
|
elif key not in found: |
|
found.add(key) |
|
yield key, value |
|
|
|
def values(self): |
|
for _key, value in self.items(): |
|
yield value |
|
|
|
def lists(self): |
|
rv = {} |
|
for d in self.dicts: |
|
for key, values in d.lists(): |
|
rv.setdefault(key, []).extend(values) |
|
return list(rv.items()) |
|
|
|
def listvalues(self): |
|
return (x[1] for x in self.lists()) |
|
|
|
def copy(self): |
|
"""Return a shallow mutable copy of this object. |
|
|
|
This returns a :class:`MultiDict` representing the data at the |
|
time of copying. The copy will no longer reflect changes to the |
|
wrapped dicts. |
|
|
|
.. versionchanged:: 0.15 |
|
Return a mutable :class:`MultiDict`. |
|
""" |
|
return MultiDict(self) |
|
|
|
def to_dict(self, flat=True): |
|
"""Return the contents as regular dict. If `flat` is `True` the |
|
returned dict will only have the first item present, if `flat` is |
|
`False` all values will be returned as lists. |
|
|
|
:param flat: If set to `False` the dict returned will have lists |
|
with all the values in it. Otherwise it will only |
|
contain the first item for each key. |
|
:return: a :class:`dict` |
|
""" |
|
if flat: |
|
return dict(self.items()) |
|
|
|
return dict(self.lists()) |
|
|
|
def __len__(self): |
|
return len(self._keys_impl()) |
|
|
|
def __contains__(self, key): |
|
for d in self.dicts: |
|
if key in d: |
|
return True |
|
return False |
|
|
|
def has_key(self, key): |
|
""" |
|
.. deprecated:: 2.0 |
|
Will be removed in Werkzeug 2.1. Use ``key in data`` |
|
instead. |
|
""" |
|
warnings.warn( |
|
"'has_key' is deprecated and will be removed in Werkzeug" |
|
" 2.1. Use 'key in data' instead.", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
return key in self |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({self.dicts!r})" |
|
|
|
|
|
class FileMultiDict(MultiDict): |
|
"""A special :class:`MultiDict` that has convenience methods to add |
|
files to it. This is used for :class:`EnvironBuilder` and generally |
|
useful for unittesting. |
|
|
|
.. versionadded:: 0.5 |
|
""" |
|
|
|
def add_file(self, name, file, filename=None, content_type=None): |
|
"""Adds a new file to the dict. `file` can be a file name or |
|
a :class:`file`-like or a :class:`FileStorage` object. |
|
|
|
:param name: the name of the field. |
|
:param file: a filename or :class:`file`-like object |
|
:param filename: an optional filename |
|
:param content_type: an optional content type |
|
""" |
|
if isinstance(file, FileStorage): |
|
value = file |
|
else: |
|
if isinstance(file, str): |
|
if filename is None: |
|
filename = file |
|
file = open(file, "rb") |
|
if filename and content_type is None: |
|
content_type = ( |
|
mimetypes.guess_type(filename)[0] or "application/octet-stream" |
|
) |
|
value = FileStorage(file, filename, name, content_type) |
|
|
|
self.add(name, value) |
|
|
|
|
|
class ImmutableDict(ImmutableDictMixin, dict): |
|
"""An immutable :class:`dict`. |
|
|
|
.. versionadded:: 0.5 |
|
""" |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({dict.__repr__(self)})" |
|
|
|
def copy(self): |
|
"""Return a shallow mutable copy of this object. Keep in mind that |
|
the standard library's :func:`copy` function is a no-op for this class |
|
like for any other python immutable type (eg: :class:`tuple`). |
|
""" |
|
return dict(self) |
|
|
|
def __copy__(self): |
|
return self |
|
|
|
|
|
class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict): |
|
"""An immutable :class:`MultiDict`. |
|
|
|
.. versionadded:: 0.5 |
|
""" |
|
|
|
def copy(self): |
|
"""Return a shallow mutable copy of this object. Keep in mind that |
|
the standard library's :func:`copy` function is a no-op for this class |
|
like for any other python immutable type (eg: :class:`tuple`). |
|
""" |
|
return MultiDict(self) |
|
|
|
def __copy__(self): |
|
return self |
|
|
|
|
|
class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict): |
|
"""An immutable :class:`OrderedMultiDict`. |
|
|
|
.. versionadded:: 0.6 |
|
""" |
|
|
|
def _iter_hashitems(self): |
|
return enumerate(self.items(multi=True)) |
|
|
|
def copy(self): |
|
"""Return a shallow mutable copy of this object. Keep in mind that |
|
the standard library's :func:`copy` function is a no-op for this class |
|
like for any other python immutable type (eg: :class:`tuple`). |
|
""" |
|
return OrderedMultiDict(self) |
|
|
|
def __copy__(self): |
|
return self |
|
|
|
|
|
class Accept(ImmutableList): |
|
"""An :class:`Accept` object is just a list subclass for lists of |
|
``(value, quality)`` tuples. It is automatically sorted by specificity |
|
and quality. |
|
|
|
All :class:`Accept` objects work similar to a list but provide extra |
|
functionality for working with the data. Containment checks are |
|
normalized to the rules of that header: |
|
|
|
>>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) |
|
>>> a.best |
|
'ISO-8859-1' |
|
>>> 'iso-8859-1' in a |
|
True |
|
>>> 'UTF8' in a |
|
True |
|
>>> 'utf7' in a |
|
False |
|
|
|
To get the quality for an item you can use normal item lookup: |
|
|
|
>>> print a['utf-8'] |
|
0.7 |
|
>>> a['utf7'] |
|
0 |
|
|
|
.. versionchanged:: 0.5 |
|
:class:`Accept` objects are forced immutable now. |
|
|
|
.. versionchanged:: 1.0.0 |
|
:class:`Accept` internal values are no longer ordered |
|
alphabetically for equal quality tags. Instead the initial |
|
order is preserved. |
|
|
|
""" |
|
|
|
def __init__(self, values=()): |
|
if values is None: |
|
list.__init__(self) |
|
self.provided = False |
|
elif isinstance(values, Accept): |
|
self.provided = values.provided |
|
list.__init__(self, values) |
|
else: |
|
self.provided = True |
|
values = sorted( |
|
values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True |
|
) |
|
list.__init__(self, values) |
|
|
|
def _specificity(self, value): |
|
"""Returns a tuple describing the value's specificity.""" |
|
return (value != "*",) |
|
|
|
def _value_matches(self, value, item): |
|
"""Check if a value matches a given accept item.""" |
|
return item == "*" or item.lower() == value.lower() |
|
|
|
def __getitem__(self, key): |
|
"""Besides index lookup (getting item n) you can also pass it a string |
|
to get the quality for the item. If the item is not in the list, the |
|
returned quality is ``0``. |
|
""" |
|
if isinstance(key, str): |
|
return self.quality(key) |
|
return list.__getitem__(self, key) |
|
|
|
def quality(self, key): |
|
"""Returns the quality of the key. |
|
|
|
.. versionadded:: 0.6 |
|
In previous versions you had to use the item-lookup syntax |
|
(eg: ``obj[key]`` instead of ``obj.quality(key)``) |
|
""" |
|
for item, quality in self: |
|
if self._value_matches(key, item): |
|
return quality |
|
return 0 |
|
|
|
def __contains__(self, value): |
|
for item, _quality in self: |
|
if self._value_matches(value, item): |
|
return True |
|
return False |
|
|
|
def __repr__(self): |
|
pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self) |
|
return f"{type(self).__name__}([{pairs_str}])" |
|
|
|
def index(self, key): |
|
"""Get the position of an entry or raise :exc:`ValueError`. |
|
|
|
:param key: The key to be looked up. |
|
|
|
.. versionchanged:: 0.5 |
|
This used to raise :exc:`IndexError`, which was inconsistent |
|
with the list API. |
|
""" |
|
if isinstance(key, str): |
|
for idx, (item, _quality) in enumerate(self): |
|
if self._value_matches(key, item): |
|
return idx |
|
raise ValueError(key) |
|
return list.index(self, key) |
|
|
|
def find(self, key): |
|
"""Get the position of an entry or return -1. |
|
|
|
:param key: The key to be looked up. |
|
""" |
|
try: |
|
return self.index(key) |
|
except ValueError: |
|
return -1 |
|
|
|
def values(self): |
|
"""Iterate over all values.""" |
|
for item in self: |
|
yield item[0] |
|
|
|
def to_header(self): |
|
"""Convert the header set into an HTTP header string.""" |
|
result = [] |
|
for value, quality in self: |
|
if quality != 1: |
|
value = f"{value};q={quality}" |
|
result.append(value) |
|
return ",".join(result) |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def _best_single_match(self, match): |
|
for client_item, quality in self: |
|
if self._value_matches(match, client_item): |
|
# self is sorted by specificity descending, we can exit |
|
return client_item, quality |
|
return None |
|
|
|
def best_match(self, matches, default=None): |
|
"""Returns the best match from a list of possible matches based |
|
on the specificity and quality of the client. If two items have the |
|
same quality and specificity, the one is returned that comes first. |
|
|
|
:param matches: a list of matches to check for |
|
:param default: the value that is returned if none match |
|
""" |
|
result = default |
|
best_quality = -1 |
|
best_specificity = (-1,) |
|
for server_item in matches: |
|
match = self._best_single_match(server_item) |
|
if not match: |
|
continue |
|
client_item, quality = match |
|
specificity = self._specificity(client_item) |
|
if quality <= 0 or quality < best_quality: |
|
continue |
|
# better quality or same quality but more specific => better match |
|
if quality > best_quality or specificity > best_specificity: |
|
result = server_item |
|
best_quality = quality |
|
best_specificity = specificity |
|
return result |
|
|
|
@property |
|
def best(self): |
|
"""The best match as value.""" |
|
if self: |
|
return self[0][0] |
|
|
|
|
|
_mime_split_re = re.compile(r"/|(?:\s*;\s*)") |
|
|
|
|
|
def _normalize_mime(value): |
|
return _mime_split_re.split(value.lower()) |
|
|
|
|
|
class MIMEAccept(Accept): |
|
"""Like :class:`Accept` but with special methods and behavior for |
|
mimetypes. |
|
""" |
|
|
|
def _specificity(self, value): |
|
return tuple(x != "*" for x in _mime_split_re.split(value)) |
|
|
|
def _value_matches(self, value, item): |
|
# item comes from the client, can't match if it's invalid. |
|
if "/" not in item: |
|
return False |
|
|
|
# value comes from the application, tell the developer when it |
|
# doesn't look valid. |
|
if "/" not in value: |
|
raise ValueError(f"invalid mimetype {value!r}") |
|
|
|
# Split the match value into type, subtype, and a sorted list of parameters. |
|
normalized_value = _normalize_mime(value) |
|
value_type, value_subtype = normalized_value[:2] |
|
value_params = sorted(normalized_value[2:]) |
|
|
|
# "*/*" is the only valid value that can start with "*". |
|
if value_type == "*" and value_subtype != "*": |
|
raise ValueError(f"invalid mimetype {value!r}") |
|
|
|
# Split the accept item into type, subtype, and parameters. |
|
normalized_item = _normalize_mime(item) |
|
item_type, item_subtype = normalized_item[:2] |
|
item_params = sorted(normalized_item[2:]) |
|
|
|
# "*/not-*" from the client is invalid, can't match. |
|
if item_type == "*" and item_subtype != "*": |
|
return False |
|
|
|
return ( |
|
(item_type == "*" and item_subtype == "*") |
|
or (value_type == "*" and value_subtype == "*") |
|
) or ( |
|
item_type == value_type |
|
and ( |
|
item_subtype == "*" |
|
or value_subtype == "*" |
|
or (item_subtype == value_subtype and item_params == value_params) |
|
) |
|
) |
|
|
|
@property |
|
def accept_html(self): |
|
"""True if this object accepts HTML.""" |
|
return ( |
|
"text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml |
|
) |
|
|
|
@property |
|
def accept_xhtml(self): |
|
"""True if this object accepts XHTML.""" |
|
return "application/xhtml+xml" in self or "application/xml" in self |
|
|
|
@property |
|
def accept_json(self): |
|
"""True if this object accepts JSON.""" |
|
return "application/json" in self |
|
|
|
|
|
_locale_delim_re = re.compile(r"[_-]") |
|
|
|
|
|
def _normalize_lang(value): |
|
"""Process a language tag for matching.""" |
|
return _locale_delim_re.split(value.lower()) |
|
|
|
|
|
class LanguageAccept(Accept): |
|
"""Like :class:`Accept` but with normalization for language tags.""" |
|
|
|
def _value_matches(self, value, item): |
|
return item == "*" or _normalize_lang(value) == _normalize_lang(item) |
|
|
|
def best_match(self, matches, default=None): |
|
"""Given a list of supported values, finds the best match from |
|
the list of accepted values. |
|
|
|
Language tags are normalized for the purpose of matching, but |
|
are returned unchanged. |
|
|
|
If no exact match is found, this will fall back to matching |
|
the first subtag (primary language only), first with the |
|
accepted values then with the match values. This partial is not |
|
applied to any other language subtags. |
|
|
|
The default is returned if no exact or fallback match is found. |
|
|
|
:param matches: A list of supported languages to find a match. |
|
:param default: The value that is returned if none match. |
|
""" |
|
# Look for an exact match first. If a client accepts "en-US", |
|
# "en-US" is a valid match at this point. |
|
result = super().best_match(matches) |
|
|
|
if result is not None: |
|
return result |
|
|
|
# Fall back to accepting primary tags. If a client accepts |
|
# "en-US", "en" is a valid match at this point. Need to use |
|
# re.split to account for 2 or 3 letter codes. |
|
fallback = Accept( |
|
[(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self] |
|
) |
|
result = fallback.best_match(matches) |
|
|
|
if result is not None: |
|
return result |
|
|
|
# Fall back to matching primary tags. If the client accepts |
|
# "en", "en-US" is a valid match at this point. |
|
fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches] |
|
result = super().best_match(fallback_matches) |
|
|
|
# Return a value from the original match list. Find the first |
|
# original value that starts with the matched primary tag. |
|
if result is not None: |
|
return next(item for item in matches if item.startswith(result)) |
|
|
|
return default |
|
|
|
|
|
class CharsetAccept(Accept): |
|
"""Like :class:`Accept` but with normalization for charsets.""" |
|
|
|
def _value_matches(self, value, item): |
|
def _normalize(name): |
|
try: |
|
return codecs.lookup(name).name |
|
except LookupError: |
|
return name.lower() |
|
|
|
return item == "*" or _normalize(value) == _normalize(item) |
|
|
|
|
|
def cache_control_property(key, empty, type): |
|
"""Return a new property object for a cache header. Useful if you |
|
want to add support for a cache extension in a subclass. |
|
|
|
.. versionchanged:: 2.0 |
|
Renamed from ``cache_property``. |
|
""" |
|
return property( |
|
lambda x: x._get_cache_value(key, empty, type), |
|
lambda x, v: x._set_cache_value(key, v, type), |
|
lambda x: x._del_cache_value(key), |
|
f"accessor for {key!r}", |
|
) |
|
|
|
|
|
def cache_property(key, empty, type): |
|
warnings.warn( |
|
"'cache_property' is renamed to 'cache_control_property'. The" |
|
" old name is deprecated and will be removed in Werkzeug 2.1.", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
return cache_control_property(key, empty, type) |
|
|
|
|
|
class _CacheControl(UpdateDictMixin, dict): |
|
"""Subclass of a dict that stores values for a Cache-Control header. It |
|
has accessors for all the cache-control directives specified in RFC 2616. |
|
The class does not differentiate between request and response directives. |
|
|
|
Because the cache-control directives in the HTTP header use dashes the |
|
python descriptors use underscores for that. |
|
|
|
To get a header of the :class:`CacheControl` object again you can convert |
|
the object into a string or call the :meth:`to_header` method. If you plan |
|
to subclass it and add your own items have a look at the sourcecode for |
|
that class. |
|
|
|
.. versionchanged:: 0.4 |
|
|
|
Setting `no_cache` or `private` to boolean `True` will set the implicit |
|
none-value which is ``*``: |
|
|
|
>>> cc = ResponseCacheControl() |
|
>>> cc.no_cache = True |
|
>>> cc |
|
<ResponseCacheControl 'no-cache'> |
|
>>> cc.no_cache |
|
'*' |
|
>>> cc.no_cache = None |
|
>>> cc |
|
<ResponseCacheControl ''> |
|
|
|
In versions before 0.5 the behavior documented here affected the now |
|
no longer existing `CacheControl` class. |
|
""" |
|
|
|
no_cache = cache_control_property("no-cache", "*", None) |
|
no_store = cache_control_property("no-store", None, bool) |
|
max_age = cache_control_property("max-age", -1, int) |
|
no_transform = cache_control_property("no-transform", None, None) |
|
|
|
def __init__(self, values=(), on_update=None): |
|
dict.__init__(self, values or ()) |
|
self.on_update = on_update |
|
self.provided = values is not None |
|
|
|
def _get_cache_value(self, key, empty, type): |
|
"""Used internally by the accessor properties.""" |
|
if type is bool: |
|
return key in self |
|
if key in self: |
|
value = self[key] |
|
if value is None: |
|
return empty |
|
elif type is not None: |
|
try: |
|
value = type(value) |
|
except ValueError: |
|
pass |
|
return value |
|
return None |
|
|
|
def _set_cache_value(self, key, value, type): |
|
"""Used internally by the accessor properties.""" |
|
if type is bool: |
|
if value: |
|
self[key] = None |
|
else: |
|
self.pop(key, None) |
|
else: |
|
if value is None: |
|
self.pop(key, None) |
|
elif value is True: |
|
self[key] = None |
|
else: |
|
self[key] = value |
|
|
|
def _del_cache_value(self, key): |
|
"""Used internally by the accessor properties.""" |
|
if key in self: |
|
del self[key] |
|
|
|
def to_header(self): |
|
"""Convert the stored values into a cache control header.""" |
|
return http.dump_header(self) |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) |
|
return f"<{type(self).__name__} {kv_str}>" |
|
|
|
cache_property = staticmethod(cache_control_property) |
|
|
|
|
|
class RequestCacheControl(ImmutableDictMixin, _CacheControl): |
|
"""A cache control for requests. This is immutable and gives access |
|
to all the request-relevant cache control headers. |
|
|
|
To get a header of the :class:`RequestCacheControl` object again you can |
|
convert the object into a string or call the :meth:`to_header` method. If |
|
you plan to subclass it and add your own items have a look at the sourcecode |
|
for that class. |
|
|
|
.. versionadded:: 0.5 |
|
In previous versions a `CacheControl` class existed that was used |
|
both for request and response. |
|
""" |
|
|
|
max_stale = cache_control_property("max-stale", "*", int) |
|
min_fresh = cache_control_property("min-fresh", "*", int) |
|
only_if_cached = cache_control_property("only-if-cached", None, bool) |
|
|
|
|
|
class ResponseCacheControl(_CacheControl): |
|
"""A cache control for responses. Unlike :class:`RequestCacheControl` |
|
this is mutable and gives access to response-relevant cache control |
|
headers. |
|
|
|
To get a header of the :class:`ResponseCacheControl` object again you can |
|
convert the object into a string or call the :meth:`to_header` method. If |
|
you plan to subclass it and add your own items have a look at the sourcecode |
|
for that class. |
|
|
|
.. versionadded:: 0.5 |
|
In previous versions a `CacheControl` class existed that was used |
|
both for request and response. |
|
""" |
|
|
|
public = cache_control_property("public", None, bool) |
|
private = cache_control_property("private", "*", None) |
|
must_revalidate = cache_control_property("must-revalidate", None, bool) |
|
proxy_revalidate = cache_control_property("proxy-revalidate", None, bool) |
|
s_maxage = cache_control_property("s-maxage", None, None) |
|
immutable = cache_control_property("immutable", None, bool) |
|
|
|
|
|
def csp_property(key): |
|
"""Return a new property object for a content security policy header. |
|
Useful if you want to add support for a csp extension in a |
|
subclass. |
|
""" |
|
return property( |
|
lambda x: x._get_value(key), |
|
lambda x, v: x._set_value(key, v), |
|
lambda x: x._del_value(key), |
|
f"accessor for {key!r}", |
|
) |
|
|
|
|
|
class ContentSecurityPolicy(UpdateDictMixin, dict): |
|
"""Subclass of a dict that stores values for a Content Security Policy |
|
header. It has accessors for all the level 3 policies. |
|
|
|
Because the csp directives in the HTTP header use dashes the |
|
python descriptors use underscores for that. |
|
|
|
To get a header of the :class:`ContentSecuirtyPolicy` object again |
|
you can convert the object into a string or call the |
|
:meth:`to_header` method. If you plan to subclass it and add your |
|
own items have a look at the sourcecode for that class. |
|
|
|
.. versionadded:: 1.0.0 |
|
Support for Content Security Policy headers was added. |
|
|
|
""" |
|
|
|
base_uri = csp_property("base-uri") |
|
child_src = csp_property("child-src") |
|
connect_src = csp_property("connect-src") |
|
default_src = csp_property("default-src") |
|
font_src = csp_property("font-src") |
|
form_action = csp_property("form-action") |
|
frame_ancestors = csp_property("frame-ancestors") |
|
frame_src = csp_property("frame-src") |
|
img_src = csp_property("img-src") |
|
manifest_src = csp_property("manifest-src") |
|
media_src = csp_property("media-src") |
|
navigate_to = csp_property("navigate-to") |
|
object_src = csp_property("object-src") |
|
prefetch_src = csp_property("prefetch-src") |
|
plugin_types = csp_property("plugin-types") |
|
report_to = csp_property("report-to") |
|
report_uri = csp_property("report-uri") |
|
sandbox = csp_property("sandbox") |
|
script_src = csp_property("script-src") |
|
script_src_attr = csp_property("script-src-attr") |
|
script_src_elem = csp_property("script-src-elem") |
|
style_src = csp_property("style-src") |
|
style_src_attr = csp_property("style-src-attr") |
|
style_src_elem = csp_property("style-src-elem") |
|
worker_src = csp_property("worker-src") |
|
|
|
def __init__(self, values=(), on_update=None): |
|
dict.__init__(self, values or ()) |
|
self.on_update = on_update |
|
self.provided = values is not None |
|
|
|
def _get_value(self, key): |
|
"""Used internally by the accessor properties.""" |
|
return self.get(key) |
|
|
|
def _set_value(self, key, value): |
|
"""Used internally by the accessor properties.""" |
|
if value is None: |
|
self.pop(key, None) |
|
else: |
|
self[key] = value |
|
|
|
def _del_value(self, key): |
|
"""Used internally by the accessor properties.""" |
|
if key in self: |
|
del self[key] |
|
|
|
def to_header(self): |
|
"""Convert the stored values into a cache control header.""" |
|
return http.dump_csp_header(self) |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) |
|
return f"<{type(self).__name__} {kv_str}>" |
|
|
|
|
|
class CallbackDict(UpdateDictMixin, dict): |
|
"""A dict that calls a function passed every time something is changed. |
|
The function is passed the dict instance. |
|
""" |
|
|
|
def __init__(self, initial=None, on_update=None): |
|
dict.__init__(self, initial or ()) |
|
self.on_update = on_update |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {dict.__repr__(self)}>" |
|
|
|
|
|
class HeaderSet(MutableSet): |
|
"""Similar to the :class:`ETags` class this implements a set-like structure. |
|
Unlike :class:`ETags` this is case insensitive and used for vary, allow, and |
|
content-language headers. |
|
|
|
If not constructed using the :func:`parse_set_header` function the |
|
instantiation works like this: |
|
|
|
>>> hs = HeaderSet(['foo', 'bar', 'baz']) |
|
>>> hs |
|
HeaderSet(['foo', 'bar', 'baz']) |
|
""" |
|
|
|
def __init__(self, headers=None, on_update=None): |
|
self._headers = list(headers or ()) |
|
self._set = {x.lower() for x in self._headers} |
|
self.on_update = on_update |
|
|
|
def add(self, header): |
|
"""Add a new header to the set.""" |
|
self.update((header,)) |
|
|
|
def remove(self, header): |
|
"""Remove a header from the set. This raises an :exc:`KeyError` if the |
|
header is not in the set. |
|
|
|
.. versionchanged:: 0.5 |
|
In older versions a :exc:`IndexError` was raised instead of a |
|
:exc:`KeyError` if the object was missing. |
|
|
|
:param header: the header to be removed. |
|
""" |
|
key = header.lower() |
|
if key not in self._set: |
|
raise KeyError(header) |
|
self._set.remove(key) |
|
for idx, key in enumerate(self._headers): |
|
if key.lower() == header: |
|
del self._headers[idx] |
|
break |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def update(self, iterable): |
|
"""Add all the headers from the iterable to the set. |
|
|
|
:param iterable: updates the set with the items from the iterable. |
|
""" |
|
inserted_any = False |
|
for header in iterable: |
|
key = header.lower() |
|
if key not in self._set: |
|
self._headers.append(header) |
|
self._set.add(key) |
|
inserted_any = True |
|
if inserted_any and self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def discard(self, header): |
|
"""Like :meth:`remove` but ignores errors. |
|
|
|
:param header: the header to be discarded. |
|
""" |
|
try: |
|
self.remove(header) |
|
except KeyError: |
|
pass |
|
|
|
def find(self, header): |
|
"""Return the index of the header in the set or return -1 if not found. |
|
|
|
:param header: the header to be looked up. |
|
""" |
|
header = header.lower() |
|
for idx, item in enumerate(self._headers): |
|
if item.lower() == header: |
|
return idx |
|
return -1 |
|
|
|
def index(self, header): |
|
"""Return the index of the header in the set or raise an |
|
:exc:`IndexError`. |
|
|
|
:param header: the header to be looked up. |
|
""" |
|
rv = self.find(header) |
|
if rv < 0: |
|
raise IndexError(header) |
|
return rv |
|
|
|
def clear(self): |
|
"""Clear the set.""" |
|
self._set.clear() |
|
del self._headers[:] |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def as_set(self, preserve_casing=False): |
|
"""Return the set as real python set type. When calling this, all |
|
the items are converted to lowercase and the ordering is lost. |
|
|
|
:param preserve_casing: if set to `True` the items in the set returned |
|
will have the original case like in the |
|
:class:`HeaderSet`, otherwise they will |
|
be lowercase. |
|
""" |
|
if preserve_casing: |
|
return set(self._headers) |
|
return set(self._set) |
|
|
|
def to_header(self): |
|
"""Convert the header set into an HTTP header string.""" |
|
return ", ".join(map(http.quote_header_value, self._headers)) |
|
|
|
def __getitem__(self, idx): |
|
return self._headers[idx] |
|
|
|
def __delitem__(self, idx): |
|
rv = self._headers.pop(idx) |
|
self._set.remove(rv.lower()) |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def __setitem__(self, idx, value): |
|
old = self._headers[idx] |
|
self._set.remove(old.lower()) |
|
self._headers[idx] = value |
|
self._set.add(value.lower()) |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def __contains__(self, header): |
|
return header.lower() in self._set |
|
|
|
def __len__(self): |
|
return len(self._set) |
|
|
|
def __iter__(self): |
|
return iter(self._headers) |
|
|
|
def __bool__(self): |
|
return bool(self._set) |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}({self._headers!r})" |
|
|
|
|
|
class ETags(Collection): |
|
"""A set that can be used to check if one etag is present in a collection |
|
of etags. |
|
""" |
|
|
|
def __init__(self, strong_etags=None, weak_etags=None, star_tag=False): |
|
if not star_tag and strong_etags: |
|
self._strong = frozenset(strong_etags) |
|
else: |
|
self._strong = frozenset() |
|
|
|
self._weak = frozenset(weak_etags or ()) |
|
self.star_tag = star_tag |
|
|
|
def as_set(self, include_weak=False): |
|
"""Convert the `ETags` object into a python set. Per default all the |
|
weak etags are not part of this set.""" |
|
rv = set(self._strong) |
|
if include_weak: |
|
rv.update(self._weak) |
|
return rv |
|
|
|
def is_weak(self, etag): |
|
"""Check if an etag is weak.""" |
|
return etag in self._weak |
|
|
|
def is_strong(self, etag): |
|
"""Check if an etag is strong.""" |
|
return etag in self._strong |
|
|
|
def contains_weak(self, etag): |
|
"""Check if an etag is part of the set including weak and strong tags.""" |
|
return self.is_weak(etag) or self.contains(etag) |
|
|
|
def contains(self, etag): |
|
"""Check if an etag is part of the set ignoring weak tags. |
|
It is also possible to use the ``in`` operator. |
|
""" |
|
if self.star_tag: |
|
return True |
|
return self.is_strong(etag) |
|
|
|
def contains_raw(self, etag): |
|
"""When passed a quoted tag it will check if this tag is part of the |
|
set. If the tag is weak it is checked against weak and strong tags, |
|
otherwise strong only.""" |
|
etag, weak = http.unquote_etag(etag) |
|
if weak: |
|
return self.contains_weak(etag) |
|
return self.contains(etag) |
|
|
|
def to_header(self): |
|
"""Convert the etags set into a HTTP header string.""" |
|
if self.star_tag: |
|
return "*" |
|
return ", ".join( |
|
[f'"{x}"' for x in self._strong] + [f'W/"{x}"' for x in self._weak] |
|
) |
|
|
|
def __call__(self, etag=None, data=None, include_weak=False): |
|
if [etag, data].count(None) != 1: |
|
raise TypeError("either tag or data required, but at least one") |
|
if etag is None: |
|
etag = http.generate_etag(data) |
|
if include_weak: |
|
if etag in self._weak: |
|
return True |
|
return etag in self._strong |
|
|
|
def __bool__(self): |
|
return bool(self.star_tag or self._strong or self._weak) |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __len__(self): |
|
return len(self._strong) |
|
|
|
def __iter__(self): |
|
return iter(self._strong) |
|
|
|
def __contains__(self, etag): |
|
return self.contains(etag) |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {str(self)!r}>" |
|
|
|
|
|
class IfRange: |
|
"""Very simple object that represents the `If-Range` header in parsed |
|
form. It will either have neither a etag or date or one of either but |
|
never both. |
|
|
|
.. versionadded:: 0.7 |
|
""" |
|
|
|
def __init__(self, etag=None, date=None): |
|
#: The etag parsed and unquoted. Ranges always operate on strong |
|
#: etags so the weakness information is not necessary. |
|
self.etag = etag |
|
#: The date in parsed format or `None`. |
|
self.date = date |
|
|
|
def to_header(self): |
|
"""Converts the object back into an HTTP header.""" |
|
if self.date is not None: |
|
return http.http_date(self.date) |
|
if self.etag is not None: |
|
return http.quote_etag(self.etag) |
|
return "" |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {str(self)!r}>" |
|
|
|
|
|
class Range: |
|
"""Represents a ``Range`` header. All methods only support only |
|
bytes as the unit. Stores a list of ranges if given, but the methods |
|
only work if only one range is provided. |
|
|
|
:raise ValueError: If the ranges provided are invalid. |
|
|
|
.. versionchanged:: 0.15 |
|
The ranges passed in are validated. |
|
|
|
.. versionadded:: 0.7 |
|
""" |
|
|
|
def __init__(self, units, ranges): |
|
#: The units of this range. Usually "bytes". |
|
self.units = units |
|
#: A list of ``(begin, end)`` tuples for the range header provided. |
|
#: The ranges are non-inclusive. |
|
self.ranges = ranges |
|
|
|
for start, end in ranges: |
|
if start is None or (end is not None and (start < 0 or start >= end)): |
|
raise ValueError(f"{(start, end)} is not a valid range.") |
|
|
|
def range_for_length(self, length): |
|
"""If the range is for bytes, the length is not None and there is |
|
exactly one range and it is satisfiable it returns a ``(start, stop)`` |
|
tuple, otherwise `None`. |
|
""" |
|
if self.units != "bytes" or length is None or len(self.ranges) != 1: |
|
return None |
|
start, end = self.ranges[0] |
|
if end is None: |
|
end = length |
|
if start < 0: |
|
start += length |
|
if http.is_byte_range_valid(start, end, length): |
|
return start, min(end, length) |
|
return None |
|
|
|
def make_content_range(self, length): |
|
"""Creates a :class:`~werkzeug.datastructures.ContentRange` object |
|
from the current range and given content length. |
|
""" |
|
rng = self.range_for_length(length) |
|
if rng is not None: |
|
return ContentRange(self.units, rng[0], rng[1], length) |
|
return None |
|
|
|
def to_header(self): |
|
"""Converts the object back into an HTTP header.""" |
|
ranges = [] |
|
for begin, end in self.ranges: |
|
if end is None: |
|
ranges.append(f"{begin}-" if begin >= 0 else str(begin)) |
|
else: |
|
ranges.append(f"{begin}-{end - 1}") |
|
return f"{self.units}={','.join(ranges)}" |
|
|
|
def to_content_range_header(self, length): |
|
"""Converts the object into `Content-Range` HTTP header, |
|
based on given length |
|
""" |
|
range = self.range_for_length(length) |
|
if range is not None: |
|
return f"{self.units} {range[0]}-{range[1] - 1}/{length}" |
|
return None |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {str(self)!r}>" |
|
|
|
|
|
def _callback_property(name): |
|
def fget(self): |
|
return getattr(self, name) |
|
|
|
def fset(self, value): |
|
setattr(self, name, value) |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
return property(fget, fset) |
|
|
|
|
|
class ContentRange: |
|
"""Represents the content range header. |
|
|
|
.. versionadded:: 0.7 |
|
""" |
|
|
|
def __init__(self, units, start, stop, length=None, on_update=None): |
|
assert http.is_byte_range_valid(start, stop, length), "Bad range provided" |
|
self.on_update = on_update |
|
self.set(start, stop, length, units) |
|
|
|
#: The units to use, usually "bytes" |
|
units = _callback_property("_units") |
|
#: The start point of the range or `None`. |
|
start = _callback_property("_start") |
|
#: The stop point of the range (non-inclusive) or `None`. Can only be |
|
#: `None` if also start is `None`. |
|
stop = _callback_property("_stop") |
|
#: The length of the range or `None`. |
|
length = _callback_property("_length") |
|
|
|
def set(self, start, stop, length=None, units="bytes"): |
|
"""Simple method to update the ranges.""" |
|
assert http.is_byte_range_valid(start, stop, length), "Bad range provided" |
|
self._units = units |
|
self._start = start |
|
self._stop = stop |
|
self._length = length |
|
if self.on_update is not None: |
|
self.on_update(self) |
|
|
|
def unset(self): |
|
"""Sets the units to `None` which indicates that the header should |
|
no longer be used. |
|
""" |
|
self.set(None, None, units=None) |
|
|
|
def to_header(self): |
|
if self.units is None: |
|
return "" |
|
if self.length is None: |
|
length = "*" |
|
else: |
|
length = self.length |
|
if self.start is None: |
|
return f"{self.units} */{length}" |
|
return f"{self.units} {self.start}-{self.stop - 1}/{length}" |
|
|
|
def __bool__(self): |
|
return self.units is not None |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {str(self)!r}>" |
|
|
|
|
|
class Authorization(ImmutableDictMixin, dict): |
|
"""Represents an ``Authorization`` header sent by the client. |
|
|
|
This is returned by |
|
:func:`~werkzeug.http.parse_authorization_header`. It can be useful |
|
to create the object manually to pass to the test |
|
:class:`~werkzeug.test.Client`. |
|
|
|
.. versionchanged:: 0.5 |
|
This object became immutable. |
|
""" |
|
|
|
def __init__(self, auth_type, data=None): |
|
dict.__init__(self, data or {}) |
|
self.type = auth_type |
|
|
|
@property |
|
def username(self): |
|
"""The username transmitted. This is set for both basic and digest |
|
auth all the time. |
|
""" |
|
return self.get("username") |
|
|
|
@property |
|
def password(self): |
|
"""When the authentication type is basic this is the password |
|
transmitted by the client, else `None`. |
|
""" |
|
return self.get("password") |
|
|
|
@property |
|
def realm(self): |
|
"""This is the server realm sent back for HTTP digest auth.""" |
|
return self.get("realm") |
|
|
|
@property |
|
def nonce(self): |
|
"""The nonce the server sent for digest auth, sent back by the client. |
|
A nonce should be unique for every 401 response for HTTP digest auth. |
|
""" |
|
return self.get("nonce") |
|
|
|
@property |
|
def uri(self): |
|
"""The URI from Request-URI of the Request-Line; duplicated because |
|
proxies are allowed to change the Request-Line in transit. HTTP |
|
digest auth only. |
|
""" |
|
return self.get("uri") |
|
|
|
@property |
|
def nc(self): |
|
"""The nonce count value transmitted by clients if a qop-header is |
|
also transmitted. HTTP digest auth only. |
|
""" |
|
return self.get("nc") |
|
|
|
@property |
|
def cnonce(self): |
|
"""If the server sent a qop-header in the ``WWW-Authenticate`` |
|
header, the client has to provide this value for HTTP digest auth. |
|
See the RFC for more details. |
|
""" |
|
return self.get("cnonce") |
|
|
|
@property |
|
def response(self): |
|
"""A string of 32 hex digits computed as defined in RFC 2617, which |
|
proves that the user knows a password. Digest auth only. |
|
""" |
|
return self.get("response") |
|
|
|
@property |
|
def opaque(self): |
|
"""The opaque header from the server returned unchanged by the client. |
|
It is recommended that this string be base64 or hexadecimal data. |
|
Digest auth only. |
|
""" |
|
return self.get("opaque") |
|
|
|
@property |
|
def qop(self): |
|
"""Indicates what "quality of protection" the client has applied to |
|
the message for HTTP digest auth. Note that this is a single token, |
|
not a quoted list of alternatives as in WWW-Authenticate. |
|
""" |
|
return self.get("qop") |
|
|
|
def to_header(self): |
|
"""Convert to a string value for an ``Authorization`` header. |
|
|
|
.. versionadded:: 2.0 |
|
Added to support passing authorization to the test client. |
|
""" |
|
if self.type == "basic": |
|
value = base64.b64encode( |
|
f"{self.username}:{self.password}".encode() |
|
).decode("utf8") |
|
return f"Basic {value}" |
|
|
|
if self.type == "digest": |
|
return f"Digest {http.dump_header(self)}" |
|
|
|
raise ValueError(f"Unsupported type {self.type!r}.") |
|
|
|
|
|
def auth_property(name, doc=None): |
|
"""A static helper function for Authentication subclasses to add |
|
extra authentication system properties onto a class:: |
|
|
|
class FooAuthenticate(WWWAuthenticate): |
|
special_realm = auth_property('special_realm') |
|
|
|
For more information have a look at the sourcecode to see how the |
|
regular properties (:attr:`realm` etc.) are implemented. |
|
""" |
|
|
|
def _set_value(self, value): |
|
if value is None: |
|
self.pop(name, None) |
|
else: |
|
self[name] = str(value) |
|
|
|
return property(lambda x: x.get(name), _set_value, doc=doc) |
|
|
|
|
|
def _set_property(name, doc=None): |
|
def fget(self): |
|
def on_update(header_set): |
|
if not header_set and name in self: |
|
del self[name] |
|
elif header_set: |
|
self[name] = header_set.to_header() |
|
|
|
return http.parse_set_header(self.get(name), on_update) |
|
|
|
return property(fget, doc=doc) |
|
|
|
|
|
class WWWAuthenticate(UpdateDictMixin, dict): |
|
"""Provides simple access to `WWW-Authenticate` headers.""" |
|
|
|
#: list of keys that require quoting in the generated header |
|
_require_quoting = frozenset(["domain", "nonce", "opaque", "realm", "qop"]) |
|
|
|
def __init__(self, auth_type=None, values=None, on_update=None): |
|
dict.__init__(self, values or ()) |
|
if auth_type: |
|
self["__auth_type__"] = auth_type |
|
self.on_update = on_update |
|
|
|
def set_basic(self, realm="authentication required"): |
|
"""Clear the auth info and enable basic auth.""" |
|
dict.clear(self) |
|
dict.update(self, {"__auth_type__": "basic", "realm": realm}) |
|
if self.on_update: |
|
self.on_update(self) |
|
|
|
def set_digest( |
|
self, realm, nonce, qop=("auth",), opaque=None, algorithm=None, stale=False |
|
): |
|
"""Clear the auth info and enable digest auth.""" |
|
d = { |
|
"__auth_type__": "digest", |
|
"realm": realm, |
|
"nonce": nonce, |
|
"qop": http.dump_header(qop), |
|
} |
|
if stale: |
|
d["stale"] = "TRUE" |
|
if opaque is not None: |
|
d["opaque"] = opaque |
|
if algorithm is not None: |
|
d["algorithm"] = algorithm |
|
dict.clear(self) |
|
dict.update(self, d) |
|
if self.on_update: |
|
self.on_update(self) |
|
|
|
def to_header(self): |
|
"""Convert the stored values into a WWW-Authenticate header.""" |
|
d = dict(self) |
|
auth_type = d.pop("__auth_type__", None) or "basic" |
|
kv_items = ( |
|
(k, http.quote_header_value(v, allow_token=k not in self._require_quoting)) |
|
for k, v in d.items() |
|
) |
|
kv_string = ", ".join([f"{k}={v}" for k, v in kv_items]) |
|
return f"{auth_type.title()} {kv_string}" |
|
|
|
def __str__(self): |
|
return self.to_header() |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__} {self.to_header()!r}>" |
|
|
|
type = auth_property( |
|
"__auth_type__", |
|
doc="""The type of the auth mechanism. HTTP currently specifies |
|
``Basic`` and ``Digest``.""", |
|
) |
|
realm = auth_property( |
|
"realm", |
|
doc="""A string to be displayed to users so they know which |
|
username and password to use. This string should contain at |
|
least the name of the host performing the authentication and |
|
might additionally indicate the collection of users who might |
|
have access.""", |
|
) |
|
domain = _set_property( |
|
"domain", |
|
doc="""A list of URIs that define the protection space. If a URI |
|
is an absolute path, it is relative to the canonical root URL of |
|
the server being accessed.""", |
|
) |
|
nonce = auth_property( |
|
"nonce", |
|
doc=""" |
|
A server-specified data string which should be uniquely generated |
|
each time a 401 response is made. It is recommended that this |
|
string be base64 or hexadecimal data.""", |
|
) |
|
opaque = auth_property( |
|
"opaque", |
|
doc="""A string of data, specified by the server, which should |
|
be returned by the client unchanged in the Authorization header |
|
of subsequent requests with URIs in the same protection space. |
|
It is recommended that this string be base64 or hexadecimal |
|
data.""", |
|
) |
|
algorithm = auth_property( |
|
"algorithm", |
|
doc="""A string indicating a pair of algorithms used to produce |
|
the digest and a checksum. If this is not present it is assumed |
|
to be "MD5". If the algorithm is not understood, the challenge |
|
should be ignored (and a different one used, if there is more |
|
than one).""", |
|
) |
|
qop = _set_property( |
|
"qop", |
|
doc="""A set of quality-of-privacy directives such as auth and |
|
auth-int.""", |
|
) |
|
|
|
@property |
|
def stale(self): |
|
"""A flag, indicating that the previous request from the client |
|
was rejected because the nonce value was stale. |
|
""" |
|
val = self.get("stale") |
|
if val is not None: |
|
return val.lower() == "true" |
|
|
|
@stale.setter |
|
def stale(self, value): |
|
if value is None: |
|
self.pop("stale", None) |
|
else: |
|
self["stale"] = "TRUE" if value else "FALSE" |
|
|
|
auth_property = staticmethod(auth_property) |
|
|
|
|
|
class FileStorage: |
|
"""The :class:`FileStorage` class is a thin wrapper over incoming files. |
|
It is used by the request object to represent uploaded files. All the |
|
attributes of the wrapper stream are proxied by the file storage so |
|
it's possible to do ``storage.read()`` instead of the long form |
|
``storage.stream.read()``. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
stream=None, |
|
filename=None, |
|
name=None, |
|
content_type=None, |
|
content_length=None, |
|
headers=None, |
|
): |
|
self.name = name |
|
self.stream = stream or BytesIO() |
|
|
|
# if no filename is provided we can attempt to get the filename |
|
# from the stream object passed. There we have to be careful to |
|
# skip things like <fdopen>, <stderr> etc. Python marks these |
|
# special filenames with angular brackets. |
|
if filename is None: |
|
filename = getattr(stream, "name", None) |
|
s = _make_encode_wrapper(filename) |
|
if filename and filename[0] == s("<") and filename[-1] == s(">"): |
|
filename = None |
|
|
|
# Make sure the filename is not bytes. This might happen if |
|
# the file was opened from the bytes API. |
|
if isinstance(filename, bytes): |
|
filename = filename.decode(get_filesystem_encoding(), "replace") |
|
|
|
self.filename = filename |
|
if headers is None: |
|
headers = Headers() |
|
self.headers = headers |
|
if content_type is not None: |
|
headers["Content-Type"] = content_type |
|
if content_length is not None: |
|
headers["Content-Length"] = str(content_length) |
|
|
|
def _parse_content_type(self): |
|
if not hasattr(self, "_parsed_content_type"): |
|
self._parsed_content_type = http.parse_options_header(self.content_type) |
|
|
|
@property |
|
def content_type(self): |
|
"""The content-type sent in the header. Usually not available""" |
|
return self.headers.get("content-type") |
|
|
|
@property |
|
def content_length(self): |
|
"""The content-length sent in the header. Usually not available""" |
|
return int(self.headers.get("content-length") or 0) |
|
|
|
@property |
|
def mimetype(self): |
|
"""Like :attr:`content_type`, but without parameters (eg, without |
|
charset, type etc.) and always lowercase. For example if the content |
|
type is ``text/HTML; charset=utf-8`` the mimetype would be |
|
``'text/html'``. |
|
|
|
.. versionadded:: 0.7 |
|
""" |
|
self._parse_content_type() |
|
return self._parsed_content_type[0].lower() |
|
|
|
@property |
|
def mimetype_params(self): |
|
"""The mimetype parameters as dict. For example if the content |
|
type is ``text/html; charset=utf-8`` the params would be |
|
``{'charset': 'utf-8'}``. |
|
|
|
.. versionadded:: 0.7 |
|
""" |
|
self._parse_content_type() |
|
return self._parsed_content_type[1] |
|
|
|
def save(self, dst, buffer_size=16384): |
|
"""Save the file to a destination path or file object. If the |
|
destination is a file object you have to close it yourself after the |
|
call. The buffer size is the number of bytes held in memory during |
|
the copy process. It defaults to 16KB. |
|
|
|
For secure file saving also have a look at :func:`secure_filename`. |
|
|
|
:param dst: a filename, :class:`os.PathLike`, or open file |
|
object to write to. |
|
:param buffer_size: Passed as the ``length`` parameter of |
|
:func:`shutil.copyfileobj`. |
|
|
|
.. versionchanged:: 1.0 |
|
Supports :mod:`pathlib`. |
|
""" |
|
from shutil import copyfileobj |
|
|
|
close_dst = False |
|
|
|
if hasattr(dst, "__fspath__"): |
|
dst = fspath(dst) |
|
|
|
if isinstance(dst, str): |
|
dst = open(dst, "wb") |
|
close_dst = True |
|
|
|
try: |
|
copyfileobj(self.stream, dst, buffer_size) |
|
finally: |
|
if close_dst: |
|
dst.close() |
|
|
|
def close(self): |
|
"""Close the underlying file if possible.""" |
|
try: |
|
self.stream.close() |
|
except Exception: |
|
pass |
|
|
|
def __bool__(self): |
|
return bool(self.filename) |
|
|
|
def __getattr__(self, name): |
|
try: |
|
return getattr(self.stream, name) |
|
except AttributeError: |
|
# SpooledTemporaryFile doesn't implement IOBase, get the |
|
# attribute from its backing file instead. |
|
# https://github.com/python/cpython/pull/3249 |
|
if hasattr(self.stream, "_file"): |
|
return getattr(self.stream._file, name) |
|
raise |
|
|
|
def __iter__(self): |
|
return iter(self.stream) |
|
|
|
def __repr__(self): |
|
return f"<{type(self).__name__}: {self.filename!r} ({self.content_type!r})>" |
|
|
|
|
|
# circular dependencies |
|
from . import http
|
|
|