Repository navigation
fix(core): remap injected __context/__tracer for single-step actions - #1026
xihaian251 wants to merge 4 commits into
Conversation
The four single-step runners (_run_single_step_action, _arun_single_step_action, _run_single_step_streaming_action, _arun_single_step_streaming_action) passed the framework-injected "__context"/"__tracer" keys straight to the action method. A class-based action that follows docs/concepts/actions.rst and declares the parameter as __context has that name mangled to _ClassName__context, so the call raised "TypeError: ... got an unexpected keyword argument '__context'". The other four runners already solve this with _remap_injected_inputs, so this just applies the same call after validate_inputs. Function-based actions are unaffected: their parameter name is not mangled, so the remap is a no-op.
…ameters The mangled-name lookup accepted any parameter whose name *ends* with "__context"/"__tracer". A class-based action that also declares a real input whose name happens to end with the dunder (e.g. ``user__context``) matched first, so the framework then moved the injected ``__context`` value onto that parameter and clobbered the user's own input. Require the prefix before the dunder to start with an underscore, which is what Python's own mangling produces (``_ClassName__context``). Function-based actions keep using the literal name and are unaffected.
Requiring an underscore before the dunder was not enough. CPython produces "_ClassName__context" (leading underscores of the class name stripped), so a user input named "_user__context" still matched, and so did a name that is merely another class' mangled form -- both were silently overwritten while the real parameter stayed unbound. Compute the single candidate name instead of scanning the signature: take the class from __qualname__, which records the class whose body defines the method, so an inherited run() correctly keeps the base class' name. When the parameter kept its literal name the method was not defined in a class body, and no remapping is needed. Covers __tracer by the same path, leaves function-based actions untouched, and pins the injected-value precedence when a caller passes the mangled name.
|
Hi maintainers, Could someone with repository permissions please approve and run the two workflows currently gated for this fork PR?
Both runs are currently marked Separately, the documentation workflow for this PR ended with The same workflow has also ended with Neither documentation run has any jobs, so this appears to be a workflow-startup issue rather than a test failure introduced by this PR. Could someone with the necessary permissions please investigate the workflow startup failure and let me know whether any action is needed from my side? The PR description records the local validation results and their limitations separately. I am not treating any of these hosted checks as passing. Thank you! |
Summary
Class-based actions that follow the documented pattern for reading
app_id/partition_key/sequence_id— declaring__contextininputsand in the run method(
docs/concepts/actions.rst, "add__contextto theinputsproperty as well as the run method") —fail at runtime on every single-step action:
This affects
SingleStepAction,SingleStepStreamingAction, and therefore every@action-decoratedfunction, across
step(),astep(),stream_result()andastream_result().Root Cause
Python name-mangles a parameter written
__contextinside a class body to_MyClass__context. Theframework injects the dependency under the literal key
__context, so the call site has to rename it._remap_injected_inputsdoes exactly that and is already applied in four runners (_run_function,_arun_function,_run_multi_step_streaming_action,_arun_multi_step_streaming_action). The foursingle-step runners did not call it at all — each carried the note
# TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).Measured on current
main, one action per runner, all in the documented shape:main_run_function/_arun_function(remapped)step()/astep()_run_multi_step_streaming_action/_arun_…(remapped)stream_result()/astream_result()_run_single_step_actionstep()_arun_single_step_actionastep()_run_single_step_streaming_actionstream_result()_arun_single_step_streaming_actionastream_result()A second problem surfaced in
_remap_dunder_parameterswhile wiring this up. It selected the injectedparameter by scanning the signature for any name ending with the dunder and taking the first match.
A class-based action that also declares a regular input ending with that suffix therefore gets its own
parameter treated as the injected one, silently:
That is reachable on
maintoday through the already-remapped runners, and applying the remap to fourmore runners would have widened it, so it is fixed in the same change.
Changes
burr/core/application.py_run_single_step_action,_arun_single_step_action,_run_single_step_streaming_action,_arun_single_step_streaming_actionnow call_remap_injected_inputs(...)on the method they areabout to invoke, replacing the four TODOs. It is placed after
validate_inputs, which must seethe literal
__contextname the action declared._remap_dunder_parametersno longer scans the signature. It derives the single candidate name theway CPython does —
_+ the defining class name with leading underscores stripped + the dunder —and uses it only if that parameter actually exists. The defining class comes from
__qualname__,not
type(self), so an inheritedrun()correctly keeps the base class' mangled name. When theparameter kept its literal name the method was not defined in a class body, so nothing is renamed.
__tracergoes through the same helper and the samevars_to_remaplist, so it is covered by thesame change.
No public API, signature, or configuration changes.
Tests
12 new tests in
tests/core/test_application.py, reusing the existing_build_context_apphelper andthe style of the three class-based context tests already present:
step()on the multi-step path (that one passeson
maintoo — it closes the remaining coverage hole among the working runners and shows the changedoes not alter behaviour where behaviour was already right);
user__context,_user__context, another class' mangledname, a class name with leading underscores, an inherited method,
__tracer, and the precedence whena caller passes the mangled name explicitly;
_user__contextinput unchanged;@action(..., __context)action still binds the literal name.Local results (Windows, repo-pinned toolchain — these are my runs, not GitHub Actions):
main(3f16c1e6)pytest tests/core/test_application.pypytest tests/core tests/trackingtests/core/test_graphviz_display.pyfails 9 / passes 3 on bothmainand this branch here — nographviz binary in my environment — and is excluded from the numbers above.
The 7 that fail on
mainare the four single-step delivery tests, the two clobber unit tests, and theend-to-end clobber test — i.e. every behaviour this PR changes has a test that is red before and green
after. The other 5 pass on
mainby design: they pin behaviour that was already correct and must notbreak (the working multi-step delivery path, the class-name leading-underscore rule, inherited methods
resolving to the defining class, injected-value precedence, and function-based actions). I am stating
that explicitly rather than reporting 12 red tests.
Mutation validation. Removing the added
_remap_injected_inputs(...)call from each runner in turnmakes exactly that runner's test fail and no other — 4/4 killed, source byte-restored. Reverting the
mangling derivation to the old
endswithscan turns the two clobber tests red. So the tests distinguishthe defective behaviour from the corrected one rather than merely asserting success.
Formatting: repo-pinned
black 23.11.0 --line-length=100,flake8 6.1.0andpython scripts/check_asf_headers.pyare clean on the changed files.isort 5.12.0 --profile blackflags one pre-existing file I did not touch (
tests/integrations/test_burr_hamilton.py), identically onmain.Hosted CI: not yet run for this head at the time of writing; I will report back if anything comes
back red.
Compatibility
TypeError) now work — that is the fix.__contextparameter is not mangled, so theremap is a no-op. This is asserted by a test, and by
tests/corestaying green.pass unchanged.
overwrite of e.g.
_user__contextbecomes correct binding. Any program that depended on thatoverwrite was already losing the injected context, so nothing usable changes.
Limitations
S3TrackingClientand the OpenTelemetry integration are not exercised here; their extras(
boto3,opentelemetry-sdk) are not installed in my environment. They are affected only insofar asthey receive injected inputs through the same runners.
tests/directory cannot be collected locally without the optional integrations(
openai,pymongo,redis,pandas,ray,opentelemetry,langchain*,asyncpg,haystack,hamilton), so I rantests/core+tests/tracking, which is the subset requiring no extras.the result is no remapping (a visible
TypeError), not a silent mis-binding.Historical Context
#439 ("
app.astepdoes not work with declared context as input") reported the same symptom for afunction-based action and was closed as completed in 2024-11-30. That case works and is covered by
existing tests. It did not cover the class-based single-step runners — the four sites that carried the
TODOs are still failing on
maintoday, which is what this PR addresses.AI Disclosure
I used AI assistance (an agent running in the Qoder Desktop IDE) for parts of this work: building the
reproduction grid across action flavours, cross-checking the mangling rules against CPython's actual
behaviour, drafting and running the mutation checks, and wording assistance on this description. The
defect analysis, all test runs, and the final code were verified by me locally — the numbers above come
from runs I performed, and the code change is small enough to review line by line. Happy to answer
anything the diff does not make self-evident.
Checklist
documented behaviour is now the actual behaviour