apply setupmethod consistently

This commit is contained in:
David Lord 2022-05-23 08:49:30 -07:00
parent eb36135cfe
commit a406c297aa
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
3 changed files with 53 additions and 30 deletions

View file

@ -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

View file

@ -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(

View file

@ -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"]``.