From 198e73b6eff29538ac433feda12d603da462bd7a Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Thu, 14 Oct 2021 07:51:32 -0400 Subject: [PATCH] Implement form encoding of sequence values. This is off by default to match the `doseq` parameter of urllib.parse.urlencode. --- sprockets/mixins/mediatype/transcoders.py | 41 +++++++++++++++++------ tests.py | 19 +++++++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/sprockets/mixins/mediatype/transcoders.py b/sprockets/mixins/mediatype/transcoders.py index 90b79df..ce24f54 100644 --- a/sprockets/mixins/mediatype/transcoders.py +++ b/sprockets/mixins/mediatype/transcoders.py @@ -258,6 +258,9 @@ class FormUrlEncodingOptions: encoding: str = 'utf-8' """Encoding use when generating the byte stream from character data.""" + encode_sequences: bool = False + """Encode sequence values as multiple name=value instances.""" + literal_mapping: dict[typing.Literal[None, True, False], str] = dataclasses.field(default_factory=lambda: { None: '', @@ -311,7 +314,12 @@ class FormUrlEncodedTranscoder: Types that are not explicitly mentioned above will result in :meth:`to_bytes` raising a :exc:`TypeError`. This transcoder differs slightly from others in that it does not include - support for encoding values that are nested collections. + support for encoding values that are nested collections without + explicit configuration. + + Support for sequence values can be enabled by setting the + :attr:`~FormUrlEncodingOptions.encode_sequences` attribute of + :attr:`.options`. .. attribute:: options :type: FormUrlEncodingOptions @@ -422,6 +430,9 @@ class FormUrlEncodedTranscoder: char_map: typing.Mapping[int, str], encoding: str) -> str: if isinstance(datum, str): pass # optimization: skip additional checks for strings + elif (isinstance(datum, (float, int, str, uuid.UUID)) + and not isinstance(datum, bool)): + datum = str(datum) elif datum in self.options.literal_mapping: datum = self.options.literal_mapping[datum] elif isinstance(datum, (bytearray, bytes, memoryview)): @@ -431,8 +442,6 @@ class FormUrlEncodedTranscoder: # and MUST be before the isinstance(datum, int) check since # Boolean literals are integers instances raise TypeError(f'{datum.__class__.__name__} is not serializable') - elif isinstance(datum, (float, int, str, uuid.UUID)): - datum = str(datum) elif hasattr(datum, 'isoformat'): datum = datum.isoformat() else: @@ -440,13 +449,25 @@ class FormUrlEncodedTranscoder: return ''.join(char_map[c] for c in datum.encode(encoding)) - @staticmethod def _convert_to_tuple_sequence( - value: type_info.Serializable + self, value: type_info.Serializable ) -> typing.Iterable[typing.Tuple[typing.Any, typing.Any]]: if isinstance(value, collections.abc.Mapping): - return value.items() - try: - return [(a, b) for a, b in value] # type: ignore - except (TypeError, ValueError): - raise TypeError('Cannot convert value to sequence of tuples') + tuples = value.items() + else: + try: + tuples = [(a, b) for a, b in value] # type: ignore + except (TypeError, ValueError): + raise TypeError('Cannot convert value to sequence of tuples') + + if self.options.encode_sequences: + tuples, in_tuples = [], tuples + for a, b in in_tuples: + if (not isinstance(b, (bytes, bytearray, memoryview, str)) + and isinstance(b, collections.abc.Iterable)): + for value in b: + tuples.append((a, value)) + else: + tuples.append((a, b)) + + return tuples diff --git a/tests.py b/tests.py index 330afd6..33dd0f5 100644 --- a/tests.py +++ b/tests.py @@ -679,7 +679,22 @@ class FormUrlEncodingTranscoderTests(unittest.TestCase): self.transcoder.to_bytes(value) def test_serialization_of_sequences(self): - sequence = [[1, 2, 3], {1, 2, 3}, (1, 2, 3)] - for value in sequence: + self.transcoder: transcoders.FormUrlEncodedTranscoder + + always_illegal = [[1, 2, 3], {1, 2, 3}, (1, 2, 3)] + + self.transcoder.options.encode_sequences = False + for value in always_illegal: with self.assertRaises(TypeError): self.transcoder.to_bytes(value) + + self.transcoder.options.encode_sequences = True + for value in always_illegal: + with self.assertRaises(TypeError): + self.transcoder.to_bytes(value) + + self.transcoder.options.encode_sequences = True + value = {'list': [1, 2], 'tuple': (1, 2), 'set': {1, 2}, 'str': 'val'} + _, result = self.transcoder.to_bytes(value) + self.assertEqual(b'list=1&list=2&tuple=1&tuple=2&set=1&set=2&str=val', + result)