Opened 6 hours ago
#36696 new Uncategorized
django.utils.inspect leads to NameError with Python 3.14
| Reported by: | Patrick Rauscher | Owned by: | |
|---|---|---|---|
| Component: | Utilities | Version: | 5.2 |
| Severity: | Normal | Keywords: | typing, inspect |
| Cc: | Patrick Rauscher | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
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:
- One can add from future import annotations to switch back to Stringified annotations. However, this is called to be deprecated in the future
- One can switch to always include HttpRequest. This would work, but code linters will fight this, argueing that HttpRequest is only used as an type annotation here (which is correct)
- 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.