diff --git a/sockpuppet/consumer.py b/sockpuppet/consumer.py index 0799549..f2ef9b1 100644 --- a/sockpuppet/consumer.py +++ b/sockpuppet/consumer.py @@ -235,6 +235,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 diff --git a/sockpuppet/reflex.py b/sockpuppet/reflex.py index a6f1da0..80c8c68 100644 --- a/sockpuppet/reflex.py +++ b/sockpuppet/reflex.py @@ -1,3 +1,4 @@ +from collections import UserDict from django.urls import resolve from urllib.parse import urlparse @@ -5,26 +6,70 @@ 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): + if not self.data.get(key) and not self._attr_data.get(key): + raise KeyError(key) + return self.data.get(key) or self._attr_data.get(key) + + 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) is None + and self.__dict__["_attr_data"].get(key) is None + ): + raise AttributeError(key) + return self.data.get(key) or self._attr_data.get(key) + + 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"" + 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) @@ -45,7 +90,7 @@ def get_context_data(self, *args, **kwargs): context = view.get_context_data(**{"stimulus_reflex": True}) - self.context = context + self.context.update(context) self.context.update(**kwargs) return self.context diff --git a/tests/test_reflex.py b/tests/test_reflex.py index acaa1c7..d8f5ab0 100644 --- a/tests/test_reflex.py +++ b/tests/test_reflex.py @@ -1,5 +1,6 @@ from django.test import TestCase from sockpuppet.test_utils import reflex_factory +from sockpuppet.reflex import Context class ReflexTests(TestCase): @@ -10,3 +11,20 @@ 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']