From b146a13f633b0e2e26e3b8383b3db0feb563bc83 Mon Sep 17 00:00:00 2001 From: yk396 Date: Wed, 22 Jul 2020 15:02:31 -0400 Subject: [PATCH] extract common Flask/Blueprint API to Scaffold base class Co-authored-by: Chris Nguyen --- CHANGES.rst | 5 + src/flask/app.py | 367 ++--------------------------------- src/flask/blueprints.py | 177 +++++------------ src/flask/helpers.py | 8 - src/flask/scaffold.py | 411 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+), 495 deletions(-) create mode 100644 src/flask/scaffold.py diff --git a/CHANGES.rst b/CHANGES.rst index 43a6b5e790..effd3b13d8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,11 @@ Unreleased For example, this allows setting the ``Content-Type`` for ``jsonify()``. Use ``response.headers.extend()`` if extending is desired. :issue:`3628` +- The ``Scaffold`` class provides a common API for the ``Flask`` and + ``Blueprint`` classes. ``Blueprint`` information is stored in + attributes just like ``Flask``, rather than opaque lambda functions. + This is intended to improve consistency and maintainability. + :issue:`3215` Version 1.1.x diff --git a/src/flask/app.py b/src/flask/app.py index 6c9d0b57b6..f4d3565809 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1,7 +1,6 @@ import os import sys from datetime import timedelta -from functools import update_wrapper from itertools import chain from threading import Lock @@ -9,7 +8,6 @@ from werkzeug.datastructures import ImmutableDict from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError -from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError from werkzeug.routing import BuildError @@ -30,8 +28,6 @@ from .globals import g from .globals import request from .globals import session -from .helpers import _endpoint_from_view_func -from .helpers import _PackageBoundObject from .helpers import find_package from .helpers import get_debug_flag from .helpers import get_env @@ -41,21 +37,21 @@ from .helpers import url_for from .json import jsonify from .logging import create_logger +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod from .sessions import SecureCookieSessionInterface from .signals import appcontext_tearing_down from .signals import got_request_exception from .signals import request_finished from .signals import request_started from .signals import request_tearing_down -from .templating import _default_template_ctx_processor from .templating import DispatchingJinjaLoader from .templating import Environment from .wrappers import Request from .wrappers import Response -# a singleton sentinel value for parameter defaults -_sentinel = object() - def _make_timedelta(value): if not isinstance(value, timedelta): @@ -63,28 +59,7 @@ def _make_timedelta(value): return value -def setupmethod(f): - """Wraps a method so that it performs a check in debug mode if the - first request was already handled. - """ - - def wrapper_func(self, *args, **kwargs): - if self.debug and self._got_first_request: - 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.\n" - "To 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 update_wrapper(wrapper_func, f) - - -class Flask(_PackageBoundObject): +class Flask(Scaffold): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for @@ -394,13 +369,14 @@ def __init__( instance_relative_config=False, root_path=None, ): - _PackageBoundObject.__init__( - self, import_name, template_folder=template_folder, root_path=root_path + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, ) - self.static_url_path = static_url_path - self.static_folder = static_folder - if instance_path is None: instance_path = self.auto_find_instance_path() elif not os.path.isabs(instance_path): @@ -419,24 +395,6 @@ def __init__( #: to load a config from files. self.config = self.make_config(instance_relative_config) - #: A dictionary of all view functions registered. The keys will - #: be function names which are also used to generate URLs and - #: the values are the function objects themselves. - #: To register a view function, use the :meth:`route` decorator. - self.view_functions = {} - - #: A dictionary of all registered error handlers. The key is ``None`` - #: for error handlers active on the application, otherwise the key is - #: the name of the blueprint. Each key points to another dictionary - #: where the key is the status code of the http exception. The - #: special key ``None`` points to a list of tuples where the first item - #: is the class for the instance check and the second the error handler - #: function. - #: - #: To register an error handler, use the :meth:`errorhandler` - #: decorator. - self.error_handler_spec = {} - #: A list of functions that are called when :meth:`url_for` raises a #: :exc:`~werkzeug.routing.BuildError`. Each function registered here #: is called with `error`, `endpoint` and `values`. If a function @@ -446,13 +404,6 @@ def __init__( #: .. versionadded:: 0.9 self.url_build_error_handlers = [] - #: A dictionary with lists of functions that will be called at the - #: beginning of each request. The key of the dictionary is the name of - #: the blueprint this function is active for, or ``None`` for all - #: requests. To register a function, use the :meth:`before_request` - #: decorator. - self.before_request_funcs = {} - #: A list of functions that will be called at the beginning of the #: first request to this instance. To register a function, use the #: :meth:`before_first_request` decorator. @@ -460,25 +411,6 @@ def __init__( #: .. versionadded:: 0.8 self.before_first_request_funcs = [] - #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the blueprint - #: this function is active for, ``None`` for all requests. This can for - #: example be used to close database connections. To register a function - #: here, use the :meth:`after_request` decorator. - self.after_request_funcs = {} - - #: A dictionary with lists of functions that are called after - #: each request, even if an exception has occurred. The key of the - #: dictionary is the name of the blueprint this function is active for, - #: ``None`` for all requests. These functions are not allowed to modify - #: the request, and their return values are ignored. If an exception - #: occurred while processing the request, it gets passed to each - #: teardown_request function. To register a function here, use the - #: :meth:`teardown_request` decorator. - #: - #: .. versionadded:: 0.7 - self.teardown_request_funcs = {} - #: A list of functions that are called when the application context #: is destroyed. Since the application context is also torn down #: if the request ends this is the place to store code that disconnects @@ -487,35 +419,6 @@ def __init__( #: .. versionadded:: 0.9 self.teardown_appcontext_funcs = [] - #: A dictionary with lists of functions that are called before the - #: :attr:`before_request_funcs` functions. The key of the dictionary is - #: the name of the blueprint this function is active for, or ``None`` - #: for all requests. To register a function, use - #: :meth:`url_value_preprocessor`. - #: - #: .. versionadded:: 0.7 - self.url_value_preprocessors = {} - - #: A dictionary with lists of functions that can be used as URL value - #: preprocessors. The key ``None`` here is used for application wide - #: callbacks, otherwise the key is the name of the blueprint. - #: Each of these functions has the chance to modify the dictionary - #: of URL values before they are used as the keyword arguments of the - #: view function. For each function registered this one should also - #: provide a :meth:`url_defaults` function that adds the parameters - #: automatically again that were removed that way. - #: - #: .. versionadded:: 0.7 - self.url_default_functions = {} - - #: A dictionary with list of functions that are called without argument - #: to populate the template context. The key of the dictionary is the - #: name of the blueprint this function is active for, ``None`` for all - #: requests. Each returns a dictionary that the template context is - #: updated with. To register a function here, use the - #: :meth:`context_processor` decorator. - self.template_context_processors = {None: [_default_template_ctx_processor]} - #: A list of shell context processor functions that should be run #: when a shell context is created. #: @@ -586,6 +489,9 @@ def __init__( # the app's commands to another CLI tool. self.cli.name = self.name + def _is_setup_finished(self): + return self.debug and self._got_first_request + @locked_cached_property def name(self): """The name of the application. This is usually the import name @@ -1206,152 +1112,6 @@ def index(): ) self.view_functions[endpoint] = view_func - def route(self, rule, **options): - """A decorator that is used to register a view function for a - given URL rule. This does the same thing as :meth:`add_url_rule` - but is intended for decorator usage:: - - @app.route('/') - def index(): - return 'Hello World' - - For more information refer to :ref:`url-route-registrations`. - - :param rule: the URL rule as string - :param endpoint: the endpoint for the registered URL rule. Flask - itself assumes the name of the view function as - endpoint - :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. A change - to Werkzeug is handling of method options. methods - is a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). - Starting with Flask 0.6, ``OPTIONS`` is implicitly - added and handled by the standard request handling. - """ - - def decorator(f): - endpoint = options.pop("endpoint", None) - self.add_url_rule(rule, endpoint, f, **options) - return f - - return decorator - - @setupmethod - def endpoint(self, endpoint): - """A decorator to register a function as an endpoint. - Example:: - - @app.endpoint('example.endpoint') - def example(): - return "example" - - :param endpoint: the name of the endpoint - """ - - def decorator(f): - self.view_functions[endpoint] = f - return f - - return decorator - - @staticmethod - def _get_exc_class_and_code(exc_class_or_code): - """Get the exception class being handled. For HTTP status codes - or ``HTTPException`` subclasses, return both the exception and - status code. - - :param exc_class_or_code: Any exception class, or an HTTP status - code as an integer. - """ - if isinstance(exc_class_or_code, int): - exc_class = default_exceptions[exc_class_or_code] - else: - exc_class = exc_class_or_code - - assert issubclass( - exc_class, Exception - ), "Custom exceptions must be subclasses of Exception." - - if issubclass(exc_class, HTTPException): - return exc_class, exc_class.code - else: - return exc_class, None - - @setupmethod - def errorhandler(self, code_or_exception): - """Register a function to handle errors by code or exception class. - - A decorator that is used to register a function given an - error code. Example:: - - @app.errorhandler(404) - def page_not_found(error): - return 'This page does not exist', 404 - - You can also register handlers for arbitrary exceptions:: - - @app.errorhandler(DatabaseError) - def special_exception_handler(error): - return 'Database connection failed', 500 - - .. versionadded:: 0.7 - Use :meth:`register_error_handler` instead of modifying - :attr:`error_handler_spec` directly, for application wide error - handlers. - - .. versionadded:: 0.7 - One can now additionally also register custom exception types - that do not necessarily have to be a subclass of the - :class:`~werkzeug.exceptions.HTTPException` class. - - :param code_or_exception: the code as integer for the handler, or - an arbitrary exception - """ - - def decorator(f): - self._register_error_handler(None, code_or_exception, f) - return f - - return decorator - - @setupmethod - def register_error_handler(self, code_or_exception, f): - """Alternative error attach function to the :meth:`errorhandler` - decorator that is more straightforward to use for non decorator - usage. - - .. versionadded:: 0.7 - """ - self._register_error_handler(None, code_or_exception, f) - - @setupmethod - def _register_error_handler(self, key, code_or_exception, f): - """ - :type key: None|str - :type code_or_exception: int|T<=Exception - :type f: callable - """ - if isinstance(code_or_exception, HTTPException): # old broken behavior - raise ValueError( - "Tried to register a handler for an exception instance" - f" {code_or_exception!r}. Handlers can only be" - " registered for exception classes or HTTP error codes." - ) - - try: - exc_class, code = self._get_exc_class_and_code(code_or_exception) - except KeyError: - raise KeyError( - f"'{code_or_exception}' is not a recognized HTTP error" - " code. Use a subclass of HTTPException with that code" - " instead." - ) - - handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) - handlers[exc_class] = f - @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. @@ -1455,20 +1215,6 @@ def add_template_global(self, f, name=None): """ self.jinja_env.globals[name or f.__name__] = f - @setupmethod - def before_request(self, f): - """Registers a function to run before each request. - - For example, this can be used to open a database connection, or to load - the logged in user from the session. - - The function will be called without any arguments. If it returns a - non-None value, the value is handled as if it was the return value from - the view, and further request handling is stopped. - """ - self.before_request_funcs.setdefault(None, []).append(f) - return f - @setupmethod def before_first_request(self, f): """Registers a function to be run before the first request to this @@ -1482,59 +1228,6 @@ def before_first_request(self, f): self.before_first_request_funcs.append(f) return f - @setupmethod - def after_request(self, f): - """Register a function to be run after each request. - - Your function must take one parameter, an instance of - :attr:`response_class` and return a new response object or the - same (see :meth:`process_response`). - - As of Flask 0.7 this function might not be executed at the end of the - request in case an unhandled exception occurred. - """ - self.after_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def teardown_request(self, f): - """Register a function to be run at the end of each request, - regardless of whether there was an exception or not. These functions - are executed when the request context is popped, even if not an - actual request was performed. - - Example:: - - ctx = app.test_request_context() - ctx.push() - ... - ctx.pop() - - When ``ctx.pop()`` is executed in the above example, the teardown - functions are called just before the request context moves from the - stack of active contexts. This becomes relevant if you are using - such constructs in tests. - - Generally teardown functions must take every necessary step to avoid - that they will fail. If they do execute code that might fail they - will have to surround the execution of these code by try/except - statements and log occurring errors. - - When a teardown function was called because of an exception it will - be passed an error object. - - The return values of teardown functions are ignored. - - .. admonition:: Debug Note - - In debug mode Flask will not tear down a request on an exception - immediately. Instead it will keep it alive so that the interactive - debugger can still access it. This behavior can be controlled - by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. - """ - self.teardown_request_funcs.setdefault(None, []).append(f) - return f - @setupmethod def teardown_appcontext(self, f): """Registers a function to be called when the application context @@ -1568,12 +1261,6 @@ def teardown_appcontext(self, f): self.teardown_appcontext_funcs.append(f) return f - @setupmethod - def context_processor(self, f): - """Registers a template context processor function.""" - self.template_context_processors[None].append(f) - return f - @setupmethod def shell_context_processor(self, f): """Registers a shell context processor function. @@ -1583,32 +1270,6 @@ def shell_context_processor(self, f): self.shell_context_processors.append(f) return f - @setupmethod - def url_value_preprocessor(self, f): - """Register a URL value preprocessor function for all view - functions in the application. These functions will be called before the - :meth:`before_request` functions. - - The function can modify the values captured from the matched url before - they are passed to the view. For example, this can be used to pop a - common language code value and place it in ``g`` rather than pass it to - every view. - - The function is passed the endpoint name and values dict. The return - value is ignored. - """ - self.url_value_preprocessors.setdefault(None, []).append(f) - return f - - @setupmethod - def url_defaults(self, f): - """Callback function for URL defaults for all view functions of the - application. It's called with the endpoint and values and should - update the values passed in place. - """ - self.url_default_functions.setdefault(None, []).append(f) - return f - def _find_error_handler(self, e): """Return a registered error handler for an exception in this order: blueprint handler for a specific code, app handler for a specific code, diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 01bcc23361..2e030861a1 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -1,10 +1,8 @@ from functools import update_wrapper -from .helpers import _endpoint_from_view_func -from .helpers import _PackageBoundObject - -# a singleton sentinel value for parameter defaults -_sentinel = object() +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold class BlueprintSetupState: @@ -76,7 +74,7 @@ def add_url_rule(self, rule, endpoint=None, view_func=None, **options): ) -class Blueprint(_PackageBoundObject): +class Blueprint(Scaffold): """Represents a blueprint, a collection of routes and other app-related functions that can be registered on a real application later. @@ -167,20 +165,25 @@ def __init__( root_path=None, cli_group=_sentinel, ): - _PackageBoundObject.__init__( - self, import_name, template_folder, root_path=root_path + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, ) self.name = name self.url_prefix = url_prefix self.subdomain = subdomain - self.static_folder = static_folder - self.static_url_path = static_url_path self.deferred_functions = [] if url_defaults is None: url_defaults = {} self.url_values_defaults = url_defaults self.cli_group = cli_group + def _is_setup_finished(self): + return self.warn_on_modifications and self._got_registered_once + def record(self, func): """Registers a function that is called when the blueprint is registered on the application. This function is called with the @@ -241,6 +244,36 @@ def register(self, app, options, first_registration=False): endpoint="static", ) + # Merge app and self dictionaries. + def merge_dict_lists(self_dict, app_dict): + """Merges self_dict into app_dict. Replaces None keys with self.name. + Values of dict must be lists. + """ + for key, values in self_dict.items(): + key = self.name if key is None else f"{self.name}.{key}" + app_dict.setdefault(key, []).extend(values) + + def merge_dict_nested(self_dict, app_dict): + """Merges self_dict into app_dict. Replaces None keys with self.name. + Values of dict must be dict. + """ + for key, value in self_dict.items(): + key = self.name if key is None else f"{self.name}.{key}" + app_dict[key] = value + + app.view_functions.update(self.view_functions) + + merge_dict_lists(self.before_request_funcs, app.before_request_funcs) + merge_dict_lists(self.after_request_funcs, app.after_request_funcs) + merge_dict_lists(self.teardown_request_funcs, app.teardown_request_funcs) + merge_dict_lists(self.url_default_functions, app.url_default_functions) + merge_dict_lists(self.url_value_preprocessors, app.url_value_preprocessors) + merge_dict_lists( + self.template_context_processors, app.template_context_processors + ) + + merge_dict_nested(self.error_handler_spec, app.error_handler_spec) + for deferred in self.deferred_functions: deferred(state) @@ -258,18 +291,6 @@ def register(self, app, options, first_registration=False): self.cli.name = cli_resolved_group app.cli.add_command(self.cli) - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a blueprint. The endpoint for the - :func:`url_for` function is prefixed with the name of the blueprint. - """ - - def decorator(f): - endpoint = options.pop("endpoint", f.__name__) - self.add_url_rule(rule, endpoint, f, **options) - return f - - return decorator - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for the :func:`url_for` function is prefixed with the name of the blueprint. @@ -282,23 +303,6 @@ def add_url_rule(self, rule, endpoint=None, view_func=None, **options): ), "Blueprint view function name should not contain dots" self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) - def endpoint(self, endpoint): - """Like :meth:`Flask.endpoint` but for a blueprint. This does not - prefix the endpoint with the blueprint name, this has to be done - explicitly by the user of this method. If the endpoint is prefixed - with a `.` it will be registered to the current blueprint, otherwise - it's an application independent endpoint. - """ - - def decorator(f): - def register_endpoint(state): - state.app.view_functions[endpoint] = f - - self.record_once(register_endpoint) - return f - - return decorator - def app_template_filter(self, name=None): """Register a custom template filter, available application wide. Like :meth:`Flask.template_filter` but for a blueprint. @@ -391,16 +395,6 @@ def register_template(state): self.record_once(register_template) - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a blueprint. This function - is only executed before each request that is handled by a function of - that blueprint. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(self.name, []).append(f) - ) - return f - def before_app_request(self, f): """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. @@ -417,16 +411,6 @@ def before_app_first_request(self, f): self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a blueprint. This function - is only executed after each request that is handled by a function of - that blueprint. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(self.name, []).append(f) - ) - return f - def after_app_request(self, f): """Like :meth:`Flask.after_request` but for a blueprint. Such a function is executed after each request, even if outside of the blueprint. @@ -436,18 +420,6 @@ def after_app_request(self, f): ) return f - def teardown_request(self, f): - """Like :meth:`Flask.teardown_request` but for a blueprint. This - function is only executed when tearing down requests handled by a - function of that blueprint. Teardown request functions are executed - when the request context is popped, even when no actual request was - performed. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(self.name, []).append(f) - ) - return f - def teardown_app_request(self, f): """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of @@ -458,17 +430,6 @@ def teardown_app_request(self, f): ) return f - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a blueprint. This - function is only executed for requests handled by a blueprint. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault( - self.name, [] - ).append(f) - ) - return f - def app_context_processor(self, f): """Like :meth:`Flask.context_processor` but for a blueprint. Such a function is executed each request, even if outside of the blueprint. @@ -489,26 +450,6 @@ def decorator(f): return decorator - def url_value_preprocessor(self, f): - """Registers a function as URL value preprocessor for this - blueprint. It's called before the view functions are called and - can modify the url values provided. - """ - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(self.name, []).append(f) - ) - return f - - def url_defaults(self, f): - """Callback function for URL defaults for this blueprint. It's called - with the endpoint and values and should update the values passed - in place. - """ - self.record_once( - lambda s: s.app.url_default_functions.setdefault(self.name, []).append(f) - ) - return f - def app_url_value_preprocessor(self, f): """Same as :meth:`url_value_preprocessor` but application wide. """ @@ -524,35 +465,3 @@ def app_url_defaults(self, f): lambda s: s.app.url_default_functions.setdefault(None, []).append(f) ) return f - - def errorhandler(self, code_or_exception): - """Registers an error handler that becomes active for this blueprint - only. Please be aware that routing does not happen local to a - blueprint so an error handler for 404 usually is not handled by - a blueprint unless it is caused inside a view function. Another - special case is the 500 internal server error which is always looked - up from the application. - - Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator - of the :class:`~flask.Flask` object. - """ - - def decorator(f): - self.record_once( - lambda s: s.app._register_error_handler(self.name, code_or_exception, f) - ) - return f - - return decorator - - def register_error_handler(self, code_or_exception, f): - """Non-decorator version of the :meth:`errorhandler` error attach - function, akin to the :meth:`~flask.Flask.register_error_handler` - application-wide function of the :class:`~flask.Flask` object but - for error handlers limited to this blueprint. - - .. versionadded:: 0.11 - """ - self.record_once( - lambda s: s.app._register_error_handler(self.name, code_or_exception, f) - ) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index e46c6d26de..ede2dc446c 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -76,14 +76,6 @@ def get_load_dotenv(default=True): return val.lower() in ("0", "false", "no") -def _endpoint_from_view_func(view_func): - """Internal helper that returns the default endpoint for a given - function. This always is the function name. - """ - assert view_func is not None, "expected view func if endpoint is not provided." - return view_func.__name__ - - def stream_with_context(generator_or_function): """Request contexts disappear when the response is started on the server. This is done for efficiency reasons and to make it less likely to encounter diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py new file mode 100644 index 0000000000..f37b415d6f --- /dev/null +++ b/src/flask/scaffold.py @@ -0,0 +1,411 @@ +from functools import update_wrapper + +from werkzeug.exceptions import default_exceptions +from werkzeug.exceptions import HTTPException + +from .helpers import _PackageBoundObject +from .templating import _default_template_ctx_processor + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + + def wrapper_func(self, *args, **kwargs): + 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.\n" + "To 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 update_wrapper(wrapper_func, f) + + +class Scaffold(_PackageBoundObject): + """A common base for class Flask and class Blueprint. + """ + + #: Skeleton local JSON decoder class to use. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`. + json_encoder = None + + #: Skeleton local JSON decoder class to use. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`. + json_decoder = None + + #: The name of the package or module that this app belongs to. Do not + #: change this once it is set by the constructor. + import_name = None + + #: Location of the template files to be added to the template lookup. + #: ``None`` if templates should not be added. + template_folder = None + + #: Absolute path to the package on the filesystem. Used to look up + #: resources contained in the package. + root_path = None + + def __init__( + self, + import_name, + static_folder="static", + static_url_path=None, + template_folder=None, + root_path=None, + ): + super().__init__( + import_name=import_name, + template_folder=template_folder, + root_path=root_path, + ) + self.static_folder = static_folder + self.static_url_path = static_url_path + + #: A dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: To register a view function, use the :meth:`route` decorator. + self.view_functions = {} + + #: A dictionary of all registered error handlers. The key is ``None`` + #: for error handlers active on the application, otherwise the key is + #: the name of the blueprint. Each key points to another dictionary + #: where the key is the status code of the http exception. The + #: special key ``None`` points to a list of tuples where the first item + #: is the class for the instance check and the second the error handler + #: function. + #: + #: To register an error handler, use the :meth:`errorhandler` + #: decorator. + self.error_handler_spec = {} + + #: A dictionary with lists of functions that will be called at the + #: beginning of each request. The key of the dictionary is the name of + #: the blueprint this function is active for, or ``None`` for all + #: requests. To register a function, use the :meth:`before_request` + #: decorator. + self.before_request_funcs = {} + + #: A dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the blueprint + #: this function is active for, ``None`` for all requests. This can for + #: example be used to close database connections. To register a function + #: here, use the :meth:`after_request` decorator. + self.after_request_funcs = {} + + #: A dictionary with lists of functions that are called after + #: each request, even if an exception has occurred. The key of the + #: dictionary is the name of the blueprint this function is active for, + #: ``None`` for all requests. These functions are not allowed to modify + #: the request, and their return values are ignored. If an exception + #: occurred while processing the request, it gets passed to each + #: teardown_request function. To register a function here, use the + #: :meth:`teardown_request` decorator. + #: + #: .. versionadded:: 0.7 + self.teardown_request_funcs = {} + + #: A dictionary with list of functions that are called without argument + #: to populate the template context. The key of the dictionary is the + #: name of the blueprint this function is active for, ``None`` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = {None: [_default_template_ctx_processor]} + + #: A dictionary with lists of functions that are called before the + #: :attr:`before_request_funcs` functions. The key of the dictionary is + #: the name of the blueprint this function is active for, or ``None`` + #: for all requests. To register a function, use + #: :meth:`url_value_preprocessor`. + #: + #: .. versionadded:: 0.7 + self.url_value_preprocessors = {} + + #: A dictionary with lists of functions that can be used as URL value + #: preprocessors. The key ``None`` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: of URL values before they are used as the keyword arguments of the + #: view function. For each function registered this one should also + #: provide a :meth:`url_defaults` function that adds the parameters + #: automatically again that were removed that way. + #: + #: .. versionadded:: 0.7 + self.url_default_functions = {} + + def _is_setup_finished(self): + raise NotImplementedError + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. This does the same thing as :meth:`add_url_rule` + but is intended for decorator usage:: + + @app.route('/') + def index(): + return 'Hello World' + + For more information refer to :ref:`url-route-registrations`. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (``GET``, ``POST`` etc.). By default a rule + just listens for ``GET`` (and implicitly ``HEAD``). + Starting with Flask 0.6, ``OPTIONS`` is implicitly + added and handled by the standard request handling. + """ + + def decorator(f): + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) + return f + + return decorator + + @setupmethod + def add_url_rule( + self, + rule, + endpoint=None, + view_func=None, + provide_automatic_options=None, + **options, + ): + raise NotImplementedError + + def endpoint(self, endpoint): + """A decorator to register a function as an endpoint. + Example:: + + @app.endpoint('example.endpoint') + def example(): + return "example" + + :param endpoint: the name of the endpoint + """ + + def decorator(f): + self.view_functions[endpoint] = f + return f + + return decorator + + @setupmethod + def before_request(self, f): + """Registers a function to run before each request. + + For example, this can be used to open a database connection, or to load + the logged in user from the session. + + The function will be called without any arguments. If it returns a + non-None value, the value is handled as if it was the return value from + the view, and further request handling is stopped. + """ + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def after_request(self, f): + """Register a function to be run after each request. + + Your function must take one parameter, an instance of + :attr:`response_class` and return a new response object or the + same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception occurred. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f): + """Register a function to be run at the end of each request, + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necessary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log occurring errors. + + When a teardown function was called because of an exception it will + be passed an error object. + + The return values of teardown functions are ignored. + + .. admonition:: Debug Note + + In debug mode Flask will not tear down a request on an exception + immediately. Instead it will keep it alive so that the interactive + debugger can still access it. This behavior can be controlled + by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor(self, f): + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. + """ + self.url_value_preprocessors.setdefault(None, []).append(f) + return f + + @setupmethod + def url_defaults(self, f): + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + """ + self.url_default_functions.setdefault(None, []).append(f) + return f + + @setupmethod + def errorhandler(self, code_or_exception): + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception + """ + + def decorator(f): + self._register_error_handler(None, code_or_exception, f) + return f + + return decorator + + @setupmethod + def register_error_handler(self, code_or_exception, f): + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + self._register_error_handler(None, code_or_exception, f) + + @setupmethod + def _register_error_handler(self, key, code_or_exception, f): + """ + :type key: None|str + :type code_or_exception: int|T<=Exception + :type f: callable + """ + if isinstance(code_or_exception, HTTPException): # old broken behavior + raise ValueError( + "Tried to register a handler for an exception instance" + f" {code_or_exception!r}. Handlers can only be" + " registered for exception classes or HTTP error codes." + ) + + try: + exc_class, code = self._get_exc_class_and_code(code_or_exception) + except KeyError: + raise KeyError( + f"'{code_or_exception}' is not a recognized HTTP error" + " code. Use a subclass of HTTPException with that code" + " instead." + ) + + handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) + handlers[exc_class] = f + + @staticmethod + def _get_exc_class_and_code(exc_class_or_code): + """Get the exception class being handled. For HTTP status codes + or ``HTTPException`` subclasses, return both the exception and + status code. + + :param exc_class_or_code: Any exception class, or an HTTP status + code as an integer. + """ + if isinstance(exc_class_or_code, int): + exc_class = default_exceptions[exc_class_or_code] + else: + exc_class = exc_class_or_code + + assert issubclass( + exc_class, Exception + ), "Custom exceptions must be subclasses of Exception." + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + + +def _endpoint_from_view_func(view_func): + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__