Option C oc_diagnostics
Loading...
Searching...
No Matches
id_instrumentation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_diagnostics/id_instrumentation.py
4
5"""
6ID Instrumentation Helper
7=========================
8
9This module rewrites Dash component IDs so that every interactive component
10receives a pattern-matching ID of the form:
11
12 id = {"event": "<original-id>"}
13
14This enables the UI event logger to capture all user interactions.
15
16Instrumentation behavior is controlled by:
17- Global toggle: INSTRUMENTATION_ENABLED
18- Debug level: DEBUG_LEVELS["INSTRUMENTATION"] (0–9)
19
20Instrumentation is recursive and safe:
21- Structural IDs are never rewritten
22- Dict IDs are never rewritten
23- Components without IDs are skipped
24- Custom components may be included depending on debug level
25- Logging verbosity depends on instrumentation debug level
26
27Responsibilities
28----------------
29- Recursively walk Dash component trees via the ``.children`` attribute.
30- Rewrite string IDs to ``{"event": "<original-id>"}`` when instrumentation
31 is active.
32- Skip structural IDs (defined in ``STRUCTURAL_ID_PREFIXES`` and
33 ``STRUCTURAL_ID_EXACT``).
34- Skip dict IDs and components without an ``id`` attribute.
35- Delegate verbosity decisions to ``unified_debug`` helpers
36 (``instrumentation_verbose()``, ``instrumentation_allow_custom_components()``).
37
38Diagnostics
39-----------
40Domain: INSTRUMENTATION
41Levels:
42 L2 — lifecycle
43 L3 — semantic details
44 L4 — deep tracing
45
46Contracts
47---------
48- Structural IDs (``root-group-*``, ``tab-group-*``, etc.) must never be
49 rewritten.
50- Dict IDs must never be rewritten.
51- Must return the component unchanged when ``instrumentation_active()`` is
52 ``False``.
53- Must not raise; errors during ID rewriting are silently swallowed.
54- Must not recurse beyond ``MAX_INSTRUMENT_DEPTH`` (500) levels; a L1
55 diagnostic is emitted and the subtree is returned unchanged.
56- Standard Dash base-class components are always instrumented when active.
57- Custom (non-Dash-base-class) components are only instrumented when
58 ``instrumentation_allow_custom_components()`` returns ``True``
59 (``INSTRUMENTATION`` level ≥ 5). This prevents accidental ID rewriting
60 of plain Python objects that happen to have a ``.id`` attribute.
61"""
62
65 _dbg,
66 instrumentation_active,
67 instrumentation_verbose,
68 instrumentation_allow_custom_components,
69)
70
71
72# -------------------------------------------------------------------
73# Structural IDs — these must NEVER be instrumented
74# -------------------------------------------------------------------
75
76STRUCTURAL_ID_PREFIXES = (
77 "root-group-",
78 "tab-group-",
79 "tab-content-",
80 "pane-group-",
81)
82
83STRUCTURAL_ID_EXACT = {
84 "theme-root",
85 "theme-style-injector",
86 "ui-theme-store",
87 "system-settings-store",
88 "data-settings-store",
89 "plot-settings-store",
90 "settings-data-store",
91 "debug-settings-data-store",
92 "debug-plot-settings-store",
93 "init-once",
94 "system-auto-load-interval",
95 "plot-figure", # Plot callback outputs to this
96 "plot-status", # Plot callback outputs to this
97 "plot-size-indicator", # Plot callback outputs to this
98}
99
100
102 """Return True if this ID should NOT be instrumented.
103
104 Checks both the built-in structural-ID lists and any project-defined
105 extensions loaded from ``global_settings.structural_id_prefixes`` /
106 ``global_settings.structural_id_exact`` in ``debug_config.json``
107 (FS-02 / OI-17).
108 """
109 if cid in STRUCTURAL_ID_EXACT or cid in _ud.ADDITIONAL_STRUCTURAL_EXACT:
110 return True
111 return (
112 any(cid.startswith(p) for p in STRUCTURAL_ID_PREFIXES)
113 or any(cid.startswith(p) for p in _ud.ADDITIONAL_STRUCTURAL_PREFIXES)
114 )
115
116
117# -------------------------------------------------------------------
118# Dash base-class detection
119# -------------------------------------------------------------------
120
121def _is_dash_component(component) -> bool:
122 """Return True if *component* inherits from the Dash base Component class.
123
124 Dash is an optional dependency. When it is not installed this function
125 always returns False, which causes all components to be treated as custom
126 and therefore subject to the ``instrumentation_allow_custom_components()``
127 level gate.
128 """
129 try:
130 from dash.development.base_component import Component as _DashBase # type: ignore[import]
131 return isinstance(component, _DashBase)
132 except ImportError:
133 return False
134
135
136# -------------------------------------------------------------------
137# Recursion depth limit
138# -------------------------------------------------------------------
139
140MAX_INSTRUMENT_DEPTH = 500
141"""Maximum recursion depth for ``instrument_ids``.
142
143Prevents ``RecursionError`` on pathologically deep component trees.
144A L1 diagnostic is emitted when the limit is reached and the subtree
145below that point is left uninstrumented. Real-world Dash applications
146rarely exceed 30–50 levels of nesting.
147"""
148
149
150# -------------------------------------------------------------------
151# Recursive ID instrumentation
152# -------------------------------------------------------------------
153
154def instrument_ids(component, _depth: int = 0):
155 """
156 Recursively instrument Dash component IDs.
157
158 Behavior is controlled by unified_debug:
159
160 - instrumentation_active():
161 False → return component unchanged
162
163 - instrumentation_verbose():
164 True → log detailed recursion info
165 False → log only high-level events
166
167 - instrumentation_allow_custom_components():
168 True → include custom (non-Dash-base-class) components
169 False → only standard Dash base-class components are instrumented
170
171 Rules:
172 - Instrument ANY component that has an ID stored in `.id`,
173 or in Dash internals (`_props`, `_prop_ids`).
174 - Only rewrite string IDs (ignore dict IDs, None, etc.).
175 - Skip structural IDs.
176 - Recurse into children (list, tuple, or single child).
177 - Stop recursion if depth exceeds MAX_INSTRUMENT_DEPTH.
178 """
179
180 # ---------------------------------------------------------------
181 # 0. Global instrumentation toggle
182 # ---------------------------------------------------------------
183 if not instrumentation_active():
184 return component
185
186 # ---------------------------------------------------------------
187 # 0b. Recursion depth guard (OI-06)
188 # ---------------------------------------------------------------
189 if _depth > MAX_INSTRUMENT_DEPTH:
190 _dbg(
191 "INSTRUMENTATION",
192 "instrument_ids",
193 f"MAX_INSTRUMENT_DEPTH ({MAX_INSTRUMENT_DEPTH}) exceeded — "
194 f"instrumentation truncated at depth {_depth}",
195 level=1,
196 )
197 return component
198
199 # ---------------------------------------------------------------
200 # 1. Verbose debug header
201 # ---------------------------------------------------------------
202 if instrumentation_verbose():
203 _dbg(
204 "INSTRUMENTATION",
205 "instrument_ids",
206 f"TYPE={type(component).__name__}, "
207 f"CHILDREN_TYPE={type(getattr(component, 'children', None))}, "
208 f"ID={getattr(component, 'id', None)}",
209 level=4,
210 )
211
212 # ---------------------------------------------------------------
213 # 2. Extract the component's ID from all possible Dash locations
214 # ---------------------------------------------------------------
215 cid = getattr(component, "id", None)
216
217 # Dash core components often store IDs internally
218 if cid is None and hasattr(component, "_prop_ids"):
219 cid = component._prop_ids.get("id")
220
221 if cid is None and hasattr(component, "_props"):
222 cid = component._props.get("id")
223
224 # ---------------------------------------------------------------
225 # 3. Instrument string IDs (skip dict IDs and structural IDs)
226 # ---------------------------------------------------------------
227 if isinstance(cid, str) and not is_structural_id(cid):
228
229 # Standard Dash components are always eligible.
230 # Custom (non-Dash-base-class) components require INSTRUMENTATION >= 5,
231 # unless instrumentation_allow_all is set (FS-13), which bypasses the
232 # type guard and instruments every component regardless of base class.
233 if _ud.INSTRUMENTATION_ALL or _is_dash_component(component) or instrumentation_allow_custom_components():
234
235 new_id = {"event": cid}
236
237 # Write back to all known Dash ID locations
238 try:
239 component.id = new_id
240 except Exception:
241 pass
242
243 if hasattr(component, "_prop_ids"):
244 component._prop_ids["id"] = new_id
245
246 if hasattr(component, "_props"):
247 component._props["id"] = new_id
248
249 if instrumentation_verbose():
250 _dbg(
251 "INSTRUMENTATION",
252 "instrument_ids",
253 f"Instrumented ID: {cid}",
254 level=4,
255 )
256
257 elif instrumentation_verbose():
258 _dbg(
259 "INSTRUMENTATION",
260 "instrument_ids",
261 f"Skipped custom component ID: {cid} (INSTRUMENTATION level < 5)",
262 level=4,
263 )
264
265 # ---------------------------------------------------------------
266 # 4. Recurse into children
267 # ---------------------------------------------------------------
268 children = getattr(component, "children", None)
269
270 if isinstance(children, (list, tuple)):
271 for ch in children:
272 instrument_ids(ch, _depth + 1)
273
274 elif children is not None:
275 instrument_ids(children, _depth + 1)
276
277 return component
instrument_ids(component, int _depth=0)