[SECURITY] Centralized error management system - closes #60#81
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a centralized, application-wide error handling singleton with callback-based notifications and a small query API, along with unit tests to validate core behavior.
Changes:
- Introduces
ApplicationErrorHandlersingleton wrappingpt_errors.ErrorHandlerwith per-severity callbacks and suppression. - Adds module-level convenience functions (
get_handler,handle,on_critical/on_error/on_warning,configure_gui_alerts). - Adds
unittestcoverage for singleton behavior, callback registration, query API, and GUI callback wiring.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| app/pt_error_handler.py | Implements the centralized singleton error handler, callback routing, suppression, and query helpers. |
| app/test_error_handler.py | Adds unittest tests for singleton initialization, handling behavior, callbacks, suppression, and query APIs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Local CI Simulation ResultsRan all workflow steps locally against this branch prior to owner approval. Results: Code Quality & Testing
Unit Test Results
CI/CD Pipeline
|
|
All 7 Copilot review comments addressed. Inline replies posted directly on each thread. All threads resolved.
|
ApplicationErrorHandler singleton wraps pt_errors.ErrorHandler with per-severity notification callbacks, module suppression, and a global error bus. Module-level shortcuts (handle, on_critical, on_error, configure_gui_alerts) make adoption drop-in for existing modules. - 18 unit tests, all passing
…verage - _ensure_init uses double-checked locking (class-level _init_lock) to be thread-safe - handle_critical updates error_counts for CRITICAL so get_critical_errors is consistent - ErrorCategory/PowerTraderError unused imports removed - configure_gui_alerts uses 'is not None' instead of truthiness check for callbacks - Callback exceptions now logged with exc_info=True for full traceback - Tests: non-assertive tests replaced with deterministic assertions - Tests: suppress_module test patches handle_error to set module before _fire_callbacks - Tests: added warning callback, unregister_all, gui alerts error-level tests
396db34 to
337b55a
Compare
- Fix module docstring example: handler.handle_error() -> handler.handle() - handle(reraise=True): use sys.exc_info() to detect if error is the active exception; bare raise preserves traceback exactly when called from an except block, fallback to raise error otherwise - handle_critical: decrement original severity count before incrementing CRITICAL to prevent double-counting in error_counts/get_summary() - _suppressed_modules: protect add/discard/read with _cb_lock so concurrent suppress_module/unsuppress_module/_fire_callbacks cannot race
Manual Testing Guide — PR #81: Centralized Error ManagementPrerequisitesgit fetch fork && git checkout feat/60-centralized-error-management
cd app1. Run unit testspython -m pytest test_error_handler.py -vExpected: All 22 tests pass. 2. Smoke test — handle + callback firespython -c "
from pt_error_handler import get_handler, on_warning, ApplicationErrorHandler
from pt_errors import ErrorSeverity
ApplicationErrorHandler.reset_singleton()
fired = []
on_warning(lambda r: fired.append(r))
try:
raise ValueError('test warning')
except ValueError as e:
report = get_handler().handle(e)
print('severity:', report.severity)
print('callbacks fired:', len(fired))
assert len(fired) == 1
print('PASS')
ApplicationErrorHandler.reset_singleton()
"3. Verify reraise preserves traceback (key fix)python -c "
import traceback
from pt_error_handler import get_handler, ApplicationErrorHandler
ApplicationErrorHandler.reset_singleton()
def deep():
raise RuntimeError('original location')
try:
deep()
except RuntimeError as e:
try:
get_handler().handle(e, reraise=True)
except RuntimeError:
tb = traceback.format_exc()
print('traceback contains deep():', 'deep' in tb)
assert 'deep' in tb, 'original traceback lost!'
print('PASS')
ApplicationErrorHandler.reset_singleton()
"4. Verify handle_critical no double-count (key fix)python -c "
from pt_error_handler import get_handler, ApplicationErrorHandler
from pt_errors import ErrorSeverity
ApplicationErrorHandler.reset_singleton()
h = get_handler()
try:
raise ValueError('low severity error')
except ValueError as e:
h.handle_critical(e)
summary = h.get_summary()
total = sum(summary.get('error_counts', {}).values())
print('total error_counts:', total)
assert total == 1, f'double-counted! total={total}'
print('PASS')
ApplicationErrorHandler.reset_singleton()
"5. Verify suppression is thread-safepython -c "
import threading
from pt_error_handler import get_handler, ApplicationErrorHandler
ApplicationErrorHandler.reset_singleton()
h = get_handler()
errors = []
def worker():
for _ in range(50):
h.suppress_module('pt_test')
h.unsuppress_module('pt_test')
threads = [threading.Thread(target=worker) for _ in range(4)]
[t.start() for t in threads]
[t.join() for t in threads]
print('no race condition errors:', len(errors) == 0)
ApplicationErrorHandler.reset_singleton()
"Rollbackgit checkout main -- app/pt_error_handler.py app/test_error_handler.py |
|
/gemini review |
sjackson0109
left a comment
There was a problem hiding this comment.
An outstanding contribution towards effective error handling.
|
Ready to merge - subject to the open/non-resolved reviewer (copilot) comments. |
- pt_errors.ErrorHandler.handle_error: new severity_override parameter applied BEFORE logging so escalated errors are recorded at the final severity from the start, not at the original severity followed by a post-hoc mutation. - pt_errors.ErrorHandler.error_counts: changed to nested Dict[category, Dict[severity, int]] so callers can ask "how many TRADING_ERROR/HIGH events" without scanning error_reports. Added ErrorHandler.category_total() helper for flat totals. - get_error_summary: now derives the flat 'categories' / 'severities' views from the nested counts so a single structure drives every reporting surface; also exposes 'by_category_severity' for callers that want the nested breakdown. - ApplicationErrorHandler.handle_critical: simplified to call handle_error(..., severity_override=CRITICAL); removes the manual error_counts arithmetic and the post-handle severity mutation. - ApplicationErrorHandler suppression: normalize both stored entries and ErrorReport.module by stripping a trailing '.py' so callers can pass either 'pt_trader' or 'pt_trader.py' and either form matches the filename that ErrorHandler captures from the traceback. - test_error_handler: replace the bare monkeypatch in test_suppress_module_blocks_callbacks with patch.object + addCleanup so the original method is always restored. Added test_suppress_module_accepts_filename_and_module_name.
|
@sjackson0109 round-3 done — addressed in ad8bbfd:
24 error-handler tests pass locally (incl. new |
|
Quick heads up - please hold this one. While auditing my own PRs I noticed I changed the shape of ErrorHandler.error_counts from a flat dict to a nested one, which would silently break anyone reading that attribute directly. Reverting the shape and adding the nested view as a new attribute. Will ping back when pushed. |
…ety) - pt_error_handler.py:2 — strip stray '+' from module title so generated docs render 'PowerTraderAI Centralized Error Management System'. - clear_history() now also clears error_counts_by_severity so get_summary().severities / by_category_severity start from zero after a reset instead of compounding across clears. - ApplicationErrorHandler now holds a dedicated _state_lock around every call that mutates or reads the underlying ErrorHandler state (handle, handle_critical, get_summary, get_recent_errors, get_critical_errors, clear_history) so the thread-safety promise in the class docstring actually holds under concurrent access. Callbacks still fire outside the lock to avoid holding it across user code.
comment only, easily rectified Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Simon Jackson <admin@jacksonfamily.me>
|
Once the last copilot review is answered+resolved, then this PR can merge. |
…orrect counter thread-safety comment - get_recent_errors() now returns [] for limit <= 0 instead of returning a wrong subset via negative-index slicing; added regression test. - Corrected the error_counts comment in ErrorHandler: the two counters stay consistent within a single _update_error_counts call, but ErrorHandler is not itself thread-safe (the ApplicationErrorHandler wrapper serialises access via _state_lock).
Copilot review follow-up (commit 901ee97)
Black + flake8 clean, 24 unit tests pass (was 23). |
…ename Doc said 'not the filename' then 'either spelling works'; code normalizes both, so the docstring now states either form is accepted. Also drops a stray em dash.
|
Another great piece of work @Ibrahim-3d Got some more feature tickets on there way.. i did a little brainstorming :) |
Summary
Creates
pt_error_handler.py— an application-wide singleton error handler built on top of the existingpt_errors.ErrorHandler. Provides a global error bus where any module can report exceptions, and UI/notification layers can subscribe to severity levels.Changes
app/pt_error_handler.py—ApplicationErrorHandlersingleton,handle(),on_critical(),on_error(),configure_gui_alerts()helpersapp/test_error_handler.py— 18 testsKey features
get_handler()instanceon_critical(cb),on_error(cb),on_warning(cb)— register GUI alerts, email notifications, etc.suppress_module("pt_trader")silences callbacks from known noisy modulesget_recent_errors(),get_critical_errors(),get_summary()Usage
Test plan
Closes #60