diff --git a/docs/contributing.rst b/docs/contributing.rst index d44f865f57..ca4b3aeef4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,7 +1,7 @@ Contributing ============ -See the Pallets `detailed contributing documentation <_contrib>`_ for many ways +See the Pallets `detailed contributing documentation `_ for many ways to contribute, including reporting issues, requesting features, asking or answering questions, and making PRs. diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index 3620171410..935270c058 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -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:: @@ -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 @@ -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:: @@ -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 @@ -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:: @@ -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 diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py index 4f912cca05..d59d1b9d50 100644 --- a/src/flask/sansio/blueprints.py +++ b/src/flask/sansio/blueprints.py @@ -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 @@ -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 @@ -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 diff --git a/tests/test_decorator_no_parens.py b/tests/test_decorator_no_parens.py new file mode 100644 index 0000000000..e03fb53268 --- /dev/null +++ b/tests/test_decorator_no_parens.py @@ -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