Skip to content

Commit

Permalink
Make it possible to use dot notation for setting context in reflex
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jonathan-s committed Jul 28, 2021
1 parent 23f5bf4 commit fbee544
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 5 deletions.
19 changes: 19 additions & 0 deletions sockpuppet/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
from functools import wraps
from os import walk, path
import types
from urllib.parse import urlparse
from urllib.parse import parse_qsl

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

if not reflex.context._attr_data:
msg = (
"Setting context through instance variables is deprecated, "
'please use reflex.context.context_variable = "my_data"'
)
logger.warning(msg)
reflex_context.update(reflex.context)

original_context_data = view.view_class.get_context_data
reflex.get_context_data(**reflex_context)
# monkey patch context method
Expand All @@ -245,6 +254,16 @@ def render_page(self, reflex):
)

response = view(reflex.request, *resolved.args, **resolved.kwargs)

# When rendering the response the context has to be dict.
# Because django doesn't do the sane thing of forcing a dict we do it.
resolve_func = response.resolve_context

def resolve_context(self, context):
return resolve_func(dict(context))

response.resolve_context = types.MethodType(resolve_context, response)

# we've got the response, the function needs to work as normal again
view.view_class.get_context_data = original_context_data
reflex.session.save()
Expand Down
64 changes: 59 additions & 5 deletions sockpuppet/reflex.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,85 @@
from collections import UserDict
from django.urls import resolve
from urllib.parse import urlparse

from django.test import RequestFactory

PROTECTED_VARIABLES = [
"consumer",
"context",
"element",
"params",
"selectors",
"session",
"url",
]


class Context(UserDict):
"""
A dictionary that keeps track of whether it's been used as dictionary
or if values has been set with dot notation. We expect things to be set
in dot notation so a warning is issued until next major version (1.0)
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._attr_data = {}

def __getitem__(self, key):
data = self.__dict__
if (
data["data"].get(key, KeyError) is KeyError
and data["_attr_data"].get(key, KeyError) is KeyError
):
raise KeyError(key)
return self.data.get(key) or self._attr_data.get(key)

def __setitem__(self, key, item):
if not self.__dict__.get("data"):
self.__dict__["data"] = {}
self.__dict__["data"][key] = item

def __getattr__(self, key):
if not self.__dict__.get("data"):
self.__dict__["data"] = {}
if not self.__dict__.get("_attr_data"):
self.__dict__["_attr_data"] = {}

if (
self.__dict__["data"].get(key, KeyError) is KeyError
and self.__dict__["_attr_data"].get(key, KeyError) is KeyError
):
raise AttributeError(key)
result = self.data.get(key) or self._attr_data.get(key)
return result

def __setattr__(self, key, value):
if not self.__dict__.get("_attr_data"):
self.__dict__["_attr_data"] = {}
self.__dict__["_attr_data"][key] = value


class Reflex:
def __init__(self, consumer, url, element, selectors, params):
self.consumer = consumer
self.url = url
self.context = Context()
self.element = element
self.params = params
self.selectors = selectors
self.session = consumer.scope["session"]
self.params = params
self.context = {}
self.url = url

self._init_run = True

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

def __setattr__(self, name, value):
if name in PROTECTED_VARIABLES and getattr(self, "_init_run", None):
raise ValueError("This instance variable is used by the reflex.")
super().__setattr__(name, value)

def get_context_data(self, *args, **kwargs):
if self.context:
self.context.update(**kwargs)
Expand All @@ -44,8 +99,7 @@ def get_context_data(self, *args, **kwargs):
view.object_list = view.get_queryset()

context = view.get_context_data(**{"stimulus_reflex": True})

self.context = context
self.context.update(context)
self.context.update(**kwargs)
return self.context

Expand Down
40 changes: 40 additions & 0 deletions tests/test_reflex.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.test import TestCase
from sockpuppet.test_utils import reflex_factory
from sockpuppet.reflex import Context


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

self.assertIn('count', context)
self.assertIn('otherCount', context)

def test_context_api_works_correctly(self):
'''Test that context correctly stores information'''
context = Context()
context.hello = 'hello'

self.assertEqual(context.hello, 'hello')
self.assertEqual(context['hello'], 'hello')

self.assertEqual(context.data.get('hello'), None)
self.assertEqual(context._attr_data.get('hello'), 'hello')

with self.assertRaises(AttributeError):
context.not_an_attribute

with self.assertRaises(KeyError):
context['not_in_dictionary']

def test_access_attribute_when_stored_as_dict(self):
'''When value stored as dictionary it should be accessible as attribute'''
context = Context()
context['hello'] = 'world'
print(context.__dict__)
self.assertEqual(context['hello'], 'world')
self.assertEqual(context.hello, 'world')

def test_update_context(self):
'''Update context with normal dictionary'''

context = Context()
# update is broken.
context.update({'hello': 'world'})
self.assertEqual(context.hello, 'world')

def test_context_contains_none(self):
context = Context()
context.none = None
self.assertEqual(context.none, None)
self.assertEqual(context['none'], None)

0 comments on commit fbee544

Please sign in to comment.