From 08f277903985ec8a8f486045b56eebd8204542e1 Mon Sep 17 00:00:00 2001 From: Alex Ding Date: Thu, 22 May 2025 18:13:37 +0000 Subject: [PATCH] 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 --- src/flask/sansio/app.py | 51 ++++++++++++++------ src/flask/sansio/blueprints.py | 51 ++++++++++++++------ tests/test_decorator_no_parens.py | 77 +++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 tests/test_decorator_no_parens.py diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index 36201714..935270c0 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -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 diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py index 4f912cca..d59d1b9d 100644 --- a/src/flask/sansio/blueprints.py +++ b/src/flask/sansio/blueprints.py @@ -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 diff --git a/tests/test_decorator_no_parens.py b/tests/test_decorator_no_parens.py new file mode 100644 index 00000000..9f8999f0 --- /dev/null +++ b/tests/test_decorator_no_parens.py @@ -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