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.
- {interactive_pipe-0.9.0/src/interactive_pipe.egg-info → interactive_pipe-0.9.1}/PKG-INFO +1 -1
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/pyproject.toml +1 -1
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/__init__.py +1 -1
- interactive_pipe-0.9.1/src/interactive_pipe/core/context_tracking.py +318 -0
- interactive_pipe-0.9.1/src/interactive_pipe/core/engine.py +352 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/filter.py +5 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/pipeline.py +54 -5
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/pipeline.py +1 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/filter_decorator.py +11 -4
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/pipeline_decorator.py +20 -3
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1/src/interactive_pipe.egg-info}/PKG-INFO +1 -1
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/SOURCES.txt +3 -0
- interactive_pipe-0.9.1/test/test_graph_cache.py +511 -0
- interactive_pipe-0.9.1/test/test_readonly_inputs.py +253 -0
- interactive_pipe-0.9.0/src/interactive_pipe/core/engine.py +0 -186
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/LICENSE +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/readme.md +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/setup.cfg +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/__init__.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/backend.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/cache.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/context.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/framework_state.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/graph.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/core/signature.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/__init__.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/audio.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/curves.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/data.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/image.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/parameters.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/data_objects/table.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/__init__.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_control.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_gui.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_layout.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gradio_outputs.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/gui.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_control.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_gui.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/mpl_window.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/nb_control.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/nb_gui.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_audio.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_backend.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_control.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_gui.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_image.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_panels.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/qt_widgets.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/graphical/window.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/__init__.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/control.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/keyboard.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/headless/panel.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/__init__.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/_private.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/choose_backend.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/control_abbreviation.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/helper/keyword_args_analyzer.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe/py.typed +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/dependency_links.txt +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/requires.txt +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/src/interactive_pipe.egg-info/top_level.txt +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_cache.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_context.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_control_abbreviations.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_control_registry.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_controller.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_core.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_core_exceptions.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_curves.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_data_objects_exceptions.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_decorator.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_edge_cases.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_engine.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_filter.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_framework_state.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_graphical_exceptions.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_headless.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_headless_exceptions.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_image.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_legacy_removal.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_mutable_defaults.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_parameters.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_qt_backend_cascade.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_recorder.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_gradio.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_mpl.py +0 -0
- {interactive_pipe-0.9.0 → interactive_pipe-0.9.1}/test/test_smoke_qt.py +0 -0
- {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.
|
|
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
|
|
@@ -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)
|