Skip to content

Commit bc729c2

Browse files
committed
Use typehint to automatically get Django model if appropriate when calling an action. Clean up model example to show the one-true way to use Django models.
1 parent daeafea commit bc729c2

File tree

13 files changed

+281
-114
lines changed

13 files changed

+281
-114
lines changed

django_unicorn/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def db_model(func, *args, **kwargs):
1515
@db_model
1616
def delete(self, model):
1717
...
18-
18+
1919
Will get converted to:
2020
`component.delete({ 'name': 'modelName', pk: 1})` -> `component.delete(modelInstance)`
2121
"""

django_unicorn/utils.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import hmac
22
import logging
33
import pickle
4+
from inspect import signature
5+
from typing import Dict, List, Union
46
from typing import get_type_hints as typing_get_type_hints
57

68
from django.conf import settings
79

810
import shortuuid
911
from cachetools.lru import LRUCache
1012

13+
import django_unicorn
1114
from django_unicorn.errors import UnicornCacheError
1215

1316

1417
logger = logging.getLogger(__name__)
1518

1619
type_hints_cache = LRUCache(maxsize=100)
20+
function_signature_cache = LRUCache(maxsize=100)
1721

1822

19-
def generate_checksum(data):
23+
def generate_checksum(data: Union[str, bytes]) -> str:
2024
"""
2125
Generates a checksum for the passed-in data.
2226
"""
27+
2328
if isinstance(data, str):
2429
data_bytes = str.encode(data)
2530
else:
@@ -33,10 +38,11 @@ def generate_checksum(data):
3338
return checksum
3439

3540

36-
def dicts_equal(dictionary_one, dictionary_two):
41+
def dicts_equal(dictionary_one: Dict, dictionary_two: Dict) -> bool:
3742
"""
3843
Return True if all keys and values are the same between two dictionaries.
3944
"""
45+
4046
return all(
4147
k in dictionary_two and dictionary_one[k] == dictionary_two[k]
4248
for k in dictionary_one
@@ -46,10 +52,13 @@ def dicts_equal(dictionary_one, dictionary_two):
4652
)
4753

4854

49-
def get_cacheable_component(component):
55+
def get_cacheable_component(
56+
component: "django_unicorn.views.UnicornView",
57+
) -> "django_unicorn.views.UnicornView":
5058
"""
5159
Converts a component into something that is cacheable/pickleable.
5260
"""
61+
5362
component.request = None
5463

5564
if component.parent:
@@ -77,13 +86,14 @@ def get_cacheable_component(component):
7786
return component
7887

7988

80-
def get_type_hints(obj):
89+
def get_type_hints(obj) -> Dict:
8190
"""
8291
Get type hints from an object. These get cached in a local memory cache for quicker look-up later.
8392
8493
Returns:
8594
An empty dictionary if no type hints can be retrieved.
8695
"""
96+
8797
try:
8898
if obj in type_hints_cache:
8999
return type_hints_cache[obj]
@@ -103,3 +113,20 @@ def get_type_hints(obj):
103113
# raised if the argument is not of a type that can contain annotations, and an empty dictionary
104114
# is returned if no annotations are present"
105115
return {}
116+
117+
118+
def get_method_arguments(func) -> List[str]:
119+
"""
120+
Gets the arguments for a method.
121+
122+
Returns:
123+
A list of strings, one for each argument.
124+
"""
125+
126+
if func in function_signature_cache:
127+
return function_signature_cache[func]
128+
129+
function_signature = signature(func)
130+
function_signature_cache[func] = list(function_signature.parameters)
131+
132+
return function_signature_cache[func]

django_unicorn/views/action_parsers/call_method.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from typing import Any, Dict, List
22

3+
from django.db.models import Model
4+
35
from django_unicorn.call_method_parser import (
46
InvalidKwarg,
57
parse_call_method_name,
68
parse_kwarg,
79
)
810
from django_unicorn.components import UnicornView
911
from django_unicorn.decorators import timed
12+
from django_unicorn.utils import get_method_arguments, get_type_hints
1013
from django_unicorn.views.action_parsers.utils import set_property_value
1114
from django_unicorn.views.objects import ComponentRequest, Return
1215
from django_unicorn.views.utils import set_property_from_data
@@ -103,6 +106,27 @@ def _call_method_name(
103106
if method_name is not None and hasattr(component, method_name):
104107
func = getattr(component, method_name)
105108

109+
if len(args) == 1 or len(kwargs.keys()) == 1:
110+
arguments = get_method_arguments(func)
111+
type_hints = get_type_hints(func)
112+
113+
for argument in arguments:
114+
if argument in type_hints:
115+
if issubclass(type_hints[argument], Model):
116+
DbModel = type_hints[argument]
117+
key = "pk"
118+
value = None
119+
120+
if args:
121+
value = args.pop()
122+
elif kwargs:
123+
(key, value) = list(kwargs.items())[0]
124+
del kwargs[key]
125+
126+
model = DbModel.objects.get(**{key: value})
127+
128+
args.append(model)
129+
106130
if args and kwargs:
107131
return func(*args, **kwargs)
108132
elif args:

django_unicorn/views/action_parsers/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,17 @@ class TestView(UnicornView):
7575
else:
7676
component_or_field = component_or_field[property_name_part]
7777
data_or_dict = data_or_dict.get(property_name_part, {})
78+
elif isinstance(component_or_field, list):
79+
# TODO: Check for iterable instad of list? `from collections.abc import Iterable`
80+
property_name_part = int(property_name_part)
81+
82+
if idx == len(property_name_parts) - 1:
83+
component_or_field[property_name_part] = property_value
84+
data_or_dict[property_name_part] = property_value
85+
else:
86+
component_or_field = component_or_field[property_name_part]
87+
data_or_dict = data_or_dict[property_name_part]
88+
else:
89+
break
7890

7991
component.updated(property_name, property_value)

django_unicorn/views/utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ def set_property_from_data(
4747
if is_dataclass(type_hints[name]):
4848
value = type_hints[name](**value)
4949
else:
50-
value = type_hints[name](value)
50+
try:
51+
value = type_hints[name](value)
52+
except TypeError:
53+
# Ignore this exception because some type-hints can't be instantiated like this (e.g. `List[]`)
54+
pass
5155

5256
if hasattr(component_or_field, "_set_property"):
5357
# Can assume that `component_or_field` is a component
Lines changed: 23 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,28 @@
1-
from django.shortcuts import redirect
2-
from django.utils.functional import cached_property
1+
from typing import List
32

4-
from django_unicorn.components import HashUpdate, LocationUpdate, UnicornView
5-
from django_unicorn.db import DbModel
6-
from django_unicorn.decorators import db_model
3+
from django_unicorn.components import UnicornView
74
from example.coffee.models import Flavor
85

96

107
class ModelsView(UnicornView):
11-
flavors = Flavor.objects.none()
12-
13-
# Demonstrates how to use an instantiated model; class attributes get stored on
14-
# the class, so django-unicorn handles clearing this with `_resettable_attributes_cache`
15-
# in components.py
16-
class_flavor: Flavor = Flavor()
17-
18-
def __init__(self, **kwargs):
19-
# Demonstrates how to use an instance variable on the class
20-
self.instance_flavor = Flavor()
21-
22-
# super() `has` to be called at the end
23-
super().__init__(**kwargs)
24-
25-
def hydrate(self):
26-
# Using `hydrate` is the best way to make sure that QuerySets
27-
# are re-queried every time the component is loaded
28-
self.flavors = Flavor.objects.all().order_by("-id")[:2]
29-
30-
def add_instance_flavor(self):
31-
self.instance_flavor.save()
32-
id = self.instance_flavor.id
33-
self.reset()
34-
35-
# return HashUpdate(f"#createdId={id}")
36-
return LocationUpdate(redirect(f"/models?createdId={id}"), title="new title")
37-
38-
def add_class_flavor(self):
39-
self.class_flavor.save()
40-
id = self.class_flavor.id
41-
self.reset()
42-
43-
# Note: this can cause inputs to appear to be cached
44-
return redirect(f"/models?createdId={id}")
45-
46-
@db_model
47-
def delete(self, model):
48-
model.delete()
49-
50-
@cached_property
51-
def available_flavors(self):
52-
flavors = Flavor.objects.all()
53-
54-
if self.instance_flavor and self.instance_flavor.pk:
55-
return flavors.exclude(pk=self.instance_flavor.pk)
56-
57-
return flavors
58-
59-
class Meta:
60-
db_models = [DbModel("flavor", Flavor)]
8+
# class attributes get stored on the class, so django-unicorn handles clearing
9+
# this with `_resettable_attributes_cache` in components.py
10+
flavor: Flavor = Flavor()
11+
flavors: List[Flavor] = []
12+
13+
def mount(self):
14+
self.flavors = list(Flavor.objects.all().order_by("-id")[:2])
15+
16+
def save_flavor(self):
17+
self.flavor.save()
18+
self.flavor = Flavor()
19+
20+
def save(self, flavors_idx):
21+
flavor_data = self.flavors[flavors_idx]
22+
print("call save for idx", flavors_idx)
23+
flavor = Flavor(**flavor_data)
24+
flavor.save()
25+
26+
def save_specific(self, flavor: Flavor):
27+
flavor.save()
28+
print("call save on flavor", flavor)

example/unicorn/templates/unicorn/models.html

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,82 +6,50 @@
66
</style>
77

88
<h3>Current flavors</h3>
9-
<ul>
10-
{% for flavor in flavors %}
11-
<li>{{ flavor.id }} - {{ flavor.name }} - {{ flavor.label }}</li>
12-
{% endfor %}
13-
</ul>
14-
15-
<div>
16-
<h3>New Flavor (instance variable)</h3>
17-
18-
<label>Name</label>
19-
<input type="text" unicorn:model.defer="instance_flavor.name"></input>
20-
21-
<label>Label</label>
22-
<input type="text" unicorn:model.defer="instance_flavor.label"></input>
23-
24-
<br />
25-
<button unicorn:click="add_instance_flavor">Add Flavor (instance variable)</button>
26-
</div>
279

2810
<div>
29-
<h3>New Flavor (class attribute)</h3>
30-
31-
<label>Name</label>
32-
<input type="text" unicorn:model.defer="class_flavor.name"></input>
33-
34-
<label>Label</label>
35-
<input type="text" unicorn:model.defer="class_flavor.label"></input>
11+
<h3>Using unicorn:model with Django models</h3>
3612

37-
<br />
38-
<button unicorn:click="add_class_flavor">Add Flavor (class attribute)</button>
39-
</div>
40-
41-
<div>
42-
<h3>Using unicorn:db</h3>
43-
44-
<div unicorn:db="flavor">
13+
<div>
4514
<div>
4615
<label>Name</label>
47-
<input type="text" unicorn:field.defer="name" u:dirty.class="dirty"></input>
16+
<input type="text" unicorn:model="flavor.name"></input>
4817
</div>
4918

5019
<div>
5120
<label>Label</label>
52-
<input type="text" unicorn:field.defer="label" u:dirty.class="dirty"></input>
21+
<input type="text" unicorn:model="flavor.label"></input>
5322
</div>
5423

24+
<button unicorn:click="save_flavor">save_flavor</button>
25+
5526
{% for flavor in flavors %}
56-
<div unicorn:pk="{{ flavor.pk }}">
27+
<div>
5728
<h4>{{ flavor.pk }}</h4>
5829
<label>Name</label>
59-
<input type="text" unicorn:field.defer="name" u:dirty.class="dirty" value="{{ flavor.name }}"></input>
30+
<input type="text" unicorn:model="flavors.{{ forloop.counter0 }}.name" u:dirty.class="dirty"></input>
6031
{{ flavor.name }}
6132
<br />
6233

6334
<label>Label</label>
64-
<input type="text" unicorn:field.defer="label" u:dirty.class="dirty" value="{{ flavor.label }}"></input>
35+
<input type="text" unicorn:model="flavors.{{ forloop.counter0 }}.label" u:dirty.class="dirty"></input>
6536
{{ flavor.label }}
6637
<br />
6738

6839
<label>Float</label>
69-
<input type="text" unicorn:field.defer="float_value"></input>
40+
<input type="text" unicorn:model="flavors.{{ forloop.counter0 }}.float_value"></input>
7041
{{ flavor.float_value }}
7142
<br />
7243

7344
<label>Decimal value</label>
74-
<input type="text" unicorn:field.defer="decimal_value"></input>
45+
<input type="text" unicorn:model="flavors.{{ forloop.counter0 }}.decimal_value"></input>
7546
{{ flavor.decimal_value }}
7647
<br />
7748

78-
<button unicorn:click="delete($model)">delete($model)</button>
49+
<button unicorn:click="save({{ forloop.counter0 }})">save(flavors_idx)</button>
50+
<button unicorn:click="delete({{ flavor.pk }})">delete(flavor.pk)</button>
7951
</div>
8052
{% endfor %}
8153
</div>
8254
</div>
83-
84-
<br /><br />
85-
<button unicorn:click="$refresh">$refresh</button>
86-
<button unicorn:click="$reset">$reset</button>
8755
</div>

0 commit comments

Comments
 (0)