interactive-pipe 0.9.0__tar.gz → 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {interactive_pipe-0.9.0/src/interactive_pipe.egg-info → interactive_pipe-0.9.1}/PKG-INFO +1 -1
  2. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/pyproject.toml +1 -1
  3. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/__init__.py +1 -1
  4. interactive_pipe-0.9.1/src/interactive_pipe/core/context_tracking.py +318 -0
  5. interactive_pipe-0.9.1/src/interactive_pipe/core/engine.py +352 -0
  6. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/filter.py +5 -0
  7. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/pipeline.py +54 -5
  8. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/pipeline.py +1 -0
  9. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/filter_decorator.py +11 -4
  10. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/pipeline_decorator.py +20 -3
  11. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1/src/interactive_pipe.egg-info}/PKG-INFO +1 -1
  12. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/SOURCES.txt +3 -0
  13. interactive_pipe-0.9.1/test/test_graph_cache.py +511 -0
  14. interactive_pipe-0.9.1/test/test_readonly_inputs.py +253 -0
  15. interactive_pipe-0.9.0/src/interactive_pipe/core/engine.py +0 -186
  16. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/LICENSE +0 -0
  17. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/readme.md +0 -0
  18. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/setup.cfg +0 -0
  19. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/__init__.py +0 -0
  20. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/backend.py +0 -0
  21. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/cache.py +0 -0
  22. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/context.py +0 -0
  23. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/framework_state.py +0 -0
  24. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/graph.py +0 -0
  25. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/signature.py +0 -0
  26. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/__init__.py +0 -0
  27. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/audio.py +0 -0
  28. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/curves.py +0 -0
  29. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/data.py +0 -0
  30. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/image.py +0 -0
  31. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/parameters.py +0 -0
  32. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/table.py +0 -0
  33. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/__init__.py +0 -0
  34. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_control.py +0 -0
  35. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_gui.py +0 -0
  36. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_layout.py +0 -0
  37. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_outputs.py +0 -0
  38. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gui.py +0 -0
  39. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_control.py +0 -0
  40. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_gui.py +0 -0
  41. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_window.py +0 -0
  42. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/nb_control.py +0 -0
  43. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/nb_gui.py +0 -0
  44. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_audio.py +0 -0
  45. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_backend.py +0 -0
  46. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_control.py +0 -0
  47. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_gui.py +0 -0
  48. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_image.py +0 -0
  49. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_panels.py +0 -0
  50. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_widgets.py +0 -0
  51. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/window.py +0 -0
  52. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/__init__.py +0 -0
  53. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/control.py +0 -0
  54. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/keyboard.py +0 -0
  55. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/panel.py +0 -0
  56. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/__init__.py +0 -0
  57. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/_private.py +0 -0
  58. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/choose_backend.py +0 -0
  59. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/control_abbreviation.py +0 -0
  60. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/keyword_args_analyzer.py +0 -0
  61. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/py.typed +0 -0
  62. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/dependency_links.txt +0 -0
  63. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/requires.txt +0 -0
  64. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/top_level.txt +0 -0
  65. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_cache.py +0 -0
  66. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_context.py +0 -0
  67. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_control_abbreviations.py +0 -0
  68. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_control_registry.py +0 -0
  69. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_controller.py +0 -0
  70. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_core.py +0 -0
  71. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_core_exceptions.py +0 -0
  72. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_curves.py +0 -0
  73. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_data_objects_exceptions.py +0 -0
  74. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_decorator.py +0 -0
  75. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_edge_cases.py +0 -0
  76. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_engine.py +0 -0
  77. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_filter.py +0 -0
  78. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_framework_state.py +0 -0
  79. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_graphical_exceptions.py +0 -0
  80. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_headless.py +0 -0
  81. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_headless_exceptions.py +0 -0
  82. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_image.py +0 -0
  83. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_legacy_removal.py +0 -0
  84. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_mutable_defaults.py +0 -0
  85. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_parameters.py +0 -0
  86. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_qt_backend_cascade.py +0 -0
  87. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_recorder.py +0 -0
  88. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_gradio.py +0 -0
  89. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_mpl.py +0 -0
  90. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_qt.py +0 -0
  91. {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interactive_pipe
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!
5
5
  Author-email: Balthazar Neveu <balthazarneveu@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/balthazarneveu/interactive_pipe
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "interactive_pipe"
8
- version = "0.9.0"
8
+ version = "0.9.1"
9
9
  authors = [
10
10
  { name="Balthazar Neveu", email="balthazarneveu@gmail.com" },
11
11
  ]
@@ -1,4 +1,4 @@
1
- __version__ = "0.9.0"
1
+ __version__ = "0.9.1"
2
2
  # Backend enum for type-safe backend specification
3
3
  from interactive_pipe.core.backend import Backend
4
4
 
@@ -0,0 +1,318 @@
1
+ """Runtime tracking of user-context reads and writes, powering the dependency-aware cache.
2
+
3
+ When a pipeline runs with ``cache="graph"``, the user context (accessed by filters through
4
+ the ``context`` proxy or ``get_context()``) is wrapped in a :class:`ContextTracker`.
5
+ The tracker records which filter reads and which filter writes each context key, so the
6
+ engine can invalidate the cached result of a filter whenever a context key it relies on
7
+ has been updated - even though this data dependency is invisible to the AST call graph.
8
+
9
+ Change detection combines two mechanisms:
10
+
11
+ - dict instrumentation attributes reads and writes to the running filter;
12
+ - content fingerprints (:func:`_fingerprint`) catch what instrumentation cannot see:
13
+ in-place mutation of stored objects (``context["boxes"].append(...)``) is detected by
14
+ re-fingerprinting every key a filter accessed when it finishes, and mutations happening
15
+ outside any filter (GUI callbacks, stashed references) are detected at the start of the
16
+ next run (:meth:`ContextTracker.detect_silent_changes`).
17
+
18
+ Known limitations (conservative by design, documented for users of ``cache="graph"``):
19
+
20
+ - A value mutated through a reference stashed in a previous run is only detected at the
21
+ next run start: the runs in between may serve one stale frame for its readers.
22
+ - ``dict(context)`` style copies bypass instrumentation; prefer explicit key access or
23
+ ``context.items()`` which registers the filter as a reader of every key.
24
+ - Unpicklable values cannot be fingerprinted: their key counts as changed on every check,
25
+ so their readers are recomputed on every run (never stale, but never cached either).
26
+ """
27
+
28
+ import hashlib
29
+ import pickle
30
+ from typing import Any, Dict, Optional, Set
31
+
32
+ # cache modes enabling dependency-aware caching; "graph-strict" additionally returns
33
+ # numpy arrays as read-only views so in-place mutation raises at the offending line
34
+ GRAPH_CACHE_MODES = ("graph", "graph-strict")
35
+
36
+ _UNSET = object()
37
+
38
+
39
+ def _fingerprint(value: Any) -> Any:
40
+ """Content digest used to decide whether a context value actually changed.
41
+
42
+ Detects in-place mutation that dict instrumentation alone cannot see, and
43
+ avoids spurious invalidation when a filter rewrites an equal value.
44
+
45
+ - primitives: the value itself (with its type, so True != 1)
46
+ - numpy arrays: shape + dtype + hash of the raw buffer (one fast memory pass)
47
+ - anything else: hash of its pickle serialization
48
+ - unpicklable values: a unique marker that never compares equal, so the key is
49
+ conservatively considered changed on every check (extra recompute, never stale)
50
+ """
51
+ if value is None or isinstance(value, (bool, int, float, str, bytes)):
52
+ return ("primitive", type(value).__name__, value)
53
+ value_type = type(value)
54
+ if value_type.__module__ == "numpy" and value_type.__name__ == "ndarray":
55
+ return ("ndarray", value.shape, str(value.dtype), hashlib.sha1(value.tobytes()).digest())
56
+ try:
57
+ payload = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
58
+ except Exception:
59
+ return object()
60
+ return ("pickle", hashlib.sha1(payload).digest())
61
+
62
+
63
+ class ContextTracker(dict):
64
+ """Dict recording per-filter reads and writes of the user context.
65
+
66
+ - Accesses performed while a filter runs (between :meth:`begin_filter` and
67
+ :meth:`finish_filter`) are attributed to that filter; accesses outside any filter
68
+ (GUI event handlers, ``pipeline(..., context={...})`` updates) are recorded as
69
+ *external changes* consumed by the engine at the start of the next run.
70
+ - Read tracking is cumulative across runs: once a filter is known to read a key,
71
+ it stays registered as a reader of that key.
72
+ - Whole-dict enumeration (``keys``/``values``/``items``/iteration/``len``) registers
73
+ the current filter as a reader of every key (conservative).
74
+ - A key only counts as changed when its content fingerprint differs from the
75
+ baseline (the net effect of the filter, judged at the filter boundary), so
76
+ rewriting an equal value never invalidates readers, while in-place mutations
77
+ are reliably detected.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ initial: Optional[Dict[str, Any]] = None,
83
+ ignore_prefix: Optional[str] = None,
84
+ strict: bool = False,
85
+ ):
86
+ super().__init__(initial or {})
87
+ # strict mode: reads return numpy arrays as read-only views, so in-place
88
+ # mutation raises a ValueError at the offending user line instead of being
89
+ # silently absorbed by fingerprint detection
90
+ self._strict = strict
91
+ self._reads: Dict[str, Set[Any]] = {} # filter name -> keys it reads
92
+ self._reads_all: Set[str] = set() # filters enumerating the whole context
93
+ self._current: Optional[str] = None # name of the filter currently running
94
+ self._external_changes: Set[Any] = set() # keys changed outside any filter
95
+ self._current_touched: Set[Any] = set() # keys accessed by the current filter
96
+ self._current_read_all = False # current filter enumerated the whole context
97
+ # keys starting with this prefix are not tracked at all
98
+ # (defensive exclusion of framework-internal keys when wrapping shared dicts)
99
+ self._ignore_prefix = ignore_prefix
100
+ # content fingerprints: baseline for change detection, including in-place mutation
101
+ self._digests: Dict[Any, Any] = {}
102
+ for key in dict.keys(self):
103
+ if not self._ignored(key):
104
+ self._digests[key] = _fingerprint(dict.__getitem__(self, key))
105
+
106
+ def _ignored(self, key: Any) -> bool:
107
+ return self._ignore_prefix is not None and isinstance(key, str) and key.startswith(self._ignore_prefix)
108
+
109
+ def _wrap_readonly(self, value: Any) -> Any:
110
+ if not self._strict:
111
+ return value
112
+ value_type = type(value)
113
+ if value_type.__module__ == "numpy" and value_type.__name__ == "ndarray":
114
+ view = value.view()
115
+ view.flags.writeable = False
116
+ return view
117
+ return value
118
+
119
+ # ------------------------------------------------------------------
120
+ # Engine hooks
121
+ # ------------------------------------------------------------------
122
+ def begin_filter(self, name: str) -> None:
123
+ """Attribute subsequent context accesses to the given filter."""
124
+ self._current = name
125
+ self._current_touched = set()
126
+ self._current_read_all = False
127
+
128
+ def finish_filter(self) -> Set[Any]:
129
+ """Stop attributing accesses and return the keys the filter NET-changed.
130
+
131
+ Every key the filter accessed is re-fingerprinted and compared against the
132
+ stored baseline, so that:
133
+ - in-place mutation of a stored object (context["boxes"].append(...)) counts
134
+ as a change even though no dict write ever happened;
135
+ - only the net effect matters: a filter resetting then rebuilding an equal
136
+ value (context["boxes"] = []; ...append(...)) leaves the key unchanged,
137
+ which lets feedback/self loops converge instead of recomputing forever.
138
+ """
139
+ touched = self._current_touched
140
+ if self._current_read_all:
141
+ touched = touched | {key for key in dict.keys(self) if not self._ignored(key)}
142
+ changes: Set[Any] = set()
143
+ for key in touched:
144
+ if dict.__contains__(self, key):
145
+ new_digest = _fingerprint(dict.__getitem__(self, key))
146
+ old_digest = self._digests.get(key, _UNSET)
147
+ self._digests[key] = new_digest
148
+ if old_digest is _UNSET or old_digest != new_digest:
149
+ changes.add(key)
150
+ elif self._digests.pop(key, _UNSET) is not _UNSET:
151
+ # key deleted by this filter
152
+ changes.add(key)
153
+ self._current = None
154
+ self._current_touched = set()
155
+ self._current_read_all = False
156
+ return changes
157
+
158
+ def detect_silent_changes(self) -> Set[Any]:
159
+ """Re-fingerprint every key having a registered reader and return those whose
160
+ content changed through untracked paths since the last check (in-place mutation
161
+ of a stored object between runs: GUI callbacks, stashed references...).
162
+
163
+ Called by the engine at the start of each run.
164
+ """
165
+ monitored: Set[Any] = set()
166
+ for keys in self._reads.values():
167
+ monitored |= keys
168
+ if self._reads_all:
169
+ monitored |= {key for key in dict.keys(self) if not self._ignored(key)}
170
+ changed = set()
171
+ for key in monitored:
172
+ if not dict.__contains__(self, key):
173
+ continue
174
+ new_digest = _fingerprint(dict.__getitem__(self, key))
175
+ old_digest = self._digests.get(key, _UNSET)
176
+ self._digests[key] = new_digest
177
+ if old_digest is not _UNSET and old_digest != new_digest:
178
+ changed.add(key)
179
+ return changed
180
+
181
+ def report_aborted_run(self, changed_keys: Set[Any]) -> None:
182
+ """Persist an aborted run's context changes as external changes.
183
+
184
+ When a filter raises, the run dies before downstream readers consume the keys
185
+ already changed this run (and before feedback edges are processed) - but the
186
+ fingerprint baselines were already advanced. Re-injecting the changes as
187
+ external ones makes the next run invalidate every reader of those keys.
188
+ """
189
+ self._external_changes |= changed_keys | self.finish_filter()
190
+
191
+ def consume_external_changes(self) -> Set[Any]:
192
+ """Return keys changed outside of any filter since the last run, and reset."""
193
+ changes = self._external_changes
194
+ self._external_changes = set()
195
+ return changes
196
+
197
+ def reads_changed_keys(self, filter_name: str, changed_keys: Set[Any]) -> bool:
198
+ """Check whether a filter is a known reader of any of the changed keys."""
199
+ if not changed_keys:
200
+ return False
201
+ if filter_name in self._reads_all:
202
+ return True
203
+ return bool(self._reads.get(filter_name, set()) & changed_keys)
204
+
205
+ def readers_of(self, key: Any) -> Set[str]:
206
+ """Names of all filters known to read the given key."""
207
+ readers = set(self._reads_all)
208
+ for name, keys in self._reads.items():
209
+ if key in keys:
210
+ readers.add(name)
211
+ return readers
212
+
213
+ # ------------------------------------------------------------------
214
+ # Recording helpers
215
+ # ------------------------------------------------------------------
216
+ def _record_read(self, key: Any) -> None:
217
+ if self._current is not None and not self._ignored(key):
218
+ self._reads.setdefault(self._current, set()).add(key)
219
+ self._current_touched.add(key)
220
+
221
+ def _record_read_all(self) -> None:
222
+ if self._current is not None:
223
+ self._reads_all.add(self._current)
224
+ self._current_read_all = True
225
+
226
+ def _record_write(self, key: Any, new_value: Any = _UNSET) -> None:
227
+ if self._ignored(key):
228
+ return
229
+ if self._current is not None:
230
+ # net change decided at finish_filter by fingerprint comparison
231
+ self._current_touched.add(key)
232
+ return
233
+ # write outside any filter (GUI events, user code between runs)
234
+ if new_value is _UNSET:
235
+ # deletion: a change when the key was known
236
+ if self._digests.pop(key, _UNSET) is not _UNSET:
237
+ self._external_changes.add(key)
238
+ else:
239
+ new_digest = _fingerprint(new_value)
240
+ old_digest = self._digests.get(key, _UNSET)
241
+ self._digests[key] = new_digest
242
+ if old_digest is _UNSET or old_digest != new_digest:
243
+ self._external_changes.add(key)
244
+
245
+ # ------------------------------------------------------------------
246
+ # Instrumented dict interface
247
+ # ------------------------------------------------------------------
248
+ def __getitem__(self, key):
249
+ self._record_read(key)
250
+ return self._wrap_readonly(dict.__getitem__(self, key))
251
+
252
+ def get(self, key, default=None):
253
+ self._record_read(key)
254
+ if dict.__contains__(self, key):
255
+ return self._wrap_readonly(dict.__getitem__(self, key))
256
+ return default
257
+
258
+ def __contains__(self, key):
259
+ self._record_read(key)
260
+ return dict.__contains__(self, key)
261
+
262
+ def __setitem__(self, key, value):
263
+ self._record_write(key, value)
264
+ dict.__setitem__(self, key, value)
265
+
266
+ def setdefault(self, key, default=None):
267
+ self._record_read(key)
268
+ if not dict.__contains__(self, key):
269
+ self._record_write(key, default)
270
+ return self._wrap_readonly(dict.setdefault(self, key, default))
271
+
272
+ def __delitem__(self, key):
273
+ if dict.__contains__(self, key):
274
+ self._record_write(key)
275
+ dict.__delitem__(self, key)
276
+
277
+ def pop(self, key, *args):
278
+ self._record_read(key)
279
+ if dict.__contains__(self, key):
280
+ self._record_write(key)
281
+ return dict.pop(self, key, *args)
282
+
283
+ def popitem(self):
284
+ self._record_read_all()
285
+ key, value = dict.popitem(self)
286
+ self._record_write(key)
287
+ return key, value
288
+
289
+ def update(self, *args, **kwargs):
290
+ incoming = dict(*args, **kwargs)
291
+ for key, value in incoming.items():
292
+ self._record_write(key, value)
293
+ dict.update(self, incoming)
294
+
295
+ def clear(self):
296
+ for key in list(dict.keys(self)):
297
+ self._record_write(key)
298
+ dict.clear(self)
299
+
300
+ def keys(self):
301
+ self._record_read_all()
302
+ return dict.keys(self)
303
+
304
+ def values(self):
305
+ self._record_read_all()
306
+ return dict.values(self)
307
+
308
+ def items(self):
309
+ self._record_read_all()
310
+ return dict.items(self)
311
+
312
+ def __iter__(self):
313
+ self._record_read_all()
314
+ return dict.__iter__(self)
315
+
316
+ def __len__(self):
317
+ self._record_read_all()
318
+ return dict.__len__(self)