Skip to content

Commit c7b5665

Browse files
committed
Cast fields in a view based on type hint for datetime, date, time, timedelta, and uuid.
1 parent 76ea454 commit c7b5665

File tree

8 files changed

+108
-89
lines changed

8 files changed

+108
-89
lines changed

django_unicorn/call_method_parser.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
import ast
22
import logging
3-
from datetime import date, datetime, time, timedelta
43
from functools import lru_cache
54
from types import MappingProxyType
65
from typing import Any, Dict, List, Mapping, Tuple
7-
from uuid import UUID
86

9-
from django.utils.dateparse import (
10-
parse_date,
11-
parse_datetime,
12-
parse_duration,
13-
parse_time,
14-
)
7+
from django_unicorn.utils import CASTERS
158

169

1710
logger = logging.getLogger(__name__)
1811

19-
# Functions that attempt to convert something that failed while being parsed by
20-
# `ast.literal_eval`.
21-
CASTERS = {
22-
datetime: parse_datetime,
23-
time: parse_time,
24-
date: parse_date,
25-
timedelta: parse_duration,
26-
UUID: UUID,
27-
}
28-
2912

3013
class InvalidKwarg(Exception):
3114
pass

django_unicorn/utils.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
import hmac
33
import logging
44
import pickle
5+
from datetime import date, datetime, time, timedelta
56
from inspect import signature
67
from pprint import pprint
78
from typing import Dict, List, Union
89
from typing import get_type_hints as typing_get_type_hints
10+
from uuid import UUID
911

1012
from django.conf import settings
1113
from django.core.cache import caches
1214
from django.http import HttpRequest
15+
from django.utils.dateparse import (
16+
parse_date,
17+
parse_datetime,
18+
parse_duration,
19+
parse_time,
20+
)
1321
from django.utils.html import _json_script_escapes
1422
from django.utils.safestring import mark_safe
1523

@@ -20,6 +28,19 @@
2028
from django_unicorn.settings import get_cache_alias
2129

2230

31+
try:
32+
from typing import get_args, get_origin
33+
except ImportError:
34+
# Fallback to dunder methods for older versions of Python
35+
def get_args(type_hint):
36+
if hasattr(type_hint, "__args__"):
37+
return type_hint.__args__
38+
39+
def get_origin(type_hint):
40+
if hasattr(type_hint, "__origin__"):
41+
return type_hint.__origin__
42+
43+
2344
try:
2445
from cachetools.lru import LRUCache
2546
except ImportError:
@@ -32,6 +53,17 @@
3253
function_signature_cache = LRUCache(maxsize=100)
3354

3455

56+
# Functions that attempt to convert something that failed while being parsed by
57+
# `ast.literal_eval`.
58+
CASTERS = {
59+
datetime: parse_datetime,
60+
time: parse_time,
61+
date: parse_date,
62+
timedelta: parse_duration,
63+
UUID: UUID,
64+
}
65+
66+
3567
def generate_checksum(data: Union[str, bytes]) -> str:
3668
"""
3769
Generates a checksum for the passed-in data.
@@ -241,6 +273,51 @@ def get_type_hints(obj) -> Dict:
241273
return {}
242274

243275

276+
def cast_value(type_hint, value):
277+
"""
278+
Try to cast the value based on the type hint and
279+
`django_unicorn.call_method_parser.CASTERS`.
280+
281+
Additional features:
282+
- convert `int`/`float` epoch to `datetime` or `date`
283+
- instantiate the `type_hint` class with passed-in value
284+
"""
285+
286+
type_hints = []
287+
288+
if get_origin(type_hint) is Union:
289+
for arg in get_args(type_hint):
290+
type_hints.append(arg)
291+
else:
292+
type_hints.append(type_hint)
293+
294+
for type_hint in type_hints:
295+
caster = CASTERS.get(type_hint)
296+
297+
if caster:
298+
try:
299+
value = caster(value)
300+
break
301+
except TypeError:
302+
if (type_hint is datetime or type_hint is date) and (
303+
isinstance(value, int) or isinstance(value, float)
304+
):
305+
try:
306+
value = datetime.fromtimestamp(value)
307+
308+
if type_hint is date:
309+
value = value.date()
310+
311+
break
312+
except ValueError:
313+
pass
314+
else:
315+
value = type_hint(value)
316+
break
317+
318+
return value
319+
320+
244321
def get_method_arguments(func) -> List[str]:
245322
"""
246323
Gets the arguments for a method.

