Skip to content
This repository was archived by the owner on Feb 11, 2023. It is now read-only.

Commit 990657a

Browse files
authored
Merge pull request #160 from jbasko/track-changes2
Track changes fixes and reset_changes() added
2 parents c0d95f3 + ba6e707 commit 990657a

8 files changed

Lines changed: 314 additions & 7 deletions

File tree

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.28.0
2+
current_version = 1.29.0
33
commit = true
44
tag = false
55

configmanager/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '1.28.0'
1+
__version__ = '1.29.0'
22

33
from .managers import Config
44
from .items import Item

configmanager/items.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,62 @@ def get(self, fallback=not_set):
204204
raise RequiredValueMissing(name=self.name, item=self)
205205
return fallback
206206

207+
"""
208+
Design comment:
209+
210+
It is by design that we report not_set as old_value when value is set for the first time,
211+
and not_set as new_value when item is reset.
212+
This is because reporting effective value would cause inconsistency in what is reported.
213+
Listeners of item_value_changed can always look into item.default or item.value.
214+
"""
215+
207216
def set(self, value):
208217
"""
209218
Sets config value.
210219
"""
211220
old_value = self._value
221+
212222
self.type.set_item_value(self, value)
223+
213224
new_value = self._value
225+
226+
if old_value is not_set and new_value is not_set:
227+
# Nothing to report
228+
return
229+
214230
if self.section:
215-
self.section._trigger_event(self.section.hooks.item_value_changed, item=self, old_value=old_value, new_value=new_value)
231+
self.section._trigger_event(
232+
self.section.hooks.item_value_changed,
233+
item=self,
234+
old_value=old_value,
235+
new_value=new_value,
236+
)
216237

217238
def reset(self):
218239
"""
219240
Resets the value of config item to its default value.
220241
"""
242+
old_value = self._value
243+
221244
self._value = not_set
222245
self.raw_str_value = not_set
223246

247+
new_value = self._value
248+
249+
if old_value is not_set:
250+
# Nothing to report
251+
return
252+
253+
if self.section:
254+
self.section._trigger_event(
255+
self.section.hooks.item_value_changed,
256+
item=self,
257+
old_value=old_value,
258+
new_value=new_value,
259+
)
260+
261+
262+
224263
@property
225264
def is_default(self):
226265
"""

configmanager/managers.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import collections
2+
13
from .utils import _get_persistence_adapter_for
24
from .schema_parser import parse_config_schema
35
from .meta import ConfigManagerSettings
@@ -9,7 +11,7 @@ class _TrackingContext(object):
911
def __init__(self, config):
1012
self.config = config
1113
self.hook = None
12-
self.changes = {}
14+
self._changes = collections.defaultdict(list)
1315

1416
def __enter__(self):
1517
return self.push()
@@ -19,7 +21,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
1921

2022
def _value_changed(self, item, old_value, new_value):
2123
if old_value != new_value:
22-
self.changes[item] = new_value
24+
self._changes[item].append((old_value, new_value))
2325

2426
def push(self):
2527
assert self.hook is None
@@ -32,7 +34,28 @@ def pop(self):
3234
assert popped is self
3335
self.config.hooks.unregister_hook(self.config.hooks.item_value_changed, self._value_changed)
3436
self.hook = None
35-
return self.changes
37+
38+
@property
39+
def changes(self):
40+
values = {}
41+
for k, k_changes in self._changes.items():
42+
if len(k_changes) == 1:
43+
values[k] = k_changes[0][1]
44+
elif k_changes[0][0] != k_changes[-1][1]:
45+
values[k] = k_changes[-1][1]
46+
return values
47+
48+
def reset_changes(self, item=None):
49+
50+
for k, k_changes in self._changes.items():
51+
if item is None or k is item:
52+
# TODO This doesn't reset raw_str_value properly ...
53+
k._value = k_changes[0][0]
54+
55+
if item is None:
56+
self._changes.clear()
57+
else:
58+
del self._changes[item]
3659

3760

3861
class Config(Section):

configmanager/sections.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ def get_proxy(self, *key):
255255

256256
@property
257257
def hooks(self):
258+
"""
259+
Returns:
260+
HookRegistry
261+
"""
258262
return self._hooks
259263

260264
@property

docs/index.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,17 @@ to be called when this exception is raised:
325325
326326
327327
If this function returns anything other than ``None``, the exception will not be raised.
328+
329+
How can I track changes of config values?
330+
-----------------------------------------
331+
332+
.. code-block:: python
333+
334+
>>> with config.tracking_context() as ctx:
335+
... config.greeting.value = 'Hey, what is up!'
336+
337+
>>> len(ctx.changes)
338+
1
339+
340+
>>> ctx.changes[config.greeting]
341+
'Hey, what is up!'

tests/test_hooks.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from configmanager import Config, NotFound, Section
3+
from configmanager import Config, NotFound, Section, Item
44
from configmanager.utils import not_set
55

66

@@ -255,6 +255,68 @@ def item_value_changed(old_value=None, new_value=None, item=None, **kwargs):
255255
assert calls[-1] == (config.uploads.db.user, 'Administrator', 'NEW VALUE')
256256

257257

258+
def test_item_value_changed_reports_not_set_as_old_value_if_there_was_no_value_before():
259+
config = Config({'a': 'aaa'})
260+
calls = []
261+
262+
def first(old_value, new_value):
263+
assert old_value is not_set
264+
assert new_value == 'bbb'
265+
calls.append(1)
266+
267+
def second(old_value, new_value):
268+
assert old_value == 'bbb'
269+
assert new_value == 'aaa'
270+
calls.append(2)
271+
272+
config.hooks.register_hook('item_value_changed', first)
273+
config.a.value = 'bbb'
274+
config.hooks.unregister_hook('item_value_changed', first)
275+
276+
config.hooks.register_hook('item_value_changed', second)
277+
config.a.value = 'aaa'
278+
config.hooks.unregister_hook('item_value_changed', second)
279+
280+
assert calls == [1, 2]
281+
282+
283+
def test_item_value_changed_hook_called_on_item_reset():
284+
config = Config({'a': 'aaa', 'b': 'bbb', 'c': Item()})
285+
calls = []
286+
287+
@config.hooks.item_value_changed
288+
def item_value_changed(item, old_value, new_value):
289+
calls.append(item.name)
290+
291+
assert len(calls) == 0
292+
293+
config.reset()
294+
assert len(calls) == 0
295+
296+
# Setting same value as default value triggers the event
297+
config.a.value = 'aaa'
298+
assert calls == ['a']
299+
300+
# Setting same value as the custom value before triggers the event
301+
config.a.value = 'aaa'
302+
assert calls == ['a', 'a']
303+
304+
# Actual reset
305+
config.reset()
306+
assert calls == ['a', 'a', 'a']
307+
308+
309+
def test_item_value_changed_hook_not_called_when_resetting_a_not_set():
310+
config = Config({'a': Item()})
311+
312+
@config.hooks.item_value_changed
313+
def item_value_changed(item, old_value, new_value):
314+
raise AssertionError('This should not have been called')
315+
316+
config.reset()
317+
config.a.value = not_set
318+
319+
258320
def test_hooks_arent_handled_if_hooks_enabled_setting_is_set_to_falsey_value():
259321
config = Config({
260322
'uploads': {

0 commit comments

Comments
 (0)