From a406c297aafa28074d11ec6fd27c246c70418cb4 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 08:49:30 -0700 Subject: [PATCH] apply setupmethod consistently --- src/flask/app.py | 13 ++++++++++-- src/flask/blueprints.py | 47 +++++++++++++++++++++++++++++------------ src/flask/scaffold.py | 23 +++++++------------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index e7a07add..c29c2e87 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -541,8 +541,17 @@ class Flask(Scaffold): # the app's commands to another CLI tool. self.cli.name = self.name - def _is_setup_finished(self) -> bool: - return self.debug and self._got_first_request + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) @locked_cached_property def name(self) -> str: # type: ignore diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index e4a9a5ce..a8856fe4 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -6,6 +6,7 @@ from functools import update_wrapper from .scaffold import _endpoint_from_view_func from .scaffold import _sentinel from .scaffold import Scaffold +from .scaffold import setupmethod from .typing import AfterRequestCallable from .typing import BeforeFirstRequestCallable from .typing import BeforeRequestCallable @@ -207,28 +208,33 @@ class Blueprint(Scaffold): self.cli_group = cli_group self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] - def _is_setup_finished(self) -> bool: - return self._got_registered_once + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + import warnings + warnings.warn( + f"The setup method '{f_name}' can no longer be called on" + f" the blueprint '{self.name}'. It has already been" + " registered at least once, any changes will not be" + " applied consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the blueprint are done before" + " registering it.\n" + "This warning will become an exception in Flask 2.3.", + UserWarning, + stacklevel=3, + ) + + @setupmethod def record(self, func: t.Callable) -> None: """Registers a function that is called when the blueprint is registered on the application. This function is called with the state as argument as returned by the :meth:`make_setup_state` method. """ - if self._got_registered_once: - # TODO: Upgrade this to an error and unify it setupmethod in 2.3 - from warnings import warn - - warn( - Warning( - "The blueprint was already registered once but is" - " getting modified now. These changes will not show" - " up.\n This warning will be become an exception in 2.3." - ) - ) self.deferred_functions.append(func) + @setupmethod def record_once(self, func: t.Callable) -> None: """Works like :meth:`record` but wraps the function in another function that will ensure the function is only called once. If the @@ -251,6 +257,7 @@ class Blueprint(Scaffold): """ return BlueprintSetupState(self, app, options, first_registration) + @setupmethod def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: """Register a :class:`~flask.Blueprint` on this blueprint. Keyword arguments passed to this method will override the defaults set @@ -390,6 +397,7 @@ class Blueprint(Scaffold): bp_options["name_prefix"] = name blueprint.register(app, bp_options) + @setupmethod def add_url_rule( self, rule: str, @@ -417,6 +425,7 @@ class Blueprint(Scaffold): ) ) + @setupmethod def app_template_filter( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: @@ -433,6 +442,7 @@ class Blueprint(Scaffold): return decorator + @setupmethod def add_app_template_filter( self, f: TemplateFilterCallable, name: t.Optional[str] = None ) -> None: @@ -449,6 +459,7 @@ class Blueprint(Scaffold): self.record_once(register_template) + @setupmethod def app_template_test( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: @@ -467,6 +478,7 @@ class Blueprint(Scaffold): return decorator + @setupmethod def add_app_template_test( self, f: TemplateTestCallable, name: t.Optional[str] = None ) -> None: @@ -485,6 +497,7 @@ class Blueprint(Scaffold): self.record_once(register_template) + @setupmethod def app_template_global( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: @@ -503,6 +516,7 @@ class Blueprint(Scaffold): return decorator + @setupmethod def add_app_template_global( self, f: TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: @@ -521,6 +535,7 @@ class Blueprint(Scaffold): self.record_once(register_template) + @setupmethod def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. @@ -530,6 +545,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def before_app_first_request( self, f: BeforeFirstRequestCallable ) -> BeforeFirstRequestCallable: @@ -548,6 +564,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of @@ -558,6 +575,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_context_processor( self, f: TemplateContextProcessorCallable ) -> TemplateContextProcessorCallable: @@ -569,6 +587,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. @@ -580,6 +599,7 @@ class Blueprint(Scaffold): return decorator + @setupmethod def app_url_value_preprocessor( self, f: URLValuePreprocessorCallable ) -> URLValuePreprocessorCallable: @@ -589,6 +609,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: """Same as :meth:`url_defaults` but application wide.""" self.record_once( diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index b2dd46f3..154426ca 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -37,22 +37,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any]) def setupmethod(f: F) -> F: - """Wraps a method so that it performs a check in debug mode if the - first request was already handled. - """ + f_name = f.__name__ def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any: - if self._is_setup_finished(): - raise AssertionError( - "A setup function was called after the first request " - "was handled. This usually indicates a bug in the" - " application where a module was not imported and" - " decorators or other functionality was called too" - " late.\nTo fix this make sure to import all your view" - " modules, database models, and everything related at a" - " central place before the application starts serving" - " requests." - ) + self._check_setup_finished(f_name) return f(self, *args, **kwargs) return t.cast(F, update_wrapper(wrapper_func, f)) @@ -239,7 +227,7 @@ class Scaffold: def __repr__(self) -> str: return f"<{type(self).__name__} {self.name!r}>" - def _is_setup_finished(self) -> bool: + def _check_setup_finished(self, f_name: str) -> None: raise NotImplementedError @property @@ -376,6 +364,7 @@ class Scaffold: return self.route(rule, methods=[method], **options) + @setupmethod def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["GET"]``. @@ -383,6 +372,7 @@ class Scaffold: """ return self._method_route("GET", rule, options) + @setupmethod def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["POST"]``. @@ -390,6 +380,7 @@ class Scaffold: """ return self._method_route("POST", rule, options) + @setupmethod def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. @@ -397,6 +388,7 @@ class Scaffold: """ return self._method_route("PUT", rule, options) + @setupmethod def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. @@ -404,6 +396,7 @@ class Scaffold: """ return self._method_route("DELETE", rule, options) + @setupmethod def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``.