diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 76b36067..b73a9536 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -12,6 +12,7 @@ from .scaffold import setupmethod if t.TYPE_CHECKING: # pragma: no cover from .app import Flask + DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] @@ -68,6 +69,7 @@ class BlueprintSetupState: self.url_defaults = dict(self.blueprint.url_values_defaults) self.url_defaults.update(self.options.get("url_defaults", ())) + def add_url_rule( self, rule: str, @@ -82,12 +84,17 @@ class BlueprintSetupState: if self.url_prefix is not None: if rule: rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) @@ -162,6 +169,7 @@ class Blueprint(Scaffold): #: the app's :class:`~flask.Flask.json_decoder`. json_decoder = None + def __init__( self, name: str, @@ -198,7 +206,9 @@ class Blueprint(Scaffold): self.cli_group = cli_group self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] + def _check_setup_finished(self, f_name: str) -> None: + # TODO - This method is missing a docstring - please advise if self._got_registered_once: import warnings @@ -215,6 +225,7 @@ class Blueprint(Scaffold): stacklevel=3, ) + @setupmethod def record(self, func: t.Callable) -> None: """Registers a function that is called when the blueprint is @@ -224,6 +235,7 @@ class Blueprint(Scaffold): """ self.deferred_functions.append(func) + @setupmethod def record_once(self, func: t.Callable) -> None: """Works like :meth:`record` but wraps the function in another @@ -231,13 +243,13 @@ class Blueprint(Scaffold): blueprint is registered a second time on the application, the function passed is not called. """ - def wrapper(state: BlueprintSetupState) -> None: if state.first_registration: func(state) return self.record(update_wrapper(wrapper, func)) + def make_setup_state( self, app: "Flask", options: dict, first_registration: bool = False ) -> BlueprintSetupState: @@ -247,6 +259,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 @@ -265,6 +278,7 @@ class Blueprint(Scaffold): raise ValueError("Cannot register a blueprint on itself") self._blueprints.append((blueprint, options)) + def register(self, app: "Flask", options: dict) -> None: """Called by :meth:`Flask.register_blueprint` to register all views and callbacks registered on the blueprint with the @@ -322,11 +336,13 @@ class Blueprint(Scaffold): # Merge blueprint data into parent. if first_bp_registration or first_name_registration: + def extend(bp_dict, parent_dict): for key, values in bp_dict.items(): key = name if key is None else f"{name}.{key}" parent_dict[key].extend(values) + for key, value in self.error_handler_spec.items(): key = name if key is None else f"{name}.{key}" value = defaultdict( @@ -361,9 +377,11 @@ class Blueprint(Scaffold): if self.cli.commands: if cli_resolved_group is None: app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: self.cli.name = name app.cli.add_command(self.cli) + else: self.cli.name = cli_resolved_group app.cli.add_command(self.cli) @@ -379,14 +397,17 @@ class Blueprint(Scaffold): bp_options["url_prefix"] = ( state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") ) + elif bp_url_prefix is not None: bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: bp_options["url_prefix"] = state.url_prefix bp_options["name_prefix"] = name blueprint.register(app, bp_options) + @setupmethod def add_url_rule( self, @@ -415,6 +436,7 @@ class Blueprint(Scaffold): ) ) + @setupmethod def app_template_filter( self, name: t.Optional[str] = None @@ -425,13 +447,13 @@ class Blueprint(Scaffold): :param name: the optional name of the filter, otherwise the function name will be used. """ - def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable: self.add_app_template_filter(f, name=name) return f return decorator + @setupmethod def add_app_template_filter( self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None @@ -443,12 +465,12 @@ class Blueprint(Scaffold): :param name: the optional name of the filter, otherwise the function name will be used. """ - def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.filters[name or f.__name__] = f self.record_once(register_template) + @setupmethod def app_template_test( self, name: t.Optional[str] = None @@ -461,7 +483,6 @@ class Blueprint(Scaffold): :param name: the optional name of the test, otherwise the function name will be used. """ - def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable: self.add_app_template_test(f, name=name) return f @@ -481,12 +502,12 @@ class Blueprint(Scaffold): :param name: the optional name of the test, otherwise the function name will be used. """ - def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.tests[name or f.__name__] = f self.record_once(register_template) + @setupmethod def app_template_global( self, name: t.Optional[str] = None @@ -499,13 +520,13 @@ class Blueprint(Scaffold): :param name: the optional name of the global, otherwise the function name will be used. """ - def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable: self.add_app_template_global(f, name=name) return f return decorator + @setupmethod def add_app_template_global( self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None @@ -519,12 +540,12 @@ class Blueprint(Scaffold): :param name: the optional name of the global, otherwise the function name will be used. """ - def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.globals[name or f.__name__] = f self.record_once(register_template) + @setupmethod def before_app_request( self, f: ft.BeforeRequestCallable @@ -537,6 +558,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def before_app_first_request( self, f: ft.BeforeFirstRequestCallable @@ -557,9 +579,11 @@ class Blueprint(Scaffold): DeprecationWarning, stacklevel=2, ) + self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f + def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: """Like :meth:`Flask.after_request` but for a blueprint. Such a function is executed after each request, even if outside of the blueprint. @@ -569,6 +593,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a @@ -580,6 +605,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_context_processor( self, f: ft.TemplateContextProcessorCallable @@ -592,6 +618,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_errorhandler( self, code: t.Union[t.Type[Exception], int] @@ -599,13 +626,13 @@ class Blueprint(Scaffold): """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. """ - def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: self.record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator + @setupmethod def app_url_value_preprocessor( self, f: ft.URLValuePreprocessorCallable @@ -616,6 +643,7 @@ class Blueprint(Scaffold): ) return f + @setupmethod def app_url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: """Same as :meth:`url_defaults` but application wide.""" diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b1e3ce1b..ff556d69 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -9,6 +9,7 @@ class UnexpectedUnicodeError(AssertionError, UnicodeError): """Raised in places where we want some better error reporting for unexpected unicode or binary data. """ + pass class DebugFilesKeyError(KeyError, AssertionError): @@ -26,6 +27,7 @@ class DebugFilesKeyError(KeyError, AssertionError): " were transmitted. To fix this error you should provide" ' enctype="multipart/form-data" in your form.' ] + if form_matches: names = ", ".join(repr(x) for x in form_matches) buf.append( @@ -71,16 +73,16 @@ class FormDataRoutingRedirect(AssertionError): def attach_enctype_error_multidict(request): """Patch ``request.files.__getitem__`` to raise a descriptive error about ``enctype=multipart/form-data``. - :param request: The request to patch. :meta private: """ oldcls = request.files.__class__ - class newcls(oldcls): + class NewCls(oldcls): def __getitem__(self, key): try: return super().__getitem__(key) + except KeyError as e: if key not in request.form: raise @@ -89,23 +91,28 @@ def attach_enctype_error_multidict(request): e.__traceback__ ) from None - newcls.__name__ = oldcls.__name__ - newcls.__module__ = oldcls.__module__ - request.files.__class__ = newcls + NewCls.__name__ = oldcls.__name__ + NewCls.__module__ = oldcls.__module__ + request.files.__class__ = NewCls def _dump_loader_info(loader) -> t.Generator: + # TODO - This method is missing a docstring - please advise yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): if key.startswith("_"): continue + if isinstance(value, (tuple, list)): if not all(isinstance(x, str) for x in value): continue yield f"{key}:" + for item in value: yield f" - {item}" continue + elif not isinstance(value, (str, int, float, bool)): continue yield f"{key}: {value!r}" @@ -117,14 +124,17 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None: total_found = 0 blueprint = None reqctx = _request_ctx_stack.top + if reqctx is not None and reqctx.request.blueprint is not None: blueprint = reqctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): if isinstance(srcobj, Flask): src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: src_info = repr(srcobj) @@ -135,15 +145,19 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None: if triple is None: detail = "no match" + else: detail = f"found ({triple[1] or ''!r})" total_found += 1 + info.append(f" -> {detail}") seems_fishy = False + if total_found == 0: info.append("Error: the template could not be found.") seems_fishy = True + elif total_found > 1: info.append("Warning: multiple loaders returned a match for the template.") seems_fishy = True