Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ Added
``JSONARGPARSE_PARSER_MODE`` environment variable, to change the mode of
parsers not given ``parser_mode`` (`#997
<https://github.com/mauvilsa/jsonargparse/pull/997>`__).
- Support for ``datetime``, ``date`` and ``time`` (ISO 8601), ``re.Pattern``
and ``Fraction`` types (`#1002
<https://github.com/mauvilsa/jsonargparse/pull/1002>`__).

Fixed
^^^^^
Expand Down
11 changes: 6 additions & 5 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,8 @@ Types can be nested with any complexity. Notes about the support:
:ref:`list-append`), ``Deque``, ``Iterable``, ``Sequence``,
``MutableSequence``, ``Collection``, ``Container``, ``Reversible``,
``Any``/``object``, ``Union``/``Optional`` (see :ref:`union-types`),
``Literal``, ``Type``, ``Enum``, ``PathLike``, ``UUID``, ``timedelta``, the
``Literal``, ``Type``, ``Enum``, ``PathLike``, ``UUID``, ``Fraction``,
``re.Pattern``, ``datetime``/``date``/``time`` (ISO 8601), ``timedelta``, the
restricted types of :ref:`restricted-numbers` and :ref:`restricted-strings`,
and the path and URL types of :ref:`parsing-paths` and :ref:`parsing-urls`.

Expand Down Expand Up @@ -1204,7 +1205,7 @@ representation back, only the class is needed. This is how
``jsonargparse.typing`` registers complex numbers, ``register_type(complex)``,
which is the same as ``register_type(complex, serializer=str,
deserializer=complex)``. Other classes need a serializer and/or a deserializer,
for example ``datetime``:
for example ``datetime`` with a format other than the default ISO 8601:

.. testcode::

Expand All @@ -1214,18 +1215,18 @@ for example ``datetime``:


def serializer(v):
return v.isoformat()
return v.strftime("%d/%m/%Y %H:%M")


def deserializer(v):
return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S")
return datetime.strptime(v, "%d/%m/%Y %H:%M")


register_type(datetime, serializer, deserializer)

parser = ArgumentParser()
parser.add_argument("--datetime", type=datetime)
parser.parse_args(["--datetime=2008-09-03T20:56:35"])
parser.parse_args(["--datetime=03/09/2008 20:56"])

Registering an already registered type replaces the previous one, jsonargparse's
own registrations included. A debug log names the module of each, useful when
Expand Down
29 changes: 29 additions & 0 deletions jsonargparse/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,35 @@ def raise_error():
register_type_on_first_use("datetime.timedelta", deserializer=timedelta_deserializer)


def isoformat_serializer(value) -> str:
return value.isoformat()


def fromisoformat_deserializer(class_name: str) -> Callable:
def deserializer(value):
import datetime

return getattr(datetime, class_name).fromisoformat(value)

return deserializer


for _name in ["datetime", "date", "time"]:
register_type_on_first_use(
f"datetime.{_name}", serializer=isoformat_serializer, deserializer=fromisoformat_deserializer(_name)
)


def pattern_serializer(value: re.Pattern) -> str:
return value.pattern
Comment thread
mauvilsa marked this conversation as resolved.
Outdated
Comment thread
mauvilsa marked this conversation as resolved.
Outdated


register_type_on_first_use(
"re.Pattern", serializer=pattern_serializer, deserializer=re.compile, deserializer_exceptions=(re.error, TypeError)
)
register_type_on_first_use("fractions.Fraction")


def bytes_serializer(value: bytes | bytearray) -> str:
from base64 import b64encode

Expand Down
15 changes: 4 additions & 11 deletions jsonargparse_tests/test_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,20 +402,13 @@ def test_type_any_dump_registered_type(parser):
# registered types serialize the same as when they are the type of the argument
parser.add_argument("--path", type=Any, default=Path_fr(__file__))
parser.add_argument("--bytes", type=Any, default=b"ab")
cfg = parser.parse_args([])
with assert_dump_warnings(serialized_as("Path_fr", "str"), serialized_as("bytes", "str")):
dump = json_or_yaml_load(parser.dump(cfg))
assert dump == {"path": __file__, "bytes": "YWI="}


