From f9bc35f33b8f37e25d1f77a65e0aea787ece74f1 Mon Sep 17 00:00:00 2001 From: Teo Stocco Date: Sun, 7 Nov 2021 03:45:58 +0100 Subject: [PATCH] add datetime support (#32) #minor adding datetime support --- CHANGELOG.md | 2 ++ README.md | 3 +++ confs/readme.hocon | 1 + dataconf/exceptions.py | 14 +++++++++++++- dataconf/utils.py | 17 ++++++++++++++--- tests/test_dict_list_parsing.py | 4 ++-- tests/test_parse.py | 26 ++++++++++++++++++++++++++ tests/test_regression.py | 3 +++ 8 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e56040..a39354f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- datetime parsing + ### Changed ### Fixed diff --git a/README.md b/README.md index efefc59..b87f593 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ import os from dataclasses import dataclass, field from typing import List, Dict, Text, Union from dateutil.relativedelta import relativedelta +from datetime import datetime import dataconf conf = """ @@ -37,6 +38,7 @@ str_name = test str_name = ${?HOME} dash-to-underscore = true float_num = 2.2 +iso_datetime = "2000-01-01T20:00:00" # this is a comment list_data = [ a @@ -83,6 +85,7 @@ class Config: str_name: Text dash_to_underscore: bool float_num: float + iso_datetime: datetime list_data: List[Text] nested: Nested nested_list: List[Nested] diff --git a/confs/readme.hocon b/confs/readme.hocon index 022b5ae..a6a03db 100644 --- a/confs/readme.hocon +++ b/confs/readme.hocon @@ -2,6 +2,7 @@ str_name = test str_name = ${?HOME} dash-to-underscore = true float_num = 2.2 +iso_datetime = "2000-01-01T20:00:00" # this is a comment list_data = [ a diff --git a/dataconf/exceptions.py b/dataconf/exceptions.py index 7fea20e..0f5b324 100644 --- a/dataconf/exceptions.py +++ b/dataconf/exceptions.py @@ -1,22 +1,34 @@ class TypeConfigException(Exception): + """Type mismatch exception.""" + pass class MissingTypeException(Exception): + """Missing type exception (e.g. List instead of List[int].""" + pass class MalformedConfigException(Exception): + """Missing values exception.""" + pass class UnexpectedKeysException(Exception): + """Unexpected keys exception.""" + pass class EnvListOrderException(Exception): + """Ordering exception.""" + pass -class EnvParseException(Exception): +class ParseException(Exception): + """Parsing exception.""" + pass diff --git a/dataconf/utils.py b/dataconf/utils.py index 0768014..e4aaa6e 100644 --- a/dataconf/utils.py +++ b/dataconf/utils.py @@ -2,16 +2,18 @@ from dataclasses import asdict from dataclasses import fields from dataclasses import is_dataclass +from datetime import datetime from typing import get_args from typing import get_origin from typing import Union from dataconf.exceptions import EnvListOrderException -from dataconf.exceptions import EnvParseException from dataconf.exceptions import MalformedConfigException from dataconf.exceptions import MissingTypeException +from dataconf.exceptions import ParseException from dataconf.exceptions import TypeConfigException from dataconf.exceptions import UnexpectedKeysException +from dateutil.parser import isoparse from dateutil.relativedelta import relativedelta from pyhocon import ConfigFactory from pyhocon.config_tree import ConfigList @@ -136,6 +138,15 @@ def __parse(value: any, clazz, path): if clazz is str: return __parse_type(value, clazz, path, isinstance(value, str)) + if clazz is datetime: + dt = __parse_type(value, clazz, path, isinstance(value, str)) + try: + return isoparse(dt) + except ValueError as e: + raise ParseException( + f"expected type {clazz} at {path}, cannot parse due to {e}" + ) + if clazz is relativedelta: return __parse_type(value, clazz, path, isinstance(value, relativedelta)) @@ -251,8 +262,8 @@ def int_or_string(v): try: v = ConfigFactory.parse_string(v) except pyparsing.ParseBaseException as e: - raise EnvParseException( - f"env var {k} ends with `_` and expects a nested config, but got: {e}" + raise ParseException( + f"env var {k} ends with `_` and expects a nested config, got: {e}" ) k = k[:-1] diff --git a/tests/test_dict_list_parsing.py b/tests/test_dict_list_parsing.py index 1ad956a..a48c87b 100644 --- a/tests/test_dict_list_parsing.py +++ b/tests/test_dict_list_parsing.py @@ -1,5 +1,5 @@ from dataconf.exceptions import EnvListOrderException -from dataconf.exceptions import EnvParseException +from dataconf.exceptions import ParseException from dataconf.utils import __dict_list_parsing as dict_list_parsing import pytest @@ -82,5 +82,5 @@ def test_bad_nested_config(self) -> None: env = { "P_A_0_": "::", } - with pytest.raises(EnvParseException): + with pytest.raises(ParseException): dict_list_parsing("P", env) diff --git a/tests/test_parse.py b/tests/test_parse.py index 5c0a5a9..a88f5ba 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,7 @@ from dataclasses import dataclass from dataclasses import field +from datetime import datetime +from datetime import timezone import os from typing import Dict from typing import List @@ -11,6 +13,7 @@ from dataconf import loads from dataconf.exceptions import MalformedConfigException from dataconf.exceptions import MissingTypeException +from dataconf.exceptions import ParseException from dataconf.exceptions import TypeConfigException from dataconf.exceptions import UnexpectedKeysException from dateutil.relativedelta import relativedelta @@ -154,6 +157,29 @@ class A: """ assert loads(conf, A) == A(b="test") + def test_datetime(self) -> None: + @dataclass + class A: + b: datetime + + conf = """ + b = "1997-07-16T19:20:07+01:00" + """ + assert loads(conf, A) == A( + b=datetime(1997, 7, 16, 18, 20, 7, tzinfo=timezone.utc) + ) + + def test_bad_datetime(self) -> None: + @dataclass + class A: + b: datetime + + conf = """ + b = "1997-07-16 19:20:0701:00" + """ + with pytest.raises(ParseException): + assert loads(conf, A) + def test_optional_with_default(self) -> None: @dataclass class A: diff --git a/tests/test_regression.py b/tests/test_regression.py index 5cc4d73..00ac76f 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,6 +1,7 @@ from abc import ABCMeta from dataclasses import dataclass from dataclasses import field +from datetime import datetime import os from typing import Dict from typing import List @@ -39,6 +40,7 @@ class Config: str_name: Text dash_to_underscore: bool float_num: float + iso_datetime: datetime list_data: List[Text] nested: Nested nested_list: List[Nested] @@ -56,6 +58,7 @@ class Config: str_name=os.environ.get("HOME", "test"), dash_to_underscore=True, float_num=2.2, + iso_datetime=datetime(2000, 1, 1, 20), list_data=["a", "b"], nested=Nested(a="test", b=1), nested_list=[Nested(a="test1", b=2.5)],