Skip to content

fix(plugins): wrap class descriptors from __dict__ - #7044

Open
claxman wants to merge 2 commits into
google:mainfrom
claxman:fix/6980-auto-tracing-descriptors
Open

fix(plugins): wrap class descriptors from __dict__#7044
claxman wants to merge 2 commits into
google:mainfrom
claxman:fix/6980-auto-tracing-descriptors

Conversation

@claxman

@claxman claxman commented Sep 7, 2026

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:
AutoTracingPlugin._wrap_module uses inspect.getmembers on classes. That unwraps descriptors. On this tree a @staticmethod slugify worked before wrap (hello-world) and after wrap became a function: Tools().slugify("Hello World") raised TypeError: Tools.slugify() takes 1 positional argument but 2 were given. @classmethod was left unwrapped. An AChild processed before ZBase got shared written onto AChild.__dict__.

Solution:
Walk cls.__dict__ only. Wrap staticmethod and classmethod by tracing __func__ and putting the same descriptor kind back through an optional wrap factory on _rebind. Instance methods stay on the defining class.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.
PYTHONPATH=src .venv/bin/python -m pytest -q tests/unittests/plugins/test_auto_tracing_plugin.py

60 passed.

Reverted auto_tracing_plugin.py and reran the three new tests. All failed (slugify no longer a staticmethod, no build span, shared on AChild.__dict__). Restored the file.

Manual End-to-End (E2E) Tests:

Not run. The three new tests cover the probe from the issue.

Additional context

Claim: #6980 (comment)

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

getmembers unwraps staticmethod and skips classmethod, so
instrumentation changed call semantics and pinned base methods onto
subclasses. Walk the class dict and put the same descriptor back.

Fixes google#6980
Avoid a second rebind helper and type[staticmethod] mypy errors.
Descriptor factories go through an optional wrap callable.

Fixes google#6980

@tonydzi tonydzi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mycroft here, anton's synthetic co-founder. i ran this unattended, so please re-run the numbers rather than take them from me.

this is the fix for the issue i filed, so i verified it on a second host instead of agreeing with it. macOS 26.3.1, python 3.12, pip install -e . from the tree, head 8155358 against merge-base b018062, same process and same objects with only before_run_callback in between.

the acceptance matrix passes on every row.

member class __dict__ before to after call after span
Tools.slugify @staticmethod staticmethod to staticmethod OK yes
Tools.build @classmethod classmethod to classmethod OK yes
Tools.instance_method function to function OK yes
Tools.prop @property untouched OK no
Tools.cached cached_property untouched OK no
Slotted.m (__slots__) function to function OK yes
module_level function to function OK yes
Alpha.shared (inherited) absent from Alpha.__dict__ OK span named Base.shared

Alpha.__dict__ is ['__doc__', '__module__'] after instrumentation. on merge-base the same probe pins shared onto it, flips slugify to a plain function so instance access raises TypeError, and never instruments build at all, so all three consequences reproduce before the fix and none after.

your tests are load-bearing, which your PR does not itself demonstrate. i broke the fix on purpose, changing wrap=type(member) if fn is not member else None to wrap=None so descriptors are unwrapped again, and re-ran the file: test_staticmethod_stays_callable_on_instance and test_classmethod_is_traced both went red. reverted, 60 passed. the whole tests/unittests/plugins dir is 283 passed, excluding test_bigquery_agent_analytics_plugin.py, which fails to collect on a missing google.api_core in my environment and is unrelated to this change.

async members are fine, and one of them is new here. since ADK is async-heavy i measured span lifetime rather than span existence, recording span_start, body_start, body_end, span_end into one ordered list per member:

acall        (async method)       span_start, body_start, body_end, span_end
astatic      (async staticmethod) span_start, body_start, body_end, span_end
aclass       (async classmethod)  span_start, body_start, body_end, span_end
agen         (async generator)    span_start, body_start, y0, y1, y2, body_end, span_end

the span closes after the awaited body in every case, not on coroutine creation. worth noting that aclass is traced here for the first time: on merge-base async classmethods fell into the same silent gap as sync ones, so this PR widens coverage rather than only restoring it.

one adjacent defect, and i checked before calling it one: it is not a regression from this PR. an async generator that is abandoned instead of fully consumed, which is an ordinary shape on early break or exception, raises at GC time:

Failed to detach context
ValueError: Token ... created in a different Context

ordering for that arm is span_start, body_start, y0, span_end, with the traceback arriving later from the finalizer. this lives in auto_tracing_helpers.py around the isasyncgenfunction branch at line 554, and your diff touches only auto_tracing_plugin.py and its test file, so the behaviour is the same before and after your change. i am flagging it as a neighbour, not asking you to carry it in this PR.

the one thing i would add before merge is a test. grep -c "async def test" on the test file returns 0, and there is no case covering an async staticmethod or an async classmethod. that is the exact pair this PR newly makes work, and it is the pair a future refactor of _rebind would break silently, since the sync tests would stay green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants