Fix function introspection on Python 3.14 with deferred annotations (PEP 649)#323
Open
kozlek wants to merge 2 commits into
Open
Fix function introspection on Python 3.14 with deferred annotations (PEP 649)#323kozlek wants to merge 2 commits into
kozlek wants to merge 2 commits into
Conversation
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 #322.
Summary
On Python 3.14, PEP 649 makes annotations
evaluated lazily. respx introspects decorated callables (and side effects) with
inspect.getfullargspec(...)to detect parameters such asrespx_mockandroute—but
getfullargspecforces annotation evaluation, so a parameter annotated with aTYPE_CHECKING-only name raisesNameError, re-raised asTypeError: unsupported callable(see #322 for a full repro/traceback).This replaces
getfullargspecwith a small helper,respx.utils.get_arg_spec(), usedat all three call sites (
router.py,models.py,mocks.py).How
respx only needs the positional parameter names and their defaults — never the
annotation values.
get_arg_specderives those frominspect.signature():annotation_format=annotationlib.Format.FORWARDREF, sounresolved (
TYPE_CHECKING-only) names degrade toForwardRefplaceholders insteadof raising.
inspect.signature(), which yieldsidentical
args/defaults. (annotationliband theannotation_formatargument are3.14+, so they're version-guarded.)
Bonus: detect parameters through wrapping decorators
inspect.getfullargspecignored__wrapped__, whereasinspect.signaturefollows it bydefault — so as a side benefit, respx now detects
respx_mock/routedeclared onfunctions wrapped with
functools.wraps(e.g.@respx.mockstacked on top of anotherdecorator). Previously the wrapper's
(*args, **kwargs)signature hid the parameter, sorespx didn't inject it and the call failed with a missing-argument
TypeError. This isalso consistent with how respx itself uses
functools.wraps/update_wrapperon itsown decorators.
The two changes are in separate commits (the crash fix, then the
wrapsbehavior),so they can be reviewed or taken independently.
Compatibility
No public API change. Supported Python versions (3.8–3.14) are unaffected; the
annotationlibpath is guarded bysys.version_info >= (3, 14), and mypy(
python_version = 3.10) passes clean.Tests
TYPE_CHECKING-only-typed parameter(skipped on < 3.14, where the scenario can't occur).
get_arg_spec(args/defaults extraction and__wrapped__following).respx_mockis injected through a wrapping decorator.