diff --git a/sprockets/mixins/mediatype/transcoders.py b/sprockets/mixins/mediatype/transcoders.py index c86891a..c28042c 100644 --- a/sprockets/mixins/mediatype/transcoders.py +++ b/sprockets/mixins/mediatype/transcoders.py @@ -322,28 +322,34 @@ class FormUrlEncodedTranscoder: #urlencoded-serializing """ - # Generate a sequence of name+value tuples to encode - if isinstance(inst_data, collections.abc.Mapping): - tuples = ((self._normalize(a), self._normalize(b)) - for a, b in inst_data.items()) - else: - tuples = ((self._normalize(a), self._normalize(b)) - for a, b in inst_data) - - # Encode each pair and run the encoded form through the - # appropriate octet to string mapping table + # Select the appropriate encoding table and use the default + # character encoding if necessary. Binding these to locals + # removes branches from the inner loop. chr_map: typing.Mapping[int, str] chr_map = (_FORM_URLENCODING_PLUS if self.options.space_as_plus else _FORM_URLENCODING) if encoding is None: encoding = self.options.encoding + + # Generate a sequence of name+value tuples to encode + if isinstance(inst_data, type_info.SerializablePrimitives): + encoded = self._encode(inst_data, chr_map, encoding) + return self.content_type, encoded.encode('ascii') + + if isinstance(inst_data, collections.abc.Mapping): + tuples = inst_data.items() + else: + tuples = inst_data + + # Encode each pair and run the encoded form through the + # appropriate octet to string mapping table prefix = '' # micro-optimization removes if statement from inner loop buf = [] for name, value in tuples: buf.append(prefix) - buf.extend(chr_map[c] for c in name.encode(encoding)) + buf.extend(self._encode(name, chr_map, encoding)) buf.append('=') - buf.extend(chr_map[c] for c in value.encode(encoding)) + buf.extend(self._encode(value, chr_map, encoding)) prefix = '&' return self.content_type, ''.join(buf).encode('ascii') @@ -386,10 +392,9 @@ class FormUrlEncodedTranscoder: return dict(output) - def _normalize( - self, datum: typing.Union[bool, None, float, int, str, - type_info.DefinesIsoFormat] - ) -> str: + def _encode(self, datum: typing.Union[bool, None, float, int, str, + type_info.DefinesIsoFormat], + char_map: typing.Mapping[int, str], encoding: str) -> str: try: datum = self.options.literal_mapping[datum] # type: ignore except (KeyError, TypeError): @@ -397,9 +402,11 @@ class FormUrlEncodedTranscoder: datum = str(datum) elif hasattr(datum, 'isoformat'): datum = datum.isoformat() + elif isinstance(datum, (bytearray, bytes, memoryview)): + return ''.join(char_map[c] for c in datum) else: raise TypeError( f'{datum.__class__.__name__} is not serializable' ) from None - return datum + return ''.join(char_map[c] for c in datum.encode(encoding)) diff --git a/sprockets/mixins/mediatype/type_info.py b/sprockets/mixins/mediatype/type_info.py index 06e0e72..4b63a3a 100644 --- a/sprockets/mixins/mediatype/type_info.py +++ b/sprockets/mixins/mediatype/type_info.py @@ -24,6 +24,10 @@ class HasSettings(Protocol): """Application settings.""" +SerializablePrimitives = (type(None), bool, bytearray, bytes, float, int, + memoryview, str, uuid.UUID) +"""Use this with isinstance to identify simple values.""" + Serializable = typing.Union[DefinesIsoFormat, None, bool, bytearray, bytes, float, int, memoryview, str, typing.Mapping, typing.Sequence, typing.Set, uuid.UUID] diff --git a/tests.py b/tests.py index 4ad5eca..dd6d846 100644 --- a/tests.py +++ b/tests.py @@ -621,3 +621,18 @@ class FormUrlEncodingTranscoderTests(unittest.TestCase): expected = f'test_string={expected}'.encode() _, result = self.transcoder.to_bytes({'test_string': test_string}) self.assertEqual(expected, result) + + def test_serialization_of_primitives(self): + expectations = { + None: b'', + 'a string': b'a%20string', + 10: b'10', + 2.3: str(2.3).encode(), + True: b'true', + False: b'false', + b'\xfe\xed\xfa\xce': b'%FE%ED%FA%CE', + memoryview(b'\xfe\xed\xfa\xce'): b'%FE%ED%FA%CE', + } + for value, expected in expectations.items(): + _, result = self.transcoder.to_bytes(value) + self.assertEqual(expected, result)