diff --git a/RELEASES.rst b/RELEASES.rst index 1e9d4aa..7425565 100644 --- a/RELEASES.rst +++ b/RELEASES.rst @@ -6,11 +6,18 @@ Releases *Unreleased* -- Add ``IpAddress`` field. -- Extended string types now subclass ``Text`` not ``Str``. -- Remove `fields.create()` method -- Completely rework error handling. -- Make ``Tag.lookup_tag`` default to module + qualname. +- Improve ``Uuid`` field normalization. +- Add ``IpAddress`` field. (`667ca96`_) +- Extended string types now subclass ``Text`` not ``Str``. (`5862375`_) +- Remove `fields.create()` method. (`2a54886`_) +- Completely rework error handling. (`c95b6b3`_) +- Make ``Tag.lookup_tag`` default to module + qualname. (`26963d6`_) + +.. _26963d6: https://github.com/rossmacarthur/serde/commit/26963d65b45229db50444665a3160bd0f1358421 +.. _c95b6b3: https://github.com/rossmacarthur/serde/commit/c95b6b329125164e3301a7e43292c7a858d5e34f +.. _2a54886: https://github.com/rossmacarthur/serde/commit/2a548865106b1cfc98f5dea8bf00084f62e6334e +.. _5862375: https://github.com/rossmacarthur/serde/commit/5862375ac82a98364d7cf7ef82c6bb668d9abded +.. _667ca96: https://github.com/rossmacarthur/serde/commit/667ca9629028b29a6d420a9ce4eb14460baca448 0.7.3 ----- @@ -26,15 +33,18 @@ Releases *Released on December 2nd, 2019* -- Add ``FrozenSet`` field. -- Add ``Deque`` field. +- Add ``FrozenSet`` field. (`1d2f0cd`_) +- Add ``Deque`` field. (`ec44671`_) - Add ``default`` keyword argument to ``Field``. (`#111`_) -- Fix bug in ``Uuid.normalize()``. +- Fix bug in ``Uuid.normalize()``. (`408bc83`_) - Rename ``Constant`` field to ``Literal``. (`#118`_) -.. _#118: https://github.com/rossmacarthur/serde/pull/118 +.. _408bc83: https://github.com/rossmacarthur/serde/commit/408bc834ee8c4dbf92438ebcc6e94989927e2d89 +.. _ec44671: https://github.com/rossmacarthur/serde/commit/ec44671704529aca505fc10716cde030c9295188 +.. _1d2f0cd: https://github.com/rossmacarthur/serde/commit/1d2f0cd09cc49e350ac36ba2697e8023a2f9e47f .. _#111: https://github.com/rossmacarthur/serde/pull/111 +.. _#118: https://github.com/rossmacarthur/serde/pull/118 0.7.1 ----- @@ -201,8 +211,8 @@ Releases *Released on December 19th, 2018* -- Fix a bug where overriding ``Model.__init__()`` method affected ``Model.from_dict``. - (`#45`_, `#46`_) +- Fix a bug where overriding ``Model.__init__()`` method affected + ``Model.from_dict``. (`#45`_, `#46`_) .. _#46: https://github.com/rossmacarthur/serde/pull/46 @@ -300,11 +310,11 @@ Releases *Released on October 27th, 2018* -- Initial release +- Initial release, fixed. 0.1.0 ----- *Released on October 27th, 2018* -- This release is broken and was yanked. +- Initial release, yanked. diff --git a/src/serde/fields.py b/src/serde/fields.py index 61a86ef..a119901 100644 --- a/src/serde/fields.py +++ b/src/serde/fields.py @@ -8,7 +8,7 @@ import uuid import isodate -from six import binary_type, integer_types, text_type +from six import PY3, binary_type, integer_types, text_type from six.moves.collections_abc import Mapping as MappingType from serde.exceptions import ContextError, ValidationError @@ -1090,31 +1090,52 @@ class Uuid(Instance): A `~uuid.UUID` field. A `Uuid` field validates that the input data is a UUID. It serializes the - UUID as a hex string, and deserializes hex strings or integers as UUIDs. + UUID into the specified output form and deserializes hex strings, bytes, + fields, or integers as UUIDs. Args: + output_form (str): the type of output form to serialize to. Possible + values include 'str', 'urn', 'hex', 'int', 'bytes', or 'fields'. **kwargs: keyword arguments for the `Field` constructor. """ - def __init__(self, **kwargs): + def __init__(self, output_form='str', **kwargs): """ Create a new `Uuid`. """ + if output_form not in ('str', 'urn', 'hex', 'int', 'bytes', 'fields'): + raise ValueError('invalid output form') super(Uuid, self).__init__(uuid.UUID, **kwargs) + self.output_form = output_form def serialize(self, value): """ Serialize the given `~uuid.UUID` as a string. """ - return str(value) + if self.output_form == 'str': + return str(value) + else: + return getattr(value, self.output_form) def normalize(self, value): """ Normalize the value into a `~uuid.UUID`. """ if not isinstance(value, uuid.UUID): - input_form = 'int' if isinstance(value, integer_types) else 'hex' - return uuid.UUID(**{input_form: value}) + input_form = None + if isinstance(value, text_type): + input_form = 'hex' + elif isinstance(value, binary_type): + input_form = 'bytes' if PY3 or len(value) == 16 else 'hex' + elif isinstance(value, integer_types): + input_form = 'int' + elif isinstance(value, (list, tuple)): + input_form = 'fields' + if input_form: + try: + return uuid.UUID(**{input_form: value}) + except ValueError: + pass return value diff --git a/tests/test_fields.py b/tests/test_fields.py index 4383862..00fc139 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1357,10 +1357,22 @@ def test_validate(self): class TestUuid: - def test___init__(self): + def test___init___basic(self): # Construct a basic Uuid and check values are set correctly. field = Uuid() assert field.ty == uuid.UUID + assert field.output_form == 'str' + + def test___init___options(self): + # Construct a Uuid with extra options and check values are set correctly. + field = Uuid(output_form='hex', validators=[None]) + assert field.ty == uuid.UUID + assert field.output_form == 'hex' + + def test___init___invalid_output_form(self): + # Check that an invalid output form is denied. + with raises(ValueError): + Uuid(output_form='invalid') def test_serialize(self): # A Uuid should serialize a uuid.UUID as a string. @@ -1368,6 +1380,21 @@ def test_serialize(self): value = uuid.UUID('2d7026c8-cc58-11e8-bd7a-784f4386978e') assert field.serialize(value) == '2d7026c8-cc58-11e8-bd7a-784f4386978e' + def test_serialize_output_form(self): + # A Uuid should serialize a uuid.UUID based on the output form. + value = uuid.UUID('c07fb668-b3cb-4719-9b3d-0881d5eeba3b') + cases = [ + ('str', 'c07fb668-b3cb-4719-9b3d-0881d5eeba3b'), + ('urn', 'urn:uuid:c07fb668-b3cb-4719-9b3d-0881d5eeba3b'), + ('hex', 'c07fb668b3cb47199b3d0881d5eeba3b'), + ('int', 255874896585658101253640125750883301947), + ('bytes', b'\xc0\x7f\xb6h\xb3\xcbG\x19\x9b=\x08\x81\xd5\xee\xba;'), + ('fields', (3229595240, 46027, 18201, 155, 61, 9353732995643)), + ] + for output_form, expected in cases: + field = Uuid(output_form=output_form) + assert field.serialize(value) == expected + def test_normalize_uuid(self): # A Uuid should normalize a uuid.UUID as a uuid.UUID field = Uuid() @@ -1382,14 +1409,37 @@ def test_normalize_str(self): '2d7026c8-cc58-11e8-bd7a-784f4386978e' ) + def test_normalize_bytes(self): + # A Uuid should normalize a byte string a a uuid.UUID. + field = Uuid() + value = b'\x99\x1a\xf7\xc7\xee\x17G\x02\xb6C\xe2\x93<\xe8:\x01' + field.normalize(value) == uuid.UUID('991af7c7-ee17-4702-b643-e2933ce83a01') + def test_normalize_int(self): - # A Uuid should normalize a string as a uuid.UUID. + # A Uuid should normalize an integer as a uuid.UUID. field = Uuid() value = 255874896585658101253640125750883301947 assert field.normalize(value) == uuid.UUID( 'c07fb668-b3cb-4719-9b3d-0881d5eeba3b' ) + def test_normalize_fields(self): + # A Uuid should normalize a tuple/list as a uuid.UUID. + field = Uuid() + value = (3375074170, 20614, 19730, 172, 202, 2390245548685) + assert field.normalize(value) == uuid.UUID( + 'c92b8b7a-5086-4d12-acca-022c85bca28d' + ) + assert field.normalize(list(value)) == uuid.UUID( + 'c92b8b7a-5086-4d12-acca-022c85bca28d' + ) + + def test_normalize_invalid(self): + # A Uuid should not raise an error on a normalization failure. + field = Uuid() + value = b'\x99' + assert field.normalize(value) == value + def test_validate(self): # A Uuid should validate that the value is an instance of uuid.UUID. field = Uuid()