Skip to content

Add date to preconfigured converters #420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 10 additions & 7 deletions docs/preconf.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -57,21 +57,21 @@ 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')`.

## _orjson_

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.
Expand All @@ -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.

Expand All @@ -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).
Expand All @@ -118,19 +120,20 @@ _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.

## _pyyaml_

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_

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.
10 changes: 8 additions & 2 deletions src/cattrs/preconf/bson.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"] = {
Expand Down
4 changes: 3 additions & 1 deletion src/cattrs/preconf/cbor2.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/cattrs/preconf/json.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion src/cattrs/preconf/msgpack.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/cattrs/preconf/orjson.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/cattrs/preconf/pyyaml.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion src/cattrs/preconf/tomlkit.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/cattrs/preconf/ujson.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion tests/test_preconf.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +15,7 @@
characters,
composite,
datetimes,
dates,
dictionaries,
floats,
frozensets,
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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),
Expand Down