Opened 4 weeks ago

Closed 4 weeks ago

Last modified 4 weeks ago

#36696 closed Bug (fixed)

Using deferred annotations on signals, tasks, etc. in Python 3.14 raises NameError

Reported by: Patrick Rauscher Owned by: Patrick Rauscher
Component: Utilities Version: 5.2
Severity: Release blocker Keywords: typing, inspect, deferred annotations
Cc: Patrick Rauscher Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jacob Walls)

Starting with Python 3.14, deferred evaluation of annotations (PEP-649) is default now. This would suggest that using annotations in templatetags or signals should be as easy as:

from typing import TYPE_CHECKING
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver

if TYPE_CHECKING:
    from django.http import HttpRequest

@receiver(user_logged_in)
def handle_successful_login(request: HttpRequest, *args, **kwargs) -> None:
    print("Someone logged in successful - this would contain useful code")

Weirdly, as long as DEBUG is set to False, this code will run without problems. However, if DEBUG is turned to True, Signal.connect will use func_accepts_kwargs from django.utils.inspect to check if handle_successful_login accepts kwargs. func_accepts_kwargs will ultimatively use inspect.signature on the function, which triggers evaluation of the annotations and leads to NameError, as TYPE_CHECKING is False here.

This can be resolved in various ways:

  1. One can add from future import annotations to switch back to Stringified annotations. However, this is called to be deprecated in the future
  2. One can switch to always import HttpRequest. This would work, but code linters will fight this, argueing that HttpRequest is only used as an type annotation here (which is correct)
  3. As django.utils.inspect does not actually care for the annotations, inspect.inspect(..., annotation_format=annotationlib.Format.STRING) can be used (sadly this works only in Python 3.14).

My suggestion would be to something around

@functools.lru_cache(maxsize=512)
def _get_func_parameters(func, remove_first):
    if sys.version_info[0:2] >= (3, 14):
        import annotationlib
        signature = inspect.signature(func, annotation_format=annotationlib.Format.STRING)
    else:
        signature = inspect.signature(func)
    parameters = tuple(signature.parameters.values())
    if remove_first:
        parameters = parameters[1:]
    return parameters

But I agree that it might not be the nicest solution.

Change History (8)

comment:1 by Jacob Walls, 4 weeks ago

Description: modified (diff)
Keywords: deferred annotations added
Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted
Type: UncategorizedBug

Thanks for the report. I might opt for FORWARDREF instead of STRING, but otherwise I agree.

Would you like to submit a PR targeting main?

I think this should be backported as part of Python 3.14 support, as otherwise you need to know to watch out for this gotcha when using a major 3.14 language feature.

comment:2 by Jacob Walls, 4 weeks ago

Summary: django.utils.inspect leads to NameError with Python 3.14Using deferred annotations on signals, tasks, etc. in Python 3.14 raises NameError

comment:3 by Patrick Rauscher, 4 weeks ago

Owner: set to Patrick Rauscher
Status: newassigned

I will try and draft a PR :)

comment:4 by Patrick Rauscher, 4 weeks ago

Has patch: set

Drafted a PR in GitHub, still waiting for the test suite to start up and confirm.

comment:5 by Jacob Walls, 4 weeks ago

Triage Stage: AcceptedReady for checkin

comment:6 by Jacob Walls <jacobtylerwalls@…>, 4 weeks ago

Resolution: fixed
Status: assignedclosed

In 6019147:

Fixed #36696 -- Fixed NameError when inspecting functions with deferred annotations.

In Python 3.14, annotations are deferred by default, so we should not
assume that the names in them have been imported unconditionally.

comment:7 by Jacob Walls <jacobtylerwalls@…>, 4 weeks ago

In 953d310:

[6.0.x] Fixed #36696 -- Fixed NameError when inspecting functions with deferred annotations.

In Python 3.14, annotations are deferred by default, so we should not
assume that the names in them have been imported unconditionally.

Backport of 601914722956cc41f1f2c53972d669ddee6ffc04 from main.

comment:8 by Jacob Walls <jacobtylerwalls@…>, 4 weeks ago

In 6775888:

[5.2.x] Fixed #36696 -- Fixed NameError when inspecting functions with deferred annotations.

In Python 3.14, annotations are deferred by default, so we should not
assume that the names in them have been imported unconditionally.

Note: See TracTickets for help on using tickets.
Back to Top