Skip to content

Commit fbee544

Browse files
committed
Make it possible to use dot notation for setting context in reflex
In an effort to improve the api for setting the context that is used in the final context in the reflex we introduce a dot notation for setting the context. Ie `reflex.context.my_context = 'value'`. For the old way of setting the context in instance variables you now also prevented to set an instance variable that is already used by the reflex.
1 parent 23f5bf4 commit fbee544

File tree

3 files changed

+118
-5
lines changed

3 files changed

+118
-5
lines changed

sockpuppet/consumer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import inspect
66
from functools import wraps
77
from os import walk, path
8+
import types
89
from urllib.parse import urlparse
910
from urllib.parse import parse_qsl
1011

@@ -235,6 +236,14 @@ def render_page(self, reflex):
235236
reflex_context = {key: getattr(reflex, key) for key in instance_variables}
236237
reflex_context["stimulus_reflex"] = True
237238

239+
if not reflex.context._attr_data:
240+
msg = (
241+
"Setting context through instance variables is deprecated, "
242+
'please use reflex.context.context_variable = "my_data"'
243+
)
244+
logger.warning(msg)
245+
reflex_context.update(reflex.context)
246+
238247
original_context_data = view.view_class.get_context_data
239248
reflex.get_context_data(**reflex_context)
240249
# monkey patch context method
@@ -245,6 +254,16 @@ def render_page(self, reflex):
245254
)
246255

247256
response = view(reflex.request, *resolved.args, **resolved.kwargs)
257+
258+
# When rendering the response the context has to be dict.
259+
# Because django doesn't do the sane thing of forcing a dict we do it.
260+
resolve_func = response.resolve_context
261+
262+
def resolve_context(self, context):
263+
return resolve_func(dict(context))
264+
265+
response.resolve_context = types.MethodType(resolve_context, response)
266+
248267
# we've got the response, the function needs to work as normal again
249268
view.view_class.get_context_data = original_context_data
250269
reflex.session.save()

sockpuppet/reflex.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,85 @@
1+
from collections import UserDict
12
from django.urls import resolve
23
from urllib.parse import urlparse
34

45
from django.test import RequestFactory
56

67
PROTECTED_VARIABLES = [
78
"consumer",
9+
"context",
810
"element",
11+
"params",
912
"selectors",
1013
"session",
1114
"url",
1215
]
1316

1417

18+
class Context(UserDict):
19+
"""
20+
A dictionary that keeps track of whether it's been used as dictionary
21+
or if values has been set with dot notation. We expect things to be set
22+
in dot notation so a warning is issued until next major version (1.0)
23+
"""
24+
25+
def __init__(self, *args, **kwargs):
26+
super().__init__(*args, **kwargs)
27+
self._attr_data = {}
28+
29+
def __getitem__(self, key):
30+
data = self.__dict__
31+
if (
32+
data["data"].get(key, KeyError) is KeyError
33+
and data["_attr_data"].get(key, KeyError) is KeyError
34+
):
35+
raise KeyError(key)
36+
return self.data.get(key) or self._attr_data.get(key)
37+
38+
def __setitem__(self, key, item):
39+
if not self.__dict__.get("data"):
40+
self.__dict__["data"] = {}
41+
self.__dict__["data"][key] = item
42+
43+
def __getattr__(self, key):
44+
if not self.__dict__.get("data"):
45+
self.__dict__["data"] = {}
46+
if not self.__dict__.get("_attr_data"):
47+
self.__dict__["_attr_data"] = {}
48+
49+
if (
50+
self.__dict__["data"].get(key, KeyError) is KeyError
51+
and self.__dict__["_attr_data"].get(key, KeyError) is KeyError
52+
):
53+
raise AttributeError(key)
54+
result = self.data.get(key) or self._attr_data.get(key)
55+
return result
56+
57+
def __setattr__(self, key, value):
58+
if not self.__dict__.get("_attr_data"):
59+
self.__dict__["_attr_data"] = {}
60+
self.__dict__["_attr_data"][key] = value
61+
62+
1563
class Reflex:
1664
def __init__(self, consumer, url, element, selectors, params):
1765
self.consumer = consumer
18-
self.url = url
66+
self.context = Context()
1967
self.element = element
68+
self.params = params
2069
self.selectors = selectors
2170
self.session = consumer.scope["session"]
22-
self.params = params
23-
self.context = {}
71+
self.url = url
72+
73+
self._init_run = True
2474

2575
def __repr__(self):
2676
return f"<Reflex url: {self.url}, session: {self.get_channel_id()}>"
2777

78+
def __setattr__(self, name, value):
79+
if name in PROTECTED_VARIABLES and getattr(self, "_init_run", None):
80+
raise ValueError("This instance variable is used by the reflex.")
81+
super().__setattr__(name, value)
82+
2883
def get_context_data(self, *args, **kwargs):
2984
if self.context:
3085
self.context.update(**kwargs)
@@ -44,8 +99,7 @@ def get_context_data(self, *args, **kwargs):
4499
view.object_list = view.get_queryset()
45100

46101
context = view.get_context_data(**{"stimulus_reflex": True})
47-
48-
self.context = context
102+
self.context.update(context)
49103
self.context.update(**kwargs)
50104
return self.context
51105

tests/test_reflex.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.test import TestCase
22
from sockpuppet.test_utils import reflex_factory
3+
from sockpuppet.reflex import Context
34

45

56
class ReflexTests(TestCase):
@@ -10,3 +11,42 @@ def test_reflex_can_access_context(self):
1011

1112
self.assertIn('count', context)
1213
self.assertIn('otherCount', context)
14+
15+
def test_context_api_works_correctly(self):
16+
'''Test that context correctly stores information'''
17+
context = Context()
18+
context.hello = 'hello'
19+
20+
self.assertEqual(context.hello, 'hello')
21+
self.assertEqual(context['hello'], 'hello')
22+
23+
self.assertEqual(context.data.get('hello'), None)
24+
self.assertEqual(context._attr_data.get('hello'), 'hello')
25+
26+
with self.assertRaises(AttributeError):
27+
context.not_an_attribute
28+
29+
with self.assertRaises(KeyError):
30+
context['not_in_dictionary']
31+
32+
def test_access_attribute_when_stored_as_dict(self):
33+
'''When value stored as dictionary it should be accessible as attribute'''
34+
context = Context()
35+
context['hello'] = 'world'
36+
print(context.__dict__)
37+
self.assertEqual(context['hello'], 'world')
38+
self.assertEqual(context.hello, 'world')
39+
40+
def test_update_context(self):
41+
'''Update context with normal dictionary'''
42+
43+
context = Context()
44+
# update is broken.
45+
context.update({'hello': 'world'})
46+
self.assertEqual(context.hello, 'world')
47+
48+
def test_context_contains_none(self):
49+
context = Context()
50+
context.none = None
51+
self.assertEqual(context.none, None)
52+
self.assertEqual(context['none'], None)

0 commit comments

Comments
 (0)