feat(linter): Develop custom AST linter for ImGui scopes

This commit is contained in:
2026-05-12 19:02:30 -04:00
parent 5398b4eef0
commit c359961a0a
8 changed files with 281 additions and 250 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ This file tracks all major tracks for the project. Each track has its own detail
*Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)* *Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)*
*Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.* *Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.*
13. [ ] **Track: GUI Refactor & Stabilization** 13. [~] **Track: GUI Refactor & Stabilization**
*Link: [./tracks/gui_refactor_stabilization_20260512/](./tracks/gui_refactor_stabilization_20260512/)* *Link: [./tracks/gui_refactor_stabilization_20260512/](./tracks/gui_refactor_stabilization_20260512/)*
*Goal: Refactor gui_2.py to fix regressions and enforce better imgui scoping patterns.* *Goal: Refactor gui_2.py to fix regressions and enforce better imgui scoping patterns.*
@@ -1,7 +1,7 @@
# Implementation Plan: GUI Refactor & Stabilization # Implementation Plan: GUI Refactor & Stabilization
## Phase 1: Linting & Verification Foundations ## Phase 1: Linting & Verification Foundations
- [ ] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`. - [~] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`.
- [ ] Task: Write tests for the new AST linter to ensure it catches unclosed scopes and indentation mismatches. - [ ] Task: Write tests for the new AST linter to ensure it catches unclosed scopes and indentation mismatches.
- [ ] Task: Expand API hooks in `src/api_hooks.py` to better simulate complex UI interactions (e.g., specific widget clicks, drag operations). - [ ] Task: Expand API hooks in `src/api_hooks.py` to better simulate complex UI interactions (e.g., specific widget clicks, drag operations).
- [ ] Task: Write tests for the new API hooks. - [ ] Task: Write tests for the new API hooks.
+3 -3
View File
@@ -35,7 +35,7 @@ separate_external_tools = false
"Project Settings" = true "Project Settings" = true
"Files & Media" = true "Files & Media" = true
"AI Settings" = true "AI Settings" = true
"MMA Dashboard" = false "MMA Dashboard" = true
"Task DAG" = false "Task DAG" = false
"Usage Analytics" = true "Usage Analytics" = true
"Tier 1" = false "Tier 1" = false
@@ -50,8 +50,8 @@ separate_external_tools = false
"Operations Hub" = true "Operations Hub" = true
Message = false Message = false
Response = false Response = false
"Tool Calls" = false "Tool Calls" = true
Theme = true Theme = false
"Log Management" = false "Log Management" = false
Diagnostics = false Diagnostics = false
"External Tools" = false "External Tools" = false
+75 -51
View File
@@ -12,7 +12,7 @@ ViewportPos=43,95
ViewportId=0x78C57832 ViewportId=0x78C57832
Size=897,649 Size=897,649
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000010,0
[Window][Files] [Window][Files]
ViewportPos=3125,170 ViewportPos=3125,170
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
Pos=0,17 Pos=0,17
Size=1680,730 Size=1680,730
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000010,0
[Window][Provider] [Window][Provider]
ViewportPos=43,95 ViewportPos=43,95
@@ -41,7 +41,7 @@ ViewportId=0x78C57832
Pos=0,651 Pos=0,651
Size=897,468 Size=897,468
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000010,0
[Window][Message] [Window][Message]
Pos=475,163 Pos=475,163
@@ -54,8 +54,8 @@ Size=1442,1129
Collapsed=0 Collapsed=0
[Window][Tool Calls] [Window][Tool Calls]
Pos=488,28 Pos=2063,28
Size=1777,1750 Size=1777,2132
Collapsed=0 Collapsed=0
DockId=0x0000000E,0 DockId=0x0000000E,0
@@ -75,9 +75,9 @@ DockId=0xAFC85805,2
[Window][Theme] [Window][Theme]
Pos=0,28 Pos=0,28
Size=32,1172 Size=1652,2132
Collapsed=0 Collapsed=0
DockId=0x00000005,3 DockId=0x00000010,3
[Window][Text Viewer - Entry #7] [Window][Text Viewer - Entry #7]
Pos=379,324 Pos=379,324
@@ -85,9 +85,10 @@ Size=900,700
Collapsed=0 Collapsed=0
[Window][Diagnostics] [Window][Diagnostics]
Pos=739,582 Pos=332,28
Size=1211,713 Size=886,1172
Collapsed=0 Collapsed=0
DockId=0x00000006,3
[Window][Context Hub] [Window][Context Hub]
Pos=0,975 Pos=0,975
@@ -102,28 +103,28 @@ Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][Discussion Hub] [Window][Discussion Hub]
Pos=34,28 Pos=794,28
Size=1184,1172 Size=886,1172
Collapsed=0
DockId=0x00000006,0
[Window][Operations Hub]
Pos=0,28
Size=32,1172
Collapsed=0
DockId=0x00000005,2
[Window][Files & Media]
Pos=34,28
Size=1184,1172
Collapsed=0 Collapsed=0
DockId=0x00000006,1 DockId=0x00000006,1
[Window][Operations Hub]
Pos=0,28
Size=792,1172
Collapsed=0
DockId=0x00000010,2
[Window][Files & Media]
Pos=794,28
Size=886,1172
Collapsed=0
DockId=0x00000006,0
[Window][AI Settings] [Window][AI Settings]
Pos=0,28 Pos=0,28
Size=32,1172 Size=792,1172
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000010,1
[Window][Approve Tool Execution] [Window][Approve Tool Execution]
Pos=3,524 Pos=3,524
@@ -131,14 +132,14 @@ Size=416,325
Collapsed=0 Collapsed=0
[Window][MMA Dashboard] [Window][MMA Dashboard]
Pos=1012,28 Pos=794,28
Size=668,1172 Size=886,1172
Collapsed=0 Collapsed=0
DockId=0x00000006,2 DockId=0x00000006,2
[Window][Log Management] [Window][Log Management]
Pos=949,32 Pos=1203,28
Size=1368,1622 Size=1040,1710
Collapsed=0 Collapsed=0
DockId=0x00000006,2 DockId=0x00000006,2
@@ -329,8 +330,8 @@ Size=967,499
Collapsed=0 Collapsed=0
[Window][Usage Analytics] [Window][Usage Analytics]
Pos=2267,28 Pos=3380,28
Size=460,1750 Size=460,2132
Collapsed=0 Collapsed=0
DockId=0x00000001,0 DockId=0x00000001,0
@@ -380,9 +381,10 @@ Size=1366,1032
Collapsed=0 Collapsed=0
[Window][Shader Editor] [Window][Shader Editor]
Pos=457,710 Pos=0,1976
Size=573,280 Size=1652,184
Collapsed=0 Collapsed=0
DockId=0x00000011,0
[Window][Text Viewer - list_directory] [Window][Text Viewer - list_directory]
Pos=1376,796 Pos=1376,796
@@ -407,9 +409,9 @@ DockId=0x00000006,1
[Window][Project Settings] [Window][Project Settings]
Pos=0,28 Pos=0,28
Size=32,1172 Size=792,1172
Collapsed=0 Collapsed=0
DockId=0x00000005,1 DockId=0x00000010,0
[Window][Undo/Redo History] [Window][Undo/Redo History]
Pos=1220,28 Pos=1220,28
@@ -627,23 +629,45 @@ Column 2 Width=69
Column 3 Width=91 Column 3 Width=91
Column 4 Width=70 Column 4 Width=70
[Table][0xB17BCA58,3]
RefScale=20
Column 0 Weight=1.0000
Column 1 Width=80
Column 2 Width=150
[Table][0x7804123E,5]
RefScale=20
Column 0 Width=20
Column 1 Weight=1.0000
Column 2 Width=27
Column 3 Width=36
Column 4 Width=45
[Table][0x09B0112E,3]
RefScale=20
Column 0 Weight=1.0000
Column 1 Width=80
Column 2 Width=150
[Docking][Data] [Docking][Data]
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=1199,1681 CentralNode=1 Selected=0x8CA2375C DockNode ID=0x00000005 Parent=0x00000007 SizeRef=792,1681 Split=Y Selected=0x3F1379AF
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=1379,1681 Selected=0x6F2B5B04 DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311 DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x00000006 Parent=0x00000007 SizeRef=886,1681 Selected=0x6F2B5B04
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498 DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6 DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498
DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6 DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9
DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6
DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6
DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD
;;;<<<Layout_655921752_Default>>>;;; ;;;<<<Layout_655921752_Default>>>;;;
;;;<<<HelloImGui_Misc>>>;;; ;;;<<<HelloImGui_Misc>>>;;;
+1
View File
@@ -22,6 +22,7 @@ dependencies = [
"pyopengl>=3.1.10", "pyopengl>=3.1.10",
"chromadb>=1.5.8", "chromadb>=1.5.8",
"sentence-transformers>=5.4.1", "sentence-transformers>=5.4.1",
"python-defer"
] ]
[dependency-groups] [dependency-groups]
+116
View File
@@ -0,0 +1,116 @@
import ast
import sys
class ImGuiScopeLinter:
def __init__(self):
self.pairs = {
'begin': 'end',
'begin_child': 'end_child',
'begin_group': 'end_group',
'begin_combo': 'end_combo',
'begin_main_menu_bar': 'end_main_menu_bar',
'begin_menu_bar': 'end_menu_bar',
'begin_menu': 'end_menu',
'begin_tooltip': 'end_tooltip',
'begin_popup': 'end_popup',
'begin_popup_modal': 'end_popup',
'begin_popup_context_item': 'end_popup',
'begin_popup_context_window': 'end_popup',
'begin_popup_context_void': 'end_popup',
'begin_table': 'end_table',
'begin_tab_bar': 'end_tab_bar',
'begin_tab_item': 'end_tab_item',
'push_style_var': 'pop_style_var',
'push_style_color': 'pop_style_color',
'push_font': 'pop_font',
'push_id': 'pop_id',
'push_item_width': 'pop_item_width',
'push_text_wrap_pos': 'pop_text_wrap_pos',
'push_clip_rect': 'pop_clip_rect',
'tree_node': 'tree_pop',
'tree_push': 'tree_pop',
'indent': 'unindent',
}
self.starts = set(self.pairs.keys())
self.ends = set(self.pairs.values())
def check_source(self, source_code: str) -> list[str]:
try:
tree = ast.parse(source_code)
except SyntaxError as e:
return [f"Syntax error: {e}"]
visitor = ImGuiVisitor(self.pairs)
visitor.visit(tree)
return visitor.errors
class ImGuiVisitor(ast.NodeVisitor):
def __init__(self, pairs):
self.pairs = pairs
self.stack = []
self.errors = []
self.starts = set(pairs.keys())
self.ends = set(pairs.values())
def visit_FunctionDef(self, node):
saved_stack = self.stack
self.stack = []
self.generic_visit(node)
while self.stack:
start_type, lineno = self.stack.pop()
self.errors.append(f"Function '{node.name}': Unclosed scope '{start_type}' started at line {lineno}")
self.stack = saved_stack
def visit_AsyncFunctionDef(self, node):
self.visit_FunctionDef(node)
def visit_Call(self, node):
func_name = self._get_func_name(node.func)
if func_name:
parts = func_name.split('.')
if len(parts) >= 2 and parts[0] in ('imgui', 'ed', 'imgui_node_editor'):
method = parts[-1]
if method in self.starts:
self.stack.append((method, node.lineno))
elif method in self.ends:
if not self.stack:
self.errors.append(f"Extra '{method}' at line {node.lineno} (no matching start)")
else:
start_type, start_lineno = self.stack[-1]
expected_end = self.pairs.get(start_type)
if expected_end == method:
self.stack.pop()
else:
self.stack.pop()
self.errors.append(f"Mismatched scope: '{method}' at line {node.lineno} does not match '{start_type}' at line {start_lineno}")
self.generic_visit(node)
def _get_func_name(self, node):
if isinstance(node, ast.Name):
return node.id
elif isinstance(node, ast.Attribute):
value = self._get_func_name(node.value)
if value:
return f"{value}.{node.attr}"
return None
def main():
if len(sys.argv) < 2:
print("Usage: python check_imgui_scopes.py <file1> <file2> ...")
return
linter = ImGuiScopeLinter()
for path in sys.argv[1:]:
try:
with open(path, "r", encoding="utf-8") as f:
source = f.read()
errors = linter.check_source(source)
if errors:
print(f"Errors in {path}:")
for err in errors:
print(f" {err}")
else:
print(f"{path}: OK")
except Exception as e:
print(f"Error reading {path}: {e}")
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.
+84 -194
View File
@@ -1,202 +1,92 @@
from __future__ import annotations
from unittest.mock import patch, MagicMock
import pytest import pytest
from scripts.check_imgui_scopes import ImGuiScopeLinter
def test_valid_scopes():
linter = ImGuiScopeLinter()
source = """
def valid_func():
imgui.begin("Window")
imgui.push_id("sub")
imgui.text("Hello")
imgui.pop_id()
imgui.end()
"""
errors = linter.check_source(source)
assert not errors
class TestImGuiScope: def test_unclosed_scope():
def test_enter_calls_begin_with_args(self) -> None: linter = ImGuiScopeLinter()
from src.imgui_scopes import ImGuiScope source = """
mock_begin = MagicMock(return_value=True) def unclosed_func():
mock_end = MagicMock() imgui.begin("Window")
scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val") imgui.text("Hello")
with scope: # Missing imgui.end()
pass """
mock_begin.assert_called_once_with("arg1", kwarg="val") errors = linter.check_source(source)
assert len(errors) == 1
assert "Unclosed scope 'begin'" in errors[0]
def test_exit_calls_end_when_entered(self) -> None: def test_extra_pop():
from src.imgui_scopes import ImGuiScope linter = ImGuiScopeLinter()
mock_begin = MagicMock(return_value=True) source = """
mock_end = MagicMock() def extra_pop_func():
scope = ImGuiScope(mock_begin, mock_end) imgui.begin("Window")
with scope: imgui.end()
pass imgui.end() # Extra
mock_end.assert_called_once() """
errors = linter.check_source(source)
assert len(errors) == 1
assert "Extra 'end'" in errors[0]
def test_exit_does_not_call_end_when_not_entered(self) -> None: def test_mismatched_scopes():
from src.imgui_scopes import ImGuiScope linter = ImGuiScopeLinter()
mock_begin = MagicMock(return_value=False) source = """
mock_end = MagicMock() def mismatched_func():
scope = ImGuiScope(mock_begin, mock_end) imgui.begin("Window")
with scope: imgui.push_id("id")
pass imgui.end() # Should be pop_id
mock_end.assert_not_called() imgui.pop_id() # Should be end
"""
errors = linter.check_source(source)
# mismatch pops 'push_id', then pop_id() mismatches with 'begin'
assert len(errors) == 2
assert "Mismatched scope" in errors[0]
assert "end" in errors[0]
assert "push_id" in errors[0]
assert "Mismatched scope" in errors[1]
assert "pop_id" in errors[1]
assert "begin" in errors[1]
def test_enter_returns_opened_value(self) -> None: def test_nested_functions():
from src.imgui_scopes import ImGuiScope linter = ImGuiScopeLinter()
mock_begin = MagicMock(return_value=True) source = """
mock_end = MagicMock() def outer():
scope = ImGuiScope(mock_begin, mock_end) imgui.begin("Outer")
result = scope.__enter__() def inner():
assert result is True imgui.push_id("Inner")
# Missing pop_id
imgui.end()
"""
errors = linter.check_source(source)
assert len(errors) == 1
assert "Function 'inner': Unclosed scope 'push_id'" in errors[0]
def test_enter_handles_tuple_return(self) -> None: def test_node_editor_scopes():
from src.imgui_scopes import ImGuiScope linter = ImGuiScopeLinter()
mock_begin = MagicMock(return_value=(True, "extra")) source = """
mock_end = MagicMock() def ed_func():
scope = ImGuiScope(mock_begin, mock_end) ed.begin("NodeEditor")
with scope: ed.end()
pass """
mock_end.assert_called_once() errors = linter.check_source(source)
assert not errors
def test_popup_modal_end():
class TestImguiWindow: linter = ImGuiScopeLinter()
def test_imgui_window_returns_scope(self) -> None: source = """
from src.imgui_scopes import imgui_window, ImGuiScope def popup_func():
scope = imgui_window("Test") imgui.begin_popup_modal("Modal")
assert isinstance(scope, ImGuiScope) imgui.end_popup()
"""
def test_imgui_window_calls_begin_end(self) -> None: errors = linter.check_source(source)
from src.imgui_scopes import imgui_window assert not errors
with patch("src.imgui_scopes.imgui.begin", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end") as mock_end:
scope = imgui_window("Test")
with scope:
pass
mock_begin.assert_called_once_with("Test", True, 0)
mock_end.assert_called_once()
class TestImguiTable:
def test_imgui_table_returns_scope(self) -> None:
from src.imgui_scopes import imgui_table, ImGuiScope
scope = imgui_table("TestTable", 3)
assert isinstance(scope, ImGuiScope)
def test_imgui_table_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_table
with patch("src.imgui_scopes.imgui.begin_table", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_table") as mock_end:
scope = imgui_table("TestTable", 3)
with scope:
pass
mock_begin.assert_called_once_with("TestTable", 3, 0)
mock_end.assert_called_once()
class TestImguiMenuBar:
def test_imgui_menu_bar_returns_scope(self) -> None:
from src.imgui_scopes import imgui_menu_bar, ImGuiScope
scope = imgui_menu_bar()
assert isinstance(scope, ImGuiScope)
def test_imgui_menu_bar_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_menu_bar
with patch("src.imgui_scopes.imgui.begin_menu_bar", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_menu_bar") as mock_end:
scope = imgui_menu_bar()
with scope:
pass
mock_begin.assert_called_once_with()
mock_end.assert_called_once()
class TestImguiMenu:
def test_imgui_menu_returns_scope(self) -> None:
from src.imgui_scopes import imgui_menu, ImGuiScope
scope = imgui_menu("File")
assert isinstance(scope, ImGuiScope)
def test_imgui_menu_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_menu
with patch("src.imgui_scopes.imgui.begin_menu", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_menu") as mock_end:
scope = imgui_menu("File")
with scope:
pass
mock_begin.assert_called_once_with("File")
mock_end.assert_called_once()
class TestImguiChild:
def test_imgui_child_returns_scope(self) -> None:
from src.imgui_scopes import imgui_child, ImGuiScope
scope = imgui_child("child_id", 100, 200)
assert isinstance(scope, ImGuiScope)
def test_imgui_child_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_child
with patch("src.imgui_scopes.imgui.begin_child", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_child") as mock_end:
scope = imgui_child("child_id", 100, 200)
with scope:
pass
mock_begin.assert_called_once_with("child_id", 100, 200, 0)
mock_end.assert_called_once()
class TestImguiGroup:
def test_imgui_group_returns_scope(self) -> None:
from src.imgui_scopes import imgui_group, ImGuiScope
scope = imgui_group()
assert isinstance(scope, ImGuiScope)
def test_imgui_group_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_group
with patch("src.imgui_scopes.imgui.begin_group", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_group") as mock_end:
scope = imgui_group()
with scope:
pass
mock_begin.assert_called_once_with()
mock_end.assert_called_once()
class TestImguiPopup:
def test_imgui_popup_returns_scope(self) -> None:
from src.imgui_scopes import imgui_popup, ImGuiScope
scope = imgui_popup("popup_id")
assert isinstance(scope, ImGuiScope)
def test_imgui_popup_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_popup
with patch("src.imgui_scopes.imgui.begin_popup", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_popup") as mock_end:
scope = imgui_popup("popup_id")
with scope:
pass
mock_begin.assert_called_once_with("popup_id")
mock_end.assert_called_once()
class TestImguiTooltip:
def test_imgui_tooltip_returns_scope(self) -> None:
from src.imgui_scopes import imgui_tooltip, ImGuiScope
scope = imgui_tooltip()
assert isinstance(scope, ImGuiScope)
def test_imgui_tooltip_calls_begin_end(self) -> None:
from src.imgui_scopes import imgui_tooltip
with patch("src.imgui_scopes.imgui.begin_tooltip", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui.end_tooltip") as mock_end:
scope = imgui_tooltip()
with scope:
pass
mock_begin.assert_called_once_with()
mock_end.assert_called_once()
class TestNodeEditorScope:
def test_node_editor_scope_returns_scope(self) -> None:
from src.imgui_scopes import node_editor_scope, ImGuiScope
scope = node_editor_scope("TestNode")
assert isinstance(scope, ImGuiScope)
def test_node_editor_scope_calls_begin_end(self) -> None:
from src.imgui_scopes import node_editor_scope
with patch("src.imgui_scopes.imgui_node_editor.begin", return_value=True) as mock_begin:
with patch("src.imgui_scopes.imgui_node_editor.end") as mock_end:
scope = node_editor_scope("TestNode")
with scope:
pass
mock_begin.assert_called_once_with("TestNode")
mock_end.assert_called_once()