diff --git a/pyproject.toml b/pyproject.toml index 7f34026..974d0b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ dependencies = [ "arrow", "jsonschema", - "pytimeparse" + "pytimeparse2" ] [dependency-groups] diff --git a/src/implicitdict/__init__.py b/src/implicitdict/__init__.py index 26324cb..75fb39d 100644 --- a/src/implicitdict/__init__.py +++ b/src/implicitdict/__init__.py @@ -15,7 +15,7 @@ ) import arrow -import pytimeparse +import pytimeparse2 _DICT_FIELDS = set(dir({})) _KEY_FIELDS_INFO = "_fields_info" @@ -205,6 +205,19 @@ def _parse_value(value, value_type: type, root_type: type): raise _bubble_up_parse_error(e, f"[{i}]") return result + elif generic_type is tuple: + if len(value) != len(arg_types): + raise ValueError( + f"Cannot parse {len(value)} values into a tuple[{', '.join(t.__name__ for t in arg_types)}]" + ) + result = [] + for i in range(len(value)): + try: + result.append(_parse_value(value[i], arg_types[i], root_type)) + except _PARSING_ERRORS as e: + raise _bubble_up_parse_error(e, f"[{i}]") + return tuple(result) + elif generic_type is dict: # value is a dict of some kind result = {} @@ -338,14 +351,15 @@ def __new__(cls, value: str | datetime.timedelta | int | float, reformat: bool = """Create a new StringBasedTimeDelta. Args: - value: Timedelta representation. May be a pytimeparse-compatible string, Python timedelta, or number of + value: Timedelta representation. May be a pytimeparse2-compatible string, Python timedelta, or number of seconds (float). reformat: If true, override a provided string with a string representation of the parsed timedelta. """ if isinstance(value, str): - seconds = pytimeparse.parse(value) + seconds = pytimeparse2.parse(value) if seconds is None: raise ValueError(f"Could not parse type {type(value).__name__} into StringBasedTimeDelta") + assert isinstance(seconds, float) or isinstance(seconds, int) dt = datetime.timedelta(seconds=seconds) s = str(dt) if reformat else value elif isinstance(value, float) or isinstance(value, int): diff --git a/tests/test_tuples.py b/tests/test_tuples.py new file mode 100644 index 0000000..f880e1c --- /dev/null +++ b/tests/test_tuples.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import json +from datetime import datetime +from typing import Optional, Self, Tuple # noqa UP045, UP006 + +import pytest + +from implicitdict import ImplicitDict, StringBasedDateTime, StringBasedTimeDelta + + +class WithTuples(ImplicitDict): + all_floats: tuple[float, float] + big_tuple: Tuple[str, str] # noqa UP006 + mixed_values: Optional[tuple[str, float]] # noqa UP045 + nested_tuples: Optional[tuple[tuple[float, int], str]] # noqa UP045 + nested_self: Optional[tuple[str, Self]] # noqa UP045 + times: Optional[tuple[StringBasedDateTime, StringBasedTimeDelta]] # noqa UP045 + lists: Optional[tuple[list[tuple[str, float]], str | None]] # noqa UP045 + dicts: Optional[dict[int, tuple[str, dict[str, int]]]] # noqa UP045 + long_tuple: Optional[tuple[bool, str, int, float, list[str], dict[str, str], tuple[int, int, int]]] # noqa UP045 + + @staticmethod + def example_values() -> dict[str, WithTuples]: + return { + "fully_defined": WithTuples( + all_floats=(1.23, 4.56), + big_tuple=("foo", "bar"), + mixed_values=("foo", 7), + nested_tuples=((9.87, 3), "foo"), + nested_self=("foo", WithTuples(all_floats=(0, 0), big_tuple=("", ""))), + times=(StringBasedDateTime(datetime.now()), StringBasedTimeDelta("1h")), + lists=([("foo", 10), ("bar", 100)], None), + dicts={42: ("1", {"foo": 9}), 314: ("a", {"bar": 7})}, + long_tuple=(False, "foo", 8888, 1e7, [], {}, (1, 2, 3)), + ) + } + + +def test_nominal(): + data = WithTuples.example_values()["fully_defined"] + assert "all_floats" in data + assert "big_tuple" in data + assert "mixed_values" in data + assert "nested_tuples" in data + assert "nested_self" in data + assert "times" in data + assert "lists" in data + assert "dicts" in data + assert "long_tuple" in data + s = json.dumps(data) + assert "all_floats" in s + assert "big_tuple" in s + assert "mixed_values" in s + assert "nested_tuples" in s + assert "nested_self" in s + assert "times" in s + assert "lists" in s + assert "dicts" in s + assert "long_tuple" in s + decoded = ImplicitDict.parse(json.loads(s), WithTuples) + assert decoded == data + assert decoded.times[0].datetime + assert decoded.times[1].timedelta + + +def test_wrong_element_count(): + with pytest.raises(ValueError, match="3 values"): + ImplicitDict.parse(json.loads('{"all_floats":[0,1,2],"big_tuple":["",""]}'), WithTuples) diff --git a/uv.lock b/uv.lock index e31315d..17c97cf 100644 --- a/uv.lock +++ b/uv.lock @@ -171,7 +171,7 @@ source = { editable = "." } dependencies = [ { name = "arrow" }, { name = "jsonschema" }, - { name = "pytimeparse" }, + { name = "pytimeparse2" }, ] [package.dev-dependencies] @@ -187,7 +187,7 @@ dev = [ requires-dist = [ { name = "arrow" }, { name = "jsonschema" }, - { name = "pytimeparse" }, + { name = "pytimeparse2" }, ] [package.metadata.requires-dev] @@ -346,12 +346,12 @@ wheels = [ ] [[package]] -name = "pytimeparse" -version = "1.1.8" +name = "pytimeparse2" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/5d/231f5f33c81e09682708fb323f9e4041408d8223e2f0fb9742843328778f/pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a", size = 9403, upload-time = "2018-05-18T17:40:42.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/10/cc63fecd69905eb4d300fe71bd580e4a631483e9f53fdcb8c0ad345ce832/pytimeparse2-1.7.1.tar.gz", hash = "sha256:98668cdcba4890e1789e432e8ea0059ccf72402f13f5d52be15bdfaeb3a8b253", size = 10431, upload-time = "2023-05-11T21:40:55.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/b4/afd75551a3b910abd1d922dbd45e49e5deeb4d47dc50209ce489ba9844dd/pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd", size = 9969, upload-time = "2018-05-18T17:40:41.28Z" }, + { url = "https://files.pythonhosted.org/packages/17/9e/85abf91ef5df452f56498927affdb7128194d15644084f6c6722477c305b/pytimeparse2-1.7.1-py3-none-any.whl", hash = "sha256:a162ea6a7707fd0bb82dd99556efb783935f51885c8bdced0fce3fffe85ab002", size = 6136, upload-time = "2023-05-11T21:40:46.051Z" }, ] [[package]]