Fix template decorators to work without parentheses

This fixes an issue where template decorators like @app.template_filter
don't work when used without parentheses. The fix allows the decorators
to be used in both ways:

- @app.template_filter
- @app.template_filter()

The following decorators have been fixed:
- template_filter, template_test, template_global in Flask
- app_template_filter, app_template_test, app_template_global in Blueprint

Fixes #5729
This commit is contained in:
Alex Ding 2025-05-22 18:13:37 +00:00
parent a42c4d54a3
commit 08f2779039
3 changed files with 149 additions and 30 deletions

View file

@ -662,8 +662,8 @@ class App(Scaffold):
@setupmethod
def template_filter(
self, name: str | None = None
) -> t.Callable[[T_template_filter], T_template_filter]:
self, name_or_function: str | T_template_filter | None = None
) -> t.Callable[[T_template_filter], T_template_filter] | T_template_filter:
"""A decorator that is used to register custom template filter.
You can specify a name for the filter, otherwise the function
name will be used. Example::
@ -672,12 +672,19 @@ class App(Scaffold):
def reverse(s):
return s[::-1]
:param name: the optional name of the filter, otherwise the
function name will be used.
:param name_or_function: the optional name of the filter, otherwise the
function name will be used. Or the function itself when
used as ``@app.template_filter`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_template_filter(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_filter) -> T_template_filter:
self.add_template_filter(f, name=name)
self.add_template_filter(f, name=name_or_function)
return f
return decorator
@ -696,8 +703,8 @@ class App(Scaffold):
@setupmethod
def template_test(
self, name: str | None = None
) -> t.Callable[[T_template_test], T_template_test]:
self, name_or_function: str | T_template_test | None = None
) -> t.Callable[[T_template_test], T_template_test] | T_template_test:
"""A decorator that is used to register custom template test.
You can specify a name for the test, otherwise the function
name will be used. Example::
@ -713,12 +720,19 @@ class App(Scaffold):
.. versionadded:: 0.10
:param name: the optional name of the test, otherwise the
function name will be used.
:param name_or_function: the optional name of the test, otherwise the
function name will be used. Or the function itself when
used as ``@app.template_test`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_template_test(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_test) -> T_template_test:
self.add_template_test(f, name=name)
self.add_template_test(f, name=name_or_function)
return f
return decorator
@ -739,8 +753,8 @@ class App(Scaffold):
@setupmethod
def template_global(
self, name: str | None = None
) -> t.Callable[[T_template_global], T_template_global]:
self, name_or_function: str | T_template_global | None = None
) -> t.Callable[[T_template_global], T_template_global] | T_template_global:
"""A decorator that is used to register a custom template global function.
You can specify a name for the global function, otherwise the function
name will be used. Example::
@ -751,12 +765,19 @@ class App(Scaffold):
.. versionadded:: 0.10
:param name: the optional name of the global function, otherwise the
function name will be used.
:param name_or_function: the optional name of the global function, otherwise the
function name will be used. Or the function itself when
used as ``@app.template_global`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_template_global(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_global) -> T_template_global:
self.add_template_global(f, name=name)
self.add_template_global(f, name=name_or_function)
return f
return decorator

View file

