Skip to content

Fix template decorators to work without parentheses #5743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/contributing.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Contributing
============

See the Pallets `detailed contributing documentation <_contrib>`_ for many ways
See the Pallets `detailed contributing documentation <contrib_>`_ for many ways
to contribute, including reporting issues, requesting features, asking or
answering questions, and making PRs.

Expand Down
51 changes: 36 additions & 15 deletions src/flask/sansio/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,8 @@ def add_url_rule(

@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::
Expand All @@ -672,12 +672,19 @@ def template_filter(
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
Expand All @@ -696,8 +703,8 @@ def add_template_filter(

@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::
Expand All @@ -713,12 +720,19 @@ def is_prime(n):

.. 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
Expand All @@ -739,8 +753,8 @@ def add_template_test(

@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::
Expand All @@ -751,12 +765,19 @@ def double(n):

.. 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
Expand Down
51 changes: 36 additions & 15 deletions src/flask/sansio/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,17 +442,24 @@ def add_url_rule(

@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
Expand All @@ -476,19 +483,26 @@ def register_template(state: BlueprintSetupState) -> None:

@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
Expand All @@ -514,19 +528,26 @@ def register_template(state: BlueprintSetupState) -> None:

@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
Expand Down
78 changes: 78 additions & 0 deletions tests/test_decorator_no_parens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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
Loading