django_unicorn/views/action_parsers/call_method.py

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
1-
from datetime import date, datetime
21
from typing import Any, Dict, Tuple, Union
32

43
from django.db.models import Model
54

65
from django_unicorn.call_method_parser import (
7-
CASTERS,
86
InvalidKwarg,
97
parse_call_method_name,
108
parse_kwarg,
119
)
1210
from django_unicorn.components import UnicornView
1311
from django_unicorn.decorators import timed
14-
from django_unicorn.utils import get_method_arguments, get_type_hints
12+
from django_unicorn.utils import cast_value, get_method_arguments, get_type_hints
1513
from django_unicorn.views.action_parsers.utils import set_property_value
1614
from django_unicorn.views.objects import ComponentRequest, Return
1715
from django_unicorn.views.utils import set_property_from_data
1816

1917

2018
try:
21-
from typing import get_args, get_origin
19+
from typing import get_origin
2220
except ImportError:
23-
# Fallback to dunder methods for older versions of Python
24-
def get_args(type_hint):
25-
if hasattr(type_hint, "__args__"):
26-
return type_hint.__args__
2721

2822
def get_origin(type_hint):
2923
if hasattr(type_hint, "__origin__"):
@@ -107,51 +101,6 @@ def handle(component_request: ComponentRequest, component: UnicornView, payload:
107101
)
108102

109103

