Skip to content
Open
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: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ classifiers = [
dependencies = [
"arrow",
"jsonschema",
"pytimeparse"
"pytimeparse2"
]

[dependency-groups]
Expand Down
20 changes: 17 additions & 3 deletions src/implicitdict/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

import arrow
import pytimeparse
import pytimeparse2

_DICT_FIELDS = set(dir({}))
_KEY_FIELDS_INFO = "_fields_info"
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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):
Expand Down
69 changes: 69 additions & 0 deletions tests/test_tuples.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 6 additions & 6 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading