Repository navigation
Stop src/modal shadowing the modal library in the executor's Pyright check - #869
Merged
Merged
Conversation
anth-volk
added this pull request to stack #867
October 21, 2026 15:03
This was referenced Oct 21, 2026
anth-volk
force-pushed
the
pyright-executor-modal
branch
from
October 21, 2026 16:21
f18e48c to
aa306ef
Compare
anth-volk
force-pushed
the
pyright-executor-modal
branch
from
October 21, 2026 16:59
03badfe to
7428fc9
Compare
anth-volk
force-pushed
the
pyright-executor-modal
branch
from
October 21, 2026 20:33
7428fc9 to
502c480
Compare
anth-volk
force-pushed
the
pyright-executor-modal
branch
from
October 21, 2026 21:37
502c480 to
5bc0962
Compare
_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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.<module> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
anth-volk
force-pushed
the
pyright-executor-modal
branch
from
October 21, 2026 22:16
5bc0962 to
b6e17fa
Compare
anth-volk
marked this pull request as ready for review
October 22, 2026 13:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #868
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
Third in the Pyright stack, on top of #866. Pyright now resolves
import modalin the executor's Modal app modules to the installed modal library instead of the localsrc/modaldirectory, and the errors that the shadowing hid are fixed. The executor's baseline (rules/baselines/pyright-executor.txt) goes from 178 entries to 126 and grows in no commit.import modalto the modal libraryThe shadowing fix: configuration only
Without
extraPaths, Pyright addssrcas an import root, soimport modalinsidesrc/modal/*.pyresolved tosrc/modal/__init__.py. At runtimesrcis never an import root: the package is installed fromsrc(an editable install addssrctosys.pathafter site-packages), and the app modules import each other assrc.modal.<module>from the project directory.extraPaths = []in the executor's[tool.pyright]turns the automatic root off. Pyright still resolvessrc.modal.*from the project root andpolicyengine_simulation_executorfrom the environment. Only Pyright reads this setting: no rename or move, and no change to Modal deployment entrypoints.On its own it removes 47 errors and exposes 9 that the shadowing hid. To keep the baseline shrinking in every commit, the 3 exposed errors that need a behaviour change in an unreachable path are fixed first, each with a test written before the fix. The configuration commit then fixes the other 6.
What changed
Noneerrors in the same functions)._routing_storereturnsNonewhen Modal finds no routing-state Dict.record_routing_planandapply_routing_planask it to create the Dict, soNoneshould never come back. If it did, they failed withAttributeError: 'NoneType' object has no attribute 'get'; they now raiseRuntimeError("could not be opened; nothing was written").complete_routing_plannow raises the sameRuntimeErrorfor a missing store, before its check that a matching plan is pending. The command exits non-zero either way, as before, and the utility'smainhandles neither exception.modal.Dict.from_namereturns a handle without contacting Modal; a Dict that doesn't exist raisesmodal.exception.NotFoundErrorat the first read. So_routing_storenever returnedNonefor a missing Dict, and everystore is Nonebranch was unreachable: callers failed with aNotFoundErrortraceback instead._routing_storenow calls the handle'shydrate()(synchronous; it looks the Dict up) when the caller passescreate_if_missing=False, so a missing Dict returnsNonewhether modal reports it at lookup or at first read. TheNonebranches are kept, and are now reachable:complete_routing_planraisesRuntimeErrorwithUNOPENED_ROUTING_STORE("the routing state Modal Dict could not be opened; nothing was written"), the message the routing writes use.restore_routing_planskeeps its existing, tested contract for a missing Dict:"unchanged"when no plan had a prior state, otherwiseRoutingConflictError("there is no routing state to restore into"). I did not change it to theRuntimeError: a first restore with nothing to restore is a valid no-op that the existing tests require, and the conflict already names the missing state.no-pendinginstead of aNotFoundError;.github/scripts/simulation-deployment-ready.shstill fails the deployment, because it requiresFINALIZE_OUTCOME=completed. Insimulation-rollback.yml, the step that resolves the unfinished routing publication (--print-pending-plan) printsnullinstead of failing.create_if_missing=True, so they don't callhydrate()and are unchanged.modal.current_function_call_id()returnsstr | None. The batch keys its children and state by it, and its docstring says it must run inside a Modal call. Outside one, it went on until building its state failed with a PydanticValidationError. It now raisesRuntimeErrorfirst, as the Stage 12 coordinator inv2_app.pyalready does. Nothing changes inside a Modal call.RemoteFunction(5 exposed errors, previously baselined with other wording). Each Modal function'sremoteandspawntake that function's own parameters, so a protocol method declared(*args: Any)didn't accept them. The protocol now declares(*args: Any, **kwargs: Any), the gradual signature type checkers treat as "any arguments". This changes only the lines' annotations; their existingnoqa: ANN401comments already coverAny.spawnthat_spawn_childreturns now usesscopes.span, so Pyright no longer reports the method as missing a return.Behaviour changes
Both are in paths that production doesn't reach, and the code failed in both before. Tests written first:
RuntimeErrortest_routing_writes_refuse_a_routing_store_that_cannot_be_openedAttributeError: 'NoneType' object has no attribute 'get'test_run_budget_window_batch_impl_refuses_to_run_outside_a_modal_callValidationErroronbatch_job_idThe last commit changes what a missing routing-state Dict produces. With modal 1.4.2 these paths failed with
NotFoundErrorbefore:complete_routing_planraisesRuntimeError(UNOPENED_ROUTING_STORE)for a missing Dict, reported at lookup or at first readtest_completing_a_plan_refuses_a_missing_routing_store(parametrised over a stand-in that reports at lookup and one that reports at first read, like modal 1.x)RoutingConflictErrorandNotFoundErrorrestore_routing_plansreturns its tested outcome for a Dict modal reports missing at first readtest_restoring_with_a_lazily_missing_routing_storeNotFoundErrorCommits (red, then green)
extraPaths = [], the check reported 6 errors the baseline didn't list and 47 entries that no longer occur. Green: 0 new errors after the protocol and span fixes; baseline 126.RuntimeErrorfor the routing writes (review finding 2). Red: the test pinning the exact type gotRoutingConflictError. Green: passes.extraPathscomment (review finding 1). Comment only.Suppressions added
None.
Real bugs found
None in production paths. The two behaviour changes above replace a confusing failure with an explicit one in paths that production doesn't reach.
Self-review
review-codeon this PR's diff, run by a separate agent, found no bugs. It reproduced the 47 removed and 9 exposed errors, and showed both new tests failing on the parent revision. Code-quality findings:extraPathscomment saidsrcis never an import root at runtime, but pytest'spythonpathputs it first in the unit tests. Fixed: the comment now says where the apps run and names the test exception.RoutingConflictErrormeans the live state isn't the one a plan expects, which a Dict that can't be opened isn't. Fixed: the two writers raise a plainRuntimeErrorwith the same message. The test pins the exact type, and was red againstRoutingConflictError. The reviewer also noted thatmodal.Dict.from_namereturns a handle without contacting Modal, so this path needs a lookup error and is unreachable in a real deployment; the comment now says so.segmented_nationalis converted here. Declined: the plan converts the rest in Fix the executor's remaining Pyright errors #873.A later
review-codepass over the whole stack found one more:Dict.from_nameis lazy, thestore is Nonebranches could not run, andcomplete_routing_planreported a missing Dict asNotFoundError(or, with an eager stand-in, as a routing conflict). Fixed in the last commit, as described above.A
review-codepass over the last commit found no bugs. It confirmed in the installed modal 1.4.2 thatfrom_nameis lazy, thathydrate()is synchronous, and that a missing Dict raisesNotFoundError, and it ran the new tests red against the parent commit. One minor finding, not changed: finalize and--print-pending-planwith a Dict that modal reports missing at first read are checked by hand, not by a test.Environment variables
update_version_registry.pyis a deployment utility that the deploy workflows run. Its environment-variable reads are unchanged, including in the last commit, which changes only_routing_storeandcomplete_routing_plan:GITHUB_ACTIONS,GITHUB_REPOSITORYandGITHUB_WORKFLOW_REF, which GitHub Actions provides to every step, are read in_require_serialized_routing_workflow.budget_window_batch.pyandsegmented_national.pyrun inside Modal and read no new variables.segmented_national.pynow importspolicyengine_simulation_observability.scopes(#864), part of the observability library that the executor images already install. No workflow, image or Modal app definition changed.Rebase onto #879 (2026-10-22)
Rebased onto
mainafter #879 merged. Where this section and the text above differ, this section is current.What conflicted.
rules/registry.toml,docs/generated/rules.mdandrules/baselines/pyright-executor.txt. #879 removed 54 executor baseline entries (178 to 124): 49 insimulation_runtime.py, and the 5precompute_app.pyerrors for the precompute functions passed toPrecomputeFunctions, which #879 fixed by casting its six Modal functions (it addsbuild_state_datasets) toRemoteFunction.How it was resolved.
python -m scripts.rules pyright projects/policyengine-simulation-executor --write-baseline, starting from its parent's baseline, and the count set to match. Pyright passes at every commit.RemoteFunctionprotocol change (*args: Any, **kwargs: Any) is dropped, and Bake per-state ACS-local year files into the main path's worker image #879's casts are kept. With the casts, Pyright reports nothing at that call with or without the protocol change. Without either, it reports 6 errors oncemodalresolves.modalinprecompute_app.py, which this PR makes untrue. It now says why the casts are still needed: eachmodal.Function'sremoteandspawntake that function's own parameters, which Pyright doesn't accept forRemoteFunction's*args: Any. No code changed there.extraPaths = []now removes 42 errors and exposes 4: the 3 fixed by the first two commits, and the segmented national span.import modalto the modal libraryBaseline at this PR's head: 77 entries (from 124 on
main).Checks after the rebase. Pyright passes with 77. Executor unit tests: 1645 passed, 22 skipped, 3 deselected. Ruff 0.15.22 format and check are clean. The quality guards and
python -m scripts.rules guard --base-ref origin/mainpass.renderleaves no changes. No newnoqa.Testing
python -m scripts.rules pyright projects/policyengine-simulation-executor: passes with 126.tests/test_update_version_registry.py: 277 passed.This PR is above the bottom of the stack, so its CI runs only
CI complete, which fails by design until the PRs below merge.🤖 Generated with Claude Code