Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it possible to use dot notation for setting context in reflex #136

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/reflexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,17 @@ class ExampleReflex(Reflex):
def work(self):
# All new instance variables in the reflex will be accessible
# in the context during rendering.
self.instance_variable = 'hello world'
self.context.instance_variable = 'hello world'

context = self.get_context_data()
context['a_key'] = 'a pink elephant'

self.context.update(context)
# If "a_key" existed in the context before the reflex was triggered
# the context variable will now be modified to "a pink elephant"

# if it didn't exist, the context variable is then created with the
# data "a pink elephant" 🐘

```
{% endtab %}

Expand Down
1 change: 0 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ invoke
twine
wheel
zest.releaser

3 changes: 3 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ codecov>=2.0.0
gitpython
invoke
tox-venv

pytest
pytest-django
24 changes: 23 additions & 1 deletion 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 @@ -230,11 +231,22 @@ def render_page(self, reflex):
instance_variables = [
name
for (name, member) in inspect.getmembers(reflex)
if not name.startswith("__") and name not in PROTECTED_VARIABLES
if not name.startswith("__")
and name not in PROTECTED_VARIABLES
and not callable(getattr(reflex, name))
]

reflex_context = {key: getattr(reflex, key) for key in instance_variables}
reflex_context["stimulus_reflex"] = True

if len(instance_variables) > 0:
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 +257,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
81 changes: 75 additions & 6 deletions sockpuppet/reflex.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,95 @@
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",
"request",
"selectors",
"session",
"url",
"_init_run",
]


class Context(UserDict):
"""
This class represents the context that will be rendered in a template
and then sent client-side through websockets.

It works just like a dictionary with the extension that you can set and get
data through dot access.

> context.my_data = 'hello'
> context.my_data # 'hello'

The following property will contain all data of the dictionary
> context.data
"""

# NOTE for maintainer
# 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 __getitem__(self, key):
data = self.__dict__
if data["data"].get(key, KeyError) is KeyError:
raise KeyError(key)
return self.data.get(key)

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

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

def __setattr__(self, key, value):
if not self.__dict__.get("data"):
self.__dict__["data"] = {}
if key == "data" and value == {}:
return
self.__dict__["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):
"""
Fetches the context from the view which the reflex belongs to.
Once you've made modifications you can update the reflex context.

> context = self.get_context_data()
> context['a_key'] = 'some data'
> self.context.update(context)
"""

if self.context:
self.context.update(**kwargs)
return self.context
Expand All @@ -44,8 +108,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 All @@ -58,6 +121,7 @@ def get_channel_id(self):

@property
def request(self):
"""A synthetic request used to mimic the request-response cycle"""
factory = RequestFactory()
request = factory.get(self.url)
request.session = self.consumer.scope["session"]
Expand All @@ -66,5 +130,10 @@ def request(self):
return request

def reload(self):
"""A default reflex to force a refresh"""
"""
A default reflex to force a refresh, when used in html it will
refresh the page

data-action="click->MyReflexClass#reload"
"""
pass
37 changes: 37 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,39 @@ 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')

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)