Private
Public Access
Merge branch 'tier2/result_migration_gui_2_20260619'
This commit is contained in:
@@ -222,6 +222,23 @@ PROGRAMMER_ERROR_EXCEPTIONS: frozenset[str] = frozenset({
|
||||
"NotImplementedError",
|
||||
})
|
||||
|
||||
# Lazy-loader method names: the canonical naming convention for proxy
|
||||
# classes that defer a heavy import until first attribute access or call
|
||||
# (e.g. _LazyModule._resolve, _load, _get, _try_load). The audit
|
||||
# recognizes these as the canonical context for the sentinel-fallback
|
||||
# pattern (Phase 12.1 result_migration_gui_2_20260619): when the import
|
||||
# or attribute access fails, the except body falls back to a documented
|
||||
# sentinel class instance with an `available: bool = False` flag (or
|
||||
# similar) so the UI can detect the stub and offer an alternative
|
||||
# path. This is the canonical graceful-degradation pattern per
|
||||
# error_handling.md:625-690 (Re-Raise Patterns).
|
||||
LAZY_LOADER_METHOD_NAMES: frozenset[str] = frozenset({
|
||||
"_resolve",
|
||||
"_load",
|
||||
"_get",
|
||||
"_try_load",
|
||||
})
|
||||
|
||||
# Categories that are considered violations
|
||||
VIOLATION_CATEGORIES: frozenset[str] = frozenset({
|
||||
"INTERNAL_SILENT_SWALLOW",
|
||||
@@ -743,6 +760,35 @@ class ExceptionVisitor(ast.NodeVisitor):
|
||||
f"Compliant: `try: ...; except ({', '.join(sorted(exc_set))}): return Result(data=..., errors=[...])` is the canonical Result-recovery pattern. The function-name-not-ending-in-`_result` is a smell (rename to `xxx_result`); the pattern itself is the data-oriented convention. (per result_migration_small_files_20260617 Phase 11.2)",
|
||||
)
|
||||
|
||||
# B. Lazy-loading sentinel fallback — Phase 12.1 (result_migration_gui_2_20260619)
|
||||
# Per error_handling.md:625-690 (Re-Raise Patterns) and the lazy-loading
|
||||
# pattern guidance, when a module is loaded lazily (e.g. numpy, tkinter
|
||||
# at first attribute access) and the import or attribute access fails,
|
||||
# falling back to a documented sentinel class instance with an
|
||||
# `available: bool = False` flag is the canonical graceful-degradation
|
||||
# pattern. The sentinel is NOT a silent swallow: the UI can detect the
|
||||
# stub via the `available` flag and offer an alternative code path
|
||||
# (e.g. ImGui file dialog when tkinter.filedialog is unavailable).
|
||||
# This is analogous to the nil-sentinel dataclass (Pattern 1 in
|
||||
# error_handling.md). The function-name heuristic (`_resolve`/`_load`/
|
||||
# `_get`/`_try_load`) is the standard lazy-loader naming convention.
|
||||
# The except body must NOT re-raise; the recovery is via assignment
|
||||
# to `self.<attr>` (directly or via a nested try/except).
|
||||
except_body_re_raises = any(
|
||||
isinstance(s, ast.Raise) and s.exc is None
|
||||
for s in ast.walk(ast.Module(body=except_body, type_ignores=[]))
|
||||
)
|
||||
if (
|
||||
self._current_func_name() in LAZY_LOADER_METHOD_NAMES
|
||||
and not except_body_re_raises
|
||||
and exc_set & {"AttributeError", "ImportError", "ModuleNotFoundError"}
|
||||
and self._has_self_attr_assign(except_body)
|
||||
):
|
||||
return (
|
||||
"INTERNAL_COMPLIANT",
|
||||
f"Compliant: lazy-loading sentinel fallback. `try: ...; except ({', '.join(sorted(exc_set))}): self.<attr> = <sentinel>()` in `{self._current_func_name()}` is the canonical graceful-degradation pattern. The sentinel class exposes an `available: bool = False` flag (or similar) so the UI can detect the stub and offer an alternative path. Per error_handling.md:625-690 and Phase 12.1 result_migration_gui_2_20260619.",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _has_string_return(self, stmts: list[ast.stmt]) -> bool:
|
||||
@@ -921,6 +967,37 @@ class ExceptionVisitor(ast.NodeVisitor):
|
||||
has_return_none_after = True
|
||||
return has_for_range_with_try and has_return_none_after
|
||||
|
||||
def _has_self_attr_assign(self, stmts: list[ast.stmt]) -> bool:
|
||||
"""True if any statement (recursively) assigns to a `self.<attr>` attribute.
|
||||
|
||||
Used by the lazy-loading sentinel fallback heuristic (Phase 12.1) to
|
||||
detect the canonical graceful-degradation pattern: the except body
|
||||
falls back to a sentinel class instance via `self._cached = _Stub()`
|
||||
either directly OR via a nested try/except (e.g., an outer try that
|
||||
catches AttributeError and a nested try that ultimately falls back
|
||||
to the stub). The recursive walk handles both cases:
|
||||
|
||||
- Direct: `try: getattr(...); except AttributeError: self._cached = _Stub()`
|
||||
- Nested: `try: getattr(...); except AttributeError: try: importlib...; except: self._cached = _Stub()`
|
||||
|
||||
Per the styleguide (error_handling.md:625-690), this is the canonical
|
||||
graceful-degradation pattern for lazy-loading modules that may not
|
||||
be present on every Python install. The sentinel's `available: bool = False`
|
||||
flag (or similar) lets the UI detect the stub and offer an alternative
|
||||
path (e.g., ImGui file dialog when tkinter.filedialog is unavailable).
|
||||
"""
|
||||
for s in stmts:
|
||||
for node in ast.walk(s):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if (
|
||||
isinstance(target, ast.Attribute)
|
||||
and isinstance(target.value, ast.Name)
|
||||
and target.value.id == "self"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_imgui_end_call(self, stmts: list[ast.stmt]) -> bool:
|
||||
"""True if any statement is a call to an imgui.end_* function."""
|
||||
for s in stmts:
|
||||
@@ -1028,6 +1105,21 @@ class ExceptionVisitor(ast.NodeVisitor):
|
||||
f"Compliant: `raise {exc_short}` inside `if <var> is None:` is the canonical validation/precondition-check pattern (per result_migration_review_pass_20260617).",
|
||||
)
|
||||
|
||||
# Heuristic added by result_migration_gui_2_20260619 (Phase 11):
|
||||
# Bare `raise AttributeError(...)` or `raise NameError(...)` in a dunder
|
||||
# method (__getattr__/__getattribute__/__setattr__/__delattr__) is the
|
||||
# canonical Python dunder-method programmer-error pattern. Per the
|
||||
# styleguide "Re-Raise Patterns" (error_handling.md lines 625-690), bare
|
||||
# raises are reserved for programmer errors / impossible states /
|
||||
# canonical dunder method behaviors. The Python data-model contract for
|
||||
# these dunders explicitly raises AttributeError when an attribute does
|
||||
# not exist or is not settable.
|
||||
if exc_short in {"AttributeError", "NameError"} and self._current_func_name() in {"__getattr__", "__getattribute__", "__setattr__", "__delattr__"}:
|
||||
return (
|
||||
"INTERNAL_PROGRAMMER_RAISE",
|
||||
f"Compliant: `raise {exc_short}` in `{self._current_func_name()}` is the canonical dunder-method programmer-error pattern (per styleguide 'Re-Raise Patterns' and Phase 11 result_migration_gui_2_20260619).",
|
||||
)
|
||||
|
||||
return (
|
||||
"INTERNAL_RETHROW",
|
||||
f"Review: `raise {exc_name}` in internal code. Confirm this is a programmer error (assertion) and not a runtime failure (which should be a Result).",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
with open('src/app_controller.py', 'rb') as f:
|
||||
data = f.read()
|
||||
needle = b' at_data = mma_sec.get'
|
||||
idx = data.find(needle)
|
||||
chunk = data[idx:idx+800]
|
||||
print(repr(chunk.decode('utf-8', errors='replace')))
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
sys.path.insert(0, 'scripts')
|
||||
from audit_exception_handling import audit_file
|
||||
from pathlib import Path
|
||||
|
||||
r = audit_file(Path('src/app_controller.py'))
|
||||
silent = [f for f in r.findings if f.category == 'INTERNAL_SILENT_SWALLOW']
|
||||
broad = [f for f in r.findings if f.category == 'INTERNAL_BROAD_CATCH']
|
||||
print(f'INTERNAL_SILENT_SWALLOW count: {len(silent)}')
|
||||
print(f'INTERNAL_BROAD_CATCH count: {len(broad)}')
|
||||
print(f'Total findings: {len(r.findings)}')
|
||||
for s in silent:
|
||||
print(f' L{s.line}: {s.snippet[:80].strip()}')
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import sys, json, subprocess
|
||||
result = subprocess.run(['uv', 'run', 'python', 'scripts/audit_exception_handling.py', '--json'],
|
||||
capture_output=True, text=True)
|
||||
data = json.loads(result.stdout)
|
||||
for f in data['files']:
|
||||
fn = f.get('filename', '')
|
||||
if fn.endswith('api_hooks.py') or fn.endswith('app_controller.py'):
|
||||
bfapi = [x for x in f.get('findings', []) if x.get('category') == 'BOUNDARY_FASTAPI']
|
||||
print(fn + ': ' + str(len(bfapi)) + ' BOUNDARY_FASTAPI sites')
|
||||
for x in bfapi[:5]:
|
||||
print(' L' + str(x['line']) + ': ' + x['snippet'][:60])
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import sys
|
||||
sys.path.insert(0, 'scripts')
|
||||
from audit_exception_handling import audit_file
|
||||
from pathlib import Path
|
||||
r = audit_file(Path('src/app_controller.py'))
|
||||
for f in r.findings:
|
||||
if f.line in (242, 256, 5064, 5093):
|
||||
print(f'L{f.line}: category={f.category}')
|
||||
print(f' snippet: {f.snippet[:120].strip()}')
|
||||
@@ -0,0 +1,7 @@
|
||||
with open('tests/test_audit_heuristics.py', 'r', encoding='utf-8') as f:
|
||||
src = f.read()
|
||||
lines = src.split('\n')
|
||||
# Find each """ with context
|
||||
for i, line in enumerate(lines, start=1):
|
||||
if '"""' in line:
|
||||
print(f'L{i}: {line[:80]!r}')
|
||||
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
from src.app_controller import AppController
|
||||
from src.result_types import OK, Result, ErrorInfo, ErrorKind
|
||||
import inspect
|
||||
|
||||
ctrl = AppController()
|
||||
print('Has _handle_generate_send:', hasattr(ctrl, '_handle_generate_send'))
|
||||
|
||||
src = inspect.getsource(ctrl._handle_generate_send)
|
||||
print('Has Result[None] annotation:', 'Result[None]' in src)
|
||||
print('Has return OK:', 'return OK' in src)
|
||||
print('Has event_queue.put:', 'event_queue.put' in src)
|
||||
print('Has ai_status sending:', "ai_status = \"sending...\"" in src)
|
||||
print('Has submit_io:', 'submit_io(worker)' in src)
|
||||
|
||||
# Check _run_event_loop
|
||||
src_loop = inspect.getsource(ctrl._run_event_loop)
|
||||
print('_run_event_loop has _process_event_queue:', '_process_event_queue()' in src_loop)
|
||||
print('_run_event_loop position of _process_event_queue:')
|
||||
for i, line in enumerate(src_loop.split('\n')):
|
||||
if '_process_event_queue' in line:
|
||||
print(f' Line {i}: {line!r}')
|
||||
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
import sys
|
||||
import json
|
||||
audit_path = sys.argv[1] if len(sys.argv) > 1 else 'C:/tmp/audit_after_l7208.json'
|
||||
with open(audit_path, 'rb') as f:
|
||||
raw = f.read()
|
||||
if raw.startswith(b'\xff\xfe'):
|
||||
raw = raw.decode('utf-16-le')[1:]
|
||||
else:
|
||||
raw = raw.decode('utf-8')
|
||||
data = json.loads(raw)
|
||||
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
|
||||
v_count = sum(1 for f in gui['findings'] if f['category'] == 'INTERNAL_BROAD_CATCH')
|
||||
print(f'V count: {v_count}')
|
||||
print('Remaining V sites:')
|
||||
for f in gui['findings']:
|
||||
if f['category'] == 'INTERNAL_BROAD_CATCH':
|
||||
ctx = f.get('context', '')[:80]
|
||||
line = f['line']
|
||||
print(f' L{line}: [{f["category"]}] {ctx}')
|
||||
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
with open('tests/artifacts/PHASE1_AUDIT.json', 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
gui2 = None
|
||||
for r in data['files']:
|
||||
if 'gui_2' in r['filename']:
|
||||
gui2 = r
|
||||
break
|
||||
cats = {}
|
||||
for f in gui2['findings']:
|
||||
cats.setdefault(f['category'], []).append((f['line'], f['context'], f['kind']))
|
||||
for cat in sorted(cats):
|
||||
print(f'\n{cat} ({len(cats[cat])}):')
|
||||
for line, ctx, kind in sorted(cats[cat]):
|
||||
print(f' L{line:>4} {kind:<10} {ctx}')
|
||||
@@ -0,0 +1,16 @@
|
||||
import json
|
||||
with open('C:/tmp/audit_pre.json', encoding='utf-16-le') as f:
|
||||
raw = f.read()
|
||||
# Strip BOM if present
|
||||
if raw.startswith('\ufeff'):
|
||||
raw = raw[1:]
|
||||
data = json.loads(raw)
|
||||
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
|
||||
print(f'Current V (INTERNAL_BROAD_CATCH) count: {sum(1 for f in gui["findings"] if f["category"] == "INTERNAL_BROAD_CATCH")}')
|
||||
print(f'Current total sites: {len(gui["findings"])}')
|
||||
print()
|
||||
print('All INTERNAL_BROAD_CATCH sites in gui_2.py:')
|
||||
for f in gui['findings']:
|
||||
if f['category'] == 'INTERNAL_BROAD_CATCH':
|
||||
ctx = f.get('context', '')[:120]
|
||||
print(f' L{f["line"]}: [{f["category"]}] {ctx}')
|
||||
@@ -0,0 +1,11 @@
|
||||
import json, subprocess
|
||||
r = subprocess.run(['uv', 'run', 'python', 'scripts/audit_exception_handling.py', '--src', 'src', '--json'], capture_output=True, text=True)
|
||||
data = json.loads(r.stdout)
|
||||
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
|
||||
for f in gui['findings']:
|
||||
if f['category'] == 'INTERNAL_BROAD_CATCH':
|
||||
print(f"L{f['line']}: [{f['category']}] {f.get('context', '')}")
|
||||
print()
|
||||
for f in gui['findings']:
|
||||
if f['category'] == 'INTERNAL_SILENT_SWALLOW':
|
||||
print(f"L{f['line']}: [{f['category']}] {f.get('context', '')}")
|
||||
Reference in New Issue
Block a user