110-
def _cast_value(type_hint, value):
111-
"""
112-
Try to cast the value based on the type hint and
113-
`django_unicorn.call_method_parser.CASTERS`.
114-
115-
Additional features:
116-
- convert `int`/`float` epoch to `datetime` or `date`
117-
- instantiate the `type_hint` class with passed-in value
118-
"""
119-
120-
type_hints = []
121-
122-
if get_origin(type_hint) is Union:
123-
for arg in get_args(type_hint):
124-
type_hints.append(arg)
125-
else:
126-
type_hints.append(type_hint)
127-
128-
for type_hint in type_hints:
129-
caster = CASTERS.get(type_hint)
130-
131-
if caster:
132-
try:
133-
value = caster(value)
134-
break
135-
except TypeError:
136-
if (type_hint is datetime or type_hint is date) and (
137-
isinstance(value, int) or isinstance(value, float)
138-
):
139-
try:
140-
value = datetime.fromtimestamp(value)
141-
142-
if type_hint is date:
143-
value = value.date()
144-
145-
break
146-
except ValueError:
147-
pass
148-
else:
149-
value = type_hint(value)
150-
break
151-
152-
return value
153-
154-
155104
@timed
156105
def _call_method_name(
157106
component: UnicornView, method_name: str, args: Tuple[Any], kwargs: Dict[str, Any]
@@ -207,9 +156,9 @@ def _call_method_name(
207156
parsed_kwargs[argument] = DbModel.objects.get(**{key: value})
208157

209158
elif argument in kwargs:
210-
parsed_kwargs[argument] = _cast_value(type_hint, kwargs[argument])
159+
parsed_kwargs[argument] = cast_value(type_hint, kwargs[argument])
211160
else:
212-
parsed_args.append(_cast_value(type_hint, args[len(parsed_args)]))
161+
parsed_args.append(cast_value(type_hint, args[len(parsed_args)]))
213162
elif argument in kwargs:
214163
parsed_kwargs[argument] = kwargs[argument]
215164
else:

django_unicorn/views/utils.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django_unicorn.components.typing import QuerySetType
99
from django_unicorn.decorators import timed
1010
from django_unicorn.utils import get_type_hints
11+
from django_unicorn.views.action_parsers.call_method import cast_value
1112

1213

1314
try:
@@ -73,12 +74,8 @@ def set_property_from_data(
7374
if is_dataclass(type_hint):
7475
value = type_hint(**value)
7576
else:
76-
# Construct the specified type by passing the value in
77-
# Usually the value will be a string (because it is coming from JSON)
78-
# and basic types can be constructed by passing in a string,
79-
# i.e. int("1") or float("1.1")
8077
try:
81-
value = type_hint(value)
78+
value = cast_value(type_hint, value)
8279
except TypeError:
8380
# Ignore this exception because some type-hints can't be instantiated like this (e.g. `List[]`)
8481
pass
@@ -208,7 +205,7 @@ class TestComponent(UnicornView):
208205
# Explicitly set `_result_cache` to an empty list
209206
queryset._result_cache = []
210207

211-
for (idx, model) in enumerate(queryset._result_cache):
208+
for idx, model in enumerate(queryset._result_cache):
212209
if hasattr(model, "pk") and model.pk == model_value.get("pk"):
213210
constructed_model = _construct_model(model_type, model_value)
214211
queryset._result_cache[idx] = constructed_model

example/unicorn/components/test_datetime.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from datetime import datetime
2+
23
from django.utils import timezone
4+
35
from django_unicorn.components import UnicornView
46

7+
58
class TestDatetimeView(UnicornView):
69
dt: datetime = None
710

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% load unicorn %}
22
<div>
3-
<div style="width:200px;height:200px;margin:10px;background-color:#aaffaa" unicorn:click="foo()">
4-
{{ dt }}<br>
5-
(click me)
6-
</div>
3+
<div style="width:200px;height:200px;margin:10px;background-color:#aaffaa" unicorn:click="foo()">
4+
{{ dt }}<br>
5+
(click me)
6+
</div>
77
</div>

tests/call_method_parser/test_parse_args.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from datetime import datetime
22
from uuid import UUID
33

4-
import pytest
5-
64
from django_unicorn.call_method_parser import eval_value
75

86

tests/views/utils/test_set_property_from_data.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
class FakeComponent(UnicornView):
1414
string = "property_view"
1515
integer = 99
16-
datetime = datetime(2020, 1, 1)
16+
datetime_without_typehint = datetime(2020, 1, 1)
17+
datetime_with_typehint: datetime = datetime(2020, 2, 1)
1718
array: List[str] = []
1819
model = Flavor(name="test-initial")
1920
queryset = Flavor.objects.none()
@@ -69,11 +70,22 @@ def test_set_property_from_data_int():
6970

7071
def test_set_property_from_data_datetime():
7172
component = FakeComponent(component_name="test", component_id="12345678")
72-
assert datetime(2020, 1, 1) == component.datetime
73+
assert datetime(2020, 1, 1) == component.datetime_without_typehint
7374

74-
set_property_from_data(component, "datetime", datetime(2020, 1, 2))
75+
set_property_from_data(component, "datetime_without_typehint", datetime(2020, 1, 2))
7576

76-
assert datetime(2020, 1, 2) == component.datetime
77+
assert datetime(2020, 1, 2) == component.datetime_without_typehint
78+
79+
80+
def test_set_property_from_data_datetime_with_typehint():
81+
component = FakeComponent(component_name="test", component_id="12345678")
82+
assert datetime(2020, 2, 1) == component.datetime_with_typehint
83+
84+
set_property_from_data(
85+
component, "datetime_with_typehint", str(datetime(2020, 2, 2))
86+
)
87+
88+
assert datetime(2020, 2, 2) == component.datetime_with_typehint
7789

7890

7991
def test_set_property_from_data_list():

0 commit comments

Comments
 (0)