fix(dynamic-instrumentation): bind FastAPI sub-dependencies, Starlette/aiohttp & functools.partial handlers, and surface non-bindable line breakpoints#787
Conversation
Claude Code Review
|
…handlers (Starlette/aiohttp) and functools.partial targets
Two related silent-failure fixes in the DI function wrapper. In both, a
function-level breakpoint reported READY but never fired because the
wrapper/patcher never reached the live callable.
Unsupported-framework route handlers (Starlette, aiohttp)
_patch_framework_references dispatched to only Flask/Django/FastAPI, so a
route handler on any other web framework was never rebound and the breakpoint
silently never fired. Adds two additive patchers:
* _patch_starlette_routes: pure-Starlette apps build
`route.app = request_response(endpoint)` at import time (the per-request
path invokes route.app, not route.endpoint), so the handler is captured in
a closure that setattr/endpoint-rebind cannot reach. The patcher rebuilds
route.app from the wrapper. FastAPI subclasses Starlette and is handled by
_patch_fastapi_routes, so this patcher explicitly SKIPS FastAPI instances
(no double-patch). Routes carrying per-route middleware are left untouched
(rebuilding route.app would drop the middleware) -- a safe no-op.
* _patch_aiohttp_routes: rebinds the stored handler on aiohttp routes.
Both match by identity then name+module (mirroring the existing patchers) and
run in their own try/except in the dispatcher so a failure is isolated.
functools.partial (and other callables whose runtime name != configured name)
The wrapper computed its _active_functions lookup key from the target's runtime
__qualname__/__name__. A functools.partial has neither, so the key resolved to
"<module>.<anonymous>" and missed the set the manager registered under the
CONFIGURED key "<module>.<function_name>" -> the wrapper silently never fired.
The fix threads the configured function_name into the wrapper and keys off it
(falling back to runtime introspection only when not provided), aligning the
wrapper's lookup with the manager's registration key. Generalizes to any
callable whose runtime name differs from the configured name.
Tests
* Unit: new test_function_wrapper_starlette.py (5) and
test_function_wrapper_aiohttp.py (4) cover identity match, name+module
fallback, the FastAPI-skip guard, the middleware safe-skip, and no-op safety;
new partial regression test in test_function_wrapper.py. All verified failing
without the fix.
* Updated existing wrapper tests that registered breakpoint sets under the
runtime __qualname__ to use the configured key the manager actually uses
(module.function_name) -- these previously only coincidentally matched.
* Contract: new di-starlette app + starlette_test.py prove a Starlette route
handler fires end-to-end; di-fastapi gains a functools.partial target + test.
Full debugger unit suite: 557 passed, 43 skipped. black/isort/flake8 clean;
pylint 10.00/10 on the source.
No change to the Flask/Django/FastAPI patch paths, so existing behavior is preserved.
903db23 to
af3b772
Compare
|
Claude finished @syed-ahsan-ishtiaque's task in 15m 45s —— View job Claude Code Review
The two fixes are well-scoped and the regression-guard tests are genuine. A few substantive concerns I'd ask the author to address before merge — none are blockers but a couple have non-trivial silent-failure risk that's worth pinning. 1. Starlette
|
…per review
Addresses automated review feedback on the framework-dispatch patchers.
* Starlette plain-app detection is now structural instead of name-based.
Previously it recognized the "plain request_response closure" by checking
current_app.__qualname__.startswith("request_response"); a future Starlette
rename would misclassify every plain route as middleware-wrapped and silently
skip it, re-introducing the never-fires bug. New _starlette_app_wraps_endpoint
helper inspects route.app's closure cells for the route's live function
(captured directly for async endpoints, inside a functools.partial for sync
endpoints wrapped via run_in_threadpool). Accepts either the matched endpoint
or original_func to keep the name+module fallback path working. The skip log
is upgraded from debug to warning so a future internals change is observable
rather than silent.
* aiohttp: warn (instead of silently no-op'ing) when a route matches by the
public route.handler property but has no writable _handler attribute, so a
future aiohttp rename surfaces. Added a comment explaining the read-public /
write-private asymmetry and reconciled the docstring.
* di-fastapi contract app: moved `import functools` to the top import block and
dropped the `# noqa: E402`.
* Tests: new test_starlette_app_wraps_endpoint_detection covers async/sync/
middleware/non-closure inputs; the middleware-skip test now asserts the route
IS matched (pinning "matched-but-middleware -> safe skip" rather than passing
via no-match).
Full debugger unit suite: 558 passed, 43 skipped. black/isort/flake8 clean;
pylint 10.00/10. Async and sync Starlette handlers re-verified firing in-process.
|
Claude finished @syed-ahsan-ishtiaque's task in 7m 43s —— View job Claude Code Review
The follow-up commit ( Verified addressed from the prior review
Observations (not blocking, not requiring changes)
Architecture (per-framework patchers in their own |
…rgets without a code object A line-level breakpoint whose target resolves to a callable with no __code__ object (for example a callable-class instance or a functools.partial used as the handler) cannot arm the line engine. Previously this was skipped silently and the configuration still reported READY, which is indistinguishable from "applied, waiting for traffic" — the breakpoint could never fire. The manager now reports a non-silent LINE_NOT_EXECUTABLE error for the line-level configuration(s) in that case and records them as failed. The per-config READY report in apply_configuration and the periodic status sweep both skip configurations already marked failed, so the ERROR status is not subsequently overwritten by a misleading READY. Tests: added a unit test for the codeless-target branch, an end-to-end test through apply_configuration asserting READY does not overwrite the ERROR, and a status-reporter test asserting the periodic sweep suppresses READY for a failed config.
|
Claude finished @syed-ahsan-ishtiaque's task in 6m 31s —— View job Claude Code Review
The new commit ( Verified correctness
TestsThe three new tests cover the right contract surface:
Minor (non-blocking) observation
Architecture remains consistent (per-framework patchers in their own try/except, structural detection for Starlette plain-app, WARN logs on observability-critical skips, identity-then-name+module fallback). Nothing further from me. |
…on to Dict[str, ErrorCause]
|
Claude finished @syed-ahsan-ishtiaque's task in 50s —— View job Claude Code Review
The new commit ( Nothing further from me. Approving in principle — all prior review feedback (Starlette structural detection, WARN logs, aiohttp asymmetry comment, middleware-skip test pinning, |

Summary
Four independent Dynamic Instrumentation correctness fixes in the Python distro's debugger. Each closes a silent failure: a breakpoint that reported
READY(or a status that looked healthy) but never actually fired, because the wrapper/patcher never reached the live callable — or, in the last case, a target that can never bind yet never surfaced an error. They are bundled here because they touch the same subsystem (_function_wrapper.pydispatch + the manager/status-reporter status path); each is additive and leaves the existing Flask/Django/FastAPI patch paths intact.1. FastAPI
Depends()sub-dependency handlers silently never fireA function used as a FastAPI sub-dependency (
Depends(get_thing)) is captured at import time inside the route's pre-builtDependanttree (dependant.dependencies[*].call). The per-request path invokes thosecallreferences, not the module-level name, so a function-level breakpoint on a sub-dependency reportedREADYbut never fired (line-level worked).Fix:
_rebind_dependantwalks theDependanttree and_rebind_routerebinds every matchingcall(and the route's ownendpoint/dependant.call) to the instrumented wrapper. Matching is identity-then-name+module (_matches/_is_identity), mirroring the existing patchers.2. Route handlers of unsupported frameworks (Starlette, aiohttp) silently never fire
_patch_framework_referencesdispatched to only Flask, Django, and FastAPI. A handler on any other framework was never rebound, so a function-level breakpoint on it silently never fired — reproduced on Starlette and aiohttp._patch_starlette_routes— a pure-Starlette app buildsroute.app = request_response(endpoint)at import time and the per-request path callsroute.app; the original handler lives in a closure thatsetattr/endpoint-rebind cannot reach. The patcher rebuildsroute.appfrom the wrapper._starlette_app_wraps_endpointinspects the closure cells for the captured endpoint, handling both the async direct-capture and the syncfunctools.partial(run_in_threadpool, endpoint)forms) rather than matching on an internal function name, so a future Starlette rename won't silently misclassify routes.route.appwould drop the middleware) — a deliberate, logged safe no-op.Mount/sub-routers are descended (_walk, depth-capped)._patch_aiohttp_routes— matches on the publicroute.handlerand rebinds the backing_handler(no public setter), warning if the internal attribute is ever absent.3.
functools.partial(any callable whose runtime name ≠ configured name) silently never firesThe wrapper computed its
_active_functionslookup key from the target's runtime__qualname__/__name__. Afunctools.partialhas neither, so the key became"<module>.<anonymous>"and missed the set the manager registered under the configured key"<module>.<function_name>". The fix threads the configuredfunction_nameinto the wrapper and keys off it (falling back to runtime introspection only when not provided), aligning the wrapper's lookup with the manager's registration key.4. Line breakpoint on a target with no code object reported READY instead of ERROR
A line-level breakpoint whose target resolves to a callable with no
__code__(a callable-class instance, or afunctools.partial) cannot arm the line engine. Previously this was skipped silently and the config still reportedREADY— indistinguishable from "applied, waiting for traffic", though it could never fire.Fix: the manager now reports a non-silent
LINE_NOT_EXECUTABLEerror for the line-level config(s) and records them as failed. The per-configREADYreport inapply_configurationand the periodic status sweep both skip already-failed configs, so theERRORis not overwritten by a laterREADY.Testing
test_function_wrapper_starlette.pyandtest_function_wrapper_aiohttp.py(identity match, name+module fallback, the FastAPI-skip guard, the middleware safe-skip, structural-detection contract); newfunctools.partialtest intest_function_wrapper.py; FastAPI sub-dependency rebind tests intest_function_wrapper_fastapi.py; codeless-line tests intest_instrumentation_manager.py(unit, plus an end-to-endapply_configurationtest assertingREADYdoes not overwriteERROR) andtest_status_reporter_logic.py(periodic sweep suppressesREADYfor a failed config). All are regression guards (verified failing without the fix). Updated pre-existing wrapper tests that keyed breakpoint sets by the runtime__qualname__to use the configured key the manager actually registers (module.function_name).di-starletteapp +starlette_test.pyprove a Starlette route handler fires end-to-end;di-fastapigains afunctools.partialtarget + test.black/isort/flake8clean;pylint10.00/10 on the source.No change to the existing Flask/Django/FastAPI-route patch paths (their tests are unaffected).
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.