-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Support serializable toolbar #2138
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
bb88c05
Add the Store API and initial documentation.
tim-schilling 19b5695
Remove config from docs as sphinx says it's misspelled.
tim-schilling 97fcda7
Switch to Store.request_ids and remove serialization force_str.
tim-schilling 487dfb3
Log serialization warning when a panel errors. (#1810)
tim-schilling e7cf575
Ignore common venv folder.
tim-schilling c4201fa
Rename store_id variants to request_id
bbbbb34
Support serializable panels. This is a WIP and needs clean-up.
e2f695b
Support serializable sql panel
tim-schilling 14a5e0c
Make Panel.panel_id a classmember.
tim-schilling a31115f
Force everything to a string if it can't be serialized.
tim-schilling 71edcf5
Support serialization of FunctionCall
tim-schilling c03f08f
Update all panels to use data from get_stats on render
tim-schilling 47bdabe
Extend example app to have an async version.
tim-schilling dd53424
Merge branch 'main' into serialize-panels
tim-schilling 16e02f5
Rework the alerts panel to be compatible with serialization.
tim-schilling 3e4c484
Make template panel serializable.
tim-schilling f4ff5f4
Avoid caching the config settings.
d3730a6
Fix tests for serializable changes with selenium.
c660269
Comment out the async button because it breaks the wsgi app.
8402c4d
Hack: Sleep before checking to see if the history panel auto updated.
e1c0755
Improve clarity of record_stats for serialization. (#1965)
tim-schilling 5a21920
Merge branch 'main' into serializable
tim-schilling 89c4786
Added check for pytest as test runner for IS_RUNNING_TESTS.
tim-schilling 73eea66
Fixes #2073 -- Added DatabaseStore for persistent debug data storage.…
dr-rompecabezas 153c22b
Merge branch 'main' into serializable
tim-schilling f8bfb0d
Move serializable changes into the main change log.
tim-schilling bf77c70
Updated replaceToolbarState to use request id.
tim-schilling 0cbf478
Merge branch 'main' into serializable
matthiask 7a638c7
Removed unnecessary SQLPanel.record_stats
tim-schilling 2fafbb4
Merge branch 'main' into serializable
tim-schilling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
Add the Store API and initial documentation.
- Loading branch information
commit bb88c0526c772a55e40bdf074caeb3b3d5124056
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import json | ||
| from collections import defaultdict, deque | ||
| from typing import Any, Dict, Iterable | ||
|
|
||
| from django.core.serializers.json import DjangoJSONEncoder | ||
| from django.utils.encoding import force_str | ||
| from django.utils.module_loading import import_string | ||
|
|
||
| from debug_toolbar import settings as dt_settings | ||
|
|
||
|
|
||
| class DebugToolbarJSONEncoder(DjangoJSONEncoder): | ||
| def default(self, o: Any) -> Any: | ||
| try: | ||
| return super().default(o) | ||
| except TypeError: | ||
| return force_str(o) | ||
|
|
||
|
|
||
| def serialize(data: Any) -> str: | ||
| return json.dumps(data, cls=DebugToolbarJSONEncoder) | ||
|
|
||
|
|
||
| def deserialize(data: str) -> Any: | ||
| return json.loads(data) | ||
|
|
||
|
|
||
| class BaseStore: | ||
| _config = dt_settings.get_config().copy() | ||
|
|
||
| @classmethod | ||
| def ids(cls) -> Iterable: | ||
| """The stored ids""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def exists(cls, request_id: str) -> bool: | ||
| """Does the given request_id exist in the store""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def set(cls, request_id: str): | ||
| """Set a request_id in the store""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def clear(cls): | ||
| """Remove all requests from the request store""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def delete(cls, request_id: str): | ||
| """Delete the store for the given request_id""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def save_panel(cls, request_id: str, panel_id: str, data: Any = None): | ||
| """Save the panel data for the given request_id""" | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def panel(cls, request_id: str, panel_id: str) -> Any: | ||
| """Fetch the panel data for the given request_id""" | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class MemoryStore(BaseStore): | ||
| # ids is the collection of storage ids that have been used. | ||
| # Use a dequeue to support O(1) appends and pops | ||
| # from either direction. | ||
| _ids: deque = deque() | ||
| _request_store: Dict[str, Dict] = defaultdict(dict) | ||
|
|
||
| @classmethod | ||
| def ids(cls) -> Iterable: | ||
| """The stored ids""" | ||
| return cls._ids | ||
|
|
||
| @classmethod | ||
| def exists(cls, request_id: str) -> bool: | ||
| """Does the given request_id exist in the request store""" | ||
| return request_id in cls._ids | ||
|
|
||
| @classmethod | ||
| def set(cls, request_id: str): | ||
| """Set a request_id in the request store""" | ||
| if request_id not in cls._ids: | ||
| cls._ids.append(request_id) | ||
| for _ in range(len(cls._ids) - cls._config["RESULTS_CACHE_SIZE"]): | ||
| removed_id = cls._ids.popleft() | ||
| cls._request_store.pop(removed_id, None) | ||
|
|
||
| @classmethod | ||
| def clear(cls): | ||
| """Remove all requests from the request store""" | ||
| cls._ids.clear() | ||
| cls._request_store.clear() | ||
|
|
||
| @classmethod | ||
| def delete(cls, request_id: str): | ||
| """Delete the stored request for the given request_id""" | ||
| cls._request_store.pop(request_id, None) | ||
| try: | ||
| cls._ids.remove(request_id) | ||
| except ValueError: | ||
| # The request_id doesn't exist in the collection of ids. | ||
| pass | ||
|
|
||
| @classmethod | ||
| def save_panel(cls, request_id: str, panel_id: str, data: Any = None): | ||
| """Save the panel data for the given request_id""" | ||
| cls.set(request_id) | ||
| cls._request_store[request_id][panel_id] = serialize(data) | ||
|
|
||
| @classmethod | ||
| def panel(cls, request_id: str, panel_id: str) -> Any: | ||
| """Fetch the panel data for the given request_id""" | ||
| try: | ||
| data = cls._request_store[request_id][panel_id] | ||
| except KeyError: | ||
| return {} | ||
| else: | ||
| return deserialize(data) | ||
|
|
||
|
|
||
| def get_store(): | ||
| return import_string(dt_settings.get_config()["TOOLBAR_STORE_CLASS"]) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| from django.test import TestCase | ||
| from django.test.utils import override_settings | ||
|
|
||
| from debug_toolbar import store | ||
|
|
||
|
|
||
| class SerializationTestCase(TestCase): | ||
| def test_serialize(self): | ||
| self.assertEqual( | ||
| store.serialize({"hello": {"foo": "bar"}}), | ||
| '{"hello": {"foo": "bar"}}', | ||
| ) | ||
|
|
||
| def test_serialize_force_str(self): | ||
| class Foo: | ||
| spam = "bar" | ||
|
|
||
| def __str__(self): | ||
| return f"Foo spam={self.spam}" | ||
|
|
||
| self.assertEqual( | ||
| store.serialize({"hello": Foo()}), | ||
| '{"hello": "Foo spam=bar"}', | ||
| ) | ||
|
|
||
| def test_deserialize(self): | ||
| self.assertEqual( | ||
| store.deserialize('{"hello": {"foo": "bar"}}'), | ||
| {"hello": {"foo": "bar"}}, | ||
| ) | ||
|
|
||
|
|
||
| class BaseStoreTestCase(TestCase): | ||
| def test_methods_are_not_implemented(self): | ||
| # Find all the non-private and dunder class methods | ||
| methods = [ | ||
| member for member in vars(store.BaseStore) if not member.startswith("_") | ||
| ] | ||
| self.assertEqual(len(methods), 7) | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.ids() | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.exists("") | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.set("") | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.clear() | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.delete("") | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.save_panel("", "", None) | ||
| with self.assertRaises(NotImplementedError): | ||
| store.BaseStore.panel("", "") | ||
|
|
||
|
|
||
| class MemoryStoreTestCase(TestCase): | ||
| @classmethod | ||
| def setUpTestData(cls) -> None: | ||
| cls.store = store.MemoryStore | ||
|
|
||
| def tearDown(self) -> None: | ||
| self.store.clear() | ||
|
|
||
| def test_ids(self): | ||
| self.store.set("foo") | ||
| self.store.set("bar") | ||
| self.assertEqual(list(self.store.ids()), ["foo", "bar"]) | ||
|
|
||
| def test_exists(self): | ||
| self.assertFalse(self.store.exists("missing")) | ||
| self.store.set("exists") | ||
| self.assertTrue(self.store.exists("exists")) | ||
|
|
||
| def test_set(self): | ||
| self.store.set("foo") | ||
| self.assertEqual(list(self.store.ids()), ["foo"]) | ||
|
|
||
| def test_set_max_size(self): | ||
| existing = self.store._config["RESULTS_CACHE_SIZE"] | ||
| self.store._config["RESULTS_CACHE_SIZE"] = 1 | ||
| self.store.save_panel("foo", "foo.panel", "foo.value") | ||
| self.store.save_panel("bar", "bar.panel", {"a": 1}) | ||
| self.assertEqual(list(self.store.ids()), ["bar"]) | ||
| self.assertEqual(self.store.panel("foo", "foo.panel"), {}) | ||
| self.assertEqual(self.store.panel("bar", "bar.panel"), {"a": 1}) | ||
| # Restore the existing config setting since this config is shared. | ||
| self.store._config["RESULTS_CACHE_SIZE"] = existing | ||
|
|
||
| def test_clear(self): | ||
| self.store.save_panel("bar", "bar.panel", {"a": 1}) | ||
| self.store.clear() | ||
| self.assertEqual(list(self.store.ids()), []) | ||
| self.assertEqual(self.store.panel("bar", "bar.panel"), {}) | ||
|
|
||
| def test_delete(self): | ||
| self.store.save_panel("bar", "bar.panel", {"a": 1}) | ||
| self.store.delete("bar") | ||
| self.assertEqual(list(self.store.ids()), []) | ||
| self.assertEqual(self.store.panel("bar", "bar.panel"), {}) | ||
| # Make sure it doesn't error | ||
| self.store.delete("bar") | ||
|
|
||
| def test_save_panel(self): | ||
| self.store.save_panel("bar", "bar.panel", {"a": 1}) | ||
| self.assertEqual(list(self.store.ids()), ["bar"]) | ||
| self.assertEqual(self.store.panel("bar", "bar.panel"), {"a": 1}) | ||
|
|
||
| def test_panel(self): | ||
| self.assertEqual(self.store.panel("missing", "missing"), {}) | ||
| self.store.save_panel("bar", "bar.panel", {"a": 1}) | ||
| self.assertEqual(self.store.panel("bar", "bar.panel"), {"a": 1}) | ||
|
|
||
|
|
||
| class StubStore(store.BaseStore): | ||
| pass | ||
|
|
||
|
|
||
| class GetStoreTestCase(TestCase): | ||
| def test_get_store(self): | ||
| self.assertIs(store.get_store(), store.MemoryStore) | ||
|
|
||
| @override_settings( | ||
| DEBUG_TOOLBAR_CONFIG={"TOOLBAR_STORE_CLASS": "tests.test_store.StubStore"} | ||
| ) | ||
| def test_get_store_with_setting(self): | ||
| self.assertIs(store.get_store(), StubStore) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
_request_ids = deque(maxlen=dt_settings.get_config()["RESULTS_CACHE_SIZE"])would keep this bounded without having to manually pop items off. You'd still have to maintain the contents of_request_store.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing things from
_request_storeis the problem unfortunately. We need the ids from_request_idsto remove the correct elements from_request_store. If we make_request_idsbounded, we lose the ability to identify which ids are being removed from the collection to remove the correct elements from the store.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, after adding to
_request_idsyou'd have to get the set of current ids, find the stale ids in the_request_storeif any got popped off b/c of the bounding, then delete those.... which would be less efficient. What you have is optimized, I agree.