imgui_debugger 0.1.0__py3-none-any.whl
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.
- imgui_debugger/__init__.py +88 -0
- imgui_debugger/_assets.py +125 -0
- imgui_debugger/debugger.py +553 -0
- imgui_debugger/edit.py +125 -0
- imgui_debugger/format.py +130 -0
- imgui_debugger/py.typed +0 -0
- imgui_debugger/runner.py +91 -0
- imgui_debugger/scopes.py +734 -0
- imgui_debugger/search.py +170 -0
- imgui_debugger/theme.py +129 -0
- imgui_debugger/tree.py +285 -0
- imgui_debugger-0.1.0.dist-info/METADATA +225 -0
- imgui_debugger-0.1.0.dist-info/RECORD +16 -0
- imgui_debugger-0.1.0.dist-info/WHEEL +5 -0
- imgui_debugger-0.1.0.dist-info/licenses/LICENSE +21 -0
- imgui_debugger-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""The :class:`Debugger` widget and its configuration.
|
|
2
|
+
|
|
3
|
+
Examples
|
|
4
|
+
--------
|
|
5
|
+
>>> from imgui_debugger import attach
|
|
6
|
+
>>> class W:
|
|
7
|
+
... def __init__(self):
|
|
8
|
+
... self.visible = True
|
|
9
|
+
>>> dbg = attach(W(), show_frame=False, show_runtime=False)
|
|
10
|
+
>>> [s.name for s in dbg.scopes()]
|
|
11
|
+
['instance', 'properties', 'class']
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, List, Optional, Sequence, Tuple
|
|
20
|
+
|
|
21
|
+
from imgui_bundle import imgui, imgui_ctx
|
|
22
|
+
|
|
23
|
+
from .scopes import Scope, Watch, frame_scopes, object_scopes, runtime_scope
|
|
24
|
+
from .search import clear_cache
|
|
25
|
+
from .theme import Theme, to_vec4
|
|
26
|
+
from .tree import TreeStyle, draw_scope
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DebuggerConfig:
|
|
31
|
+
"""What the debugger inspects and how it draws it.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
target : object | None
|
|
36
|
+
The object whose scopes are shown first, usually the widget being
|
|
37
|
+
debugged.
|
|
38
|
+
title : str
|
|
39
|
+
Header text above the tree.
|
|
40
|
+
theme : Theme
|
|
41
|
+
Palette.
|
|
42
|
+
private : bool
|
|
43
|
+
Show ``_name`` attributes.
|
|
44
|
+
properties : bool
|
|
45
|
+
Show the ``properties`` scope and expand properties of nested values.
|
|
46
|
+
class_attrs : bool
|
|
47
|
+
Show the ``class`` scope.
|
|
48
|
+
editable : bool
|
|
49
|
+
Draw inline editors for writable leaves.
|
|
50
|
+
show_frame : bool
|
|
51
|
+
Show ``locals`` and ``globals`` from the captured call frame.
|
|
52
|
+
show_runtime : bool
|
|
53
|
+
Show the live ``imgui`` scope.
|
|
54
|
+
max_depth : int
|
|
55
|
+
Deepest level the tree expands.
|
|
56
|
+
max_items : int
|
|
57
|
+
Rows rendered per container.
|
|
58
|
+
value_col : float
|
|
59
|
+
Pixel column values are aligned at; ``0`` packs them after the name.
|
|
60
|
+
show_toolbar : bool
|
|
61
|
+
Draw the filter box and toggles above the tree.
|
|
62
|
+
window_title : str
|
|
63
|
+
OS window title in one-shot mode.
|
|
64
|
+
window_size : tuple[int, int]
|
|
65
|
+
OS window size in one-shot mode.
|
|
66
|
+
resizable : bool
|
|
67
|
+
Whether the one-shot window can be resized.
|
|
68
|
+
ini_path : str | None
|
|
69
|
+
Where hello_imgui saves the window layout.
|
|
70
|
+
assets_folder : str | None
|
|
71
|
+
Folder providing the icon font; unset never overrides a host app's.
|
|
72
|
+
|
|
73
|
+
Examples
|
|
74
|
+
--------
|
|
75
|
+
>>> from imgui_debugger import DebuggerConfig, Theme
|
|
76
|
+
>>> cfg = DebuggerConfig(title="Voltage tab", theme=Theme.light(), private=True)
|
|
77
|
+
>>> cfg.title, cfg.private
|
|
78
|
+
('Voltage tab', True)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
target: Any = None
|
|
82
|
+
title: str = "Debugger"
|
|
83
|
+
theme: Theme = field(default_factory=Theme.dark)
|
|
84
|
+
private: bool = False
|
|
85
|
+
properties: bool = True
|
|
86
|
+
class_attrs: bool = True
|
|
87
|
+
editable: bool = True
|
|
88
|
+
show_frame: bool = True
|
|
89
|
+
show_runtime: bool = True
|
|
90
|
+
max_depth: int = 8
|
|
91
|
+
max_items: int = 200
|
|
92
|
+
value_col: float = 0.0
|
|
93
|
+
show_toolbar: bool = True
|
|
94
|
+
window_title: str = ""
|
|
95
|
+
window_size: Tuple[int, int] = (520, 780)
|
|
96
|
+
resizable: bool = True
|
|
97
|
+
ini_path: Optional[str] = None
|
|
98
|
+
assets_folder: Optional[str] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Debugger:
|
|
102
|
+
"""A live variable inspector drawn inside any imgui frame.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
config : DebuggerConfig | None
|
|
107
|
+
What to inspect and how. Defaults to an empty debugger you add
|
|
108
|
+
watches to.
|
|
109
|
+
frame_depth : int
|
|
110
|
+
Which stack frame to capture for the ``locals`` / ``globals`` scopes;
|
|
111
|
+
``1`` is the caller of the constructor.
|
|
112
|
+
|
|
113
|
+
Examples
|
|
114
|
+
--------
|
|
115
|
+
>>> from imgui_debugger import Debugger, DebuggerConfig
|
|
116
|
+
>>> class Widget:
|
|
117
|
+
... def __init__(self):
|
|
118
|
+
... self.open = True
|
|
119
|
+
>>> cfg = DebuggerConfig(target=Widget(), show_frame=False, show_runtime=False)
|
|
120
|
+
>>> dbg = Debugger(cfg)
|
|
121
|
+
>>> dbg.watch("counters", {"frames": 0})
|
|
122
|
+
>>> [s.name for s in dbg.scopes()][-1]
|
|
123
|
+
'counters'
|
|
124
|
+
>>> dbg.render() # doctest: +SKIP
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, config: Optional[DebuggerConfig] = None, frame_depth: int = 1):
|
|
128
|
+
self.config = config or DebuggerConfig()
|
|
129
|
+
self.visible = True
|
|
130
|
+
self.filter = ""
|
|
131
|
+
self._watches: List[Watch] = []
|
|
132
|
+
self._frame = None
|
|
133
|
+
self._force_open: Optional[bool] = None
|
|
134
|
+
self._focus_filter = False
|
|
135
|
+
if self.config.show_frame:
|
|
136
|
+
self.capture(frame_depth + 1)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def theme(self) -> Theme:
|
|
140
|
+
"""The active palette.
|
|
141
|
+
|
|
142
|
+
Examples
|
|
143
|
+
--------
|
|
144
|
+
>>> from imgui_debugger import Debugger
|
|
145
|
+
>>> Debugger().theme.frame_rounding
|
|
146
|
+
4.0
|
|
147
|
+
"""
|
|
148
|
+
return self.config.theme
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def target(self):
|
|
152
|
+
"""The object whose scopes are listed first.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
>>> from imgui_debugger import attach
|
|
157
|
+
>>> attach({"fs": 9.6}).target
|
|
158
|
+
{'fs': 9.6}
|
|
159
|
+
"""
|
|
160
|
+
return self.config.target
|
|
161
|
+
|
|
162
|
+
def set_target(self, target) -> None:
|
|
163
|
+
"""Point the debugger at another object.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
target : object
|
|
168
|
+
The new object to inspect.
|
|
169
|
+
|
|
170
|
+
Examples
|
|
171
|
+
--------
|
|
172
|
+
>>> from imgui_debugger import attach
|
|
173
|
+
>>> dbg = attach({"a": 1})
|
|
174
|
+
>>> dbg.set_target({"b": 2})
|
|
175
|
+
>>> dbg.target
|
|
176
|
+
{'b': 2}
|
|
177
|
+
"""
|
|
178
|
+
self.config.target = target
|
|
179
|
+
|
|
180
|
+
def capture(self, depth: int = 1) -> None:
|
|
181
|
+
"""Capture a stack frame for the ``locals`` / ``globals`` scopes.
|
|
182
|
+
|
|
183
|
+
Call it from inside the function you want to watch — a widget's draw
|
|
184
|
+
method, say — to follow that function's locals live.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
depth : int
|
|
189
|
+
``1`` is the caller of :meth:`capture`.
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> from imgui_debugger import Debugger
|
|
194
|
+
>>> dbg = Debugger()
|
|
195
|
+
>>> rows = 12
|
|
196
|
+
>>> dbg.capture()
|
|
197
|
+
>>> any(s.name == "locals" for s in dbg.scopes())
|
|
198
|
+
True
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
self._frame = sys._getframe(depth)
|
|
202
|
+
except ValueError:
|
|
203
|
+
self._frame = None
|
|
204
|
+
|
|
205
|
+
def watch(self, name: str, source, role: str = "name", start_open: bool = True) -> None:
|
|
206
|
+
"""Add an extra scope for a value or a callable re-read every frame.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
name : str
|
|
211
|
+
Scope header text; adding the same name twice replaces it.
|
|
212
|
+
source : object | callable
|
|
213
|
+
The value, or a zero-argument callable returning it.
|
|
214
|
+
role : str
|
|
215
|
+
Theme color role for its rows.
|
|
216
|
+
start_open : bool
|
|
217
|
+
Whether the header starts expanded.
|
|
218
|
+
|
|
219
|
+
Examples
|
|
220
|
+
--------
|
|
221
|
+
>>> from imgui_debugger import Debugger, DebuggerConfig
|
|
222
|
+
>>> dbg = Debugger(DebuggerConfig(show_frame=False, show_runtime=False))
|
|
223
|
+
>>> state = {"frames": 0}
|
|
224
|
+
>>> dbg.watch("state", state)
|
|
225
|
+
>>> dbg.watch("fps", lambda: {"now": 60.0}, role="runtime")
|
|
226
|
+
>>> [s.name for s in dbg.scopes()]
|
|
227
|
+
['state', 'fps']
|
|
228
|
+
"""
|
|
229
|
+
self.unwatch(name)
|
|
230
|
+
self._watches.append(Watch(name, source, role, start_open, self.config.private))
|
|
231
|
+
|
|
232
|
+
def unwatch(self, name: str) -> None:
|
|
233
|
+
"""Remove a watch added by :meth:`watch`.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
name : str
|
|
238
|
+
The watch's scope name.
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
>>> from imgui_debugger import Debugger, DebuggerConfig
|
|
243
|
+
>>> dbg = Debugger(DebuggerConfig(show_frame=False, show_runtime=False))
|
|
244
|
+
>>> dbg.watch("state", {"a": 1})
|
|
245
|
+
>>> dbg.unwatch("state")
|
|
246
|
+
>>> dbg.scopes()
|
|
247
|
+
[]
|
|
248
|
+
"""
|
|
249
|
+
self._watches = [w for w in self._watches if w.name != name]
|
|
250
|
+
|
|
251
|
+
def scopes(self) -> List[Scope]:
|
|
252
|
+
"""Every scope the tree draws this frame, in display order.
|
|
253
|
+
|
|
254
|
+
Examples
|
|
255
|
+
--------
|
|
256
|
+
>>> from imgui_debugger import attach
|
|
257
|
+
>>> dbg = attach({"fs": 9.6}, show_frame=False, show_runtime=False)
|
|
258
|
+
>>> [s.name for s in dbg.scopes()]
|
|
259
|
+
['instance', 'properties', 'class']
|
|
260
|
+
"""
|
|
261
|
+
cfg = self.config
|
|
262
|
+
out: List[Scope] = []
|
|
263
|
+
if cfg.target is not None:
|
|
264
|
+
out += object_scopes(cfg.target, cfg.private, cfg.properties, cfg.class_attrs)
|
|
265
|
+
out += [w.scope() for w in self._watches]
|
|
266
|
+
if cfg.show_frame and self._frame is not None:
|
|
267
|
+
out += frame_scopes(self._frame, cfg.private)
|
|
268
|
+
if cfg.show_runtime:
|
|
269
|
+
out.append(runtime_scope())
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
def style(self) -> TreeStyle:
|
|
273
|
+
"""The :class:`~imgui_debugger.tree.TreeStyle` for this frame.
|
|
274
|
+
|
|
275
|
+
Examples
|
|
276
|
+
--------
|
|
277
|
+
>>> from imgui_debugger import Debugger, DebuggerConfig
|
|
278
|
+
>>> Debugger(DebuggerConfig(value_col=200.0)).style().value_col
|
|
279
|
+
200.0
|
|
280
|
+
"""
|
|
281
|
+
cfg = self.config
|
|
282
|
+
return TreeStyle(
|
|
283
|
+
theme=cfg.theme,
|
|
284
|
+
filter=self.filter,
|
|
285
|
+
editable=cfg.editable,
|
|
286
|
+
private=cfg.private,
|
|
287
|
+
properties=cfg.properties,
|
|
288
|
+
max_depth=cfg.max_depth,
|
|
289
|
+
max_items=cfg.max_items,
|
|
290
|
+
value_col=cfg.value_col,
|
|
291
|
+
force_open=self._force_open,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def show(self) -> None:
|
|
295
|
+
"""Make :meth:`render_window` draw the window again.
|
|
296
|
+
|
|
297
|
+
Examples
|
|
298
|
+
--------
|
|
299
|
+
>>> from imgui_debugger import Debugger
|
|
300
|
+
>>> dbg = Debugger()
|
|
301
|
+
>>> dbg.hide(); dbg.show(); dbg.visible
|
|
302
|
+
True
|
|
303
|
+
"""
|
|
304
|
+
self.visible = True
|
|
305
|
+
|
|
306
|
+
def hide(self) -> None:
|
|
307
|
+
"""Stop :meth:`render_window` from drawing the window.
|
|
308
|
+
|
|
309
|
+
Examples
|
|
310
|
+
--------
|
|
311
|
+
>>> from imgui_debugger import Debugger
|
|
312
|
+
>>> dbg = Debugger()
|
|
313
|
+
>>> dbg.hide(); dbg.visible
|
|
314
|
+
False
|
|
315
|
+
"""
|
|
316
|
+
self.visible = False
|
|
317
|
+
|
|
318
|
+
def toggle(self) -> None:
|
|
319
|
+
"""Flip :attr:`visible`, for a menu item or a hotkey.
|
|
320
|
+
|
|
321
|
+
Examples
|
|
322
|
+
--------
|
|
323
|
+
>>> from imgui_debugger import Debugger
|
|
324
|
+
>>> dbg = Debugger()
|
|
325
|
+
>>> dbg.toggle(); dbg.visible
|
|
326
|
+
False
|
|
327
|
+
"""
|
|
328
|
+
self.visible = not self.visible
|
|
329
|
+
|
|
330
|
+
def expand_all(self) -> None:
|
|
331
|
+
"""Expand every node on the next frame.
|
|
332
|
+
|
|
333
|
+
Examples
|
|
334
|
+
--------
|
|
335
|
+
>>> from imgui_debugger import Debugger
|
|
336
|
+
>>> dbg = Debugger()
|
|
337
|
+
>>> dbg.expand_all()
|
|
338
|
+
"""
|
|
339
|
+
self._force_open = True
|
|
340
|
+
|
|
341
|
+
def collapse_all(self) -> None:
|
|
342
|
+
"""Collapse every node on the next frame.
|
|
343
|
+
|
|
344
|
+
Examples
|
|
345
|
+
--------
|
|
346
|
+
>>> from imgui_debugger import Debugger
|
|
347
|
+
>>> dbg = Debugger()
|
|
348
|
+
>>> dbg.collapse_all()
|
|
349
|
+
"""
|
|
350
|
+
self._force_open = False
|
|
351
|
+
|
|
352
|
+
def set_filter(self, text: str) -> None:
|
|
353
|
+
"""Set the filter text and drop the stale match cache.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
text : str
|
|
358
|
+
Case-insensitive filter over names and leaf values.
|
|
359
|
+
|
|
360
|
+
Examples
|
|
361
|
+
--------
|
|
362
|
+
>>> from imgui_debugger import Debugger
|
|
363
|
+
>>> dbg = Debugger()
|
|
364
|
+
>>> dbg.set_filter("fs")
|
|
365
|
+
>>> dbg.filter
|
|
366
|
+
'fs'
|
|
367
|
+
"""
|
|
368
|
+
self.filter = text
|
|
369
|
+
clear_cache()
|
|
370
|
+
|
|
371
|
+
def render(self) -> None:
|
|
372
|
+
"""Draw the toolbar and the whole tree at the current cursor.
|
|
373
|
+
|
|
374
|
+
Call it inside your own frame, e.g. from a dock space window or a
|
|
375
|
+
pipeline widget's config panel.
|
|
376
|
+
|
|
377
|
+
Examples
|
|
378
|
+
--------
|
|
379
|
+
>>> from imgui_debugger import attach
|
|
380
|
+
>>> dbg = attach(object())
|
|
381
|
+
>>> dbg.render() # doctest: +SKIP
|
|
382
|
+
"""
|
|
383
|
+
if self.config.show_toolbar:
|
|
384
|
+
self.draw_toolbar()
|
|
385
|
+
self.draw_tree()
|
|
386
|
+
self._force_open = None
|
|
387
|
+
|
|
388
|
+
def draw_toolbar(self) -> None:
|
|
389
|
+
"""Draw the title, filter box, expand/collapse and the display toggles.
|
|
390
|
+
|
|
391
|
+
Examples
|
|
392
|
+
--------
|
|
393
|
+
>>> from imgui_debugger import attach
|
|
394
|
+
>>> attach(object()).draw_toolbar() # doctest: +SKIP
|
|
395
|
+
"""
|
|
396
|
+
cfg = self.config
|
|
397
|
+
if cfg.title:
|
|
398
|
+
imgui.text_colored(to_vec4(cfg.theme.accent), cfg.title)
|
|
399
|
+
if cfg.target is not None and imgui.is_item_hovered():
|
|
400
|
+
imgui.set_tooltip(f"{type(cfg.target).__name__} at 0x{id(cfg.target):x}")
|
|
401
|
+
imgui.same_line()
|
|
402
|
+
imgui.text_disabled(f"{len(self.scopes())} scopes")
|
|
403
|
+
|
|
404
|
+
if self._focus_filter:
|
|
405
|
+
imgui.set_keyboard_focus_here()
|
|
406
|
+
self._focus_filter = False
|
|
407
|
+
imgui.set_next_item_width(-1)
|
|
408
|
+
changed, text = imgui.input_text_with_hint(
|
|
409
|
+
"##debug_filter", "filter by name or value...", self.filter
|
|
410
|
+
)
|
|
411
|
+
if changed:
|
|
412
|
+
self.set_filter(text)
|
|
413
|
+
|
|
414
|
+
if imgui.small_button("expand"):
|
|
415
|
+
self.expand_all()
|
|
416
|
+
imgui.same_line()
|
|
417
|
+
if imgui.small_button("collapse"):
|
|
418
|
+
self.collapse_all()
|
|
419
|
+
imgui.same_line()
|
|
420
|
+
_, cfg.private = imgui.checkbox("private", cfg.private)
|
|
421
|
+
imgui.same_line()
|
|
422
|
+
_, cfg.editable = imgui.checkbox("edit", cfg.editable)
|
|
423
|
+
imgui.separator()
|
|
424
|
+
|
|
425
|
+
def draw_tree(self) -> None:
|
|
426
|
+
"""Draw every scope in a scrollable child, without the toolbar.
|
|
427
|
+
|
|
428
|
+
Examples
|
|
429
|
+
--------
|
|
430
|
+
>>> from imgui_debugger import attach
|
|
431
|
+
>>> attach(object()).draw_tree() # doctest: +SKIP
|
|
432
|
+
"""
|
|
433
|
+
style = self.style()
|
|
434
|
+
with imgui_ctx.begin_child("##debug_tree"):
|
|
435
|
+
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(8, 4))
|
|
436
|
+
try:
|
|
437
|
+
for scope in self.scopes():
|
|
438
|
+
draw_scope(scope, style)
|
|
439
|
+
finally:
|
|
440
|
+
imgui.pop_style_var()
|
|
441
|
+
|
|
442
|
+
def render_window(self, flags: int = 0) -> bool:
|
|
443
|
+
"""Draw the debugger in its own imgui window, honoring :attr:`visible`.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
flags : int
|
|
448
|
+
Extra ``imgui.WindowFlags_`` bits.
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
bool
|
|
453
|
+
True when the window was drawn this frame.
|
|
454
|
+
|
|
455
|
+
Examples
|
|
456
|
+
--------
|
|
457
|
+
>>> from imgui_debugger import attach
|
|
458
|
+
>>> dbg = attach(object())
|
|
459
|
+
>>> dbg.render_window() # doctest: +SKIP
|
|
460
|
+
True
|
|
461
|
+
"""
|
|
462
|
+
if not self.visible:
|
|
463
|
+
return False
|
|
464
|
+
title = self.config.window_title or self.config.title or "Debugger"
|
|
465
|
+
expanded, self.visible = imgui.begin(f"{title}##imgui_debugger", True, flags)
|
|
466
|
+
if expanded:
|
|
467
|
+
self.render()
|
|
468
|
+
imgui.end()
|
|
469
|
+
return expanded
|
|
470
|
+
|
|
471
|
+
def focus_filter(self) -> None:
|
|
472
|
+
"""Put the keyboard cursor in the filter box on the next frame.
|
|
473
|
+
|
|
474
|
+
Examples
|
|
475
|
+
--------
|
|
476
|
+
>>> from imgui_debugger import Debugger
|
|
477
|
+
>>> dbg = Debugger()
|
|
478
|
+
>>> dbg.focus_filter()
|
|
479
|
+
"""
|
|
480
|
+
self._focus_filter = True
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def attach(target=None, config: Optional[DebuggerConfig] = None, **kwargs) -> Debugger:
|
|
484
|
+
"""Build a :class:`Debugger` for one object in a single call.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
target : object | None
|
|
489
|
+
The object to inspect.
|
|
490
|
+
config : DebuggerConfig | None
|
|
491
|
+
A prebuilt config; ``target`` and ``kwargs`` override its fields.
|
|
492
|
+
**kwargs
|
|
493
|
+
Any :class:`DebuggerConfig` field.
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
Debugger
|
|
498
|
+
Ready to render; keep it on your widget, not per frame.
|
|
499
|
+
|
|
500
|
+
Examples
|
|
501
|
+
--------
|
|
502
|
+
>>> from imgui_debugger import attach
|
|
503
|
+
>>> class Tab:
|
|
504
|
+
... def __init__(self):
|
|
505
|
+
... self.selected = 2
|
|
506
|
+
>>> dbg = attach(Tab(), title="Voltage tab", show_runtime=False)
|
|
507
|
+
>>> dbg.config.title
|
|
508
|
+
'Voltage tab'
|
|
509
|
+
>>> dbg.render_window() # doctest: +SKIP
|
|
510
|
+
"""
|
|
511
|
+
cfg = config or DebuggerConfig()
|
|
512
|
+
if target is not None:
|
|
513
|
+
cfg.target = target
|
|
514
|
+
for key, value in kwargs.items():
|
|
515
|
+
if not hasattr(cfg, key):
|
|
516
|
+
raise TypeError(f"DebuggerConfig has no field {key!r}")
|
|
517
|
+
setattr(cfg, key, value)
|
|
518
|
+
return Debugger(cfg, frame_depth=2)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def watch_all(target, names: Sequence[str], **kwargs) -> Debugger:
|
|
522
|
+
"""Attach to an object and add one watch per named attribute.
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
target : object
|
|
527
|
+
The object to inspect.
|
|
528
|
+
names : Sequence[str]
|
|
529
|
+
Attribute names to promote to top-level scopes.
|
|
530
|
+
**kwargs
|
|
531
|
+
Any :class:`DebuggerConfig` field.
|
|
532
|
+
|
|
533
|
+
Returns
|
|
534
|
+
-------
|
|
535
|
+
Debugger
|
|
536
|
+
With one extra scope per name, re-read every frame.
|
|
537
|
+
|
|
538
|
+
Examples
|
|
539
|
+
--------
|
|
540
|
+
>>> from imgui_debugger import watch_all
|
|
541
|
+
>>> class Viewer:
|
|
542
|
+
... def __init__(self):
|
|
543
|
+
... self.metadata = {"fs": 9.6}
|
|
544
|
+
... self.indices = {"t": 0}
|
|
545
|
+
>>> dbg = watch_all(Viewer(), ["metadata", "indices"],
|
|
546
|
+
... show_frame=False, show_runtime=False)
|
|
547
|
+
>>> [s.name for s in dbg.scopes()][-2:]
|
|
548
|
+
['metadata', 'indices']
|
|
549
|
+
"""
|
|
550
|
+
dbg = attach(target, **kwargs)
|
|
551
|
+
for name in names:
|
|
552
|
+
dbg.watch(name, functools.partial(getattr, target, name))
|
|
553
|
+
return dbg
|
imgui_debugger/edit.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Inline editors that write a leaf value back onto the object it came from.
|
|
2
|
+
|
|
3
|
+
Examples
|
|
4
|
+
--------
|
|
5
|
+
>>> from imgui_debugger.edit import can_edit
|
|
6
|
+
>>> can_edit(1.5), can_edit((0.2, 0.4, 0.6)), can_edit(object())
|
|
7
|
+
(True, True, False)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from imgui_bundle import imgui
|
|
13
|
+
|
|
14
|
+
from .theme import Theme, to_vec4
|
|
15
|
+
|
|
16
|
+
MAX_EDIT_STR = 512
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_color(value) -> bool:
|
|
20
|
+
"""Whether a value looks like an ``(r, g, b[, a])`` float tuple.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
value : object
|
|
25
|
+
The value to test.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> from imgui_debugger.edit import is_color
|
|
30
|
+
>>> is_color((0.1, 0.2, 0.3)), is_color((1, 2, 3, 4, 5)), is_color("red")
|
|
31
|
+
(True, False, False)
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(value, (tuple, list)) or len(value) not in (3, 4):
|
|
34
|
+
return False
|
|
35
|
+
return all(isinstance(v, float) and 0.0 <= v <= 1.0 for v in value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def can_edit(value) -> bool:
|
|
39
|
+
"""Whether :func:`edit_value` knows how to render an editor for a value.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
value : object
|
|
44
|
+
The value to test.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> from imgui_debugger.edit import can_edit
|
|
49
|
+
>>> can_edit(True), can_edit("path"), can_edit([1, 2])
|
|
50
|
+
(True, True, False)
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(value, bool) or isinstance(value, (int, float)):
|
|
53
|
+
return True
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
return len(value) <= MAX_EDIT_STR
|
|
56
|
+
return is_color(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def edit_value(ident: str, value, setter, theme: Theme = None, width: float = -1.0):
|
|
60
|
+
"""Draw an editor for one leaf and call ``setter`` when it changes.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
ident : str
|
|
65
|
+
Unique imgui id for the widget, usually the row's dotted path.
|
|
66
|
+
value : object
|
|
67
|
+
Current value; its type picks the editor.
|
|
68
|
+
setter : callable
|
|
69
|
+
``setter(new_value)``, called only on a real change.
|
|
70
|
+
theme : Theme | None
|
|
71
|
+
Palette for the editor frame.
|
|
72
|
+
width : float
|
|
73
|
+
Item width; ``-1`` fills the remaining line.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
bool
|
|
78
|
+
True when the value changed this frame.
|
|
79
|
+
|
|
80
|
+
Examples
|
|
81
|
+
--------
|
|
82
|
+
>>> from imgui_debugger.edit import edit_value
|
|
83
|
+
>>> box = {"fs": 9.6}
|
|
84
|
+
>>> edit_value("md.fs", box["fs"], box.__setitem__) # doctest: +SKIP
|
|
85
|
+
False
|
|
86
|
+
"""
|
|
87
|
+
theme = theme or Theme.dark()
|
|
88
|
+
imgui.push_style_color(imgui.Col_.frame_bg, to_vec4(theme.frame_bg))
|
|
89
|
+
imgui.push_style_var(imgui.StyleVar_.frame_rounding, theme.frame_rounding)
|
|
90
|
+
imgui.set_next_item_width(width)
|
|
91
|
+
try:
|
|
92
|
+
changed, new_value = _draw_editor(ident, value)
|
|
93
|
+
finally:
|
|
94
|
+
imgui.pop_style_var()
|
|
95
|
+
imgui.pop_style_color()
|
|
96
|
+
if changed:
|
|
97
|
+
try:
|
|
98
|
+
setter(new_value)
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
101
|
+
return changed
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _draw_editor(ident: str, value):
|
|
105
|
+
"""Draw the type-appropriate imgui input and return ``(changed, value)``.
|
|
106
|
+
|
|
107
|
+
Examples
|
|
108
|
+
--------
|
|
109
|
+
>>> from imgui_debugger.edit import _draw_editor
|
|
110
|
+
>>> _draw_editor("##n", 3) # doctest: +SKIP
|
|
111
|
+
(False, 3)
|
|
112
|
+
"""
|
|
113
|
+
label = f"##{ident}"
|
|
114
|
+
if isinstance(value, bool):
|
|
115
|
+
return imgui.checkbox(label, value)
|
|
116
|
+
if is_color(value):
|
|
117
|
+
rgba = list(value) + ([1.0] if len(value) == 3 else [])
|
|
118
|
+
changed, out = imgui.color_edit4(label, imgui.ImVec4(*rgba))
|
|
119
|
+
new = (out.x, out.y, out.z) if len(value) == 3 else (out.x, out.y, out.z, out.w)
|
|
120
|
+
return changed, type(value)(new)
|
|
121
|
+
if isinstance(value, int):
|
|
122
|
+
return imgui.input_int(label, value)
|
|
123
|
+
if isinstance(value, float):
|
|
124
|
+
return imgui.input_float(label, value, 0.0, 0.0, "%.6g")
|
|
125
|
+
return imgui.input_text(label, value)
|