Skip to content

Commit 1f5781b

Browse files
authored
Add date to preconfigured converters (#420)
* Extend preconf test cases to test date type structuring * Adapt preconfigured converters to support date * Update docs * Update history
1 parent acf92d2 commit 1f5781b

11 files changed

Lines changed: 57 additions & 17 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
([#412](https://github.com/python-attrs/cattrs/issues/412))
3737
- Fix certain cases of structuring `Annotated` types.
3838
([#418](https://github.com/python-attrs/cattrs/issues/418))
39+
- Add support for `date` to preconfigured converters.
40+
([#420](https://github.com/python-attrs/cattrs/pull/420))
3941

4042

4143
## 23.1.2 (2023-06-02)

docs/preconf.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ These converters support the following classes and type annotations, both for st
2020
- lists, homogenous tuples, heterogenous tuples, dictionaries, counters, sets, frozensets
2121
- optionals
2222
- sequences, mutable sequences, mappings, mutable mappings, sets, mutable sets
23-
- `datetime.datetime`
23+
- `datetime.datetime`, `datetime.date`
2424

2525
```{versionadded} 22.1.0
2626
All preconf converters now have `loads` and `dumps` methods, which combine un/structuring and the de/serialization logic from their underlying libraries.
@@ -57,21 +57,21 @@ poetry add --extras tomlkit cattrs
5757

5858
Found at {mod}`cattrs.preconf.json`.
5959

60-
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.
60+
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.
6161

6262
## _ujson_
6363

6464
Found at {mod}`cattrs.preconf.ujson`.
6565

66-
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.
66+
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.
6767

6868
`ujson` doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807, nor does it support `float('inf')`.
6969

7070
## _orjson_
7171

7272
Found at {mod}`cattrs.preconf.orjson`.
7373

74-
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.
74+
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.
7575

7676
_orjson_ doesn't support integers less than -9223372036854775808, and greater than 9223372036854775807.
7777
_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
8080

8181
Found at {mod}`cattrs.preconf.msgpack`.
8282

83-
Sets are serialized as lists, and deserialized back into sets. `datetime` s are serialized as UNIX timestamp float values.
83+
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.
8484

8585
_msgpack_ doesn't support integers less than -9223372036854775808, and greater than 18446744073709551615.
8686

@@ -103,6 +103,8 @@ Tuples are serialized as lists.
103103
Use keyword argument `datetime_as_timestamp=True` to encode as UNIX timestamp integer/float (CBOR Tag 1)
104104
**note:** this replaces timezone information as UTC.
105105

106+
`date` s are serialized as ISO 8601 strings.
107+
106108
Use keyword argument `canonical=True` for efficient encoding to the smallest binary output.
107109

108110
Floats can be forced to smaller output by casting to lower-precision formats by casting to `numpy` floats (and back to Python floats).
@@ -118,19 +120,20 @@ _bson_ doesn't support integers less than -9223372036854775808 or greater than 9
118120
_bson_ does not support null bytes in mapping keys.
119121
_bson_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization.
120122
The _bson_ datetime representation doesn't support microsecond accuracy.
123+
`date` s are serialized as ISO 8601 strings.
121124

122125
When encoding and decoding, the library needs to be passed `codec_options=bson.CodecOptions(tz_aware=True)` to get the full range of compatibility.
123126

124127
## _pyyaml_
125128

126129
Found at {mod}`cattrs.preconf.pyyaml`.
127130

128-
Frozensets are serialized as lists, and deserialized back into frozensets.
131+
Frozensets are serialized as lists, and deserialized back into frozensets. `date` s are serialized as ISO 8601 strings.
129132

130133
## _tomlkit_
131134

132135
Found at {mod}`cattrs.preconf.tomlkit`.
133136

134137
Bytes are serialized as base 85 strings. Sets are serialized as lists, and deserialized back into sets.
135138
Tuples are serialized as lists, and deserialized back into tuples.
136-
_tomlkit_ only supports mappings with string keys so mappings will have their keys stringified before serialization, and destringified during deserialization.
139+
_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.

src/cattrs/preconf/bson.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Preconfigured converters for bson."""
22
from base64 import b85decode, b85encode
3-
from datetime import datetime
3+
from datetime import datetime, date
44
from typing import Any, Type, TypeVar
55

66
from bson import DEFAULT_CODEC_OPTIONS, CodecOptions, ObjectId, decode, encode
@@ -82,9 +82,15 @@ def gen_structure_mapping(cl: Any):
8282
[(is_mapping, gen_structure_mapping, True)]
8383
)
8484

85-
converter.register_structure_hook(datetime, validate_datetime)
8685
converter.register_structure_hook(ObjectId, lambda v, _: ObjectId(v))
8786

87+
# datetime inherits from date, so identity unstructure hook used
88+
# here to prevent the date unstructure hook running.
89+
converter.register_unstructure_hook(datetime, lambda v: v)
90+
converter.register_structure_hook(datetime, validate_datetime)
91+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
92+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
93+
8894

8995
def make_converter(*args: Any, **kwargs: Any) -> BsonConverter:
9096
kwargs["unstruct_collection_overrides"] = {

src/cattrs/preconf/cbor2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Preconfigured converters for cbor2."""
2-
from datetime import datetime, timezone
2+
from datetime import datetime, timezone, date
33
from typing import Any, Type, TypeVar
44

55
from cbor2 import dumps, loads
@@ -30,6 +30,8 @@ def configure_converter(converter: BaseConverter):
3030
converter.register_structure_hook(
3131
datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc)
3232
)
33+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
34+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
3335

3436

3537
def make_converter(*args: Any, **kwargs: Any) -> Cbor2Converter:

src/cattrs/preconf/json.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Preconfigured converters for the stdlib json."""
22
from base64 import b85decode, b85encode
3-
from datetime import datetime
3+
from datetime import datetime, date
44
from json import dumps, loads
55
from typing import Any, Type, TypeVar, Union
66

@@ -34,6 +34,8 @@ def configure_converter(converter: BaseConverter):
3434
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
3535
converter.register_unstructure_hook(datetime, lambda v: v.isoformat())
3636
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
37+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
38+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
3739

3840

3941
def make_converter(*args: Any, **kwargs: Any) -> JsonConverter:

src/cattrs/preconf/msgpack.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Preconfigured converters for msgpack."""
2-
from datetime import datetime, timezone
2+
from datetime import datetime, timezone, date, time
33
from typing import Any, Type, TypeVar
44

55
from msgpack import dumps, loads
@@ -30,6 +30,12 @@ def configure_converter(converter: BaseConverter):
3030
converter.register_structure_hook(
3131
datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc)
3232
)
33+
converter.register_unstructure_hook(
34+
date, lambda v: datetime.combine(v, time(tzinfo=timezone.utc)).timestamp()
35+
)
36+
converter.register_structure_hook(
37+
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
38+
)
3339

3440

3541
def make_converter(*args: Any, **kwargs: Any) -> MsgpackConverter:

src/cattrs/preconf/orjson.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Preconfigured converters for orjson."""
22
from base64 import b85decode, b85encode
3-
from datetime import datetime
3+
from datetime import datetime, date
44
from enum import Enum
55
from typing import Any, Type, TypeVar, Union
66

@@ -38,6 +38,8 @@ def configure_converter(converter: BaseConverter):
3838

3939
converter.register_unstructure_hook(datetime, lambda v: v.isoformat())
4040
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
41+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
42+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
4143

4244
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
4345
key_handler = str

src/cattrs/preconf/pyyaml.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Preconfigured converters for pyyaml."""
2-
from datetime import datetime
2+
from datetime import datetime, date
33
from typing import Any, Type, TypeVar
44

55
from yaml import safe_dump, safe_load
@@ -30,7 +30,13 @@ def configure_converter(converter: BaseConverter):
3030
converter.register_unstructure_hook(
3131
str, lambda v: v if v.__class__ is str else v.value
3232
)
33+
34+
# datetime inherits from date, so identity unstructure hook used
35+
# here to prevent the date unstructure hook running.
36+
converter.register_unstructure_hook(datetime, lambda v: v)
3337
converter.register_structure_hook(datetime, validate_datetime)
38+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
39+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
3440

3541

3642
def make_converter(*args: Any, **kwargs: Any) -> PyyamlConverter:

src/cattrs/preconf/tomlkit.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Preconfigured converters for tomlkit."""
22
from base64 import b85decode, b85encode
3-
from datetime import datetime
3+
from datetime import datetime, date
44
from enum import Enum
55
from operator import attrgetter
66
from typing import Any, Type, TypeVar
@@ -59,7 +59,13 @@ def key_handler(k: bytes):
5959
converter._unstructure_func.register_func_list(
6060
[(is_mapping, gen_unstructure_mapping, True)]
6161
)
62+
63+
# datetime inherits from date, so identity unstructure hook used
64+
# here to prevent the date unstructure hook running.
65+
converter.register_unstructure_hook(datetime, lambda v: v)
6266
converter.register_structure_hook(datetime, validate_datetime)
67+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
68+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
6369

6470

6571
def make_converter(*args: Any, **kwargs: Any) -> TomlkitConverter:

src/cattrs/preconf/ujson.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Preconfigured converters for ujson."""
22
from base64 import b85decode, b85encode
3-
from datetime import datetime
3+
from datetime import datetime, date
44
from typing import Any, AnyStr, Type, TypeVar
55

66
from ujson import dumps, loads
@@ -35,6 +35,8 @@ def configure_converter(converter: BaseConverter):
3535

3636
converter.register_unstructure_hook(datetime, lambda v: v.isoformat())
3737
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
38+
converter.register_unstructure_hook(date, lambda v: v.isoformat())
39+
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
3840

3941

4042
def make_converter(*args: Any, **kwargs: Any) -> UjsonConverter:

0 commit comments

Comments
 (0)