Change form encoder to default to stringify.

This matches what urlencode would do and is will remove surprises where
a value is supported by the other transcoders but not the form encoder.
This commit is contained in:
Dave Shawley 2021-10-15 07:18:01 -04:00
parent 7f03f29175
commit 675ffbdf98
No known key found for this signature in database
GPG key ID: F41A8A99298F8EED
3 changed files with 39 additions and 41 deletions

View file

@ -282,9 +282,7 @@ class FormUrlEncodedTranscoder:
This transcoder implements transcoding according to the current
W3C documentation. The encoding interface takes mappings or
sequences of pairs and encodes both the name and value. The
following list describes how each supported type is encoded.
Any value type that is not on the list will result in a
:exc:`TypeError`.
following table describes how each supported type is encoded.
+----------------------------+---------------------------------------+
| Value / Type | Encoding |
@ -315,14 +313,16 @@ class FormUrlEncodedTranscoder:
.. warning::
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 without
explicit configuration.
:meth:`to_bytes` simply calling ``str(value)`` and encoding
the result. This causes nested sequences to be encoded as
their ``repr``. For example, encoding ``{'a': [1, 2]}`` will
result in ``a=%5B1%2C%202%5D``. This matches what
:func:`urllib.parse.urlencode` does by default.
Support for sequence values can be enabled by setting the
:attr:`~FormUrlEncodingOptions.encode_sequences` attribute of
:attr:`.options`.
Better support for sequence values can be enabled by setting
the :attr:`~FormUrlEncodingOptions.encode_sequences` attribute
of :attr:`.options`. This mimics the ``doseq`` parameter of
:func:`urllib,parse.urlencode`.
.. attribute:: options
:type: FormUrlEncodingOptions
@ -332,7 +332,7 @@ class FormUrlEncodedTranscoder:
"""
content_type = 'application/x-www-formurlencoded'
def __init__(self, **encoding_options) -> None:
def __init__(self, **encoding_options: typing.Any) -> None:
self.options = FormUrlEncodingOptions(**encoding_options)
def to_bytes(
@ -436,25 +436,23 @@ class FormUrlEncodedTranscoder:
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, collections.abc.Hashable)
and datum in self.options.literal_mapping):
# the isinstance Hashable check confuses mypy
datum = self.options.literal_mapping[datum] # type: ignore
elif isinstance(datum, (bytearray, bytes, memoryview)):
return ''.join(char_map[c] for c in datum)
elif datum is None or isinstance(datum, bool):
# This could happen if the user modifies the literal mapping
# 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 hasattr(datum, 'isoformat'):
elif isinstance(datum, type_info.DefinesIsoFormat):
datum = datum.isoformat()
else:
raise TypeError(f'{datum.__class__.__name__} is not serializable')
datum = str(datum)
return ''.join(char_map[c] for c in datum.encode(encoding))
def _convert_to_tuple_sequence(
self, value: type_info.Serializable
) -> typing.Iterable[typing.Tuple[typing.Any, typing.Any]]:
tuples: typing.Iterable[typing.Tuple[typing.Any, typing.Any]]
if isinstance(value, collections.abc.Mapping):
tuples = value.items()
else:
@ -464,13 +462,14 @@ class FormUrlEncodedTranscoder:
raise TypeError('Cannot convert value to sequence of tuples')
if self.options.encode_sequences:
tuples, in_tuples = [], tuples
for a, b in in_tuples:
out_tuples = []
for a, b in tuples:
if (not isinstance(b, (bytes, bytearray, memoryview, str))
and isinstance(b, collections.abc.Iterable)):
for value in b:
tuples.append((a, value))
out_tuples.append((a, value))
else:
tuples.append((a, b))
out_tuples.append((a, b))
tuples = out_tuples
return tuples

View file

@ -4,13 +4,14 @@ import typing
import uuid
try:
from typing import Protocol
from typing import Protocol, runtime_checkable
except ImportError:
# "ignore" is required to avoid an incompatible import
# error due to different bindings of _SpecialForm
from typing_extensions import Protocol # type: ignore
from typing_extensions import Protocol, runtime_checkable # type: ignore
@runtime_checkable
class DefinesIsoFormat(Protocol):
"""An object that has an isoformat method."""
def isoformat(self) -> str:

View file

@ -634,9 +634,13 @@ class FormUrlEncodingTranscoderTests(unittest.TestCase):
_, result = self.transcoder.to_bytes({'value': 'with space'})
self.assertEqual(b'value=with%20space', result)
def test_that_serializing_unsupported_types_fails(self):
with self.assertRaises(TypeError):
self.transcoder.to_bytes({'unsupported': object()})
def test_that_serializing_unsupported_types_stringifies(self):
obj = object()
# quick & dirty URL encoding
expected = str(obj).translate({0x20: '%20', 0x3C: '%3C', 0x3E: '%3E'})
_, result = self.transcoder.to_bytes({'unsupported': obj})
self.assertEqual(f'unsupported={expected}'.encode(), result)
def test_that_required_octets_are_encoded(self):
# build the set of all characters required to be encoded by
@ -675,26 +679,20 @@ class FormUrlEncodingTranscoderTests(unittest.TestCase):
self.transcoder: transcoders.FormUrlEncodedTranscoder
self.transcoder.options.literal_mapping.clear()
for value in {None, True, False}:
with self.assertRaises(TypeError):
self.transcoder.to_bytes(value)
_, result = self.transcoder.to_bytes(value)
self.assertEqual(str(value).encode(), result)
def test_serialization_of_sequences(self):
self.transcoder: transcoders.FormUrlEncodedTranscoder
always_illegal = [[1, 2, 3], {1, 2, 3}, (1, 2, 3)]
value = {'list': [1, 2], 'tuple': (1, 2), 'set': {1, 2}, 'str': 'val'}
self.transcoder.options.encode_sequences = False
for value in always_illegal:
with self.assertRaises(TypeError):
self.transcoder.to_bytes(value)
_, result = self.transcoder.to_bytes(value)
self.assertEqual((b'list=%5B1%2C%202%5D&tuple=%281%2C%202%29'
b'&set=%7B1%2C%202%7D&str=val'), result)
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)