Private
Public Access
0
0

refactor(gui_2,app_controller): remove hasattr defensive checks + fix _do_generate type

Phase 3 follow-up: gui_2.py hasattr removal
Before: 23 hasattr(f, ...) defensive checks in src/gui_2.py
After:  0 (self.files / self.context_files are GUARANTEED List[FileItem])
Delta:  -23 sites

Phase 4: _do_generate return type
Before: def _do_generate(self) -> tuple[str, Path, list[Metadata], str, str]: at src/app_controller.py:4014
After:  def _do_generate(self) -> tuple[str, Path, list[FileItem], str, str]:
Delta:  -1 wrong type annotation (file_items comes from aggregate.run() which returns List[FileItem])

Combined: 18 hasattr(f, 'path') checks in gui_2.py + 5 hasattr(f, ...) checks
on other FileItem fields (view_mode/custom_slices/ast_mask/ast_signatures/
ast_definitions/auto_aggregate/to_dict) + 1 _do_generate return type fix.

All removed defensive checks are redundant because:
1. self.files and self.context_files are populated via the
   isinstance + FileItem.from_dict() pattern (gui_2.py:869-873 + 980-985
   for restore; app_controller.py:1996-2005 for project init)
2. FileItem has explicit fields for path, view_mode, custom_slices,
   ast_mask, ast_signatures, ast_definitions, auto_aggregate, to_dict

Verification:
- audit_weak_types --strict: OK (107 <= 112 baseline)
- py_check_syntax src/gui_2.py: OK
- py_check_syntax src/app_controller.py: OK
- 95 tests pass (type_aliases, openai_schemas, rag_engine, file_item,
  rag_chunk, main_thread_purity, app_controller_result,
  context_composition_decoupled)
