From cdfacb474ff7f070c78ed87383662aec9eddafbf Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Fri, 7 Apr 2017 20:19:44 +0200 Subject: [PATCH 1/3] Allow Headers.add to get a list of parameters on *args This ensures the order of parameters, which can be useful for issues like supporting IE8 in flask's #2223 --- werkzeug/datastructures.py | 8 +++++++- werkzeug/http.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index ee620e935..d976db2f5 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -1140,7 +1140,7 @@ def __iter__(self): def __len__(self): return len(self._list) - def add(self, _key, _value, **kw): + def add(self, _key, _value, *args, **kw): """Add a new header tuple to the list. Keyword arguments can specify additional parameters for the header @@ -1153,9 +1153,15 @@ def add(self, _key, _value, **kw): The keyword argument dumping uses :func:`dump_options_header` behind the scenes. + In some cases, it would be needed to ensure the order of parameters. + For those cases, a list of ``(key, value)`` tuples can be passed in + *args. + .. versionadded:: 0.4.1 keyword arguments were added for :mod:`wsgiref` compatibility. """ + if args: + _value = dump_options_header(_value, args) if kw: _value = _options_header_vkw(_value, kw) _value = _unicodify_header_value(_value) diff --git a/werkzeug/http.py b/werkzeug/http.py index 0b1876f2f..fa9e82b09 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -212,7 +212,11 @@ def dump_options_header(header, options): segments = [] if header is not None: segments.append(header) - for key, value in iteritems(options): + if isinstance(options, tuple): + iter_func = iter + else: + iter_func = iteritems + for key, value in iter_func(options): if value is None: segments.append(key) else: From 2ed855660e40135bbeb2d4453c3f24183e660714 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 7 Apr 2017 17:35:43 -0700 Subject: [PATCH 2/3] add test for positional header parameters pass args to _options_header_vkw allow positional args to set too more permissive mapping vs sequence check --- tests/test_datastructures.py | 3 +++ werkzeug/datastructures.py | 48 ++++++++++++++++++++++++------------ werkzeug/http.py | 9 +++---- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 28bfa8b15..558158bd2 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -631,6 +631,9 @@ def test_basic_interface(self): headers.add('x', 'y', z='"') assert headers['x'] == r'y; z="\""' + headers.add('multi', 'yes', ('a', '1',), ('b', '2'), c='3') + assert headers['multi'] == 'yes; a=1; b=2; c=3' + def test_defaults_and_conversion(self): # defaults headers = self.storage_class([ diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index d976db2f5..5f7f267a3 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -889,9 +889,20 @@ def popitemlist(self): return key, [x.value for x in buckets] -def _options_header_vkw(value, kw): - return dump_options_header(value, dict((k.replace('_', '-'), v) - for k, v in kw.items())) +def _options_header_vkw(value, args, kwargs): + if args: + value = dump_options_header( + value, + ((k.replace('_', '-'), v) for k, v in args) + ) + + if kwargs: + value = dump_options_header( + value, + dict((k.replace('_', '-'), v) for k, v in iteritems(kwargs)) + ) + + return value def _unicodify_header_value(value): @@ -1140,7 +1151,7 @@ def __iter__(self): def __len__(self): return len(self._list) - def add(self, _key, _value, *args, **kw): + def add(self, _key, _value, *args, **kwargs): """Add a new header tuple to the list. Keyword arguments can specify additional parameters for the header @@ -1153,17 +1164,23 @@ def add(self, _key, _value, *args, **kw): The keyword argument dumping uses :func:`dump_options_header` behind the scenes. - In some cases, it would be needed to ensure the order of parameters. + In some cases the order of the additional parameters is important. For those cases, a list of ``(key, value)`` tuples can be passed in - *args. + ``*args``:: + + d.add( + 'Content-Disposition', 'attachment', + ('filename', 'foo.png'), ('filename*', "UTF-8''fo%C3%B6.png") + ) + + .. versionadded:: 0.13.0 + Key/value tuples as positional arguments are supported to ensure + the order of parameters. .. versionadded:: 0.4.1 keyword arguments were added for :mod:`wsgiref` compatibility. """ - if args: - _value = dump_options_header(_value, args) - if kw: - _value = _options_header_vkw(_value, kw) + _value = _options_header_vkw(_value, args, kwargs) _value = _unicodify_header_value(_value) self._validate_value(_value) self._list.append((_key, _value)) @@ -1187,14 +1204,14 @@ def clear(self): """Clears all headers.""" del self._list[:] - def set(self, _key, _value, **kw): + def set(self, _key, _value, *args, **kwargs): """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. + Positional and 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`. @@ -1202,8 +1219,7 @@ def set(self, _key, _value, **kw): :param key: The key to be inserted. :param value: The value to be inserted. """ - if kw: - _value = _options_header_vkw(_value, kw) + _value = _options_header_vkw(_value, args, kwargs) _value = _unicodify_header_value(_value) self._validate_value(_value) if not self._list: diff --git a/werkzeug/http.py b/werkzeug/http.py index fa9e82b09..d3a7f2237 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -17,6 +17,7 @@ :license: BSD, see LICENSE for more details. """ import re +from collections import Mapping from time import time, gmtime try: from email.utils import parsedate_tz @@ -207,15 +208,13 @@ def dump_options_header(header, options): """The reverse function to :func:`parse_options_header`. :param header: the header to dump - :param options: a dict of options to append. + :param options: a dict of options to append. Or pass a sequence of + key, value pairs to ensure the order of the keys """ segments = [] if header is not None: segments.append(header) - if isinstance(options, tuple): - iter_func = iter - else: - iter_func = iteritems + iter_func = iteritems if isinstance(options, Mapping) else iter for key, value in iter_func(options): if value is None: segments.append(key) From e2be79bfb57721683169ec95ffc280f99954aea7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 7 Apr 2017 18:04:15 -0700 Subject: [PATCH 3/3] minor docs fix --- werkzeug/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/http.py b/werkzeug/http.py index d3a7f2237..0e4f9b754 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -209,7 +209,7 @@ def dump_options_header(header, options): :param header: the header to dump :param options: a dict of options to append. Or pass a sequence of - key, value pairs to ensure the order of the keys + ``(key, value)`` pairs to ensure the order of the keys. """ segments = [] if header is not None: