Skip to content

Fix function introspection on Python 3.14 with deferred annotations (PEP 649)#323

Open
kozlek wants to merge 2 commits into
lundberg:masterfrom
kozlek:fix/pep649-deferred-annotations
Open

Fix function introspection on Python 3.14 with deferred annotations (PEP 649)#323
kozlek wants to merge 2 commits into
lundberg:masterfrom
kozlek:fix/pep649-deferred-annotations

Conversation

@kozlek

@kozlek kozlek commented Jun 23, 2026

Copy link
Copy Markdown

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 as respx_mock and route
but getfullargspec forces annotation evaluation, so a parameter annotated with a
TYPE_CHECKING-only name raises NameError, re-raised as
TypeError: unsupported callable (see #322 for a full repro/traceback).

This replaces getfullargspec with a small helper, respx.utils.get_arg_spec(), used
at 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_spec derives those from inspect.signature():

  • On Python 3.14+ it passes annotation_format=annotationlib.Format.FORWARDREF, so
    unresolved (TYPE_CHECKING-only) names degrade to ForwardRef placeholders instead
    of raising.
  • On older Pythons it falls back to plain inspect.signature(), which yields
    identical args/defaults. (annotationlib and the annotation_format argument are
    3.14+, so they're version-guarded.)

Bonus: detect parameters through wrapping decorators

inspect.getfullargspec ignored __wrapped__, whereas inspect.signature follows it by
default — so as a side benefit, respx now detects respx_mock / route declared on
functions wrapped with functools.wraps (e.g. @respx.mock stacked on top of another
decorator). Previously the wrapper's (*args, **kwargs) signature hid the parameter, so
respx didn't inject it and the call failed with a missing-argument TypeError. This is
also consistent with how respx itself uses functools.wraps / update_wrapper on its
own decorators.

The two changes are in separate commits (the crash fix, then the wraps behavior),
so they can be reviewed or taken independently.

Compatibility

No public API change. Supported Python versions (3.8–3.14) are unaffected; the
annotationlib path is guarded by sys.version_info >= (3, 14), and mypy
(python_version = 3.10) passes clean.

Tests

  • Regression test decorating a function with a TYPE_CHECKING-only-typed parameter
    (skipped on < 3.14, where the scenario can't occur).
  • Unit tests for get_arg_spec (args/defaults extraction and __wrapped__ following).
  • End-to-end test that respx_mock is injected through a wrapping decorator.
  • Full suite: 326 passed, 100% coverage; mypy clean.

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.

Python 3.14: inspect.getfullargspec raises TypeError: unsupported callable for functions with TYPE_CHECKING only annotations (PEP 649)

1 participant