fix: preserve introspection metadata + coroutine-function detection (#43, #44)#81
Merged
Conversation
…#43, #44) #43: sync decorated functions returned the raw Rust object with no metadata — __name__/__qualname__/__module__/__doc__/__wrapped__ missing and inspect.signature collapsing to (*args, **kwargs), breaking logging, doc tools, and signature-introspecting frameworks (FastAPI, click). Only the async wrapper set these, so sync and async were inconsistent. Give CachedFunction and SharedCachedFunction a __dict__ (`#[pyclass(frozen, dict)]`) and apply functools.wraps(fn) to the returned object in the sync path. The Rust object is still returned directly, so __call__ stays a single FFI crossing (no Python wrapper on the hot path); the __dict__ is touched only at decoration time. inspect.signature now resolves via __wrapped__. #44: an async decorated function (AsyncCachedFunction instance with an `async def __call__`) was not detected by inspect/asyncio.iscoroutinefunction, so frameworks branching on it (FastAPI/Starlette, anyio, pytest-asyncio) treated it as sync and dropped the returned coroutine. Mark the wrapper in __init__: inspect.markcoroutinefunction on 3.12+, else the asyncio.coroutines._is_coroutine sentinel on 3.10/3.11. Tests (tests/test_introspection.py): sync (memory + shared) exposes name/qualname/module/doc/__wrapped__ and a resolvable signature and still caches; async keeps name/doc/signature and is detected as a coroutine function (verified across 3.10–3.13, covering both the markcoroutinefunction and sentinel paths). Verified fail-before -> pass-after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared backend is Unix-only (lib.rs gates it behind not(windows); on Windows SharedCachedFunction is a stub whose __new__ rejects ttl). CI ignores the shared-backend test files on Windows, but the new test_introspection.py isn't in that ignore list, so test_sync_preserves_introspection[shared] ran on Windows and failed with "unexpected keyword argument 'ttl'". Mark the shared param skipif win32, matching the convention in the other shared tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #43
Closes #44
Two related introspection bugs in the decorator, fixed together.
#43 — sync functions lose all introspection metadata
The sync path returned the raw Rust
CachedFunction/SharedCachedFunctiondirectly, which copied no metadata:__name__/__qualname__/__module__/__doc__/__wrapped__were missing andinspect.signature()collapsed to(*args, **kwargs). This breaks logging, documentation tools, and signature-introspecting frameworks (FastAPI, click) — and was inconsistent with the async wrapper, which already set these.Fix: give both pyclasses a
__dict__(#[pyclass(frozen, dict)]) and applyfunctools.wraps(fn)to the returned object in the sync path. The Rust object is still returned directly, so__call__stays a single FFI crossing (no Python wrapper on the hot path) — the__dict__is touched only at decoration time.inspect.signatureresolves through__wrapped__.#44 — async function not detected as a coroutine function
AsyncCachedFunctionis a plain class with anasync def __call__, but the instance wasn't flagged, soinspect.iscoroutinefunction/asyncio.iscoroutinefunctionreturnedFalsewhile calling it returned a coroutine. Frameworks branching oniscoroutinefunction(FastAPI/Starlette, anyio, pytest-asyncio) treated it as sync and dropped the returned coroutine ("coroutine was never awaited").Fix: mark the wrapper in
__init__—inspect.markcoroutinefunction(self)on Python 3.12+, else set theasyncio.coroutines._is_coroutinesentinel on 3.10/3.11 (the only pre-3.12 mechanismasyncio.iscoroutinefunctionrecognizes).Test —
tests/test_introspection.py__name__/__qualname__/__module__/__doc__/__wrapped__correct,inspect.signature==(a, b=2), and caching still works after wrapping.Verified fail-before → pass-after by stashing the fixes: the three targeted tests failed (missing metadata /
iscoroutinefunctionFalse); the async-introspection test already passed pre-fix (that path always set its dunders). The coroutine-detection test passes on 3.10–3.13, exercising both themarkcoroutinefunction(3.12+) and sentinel (3.10/3.11) paths.Gates run (risky — PyO3 boundary
#[pyclass]change; #44 is Python-version-dependent)make fmt/make lint(ruff, ty, clippy-D warnings) ✓make test—cargo test(11) + pytest (106, +4 new) ✓make test-matrix— Python 3.10–3.13 ✓ (3.14 skipped locally via the documenteduv-resolves-stale-alpha guard; CI covers 3.14 final)make bench— no regression: thedictflag doesn't touch__call__; memory ~18.2M ops/s, single-thread 17.5–21.8MNo public API surface change (same decorator/methods), so no README/docs updates.
🤖 Generated with Claude Code