DX improvements: IDE typing, guard transitions, structured logging, and API stability guarantees - #115
DX improvements: IDE typing, guard transitions, structured logging, and API stability guarantees#115sanbales wants to merge 11 commits into
Conversation
Apply PEP 681 @dataclass_transform to Actor so type checkers (Pylance, mypy, pyright) automatically generate __init__ signatures for Actor subclasses from their State field declarations. This means modelers get autocomplete, type errors, and hover docs without writing .pyi stubs for every model class. Also replaces the `no_init` parameter on State with `init` (inverted) to match the PEP 681 field specifier protocol. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskNetworkFactory now accepts class references as keys in task_links,
deriving task_classes automatically:
TaskNetworkFactory("Net", task_links={
GoToWork: TaskLinks(default=TalkToBoss, allowed=[TalkToBoss]),
...
})
TaskLinks.default and .allowed also accept class references alongside
strings. All references are resolved to __name__ strings internally.
Construction-time validation checks that all referenced task names
exist in task_classes and warns about orphaned entries.
The existing string-keyed API continues to work unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alization TaskLinks now supports a `transitions` field: an ordered list of (target, guard) tuples where the first matching guard picks the next task. This enables declarative branching without DecisionTask. Task gains on_enter() and on_exit() hooks — zero-time methods called before/after task() by the network loop. These replace the setup and teardown roles of DecisionTask. TaskNetworkFactory gains to_mermaid() and to_dot() methods for rendering task network diagrams. Guard evaluation priority: task queue (imperative) > guards > default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update how-to docs for guard transitions, on_enter/on_exit hooks, and network visualization. Expand the cashier JupyterLite example and add jupyterlite model sources to lint tasks.
The jupyterlite example code is pedagogical and shouldn't need the full Google-style docstrings that the src/ tree enforces. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lity doc - Introduce Transition NamedTuple so task-network transitions expose .target/.guard/.label and consumers no longer branch on tuple length. Raw (target, guard[, label]) tuples remain accepted. - Declare every task node in to_mermaid()/to_dot() output so hooked and unhooked tasks render consistently. - Rename internal State._no_init to _init to match the PEP 681 public parameter and drop the double-negation. - Re-export TASK_GEN/SIMPY_GEN/ROUTINE_GEN and Transition through upstage_des.api; update the cashier example to import only via UP. - Add an API stability guide (stable / experimental / internal tiers and deprecation policy) under user_guide/how_tos/. - Add golden-file snapshot tests for to_mermaid()/to_dot(). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Actor.log() now gates on two independent sinks: the existing per-actor _debug_log list (controlled by the `debug_log` flag set at Actor construction) and the standard Python logging module rooted at `upstage_des`. The library attaches a NullHandler on import and defaults to WARNING, so it remains silent unless the user opts in. Call sites use printf-style arguments so interpolation only runs when at least one sink will actually consume the record. Internal callers in task.py, actor.py, task_network.py, and nucleus.py were converted to this form, and the expensive inspect.stack() walk inside Actor._log_caller is now skipped when both sinks are disabled. Actor loggers are named `upstage_des.actor.<name>` so users can filter per-actor; rehearsal clones log to `<name>.rehearsal`. Back-compat: the `_debug_log` list shape and `actor.log()` (read) / `actor.log(msg)` (write) surface are unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reading the in-memory event log via `actor.log()` (no args) is now deprecated. The canonical readers are: * `actor.logs` — new read-only property returning the list. * `actor.get_log()` — existing method, unchanged. `actor.log()` with no arguments still returns the list but raises DeprecationWarning pointing users to the replacements. This resolves the dual-purpose signature that was particularly confusing for code-generation agents, which often pass `None` intending to write. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI pins prefix-dev/setup-pixi@v0.9.4 with pixi-version v0.41.0, but the lock in 2baa23e was written by a newer pixi (0.67.0). The format difference (older pixi uses `.` instead of `./` for the workspace package URL and omits the `options.pypi-prerelease-mode` block) was causing `pixi install --locked` to fail in GitHub Actions. No dependency scope changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The notebook was inadvertently committed with executed outputs in 2baa23e. This clears them forward; prior revisions still carry the outputs in history but that's acceptable given the risk of rewriting a pushed branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Convert two grid tables in api_stability.rst to list-tables. The grid-table variants were flagged by docutils for an unterminated inline literal (`` ``UP.Event`` `` with no trailing space before the column separator) and a malformed table (same issue on `` ``UP.MessageContent`` ``). list-tables are whitespace-robust and easier to maintain. - Add an explicit `guard-based transitions` ref target above the section heading in task_networks.rst so the cross-reference from task.rst resolves. Build now completes with zero warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks for this! These are some great suggestions. A yet un-posted version 1.0 branch is being made that incorporates some of this advice already (the When I get it posted, I will ask you to put in an MR into that branch so you can get contribution credit. I will likely not approve/merge this in given the changes in the background that you weren't aware of! |
Thanks, James! FWIW, not worried about the credit (Opus did most of the work 🤣 anyways), and not expecting these to be included in |
Summary
Eight commits' worth of developer-experience improvements for UPSTAGE, focused
on making the framework easier and faster to use for both human modelers and
AI-assisted development (Copilot, Cursor, Claude Code, etc.). All changes are
backwards-compatible — existing simulations keep working; every new API is
additive.
What's in this PR
Actor type-checking (PEP 681)
@dataclass_transform()onActor— type checkers (Pylance, mypy, pyright)now auto-generate
__init__signatures forActorsubclasses from theirStatefield declarations. No more.pyistubs for user models.State(init=True|False)parameter replacing the invertedno_init, soPEP 681 can recognize it as a field specifier.
Task networks: ergonomics and introspection
Class-reference API for
TaskNetworkFactory. Users passTaskclasses as keys instead of stringifying them by hand:
Construction-time validation — unknown task-link references now raise
immediately with a helpful error instead of failing at simulation time.
Guard-based transitions — declarative branching without needing a
DecisionTask:A new
TransitionNamedTuple is the normalized internal form and isre-exported via
UP.Transition.on_enter()/on_exit()hooks onTask— zero-time setup/teardownwithout introducing a separate task.
to_mermaid()andto_dot()onTaskNetworkandTaskNetworkFactoryfor quick network visualization (renders inline in Jupyter, GitHub
Markdown, any Mermaid viewer). Covered by golden-file snapshot tests.
Structured logging
actor.log()now routes through Python's standardloggingmodule inaddition to the existing
_debug_loglist. Logger hierarchy isupstage_des.actor.<name>(.rehearsalsuffix for rehearsal clones) sousers can filter per-actor or suppress rehearsal noise with standard
loggingconfig. ANullHandleris installed on import — library useis silent by default.
actor.log(msg, *args, level=logging.INFO)accepts printf-stylearguments; formatting is deferred until at least one sink wants the
record. Benchmarks show ~15% faster log-heavy loops when both sinks are
off vs. legacy f-string call sites.
actor.log()is now write-only. The canonical reader is the newactor.logsproperty (actor.get_log()also still works). Callingactor.log()with no args emits aDeprecationWarning.API stability guarantees
docs/source/user_guide/how_tos/api_stability.rst— lists everypublic symbol in stable / experimental / internal tiers, documents
the deprecation policy, and explains the logging hierarchy. This gives
users (and AI agents) a single authoritative reference for what's safe
to depend on.
api.pygains re-exports forTASK_GEN,SIMPY_GEN,ROUTINE_GEN,and
Transitionso modelers canimport upstage_des.api as UPandnever reach into private modules.
Docs and examples
factories.
UP.import idiom so AIagents mimicking the example pick up consistent patterns.
Performance
Bench (same workload, 10 trials each, 20 actors × 200 log calls × 5s sim):
gtri/devdebug_log=Truedebug_log=Falsedebug_log=False+ logger offdebug_log=TrueLegacy f-string call sites: within a few percent of baseline. Users who
migrate hot logs to printf-style and disable both sinks: ~13% faster than
baseline. No regression in the network-step path.
Back-compat
TaskLinks(default=..., allowed=...)usageworks unchanged.
actor.get_log()/actor._debug_logsemantics unchanged.actor.log()with no args still returns the list (with aDeprecationWarning).TaskNetworkFactory(name, task_classes, task_links)signature still works.
Test plan
pixi run -e dev test— 274 tests pass (was 262 pre-PR; 12 newtests added for logging, diagram snapshots, and guard transitions).
pixi run -e dev lint-check— ruff, ssort, mypy, pyproject-fmt allclean.
(5 edits total).
gtri/devtip to verify nomeaningful regression.
Housekeeping (post-initial review)
RunCashier.ipynbstripped of outputs and execution counts.pixi.lockregenerated with pixi 0.41.0 to match CI's pinned version.api_stability.rstconverted to list-tables, and a missingguard-based transitionsxref anchor added totask_networks.rst.Docs now build with zero warnings.
Commits in order
02ae210Add@dataclass_transformtoActorfor IDE type-checking support8901f4bAdd class-reference API and validation toTaskNetworkFactoryde91174Add guard-based transitions,on_enter/on_exithooks, and network visualization2baa23eDocument task network improvements and polish cashier examplefbe6c9cExempt jupyterlite example model from docstring linte06d46fDX improvements:TransitionNamedTuple, normalized imports, API stability doce459b19Route actor events through Python logging; defer format interpolation3673928MakeActor.log()write-only; addlogsproperty as canonical reader408a49cStrip outputs and execution counts fromRunCashier.ipynb113f760Regeneratepixi.lockwith pixi 0.41.0 to match CI8cf9f7cFix Sphinx warnings: malformed tables and missing xref anchor