Skip to content

fix(telemetry): preserve function signature in ApiTelemetry decorator#4932

Closed
Austin-s-h wants to merge 6 commits into
masterfrom
fix/telemetry-signature
Closed

fix(telemetry): preserve function signature in ApiTelemetry decorator#4932
Austin-s-h wants to merge 6 commits into
masterfrom
fix/telemetry-signature

Conversation

@Austin-s-h

@Austin-s-h Austin-s-h commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Description

The ApiTelemetry decorator wraps functions with functools.wraps but 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

  • Unit tests — tests/test_telemetry.py::TelemetryTest::test_preserves_signature
  • Security: no security impact (pure introspection metadata)

🤖 Generated with Claude Code

Greptile Summary

This PR fixes ApiTelemetry.__call__ to explicitly set decorated.__signature__ after functools.wraps, so inspect.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 @classmethod failure path.

  • telemetry.py: Assigns decorated.__signature__ = inspect.signature(func) inside a try/except (ValueError, TypeError) after the functools.wraps wrapper is built; the fix is minimal and side-effect-free.
  • test_telemetry.py: Adds test_preserves_signature that verifies the signature is preserved on a plain wrapped function, but omits the @classmethod chain 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 @classmethod failure case that motivated the change is not concretely guarded against regressions.

api/python/tests/test_telemetry.py — the new test should cover the @classmethod scenario to serve as a true regression guard for the reported breakage.

Important Files Changed

Filename Overview
api/python/quilt3/telemetry.py Adds explicit __signature__ assignment after functools.wraps in ApiTelemetry.__call__, wrapped in a defensive try/except to handle cases where inspect.signature cannot resolve the function.
api/python/tests/test_telemetry.py Adds test_preserves_signature which 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)"
Loading

Reviews (1): Last reviewed commit: "fix(telemetry): preserve function signat..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

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

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.53%. Comparing base (3988c54) to head (377224d).

Files with missing lines Patch % Lines
api/python/tests/test_telemetry.py 89.47% 2 Missing ⚠️
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              
Flag Coverage Δ
api-python 93.14% <91.66%> (-0.01%) ⬇️
catalog 21.55% <ø> (ø)
lambda 96.63% <ø> (ø)
py-shared 98.02% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread api/python/tests/test_telemetry.py
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>
@Austin-s-h

Copy link
Copy Markdown
Collaborator Author

@drernie ready

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) in ApiTelemetry.__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.

Comment on lines +52 to +63
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

drernie
drernie previously approved these changes Jun 2, 2026

@drernie drernie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Passes. Okay to merge.

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>
@drernie

drernie commented Jun 2, 2026

Copy link
Copy Markdown
Member

@Austin-s-h Looks good to me - thanks for doing this! I fixed one comment.
Hit 'Merge when ready' (our convention is only the author should merge). Note you can hit that ahead of time, and it will auto-merge when we approve.

@Austin-s-h Austin-s-h enabled auto-merge June 2, 2026 16:16

@sir-sigurd sir-sigurd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@Austin-s-h

Copy link
Copy Markdown
Collaborator Author

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

Known CI status

test-gendocs is red on this branch and that is expected. Bumping gendocs from Python 3.9 to >=3.11 surfaces a latent inspect.signature bug on classmethods wrapped by ApiTelemetry (<classmethod(...)> is not a callable object). The fix is #4932 (fix(telemetry): preserve function signature). This branch will go green on gendocs once #4932 merges to master and this branch is rebased — i.e. merge #4932 before #4933.

The local catalog work also surfaced this bug, this experimental feature I want more feedback on for sure. #4938

tl;dr
Dropping Python 3.9 as a gendocs requirement exposed a fragile API documentation surface that is addressed with these changes.

@Austin-s-h

Copy link
Copy Markdown
Collaborator Author

@sir-sigurd

Copy link
Copy Markdown
Member

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:

  • On refactor(packaging): single source of truth for lambda shared libraries (uv workspace) #4933 (Python ≥3.11), build.py fails with TypeError: <classmethod(...)> is not a callable object at pydocmd loader.py:138.
  • Running the build with this PR's fix applied verbatim fails identically — so the patch doesn't address the failure.
  • Root cause appears to be in the pydoc-markdown fork's loader: it fetches the raw classmethod via __dict__ (not getattr), and on Python ≥3.10 a classmethod object has __name__, so the loader skips the __get__() unwrap and calls inspect.signature() on a non-callable classmethod object — before the wrapped function's __signature__ is ever consulted.

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 requires-python to >=3.11 — so CI actually runs gendocs on 3.11+ and proves the fix (and sets up dropping old Python next). That leaves nothing for this PR, so I'd lean toward closing it, but I'll leave the call to you.

@Austin-s-h

Copy link
Copy Markdown
Collaborator Author

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:

  • The __signature__ assignment here is never consulted in the failing path. pydoc-markdown imports each object via cls.__dict__[name] (the raw classmethod descriptor) and calls inspect.signature() on that — not on the ApiTelemetry-wrapped function. So this change has no effect on gendocs.
  • The real root cause is in the docs tooling: on Python ≥3.10 a classmethod descriptor gained __name__, which sends quilt's pydoc-markdown fork down a branch that skips the descriptor unwrap and calls inspect.signature() on the non-callable descriptor → TypeError. It affects every @classmethod in the API, telemetry or not. It's dormant on master only because gendocs is still pinned to Python 3.9.

#4939 fixes it where it belongs (gendocs/build.py) and bumps gendocs to Python 3.11+ so CI actually exercises the path and proves the fix. No telemetry changes. The rest of my stack (#4933#4936#4937#4938) has been re-based onto #4939 and the __signature__ change dropped entirely.

@Austin-s-h Austin-s-h closed this Jun 2, 2026
auto-merge was automatically disabled June 2, 2026 17:43

Pull request was closed

@sir-sigurd sir-sigurd deleted the fix/telemetry-signature branch June 2, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants