refactor: config access is typed, not defensive#353
Conversation
There was a problem hiding this comment.
Pull request overview
This PR standardizes config/backend access across RAITAP by removing defensive getattr(..., default) patterns in favor of direct, typed attribute access on the structured AppConfig schema and ModelBackend ABC, so missing fields fail loudly rather than silently defaulting.
Changes:
- Reworked test config/backends to use schema-faithful builders (
make_app_configreturning structuredDictConfig; newmake_fake_backend) and updated many tests accordingly. - Swept production modules to read config/backend fields directly (e.g.,
config.transparency,backend.provides,config.data.forward_batch_size) and tightened adapter/backend typing (e.g.,backend: ModelBackend | None). - Added guardrails: an ADR documenting the policy and a CI “Quality Gate” step that rejects defensive
getattron config/backend insrc/**.
Reviewed changes
Copilot reviewed 60 out of 60 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/raitap/transparency/tests/test_factory.py | Ensures test configs include schema-defaulted data to support direct reads. |
| src/raitap/transparency/tests/test_e2e_integration.py | Adds schema-defaulted data in e2e config builders. |
| src/raitap/transparency/tests/test_assess_transparency_detection.py | Switches to schema ModelConfig to avoid missing-key crashes. |
| src/raitap/transparency/tests/e2e_case_matrix.py | Adds schema-defaulted data in matrix configs. |
| src/raitap/transparency/phase.py | Replaces defensive config reads with direct schema access. |
| src/raitap/transparency/explainers/tests/test_base_explainer.py | Updates explainer tests to pass a typed backend via make_fake_backend(). |
| src/raitap/transparency/explainers/base_explainer.py | Types backend as `ModelBackend |
| src/raitap/transparency/contracts.py | Tightens adapter Protocol typing for backend compatibility checks. |
| src/raitap/tests/test_capability_migration_guard.py | Uses make_fake_backend to exercise capability gating without ad-hoc stubs. |
| src/raitap/tests/test_adapter_capability_gate.py | Uses make_fake_backend to simplify capability-gate tests. |
| src/raitap/testing/tests/test_factories.py | Adds tests asserting make_app_config structness/defaults + typed fake backend behavior. |
| src/raitap/testing/factories.py | Replaces SimpleNamespace config fake with structured DictConfig; introduces make_fake_backend. |
| src/raitap/testing/init.py | Exposes make_fake_backend in testing public API. |
| src/raitap/task_families/tests/test_classification.py | Adds regression coverage for binary metrics configs and num_classes access. |
| src/raitap/task_families/tests/test_classification_forward_mask.py | Switches forward-pass test config to make_app_config. |
| src/raitap/task_families/classification.py | Adjusts num_classes access to avoid config-missing crashes across metrics variants. |
| src/raitap/robustness/tests/test_marabou_parity.py | Updates backend stubs to implement ModelBackend contract. |
| src/raitap/robustness/tests/test_e2e_robustness_matrix.py | Aligns robustness matrix configs with new direct config reads (explicit attrs). |
| src/raitap/robustness/tests/test_e2e_real_data.py | Updates robustness-only pipeline test to use real AppConfig where needed. |
| src/raitap/robustness/tests/test_e2e_marabou_acas_xu.py | Updates ONNX backend stubs to satisfy ModelBackend ABC. |
| src/raitap/robustness/phase.py | Replaces defensive robustness config reads with direct schema access. |
| src/raitap/robustness/factory.py | Uses direct config.experiment_name reads. |
| src/raitap/robustness/contracts.py | Tightens assessor Protocol typing to accept `ModelBackend |
| src/raitap/robustness/assessors/tests/test_torchattacks_assessor.py | Uses make_fake_backend instead of bespoke stubs. |
| src/raitap/robustness/assessors/tests/test_marabou_assessor.py | Updates backend stubs to implement ModelBackend; uses make_fake_backend where appropriate. |
| src/raitap/robustness/assessors/tests/test_foolbox_assessor.py | Uses make_fake_backend instead of bespoke stubs. |
| src/raitap/robustness/assessors/tests/test_auto_lirpa_assessor.py | Updates backend stubs to implement ModelBackend contract. |
| src/raitap/robustness/assessors/marabou_assessor.py | Types backend compat hook as `ModelBackend |
| src/raitap/robustness/assessors/base_assessor.py | Uses typed backend and direct _prepare_inputs call. |
| src/raitap/robustness/assessors/auto_lirpa_assessor.py | Types backend compat hook as `ModelBackend |
| src/raitap/reporting/tests/test_builder.py | Uses make_app_config and attaches Hydra runtime node outside struct schema. |
| src/raitap/reporting/pdf_reporter.py | Removes defensive reporting filename/model reads; asserts reporting is enabled. |
| src/raitap/reporting/hydra_callback.py | Uses direct config.reporting access. |
| src/raitap/reporting/html_reporter.py | Removes defensive metadata reads; asserts reporting is enabled. |
| src/raitap/reporting/factory.py | Uses direct config.reporting access to gate reporting. |
| src/raitap/reporting/builder.py | Uses direct schema reads for report metadata + manifest naming. |
| src/raitap/pipeline/ui.py | Uses direct schema reads for summary config blocks. |
| src/raitap/pipeline/tests/test_forward_output_kind.py | Replaces bespoke backends/configs with make_fake_backend/make_app_config. |
| src/raitap/pipeline/phases/tests/test_forward_pass.py | Adjusts config stub to match new batch-size resolution path. |
| src/raitap/pipeline/phases/input_metadata.py | Uses direct config.data.input_metadata. |
| src/raitap/pipeline/phases/forward_pass.py | Resolves batch size from schema data.forward_batch_size; reads backend.provides directly. |
| src/raitap/pipeline/orchestrator.py | Uses direct schema reads for seed/tracking/reporting gating. |
| src/raitap/models/torch_backend.py | Narrows device type to non-optional for TorchBackend. |
| src/raitap/models/tests/test_tree_backend.py | Uses make_fake_backend instead of bespoke stubs. |
| src/raitap/models/tests/test_models.py | Switches to real schema dataclasses for config shaping. |
| src/raitap/models/tests/test_model_class.py | Switches model config test stub to make_app_config. |
| src/raitap/models/model.py | Uses direct schema reads for model source/hardware/tokenizer/task_kind/data metadata. |
| src/raitap/models/base_backend.py | Adds device attribute on ModelBackend ABC to support direct reads. |
| src/raitap/metrics/factory.py | Uses direct schema reads for metrics gating. |
| src/raitap/data/tests/test_text_metadata.py | Adjusts config fakes to provide required schema-like data fields. |
| src/raitap/data/tests/test_metadata.py | Adjusts config fakes to provide required schema-like data fields. |
| src/raitap/data/tests/test_detection_ragged.py | Ensures test config provides model.class_names for direct reads. |
| src/raitap/data/tests/test_data.py | Introduces helper to build minimal real AppConfig for data tests. |
| src/raitap/data/tests/test_data_class.py | Ensures test config provides model.class_names for direct reads. |
| src/raitap/data/metadata.py | Tightens signature to config: AppConfig and reads config.data fields directly. |
| src/raitap/data/data.py | Removes defensive cfg.model access and unconditionally uses schema cfg.model. |
| src/raitap/configs/adapter_factory.py | Uses direct schema reads for model/data preprocessing resolution. |
| src/raitap/_adapters.py | Types backend compat to `ModelBackend |
| docs/contributor/adr.md | Documents the “typed, not defensive” config access policy. |
| .github/workflows/code-quality.yaml | Adds CI guard banning defensive getattr on config/backend in src/** (excluding tests). |
…lymorphic num_classes getattr (refs #352)
Automated-review triageAll CI green. The bot comments are false positives — no code change needed:
Unused
|
Summary
Closes #352. Kills the defensive
getattr(config|backend, "field", default)epidemic in favor of direct typed attribute access, so a missingAppConfig/ModelBackendfield fails loud instead of returning a silent default. The type system becomes load-bearing.Foundation
make_app_confignow returns a real struct-mode OmegaConfDictConfig(was a bareSimpleNamespace) — the same shape Hydra hands prod, so direct reads are genuinely exercised.make_fake_backend+ModelBackend.deviceon the ABC.Sweep (~56 prod sites across 9 modules) —
getattr(config, "x", d)→config.x; deficientSimpleNamespacetest fakes promoted to the real builders. Judgment sites handled: deleted the deadconfig.runread;backend.devicevia the ABC; excluded dynamic reflection (getattr(obj, variable)); preservedNone-fallbacks on genuinely-optional fields (config.reporting,config.model.source, polymorphicnum_classes).Typing reconcile —
check_backend_compat(backend: ModelBackend | None)across base + both adapter Protocols + overrides + test doubles; the no-backend gate staysbackend.provides if backend is not None else frozenset()(an absent backend provides nothing).Guardrails — a CI step in the Quality Gate bans the defensive pattern in
src/**; a new ADR record documents the policy.Review
Built via subagent-driven-development: 11 tasks (foundation sequential, 7 parallel module sweeps, guard + ADR), plus a cross-task typing reconcile. Final whole-branch correctness review (found + fixed one Critical:
num_classescrash on binary/multilabel metrics configs, now regression-tested) and a whole-branch refactor review (clean, zero Refactor-Critical).uv run pyright src/raitap= 0 real errors; strict Sphinx build green.Checklist
raitap.runand the config schema are unchanged; internalcheck_backend_compat/make_app_configsignatures changed (pre-1.0, no shims).Optional
🤖 Generated with Claude Code