From 3e0b5e34008a83818c036b9d7d558dd277c0c447 Mon Sep 17 00:00:00 2001 From: Peter Byfield Date: Mon, 21 Aug 2023 14:51:32 +0200 Subject: [PATCH 1/4] Extend preconf test cases to test date type structuring --- tests/test_preconf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_preconf.py b/tests/test_preconf.py index 5f57ece7..acb13e5e 100644 --- a/tests/test_preconf.py +++ b/tests/test_preconf.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import datetime, timezone, date from enum import Enum, IntEnum, unique from json import dumps as json_dumps from json import loads as json_loads @@ -15,6 +15,7 @@ characters, composite, datetimes, + dates, dictionaries, floats, frozensets, @@ -76,6 +77,7 @@ class AStringEnum(str, Enum): an_int_enum: AnIntEnum a_str_enum: AStringEnum a_datetime: datetime + a_date: date a_string_enum_dict: Dict[AStringEnum, int] a_bytes_dict: Dict[bytes, bytes] @@ -148,6 +150,7 @@ def everythings( Everything.AnIntEnum.A, Everything.AStringEnum.A, draw(dts), + draw(dates(min_value=date(1970, 1, 1), max_value=date(2038, 1, 1))), draw( dictionaries( just(Everything.AStringEnum.A), From e15fb530a451c84494cf654006408c1f82eb56c8 Mon Sep 17 00:00:00 2001 From: Peter Byfield Date: Mon, 21 Aug 2023 15:21:33 +0200 Subject: [PATCH 2/4] Adapt preconfigured converters to support date --- src/cattrs/preconf/bson.py | 10 ++++++++-- src/cattrs/preconf/cbor2.py | 4 +++- src/cattrs/preconf/json.py | 4 +++- src/cattrs/preconf/msgpack.py | 8 +++++++- src/cattrs/preconf/orjson.py | 4 +++- src/cattrs/preconf/pyyaml.py | 8 +++++++- src/cattrs/preconf/tomlkit.py | 8 +++++++- src/cattrs/preconf/ujson.py | 4 +++- 8 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/cattrs/preconf/bson.py b/src/cattrs/preconf/bson.py index cb05c60f..618a4907 100644 --- a/src/cattrs/preconf/bson.py +++ b/src/cattrs/preconf/bson.py @@ -1,6 +1,6 @@ """Preconfigured converters for bson.""" from base64 import b85decode, b85encode -from datetime import datetime +from datetime import datetime, date from typing import Any, Type, TypeVar from bson import DEFAULT_CODEC_OPTIONS, CodecOptions, ObjectId, decode, encode @@ -82,9 +82,15 @@ def gen_structure_mapping(cl: Any): [(is_mapping, gen_structure_mapping, True)] ) - converter.register_structure_hook(datetime, validate_datetime) converter.register_structure_hook(ObjectId, lambda v, _: ObjectId(v)) + # datetime inherits from date, so identity unstructure hook used + # here to prevent the date unstructure hook running. + converter.register_unstructure_hook(datetime, lambda v: v) + converter.register_structure_hook(datetime, validate_datetime) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) + def make_converter(*args: Any, **kwargs: Any) -> BsonConverter: kwargs["unstruct_collection_overrides"] = { diff --git a/src/cattrs/preconf/cbor2.py b/src/cattrs/preconf/cbor2.py index df5b481b..11756f04 100644 --- a/src/cattrs/preconf/cbor2.py +++ b/src/cattrs/preconf/cbor2.py @@ -1,5 +1,5 @@ """Preconfigured converters for cbor2.""" -from datetime import datetime, timezone +from datetime import datetime, timezone, date from typing import Any, Type, TypeVar from cbor2 import dumps, loads @@ -30,6 +30,8 @@ def configure_converter(converter: BaseConverter): converter.register_structure_hook( datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc) ) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def make_converter(*args: Any, **kwargs: Any) -> Cbor2Converter: diff --git a/src/cattrs/preconf/json.py b/src/cattrs/preconf/json.py index 1842ef46..61abc365 100644 --- a/src/cattrs/preconf/json.py +++ b/src/cattrs/preconf/json.py @@ -1,6 +1,6 @@ """Preconfigured converters for the stdlib json.""" from base64 import b85decode, b85encode -from datetime import datetime +from datetime import datetime, date from json import dumps, loads from typing import Any, Type, TypeVar, Union @@ -34,6 +34,8 @@ def configure_converter(converter: BaseConverter): converter.register_structure_hook(bytes, lambda v, _: b85decode(v)) converter.register_unstructure_hook(datetime, lambda v: v.isoformat()) converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def make_converter(*args: Any, **kwargs: Any) -> JsonConverter: diff --git a/src/cattrs/preconf/msgpack.py b/src/cattrs/preconf/msgpack.py index ef716759..eb13b6e6 100644 --- a/src/cattrs/preconf/msgpack.py +++ b/src/cattrs/preconf/msgpack.py @@ -1,5 +1,5 @@ """Preconfigured converters for msgpack.""" -from datetime import datetime, timezone +from datetime import datetime, timezone, date, time from typing import Any, Type, TypeVar from msgpack import dumps, loads @@ -30,6 +30,12 @@ def configure_converter(converter: BaseConverter): converter.register_structure_hook( datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc) ) + converter.register_unstructure_hook( + date, lambda v: datetime.combine(v, time(tzinfo=timezone.utc)).timestamp() + ) + converter.register_structure_hook( + date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date() + ) def make_converter(*args: Any, **kwargs: Any) -> MsgpackConverter: diff --git a/src/cattrs/preconf/orjson.py b/src/cattrs/preconf/orjson.py index 297a79fb..0be83049 100644 --- a/src/cattrs/preconf/orjson.py +++ b/src/cattrs/preconf/orjson.py @@ -1,6 +1,6 @@ """Preconfigured converters for orjson.""" from base64 import b85decode, b85encode -from datetime import datetime +from datetime import datetime, date from enum import Enum from typing import Any, Type, TypeVar, Union @@ -38,6 +38,8 @@ def configure_converter(converter: BaseConverter): converter.register_unstructure_hook(datetime, lambda v: v.isoformat()) converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def gen_unstructure_mapping(cl: Any, unstructure_to=None): key_handler = str diff --git a/src/cattrs/preconf/pyyaml.py b/src/cattrs/preconf/pyyaml.py index 9d182098..5de6e9cf 100644 --- a/src/cattrs/preconf/pyyaml.py +++ b/src/cattrs/preconf/pyyaml.py @@ -1,5 +1,5 @@ """Preconfigured converters for pyyaml.""" -from datetime import datetime +from datetime import datetime, date from typing import Any, Type, TypeVar from yaml import safe_dump, safe_load @@ -30,7 +30,13 @@ def configure_converter(converter: BaseConverter): converter.register_unstructure_hook( str, lambda v: v if v.__class__ is str else v.value ) + + # datetime inherits from date, so identity unstructure hook used + # here to prevent the date unstructure hook running. + converter.register_unstructure_hook(datetime, lambda v: v) converter.register_structure_hook(datetime, validate_datetime) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def make_converter(*args: Any, **kwargs: Any) -> PyyamlConverter: diff --git a/src/cattrs/preconf/tomlkit.py b/src/cattrs/preconf/tomlkit.py index 490c282f..5ee8d1c0 100644 --- a/src/cattrs/preconf/tomlkit.py +++ b/src/cattrs/preconf/tomlkit.py @@ -1,6 +1,6 @@ """Preconfigured converters for tomlkit.""" from base64 import b85decode, b85encode -from datetime import datetime +from datetime import datetime, date from enum import Enum from operator import attrgetter from typing import Any, Type, TypeVar @@ -59,7 +59,13 @@ def key_handler(k: bytes): converter._unstructure_func.register_func_list( [(is_mapping, gen_unstructure_mapping, True)] ) + + # datetime inherits from date, so identity unstructure hook used + # here to prevent the date unstructure hook running. + converter.register_unstructure_hook(datetime, lambda v: v) converter.register_structure_hook(datetime, validate_datetime) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def make_converter(*args: Any, **kwargs: Any) -> TomlkitConverter: diff --git a/src/cattrs/preconf/ujson.py b/src/cattrs/preconf/ujson.py index f326a802..d48abb43 100644 --- a/src/cattrs/preconf/ujson.py +++ b/src/cattrs/preconf/ujson.py @@ -1,6 +1,6 @@ """Preconfigured converters for ujson.""" from base64 import b85decode, b85encode -from datetime import datetime +from datetime import datetime, date from typing import Any, AnyStr, Type, TypeVar from ujson import dumps, loads @@ -35,6 +35,8 @@ def configure_converter(converter: BaseConverter): converter.register_unstructure_hook(datetime, lambda v: v.isoformat()) converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) + converter.register_unstructure_hook(date, lambda v: v.isoformat()) + converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) def make_converter(*args: Any, **kwargs: Any) -> UjsonConverter: From 4bab1f426d7b4afb3d50853dcc5a5103f4e9612e Mon Sep 17 00:00:00 2001 From: Peter Byfield Date: Mon, 21 Aug 2023 15:29:11 +0200 Subject: [PATCH 3/4] Update docs --- docs/preconf.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/preconf.md b/docs/preconf.md index c6847327..d68b3dfa 100644 --- a/docs/preconf.md +++ b/docs/preconf.md @@ -20,7 +20,7 @@ These converters support the following classes and type annotations, both for st - lists, homogenous tuples, heterogenous tuples, dictionaries, counters, sets, frozensets - optionals - sequences, mutable sequences, mappings, mutable mappings, sets, mutable sets -- `datetime.datetime` +- `datetime.datetime`, `datetime.date` ```{versionadded} 22.1.0 All preconf converters now have `loads` and `dumps` methods, which combine un/structuring and the de/serialization logic from their underlying libraries. @@ -57,13 +57,13 @@ poetry add --extras tomlkit cattrs Found at {mod}`cattrs.preconf.json`. -Bytes are serialized as base 85 strings. Counters are serialized as dictionaries. Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as ISO 8601 strings. +Bytes are serialized as base 85 strings. Counters are serialized as dictionaries. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings. ## _ujson_ Found at {mod}`cattrs.preconf.ujson`. -Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as ISO 8601 strings. +Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings. `ujson` doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807, nor does it support `float('inf')`. @@ -71,7 +71,7 @@ Bytes are serialized as base 85 strings. Sets are serialized as lists, and deser Found at {mod}`cattrs.preconf.orjson`. -Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as ISO 8601 strings. +Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. `datetime` s and `date` s are serialized as ISO 8601 strings. _orjson_ doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807. _orjson_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization. @@ -80,7 +80,7 @@ _orjson_ only supports mappings with string keys so mappings will have their key Found at {mod}`cattrs.preconf.msgpack`. -Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as UNIX timestamp float values. +Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as UNIX timestamp float values. `date` s are serialized as midnight-aligned UNIX timestamp float values. _msgpack_ doesn't support integers less than -9223372036854775808, and greater than 18446744073709551615. @@ -103,6 +103,8 @@ Tuples are serialized as lists. Use keyword argument `datetime_as_timestamp=True` to encode as UNIX timestamp integer/float (CBOR Tag 1) **note:** this replaces timezone information as UTC. +`date` s are serialized as ISO 8601 strings. + Use keyword argument `canonical=True` for efficient encoding to the smallest binary output. Floats can be forced to smaller output by casting to lower-precision formats by casting to `numpy` floats (and back to Python floats). @@ -118,6 +120,7 @@ _bson_ doesn't support integers less than -9223372036854775808 or greater than 9 _bson_ does not support null bytes in mapping keys. _bson_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization. The _bson_ datetime representation doesn't support microsecond accuracy. +`date` s are serialized as ISO 8601 strings. When encoding and decoding, the library needs to be passed `codec_options=bson.CodecOptions(tz_aware=True)` to get the full range of compatibility. @@ -125,7 +128,7 @@ When encoding and decoding, the library needs to be passed `codec_options=bson.C Found at {mod}`cattrs.preconf.pyyaml`. -Frozensets are serialized as lists, and deserialized back into frozensets. +Frozensets are serialized as lists, and deserialized back into frozensets. `date` s are serialized as ISO 8601 strings. ## _tomlkit_ @@ -133,4 +136,4 @@ Found at {mod}`cattrs.preconf.tomlkit`. Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets. Tuples are serialized as lists, and deserialized back into tuples. -_tomlkit_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization. +_tomlkit_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization. `date` s are serialized as ISO 8601 strings. From 333a9118999861ad507199fd0e7f7f5ee757d766 Mon Sep 17 00:00:00 2001 From: Peter Byfield Date: Mon, 21 Aug 2023 15:32:06 +0200 Subject: [PATCH 4/4] Update history --- HISTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 18da3bd5..6adb394b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -36,6 +36,8 @@ ([#412](https://github.com/python-attrs/cattrs/issues/412)) - Fix certain cases of structuring `Annotated` types. ([#418](https://github.com/python-attrs/cattrs/issues/418)) +- Add support for `date` to preconfigured converters. + ([#420](https://github.com/python-attrs/cattrs/pull/420)) ## 23.1.2 (2023-06-02)