From 0a52b6b78d98ed96b5bec2293d3e6cc1dd4310c6 Mon Sep 17 00:00:00 2001 From: kadai0308 Date: Sat, 17 May 2025 12:17:18 +0800 Subject: [PATCH 1/5] feat: support call template_filter without parens --- src/flask/sansio/app.py | 26 +++++++++++++++++++++----- tests/test_templating.py | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index ceab45cb5e..6a88aeeee9 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -662,20 +662,36 @@ def add_url_rule( @setupmethod def template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: + self, name: t.Callable[..., t.Any] | str | 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:: - @app.template_filter() - def reverse(s): - return s[::-1] + @app.template_filter() + def reverse(s): + return s[::-1] + + The decorator also can be used without parentheses:: + + @app.template_filter + def reverse(s): + return s[::-1] :param name: the optional name of the filter, otherwise the function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @app.template_filter + # def func(): + + func = name + self.add_template_filter(func) + return func + def decorator(f: T_template_filter) -> T_template_filter: self.add_template_filter(f, name=name) return f diff --git a/tests/test_templating.py b/tests/test_templating.py index c9fb3754a3..4f8f42da6f 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -129,6 +129,30 @@ def my_reverse(s): assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + @app.template_filter + def my_reverse_2(s): + return s[::-1] + + assert "my_reverse_2" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 + assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" + + @app.template_filter("my_reverse_custom_name") + def my_reverse_3(s): + return s[::-1] + + assert "my_reverse_custom_name" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name"] == my_reverse_3 + assert app.jinja_env.filters["my_reverse_custom_name"]("abcd") == "dcba" + + @app.template_filter(name="my_reverse_custom_name_2") + def my_reverse_4(s): + return s[::-1] + + assert "my_reverse_custom_name_2" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name_2"] == my_reverse_4 + assert app.jinja_env.filters["my_reverse_custom_name_2"]("abcd") == "dcba" + def test_add_template_filter(app): def my_reverse(s): From 6d6c377a1fe71931c107874fe70ce04d567281f5 Mon Sep 17 00:00:00 2001 From: kadai0308 Date: Mon, 26 May 2025 17:20:12 +0800 Subject: [PATCH 2/5] feat: support call template_test & template_global without parens --- src/flask/sansio/app.py | 58 +++++++++++++++++++----- src/flask/sansio/blueprints.py | 42 ++++++++++++++--- tests/test_blueprints.py | 83 ++++++++++++++++++++++++++++++++++ tests/test_templating.py | 73 ++++++++++++++++++++++++++---- 4 files changed, 231 insertions(+), 25 deletions(-) diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index 6a88aeeee9..c2e5586064 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -712,27 +712,47 @@ def add_template_filter( @setupmethod def template_test( - self, name: str | None = None - ) -> t.Callable[[T_template_test], T_template_test]: + self, name: t.Callable[..., t.Any] | str | None = None + ) -> t.Callable[[T_template_test], T_template_test] | T_template_filter: """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:: - @app.template_test() - def is_prime(n): - if n == 2: - return True - for i in range(2, int(math.ceil(math.sqrt(n))) + 1): - if n % i == 0: - return False + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False return True + The decorator also can be used without parentheses:: + + @app.template_test + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + .. versionadded:: 0.10 :param name: the optional name of the test, otherwise the function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @app.template_test + # def func(): + + func = name + self.add_template_test(func) + return func + def decorator(f: T_template_test) -> T_template_test: self.add_template_test(f, name=name) return f @@ -755,8 +775,8 @@ def add_template_test( @setupmethod def template_global( - self, name: str | None = None - ) -> t.Callable[[T_template_global], T_template_global]: + self, name: t.Callable[..., t.Any] | str | None = None + ) -> t.Callable[[T_template_global], T_template_global] | T_template_filter: """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:: @@ -765,12 +785,28 @@ def template_global( def double(n): return 2 * n + The decorator also can be used without parentheses:: + + @app.template_global + def double(n): + return 2 * n + .. versionadded:: 0.10 :param name: the optional name of the global function, otherwise the function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @app.template_global + # def func(): + + func = name + self.add_template_global(func) + return func + def decorator(f: T_template_global) -> T_template_global: self.add_template_global(f, name=name) return f diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py index 4f912cca05..eff6d8d783 100644 --- a/src/flask/sansio/blueprints.py +++ b/src/flask/sansio/blueprints.py @@ -442,8 +442,8 @@ def add_url_rule( @setupmethod def app_template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: + self, name: t.Callable[..., t.Any] | str | 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`. @@ -451,6 +451,17 @@ def app_template_filter( function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @bp.add_template_filter + # def func(): + + func = name + self.add_app_template_filter(func) + return func + + def decorator(f: T_template_filter) -> T_template_filter: self.add_app_template_filter(f, name=name) return f @@ -476,8 +487,8 @@ 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: t.Callable[..., t.Any] | str | None = None + ) -> t.Callable[[T_template_test], T_template_test] | T_template_filter: """Register a template test, available in any template rendered by the application. Equivalent to :meth:`.Flask.template_test`. @@ -487,6 +498,16 @@ def app_template_test( function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @bp.add_template_test + # def func(): + + func = name + self.add_app_template_test(func) + return func + def decorator(f: T_template_test) -> T_template_test: self.add_app_template_test(f, name=name) return f @@ -514,8 +535,8 @@ 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: t.Callable[..., t.Any] | str | None = None + ) -> t.Callable[[T_template_global], T_template_global] | T_template_filter: """Register a template global, available in any template rendered by the application. Equivalent to :meth:`.Flask.template_global`. @@ -524,6 +545,15 @@ def app_template_global( :param name: the optional name of the global, otherwise the function name will be used. """ + if callable(name): + # If name is callable, it is the function to register. + # This is a shortcut for the common case of + # @bp.add_template_global + # def func(): + + func = name + self.add_app_template_global(func) + return func def decorator(f: T_template_global) -> T_template_global: self.add_app_template_global(f, name=name) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index e3e2905ab3..8ebfebb345 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -366,11 +366,37 @@ def test_template_filter(app): def my_reverse(s): return s[::-1] + @bp.app_template_filter + def my_reverse_2(s): + return s[::-1] + + @bp.app_template_filter("my_reverse_custom_name_3") + def my_reverse_3(s): + return s[::-1] + + @bp.app_template_filter(name="my_reverse_custom_name_4") + def my_reverse_4(s): + return s[::-1] + app.register_blueprint(bp, url_prefix="/py") assert "my_reverse" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + assert "my_reverse_2" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 + assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" + + + assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3 + assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba" + + + assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4 + assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba" + def test_add_template_filter(app): bp = flask.Blueprint("bp", __name__) @@ -502,11 +528,35 @@ def test_template_test(app): def is_boolean(value): return isinstance(value, bool) + @bp.app_template_test + def boolean_2(value): + return isinstance(value, bool) + + @bp.app_template_test("my_boolean_custom_name") + def boolean_3(value): + return isinstance(value, bool) + + @bp.app_template_test(name="my_boolean_custom_name_2") + def boolean_4(value): + return isinstance(value, bool) + app.register_blueprint(bp, url_prefix="/py") assert "is_boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["is_boolean"] == is_boolean assert app.jinja_env.tests["is_boolean"](False) + assert "boolean_2" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean_2"] == boolean_2 + assert app.jinja_env.tests["boolean_2"](False) + + assert "my_boolean_custom_name" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3 + assert app.jinja_env.tests["my_boolean_custom_name"](False) + + assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4 + assert app.jinja_env.tests["my_boolean_custom_name_2"](False) + def test_add_template_test(app): bp = flask.Blueprint("bp", __name__) @@ -679,6 +729,18 @@ def test_template_global(app): def get_answer(): return 42 + @bp.app_template_global + def get_stuff_1(): + return "get_stuff_1" + + @bp.app_template_global("my_get_stuff_custom_name_2") + def get_stuff_2(): + return "get_stuff_2" + + @bp.app_template_global(name="my_get_stuff_custom_name_3") + def get_stuff_3(): + return "get_stuff_3" + # Make sure the function is not in the jinja_env already assert "get_answer" not in app.jinja_env.globals.keys() app.register_blueprint(bp) @@ -688,10 +750,31 @@ def get_answer(): assert app.jinja_env.globals["get_answer"] is get_answer assert app.jinja_env.globals["get_answer"]() == 42 + assert "get_stuff_1" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1 + assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1" + + assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2 + assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2" + + assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3 + assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3" + with app.app_context(): rv = flask.render_template_string("{{ get_answer() }}") assert rv == "42" + rv = flask.render_template_string("{{ get_stuff_1() }}") + assert rv == "get_stuff_1" + + rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}") + assert rv == "get_stuff_2" + + rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}") + assert rv == "get_stuff_3" + def test_request_processing(app, client): bp = flask.Blueprint("bp", __name__) diff --git a/tests/test_templating.py b/tests/test_templating.py index 4f8f42da6f..85549df094 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -137,21 +137,21 @@ def my_reverse_2(s): assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" - @app.template_filter("my_reverse_custom_name") + @app.template_filter("my_reverse_custom_name_3") def my_reverse_3(s): return s[::-1] - assert "my_reverse_custom_name" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name"] == my_reverse_3 - assert app.jinja_env.filters["my_reverse_custom_name"]("abcd") == "dcba" + assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3 + assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba" - @app.template_filter(name="my_reverse_custom_name_2") + @app.template_filter(name="my_reverse_custom_name_4") def my_reverse_4(s): return s[::-1] - assert "my_reverse_custom_name_2" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name_2"] == my_reverse_4 - assert app.jinja_env.filters["my_reverse_custom_name_2"]("abcd") == "dcba" + assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4 + assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba" def test_add_template_filter(app): @@ -247,6 +247,30 @@ def boolean(value): assert app.jinja_env.tests["boolean"] == boolean assert app.jinja_env.tests["boolean"](False) + @app.template_test + def boolean_2(value): + return isinstance(value, bool) + + assert "boolean_2" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean_2"] == boolean_2 + assert app.jinja_env.tests["boolean_2"](False) + + @app.template_test("my_boolean_custom_name") + def boolean_3(value): + return isinstance(value, bool) + + assert "my_boolean_custom_name" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3 + assert app.jinja_env.tests["my_boolean_custom_name"](False) + + @app.template_test(name="my_boolean_custom_name_2") + def boolean_4(value): + return isinstance(value, bool) + + assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4 + assert app.jinja_env.tests["my_boolean_custom_name_2"](False) + def test_add_template_test(app): def boolean(value): @@ -344,6 +368,39 @@ def get_stuff(): rv = flask.render_template_string("{{ get_stuff() }}") assert rv == "42" + @app.template_global + def get_stuff_1(): + return "get_stuff_1" + + assert "get_stuff_1" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1 + assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1" + + rv = flask.render_template_string("{{ get_stuff_1() }}") + assert rv == "get_stuff_1" + + @app.template_global("my_get_stuff_custom_name_2") + def get_stuff_2(): + return "get_stuff_2" + + assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2 + assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2" + + rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}") + assert rv == "get_stuff_2" + + @app.template_global(name="my_get_stuff_custom_name_3") + def get_stuff_3(): + return "get_stuff_3" + + assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3 + assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3" + + rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}") + assert rv == "get_stuff_3" + def test_custom_template_loader(client): class MyFlask(flask.Flask): From 1f027e498df9e82b468f4f19eccc6668784e3cad Mon Sep 17 00:00:00 2001 From: kadai0308 Date: Mon, 26 May 2025 17:23:06 +0800 Subject: [PATCH 3/5] formatting --- src/flask/sansio/blueprints.py | 1 - src/flask/testing.py | 6 +++--- tests/test_blueprints.py | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py index eff6d8d783..37da2b3c63 100644 --- a/src/flask/sansio/blueprints.py +++ b/src/flask/sansio/blueprints.py @@ -461,7 +461,6 @@ def app_template_filter( self.add_app_template_filter(func) return func - def decorator(f: T_template_filter) -> T_template_filter: self.add_app_template_filter(f, name=name) return f diff --git a/src/flask/testing.py b/src/flask/testing.py index da156cc125..a62c4836dc 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -58,9 +58,9 @@ def __init__( ) -> None: assert not (base_url or subdomain or url_scheme) or ( base_url is not None - ) != bool(subdomain or url_scheme), ( - 'Cannot pass "subdomain" or "url_scheme" with "base_url".' - ) + ) != bool( + subdomain or url_scheme + ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' if base_url is None: http_host = app.config.get("SERVER_NAME") or "localhost" diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 8ebfebb345..ed1683c470 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -387,12 +387,10 @@ def my_reverse_4(s): assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" - assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3 assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba" - assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4 assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba" From 45a0ff357fcd6637c47e51e867e0914c8c384cfb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 09:26:20 +0000 Subject: [PATCH 4/5] [pre-commit.ci lite] apply automatic fixes --- src/flask/testing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/flask/testing.py b/src/flask/testing.py index a62c4836dc..da156cc125 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -58,9 +58,9 @@ def __init__( ) -> None: assert not (base_url or subdomain or url_scheme) or ( base_url is not None - ) != bool( - subdomain or url_scheme - ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + ) != bool(subdomain or url_scheme), ( + 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + ) if base_url is None: http_host = app.config.get("SERVER_NAME") or "localhost" From 0abda4d277ffc60f6505feeea007f3c4a44823c9 Mon Sep 17 00:00:00 2001 From: kadai0308 Date: Mon, 26 May 2025 17:29:59 +0800 Subject: [PATCH 5/5] docs: update docs and CHANGES.rst --- CHANGES.rst | 1 + docs/templating.rst | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 37e777dca8..63271214b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Unreleased - Drop support for Python 3.9. :pr:`5730` - Remove previously deprecated code: ``__version__``. :pr:`5648` +- Support for using @app.template_filter, @app.template_test, and @app.template_global decorators without parentheses. :pr:`5736` Version 3.1.1 diff --git a/docs/templating.rst b/docs/templating.rst index 23cfee4cb3..e5cb955a39 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -145,7 +145,11 @@ that. You can either put them by hand into the :attr:`~flask.Flask.jinja_env` of the application or use the :meth:`~flask.Flask.template_filter` decorator. -The two following examples work the same and both reverse an object:: +The following examples work the same and all reverse an object:: + + @app.template_filter # use the function name as filter name + def reverse_filter(s): + return s[::-1] @app.template_filter('reverse') def reverse_filter(s):