Skip to content

Commit 1540cd9

Browse files
Merge pull request #10 from PinnLabs/5-improve-type-casting
Improve type casting
2 parents e9fc991 + f9c6f4f commit 1540cd9

File tree

3 files changed

+79
-21
lines changed

3 files changed

+79
-21
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,4 @@ marimo/_static/
206206
marimo/_lsp/
207207
__marimo__/
208208

209-
DS_Store
209+
.DS_Store

src/venvalid/utils.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,70 @@
22
from datetime import datetime
33
from decimal import Decimal
44
from pathlib import Path
5+
from typing import Any, Optional, Type
56

67

7-
def _cast(value: str, expected_type: type) -> object:
8+
def _cast(
9+
value: str, expected_type: Type, *, datetime_format: Optional[str] = None
10+
) -> Any:
811
v = value.strip()
912

1013
if expected_type is bool:
11-
return v.lower() in ("1", "true", "yes", "on")
14+
truthy = {"1", "true", "yes", "on"}
15+
falsy = {"0", "false", "no", "off"}
16+
val_lower = v.lower()
17+
if val_lower in truthy:
18+
return True
19+
elif val_lower in falsy:
20+
return False
21+
else:
22+
raise ValueError(f"Invalid boolean value: {v}")
23+
1224
if expected_type is list:
25+
if not v:
26+
return []
27+
if v.startswith("[") and v.endswith("]"):
28+
try:
29+
parsed = json.loads(v)
30+
if not isinstance(parsed, list):
31+
raise ValueError(f"Expected list, got {type(parsed).__name__}")
32+
return parsed
33+
except json.JSONDecodeError as e:
34+
raise ValueError(f"Invalid JSON for list: {v}") from e
1335
return [item.strip() for item in v.split(",")]
14-
if expected_type is dict:
36+
37+
if expected_type in (dict, list):
1538
try:
16-
return json.loads(v)
39+
parsed = json.loads(v)
40+
if not isinstance(parsed, expected_type):
41+
raise ValueError(
42+
f"Expected {expected_type.__name__}, got {type(parsed).__name__}"
43+
)
44+
return parsed
1745
except json.JSONDecodeError as e:
18-
raise ValueError(f"Invalid JSON for dict: {v}") from e
46+
raise ValueError(f"Invalid JSON for {expected_type.__name__}: {v}") from e
47+
1948
if expected_type is Path:
2049
return Path(v)
50+
2151
if expected_type is Decimal:
22-
return Decimal(v)
52+
try:
53+
return Decimal(v)
54+
except Exception as e:
55+
raise ValueError(f"Invalid Decimal value: {v}") from e
56+
2357
if expected_type is datetime:
2458
try:
2559
return datetime.fromisoformat(v)
26-
except ValueError as e:
27-
raise ValueError(f"Invalid ISO datetime: {v}") from e
28-
return expected_type(v)
60+
except ValueError:
61+
if datetime_format:
62+
try:
63+
return datetime.strptime(v, datetime_format)
64+
except ValueError as e:
65+
raise ValueError(f"Invalid datetime format: {v}") from e
66+
raise ValueError(f"Invalid ISO datetime: {v}")
67+
68+
try:
69+
return expected_type(v)
70+
except Exception as e:
71+
raise ValueError(f"Cannot cast '{v}' to {expected_type.__name__}") from e

tests/test_utils.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,28 @@
1616
("yes", True),
1717
("on", True),
1818
("false", False),
19+
("False", False),
1920
("0", False),
2021
("no", False),
2122
("off", False),
22-
("", False),
23-
("maybe", False), # unexpected value
2423
],
2524
)
26-
def test_cast_bool(val, expected):
25+
def test_cast_bool_valid(val, expected):
2726
assert _cast(val, bool) == expected
2827

2928

29+
@pytest.mark.parametrize("val", ["", "maybe", "truth", "none"])
30+
def test_cast_bool_invalid(val):
31+
with pytest.raises(ValueError):
32+
_cast(val, bool)
33+
34+
3035
def test_cast_list_normal():
3136
assert _cast("a,b , c", list) == ["a", "b", "c"]
3237

3338

3439
def test_cast_list_empty():
35-
assert _cast("", list) == [""]
40+
assert _cast("", list) == []
3641

3742

3843
def test_cast_path():
@@ -45,14 +50,20 @@ def test_cast_decimal_valid():
4550

4651

4752
def test_cast_decimal_invalid():
48-
with pytest.raises(Exception):
53+
with pytest.raises(ValueError):
4954
_cast("abc", Decimal)
5055

5156

52-
def test_cast_datetime_valid():
57+
def test_cast_datetime_valid_iso():
5358
assert _cast("2024-01-01T10:00:00", datetime) == datetime(2024, 1, 1, 10, 0, 0)
5459

5560

61+
def test_cast_datetime_valid_custom_format():
62+
val = "01-02-2025 15:30"
63+
fmt = "%d-%m-%Y %H:%M"
64+
assert _cast(val, datetime, datetime_format=fmt) == datetime(2025, 2, 1, 15, 30)
65+
66+
5667
def test_cast_datetime_invalid():
5768
with pytest.raises(ValueError):
5869
_cast("not-a-date", datetime)
@@ -62,7 +73,11 @@ def test_cast_dict_valid():
6273
assert _cast('{"debug": true, "port": 8000}', dict) == {"debug": True, "port": 8000}
6374

6475

65-
def test_cast_dict_invalid():
76+
def test_cast_list_json_valid():
77+
assert _cast('["a", "b", "c"]', list) == ["a", "b", "c"]
78+
79+
80+
def test_cast_dict_invalid_json():
6681
with pytest.raises(ValueError):
6782
_cast("{not: valid}", dict)
6883

@@ -89,11 +104,11 @@ def test_cast_float_valid():
89104

90105

91106
class Dummy:
92-
pass
107+
def __init__(self, v):
108+
raise TypeError("Cannot instantiate Dummy with value")
93109

94110

95111
def test_cast_unknown_type():
96112
val = "123"
97-
dummy_type = Dummy
98-
with pytest.raises(TypeError):
99-
_cast(val, dummy_type)
113+
with pytest.raises(ValueError):
114+
_cast(val, Dummy)

0 commit comments

Comments
 (0)