Skip to content

Commit

Permalink
Utilise a CaseInsensitiveDict for the parsed options
Browse files Browse the repository at this point in the history
I cannot find a RFC that indicates that the option names are case
sensitive whereas RFC6266 states that the `filename` option name is
case insensitive. Therefore the best user experience is to use a
CaseInsensitiveDict whereby any key casing returns the same value.

This also adds the CaseInsensitiveDict for usage here and possibly
elsewhere.

This could cause breaking changes if case sensitivity is relied upon,
if so we will have to rethink.
  • Loading branch information
pgjones committed Jul 1, 2022
1 parent c183cde commit 84ad4ca
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/werkzeug/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import re
from collections.abc import Collection
from collections.abc import MutableMapping
from collections.abc import MutableSet
from copy import deepcopy
from io import BytesIO
Expand Down Expand Up @@ -180,6 +181,39 @@ def setlistdefault(self, key, default_list=None):
is_immutable(self)


class CaseInsensitiveDict(dict):
"""A case insensitive dict, with respect to the keys.
.. versionadded:: 2.2
:private:
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for key in list(self.keys()):
value = super().pop(key)
self.__setitem__(key, value)

def _lower_key(self, key):
if isinstance(key, str):
return key.lower()
else:
return key

def __setitem__(self, key, value):
super().__setitem__(self._lower_key(key), value)

def __getitem__(self, key):
return super().__getitem__(self._lower_key(key))

def __delitem__(self, key):
super().__delitem__(self._lower_key(key))

def pop(self, key, *args, **kwargs):
super().pop(self._lower_key(key), *args, **kwargs)


def _calls_update(name):
def oncall(self, *args, **kw):
rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw)
Expand Down
8 changes: 8 additions & 0 deletions src/werkzeug/datastructures.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ class ImmutableMultiDictMixin(ImmutableDictMixin[K, V]):

def _calls_update(name: str) -> Callable[[UpdateDictMixin[K, V]], Any]: ...

class CaseInsensitiveDict(Dict[K, V]):
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
def _lower_key(self, key: K) -> K: ...
def __setitem__(self, key: K, value: V) -> None: ...
def __getitem__(self, key: K) -> V: ...
def __delitem__(self, key: K) -> None: ...
def pop(self, key: K, *args: Any, **kwargs: Any) -> V: ...

class UpdateDictMixin(Dict[K, V]):
on_update: Optional[Callable[[UpdateDictMixin[K, V]], None]]
def setdefault(self, key: K, default: Optional[V] = None) -> V: ...
Expand Down
3 changes: 2 additions & 1 deletion src/werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ._internal import _to_bytes
from ._internal import _to_str
from ._internal import _wsgi_decoding_dance
from .datastructures import CaseInsensitiveDict
from werkzeug._internal import _dt_as_utc

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -420,7 +421,7 @@ def parse_options_header(
if not match:
break
result.append(match.group(1)) # mimetype
options: t.Dict[str, str] = {}
options: CaseInsensitiveDict[str, str] = CaseInsensitiveDict()
# Parse options
rest = match.group(2)
encoding: t.Optional[str]
Expand Down
5 changes: 5 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ def test_parse_options_header_broken_values(self):
assert http.parse_options_header(" , a ") == ("", {})
assert http.parse_options_header(" ; a ") == ("", {})

def test_parse_options_header_case_insensitive(self):
_, options = http.parse_options_header(r'something; fileName="File.ext"')
assert options["fileName"] == "File.ext"
assert options["filename"] == "File.ext"

def test_dump_options_header(self):
assert http.dump_options_header("foo", {"bar": 42}) == "foo; bar=42"
assert http.dump_options_header("foo", {"bar": 42, "fizz": None}) in (
Expand Down

0 comments on commit 84ad4ca

Please sign in to comment.