mirror of
https://github.com/django/django.git
synced 2026-02-09 02:49:25 +08:00
Fixed #36712 -- Evaluated type annotations lazily in template tag registration.
Ideally, this will be reverted when an upstream solution is available for https://github.com/python/cpython/issues/141560. Thanks Patrick Rauscher for the report and Augusto Pontes for the first iteration and test.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from inspect import getfullargspec
|
||||
|
||||
from django.template.library import InclusionNode, parse_bits
|
||||
from django.utils.inspect import lazy_annotations
|
||||
|
||||
|
||||
class InclusionAdminNode(InclusionNode):
|
||||
@@ -11,9 +12,10 @@ class InclusionAdminNode(InclusionNode):
|
||||
|
||||
def __init__(self, parser, token, func, template_name, takes_context=True):
|
||||
self.template_name = template_name
|
||||
params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(
|
||||
func
|
||||
)
|
||||
with lazy_annotations():
|
||||
params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = (
|
||||
getfullargspec(func)
|
||||
)
|
||||
bits = token.split_contents()
|
||||
args, kwargs = parse_bits(
|
||||
parser,
|
||||
|
||||
@@ -60,6 +60,7 @@ from django.template.context import BaseContext
|
||||
from django.utils.deprecation import django_file_prefixes
|
||||
from django.utils.formats import localize
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.inspect import lazy_annotations
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.safestring import SafeData, SafeString, mark_safe
|
||||
from django.utils.text import get_text_list, smart_split, unescape_string_literal
|
||||
@@ -825,7 +826,8 @@ class FilterExpression:
|
||||
# Check to see if a decorator is providing the real function.
|
||||
func = inspect.unwrap(func)
|
||||
|
||||
args, _, _, defaults, _, _, _ = inspect.getfullargspec(func)
|
||||
with lazy_annotations():
|
||||
args, _, _, defaults, _, _, _ = inspect.getfullargspec(func)
|
||||
alen = len(args)
|
||||
dlen = len(defaults or [])
|
||||
# Not enough OR Too many
|
||||
|
||||
@@ -4,6 +4,7 @@ from importlib import import_module
|
||||
from inspect import getfullargspec, unwrap
|
||||
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.inspect import lazy_annotations
|
||||
|
||||
from .base import Node, Template, token_kwargs
|
||||
from .exceptions import TemplateSyntaxError
|
||||
@@ -110,15 +111,16 @@ class Library:
|
||||
"""
|
||||
|
||||
def dec(func):
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
with lazy_annotations():
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
function_name = name or func.__name__
|
||||
|
||||
@wraps(func)
|
||||
@@ -165,16 +167,16 @@ class Library:
|
||||
|
||||
def dec(func):
|
||||
nonlocal end_name
|
||||
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
with lazy_annotations():
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
function_name = name or func.__name__
|
||||
|
||||
if end_name is None:
|
||||
@@ -249,15 +251,16 @@ class Library:
|
||||
"""
|
||||
|
||||
def dec(func):
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
with lazy_annotations():
|
||||
(
|
||||
params,
|
||||
varargs,
|
||||
varkw,
|
||||
defaults,
|
||||
kwonly,
|
||||
kwonly_defaults,
|
||||
_,
|
||||
) = getfullargspec(unwrap(func))
|
||||
function_name = name or func.__name__
|
||||
|
||||
@wraps(func)
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import functools
|
||||
import inspect
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.utils.version import PY314
|
||||
|
||||
if PY314:
|
||||
import annotationlib
|
||||
|
||||
lock = threading.Lock()
|
||||
safe_signature_from_callable = functools.partial(
|
||||
inspect._signature_from_callable,
|
||||
annotation_format=annotationlib.Format.FORWARDREF,
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def _get_func_parameters(func, remove_first):
|
||||
@@ -98,3 +106,27 @@ def is_module_level_function(func):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def lazy_annotations():
|
||||
"""
|
||||
inspect.getfullargspec eagerly evaluates type annotations. To add
|
||||
compatibility with Python 3.14+ deferred evaluation, patch the module-level
|
||||
helper to provide the annotation_format that we are using elsewhere.
|
||||
|
||||
This private helper could be removed when there is an upstream solution for
|
||||
https://github.com/python/cpython/issues/141560.
|
||||
|
||||
This context manager is not reentrant.
|
||||
"""
|
||||
if not PY314:
|
||||
yield
|
||||
return
|
||||
with lock:
|
||||
original_helper = inspect._signature_from_callable
|
||||
inspect._signature_from_callable = safe_signature_from_callable
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
inspect._signature_from_callable = original_helper
|
||||
|
||||
@@ -26,3 +26,7 @@ Bugfixes
|
||||
:class:`~django.http.HttpResponseRedirect` and
|
||||
:class:`~django.http.HttpResponsePermanentRedirect` for URLs longer than 2048
|
||||
characters. The limit is now 16384 characters (:ticket:`36743`).
|
||||
|
||||
* Fixed a crash on Python 3.14+ that prevented template tag functions from
|
||||
being registered when their type annotations required deferred evaluation
|
||||
(:ticket:`36712`).
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.admin.templatetags.admin_list import date_hierarchy
|
||||
from django.contrib.admin.templatetags.admin_modify import submit_row
|
||||
from django.contrib.admin.templatetags.base import InclusionAdminNode
|
||||
from django.contrib.auth import get_permission_codename
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import User
|
||||
from django.template.base import Token, TokenType
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.version import PY314
|
||||
|
||||
from .admin import ArticleAdmin, site
|
||||
from .models import Article, Question
|
||||
@@ -131,6 +135,22 @@ class AdminTemplateTagsTest(AdminViewBasicTestCase):
|
||||
self.assertContains(response, "override-pagination")
|
||||
self.assertContains(response, "override-search_form")
|
||||
|
||||
@unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
|
||||
def test_inclusion_admin_node_deferred_annotation(self):
|
||||
def action(arg: SomeType = None): # NOQA: F821
|
||||
pass
|
||||
|
||||
# This used to raise TypeError via the underlying call to
|
||||
# inspect.getfullargspec(), which is not ready for deferred
|
||||
# evaluation of annotations.
|
||||
InclusionAdminNode(
|
||||
parser=object(),
|
||||
token=Token(token_type=TokenType.TEXT, contents="a"),
|
||||
func=action,
|
||||
template_name="test.html",
|
||||
takes_context=False,
|
||||
)
|
||||
|
||||
|
||||
class DateHierarchyTests(TestCase):
|
||||
factory = RequestFactory()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import functools
|
||||
import unittest
|
||||
|
||||
from django.template import Library
|
||||
from django.template.base import Node
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils.version import PY314
|
||||
|
||||
|
||||
class FilterRegistrationTests(SimpleTestCase):
|
||||
@@ -78,6 +80,14 @@ class InclusionTagRegistrationTests(SimpleTestCase):
|
||||
self.assertIs(func_wrapped, func)
|
||||
self.assertTrue(hasattr(func_wrapped, "cache_info"))
|
||||
|
||||
@unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
|
||||
def test_inclusion_tag_deferred_annotation(self):
|
||||
@self.library.inclusion_tag("template.html")
|
||||
def func(arg: SomeType): # NOQA: F821
|
||||
return ""
|
||||
|
||||
self.assertIn("func", self.library.tags)
|
||||
|
||||
|
||||
class SimpleTagRegistrationTests(SimpleTestCase):
|
||||
def setUp(self):
|
||||
@@ -104,6 +114,14 @@ class SimpleTagRegistrationTests(SimpleTestCase):
|
||||
|
||||
self.assertIn("name", self.library.tags)
|
||||
|
||||
@unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
|
||||
def test_tag_deferred_annotation(self):
|
||||
@self.library.simple_tag
|
||||
def func(parser, token: SomeType): # NOQA: F821
|
||||
return Node()
|
||||
|
||||
self.assertIn("func", self.library.tags)
|
||||
|
||||
def test_simple_tag_invalid(self):
|
||||
msg = "Invalid arguments provided to simple_tag"
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
@@ -145,6 +163,14 @@ class SimpleBlockTagRegistrationTests(SimpleTestCase):
|
||||
|
||||
self.assertIn("name", self.library.tags)
|
||||
|
||||
@unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
|
||||
def test_simple_block_tag_deferred_annotation(self):
|
||||
@self.library.simple_block_tag
|
||||
def func(content: SomeType): # NOQA: F821
|
||||
return content
|
||||
|
||||
self.assertIn("func", self.library.tags)
|
||||
|
||||
def test_simple_block_tag_invalid(self):
|
||||
msg = "Invalid arguments provided to simple_block_tag"
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
|
||||
@@ -3,6 +3,8 @@ Testing some internals of the template processing.
|
||||
These are *not* examples to be copied in user code.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from django.template import Library, TemplateSyntaxError
|
||||
from django.template.base import (
|
||||
FilterExpression,
|
||||
@@ -15,6 +17,7 @@ from django.template.base import (
|
||||
)
|
||||
from django.template.defaultfilters import register as filter_library
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils.version import PY314
|
||||
|
||||
|
||||
class ParserTests(SimpleTestCase):
|
||||
@@ -240,3 +243,17 @@ class ParserTests(SimpleTestCase):
|
||||
FilterExpression(num, p).resolve({})
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
FilterExpression(f"0|default:{num}", p).resolve({})
|
||||
|
||||
@unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
|
||||
def test_filter_deferred_annotation(self):
|
||||
register = Library()
|
||||
|
||||
@register.filter("example")
|
||||
def example_filter(value: str, arg: SomeType): # NOQA: F821
|
||||
return f"{value}_{arg}"
|
||||
|
||||
result = FilterExpression.args_check(
|
||||
"example", example_filter, ["extra_example"]
|
||||
)
|
||||
|
||||
self.assertIs(result, True)
|
||||
|
||||
Reference in New Issue
Block a user