Skip to content

Commit a52a7db

Browse files
authored
Merge pull request #4577 from hallacy/hallacy/fix_4571
update setupmethod behavior
2 parents ca8e621 + e044b00 commit a52a7db

File tree

5 files changed

+102
-83
lines changed

5 files changed

+102
-83
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Unreleased
1616

1717
- Refactor ``register_error_handler`` to consolidate error checking.
1818
Rewrite some error messages to be more consistent. :issue:`4559`
19+
- Use Blueprint decorators and functions intended for setup after
20+
registering the blueprint will show a warning. In the next version,
21+
this will become an error just like the application setup methods.
22+
:issue:`4571`
1923

2024

2125
Version 2.1.2

src/flask/app.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,17 @@ def __init__(
541541
# the app's commands to another CLI tool.
542542
self.cli.name = self.name
543543

544-
def _is_setup_finished(self) -> bool:
545-
return self.debug and self._got_first_request
544+
def _check_setup_finished(self, f_name: str) -> None:
545+
if self._got_first_request:
546+
raise AssertionError(
547+
f"The setup method '{f_name}' can no longer be called"
548+
" on the application. It has already handled its first"
549+
" request, any changes will not be applied"
550+
" consistently.\n"
551+
"Make sure all imports, decorators, functions, etc."
552+
" needed to set up the application are done before"
553+
" running it."
554+
)
546555

547556
@locked_cached_property
548557
def name(self) -> str: # type: ignore

src/flask/blueprints.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .scaffold import _endpoint_from_view_func
77
from .scaffold import _sentinel
88
from .scaffold import Scaffold
9+
from .scaffold import setupmethod
910
from .typing import AfterRequestCallable
1011
from .typing import BeforeFirstRequestCallable
1112
from .typing import BeforeRequestCallable
@@ -162,7 +163,6 @@ class Blueprint(Scaffold):
162163
.. versionadded:: 0.7
163164
"""
164165

165-
warn_on_modifications = False
166166
_got_registered_once = False
167167

168168
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
@@ -208,27 +208,33 @@ def __init__(
208208
self.cli_group = cli_group
209209
self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
210210

211-
def _is_setup_finished(self) -> bool:
212-
return self.warn_on_modifications and self._got_registered_once
211+
def _check_setup_finished(self, f_name: str) -> None:
212+
if self._got_registered_once:
213+
import warnings
214+
215+
warnings.warn(
216+
f"The setup method '{f_name}' can no longer be called on"
217+
f" the blueprint '{self.name}'. It has already been"
218+
" registered at least once, any changes will not be"
219+
" applied consistently.\n"
220+
"Make sure all imports, decorators, functions, etc."
221+
" needed to set up the blueprint are done before"
222+
" registering it.\n"
223+
"This warning will become an exception in Flask 2.3.",
224+
UserWarning,
225+
stacklevel=3,
226+
)
213227

228+
@setupmethod
214229
def record(self, func: t.Callable) -> None:
215230
"""Registers a function that is called when the blueprint is
216231
registered on the application. This function is called with the
217232
state as argument as returned by the :meth:`make_setup_state`
218233
method.
219234
"""
220-
if self._got_registered_once and self.warn_on_modifications:
221-
from warnings import warn
222-
223-
warn(
224-
Warning(
225-
"The blueprint was already registered once but is"
226-
" getting modified now. These changes will not show"
227-
" up."
228-
)
229-
)
230235
self.deferred_functions.append(func)
231236

237+
@setupmethod
232238
def record_once(self, func: t.Callable) -> None:
233239
"""Works like :meth:`record` but wraps the function in another
234240
function that will ensure the function is only called once. If the
@@ -251,6 +257,7 @@ def make_setup_state(
251257
"""
252258
return BlueprintSetupState(self, app, options, first_registration)
253259

260+
@setupmethod
254261
def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
255262
"""Register a :class:`~flask.Blueprint` on this blueprint. Keyword
256263
arguments passed to this method will override the defaults set
@@ -390,6 +397,7 @@ def extend(bp_dict, parent_dict):
390397
bp_options["name_prefix"] = name
391398
blueprint.register(app, bp_options)
392399

400+
@setupmethod
393401
def add_url_rule(
394402
self,
395403
rule: str,
@@ -417,6 +425,7 @@ def add_url_rule(
417425
)
418426
)
419427

428+
@setupmethod
420429
def app_template_filter(
421430
self, name: t.Optional[str] = None
422431
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
@@ -433,6 +442,7 @@ def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable:
433442

434443
return decorator
435444

445+
@setupmethod
436446
def add_app_template_filter(
437447
self, f: TemplateFilterCallable, name: t.Optional[str] = None
438448
) -> None:
@@ -449,6 +459,7 @@ def register_template(state: BlueprintSetupState) -> None:
449459

450460
self.record_once(register_template)
451461

462+
@setupmethod
452463
def app_template_test(
453464
self, name: t.Optional[str] = None
454465
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
@@ -467,6 +478,7 @@ def decorator(f: TemplateTestCallable) -> TemplateTestCallable:
467478

468479
return decorator
469480

481+
@setupmethod
470482
def add_app_template_test(
471483
self, f: TemplateTestCallable, name: t.Optional[str] = None
472484
) -> None:
@@ -485,6 +497,7 @@ def register_template(state: BlueprintSetupState) -> None:
485497

486498
self.record_once(register_template)
487499

500+
@setupmethod
488501
def app_template_global(
489502
self, name: t.Optional[str] = None
490503
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
@@ -503,6 +516,7 @@ def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable:
503516

504517
return decorator
505518

519+
@setupmethod
506520
def add_app_template_global(
507521
self, f: TemplateGlobalCallable, name: t.Optional[str] = None
508522
) -> None:
@@ -521,6 +535,7 @@ def register_template(state: BlueprintSetupState) -> None:
521535

522536
self.record_once(register_template)
523537

538+
@setupmethod
524539
def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
525540
"""Like :meth:`Flask.before_request`. Such a function is executed
526541
before each request, even if outside of a blueprint.
@@ -530,6 +545,7 @@ def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
530545
)
531546
return f
532547

548+
@setupmethod
533549
def before_app_first_request(
534550
self, f: BeforeFirstRequestCallable
535551
) -> BeforeFirstRequestCallable:
@@ -548,6 +564,7 @@ def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
548564
)
549565
return f
550566

567+
@setupmethod
551568
def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable:
552569
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
553570
function is executed when tearing down each request, even if outside of
@@ -558,6 +575,7 @@ def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable:
558575
)
559576
return f
560577

578+
@setupmethod
561579
def app_context_processor(
562580
self, f: TemplateContextProcessorCallable
563581
) -> TemplateContextProcessorCallable:
@@ -569,6 +587,7 @@ def app_context_processor(
569587
)
570588
return f
571589

590+
@setupmethod
572591
def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable:
573592
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
574593
handler is used for all requests, even if outside of the blueprint.
@@ -580,6 +599,7 @@ def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable":
580599

581600
return decorator
582601

602+
@setupmethod
583603
def app_url_value_preprocessor(
584604
self, f: URLValuePreprocessorCallable
585605
) -> URLValuePreprocessorCallable:
@@ -589,6 +609,7 @@ def app_url_value_preprocessor(
589609
)
590610
return f
591611

612+
@setupmethod
592613
def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable:
593614
"""Same as :meth:`url_defaults` but application wide."""
594615
self.record_once(

src/flask/scaffold.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
from .typing import URLValuePreprocessorCallable
2828

2929
if t.TYPE_CHECKING: # pragma: no cover
30-
from .wrappers import Response
3130
from .typing import ErrorHandlerCallable
31+
from .wrappers import Response
3232

3333
# a singleton sentinel value for parameter defaults
3434
_sentinel = object()
@@ -37,22 +37,10 @@
3737

3838

3939
def setupmethod(f: F) -> F:
40-
"""Wraps a method so that it performs a check in debug mode if the
41-
first request was already handled.
42-
"""
40+
f_name = f.__name__
4341

4442
def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
45-
if self._is_setup_finished():
46-
raise AssertionError(
47-
"A setup function was called after the first request "
48-
"was handled. This usually indicates a bug in the"
49-
" application where a module was not imported and"
50-
" decorators or other functionality was called too"
51-
" late.\nTo fix this make sure to import all your view"
52-
" modules, database models, and everything related at a"
53-
" central place before the application starts serving"
54-
" requests."
55-
)
43+
self._check_setup_finished(f_name)
5644
return f(self, *args, **kwargs)
5745

5846
return t.cast(F, update_wrapper(wrapper_func, f))
@@ -239,7 +227,7 @@ def __init__(
239227
def __repr__(self) -> str:
240228
return f"<{type(self).__name__} {self.name!r}>"
241229

242-
def _is_setup_finished(self) -> bool:
230+
def _check_setup_finished(self, f_name: str) -> None:
243231
raise NotImplementedError
244232

245233
@property
@@ -376,41 +364,47 @@ def _method_route(
376364

377365
return self.route(rule, methods=[method], **options)
378366

367+
@setupmethod
379368
def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
380369
"""Shortcut for :meth:`route` with ``methods=["GET"]``.
381370
382371
.. versionadded:: 2.0
383372
"""
384373
return self._method_route("GET", rule, options)
385374

375+
@setupmethod
386376
def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
387377
"""Shortcut for :meth:`route` with ``methods=["POST"]``.
388378
389379
.. versionadded:: 2.0
390380
"""
391381
return self._method_route("POST", rule, options)
392382

383+
@setupmethod
393384
def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
394385
"""Shortcut for :meth:`route` with ``methods=["PUT"]``.
395386
396387
.. versionadded:: 2.0
397388
"""
398389
return self._method_route("PUT", rule, options)
399390

391+
@setupmethod
400392
def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
401393
"""Shortcut for :meth:`route` with ``methods=["DELETE"]``.
402394
403395
.. versionadded:: 2.0
404396
"""
405397
return self._method_route("DELETE", rule, options)
406398

399+
@setupmethod
407400
def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
408401
"""Shortcut for :meth:`route` with ``methods=["PATCH"]``.
409402
410403
.. versionadded:: 2.0
411404
"""
412405
return self._method_route("PATCH", rule, options)
413406

407+
@setupmethod
414408
def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
415409
"""Decorate a view function to register it with the given URL
416410
rule and options. Calls :meth:`add_url_rule`, which has more
@@ -510,6 +504,7 @@ def index():
510504
"""
511505
raise NotImplementedError
512506

507+
@setupmethod
513508
def endpoint(self, endpoint: str) -> t.Callable:
514509
"""Decorate a view function to register it for the given
515510
endpoint. Used if a rule is added without a ``view_func`` with

0 commit comments

Comments
 (0)