fix(telemetry): preserve function signature in ApiTelemetry decorator#4932
fix(telemetry): preserve function signature in ApiTelemetry decorator#4932Austin-s-h wants to merge 6 commits into
Conversation
The ApiTelemetry decorator wrapped functions with functools.wraps but did not expose an explicit __signature__. As a result inspect.signature() fails to resolve the underlying signature through a @classmethod, which breaks pydoc-markdown doc generation. Set decorated.__signature__ from the wrapped function so introspection works, and add a focused regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4932 +/- ##
==========================================
+ Coverage 46.50% 46.53% +0.03%
==========================================
Files 832 832
Lines 34089 34113 +24
Branches 5833 5833
==========================================
+ Hits 15853 15875 +22
- Misses 16237 16239 +2
Partials 1999 1999
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The previous regression test only exercised the happy path of the __signature__ assignment, leaving the except branch uncovered and failing codecov/patch. Add a test that patches inspect.signature to raise, verifying the decorator swallows the error and still returns a working callable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing test_preserves_signature passes even without the fix because inspect.signature() follows __wrapped__ by default (set by functools.wraps). Add a test that asserts signature resolution with follow_wrapped=False, which is the path that actually breaks without the explicit __signature__ assignment (and what pydoc-markdown's gendocs build hits for @classmethod-wrapped APIs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@drernie ready |
There was a problem hiding this comment.
Pull request overview
Updates the ApiTelemetry decorator to preserve the wrapped callable’s explicit signature (__signature__) so inspect.signature() can correctly introspect APIs even when they are additionally wrapped (notably via @classmethod), unblocking pydoc-markdown doc generation.
Changes:
- Set
decorated.__signature__ = inspect.signature(func)inApiTelemetry.__call__with a defensive fallback when the signature cannot be introspected. - Add unit tests asserting the decorator exposes the wrapped function’s signature and behaves safely when signature introspection fails.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| api/python/quilt3/telemetry.py | Explicitly assigns __signature__ to the decorator wrapper to support reliable introspection through additional wrappers like @classmethod. |
| api/python/tests/test_telemetry.py | Adds regression tests for signature preservation and for the “unintrospectable signature” fallback behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_preserves_signature(self): | ||
| """ | ||
| The decorator exposes the wrapped function's signature so that | ||
| inspect.signature() works (e.g. through @classmethod, as pydoc-markdown relies on). | ||
| """ | ||
|
|
||
| def func(a, b, c=1, *, d=2): | ||
| pass | ||
|
|
||
| decorated = ApiTelemetry(mock.sentinel.API_NAME)(func) | ||
| assert inspect.signature(decorated) == inspect.signature(func) | ||
|
|
There was a problem hiding this comment.
Thanks — you and greptile both correctly caught that the original test_preserves_signature was a tautology. But a @classmethod test wouldn't be the right replacement: accessing the bound method goes through the descriptor, which follows __wrapped__, so inspect.signature(C.method) returns the real params even without the fix (verified empirically). The behavior that actually changes is follow_wrapped=False — which is what test_signature_does_not_require_following_wrapped now guards, matching how pydoc-markdown resolves signatures during gendocs. I've also reworded the test_preserves_signature docstring to stop implying it covers the classmethod path.
It passes regardless of the fix (inspect.signature follows __wrapped__ by default), so point readers to test_signature_does_not_require_following_wrapped as the real regression guard rather than implying it covers @classmethod. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@Austin-s-h Looks good to me - thanks for doing this! I fixed one comment. |
sir-sigurd
left a comment
There was a problem hiding this comment.
Requesting changes on scope/clarity. I can't tell what this fixes, and it adds
code (a __signature__ assignment, a try/except, and a test) that future
maintainers will have to carry without knowing why.
It's claimed to fix gendocs, but everything works on master: test-gendocs is
green and the committed Package.md already shows the full
Package.install(name, registry=None, ...) signature — there's no breakage.
It's also claimed to "unblock a later local-catalog PR," but that PR isn't linked,
so the motivation is unverifiable — which makes "independently useful" hard to
square, since the stated value is being a prerequisite.
Could you (1) link the PR this unblocks and show what concretely breaks without
it, or (2) if there's no standalone justification, fold it into that PR? Happy to
approve once the purpose is clear.
Hi @sir-sigurd , thank you for the feedback! This is an independent change from the rest of my work that I discovered while upgrading gendocs from python 3.9 to 3.11. This can preceed the refactor for local-packaged python lambdas with more details in #4933
The local catalog work also surfaced this bug, this experimental feature I want more feedback on for sure. #4938 tl;dr |
|
Note: the reproduction and analysis below were generated by Claude (AI-assisted), so please sanity-check rather than take as authoritative — but the conclusion and scoping suggestion at the end are my own. Claude's findings:
My take: pydocmd is the right place to fix this (quiltdata/pydoc-markdown is Quilt-owned, so it can be patched and re-tagged in-house). The telemetry change doesn't address the failure, so I don't think it belongs here. I'd suggest two PRs: one in quiltdata/pydoc-markdown for the loader fix + new tag, and one here to bump gendocs to that tag and move its |
|
Closing in favor of #4939. @sir-sigurd you were right on both counts, thank you for pushing on this. I dug into the actual failure mechanism and confirmed your AI-assisted analysis:
#4939 fixes it where it belongs ( |
Description
The
ApiTelemetrydecorator wraps functions withfunctools.wrapsbut does not expose an explicit__signature__. As a result,inspect.signature()fails to resolve the underlying signature through a@classmethod, which breaks pydoc-markdown doc generation (gendocs/build.py).This sets
decorated.__signature__from the wrapped function so introspection works through@classmethod, and adds a focused regression test.This is the first of a series of small, atomic PRs carved out of a larger packaging/hygiene branch. It is dependency-free and independently useful (it also unblocks a later local-catalog PR).
TODO
tests/test_telemetry.py::TelemetryTest::test_preserves_signature🤖 Generated with Claude Code
Greptile Summary
This PR fixes
ApiTelemetry.__call__to explicitly setdecorated.__signature__afterfunctools.wraps, soinspect.signature()resolves the correct signature when the decorated function is further wrapped by@classmethod(as needed by pydoc-markdown). A regression test is included, though it does not exercise the specific@classmethodfailure path.telemetry.py: Assignsdecorated.__signature__ = inspect.signature(func)inside atry/except (ValueError, TypeError)after thefunctools.wrapswrapper is built; the fix is minimal and side-effect-free.test_telemetry.py: Addstest_preserves_signaturethat verifies the signature is preserved on a plain wrapped function, but omits the@classmethodchain that was the original failure scenario.Confidence Score: 4/5
The production change is a small, non-breaking addition with a silent fallback; safe to merge, but the test only partially validates the stated fix.
The explicit
__signature__assignment is the correct pattern and introduces no runtime risk. The test exercises a path that already worked via__wrapped__before the fix, so the@classmethodfailure case that motivated the change is not concretely guarded against regressions.api/python/tests/test_telemetry.py — the new test should cover the
@classmethodscenario to serve as a true regression guard for the reported breakage.Important Files Changed
__signature__assignment afterfunctools.wrapsinApiTelemetry.__call__, wrapped in a defensive try/except to handle cases whereinspect.signaturecannot resolve the function.test_preserves_signaturewhich verifies the decorator preserves the wrapped function's signature on a plain function; the actual failure case (through@classmethod) is not covered by the new test.Sequence Diagram
sequenceDiagram participant Caller participant classmethod participant decorated participant func Note over Caller,func: Before fix — __signature__ not set Caller->>classmethod: inspect.signature(Foo.method) classmethod->>decorated: follow __wrapped__ decorated-->>Caller: ❌ ValueError (fails through classmethod) Note over Caller,func: After fix — __signature__ explicitly set Caller->>classmethod: inspect.signature(Foo.method) classmethod->>decorated: read __signature__ decorated-->>Caller: "✅ Signature(a, b, c=1, *, d=2)"Reviews (1): Last reviewed commit: "fix(telemetry): preserve function signat..." | Re-trigger Greptile