From 47dc91e2cb161ce6b513e191b60030fedcde4a43 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:59:01 +0400 Subject: [PATCH 1/6] Refuse a routing write when the routing-state Dict can't be opened _routing_store returns None when Modal finds no routing-state Dict. record_routing_plan and apply_routing_plan ask it to create the Dict, so None should never come back, but if it did they failed with AttributeError ('NoneType' object has no attribute 'get'). They now raise RoutingConflictError, saying nothing was written. complete_routing_plan already refused a missing store through its check that a matching plan is pending; its condition now says so, with the same error. Either way the command exits non-zero, as before. Pyright no longer reports the store as possibly None: 5 baseline entries removed, 124 to 119. Co-Authored-By: Claude Opus 5.5 --- docs/generated/rules.md | 2 +- .../modal/utils/update_version_registry.py | 13 ++++++- .../tests/test_update_version_registry.py | 39 +++++++++++++++++++ rules/baselines/pyright-executor.txt | 5 --- rules/registry.toml | 2 +- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/generated/rules.md b/docs/generated/rules.md index 52a3fa432..c82259fed 100644 --- a/docs/generated/rules.md +++ b/docs/generated/rules.md @@ -68,7 +68,7 @@ Every rule that governs `policyengine-sim-api` is listed here with how it is enf - `sim.ci.multiline-run-is-script`: 13 remaining in `rules/baselines/ci-multiline-run.txt` - `sim.microdata.weight-stripping-confined`: 1 remaining in `rules/baselines/sim-microdata-weight-stripping.txt` - `sim.quality.pyright-apis-integ`: 4 remaining in `rules/baselines/pyright-apis-integ.txt` -- `sim.quality.pyright-executor`: 124 remaining in `rules/baselines/pyright-executor.txt` +- `sim.quality.pyright-executor`: 119 remaining in `rules/baselines/pyright-executor.txt` - `sim.quality.pyright-gateway`: 0 remaining in `rules/baselines/pyright-gateway.txt` ### Exceptions and review dates diff --git a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py index 6ff03db4e..e6c722c8d 100644 --- a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py +++ b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py @@ -439,6 +439,13 @@ def build_restored_routing_state( ROUTING_PLAN_SCHEMA = "policyengine-simulation.routing-plan/v1" +# Raised by a routing write when _routing_store, asked to create the Dict, +# still returns None. +UNOPENED_ROUTING_STORE = ( + "the routing state Modal Dict could not be opened; nothing was written" +) + + class RoutingConflictError(RuntimeError): """The live routing state is not the one a plan expects.""" @@ -617,6 +624,8 @@ def record_routing_plan(*, environment: str, plan: dict) -> None: _require_serialized_routing_workflow() validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=True) + if store is None: + raise RoutingConflictError(UNOPENED_ROUTING_STORE) _require_finished_publication(store) if ( routing_state_digest(store.get(ROUTING_STATE_ACTIVE_KEY)) @@ -632,7 +641,7 @@ def complete_routing_plan(*, environment: str, plan: dict) -> None: _require_serialized_routing_workflow() validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=False) - if _unfinished_publication(store) != plan: + if store is None or _unfinished_publication(store) != plan: msg = "no matching unfinished routing publication to complete" raise RoutingConflictError(msg) if ( @@ -649,6 +658,8 @@ def apply_routing_plan(*, environment: str, plan: dict) -> None: _require_serialized_routing_workflow() validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=True) + if store is None: + raise RoutingConflictError(UNOPENED_ROUTING_STORE) _require_finished_publication(store, allowed_plans=[plan]) current = store.get(ROUTING_STATE_ACTIVE_KEY) if routing_state_digest(current) != plan["before_sha256"]: diff --git a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py index a008fb5b1..76ff396e1 100644 --- a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py +++ b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py @@ -1336,6 +1336,45 @@ def pop(self, key): assert store.get("active") == plan["after"] +class _UnopenableDict: + """A Modal Dict lookup that finds nothing, even when asked to create the Dict.""" + + @staticmethod + def from_name(name: str, **lookup_options: object): + missing_dict = f"{lookup_options['environment_name']}/{name}" + raise KeyError(missing_dict) + + +def test_routing_writes_refuse_a_routing_store_that_cannot_be_opened( + monkeypatch, serialized_workflow +): + """Recording or applying a plan stops with a conflict, not an AttributeError.""" + monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=_UnopenableDict)) + original = _publish(monkeypatch, None, "6.0.0", "1.1.0", "2.1.0") + plan = _plan(monkeypatch, original, ("6.1.0", "1.1.0", "2.2.0")) + + for operation in (registry.record_routing_plan, registry.apply_routing_plan): + with pytest.raises( + registry.RoutingConflictError, + match="the routing state Modal Dict could not be opened", + ): + operation(environment="staging", plan=plan) + + +def test_completing_a_plan_refuses_a_missing_routing_store( + monkeypatch, serialized_workflow +): + monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=_UnopenableDict)) + original = _publish(monkeypatch, None, "6.0.0", "1.1.0", "2.1.0") + plan = _plan(monkeypatch, original, ("6.1.0", "1.1.0", "2.2.0")) + + with pytest.raises( + registry.RoutingConflictError, + match="no matching unfinished routing publication to complete", + ): + registry.complete_routing_plan(environment="staging", plan=plan) + + def test_runner_loss_cannot_be_adopted_by_the_next_release(monkeypatch, patched_modal): """Failed A cannot become successful B's starting state or rollback target.""" original = _publish(monkeypatch, None, "6.0.0", "1.1.0", "2.1.0") diff --git a/rules/baselines/pyright-executor.txt b/rules/baselines/pyright-executor.txt index 181b6e689..544e0ce53 100644 --- a/rules/baselines/pyright-executor.txt +++ b/rules/baselines/pyright-executor.txt @@ -68,11 +68,6 @@ projects/policyengine-simulation-executor/src/modal/utils/update_version_registr projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalSubscript :: Object of type "None" is not subscriptable projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportReturnType :: Type "dict[str, int | str | RouteMaps | dict[str, BundleManifestMetadata]]" is not assignable to return type "RoutingState" projects/policyengine-simulation-executor/src/modal/utils/validate_v2_worker_release.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/baseline_artifacts.py :: reportAttributeAccessIssue :: Cannot access attribute "resolve_entity_variables" for class "TaxBenefitModelVersion" diff --git a/rules/registry.toml b/rules/registry.toml index 618222fb7..fa633d645 100644 --- a/rules/registry.toml +++ b/rules/registry.toml @@ -1139,7 +1139,7 @@ job = "pyright" invocation = "python -m scripts.rules pyright" [rule.baseline] file = "rules/baselines/pyright-executor.txt" -count = 124 +count = 119 [rule.proof] bad_fixture = "rules/fixtures/sim.quality.pyright-executor/bad" good_fixture = "rules/fixtures/sim.quality.pyright-executor/good" From b861d5f89ad4a9136782fb88f2016f71df0ca9be Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:00:00 +0400 Subject: [PATCH 2/6] Refuse to run a budget-window batch outside a Modal function call The batch keys its child jobs and stored state by the Modal call id, and its docstring says it must run inside a Modal call. Outside one, modal.current_function_call_id() returns None, and the batch went on until building its state failed with a Pydantic ValidationError on batch_job_id. It now raises RuntimeError before doing anything, as the Stage 12 coordinator in v2_app does. Inside a Modal call nothing changes. This is one of the errors Pyright reports once it resolves the real modal library (str | None passed as str), so it's fixed before that configuration change. Co-Authored-By: Claude Opus 5.5 --- .../src/modal/budget_window_batch.py | 3 +++ .../tests/test_budget_window_batch.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py b/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py index efe7f8e91..90ec90208 100644 --- a/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py +++ b/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py @@ -32,6 +32,9 @@ def run_budget_window_batch_impl( if selection is not None: params = {**params, "spm": selection} batch_job_id = modal.current_function_call_id() + if batch_job_id is None: + msg = "run_budget_window_batch must run inside a Modal function call" + raise RuntimeError(msg) runtime.set_context(batch_job_id=batch_job_id) with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_CONTEXT)): context = build_batch_context( diff --git a/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py b/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py index d74e3f69c..72ede3566 100644 --- a/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py +++ b/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py @@ -255,6 +255,19 @@ def test_run_budget_window_batch_impl_completes_and_respects_max_parallel( assert state.result.totals.budgetaryImpact == 51 +def test_run_budget_window_batch_impl_refuses_to_run_outside_a_modal_call( + mock_batch_modal, observability_runtime, monkeypatch +): + """Without a Modal call id there is no batch job id to key the batch by.""" + _, payload = _build_parent_payload() + monkeypatch.setattr(batch_module.modal, "current_function_call_id", lambda: None) + + with pytest.raises(RuntimeError, match="must run inside a Modal function call"): + run_budget_window_batch_impl(payload, runtime=observability_runtime) + + assert mock_batch_modal["call_registry"] == {} + + def test_run_budget_window_batch_impl_marks_failure( mock_batch_modal, observability_runtime ): From cde85eedc9366d026310413ec575803f7a892b09 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:08:49 +0400 Subject: [PATCH 3/6] Make Pyright resolve `import modal` to the modal library Without extraPaths, Pyright adds the executor's src directory as an import root, so `import modal` in the Modal app modules under src/modal resolved to that directory, and every modal attribute was unknown. At runtime src is never an import root: the package is installed from src, and the app modules import each other as src.modal. from the project directory, which Pyright still resolves. `extraPaths = []` in the executor's [tool.pyright] turns the src root off. Only Pyright reads it; nothing about how Modal loads the apps changes. That removes 42 errors and exposes 4 the shadowing hid. The previous two commits fixed 3 of them (the routing store and the batch call id). This one fixes the fourth: the segmented national child spawn opens its span with scopes.span, so Pyright no longer reports the method as missing a return. The precompute app already casts its six Modal functions to RemoteFunction (#879), so passing them to PrecomputeFunctions reports nothing. Its comment said Pyright doesn't resolve `modal` there; it now says why the casts are still needed: each modal.Function's `remote` and `spawn` take that function's own parameters, which Pyright doesn't accept for RemoteFunction's `*args: Any`. The executor's baseline drops from 119 entries to 77. Co-Authored-By: Claude Opus 5.5 --- docs/generated/rules.md | 2 +- .../pyproject.toml | 6 +++ .../src/modal/precompute_app.py | 7 ++-- .../src/modal/segmented_national.py | 4 +- rules/baselines/pyright-executor.txt | 42 ------------------- rules/registry.toml | 2 +- 6 files changed, 15 insertions(+), 48 deletions(-) diff --git a/docs/generated/rules.md b/docs/generated/rules.md index c82259fed..f394f4683 100644 --- a/docs/generated/rules.md +++ b/docs/generated/rules.md @@ -68,7 +68,7 @@ Every rule that governs `policyengine-sim-api` is listed here with how it is enf - `sim.ci.multiline-run-is-script`: 13 remaining in `rules/baselines/ci-multiline-run.txt` - `sim.microdata.weight-stripping-confined`: 1 remaining in `rules/baselines/sim-microdata-weight-stripping.txt` - `sim.quality.pyright-apis-integ`: 4 remaining in `rules/baselines/pyright-apis-integ.txt` -- `sim.quality.pyright-executor`: 119 remaining in `rules/baselines/pyright-executor.txt` +- `sim.quality.pyright-executor`: 77 remaining in `rules/baselines/pyright-executor.txt` - `sim.quality.pyright-gateway`: 0 remaining in `rules/baselines/pyright-gateway.txt` ### Exceptions and review dates diff --git a/projects/policyengine-simulation-executor/pyproject.toml b/projects/policyengine-simulation-executor/pyproject.toml index c635c4228..71e5d1cad 100644 --- a/projects/policyengine-simulation-executor/pyproject.toml +++ b/projects/policyengine-simulation-executor/pyproject.toml @@ -97,3 +97,9 @@ markers = [ [tool.pyright] # The pr.yml pyright job checks these paths: python -m scripts.rules pyright. include = ["src"] +# Without extraPaths, Pyright adds src as an import root, and `import modal` +# in the Modal app modules under src/modal resolves to that directory instead +# of the installed modal library. At runtime src is never an import root: the +# package is installed from src, and the app modules import each other as +# src.modal. from the project directory. +extraPaths = [] diff --git a/projects/policyengine-simulation-executor/src/modal/precompute_app.py b/projects/policyengine-simulation-executor/src/modal/precompute_app.py index 3153b2056..8b9c1c54c 100644 --- a/projects/policyengine-simulation-executor/src/modal/precompute_app.py +++ b/projects/policyengine-simulation-executor/src/modal/precompute_app.py @@ -197,9 +197,10 @@ def main(*, force: bool = False) -> None: run_precompute( resolve_bucket_name(), force=force, - # Pyright doesn't resolve `modal` here to the Modal package (it reports - # `modal.App` as unknown), so it types each decorated function as the - # plain function it wraps. At run time each is a Modal function. + # Pyright types each decorated function as a modal.Function whose + # `remote` and `spawn` take that function's own parameters, which it + # doesn't accept for RemoteFunction's `*args: Any`. At run time each is + # a Modal function, called with its own arguments. functions=PrecomputeFunctions( plan_artifacts=cast("RemoteFunction", plan_artifacts), build_dataset=cast("RemoteFunction", build_dataset), diff --git a/projects/policyengine-simulation-executor/src/modal/segmented_national.py b/projects/policyengine-simulation-executor/src/modal/segmented_national.py index 03693a0d1..0a291da68 100644 --- a/projects/policyengine-simulation-executor/src/modal/segmented_national.py +++ b/projects/policyengine-simulation-executor/src/modal/segmented_national.py @@ -46,6 +46,7 @@ NationalReduceInputs, build_national_output, ) +from policyengine_simulation_observability import scopes from policyengine_simulation_observability.errors import log_and_redact_exception from policyengine_simulation_observability.stages import ( SEGMENTED_NATIONAL_STAGES, @@ -329,7 +330,8 @@ def _extend_last_group_with_uncovered_states( def _spawn_child(self, group: list[str], *, attempt: int) -> modal.FunctionCall: payload = build_group_child_payload(self.params, group) observability_context = self.runtime.capture_context() - with self.runtime.span( + with scopes.span( + self.runtime, SEGMENTED_NATIONAL_STAGES.name(Stage.SEGMENTED_NATIONAL_CHILD_SPAWN), attributes={"region_group": "+".join(group), "child_attempt": attempt}, ): diff --git a/rules/baselines/pyright-executor.txt b/rules/baselines/pyright-executor.txt index 544e0ce53..77651b94b 100644 --- a/rules/baselines/pyright-executor.txt +++ b/rules/baselines/pyright-executor.txt @@ -7,26 +7,11 @@ # occur with # python -m scripts.rules pyright projects/policyengine-simulation-executor --write-baseline # then set the rule's [rule.baseline] count in rules/registry.toml to match. -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "App" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "Image" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "Image" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "Image" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "Secret" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "Secret" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "current_function_call_id" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "current_function_call_id" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "is_local" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "is_local" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "is_local" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/app.py :: reportAttributeAccessIssue :: "is_local" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/app.py :: reportReturnType :: Function with declared return type "dict[Unknown, Unknown]" must return value on all code paths projects/policyengine-simulation-executor/src/modal/app.py :: reportReturnType :: Function with declared return type "dict[Unknown, Unknown]" must return value on all code paths projects/policyengine-simulation-executor/src/modal/app.py :: reportReturnType :: Function with declared return type "dict[Unknown, Unknown]" must return value on all code paths -projects/policyengine-simulation-executor/src/modal/budget_window_batch.py :: reportAttributeAccessIssue :: "current_function_call_id" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/budget_window_batch.py :: reportPossiblyUnboundVariable :: "context" is possibly unbound projects/policyengine-simulation-executor/src/modal/budget_window_results.py :: reportArgumentType :: Argument of type "float" cannot be assigned to parameter "year" of type "Literal['Total']" in function "__init__" -projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportAttributeAccessIssue :: "FunctionCall" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportPossiblyUnboundVariable :: "annual_impact" is possibly unbound projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportPossiblyUnboundVariable :: "call" is possibly unbound @@ -37,31 +22,12 @@ projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py : projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportPossiblyUnboundVariable :: "state" is possibly unbound projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportPossiblyUnboundVariable :: "state" is possibly unbound projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py :: reportReturnType :: Function with declared return type "dict[str, Any]" must return value on all code paths -projects/policyengine-simulation-executor/src/modal/precompute_app.py :: reportAttributeAccessIssue :: "App" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportAttributeAccessIssue :: "FunctionCall" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportAttributeAccessIssue :: "FunctionCall" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportPossiblyUnboundVariable :: "output" is possibly unbound projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportPossiblyUnboundVariable :: "output" is possibly unbound projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportPossiblyUnboundVariable :: "output" is possibly unbound projects/policyengine-simulation-executor/src/modal/segmented_national.py :: reportPossiblyUnboundVariable :: "output" is possibly unbound -projects/policyengine-simulation-executor/src/modal/smoke_app.py :: reportAttributeAccessIssue :: "App" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/smoke_app.py :: reportAttributeAccessIssue :: "is_local" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/smoke_app.py :: reportFunctionMemberAccess :: Cannot access attribute "remote" for class "FunctionType" projects/policyengine-simulation-executor/src/modal/utils/release_sentinel.py :: reportOptionalMemberAccess :: "split" is not a known attribute of "None" -projects/policyengine-simulation-executor/src/modal/utils/update_v2_version_manifest.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportArgumentType :: Argument of type "dict[Unknown, Unknown]" cannot be assigned to parameter "value" of type "BundleManifestMetadata" in function "__setitem__" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportAttributeAccessIssue :: "Dict" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportGeneralTypeIssues :: Could not assign item in TypedDict projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportIndexIssue :: "__getitem__" method not defined on type "object" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportIndexIssue :: "__setitem__" method not defined on type "object" @@ -69,7 +35,6 @@ projects/policyengine-simulation-executor/src/modal/utils/update_version_registr projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportOptionalMemberAccess :: "get" is not a known attribute of "None" projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py :: reportReturnType :: Type "dict[str, int | str | RouteMaps | dict[str, BundleManifestMetadata]]" is not assignable to return type "RoutingState" -projects/policyengine-simulation-executor/src/modal/utils/validate_v2_worker_release.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/baseline_artifacts.py :: reportAttributeAccessIssue :: Cannot access attribute "resolve_entity_variables" for class "TaxBenefitModelVersion" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/precompute.py :: reportArgumentType :: Argument of type "str | None" cannot be assigned to parameter "outcome" of type "ArtifactOutcome | None" in function "__init__" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/precompute.py :: reportAttributeAccessIssue :: Cannot access attribute "artifact_outcome" for class "Simulation" @@ -105,11 +70,6 @@ projects/policyengine-simulation-executor/src/policyengine_simulation_executor/s projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py :: reportOptionalMemberAccess :: "entity_data" is not a known attribute of "None" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py :: reportOptionalMemberAccess :: "items" is not a known attribute of "None" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/spm.py :: reportArgumentType :: Argument of type "object" cannot be assigned to parameter "defaults" of type "SPMSelection" in function "__init__" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_cache/precompute.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_cache/precompute.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_cache/precompute.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_cache/precompute.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_cache/precompute.py :: reportAttributeAccessIssue :: "Function" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportArgumentType :: Argument of type "Series | Unknown | Self@DataFrame" cannot be assigned to parameter "actual_values" of type "Series" in function "_compare_column" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportArgumentType :: Argument of type "Series | Unknown | Self@DataFrame" cannot be assigned to parameter "expected_values" of type "Series" in function "_compare_column" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportArgumentType :: Argument of type "object" cannot be assigned to parameter "expected" of type "Sequence[Unknown]" in function "_compare_report_lists" @@ -117,8 +77,6 @@ projects/policyengine-simulation-executor/src/policyengine_simulation_executor/s projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportArgumentType :: Argument of type "object" cannot be assigned to parameter "x" of type "ConvertibleToFloat" in function "__new__" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportArgumentType :: Argument of type "object" cannot be assigned to parameter "x" of type "ConvertibleToFloat" in function "__new__" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_parity.py :: reportIndexIssue :: "__getitem__" method not defined on type "object" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/fanout.py :: reportAttributeAccessIssue :: "FunctionCall" is not a known attribute of module "modal" -projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/fanout.py :: reportAttributeAccessIssue :: "FunctionCall" is not a known attribute of module "modal" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py :: reportArgumentType :: Argument of type "str" cannot be assigned to parameter "created_at" of type "datetime" in function "__init__" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py :: reportArgumentType :: Argument of type "str" cannot be assigned to parameter "dynamic" of type "Dynamic | dict[str, Any] | None" in function "__init__" projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py :: reportArgumentType :: Argument of type "str" cannot be assigned to parameter "extra_variables" of type "dict[str, list[str]]" in function "__init__" diff --git a/rules/registry.toml b/rules/registry.toml index fa633d645..44257ba8c 100644 --- a/rules/registry.toml +++ b/rules/registry.toml @@ -1139,7 +1139,7 @@ job = "pyright" invocation = "python -m scripts.rules pyright" [rule.baseline] file = "rules/baselines/pyright-executor.txt" -count = 119 +count = 77 [rule.proof] bad_fixture = "rules/fixtures/sim.quality.pyright-executor/bad" good_fixture = "rules/fixtures/sim.quality.pyright-executor/good" From 13b4140946438846b6402cbd695d7f5c084ed2e8 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:22:25 +0400 Subject: [PATCH 4/6] Raise RuntimeError, not a routing conflict, for an unopened store RoutingConflictError means the live routing state isn't the one a plan expects. A routing-state Dict that can't be opened isn't that: no plan disagrees with anything. The two writers now raise a plain RuntimeError with the same message. Nothing catches either exception, so the command still exits non-zero. Co-Authored-By: Claude Opus 5.5 --- .../src/modal/utils/update_version_registry.py | 9 +++++---- .../tests/test_update_version_registry.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py index e6c722c8d..fe58ac4fd 100644 --- a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py +++ b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py @@ -439,8 +439,9 @@ def build_restored_routing_state( ROUTING_PLAN_SCHEMA = "policyengine-simulation.routing-plan/v1" -# Raised by a routing write when _routing_store, asked to create the Dict, -# still returns None. +# Raised, as a RuntimeError, by a routing write when _routing_store, asked to +# create the Dict, still returns None. That needs a lookup error: modal's +# Dict.from_name returns a handle without contacting Modal. UNOPENED_ROUTING_STORE = ( "the routing state Modal Dict could not be opened; nothing was written" ) @@ -625,7 +626,7 @@ def record_routing_plan(*, environment: str, plan: dict) -> None: validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=True) if store is None: - raise RoutingConflictError(UNOPENED_ROUTING_STORE) + raise RuntimeError(UNOPENED_ROUTING_STORE) _require_finished_publication(store) if ( routing_state_digest(store.get(ROUTING_STATE_ACTIVE_KEY)) @@ -659,7 +660,7 @@ def apply_routing_plan(*, environment: str, plan: dict) -> None: validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=True) if store is None: - raise RoutingConflictError(UNOPENED_ROUTING_STORE) + raise RuntimeError(UNOPENED_ROUTING_STORE) _require_finished_publication(store, allowed_plans=[plan]) current = store.get(ROUTING_STATE_ACTIVE_KEY) if routing_state_digest(current) != plan["before_sha256"]: diff --git a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py index 76ff396e1..d8f6c9426 100644 --- a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py +++ b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py @@ -1348,17 +1348,18 @@ def from_name(name: str, **lookup_options: object): def test_routing_writes_refuse_a_routing_store_that_cannot_be_opened( monkeypatch, serialized_workflow ): - """Recording or applying a plan stops with a conflict, not an AttributeError.""" + """Recording or applying a plan stops with a RuntimeError, not an AttributeError.""" monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=_UnopenableDict)) original = _publish(monkeypatch, None, "6.0.0", "1.1.0", "2.1.0") plan = _plan(monkeypatch, original, ("6.1.0", "1.1.0", "2.2.0")) for operation in (registry.record_routing_plan, registry.apply_routing_plan): with pytest.raises( - registry.RoutingConflictError, - match="the routing state Modal Dict could not be opened", - ): + RuntimeError, match="the routing state Modal Dict could not be opened" + ) as raised: operation(environment="staging", plan=plan) + # Not a RoutingConflictError: no plan disagrees with the live state. + assert type(raised.value) is RuntimeError def test_completing_a_plan_refuses_a_missing_routing_store( From d5ed9a51a7de5d104d346be57faafefa6dd8ea37 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:22:25 +0400 Subject: [PATCH 5/6] Correct the executor's extraPaths comment about the unit tests The comment said src is never an import root at runtime. That holds where the Modal apps run, but pytest's pythonpath puts src first in the unit tests, where `import modal` can find src/modal; the comment now says so. Co-Authored-By: Claude Opus 5.5 --- projects/policyengine-simulation-executor/pyproject.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/policyengine-simulation-executor/pyproject.toml b/projects/policyengine-simulation-executor/pyproject.toml index 71e5d1cad..007a155d4 100644 --- a/projects/policyengine-simulation-executor/pyproject.toml +++ b/projects/policyengine-simulation-executor/pyproject.toml @@ -99,7 +99,10 @@ markers = [ include = ["src"] # Without extraPaths, Pyright adds src as an import root, and `import modal` # in the Modal app modules under src/modal resolves to that directory instead -# of the installed modal library. At runtime src is never an import root: the -# package is installed from src, and the app modules import each other as -# src.modal. from the project directory. +# of the installed modal library. Where the apps run (modal deploy and +# modal run, and the Modal containers), src is not an import root: the package +# is installed from src, and the app modules import each other as +# src.modal. from the project directory. The unit tests are the +# exception: pytest's pythonpath puts src first, so there `import modal` can +# find src/modal, and the tests replace the modal module where they need it. extraPaths = [] From b6e17faddf5e64877642306e81d3a59e0bfe153a Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:55:04 +0400 Subject: [PATCH 6/6] Report a missing routing-state Dict however modal reports it modal 1.4.2's Dict.from_name returns a handle without contacting Modal, so for a Dict that doesn't exist it never raised the NotFoundError that _routing_store turned into None. The first read raised it instead. So every `store is None` branch was unreachable, and a missing Dict failed with a NotFoundError traceback. _routing_store now looks the Dict up (hydrate) when the caller needs it to exist already, so a missing Dict returns None whether modal reports it at lookup or not. Then: - complete_routing_plan raises the routing writes' explicit RuntimeError ("could not be opened; nothing was written"), where it raised RoutingConflictError for an eager report and NotFoundError for a lazy one; - restore_routing_plans reaches its tested outcome for a missing Dict ("unchanged" when no plan had a prior state, otherwise a routing conflict) instead of a NotFoundError; - finalize and the pending-plan report reach their existing "nothing pending" outcomes. New tests use a stand-in that behaves like modal 1.x; both failed with NotFoundError before. Environment-variable reads are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../modal/utils/update_version_registry.py | 20 +++++-- .../tests/test_update_version_registry.py | 57 +++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py index fe58ac4fd..b11acff9b 100644 --- a/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py +++ b/projects/policyengine-simulation-executor/src/modal/utils/update_version_registry.py @@ -439,9 +439,8 @@ def build_restored_routing_state( ROUTING_PLAN_SCHEMA = "policyengine-simulation.routing-plan/v1" -# Raised, as a RuntimeError, by a routing write when _routing_store, asked to -# create the Dict, still returns None. That needs a lookup error: modal's -# Dict.from_name returns a handle without contacting Modal. +# Raised, as a RuntimeError, when a routing write or a plan's completion finds +# no routing-state Dict to write to. UNOPENED_ROUTING_STORE = ( "the routing state Modal Dict could not be opened; nothing was written" ) @@ -517,18 +516,27 @@ def validate_routing_plan(plan: object) -> dict: def _routing_store(environment: str, *, create_if_missing: bool) -> "modal.Dict | None": + """Return the routing-state Dict, or None when it doesn't exist. + + modal's Dict.from_name returns a handle without contacting Modal, so a + missing Dict would surface as NotFoundError at the first read. When the + Dict must already exist, look it up now, so a missing one is reported here. + """ try: - return modal.Dict.from_name( + store = modal.Dict.from_name( ROUTING_STATE_DICT_NAME, environment_name=environment, create_if_missing=create_if_missing, ) + if not create_if_missing: + store.hydrate() except KeyError: return None except Exception as exc: if exc.__class__.__name__ == "NotFoundError": return None raise + return store CANONICAL_GITHUB_REPOSITORY = "PolicyEngine/policyengine-sim-api" @@ -642,7 +650,9 @@ def complete_routing_plan(*, environment: str, plan: dict) -> None: _require_serialized_routing_workflow() validate_routing_plan(plan) store = _routing_store(environment, create_if_missing=False) - if store is None or _unfinished_publication(store) != plan: + if store is None: + raise RuntimeError(UNOPENED_ROUTING_STORE) + if _unfinished_publication(store) != plan: msg = "no matching unfinished routing publication to complete" raise RoutingConflictError(msg) if ( diff --git a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py index d8f6c9426..e4b56a658 100644 --- a/projects/policyengine-simulation-executor/tests/test_update_version_registry.py +++ b/projects/policyengine-simulation-executor/tests/test_update_version_registry.py @@ -42,6 +42,9 @@ def pop(self, key): def snapshot(self) -> dict: return dict(self._data) + def hydrate(self): + return self + @pytest.fixture def serialized_workflow(monkeypatch): @@ -1362,18 +1365,64 @@ def test_routing_writes_refuse_a_routing_store_that_cannot_be_opened( assert type(raised.value) is RuntimeError +class NotFoundError(Exception): + """Named like modal.exception.NotFoundError, which the utility matches by name.""" + + +class _LazyMissingDict: + """A missing Dict as modal 1.x reports it. + + Dict.from_name returns a handle without contacting Modal; looking the Dict + up, or the first read, raises NotFoundError. + """ + + @staticmethod + def from_name(name: str, **lookup_options: object): + del lookup_options + return _LazyMissingDict.Handle(name) + + class Handle: + def __init__(self, name: str): + self.name = name + + def hydrate(self): + raise NotFoundError(self.name) + + def get(self, key, default=None): + raise NotFoundError(self.name) + + +@pytest.mark.parametrize( + "missing_dict", [_UnopenableDict, _LazyMissingDict], ids=["eager", "lazy"] +) def test_completing_a_plan_refuses_a_missing_routing_store( - monkeypatch, serialized_workflow + monkeypatch, serialized_workflow, missing_dict ): - monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=_UnopenableDict)) + monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=missing_dict)) original = _publish(monkeypatch, None, "6.0.0", "1.1.0", "2.1.0") plan = _plan(monkeypatch, original, ("6.1.0", "1.1.0", "2.2.0")) + with pytest.raises(RuntimeError, match=registry.UNOPENED_ROUTING_STORE) as raised: + registry.complete_routing_plan(environment="staging", plan=plan) + assert type(raised.value) is RuntimeError + + +def test_restoring_with_a_lazily_missing_routing_store( + monkeypatch, serialized_workflow +): + """With modal 1.x a missing Dict is reported at lookup; restore sees no store.""" + monkeypatch.setattr(registry, "modal", SimpleNamespace(Dict=_LazyMissingDict)) + first = _plan(monkeypatch, None, ("6.1.0", "1.1.0", "2.2.0")) + assert ( + registry.restore_routing_plans(environment="staging", plans=[first]) + == "unchanged" + ) + later = _plan(monkeypatch, first["after"], ("6.2.0", "1.1.0", "2.3.0")) with pytest.raises( registry.RoutingConflictError, - match="no matching unfinished routing publication to complete", + match=r"^there is no routing state to restore into$", ): - registry.complete_routing_plan(environment="staging", plan=plan) + registry.restore_routing_plans(environment="staging", plans=[first, later]) def test_runner_loss_cannot_be_adopted_by_the_next_release(monkeypatch, patched_modal):