-
Notifications
You must be signed in to change notification settings - Fork 51
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
Add CachedViews #146
base: main
Are you sure you want to change the base?
Add CachedViews #146
Changes from 13 commits
2696abc
6dda361
9522958
b891b3f
c4a2050
37491e8
d6a3722
44cc666
a253275
751537f
17ed5f9
98e4982
da6c598
b5444ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from typing import Dict, List | ||
|
||
from . import templates | ||
from .core import ROOT_NAME, Configuration, ConfigView, RootView, Subview | ||
|
||
|
||
class CachedHandle(object): | ||
"""Handle for a cached value computed by applying a template on the view. | ||
""" | ||
# some sentinel objects | ||
_INVALID = object() | ||
_MISSING = object() | ||
|
||
def __init__(self, view: ConfigView, template=templates.REQUIRED) -> None: | ||
self.value = self._INVALID | ||
self.view = view | ||
self.template = template | ||
|
||
def get(self): | ||
"""Retreive the cached value from the handle. | ||
|
||
Will re-compute the value using `view.get(template)` if it has been | ||
invalidated. | ||
|
||
May raise a `NotFoundError` if the underlying view has been | ||
invalidated. | ||
iamkroot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
if self.value is self._MISSING: | ||
# will raise a NotFoundError if no default value was provided | ||
self.value = templates.as_template(self.template).get_default_value() | ||
iamkroot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if self.value is self._INVALID: | ||
self.value = self.view.get(self.template) | ||
return self.value | ||
|
||
def _invalidate(self): | ||
"""Invalidate the cached value, will be repopulated on next `get()`. | ||
""" | ||
self.value = self._INVALID | ||
|
||
def _set_view_missing(self): | ||
"""Invalidate the handle, will raise `NotFoundError` on `get()`. | ||
""" | ||
self.value = self._MISSING | ||
|
||
|
||
class CachedViewMixin: | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
# keep track of all the handles from this view | ||
self.handles: List[CachedHandle] = [] | ||
# need to cache the subviews to be able to access their handles | ||
self.subviews: Dict[str, CachedConfigView] = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if this is a good idea, but just to try it out here: would it simplify things at all to keep the list of handles only on the The reason I'm slightly nervous (perhaps unfoundedly) about the latter thing is that we have never before required that subviews be unique… that is, doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should mention that I could perhaps try prototyping this idea if it seems plausible to you! |
||
|
||
def __getitem__(self, key) -> "CachedConfigView": | ||
try: | ||
return self.subviews[key] | ||
except KeyError: | ||
val = CachedConfigView(self, key) | ||
self.subviews[key] = val | ||
return val | ||
|
||
def __setitem__(self, key, value): | ||
subview: CachedConfigView = self[key] | ||
# invalidate the existing handles up and down the view tree | ||
for handle in subview.handles: | ||
handle._invalidate() | ||
subview._invalidate_descendants(value) | ||
self._invalidate_ancestors() | ||
|
||
return super().__setitem__(key, value) | ||
|
||
def _invalidate_ancestors(self): | ||
"""Invalidate the cached handles for all the views up the chain. | ||
|
||
This is to ensure that they aren't referring to stale values. | ||
""" | ||
parent = self | ||
while True: | ||
for handle in parent.handles: | ||
handle._invalidate() | ||
if parent.name == ROOT_NAME: | ||
break | ||
parent = parent.parent | ||
|
||
def _invalidate_descendants(self, new_val): | ||
"""Invalidate the handles for (sub)keys that were updated and | ||
set_view_missing for keys that are absent in new_val. | ||
""" | ||
for subview in self.subviews.values(): | ||
try: | ||
subval = new_val[subview.key] | ||
except (KeyError, IndexError, TypeError): | ||
# the old key doesn't exist in the new value anymore- | ||
# set view as missing for the handles. | ||
for handle in subview.handles: | ||
handle._set_view_missing() | ||
subval = None | ||
else: | ||
# old key is present, possibly with a new value- invalidate. | ||
for handle in subview.handles: | ||
handle._invalidate() | ||
subview._invalidate_descendants(subval) | ||
|
||
def get_handle(self, template=templates.REQUIRED): | ||
"""Retreive a `CachedHandle` for the current view and template. | ||
""" | ||
handle = CachedHandle(self, template) | ||
self.handles.append(handle) | ||
return handle | ||
|
||
|
||
class CachedConfigView(CachedViewMixin, Subview): | ||
pass | ||
|
||
|
||
class CachedRootView(CachedViewMixin, RootView): | ||
pass | ||
|
||
|
||
class CachedConfiguration(CachedViewMixin, Configuration): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import unittest | ||
|
||
import confuse | ||
from confuse.cache import CachedConfigView, CachedHandle, CachedRootView | ||
from confuse.exceptions import NotFoundError | ||
from confuse.templates import Sequence | ||
|
||
|
||
class CachedViewTest(unittest.TestCase): | ||
def setUp(self) -> None: | ||
self.config = CachedRootView([confuse.ConfigSource.of( | ||
{"a": ["b", "c"], | ||
"x": {"y": [1, 2], "w": "z", "p": {"q": 3}}})]) | ||
return super().setUp() | ||
|
||
def test_basic(self): | ||
view: CachedConfigView = self.config['x']['y'] | ||
handle: CachedHandle = view.get_handle(Sequence(int)) | ||
self.assertEqual(handle.get(), [1, 2]) | ||
|
||
def test_update(self): | ||
view: CachedConfigView = self.config['x']['y'] | ||
handle: CachedHandle = view.get_handle(Sequence(int)) | ||
self.config['x']['y'] = [4, 5] | ||
self.assertEqual(handle.get(), [4, 5]) | ||
|
||
def test_subview_update(self): | ||
view: CachedConfigView = self.config['x']['y'] | ||
handle: CachedHandle = view.get_handle(Sequence(int)) | ||
self.config['x'] = {'y': [4, 5]} | ||
self.assertEqual(handle.get(), [4, 5]) | ||
|
||
def test_missing(self): | ||
view: CachedConfigView = self.config['x']['y'] | ||
handle: CachedHandle = view.get_handle(Sequence(int)) | ||
|
||
self.config['x'] = {'p': [4, 5]} | ||
# new dict doesn't have a 'y' key | ||
with self.assertRaises(NotFoundError): | ||
handle.get() | ||
|
||
def test_missing2(self): | ||
view: CachedConfigView = self.config['x']['w'] | ||
handle = view.get_handle(str) | ||
self.assertEqual(handle.get(), 'z') | ||
|
||
self.config['x'] = {'y': [4, 5]} | ||
# new dict doesn't have a 'w' key | ||
with self.assertRaises(NotFoundError): | ||
handle.get() | ||
|
||
def test_list_update(self): | ||
view: CachedConfigView = self.config['a'][1] | ||
handle = view.get_handle(str) | ||
self.assertEqual(handle.get(), 'c') | ||
self.config['a'][1] = 'd' | ||
self.assertEqual(handle.get(), 'd') | ||
|
||
def test_root_update(self): | ||
root = self.config | ||
handle = self.config.get_handle({'a': Sequence(str)}) | ||
self.assertDictEqual(handle.get(), {'a': ['b', 'c']}) | ||
root['a'] = ['c', 'd'] | ||
self.assertDictEqual(handle.get(), {'a': ['c', 'd']}) | ||
|
||
def test_parent_invalidation(self): | ||
view: CachedConfigView = self.config['x']['p'] | ||
handle = view.get_handle(dict) | ||
self.assertEqual(handle.get(), {'q': 3}) | ||
self.config['x']['p']['q'] = 4 | ||
self.assertEqual(handle.get(), {'q': 4}) |
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.
Maybe some short comments on these to explain what they mean would be helpful?
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.
Added.