@ -442,17 +442,24 @@ class Blueprint(Scaffold):
@setupmethod
def app_template_filter(
self, name: str | None = None
) -> t.Callable[[T_template_filter], T_template_filter]:
self, name_or_function: str | T_template_filter | None = None
) -> t.Callable[[T_template_filter], T_template_filter] | T_template_filter:
"""Register a template filter, available in any template rendered by the
application. Equivalent to :meth:`.Flask.template_filter`.
:param name: the optional name of the filter, otherwise the
function name will be used.
:param name_or_function: the optional name of the filter, otherwise the
function name will be used. Or the function itself when
used as ``@blueprint.app_template_filter`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_app_template_filter(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_filter) -> T_template_filter:
self.add_app_template_filter(f, name=name)
self.add_app_template_filter(f, name=name_or_function)
return f
return decorator
@ -476,19 +483,26 @@ class Blueprint(Scaffold):
@setupmethod
def app_template_test(
self, name: str | None = None
) -> t.Callable[[T_template_test], T_template_test]:
self, name_or_function: str | T_template_test | None = None
) -> t.Callable[[T_template_test], T_template_test] | T_template_test:
"""Register a template test, available in any template rendered by the
application. Equivalent to :meth:`.Flask.template_test`.
.. versionadded:: 0.10
:param name: the optional name of the test, otherwise the
function name will be used.
:param name_or_function: the optional name of the test, otherwise the
function name will be used. Or the function itself when
used as ``@blueprint.app_template_test`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_app_template_test(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_test) -> T_template_test:
self.add_app_template_test(f, name=name)
self.add_app_template_test(f, name=name_or_function)
return f
return decorator
@ -514,19 +528,26 @@ class Blueprint(Scaffold):
@setupmethod
def app_template_global(
self, name: str | None = None
) -> t.Callable[[T_template_global], T_template_global]:
self, name_or_function: str | T_template_global | None = None
) -> t.Callable[[T_template_global], T_template_global] | T_template_global:
"""Register a template global, available in any template rendered by the
application. Equivalent to :meth:`.Flask.template_global`.
.. versionadded:: 0.10
:param name: the optional name of the global, otherwise the
function name will be used.
:param name_or_function: the optional name of the global, otherwise the
function name will be used. Or the function itself when
used as ``@blueprint.app_template_global`` without parentheses.
"""
# Check if the decorator was called without parentheses
# (name_or_function is the actual function to decorate)
if callable(name_or_function):
self.add_app_template_global(name_or_function)
return name_or_function
# Otherwise, the decorator was called with a name or empty parentheses
def decorator(f: T_template_global) -> T_template_global:
self.add_app_template_global(f, name=name)
self.add_app_template_global(f, name=name_or_function)
return f
return decorator

View file

@ -0,0 +1,77 @@
import pytest
import flask
def test_template_filter_no_parens(app):
"""Test that @app.template_filter works without parentheses."""
@app.template_filter
def double(x):
return x * 2
assert "double" in app.jinja_env.filters
assert app.jinja_env.filters["double"] == double
assert app.jinja_env.filters["double"](2) == 4
def test_template_test_no_parens(app):
"""Test that @app.template_test works without parentheses."""
@app.template_test
def is_even(x):
return x % 2 == 0
assert "is_even" in app.jinja_env.tests
assert app.jinja_env.tests["is_even"] == is_even
assert app.jinja_env.tests["is_even"](2) is True
assert app.jinja_env.tests["is_even"](3) is False
def test_template_global_no_parens(app):
"""Test that @app.template_global works without parentheses."""
@app.template_global
def get_answer():
return 42
assert "get_answer" in app.jinja_env.globals
assert app.jinja_env.globals["get_answer"] == get_answer
assert app.jinja_env.globals["get_answer"]() == 42
def test_blueprint_app_template_filter_no_parens(app):
"""Test that @blueprint.app_template_filter works without parentheses."""
bp = flask.Blueprint("test_bp", __name__)
@bp.app_template_filter
def triple(x):
return x * 3
app.register_blueprint(bp)
assert "triple" in app.jinja_env.filters
assert app.jinja_env.filters["triple"](3) == 9
def test_blueprint_app_template_test_no_parens(app):
"""Test that @blueprint.app_template_test works without parentheses."""
bp = flask.Blueprint("test_bp", __name__)
@bp.app_template_test
def is_odd(x):
return x % 2 == 1
app.register_blueprint(bp)
assert "is_odd" in app.jinja_env.tests
assert app.jinja_env.tests["is_odd"](3) is True
assert app.jinja_env.tests["is_odd"](2) is False
def test_blueprint_app_template_global_no_parens(app):
"""Test that @blueprint.app_template_global works without parentheses."""
bp = flask.Blueprint("test_bp", __name__)
@bp.app_template_global
def get_pi():
return 3.14
app.register_blueprint(bp)
assert "get_pi" in app.jinja_env.globals
assert app.jinja_env.globals["get_pi"]() == 3.14