def test_type_any_dump_date_not_serializable(parser):
# the loaders don't parse timestamps, so a date is not a type that the config
# formats represent, even though yaml is able to write one
parser.add_argument("--date", type=Any, default=date(2020, 1, 2))
cfg = parser.parse_args([])
with assert_dump_warnings("Unable to serialize instance 2020-01-02"):
with assert_dump_warnings(
serialized_as("Path_fr", "str"), serialized_as("bytes", "str"), serialized_as("date", "str")
):
dump = json_or_yaml_load(parser.dump(cfg))
assert dump == {"date": "Unable to serialize instance 2020-01-02"}
assert dump == {"path": __file__, "bytes": "YWI=", "date": "2020-01-02"}


def test_type_any_dump_not_round_trippable_warns(parser):
Expand Down
61 changes: 54 additions & 7 deletions jsonargparse_tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import os
import pickle
import random
import re
import sys
import uuid
from datetime import datetime, timedelta
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from fractions import Fraction
from random import Random
from typing import List, Mapping, Optional, TypeVar, Union
from unittest.mock import patch
Expand Down Expand Up @@ -382,19 +384,19 @@ def __bool__(self):
assert parser.dump(cfg, format="json_compact") == '{"elems":[1,2,3]}'


def test_register_type_datetime(parser):
def test_register_type_datetime(parser, restore_registrations):
def serializer(v):
return v.isoformat()
return v.strftime("%d/%m/%Y %H:%M")

def deserializer(v):
return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S")
return datetime.strptime(v, "%d/%m/%Y %H:%M")

register_type(datetime, serializer, deserializer)

parser.add_argument("--datetime", type=datetime)
cfg = parser.parse_args(["--datetime=2008-09-03T20:56:35"])
assert cfg.datetime == datetime(2008, 9, 3, 20, 56, 35)
assert json_or_yaml_load(parser.dump(cfg)) == {"datetime": "2008-09-03T20:56:35"}
cfg = parser.parse_args(["--datetime=03/09/2008 20:56"])
assert cfg.datetime == datetime(2008, 9, 3, 20, 56)
assert json_or_yaml_load(parser.dump(cfg)) == {"datetime": "03/09/2008 20:56"}

register_type(datetime, serializer, deserializer) # identical re-registering is okay

Expand Down Expand Up @@ -560,6 +562,51 @@ def test_uuid(parser):
assert {"uuid": str(id1), "uuids": [str(id1), str(id2)]} == json_or_yaml_load(parser.dump(cfg))


@pytest.mark.parametrize(
["type_", "value", "expected"],
[
(datetime, "2008-09-03T20:56:35", datetime(2008, 9, 3, 20, 56, 35)),
(date, "2008-09-03", date(2008, 9, 3)),
(time, "20:56:35", time(20, 56, 35)),
],
)
def test_datetime_types(parser, type_, value, expected):
parser.add_argument("--value", type=type_)
cfg = parser.parse_args([f"--value={value}"])
assert cfg.value == expected
assert type(cfg.value) is type_
assert json_or_yaml_load(parser.dump(cfg)) == {"value": value}
with pytest.raises(ArgumentError, match="--value"):
parser.parse_args(["--value=not a date"])


def test_datetime_rejects_date(parser):
parser.add_argument("--date", type=date)
with pytest.raises(ArgumentError, match="--date"):
parser.parse_args(["--date=2008-09-03T20:56:35"])


def test_re_pattern(parser):
parser.add_argument("--pattern", type=re.Pattern)
parser.add_argument("--patterns", type=List[re.Pattern[str]])
cfg = parser.parse_args([r"--pattern=^\d+$", r'--patterns=["a.c", "x*"]'])
assert cfg.pattern == re.compile(r"^\d+$")
assert cfg.patterns == [re.compile("a.c"), re.compile("x*")]
assert json_or_yaml_load(parser.dump(cfg)) == {"pattern": r"^\d+$", "patterns": ["a.c", "x*"]}
with pytest.raises(ArgumentError, match="--pattern"):
parser.parse_args(["--pattern=("])


def test_fraction(parser):
parser.add_argument("--fraction", type=Fraction)
cfg = parser.parse_args(["--fraction=1/3"])
assert cfg.fraction == Fraction(1, 3)
assert json_or_yaml_load(parser.dump(cfg)) == {"fraction": "1/3"}
assert parser.parse_args(["--fraction=0.1"]).fraction == Fraction(1, 10)
with pytest.raises(ArgumentError, match="--fraction"):
parser.parse_args(["--fraction=one third"])


def test_secret_str_methods():
value = SecretStr("secret")
assert len(value) == 6
Expand Down
Loading