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. # the app's commands to another CLI tool.
self.cli.name = self.name self.cli.name = self.name
def _is_setup_finished(self) -> bool: def _check_setup_finished(self, f_name: str) -> None:
return self.debug and self._got_first_request 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 @locked_cached_property
def name(self) -> str: # type: ignore 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 _endpoint_from_view_func
from .scaffold import _sentinel from .scaffold import _sentinel
from .scaffold import Scaffold from .scaffold import Scaffold
from .scaffold import setupmethod
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
@ -207,28 +208,33 @@ class Blueprint(Scaffold):
self.cli_group = cli_group self.cli_group = cli_group
self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
def _is_setup_finished(self) -> bool: def _check_setup_finished(self, f_name: str) -> None:
return self._got_registered_once 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: def record(self, func: t.Callable) -> None:
"""Registers a function that is called when the blueprint is """Registers a function that is called when the blueprint is
registered on the application. This function is called with the registered on the application. This function is called with the
state as argument as returned by the :meth:`make_setup_state` state as argument as returned by the :meth:`make_setup_state`
method. 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) self.deferred_functions.append(func)
@setupmethod
def record_once(self, func: t.Callable) -> None: def record_once(self, func: t.Callable) -> None:
"""Works like :meth:`record` but wraps the function in another """Works like :meth:`record` but wraps the function in another
function that will ensure the function is only called once. If the 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) return BlueprintSetupState(self, app, options, first_registration)
@setupmethod
def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
"""Register a :class:`~flask.Blueprint` on this blueprint. Keyword """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
arguments passed to this method will override the defaults set arguments passed to this method will override the defaults set
@ -390,6 +397,7 @@ class Blueprint(Scaffold):
bp_options["name_prefix"] = name bp_options["name_prefix"] = name
blueprint.register(app, bp_options) blueprint.register(app, bp_options)
@setupmethod
def add_url_rule( def add_url_rule(
self, self,
rule: str, rule: str,
@ -417,6 +425,7 @@ class Blueprint(Scaffold):
) )
) )
@setupmethod
def app_template_filter( def app_template_filter(
self, name: t.Optional[str] = None self, name: t.Optional[str] = None
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
@ -433,6 +442,7 @@ class Blueprint(Scaffold):
return decorator return decorator
@setupmethod
def add_app_template_filter( def add_app_template_filter(
self, f: TemplateFilterCallable, name: t.Optional[str] = None self, f: TemplateFilterCallable, name: t.Optional[str] = None
) -> None: ) -> None:
@ -449,6 +459,7 @@ class Blueprint(Scaffold):
self.record_once(register_template) self.record_once(register_template)
@setupmethod
def app_template_test( def app_template_test(
self, name: t.Optional[str] = None self, name: t.Optional[str] = None
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
@ -467,6 +478,7 @@ class Blueprint(Scaffold):
return decorator return decorator
@setupmethod
def add_app_template_test( def add_app_template_test(
self, f: TemplateTestCallable, name: t.Optional[str] = None self, f: TemplateTestCallable, name: t.Optional[str] = None
) -> None: ) -> None:
@ -485,6 +497,7 @@ class Blueprint(Scaffold):
self.record_once(register_template) self.record_once(register_template)
@setupmethod
def app_template_global( def app_template_global(
self, name: t.Optional[str] = None self, name: t.Optional[str] = None
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
@ -503,6 +516,7 @@ class Blueprint(Scaffold):
return decorator return decorator
@setupmethod
def add_app_template_global( def add_app_template_global(
self, f: TemplateGlobalCallable, name: t.Optional[str] = None self, f: TemplateGlobalCallable, name: t.Optional[str] = None
) -> None: ) -> None:
@ -521,6 +535,7 @@ class Blueprint(Scaffold):
self.record_once(register_template) self.record_once(register_template)
@setupmethod
def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
"""Like :meth:`Flask.before_request`. Such a function is executed """Like :meth:`Flask.before_request`. Such a function is executed
before each request, even if outside of a blueprint. before each request, even if outside of a blueprint.
@ -530,6 +545,7 @@ class Blueprint(Scaffold):
) )
return f return f
@setupmethod
def before_app_first_request( def before_app_first_request(
self, f: BeforeFirstRequestCallable self, f: BeforeFirstRequestCallable
) -> BeforeFirstRequestCallable: ) -> BeforeFirstRequestCallable:
@ -548,6 +564,7 @@ class Blueprint(Scaffold):
) )
return f return f
@setupmethod
def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable:
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a """Like :meth:`Flask.teardown_request` but for a blueprint. Such a
function is executed when tearing down each request, even if outside of function is executed when tearing down each request, even if outside of
@ -558,6 +575,7 @@ class Blueprint(Scaffold):
) )
return f return f
@setupmethod
def app_context_processor( def app_context_processor(
self, f: TemplateContextProcessorCallable self, f: TemplateContextProcessorCallable
) -> TemplateContextProcessorCallable: ) -> TemplateContextProcessorCallable:
@ -569,6 +587,7 @@ class Blueprint(Scaffold):
) )
return f return f
@setupmethod
def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable:
"""Like :meth:`Flask.errorhandler` but for a blueprint. This """Like :meth:`Flask.errorhandler` but for a blueprint. This
handler is used for all requests, even if outside of the blueprint. handler is used for all requests, even if outside of the blueprint.
@ -580,6 +599,7 @@ class Blueprint(Scaffold):
return decorator return decorator
@setupmethod
def app_url_value_preprocessor( def app_url_value_preprocessor(
self, f: URLValuePreprocessorCallable self, f: URLValuePreprocessorCallable
) -> URLValuePreprocessorCallable: ) -> URLValuePreprocessorCallable:
@ -589,6 +609,7 @@ class Blueprint(Scaffold):
) )
return f return f
@setupmethod
def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable:
"""Same as :meth:`url_defaults` but application wide.""" """Same as :meth:`url_defaults` but application wide."""
self.record_once( self.record_once(

View file

@ -37,22 +37,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def setupmethod(f: F) -> F: def setupmethod(f: F) -> F:
"""Wraps a method so that it performs a check in debug mode if the f_name = f.__name__
first request was already handled.
"""
def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any: def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
if self._is_setup_finished(): self._check_setup_finished(f_name)
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."
)
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return t.cast(F, update_wrapper(wrapper_func, f)) return t.cast(F, update_wrapper(wrapper_func, f))
@ -239,7 +227,7 @@ class Scaffold:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{type(self).__name__} {self.name!r}>" 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 raise NotImplementedError
@property @property
@ -376,6 +364,7 @@ class Scaffold:
return self.route(rule, methods=[method], **options) return self.route(rule, methods=[method], **options)
@setupmethod
def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
"""Shortcut for :meth:`route` with ``methods=["GET"]``. """Shortcut for :meth:`route` with ``methods=["GET"]``.
@ -383,6 +372,7 @@ class Scaffold:
""" """
return self._method_route("GET", rule, options) return self._method_route("GET", rule, options)
@setupmethod
def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
"""Shortcut for :meth:`route` with ``methods=["POST"]``. """Shortcut for :meth:`route` with ``methods=["POST"]``.
@ -390,6 +380,7 @@ class Scaffold:
""" """
return self._method_route("POST", rule, options) return self._method_route("POST", rule, options)
@setupmethod
def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
"""Shortcut for :meth:`route` with ``methods=["PUT"]``. """Shortcut for :meth:`route` with ``methods=["PUT"]``.
@ -397,6 +388,7 @@ class Scaffold:
""" """
return self._method_route("PUT", rule, options) return self._method_route("PUT", rule, options)
@setupmethod
def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
"""Shortcut for :meth:`route` with ``methods=["DELETE"]``. """Shortcut for :meth:`route` with ``methods=["DELETE"]``.
@ -404,6 +396,7 @@ class Scaffold:
""" """
return self._method_route("DELETE", rule, options) return self._method_route("DELETE", rule, options)
@setupmethod
def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
"""Shortcut for :meth:`route` with ``methods=["PATCH"]``. """Shortcut for :meth:`route` with ``methods=["PATCH"]``.