diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 43d87c0024..238dc6a326 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -76,6 +76,7 @@ def pagination(cl): @register.tag(name="pagination") def pagination_tag(parser, token): return InclusionAdminNode( + "pagination", parser, token, func=pagination, @@ -361,6 +362,7 @@ def result_list(cl): @register.tag(name="result_list") def result_list_tag(parser, token): return InclusionAdminNode( + "result_list", parser, token, func=result_list, @@ -476,6 +478,7 @@ def date_hierarchy(cl): @register.tag(name="date_hierarchy") def date_hierarchy_tag(parser, token): return InclusionAdminNode( + "date_hierarchy", parser, token, func=date_hierarchy, @@ -500,6 +503,7 @@ def search_form(cl): @register.tag(name="search_form") def search_form_tag(parser, token): return InclusionAdminNode( + "search_form", parser, token, func=search_form, @@ -532,7 +536,7 @@ def admin_actions(context): @register.tag(name="admin_actions") def admin_actions_tag(parser, token): return InclusionAdminNode( - parser, token, func=admin_actions, template_name="actions.html" + "admin_actions", parser, token, func=admin_actions, template_name="actions.html" ) @@ -540,6 +544,7 @@ def admin_actions_tag(parser, token): def change_list_object_tools_tag(parser, token): """Display the row of change list object tools.""" return InclusionAdminNode( + "change_list_object_tools", parser, token, func=lambda context: context, diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 0e3046ae5a..c3d2ad01d9 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -51,6 +51,7 @@ def prepopulated_fields_js(context): @register.tag(name="prepopulated_fields_js") def prepopulated_fields_js_tag(parser, token): return InclusionAdminNode( + "prepopulated_fields_js", parser, token, func=prepopulated_fields_js, @@ -115,7 +116,7 @@ def submit_row(context): @register.tag(name="submit_row") def submit_row_tag(parser, token): return InclusionAdminNode( - parser, token, func=submit_row, template_name="submit_line.html" + "submit_row", parser, token, func=submit_row, template_name="submit_line.html" ) @@ -123,6 +124,7 @@ def submit_row_tag(parser, token): def change_form_object_tools_tag(parser, token): """Display the row of change form object tools.""" return InclusionAdminNode( + "change_form_object_tools", parser, token, func=lambda context: context, diff --git a/django/contrib/admin/templatetags/base.py b/django/contrib/admin/templatetags/base.py index 9551c0e71d..25e78c0948 100644 --- a/django/contrib/admin/templatetags/base.py +++ b/django/contrib/admin/templatetags/base.py @@ -1,5 +1,6 @@ from inspect import getfullargspec +from django.template.exceptions import TemplateSyntaxError from django.template.library import InclusionNode, parse_bits @@ -9,11 +10,20 @@ class InclusionAdminNode(InclusionNode): or globally. """ - def __init__(self, parser, token, func, template_name, takes_context=True): + def __init__(self, name, parser, token, func, template_name, takes_context=True): self.template_name = template_name params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec( func ) + if takes_context: + if params and params[0] == "context": + del params[0] + else: + function_name = func.__name__ + raise TemplateSyntaxError( + f"{name!r} sets takes_context=True so {function_name!r} " + "must have a first argument of 'context'" + ) bits = token.split_contents() args, kwargs = parse_bits( parser, @@ -24,7 +34,6 @@ class InclusionAdminNode(InclusionNode): defaults, kwonly, kwonly_defaults, - takes_context, bits[0], ) super().__init__(func, takes_context, args, kwargs, filename=None) diff --git a/django/template/library.py b/django/template/library.py index 3fc4c5ebfc..ec6595f231 100644 --- a/django/template/library.py +++ b/django/template/library.py @@ -121,6 +121,15 @@ class Library: ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ + if takes_context: + if params and params[0] == "context": + del params[0] + else: + raise TemplateSyntaxError( + f"{function_name!r} is decorated with takes_context=True so it " + "must have a first argument of 'context'" + ) + @wraps(func) def compile_func(parser, token): bits = token.split_contents()[1:] @@ -137,7 +146,6 @@ class Library: defaults, kwonly, kwonly_defaults, - takes_context, function_name, ) return SimpleNode(func, takes_context, args, kwargs, target_var) @@ -180,26 +188,32 @@ class Library: if end_name is None: end_name = f"end{function_name}" - @wraps(func) - def compile_func(parser, token): - tag_params = params.copy() - - if takes_context: - if len(tag_params) >= 2 and tag_params[1] == "content": - del tag_params[1] - else: - raise TemplateSyntaxError( - f"{function_name!r} is decorated with takes_context=True so" - " it must have a first argument of 'context' and a second " - "argument of 'content'" - ) - elif tag_params and tag_params[0] == "content": - del tag_params[0] + if takes_context: + if len(params) >= 2 and params[1] == "content": + del params[1] else: raise TemplateSyntaxError( - f"'{function_name}' must have a first argument of 'content'" + f"{function_name!r} is decorated with takes_context=True so" + " it must have a first argument of 'context' and a second " + "argument of 'content'" ) + if params and params[0] == "context": + del params[0] + else: + raise TemplateSyntaxError( + f"{function_name!r} is decorated with takes_context=True so it " + "must have a first argument of 'context'" + ) + elif params and params[0] == "content": + del params[0] + else: + raise TemplateSyntaxError( + f"{function_name!r} must have a first argument of 'content'" + ) + + @wraps(func) + def compile_func(parser, token): bits = token.split_contents()[1:] target_var = None if len(bits) >= 2 and bits[-2] == "as": @@ -212,13 +226,12 @@ class Library: args, kwargs = parse_bits( parser, bits, - tag_params, + params, varargs, varkw, defaults, kwonly, kwonly_defaults, - takes_context, function_name, ) @@ -260,6 +273,15 @@ class Library: ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ + if takes_context: + if params and params[0] == "context": + params = params[1:] + else: + raise TemplateSyntaxError( + f"{function_name!r} is decorated with takes_context=True so it " + "must have a first argument of 'context'" + ) + @wraps(func) def compile_func(parser, token): bits = token.split_contents()[1:] @@ -272,7 +294,6 @@ class Library: defaults, kwonly, kwonly_defaults, - takes_context, function_name, ) return InclusionNode( @@ -391,7 +412,6 @@ def parse_bits( defaults, kwonly, kwonly_defaults, - takes_context, name, ): """ @@ -399,14 +419,6 @@ def parse_bits( particular by detecting syntax errors and by extracting positional and keyword arguments. """ - if takes_context: - if params and params[0] == "context": - params = params[1:] - else: - raise TemplateSyntaxError( - "'%s' is decorated with takes_context=True so it must " - "have a first argument of 'context'" % name - ) args = [] kwargs = {} unhandled_params = list(params) diff --git a/tests/template_tests/templatetags/custom.py b/tests/template_tests/templatetags/custom.py index bf201e74e9..f89ed1d9d3 100644 --- a/tests/template_tests/templatetags/custom.py +++ b/tests/template_tests/templatetags/custom.py @@ -268,62 +268,6 @@ def simple_unlimited_args_kwargs_block(content, one, two="hi", *args, **kwargs): ) -@register.simple_block_tag(takes_context=True) -def simple_block_tag_without_context_parameter(arg): - """Expected simple_block_tag_without_context_parameter __doc__""" - return "Expected result" - - -@register.simple_block_tag -def simple_tag_without_content_parameter(arg): - """Expected simple_tag_without_content_parameter __doc__""" - return "Expected result" - - -@register.simple_block_tag(takes_context=True) -def simple_tag_with_context_without_content_parameter(context, arg): - """Expected simple_tag_with_context_without_content_parameter __doc__""" - return "Expected result" - - -@register.simple_tag(takes_context=True) -def simple_tag_without_context_parameter(arg): - """Expected simple_tag_without_context_parameter __doc__""" - return "Expected result" - - -simple_tag_without_context_parameter.anything = ( - "Expected simple_tag_without_context_parameter __dict__" -) - - -@register.simple_block_tag(takes_context=True) -def simple_tag_takes_context_without_params_block(): - """Expected simple_tag_takes_context_without_params_block __doc__""" - return "Expected result" - - -@register.simple_tag(takes_context=True) -def simple_tag_takes_context_without_params(): - """Expected simple_tag_takes_context_without_params __doc__""" - return "Expected result" - - -simple_tag_takes_context_without_params.anything = ( - "Expected simple_tag_takes_context_without_params __dict__" -) - - -@register.simple_block_tag -def simple_block_tag_without_content(): - return "Expected result" - - -@register.simple_block_tag(takes_context=True) -def simple_block_tag_with_context_without_content(): - return "Expected result" - - @register.simple_tag(takes_context=True) def escape_naive(context): """A tag that doesn't even think about escaping issues""" diff --git a/tests/template_tests/templatetags/inclusion.py b/tests/template_tests/templatetags/inclusion.py index ea749a43b5..741eb250a2 100644 --- a/tests/template_tests/templatetags/inclusion.py +++ b/tests/template_tests/templatetags/inclusion.py @@ -269,28 +269,6 @@ inclusion_unlimited_args_kwargs.anything = ( ) -@register.inclusion_tag("inclusion.html", takes_context=True) -def inclusion_tag_without_context_parameter(arg): - """Expected inclusion_tag_without_context_parameter __doc__""" - return {} - - -inclusion_tag_without_context_parameter.anything = ( - "Expected inclusion_tag_without_context_parameter __dict__" -) - - -@register.inclusion_tag("inclusion.html", takes_context=True) -def inclusion_tag_takes_context_without_params(): - """Expected inclusion_tag_takes_context_without_params __doc__""" - return {} - - -inclusion_tag_takes_context_without_params.anything = ( - "Expected inclusion_tag_takes_context_without_params __dict__" -) - - @register.inclusion_tag("inclusion_extends1.html") def inclusion_extends1(): return {} diff --git a/tests/template_tests/test_custom.py b/tests/template_tests/test_custom.py index 424c110875..c146da5ff7 100644 --- a/tests/template_tests/test_custom.py +++ b/tests/template_tests/test_custom.py @@ -2,7 +2,7 @@ import os from django.template import Context, Engine, TemplateSyntaxError from django.template.base import Node -from django.template.library import InvalidTemplateLibrary +from django.template.library import InvalidTemplateLibrary, Library from django.test import SimpleTestCase from django.test.utils import extend_sys_path @@ -216,10 +216,6 @@ class SimpleTagTests(TagTestCase): self.verify_tag( custom.simple_unlimited_args_kwargs, "simple_unlimited_args_kwargs" ) - self.verify_tag( - custom.simple_tag_without_context_parameter, - "simple_tag_without_context_parameter", - ) def test_simple_tag_missing_context(self): # The 'context' parameter must be present when takes_context is True @@ -228,9 +224,10 @@ class SimpleTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_tag_without_context_parameter 123 %}" - ) + + @Library().simple_tag(takes_context=True) + def simple_tag_without_context_parameter(arg): + return "Expected result" def test_simple_tag_missing_context_no_params(self): msg = ( @@ -238,9 +235,10 @@ class SimpleTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_tag_takes_context_without_params %}" - ) + + @Library().simple_tag(takes_context=True) + def simple_tag_takes_context_without_params(): + return "Expected result" class SimpleBlockTagTests(TagTestCase): @@ -423,18 +421,6 @@ class SimpleBlockTagTests(TagTestCase): "of: endsimple_one_default_block.", "{% load custom %}{% simple_one_default_block %}Some content", ), - ( - "'simple_tag_without_content_parameter' must have a first argument " - "of 'content'", - "{% load custom %}{% simple_tag_without_content_parameter %}", - ), - ( - "'simple_tag_with_context_without_content_parameter' is decorated with " - "takes_context=True so it must have a first argument of 'context' and " - "a second argument of 'content'", - "{% load custom %}" - "{% simple_tag_with_context_without_content_parameter %}", - ), ] for entry in errors: @@ -485,10 +471,10 @@ class SimpleBlockTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_block_tag_without_context_parameter 123 %}" - "{% endsimple_block_tag_without_context_parameter %}" - ) + + @Library().simple_block_tag(takes_context=True) + def simple_block_tag_without_context_parameter(arg): + return "Expected result" def test_simple_block_tag_missing_context_no_params(self): msg = ( @@ -496,10 +482,10 @@ class SimpleBlockTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_tag_takes_context_without_params_block %}" - "{% endsimple_tag_takes_context_without_params_block %}" - ) + + @Library().simple_block_tag(takes_context=True) + def simple_tag_takes_context_without_params_block(): + return "Expected result" def test_simple_block_tag_missing_content(self): # The 'content' parameter must be present when takes_context is True @@ -507,10 +493,10 @@ class SimpleBlockTagTests(TagTestCase): "'simple_block_tag_without_content' must have a first argument of 'content'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_block_tag_without_content %}" - "{% endsimple_block_tag_without_content %}" - ) + + @Library().simple_block_tag + def simple_block_tag_without_content(): + return "Expected result" def test_simple_block_tag_with_context_missing_content(self): # The 'content' parameter must be present when takes_context is True @@ -520,10 +506,10 @@ class SimpleBlockTagTests(TagTestCase): "second argument of 'content'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load custom %}{% simple_block_tag_with_context_without_content %}" - "{% endsimple_block_tag_with_context_without_content %}" - ) + + @Library().simple_block_tag(takes_context=True) + def simple_block_tag_with_context_without_content(): + return "Expected result" def test_simple_block_gets_context(self): c = Context({"name": "Jack & Jill"}) @@ -720,9 +706,10 @@ class InclusionTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load inclusion %}{% inclusion_tag_without_context_parameter 123 %}" - ) + + @Library().inclusion_tag("inclusion.html", takes_context=True) + def inclusion_tag_without_context_parameter(arg): + return {} def test_include_tag_missing_context_no_params(self): msg = ( @@ -730,9 +717,10 @@ class InclusionTagTests(TagTestCase): "takes_context=True so it must have a first argument of 'context'" ) with self.assertRaisesMessage(TemplateSyntaxError, msg): - self.engine.from_string( - "{% load inclusion %}{% inclusion_tag_takes_context_without_params %}" - ) + + @Library().inclusion_tag("inclusion.html", takes_context=True) + def inclusion_tag_takes_context_without_params(): + return {} def test_inclusion_tags_from_template(self): c = Context({"value": 42}) @@ -822,10 +810,6 @@ class InclusionTagTests(TagTestCase): self.verify_tag( inclusion.inclusion_only_unlimited_args, "inclusion_only_unlimited_args" ) - self.verify_tag( - inclusion.inclusion_tag_without_context_parameter, - "inclusion_tag_without_context_parameter", - ) self.verify_tag(inclusion.inclusion_tag_use_l10n, "inclusion_tag_use_l10n") self.verify_tag( inclusion.inclusion_unlimited_args_kwargs, "inclusion_unlimited_args_kwargs"