This commit is contained in:
2026-06-26 04:49:55 -04:00
parent 0635f15ceb
commit cfd881e719
2 changed files with 45 additions and 45 deletions
+1 -1
View File
@@ -4011,7 +4011,7 @@ class AppController:
return result
self.submit_io(worker)
def _do_generate(self) -> tuple[str, Path, list[Metadata], str, str]:
def _do_generate(self) -> tuple[str, Path, list[FileItem], str, str]:
"""
Returns (full_md, output_path, file_items, stable_md, discussion_text).
[C: src/gui_2.py:App._show_menus, tests/test_context_composition_decoupled.py:test_do_generate_uses_context_files, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy]
+44 -44
View File
@@ -368,12 +368,12 @@ class App:
if not name: return
preset_files = []
for f in self.context_files:
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
p = f.path
vm = f.view_mode
slc = copy.deepcopy(f.custom_slices)
msk = copy.deepcopy(f.ast_mask)
sig = f.ast_signatures
dfn = f.ast_definitions
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots))
self.controller.save_context_preset(preset)
@@ -839,8 +839,8 @@ class App:
max_tokens = self.max_tokens,
auto_add_history = self.ui_auto_add_history,
disc_entries = copy.deepcopy(self.disc_entries),
files = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.files],
context_files = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_files],
files = [f.to_dict() for f in self.files],
context_files = [f.to_dict() for f in self.context_files],
screenshots = list(self.screenshots)
)
@@ -977,8 +977,8 @@ class App:
self.context_files = []
for f in preset.files:
fi = models.FileItem(path=f.path, view_mode=f.view_mode)
fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
fi.custom_slices = copy.deepcopy(f.custom_slices)
fi.ast_mask = copy.deepcopy(f.ast_mask)
fi.ast_signatures = getattr(f, 'ast_signatures', False)
fi.ast_definitions = getattr(f, 'ast_definitions', False)
self.context_files.append(fi)
@@ -994,13 +994,13 @@ class App:
@property
def ui_file_paths(self) -> list[str]:
return [f.path if hasattr(f, 'path') else str(f) for f in self.files]
return [f.path for f in self.files]
@ui_file_paths.setter
def ui_file_paths(self, paths: list[str]) -> None:
sys.stderr.write(f"[DEBUG] Setting ui_file_paths to: {paths}\n")
sys.stderr.flush()
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
old_files = {f.path: f for f in self.files}
new_files = []
now = time.time()
for p in paths:
@@ -1312,7 +1312,7 @@ class App:
missing_keys = []
for f in self.context_files:
f_path = f.path if hasattr(f, "path") else str(f)
f_path = f.path
mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0
cache_key = f"{f_path}_{mtime}"
if cache_key not in self._file_stats_cache: missing_keys.append((f_path, cache_key))
@@ -3666,7 +3666,7 @@ def render_files_and_media(app: App) -> None:
if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
with imscope.group():
to_remove_idx = -1
app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower())
app.files.sort(key=lambda f: f.path.lower())
file_indices = {id(f): idx for idx, f in enumerate(app.files)}
grouped = aggregate.group_files_by_dir(app.files)
if imgui.begin_table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
@@ -3719,12 +3719,12 @@ def render_files_and_media(app: App) -> None:
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
from src import models
for p in paths:
if p not in [f.path if hasattr(f, "path") else f for f in app.files]: app.files.append(models.FileItem(path=p))
if p not in [f.path for f in app.files]: app.files.append(models.FileItem(path=p))
imgui.same_line()
if imgui.button("Add Directory"):
r = hide_tk_root(); dirpath = filedialog.askdirectory(); r.destroy()
if dirpath:
existing = {f.path if hasattr(f, "path") else str(f) for f in app.files}
existing = {f.path for f in app.files}
for root, _dirs, files in os.walk(dirpath):
for fname in files:
full = os.path.join(root, fname)
@@ -3770,12 +3770,12 @@ def render_context_batch_actions(app: App, total_lines: int, total_ast: int) ->
for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]:
if imgui.button(f"{mode.capitalize()}##batch"):
for f in app.context_files:
f_path = f.path if hasattr(f, "path") else str(f)
f_path = f.path
if f_path in app.ui_selected_context_files: f.view_mode = mode
imgui.same_line()
if imgui.button("Sel All##selall"):
for f in app.context_files:
f_path = f.path if hasattr(f, "path") else str(f)
f_path = f.path
app.ui_selected_context_files.add(f_path)
imgui.same_line()
if imgui.button("Unsel All##unselall"): app.ui_selected_context_files.clear()
@@ -3783,9 +3783,9 @@ def render_context_batch_actions(app: App, total_lines: int, total_ast: int) ->
if imgui.button("Add Files##add_btn"): imgui.open_popup("Select Context Files")
imgui.same_line()
if imgui.button("Add All##addall"):
context_paths = {f.path if hasattr(f, "path") else str(f) for f in app.context_files}
context_paths = {f.path for f in app.context_files}
for f in app.files:
f_path = f.path if hasattr(f, "path") else str(f)
f_path = f.path
if f_path not in context_paths:
f_copy = copy.deepcopy(f)
app.context_files.append(f_copy)
@@ -3794,7 +3794,7 @@ def render_context_batch_actions(app: App, total_lines: int, total_ast: int) ->
if imgui.button("Del##batch"):
new_files = []
for f in app.context_files:
f_path = f.path if hasattr(f, "path") else str(f)
f_path = f.path
if f_path not in app.ui_selected_context_files: new_files.append(f)
app.context_files = new_files
app.ui_selected_context_files.clear()
@@ -3837,7 +3837,7 @@ def render_add_context_files_modal(app: App) -> None:
# Create a temporary selection set if not initialized
if not hasattr(app, '_ui_picker_selected'): app._ui_picker_selected = set()
for f in app.files:
fpath = f.path if hasattr(f, 'path') else str(f)
fpath = f.path
# Skip if already in context
if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files):
continue
@@ -4364,12 +4364,12 @@ def render_context_presets(app: App) -> None:
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
p = f.path
vm = f.view_mode
slc = copy.deepcopy(f.custom_slices)
msk = copy.deepcopy(f.ast_mask)
sig = f.ast_signatures
dfn = f.ast_definitions
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=active, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
@@ -4390,7 +4390,7 @@ def render_context_presets(app: App) -> None:
missing = []
root = app.controller.active_project_root
for f in app.context_files:
path = f.path if hasattr(f, "path") else str(f)
path = f.path
if not os.path.isabs(path): full_path = os.path.join(root, path)
else: full_path = path
if not os.path.exists(full_path): missing.append(path)
@@ -4404,12 +4404,12 @@ def render_context_presets(app: App) -> None:
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
p = f.path
vm = f.view_mode
slc = copy.deepcopy(f.custom_slices)
msk = copy.deepcopy(f.ast_mask)
sig = f.ast_signatures
dfn = f.ast_definitions
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
@@ -4539,12 +4539,12 @@ def render_context_modals(app: App) -> None:
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
p = f.path
vm = f.view_mode
slc = copy.deepcopy(f.custom_slices)
msk = copy.deepcopy(f.ast_mask)
sig = f.ast_signatures
dfn = f.ast_definitions
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
@@ -4562,9 +4562,9 @@ def render_context_modals(app: App) -> None:
def _get_context_composition_state(app: App) -> tuple:
files_state = []
for f in app.context_files:
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
agg = f.auto_aggregate if hasattr(f, 'auto_aggregate') else False
p = f.path
vm = f.view_mode
agg = f.auto_aggregate
slc = tuple((s.get('start_line'), s.get('end_line'), s.get('tag'), s.get('comment')) for s in getattr(f, 'custom_slices', []))
mask = tuple(sorted(getattr(f, 'ast_mask', {}).items()))
files_state.append((p, vm, agg, slc, mask))