diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index 985c8a0d..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,1574 +0,0 @@ -Version 3.1.0 -------------- - -- Provide a configuration option to control automatic option - responses. :pr:`5496` -- ``Flask.open_resource``/``open_instance_resource`` and - ``Blueprint.open_resource`` take an ``encoding`` parameter to use when - opening in text mode. It defaults to ``utf-8``. :issue:`5504` - -Version 3.0.3 -------------- - -Released 2024-04-07 - -- The default ``hashlib.sha1`` may not be available in FIPS builds. Don't - access it at import time so the developer has time to change the default. - :issue:`5448` -- Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in - the ``Flask`` concrete class. :pr:`5270` - - -Version 3.0.2 -------------- - -Released 2024-02-03 - -- Correct type for ``jinja_loader`` property. :issue:`5388` -- Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options. - :issue:`5391` - - -Version 3.0.1 -------------- - -Released 2024-01-18 - -- Correct type for ``path`` argument to ``send_file``. :issue:`5230` -- Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344` -- Session data is untagged without relying on the built-in ``json.loads`` - ``object_hook``. This allows other JSON providers that don't implement that. - :issue:`5381` -- Address more type findings when using mypy strict mode. :pr:`5383` - - -Version 3.0.0 -------------- - -Released 2023-09-30 - -- Remove previously deprecated code. :pr:`5223` -- Deprecate the ``__version__`` attribute. Use feature detection, or - ``importlib.metadata.version("flask")``, instead. :issue:`5230` -- Restructure the code such that the Flask (app) and Blueprint - classes have Sans-IO bases. :pr:`5127` -- Allow self as an argument to url_for. :pr:`5264` -- Require Werkzeug >= 3.0.0. - - -Version 2.3.3 -------------- - -Released 2023-08-21 - -- Python 3.12 compatibility. -- Require Werkzeug >= 2.3.7. -- Use ``flit_core`` instead of ``setuptools`` as build backend. -- Refactor how an app's root and instance paths are determined. :issue:`5160` - - -Version 2.3.2 -------------- - -Released 2023-05-01 - -- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. -- Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes. - - -Version 2.3.1 -------------- - -Released 2023-04-25 - -- Restore deprecated ``from flask import Markup``. :issue:`5084` - - -Version 2.3.0 -------------- - -Released 2023-04-25 - -- Drop support for Python 3.7. :pr:`5072` -- Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, - itsdangerous>=2.1.2, click>=8.1.3. -- Remove previously deprecated code. :pr:`4995` - - - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and - ``_request_ctx_stack`` objects are removed. ``top`` still exists to give - extensions more time to update, but it will be removed. - - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` - property are removed. - - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, - ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` - are removed. - - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and - ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. - - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators - are removed. - - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the - corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. - - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. - - Calling setup methods on blueprints after registration is an error instead of a - warning. :pr:`4997` - -- Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them - directly from ``markupsafe`` instead. :pr:`4996` -- The ``app.got_first_request`` property is deprecated. :pr:`4997` -- The ``locked_cached_property`` decorator is deprecated. Use a lock inside the - decorated function if locking is needed. :issue:`4993` -- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The - ``signals_available`` attribute is deprecated. :issue:`5056` -- Signals support ``async`` subscriber functions. :pr:`5049` -- Remove uses of locks that could cause requests to block each other very briefly. - :issue:`4993` -- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. - :pr:`4947` -- Ensure subdomains are applied with nested blueprints. :issue:`4834` -- ``config.from_file`` can use ``text=False`` to indicate that the parser wants a - binary file instead. :issue:`4989` -- If a blueprint is created with an empty name it raises a ``ValueError``. - :issue:`5010` -- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not - to set the domain, which modern browsers interpret as an exact match rather than - a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. - :issue:`5051` -- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain - matching is in use. :issue:`5004` -- Use postponed evaluation of annotations. :pr:`5071` - - -Version 2.2.5 -------------- - -Released 2023-05-02 - -- Update for compatibility with Werkzeug 2.3.3. -- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. - - -Version 2.2.4 -------------- - -Released 2023-04-25 - -- Update for compatibility with Werkzeug 2.3. - - -Version 2.2.3 -------------- - -Released 2023-02-15 - -- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` -- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` -- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` - - -Version 2.2.2 -------------- - -Released 2022-08-08 - -- Update Werkzeug dependency to >= 2.2.2. This includes fixes related - to the new faster router, header parsing, and the development - server. :pr:`4754` -- Fix the default value for ``app.env`` to be ``"production"``. This - attribute remains deprecated. :issue:`4740` - - -Version 2.2.1 -------------- - -Released 2022-08-03 - -- Setting or accessing ``json_encoder`` or ``json_decoder`` raises a - deprecation warning. :issue:`4732` - - -Version 2.2.0 -------------- - -Released 2022-08-01 - -- Remove previously deprecated code. :pr:`4667` - - - Old names for some ``send_file`` parameters have been removed. - ``download_name`` replaces ``attachment_filename``, ``max_age`` - replaces ``cache_timeout``, and ``etag`` replaces ``add_etags``. - Additionally, ``path`` replaces ``filename`` in - ``send_from_directory``. - - The ``RequestContext.g`` property returning ``AppContext.g`` is - removed. - -- Update Werkzeug dependency to >= 2.2. -- The app and request contexts are managed using Python context vars - directly rather than Werkzeug's ``LocalStack``. This should result - in better performance and memory use. :pr:`4682` - - - Extension maintainers, be aware that ``_app_ctx_stack.top`` - and ``_request_ctx_stack.top`` are deprecated. Store data on - ``g`` instead using a unique prefix, like - ``g._extension_name_attr``. - -- The ``FLASK_ENV`` environment variable and ``app.env`` attribute are - deprecated, removing the distinction between development and debug - mode. Debug mode should be controlled directly using the ``--debug`` - option or ``app.run(debug=True)``. :issue:`4714` -- Some attributes that proxied config keys on ``app`` are deprecated: - ``session_cookie_name``, ``send_file_max_age_default``, - ``use_x_sendfile``, ``propagate_exceptions``, and - ``templates_auto_reload``. Use the relevant config keys instead. - :issue:`4716` -- Add new customization points to the ``Flask`` app object for many - previously global behaviors. - - - ``flask.url_for`` will call ``app.url_for``. :issue:`4568` - - ``flask.abort`` will call ``app.aborter``. - ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used - to customize this aborter. :issue:`4567` - - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` - - ``flask.json`` is an instance of ``JSONProvider``. A different - provider can be set to use a different JSON library. - ``flask.jsonify`` will call ``app.json.response``, other - functions in ``flask.json`` will call corresponding functions in - ``app.json``. :pr:`4692` - -- JSON configuration is moved to attributes on the default - ``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, - ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are - deprecated. :pr:`4692` -- Setting custom ``json_encoder`` and ``json_decoder`` classes on the - app or a blueprint, and the corresponding ``json.JSONEncoder`` and - ``JSONDecoder`` classes, are deprecated. JSON behavior can now be - overridden using the ``app.json`` provider interface. :pr:`4692` -- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated, - the function is built-in to Jinja now. :pr:`4692` -- Refactor ``register_error_handler`` to consolidate error checking. - Rewrite some error messages to be more consistent. :issue:`4559` -- Use Blueprint decorators and functions intended for setup after - registering the blueprint will show a warning. In the next version, - this will become an error just like the application setup methods. - :issue:`4571` -- ``before_first_request`` is deprecated. Run setup code when creating - the application instead. :issue:`4605` -- Added the ``View.init_every_request`` class attribute. If a view - subclass sets this to ``False``, the view will not create a new - instance on every request. :issue:`2520`. -- A ``flask.cli.FlaskGroup`` Click group can be nested as a - sub-command in a custom CLI. :issue:`3263` -- Add ``--app`` and ``--debug`` options to the ``flask`` CLI, instead - of requiring that they are set through environment variables. - :issue:`2836` -- Add ``--env-file`` option to the ``flask`` CLI. This allows - specifying a dotenv file to load in addition to ``.env`` and - ``.flaskenv``. :issue:`3108` -- It is no longer required to decorate custom CLI commands on - ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app - context will already be active at that point. :issue:`2410` -- ``SessionInterface.get_expiration_time`` uses a timezone-aware - value. :pr:`4645` -- View functions can return generators directly instead of wrapping - them in a ``Response``. :pr:`4629` -- Add ``stream_template`` and ``stream_template_string`` functions to - render a template as a stream of pieces. :pr:`4629` -- A new implementation of context preservation during debugging and - testing. :pr:`4666` - - - ``request``, ``g``, and other context-locals point to the - correct data when running code in the interactive debugger - console. :issue:`2836` - - Teardown functions are always run at the end of the request, - even if the context is preserved. They are also run after the - preserved context is popped. - - ``stream_with_context`` preserves context separately from a - ``with client`` block. It will be cleaned up when - ``response.get_data()`` or ``response.close()`` is called. - -- Allow returning a list from a view function, to convert it to a - JSON response like a dict is. :issue:`4672` -- When type checking, allow ``TypedDict`` to be returned from view - functions. :pr:`4695` -- Remove the ``--eager-loading/--lazy-loading`` options from the - ``flask run`` command. The app is always eager loaded the first - time, then lazily loaded in the reloader. The reloader always prints - errors immediately but continues serving. Remove the internal - ``DispatchingApp`` middleware used by the previous implementation. - :issue:`4715` - - -Version 2.1.3 -------------- - -Released 2022-07-13 - -- Inline some optional imports that are only used for certain CLI - commands. :pr:`4606` -- Relax type annotation for ``after_request`` functions. :issue:`4600` -- ``instance_path`` for namespace packages uses the path closest to - the imported submodule. :issue:`4610` -- Clearer error message when ``render_template`` and - ``render_template_string`` are used outside an application context. - :pr:`4693` - - -Version 2.1.2 -------------- - -Released 2022-04-28 - -- Fix type annotation for ``json.loads``, it accepts str or bytes. - :issue:`4519` -- The ``--cert`` and ``--key`` options on ``flask run`` can be given - in either order. :issue:`4459` - - -Version 2.1.1 -------------- - -Released on 2022-03-30 - -- Set the minimum required version of importlib_metadata to 3.6.0, - which is required on Python < 3.10. :issue:`4502` - - -Version 2.1.0 -------------- - -Released 2022-03-28 - -- Drop support for Python 3.6. :pr:`4335` -- Update Click dependency to >= 8.0. :pr:`4008` -- Remove previously deprecated code. :pr:`4337` - - - The CLI does not pass ``script_info`` to app factory functions. - - ``config.from_json`` is replaced by - ``config.from_file(name, load=json.load)``. - - ``json`` functions no longer take an ``encoding`` parameter. - - ``safe_join`` is removed, use ``werkzeug.utils.safe_join`` - instead. - - ``total_seconds`` is removed, use ``timedelta.total_seconds`` - instead. - - The same blueprint cannot be registered with the same name. Use - ``name=`` when registering to specify a unique name. - - The test client's ``as_tuple`` parameter is removed. Use - ``response.request.environ`` instead. :pr:`4417` - -- Some parameters in ``send_file`` and ``send_from_directory`` were - renamed in 2.0. The deprecation period for the old names is extended - to 2.2. Be sure to test with deprecation warnings visible. - - - ``attachment_filename`` is renamed to ``download_name``. - - ``cache_timeout`` is renamed to ``max_age``. - - ``add_etags`` is renamed to ``etag``. - - ``filename`` is renamed to ``path``. - -- The ``RequestContext.g`` property is deprecated. Use ``g`` directly - or ``AppContext.g`` instead. :issue:`3898` -- ``copy_current_request_context`` can decorate async functions. - :pr:`4303` -- The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to - load command entry points. :issue:`4419` -- Overriding ``FlaskClient.open`` will not cause an error on redirect. - :issue:`3396` -- Add an ``--exclude-patterns`` option to the ``flask run`` CLI - command to specify patterns that will be ignored by the reloader. - :issue:`4188` -- When using lazy loading (the default with the debugger), the Click - context from the ``flask run`` command remains available in the - loader thread. :issue:`4460` -- Deleting the session cookie uses the ``httponly`` flag. - :issue:`4485` -- Relax typing for ``errorhandler`` to allow the user to use more - precise types and decorate the same function multiple times. - :issue:`4095, 4295, 4297` -- Fix typing for ``__exit__`` methods for better compatibility with - ``ExitStack``. :issue:`4474` -- From Werkzeug, for redirect responses the ``Location`` header URL - will remain relative, and exclude the scheme and domain, by default. - :pr:`4496` -- Add ``Config.from_prefixed_env()`` to load config values from - environment variables that start with ``FLASK_`` or another prefix. - This parses values as JSON by default, and allows setting keys in - nested dicts. :pr:`4479` - - -Version 2.0.3 -------------- - -Released 2022-02-14 - -- The test client's ``as_tuple`` parameter is deprecated and will be - removed in Werkzeug 2.1. It is now also deprecated in Flask, to be - removed in Flask 2.1, while remaining compatible with both in - 2.0.x. Use ``response.request.environ`` instead. :pr:`4341` -- Fix type annotation for ``errorhandler`` decorator. :issue:`4295` -- Revert a change to the CLI that caused it to hide ``ImportError`` - tracebacks when importing the application. :issue:`4307` -- ``app.json_encoder`` and ``json_decoder`` are only passed to - ``dumps`` and ``loads`` if they have custom behavior. This improves - performance, mainly on PyPy. :issue:`4349` -- Clearer error message when ``after_this_request`` is used outside a - request context. :issue:`4333` - - -Version 2.0.2 -------------- - -Released 2021-10-04 - -- Fix type annotation for ``teardown_*`` methods. :issue:`4093` -- Fix type annotation for ``before_request`` and ``before_app_request`` - decorators. :issue:`4104` -- Fixed the issue where typing requires template global - decorators to accept functions with no arguments. :issue:`4098` -- Support View and MethodView instances with async handlers. :issue:`4112` -- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095` -- Fix registering a blueprint twice with differing names. :issue:`4124` -- Fix the type of ``static_folder`` to accept ``pathlib.Path``. - :issue:`4150` -- ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``. - :issue:`4157` -- Correctly handle raising deferred errors in CLI lazy loading. - :issue:`4096` -- The CLI loader handles ``**kwargs`` in a ``create_app`` function. - :issue:`4170` -- Fix the order of ``before_request`` and other callbacks that trigger - before the view returns. They are called from the app down to the - closest nested blueprint. :issue:`4229` - - -Version 2.0.1 -------------- - -Released 2021-05-21 - -- Re-add the ``filename`` parameter in ``send_from_directory``. The - ``filename`` parameter has been renamed to ``path``, the old name - is deprecated. :pr:`4019` -- Mark top-level names as exported so type checking understands - imports in user projects. :issue:`4024` -- Fix type annotation for ``g`` and inform mypy that it is a namespace - object that has arbitrary attributes. :issue:`4020` -- Fix some types that weren't available in Python 3.6.0. :issue:`4040` -- Improve typing for ``send_file``, ``send_from_directory``, and - ``get_send_file_max_age``. :issue:`4044`, :pr:`4026` -- Show an error when a blueprint name contains a dot. The ``.`` has - special meaning, it is used to separate (nested) blueprint names and - the endpoint name. :issue:`4041` -- Combine URL prefixes when nesting blueprints that were created with - a ``url_prefix`` value. :issue:`4037` -- Revert a change to the order that URL matching was done. The - URL is again matched after the session is loaded, so the session is - available in custom URL converters. :issue:`4053` -- Re-add deprecated ``Config.from_json``, which was accidentally - removed early. :issue:`4078` -- Improve typing for some functions using ``Callable`` in their type - signatures, focusing on decorator factories. :issue:`4060` -- Nested blueprints are registered with their dotted name. This allows - different blueprints with the same name to be nested at different - locations. :issue:`4069` -- ``register_blueprint`` takes a ``name`` option to change the - (pre-dotted) name the blueprint is registered with. This allows the - same blueprint to be registered multiple times with unique names for - ``url_for``. Registering the same blueprint with the same name - multiple times is deprecated. :issue:`1091` -- Improve typing for ``stream_with_context``. :issue:`4052` - - -Version 2.0.0 -------------- - -Released 2021-05-11 - -- Drop support for Python 2 and 3.5. -- Bump minimum versions of other Pallets projects: Werkzeug >= 2, - Jinja2 >= 3, MarkupSafe >= 2, ItsDangerous >= 2, Click >= 8. Be sure - to check the change logs for each project. For better compatibility - with other applications (e.g. Celery) that still require Click 7, - there is no hard dependency on Click 8 yet, but using Click 7 will - trigger a DeprecationWarning and Flask 2.1 will depend on Click 8. -- JSON support no longer uses simplejson. To use another JSON module, - override ``app.json_encoder`` and ``json_decoder``. :issue:`3555` -- The ``encoding`` option to JSON functions is deprecated. :pr:`3562` -- Passing ``script_info`` to app factory functions is deprecated. This - was not portable outside the ``flask`` command. Use - ``click.get_current_context().obj`` if it's needed. :issue:`3552` -- The CLI shows better error messages when the app failed to load - when looking up commands. :issue:`2741` -- Add ``SessionInterface.get_cookie_name`` to allow setting the - session cookie name dynamically. :pr:`3369` -- Add ``Config.from_file`` to load config using arbitrary file - loaders, such as ``toml.load`` or ``json.load``. - ``Config.from_json`` is deprecated in favor of this. :pr:`3398` -- The ``flask run`` command will only defer errors on reload. Errors - present during the initial call will cause the server to exit with - the traceback immediately. :issue:`3431` -- ``send_file`` raises a ``ValueError`` when passed an ``io`` object - in text mode. Previously, it would respond with 200 OK and an empty - file. :issue:`3358` -- When using ad-hoc certificates, check for the cryptography library - instead of PyOpenSSL. :pr:`3492` -- When specifying a factory function with ``FLASK_APP``, keyword - argument can be passed. :issue:`3553` -- When loading a ``.env`` or ``.flaskenv`` file, the current working - directory is no longer changed to the location of the file. - :pr:`3560` -- When returning a ``(response, headers)`` tuple from a view, the - headers replace rather than extend existing headers on the response. - 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` -- Include ``samesite`` and ``secure`` options when removing the - session cookie. :pr:`3726` -- Support passing a ``pathlib.Path`` to ``static_folder``. :pr:`3579` -- ``send_file`` and ``send_from_directory`` are wrappers around the - implementations in ``werkzeug.utils``. :pr:`3828` -- Some ``send_file`` parameters have been renamed, the old names are - deprecated. ``attachment_filename`` is renamed to ``download_name``. - ``cache_timeout`` is renamed to ``max_age``. ``add_etags`` is - renamed to ``etag``. :pr:`3828, 3883` -- ``send_file`` passes ``download_name`` even if - ``as_attachment=False`` by using ``Content-Disposition: inline``. - :pr:`3828` -- ``send_file`` sets ``conditional=True`` and ``max_age=None`` by - default. ``Cache-Control`` is set to ``no-cache`` if ``max_age`` is - not set, otherwise ``public``. This tells browsers to validate - conditional requests instead of using a timed cache. :pr:`3828` -- ``helpers.safe_join`` is deprecated. Use - ``werkzeug.utils.safe_join`` instead. :pr:`3828` -- The request context does route matching before opening the session. - This could allow a session interface to change behavior based on - ``request.endpoint``. :issue:`3776` -- Use Jinja's implementation of the ``|tojson`` filter. :issue:`3881` -- Add route decorators for common HTTP methods. For example, - ``@app.post("/login")`` is a shortcut for - ``@app.route("/login", methods=["POST"])``. :pr:`3907` -- Support async views, error handlers, before and after request, and - teardown functions. :pr:`3412` -- Support nesting blueprints. :issue:`593, 1548`, :pr:`3923` -- Set the default encoding to "UTF-8" when loading ``.env`` and - ``.flaskenv`` files to allow to use non-ASCII characters. :issue:`3931` -- ``flask shell`` sets up tab and history completion like the default - ``python`` shell if ``readline`` is installed. :issue:`3941` -- ``helpers.total_seconds()`` is deprecated. Use - ``timedelta.total_seconds()`` instead. :pr:`3962` -- Add type hinting. :pr:`3973`. - - -Version 1.1.4 -------------- - -Released 2021-05-13 - -- Update ``static_folder`` to use ``_compat.fspath`` instead of - ``os.fspath`` to continue supporting Python < 3.6 :issue:`4050` - - -Version 1.1.3 -------------- - -Released 2021-05-13 - -- Set maximum versions of Werkzeug, Jinja, Click, and ItsDangerous. - :issue:`4043` -- Re-add support for passing a ``pathlib.Path`` for ``static_folder``. - :pr:`3579` - - -Version 1.1.2 -------------- - -Released 2020-04-03 - -- Work around an issue when running the ``flask`` command with an - external debugger on Windows. :issue:`3297` -- The static route will not catch all URLs if the ``Flask`` - ``static_folder`` argument ends with a slash. :issue:`3452` - - -Version 1.1.1 -------------- - -Released 2019-07-08 - -- The ``flask.json_available`` flag was added back for compatibility - with some extensions. It will raise a deprecation warning when used, - and will be removed in version 2.0.0. :issue:`3288` - - -Version 1.1.0 -------------- - -Released 2019-07-04 - -- Bump minimum Werkzeug version to >= 0.15. -- Drop support for Python 3.4. -- Error handlers for ``InternalServerError`` or ``500`` will always be - passed an instance of ``InternalServerError``. If they are invoked - due to an unhandled exception, that original exception is now - available as ``e.original_exception`` rather than being passed - directly to the handler. The same is true if the handler is for the - base ``HTTPException``. This makes error handler behavior more - consistent. :pr:`3266` - - - ``Flask.finalize_request`` is called for all unhandled - exceptions even if there is no ``500`` error handler. - -- ``Flask.logger`` takes the same name as ``Flask.name`` (the value - passed as ``Flask(import_name)``. This reverts 1.0's behavior of - always logging to ``"flask.app"``, in order to support multiple apps - in the same process. A warning will be shown if old configuration is - detected that needs to be moved. :issue:`2866` -- ``RequestContext.copy`` includes the current session object in the - request context copy. This prevents ``session`` pointing to an - out-of-date object. :issue:`2935` -- Using built-in RequestContext, unprintable Unicode characters in - Host header will result in a HTTP 400 response and not HTTP 500 as - previously. :pr:`2994` -- ``send_file`` supports ``PathLike`` objects as described in - :pep:`519`, to support ``pathlib`` in Python 3. :pr:`3059` -- ``send_file`` supports ``BytesIO`` partial content. - :issue:`2957` -- ``open_resource`` accepts the "rt" file mode. This still does the - same thing as "r". :issue:`3163` -- The ``MethodView.methods`` attribute set in a base class is used by - subclasses. :issue:`3138` -- ``Flask.jinja_options`` is a ``dict`` instead of an - ``ImmutableDict`` to allow easier configuration. Changes must still - be made before creating the environment. :pr:`3190` -- Flask's ``JSONMixin`` for the request and response wrappers was - moved into Werkzeug. Use Werkzeug's version with Flask-specific - support. This bumps the Werkzeug dependency to >= 0.15. - :issue:`3125` -- The ``flask`` command entry point is simplified to take advantage - of Werkzeug 0.15's better reloader support. This bumps the Werkzeug - dependency to >= 0.15. :issue:`3022` -- Support ``static_url_path`` that ends with a forward slash. - :issue:`3134` -- Support empty ``static_folder`` without requiring setting an empty - ``static_url_path`` as well. :pr:`3124` -- ``jsonify`` supports ``dataclass`` objects. :pr:`3195` -- Allow customizing the ``Flask.url_map_class`` used for routing. - :pr:`3069` -- The development server port can be set to 0, which tells the OS to - pick an available port. :issue:`2926` -- The return value from ``cli.load_dotenv`` is more consistent with - the documentation. It will return ``False`` if python-dotenv is not - installed, or if the given path isn't a file. :issue:`2937` -- Signaling support has a stub for the ``connect_via`` method when - the Blinker library is not installed. :pr:`3208` -- Add an ``--extra-files`` option to the ``flask run`` CLI command to - specify extra files that will trigger the reloader on change. - :issue:`2897` -- Allow returning a dictionary from a view function. Similar to how - returning a string will produce a ``text/html`` response, returning - a dict will call ``jsonify`` to produce a ``application/json`` - response. :pr:`3111` -- Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands - registered with a blueprint will be available as a group under the - ``flask`` command. :issue:`1357`. -- When using the test client as a context manager (``with client:``), - all preserved request contexts are popped when the block exits, - ensuring nested contexts are cleaned up correctly. :pr:`3157` -- Show a better error message when the view return type is not - supported. :issue:`3214` -- ``flask.testing.make_test_environ_builder()`` has been deprecated in - favour of a new class ``flask.testing.EnvironBuilder``. :pr:`3232` -- The ``flask run`` command no longer fails if Python is not built - with SSL support. Using the ``--cert`` option will show an - appropriate error message. :issue:`3211` -- URL matching now occurs after the request context is pushed, rather - than when it's created. This allows custom URL converters to access - the app and request contexts, such as to query a database for an id. - :issue:`3088` - - -Version 1.0.4 -------------- - -Released 2019-07-04 - -- The key information for ``BadRequestKeyError`` is no longer cleared - outside debug mode, so error handlers can still access it. This - requires upgrading to Werkzeug 0.15.5. :issue:`3249` -- ``send_file`` url quotes the ":" and "/" characters for more - compatible UTF-8 filename support in some browsers. :issue:`3074` -- Fixes for :pep:`451` import loaders and pytest 5.x. :issue:`3275` -- Show message about dotenv on stderr instead of stdout. :issue:`3285` - - -Version 1.0.3 -------------- - -Released 2019-05-17 - -- ``send_file`` encodes filenames as ASCII instead of Latin-1 - (ISO-8859-1). This fixes compatibility with Gunicorn, which is - stricter about header encodings than :pep:`3333`. :issue:`2766` -- Allow custom CLIs using ``FlaskGroup`` to set the debug flag without - it always being overwritten based on environment variables. - :pr:`2765` -- ``flask --version`` outputs Werkzeug's version and simplifies the - Python version. :pr:`2825` -- ``send_file`` handles an ``attachment_filename`` that is a native - Python 2 string (bytes) with UTF-8 coded bytes. :issue:`2933` -- A catch-all error handler registered for ``HTTPException`` will not - handle ``RoutingException``, which is used internally during - routing. This fixes the unexpected behavior that had been introduced - in 1.0. :pr:`2986` -- Passing the ``json`` argument to ``app.test_client`` does not - push/pop an extra app context. :issue:`2900` - - -Version 1.0.2 -------------- - -Released 2018-05-02 - -- Fix more backwards compatibility issues with merging slashes between - a blueprint prefix and route. :pr:`2748` -- Fix error with ``flask routes`` command when there are no routes. - :issue:`2751` - - -Version 1.0.1 -------------- - -Released 2018-04-29 - -- Fix registering partials (with no ``__name__``) as view functions. - :pr:`2730` -- Don't treat lists returned from view functions the same as tuples. - Only tuples are interpreted as response data. :issue:`2736` -- Extra slashes between a blueprint's ``url_prefix`` and a route URL - are merged. This fixes some backwards compatibility issues with the - change in 1.0. :issue:`2731`, :issue:`2742` -- Only trap ``BadRequestKeyError`` errors in debug mode, not all - ``BadRequest`` errors. This allows ``abort(400)`` to continue - working as expected. :issue:`2735` -- The ``FLASK_SKIP_DOTENV`` environment variable can be set to ``1`` - to skip automatically loading dotenv files. :issue:`2722` - - -Version 1.0 ------------ - -Released 2018-04-26 - -- Python 2.6 and 3.3 are no longer supported. -- Bump minimum dependency versions to the latest stable versions: - Werkzeug >= 0.14, Jinja >= 2.10, itsdangerous >= 0.24, Click >= 5.1. - :issue:`2586` -- Skip ``app.run`` when a Flask application is run from the command - line. This avoids some behavior that was confusing to debug. -- Change the default for ``JSONIFY_PRETTYPRINT_REGULAR`` to - ``False``. ``~json.jsonify`` returns a compact format by default, - and an indented format in debug mode. :pr:`2193` -- ``Flask.__init__`` accepts the ``host_matching`` argument and sets - it on ``Flask.url_map``. :issue:`1559` -- ``Flask.__init__`` accepts the ``static_host`` argument and passes - it as the ``host`` argument when defining the static route. - :issue:`1559` -- ``send_file`` supports Unicode in ``attachment_filename``. - :pr:`2223` -- Pass ``_scheme`` argument from ``url_for`` to - ``Flask.handle_url_build_error``. :pr:`2017` -- ``Flask.add_url_rule`` accepts the ``provide_automatic_options`` - argument to disable adding the ``OPTIONS`` method. :pr:`1489` -- ``MethodView`` subclasses inherit method handlers from base classes. - :pr:`1936` -- Errors caused while opening the session at the beginning of the - request are handled by the app's error handlers. :pr:`2254` -- Blueprints gained ``Blueprint.json_encoder`` and - ``Blueprint.json_decoder`` attributes to override the app's - encoder and decoder. :pr:`1898` -- ``Flask.make_response`` raises ``TypeError`` instead of - ``ValueError`` for bad response types. The error messages have been - improved to describe why the type is invalid. :pr:`2256` -- Add ``routes`` CLI command to output routes registered on the - application. :pr:`2259` -- Show warning when session cookie domain is a bare hostname or an IP - address, as these may not behave properly in some browsers, such as - Chrome. :pr:`2282` -- Allow IP address as exact session cookie domain. :pr:`2282` -- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through - ``SERVER_NAME``. :pr:`2282` -- Auto-detect zero-argument app factory called ``create_app`` or - ``make_app`` from ``FLASK_APP``. :pr:`2297` -- Factory functions are not required to take a ``script_info`` - parameter to work with the ``flask`` command. If they take a single - parameter or a parameter named ``script_info``, the ``ScriptInfo`` - object will be passed. :pr:`2319` -- ``FLASK_APP`` can be set to an app factory, with arguments if - needed, for example ``FLASK_APP=myproject.app:create_app('dev')``. - :pr:`2326` -- ``FLASK_APP`` can point to local packages that are not installed in - editable mode, although ``pip install -e`` is still preferred. - :pr:`2414` -- The ``View`` class attribute - ``View.provide_automatic_options`` is set in ``View.as_view``, to be - detected by ``Flask.add_url_rule``. :pr:`2316` -- Error handling will try handlers registered for ``blueprint, code``, - ``app, code``, ``blueprint, exception``, ``app, exception``. - :pr:`2314` -- ``Cookie`` is added to the response's ``Vary`` header if the session - is accessed at all during the request (and not deleted). :pr:`2288` -- ``Flask.test_request_context`` accepts ``subdomain`` and - ``url_scheme`` arguments for use when building the base URL. - :pr:`1621` -- Set ``APPLICATION_ROOT`` to ``'/'`` by default. This was already the - implicit default when it was set to ``None``. -- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode. - ``BadRequestKeyError`` has a message with the bad key in debug mode - instead of the generic bad request message. :pr:`2348` -- Allow registering new tags with ``TaggedJSONSerializer`` to support - storing other types in the session cookie. :pr:`2352` -- Only open the session if the request has not been pushed onto the - context stack yet. This allows ``stream_with_context`` generators to - access the same session that the containing view uses. :pr:`2354` -- Add ``json`` keyword argument for the test client request methods. - This will dump the given object as JSON and set the appropriate - content type. :pr:`2358` -- Extract JSON handling to a mixin applied to both the ``Request`` and - ``Response`` classes. This adds the ``Response.is_json`` and - ``Response.get_json`` methods to the response to make testing JSON - response much easier. :pr:`2358` -- Removed error handler caching because it caused unexpected results - for some exception inheritance hierarchies. Register handlers - explicitly for each exception if you want to avoid traversing the - MRO. :pr:`2362` -- Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374` -- Template auto reloading will honor debug mode even even if - ``Flask.jinja_env`` was already accessed. :pr:`2373` -- The following old deprecated code was removed. :issue:`2385` - - - ``flask.ext`` - import extensions directly by their name instead - of through the ``flask.ext`` namespace. For example, - ``import flask.ext.sqlalchemy`` becomes - ``import flask_sqlalchemy``. - - ``Flask.init_jinja_globals`` - extend - ``Flask.create_jinja_environment`` instead. - - ``Flask.error_handlers`` - tracked by - ``Flask.error_handler_spec``, use ``Flask.errorhandler`` - to register handlers. - - ``Flask.request_globals_class`` - use - ``Flask.app_ctx_globals_class`` instead. - - ``Flask.static_path`` - use ``Flask.static_url_path`` instead. - - ``Request.module`` - use ``Request.blueprint`` instead. - -- The ``Request.json`` property is no longer deprecated. :issue:`1421` -- Support passing a ``EnvironBuilder`` or ``dict`` to - ``test_client.open``. :pr:`2412` -- The ``flask`` command and ``Flask.run`` will load environment - variables from ``.env`` and ``.flaskenv`` files if python-dotenv is - installed. :pr:`2416` -- When passing a full URL to the test client, the scheme in the URL is - used instead of ``PREFERRED_URL_SCHEME``. :pr:`2430` -- ``Flask.logger`` has been simplified. ``LOGGER_NAME`` and - ``LOGGER_HANDLER_POLICY`` config was removed. The logger is always - named ``flask.app``. The level is only set on first access, it - doesn't check ``Flask.debug`` each time. Only one format is used, - not different ones depending on ``Flask.debug``. No handlers are - removed, and a handler is only added if no handlers are already - configured. :pr:`2436` -- Blueprint view function names may not contain dots. :pr:`2450` -- Fix a ``ValueError`` caused by invalid ``Range`` requests in some - cases. :issue:`2526` -- The development server uses threads by default. :pr:`2529` -- Loading config files with ``silent=True`` will ignore ``ENOTDIR`` - errors. :pr:`2581` -- Pass ``--cert`` and ``--key`` options to ``flask run`` to run the - development server over HTTPS. :pr:`2606` -- Added ``SESSION_COOKIE_SAMESITE`` to control the ``SameSite`` - attribute on the session cookie. :pr:`2607` -- Added ``Flask.test_cli_runner`` to create a Click runner that can - invoke Flask CLI commands for testing. :pr:`2636` -- Subdomain matching is disabled by default and setting - ``SERVER_NAME`` does not implicitly enable it. It can be enabled by - passing ``subdomain_matching=True`` to the ``Flask`` constructor. - :pr:`2635` -- A single trailing slash is stripped from the blueprint - ``url_prefix`` when it is registered with the app. :pr:`2629` -- ``Request.get_json`` doesn't cache the result if parsing fails when - ``silent`` is true. :issue:`2651` -- ``Request.get_json`` no longer accepts arbitrary encodings. Incoming - JSON should be encoded using UTF-8 per :rfc:`8259`, but Flask will - autodetect UTF-8, -16, or -32. :pr:`2691` -- Added ``MAX_COOKIE_SIZE`` and ``Response.max_cookie_size`` to - control when Werkzeug warns about large cookies that browsers may - ignore. :pr:`2693` -- Updated documentation theme to make docs look better in small - windows. :pr:`2709` -- Rewrote the tutorial docs and example project to take a more - structured approach to help new users avoid common pitfalls. - :pr:`2676` - - -Version 0.12.5 --------------- - -Released 2020-02-10 - -- Pin Werkzeug to < 1.0.0. :issue:`3497` - - -Version 0.12.4 --------------- - -Released 2018-04-29 - -- Repackage 0.12.3 to fix package layout issue. :issue:`2728` - - -Version 0.12.3 --------------- - -Released 2018-04-26 - -- ``Request.get_json`` no longer accepts arbitrary encodings. - Incoming JSON should be encoded using UTF-8 per :rfc:`8259`, but - Flask will autodetect UTF-8, -16, or -32. :issue:`2692` -- Fix a Python warning about imports when using ``python -m flask``. - :issue:`2666` -- Fix a ``ValueError`` caused by invalid ``Range`` requests in some - cases. - - -Version 0.12.2 --------------- - -Released 2017-05-16 - -- Fix a bug in ``safe_join`` on Windows. - - -Version 0.12.1 --------------- - -Released 2017-03-31 - -- Prevent ``flask run`` from showing a ``NoAppException`` when an - ``ImportError`` occurs within the imported application module. -- Fix encoding behavior of ``app.config.from_pyfile`` for Python 3. - :issue:`2118` -- Use the ``SERVER_NAME`` config if it is present as default values - for ``app.run``. :issue:`2109`, :pr:`2152` -- Call ``ctx.auto_pop`` with the exception object instead of ``None``, - in the event that a ``BaseException`` such as ``KeyboardInterrupt`` - is raised in a request handler. - - -Version 0.12 ------------- - -Released 2016-12-21, codename Punsch - -- The cli command now responds to ``--version``. -- Mimetype guessing and ETag generation for file-like objects in - ``send_file`` has been removed. :issue:`104`, :pr`1849` -- Mimetype guessing in ``send_file`` now fails loudly and doesn't fall - back to ``application/octet-stream``. :pr:`1988` -- Make ``flask.safe_join`` able to join multiple paths like - ``os.path.join`` :pr:`1730` -- Revert a behavior change that made the dev server crash instead of - returning an Internal Server Error. :pr:`2006` -- Correctly invoke response handlers for both regular request - dispatching as well as error handlers. -- Disable logger propagation by default for the app logger. -- Add support for range requests in ``send_file``. -- ``app.test_client`` includes preset default environment, which can - now be directly set, instead of per ``client.get``. -- Fix crash when running under PyPy3. :pr:`1814` - - -Version 0.11.1 --------------- - -Released 2016-06-07 - -- Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from - working. :pr:`1872` - - -Version 0.11 ------------- - -Released 2016-05-29, codename Absinthe - -- Added support to serializing top-level arrays to ``jsonify``. This - introduces a security risk in ancient browsers. -- Added before_render_template signal. -- Added ``**kwargs`` to ``Flask.test_client`` to support passing - additional keyword arguments to the constructor of - ``Flask.test_client_class``. -- Added ``SESSION_REFRESH_EACH_REQUEST`` config key that controls the - set-cookie behavior. If set to ``True`` a permanent session will be - refreshed each request and get their lifetime extended, if set to - ``False`` it will only be modified if the session actually modifies. - Non permanent sessions are not affected by this and will always - expire if the browser window closes. -- Made Flask support custom JSON mimetypes for incoming data. -- Added support for returning tuples in the form ``(response, - headers)`` from a view function. -- Added ``Config.from_json``. -- Added ``Flask.config_class``. -- Added ``Config.get_namespace``. -- Templates are no longer automatically reloaded outside of debug - mode. This can be configured with the new ``TEMPLATES_AUTO_RELOAD`` - config key. -- Added a workaround for a limitation in Python 3.3's namespace - loader. -- Added support for explicit root paths when using Python 3.3's - namespace packages. -- Added ``flask`` and the ``flask.cli`` module to start the - local debug server through the click CLI system. This is recommended - over the old ``flask.run()`` method as it works faster and more - reliable due to a different design and also replaces - ``Flask-Script``. -- Error handlers that match specific classes are now checked first, - thereby allowing catching exceptions that are subclasses of HTTP - exceptions (in ``werkzeug.exceptions``). This makes it possible for - an extension author to create exceptions that will by default result - in the HTTP error of their choosing, but may be caught with a custom - error handler if desired. -- Added ``Config.from_mapping``. -- Flask will now log by default even if debug is disabled. The log - format is now hardcoded but the default log handling can be disabled - through the ``LOGGER_HANDLER_POLICY`` configuration key. -- Removed deprecated module functionality. -- Added the ``EXPLAIN_TEMPLATE_LOADING`` config flag which when - enabled will instruct Flask to explain how it locates templates. - This should help users debug when the wrong templates are loaded. -- Enforce blueprint handling in the order they were registered for - template loading. -- Ported test suite to py.test. -- Deprecated ``request.json`` in favour of ``request.get_json()``. -- Add "pretty" and "compressed" separators definitions in jsonify() - method. Reduces JSON response size when - ``JSONIFY_PRETTYPRINT_REGULAR=False`` by removing unnecessary white - space included by default after separators. -- JSON responses are now terminated with a newline character, because - it is a convention that UNIX text files end with a newline and some - clients don't deal well when this newline is missing. :pr:`1262` -- The automatically provided ``OPTIONS`` method is now correctly - disabled if the user registered an overriding rule with the - lowercase-version ``options``. :issue:`1288` -- ``flask.json.jsonify`` now supports the ``datetime.date`` type. - :pr:`1326` -- Don't leak exception info of already caught exceptions to context - teardown handlers. :pr:`1393` -- Allow custom Jinja environment subclasses. :pr:`1422` -- Updated extension dev guidelines. -- ``flask.g`` now has ``pop()`` and ``setdefault`` methods. -- Turn on autoescape for ``flask.templating.render_template_string`` - by default. :pr:`1515` -- ``flask.ext`` is now deprecated. :pr:`1484` -- ``send_from_directory`` now raises BadRequest if the filename is - invalid on the server OS. :pr:`1763` -- Added the ``JSONIFY_MIMETYPE`` configuration variable. :pr:`1728` -- Exceptions during teardown handling will no longer leave bad - application contexts lingering around. -- Fixed broken ``test_appcontext_signals()`` test case. -- Raise an ``AttributeError`` in ``helpers.find_package`` with a - useful message explaining why it is raised when a :pep:`302` import - hook is used without an ``is_package()`` method. -- Fixed an issue causing exceptions raised before entering a request - or app context to be passed to teardown handlers. -- Fixed an issue with query parameters getting removed from requests - in the test client when absolute URLs were requested. -- Made ``@before_first_request`` into a decorator as intended. -- Fixed an etags bug when sending a file streams with a name. -- Fixed ``send_from_directory`` not expanding to the application root - path correctly. -- Changed logic of before first request handlers to flip the flag - after invoking. This will allow some uses that are potentially - dangerous but should probably be permitted. -- Fixed Python 3 bug when a handler from - ``app.url_build_error_handlers`` reraises the ``BuildError``. - - -Version 0.10.1 --------------- - -Released 2013-06-14 - -- Fixed an issue where ``|tojson`` was not quoting single quotes which - made the filter not work properly in HTML attributes. Now it's - possible to use that filter in single quoted attributes. This should - make using that filter with angular.js easier. -- Added support for byte strings back to the session system. This - broke compatibility with the common case of people putting binary - data for token verification into the session. -- Fixed an issue where registering the same method twice for the same - endpoint would trigger an exception incorrectly. - - -Version 0.10 ------------- - -Released 2013-06-13, codename Limoncello - -- Changed default cookie serialization format from pickle to JSON to - limit the impact an attacker can do if the secret key leaks. -- Added ``template_test`` methods in addition to the already existing - ``template_filter`` method family. -- Added ``template_global`` methods in addition to the already - existing ``template_filter`` method family. -- Set the content-length header for x-sendfile. -- ``tojson`` filter now does not escape script blocks in HTML5 - parsers. -- ``tojson`` used in templates is now safe by default. This was - allowed due to the different escaping behavior. -- Flask will now raise an error if you attempt to register a new - function on an already used endpoint. -- Added wrapper module around simplejson and added default - serialization of datetime objects. This allows much easier - customization of how JSON is handled by Flask or any Flask - extension. -- Removed deprecated internal ``flask.session`` module alias. Use - ``flask.sessions`` instead to get the session module. This is not to - be confused with ``flask.session`` the session proxy. -- Templates can now be rendered without request context. The behavior - is slightly different as the ``request``, ``session`` and ``g`` - objects will not be available and blueprint's context processors are - not called. -- The config object is now available to the template as a real global - and not through a context processor which makes it available even in - imported templates by default. -- Added an option to generate non-ascii encoded JSON which should - result in less bytes being transmitted over the network. It's - disabled by default to not cause confusion with existing libraries - that might expect ``flask.json.dumps`` to return bytes by default. -- ``flask.g`` is now stored on the app context instead of the request - context. -- ``flask.g`` now gained a ``get()`` method for not erroring out on - non existing items. -- ``flask.g`` now can be used with the ``in`` operator to see what's - defined and it now is iterable and will yield all attributes stored. -- ``flask.Flask.request_globals_class`` got renamed to - ``flask.Flask.app_ctx_globals_class`` which is a better name to what - it does since 0.10. -- ``request``, ``session`` and ``g`` are now also added as proxies to - the template context which makes them available in imported - templates. One has to be very careful with those though because - usage outside of macros might cause caching. -- Flask will no longer invoke the wrong error handlers if a proxy - exception is passed through. -- Added a workaround for chrome's cookies in localhost not working as - intended with domain names. -- Changed logic for picking defaults for cookie values from sessions - to work better with Google Chrome. -- Added ``message_flashed`` signal that simplifies flashing testing. -- Added support for copying of request contexts for better working - with greenlets. -- Removed custom JSON HTTP exception subclasses. If you were relying - on them you can reintroduce them again yourself trivially. Using - them however is strongly discouraged as the interface was flawed. -- Python requirements changed: requiring Python 2.6 or 2.7 now to - prepare for Python 3.3 port. -- Changed how the teardown system is informed about exceptions. This - is now more reliable in case something handles an exception halfway - through the error handling process. -- Request context preservation in debug mode now keeps the exception - information around which means that teardown handlers are able to - distinguish error from success cases. -- Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. -- Flask now orders JSON keys by default to not trash HTTP caches due - to different hash seeds between different workers. -- Added ``appcontext_pushed`` and ``appcontext_popped`` signals. -- The builtin run method now takes the ``SERVER_NAME`` into account - when picking the default port to run on. -- Added ``flask.request.get_json()`` as a replacement for the old - ``flask.request.json`` property. - - -Version 0.9 ------------ - -Released 2012-07-01, codename Campari - -- The ``Request.on_json_loading_failed`` now returns a JSON formatted - response by default. -- The ``url_for`` function now can generate anchors to the generated - links. -- The ``url_for`` function now can also explicitly generate URL rules - specific to a given HTTP method. -- Logger now only returns the debug log setting if it was not set - explicitly. -- Unregister a circular dependency between the WSGI environment and - the request object when shutting down the request. This means that - environ ``werkzeug.request`` will be ``None`` after the response was - returned to the WSGI server but has the advantage that the garbage - collector is not needed on CPython to tear down the request unless - the user created circular dependencies themselves. -- Session is now stored after callbacks so that if the session payload - is stored in the session you can still modify it in an after request - callback. -- The ``Flask`` class will avoid importing the provided import name if - it can (the required first parameter), to benefit tools which build - Flask instances programmatically. The Flask class will fall back to - using import on systems with custom module hooks, e.g. Google App - Engine, or when the import name is inside a zip archive (usually an - egg) prior to Python 2.7. -- Blueprints now have a decorator to add custom template filters - application wide, ``Blueprint.app_template_filter``. -- The Flask and Blueprint classes now have a non-decorator method for - adding custom template filters application wide, - ``Flask.add_template_filter`` and - ``Blueprint.add_app_template_filter``. -- The ``get_flashed_messages`` function now allows rendering flashed - message categories in separate blocks, through a ``category_filter`` - argument. -- The ``Flask.run`` method now accepts ``None`` for ``host`` and - ``port`` arguments, using default values when ``None``. This allows - for calling run using configuration values, e.g. - ``app.run(app.config.get('MYHOST'), app.config.get('MYPORT'))``, - with proper behavior whether or not a config file is provided. -- The ``render_template`` method now accepts a either an iterable of - template names or a single template name. Previously, it only - accepted a single template name. On an iterable, the first template - found is rendered. -- Added ``Flask.app_context`` which works very similar to the request - context but only provides access to the current application. This - also adds support for URL generation without an active request - context. -- View functions can now return a tuple with the first instance being - an instance of ``Response``. This allows for returning - ``jsonify(error="error msg"), 400`` from a view function. -- ``Flask`` and ``Blueprint`` now provide a ``get_send_file_max_age`` - hook for subclasses to override behavior of serving static files - from Flask when using ``Flask.send_static_file`` (used for the - default static file handler) and ``helpers.send_file``. This hook is - provided a filename, which for example allows changing cache - controls by file extension. The default max-age for ``send_file`` - and static files can be configured through a new - ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, which is used - in the default ``get_send_file_max_age`` implementation. -- Fixed an assumption in sessions implementation which could break - message flashing on sessions implementations which use external - storage. -- Changed the behavior of tuple return values from functions. They are - no longer arguments to the response object, they now have a defined - meaning. -- Added ``Flask.request_globals_class`` to allow a specific class to - be used on creation of the ``g`` instance of each request. -- Added ``required_methods`` attribute to view functions to force-add - methods on registration. -- Added ``flask.after_this_request``. -- Added ``flask.stream_with_context`` and the ability to push contexts - multiple times without producing unexpected behavior. - - -Version 0.8.1 -------------- - -Released 2012-07-01 - -- Fixed an issue with the undocumented ``flask.session`` module to not - work properly on Python 2.5. It should not be used but did cause - some problems for package managers. - - -Version 0.8 ------------ - -Released 2011-09-29, codename Rakija - -- Refactored session support into a session interface so that the - implementation of the sessions can be changed without having to - override the Flask class. -- Empty session cookies are now deleted properly automatically. -- View functions can now opt out of getting the automatic OPTIONS - implementation. -- HTTP exceptions and Bad Request errors can now be trapped so that - they show up normally in the traceback. -- Flask in debug mode is now detecting some common problems and tries - to warn you about them. -- Flask in debug mode will now complain with an assertion error if a - view was attached after the first request was handled. This gives - earlier feedback when users forget to import view code ahead of - time. -- Added the ability to register callbacks that are only triggered once - at the beginning of the first request with - ``Flask.before_first_request``. -- Malformed JSON data will now trigger a bad request HTTP exception - instead of a value error which usually would result in a 500 - internal server error if not handled. This is a backwards - incompatible change. -- Applications now not only have a root path where the resources and - modules are located but also an instance path which is the - designated place to drop files that are modified at runtime (uploads - etc.). Also this is conceptually only instance depending and outside - version control so it's the perfect place to put configuration files - etc. -- Added the ``APPLICATION_ROOT`` configuration variable. -- Implemented ``TestClient.session_transaction`` to easily modify - sessions from the test environment. -- Refactored test client internally. The ``APPLICATION_ROOT`` - configuration variable as well as ``SERVER_NAME`` are now properly - used by the test client as defaults. -- Added ``View.decorators`` to support simpler decorating of pluggable - (class-based) views. -- Fixed an issue where the test client if used with the "with" - statement did not trigger the execution of the teardown handlers. -- Added finer control over the session cookie parameters. -- HEAD requests to a method view now automatically dispatch to the - ``get`` method if no handler was implemented. -- Implemented the virtual ``flask.ext`` package to import extensions - from. -- The context preservation on exceptions is now an integral component - of Flask itself and no longer of the test client. This cleaned up - some internal logic and lowers the odds of runaway request contexts - in unittests. -- Fixed the Jinja2 environment's ``list_templates`` method not - returning the correct names when blueprints or modules were - involved. - - -Version 0.7.2 -------------- - -Released 2011-07-06 - -- Fixed an issue with URL processors not properly working on - blueprints. - - -Version 0.7.1 -------------- - -Released 2011-06-29 - -- Added missing future import that broke 2.5 compatibility. -- Fixed an infinite redirect issue with blueprints. - - -Version 0.7 ------------ - -Released 2011-06-28, codename Grappa - -- Added ``Flask.make_default_options_response`` which can be used by - subclasses to alter the default behavior for ``OPTIONS`` responses. -- Unbound locals now raise a proper ``RuntimeError`` instead of an - ``AttributeError``. -- Mimetype guessing and etag support based on file objects is now - deprecated for ``send_file`` because it was unreliable. Pass - filenames instead or attach your own etags and provide a proper - mimetype by hand. -- Static file handling for modules now requires the name of the static - folder to be supplied explicitly. The previous autodetection was not - reliable and caused issues on Google's App Engine. Until 1.0 the old - behavior will continue to work but issue dependency warnings. -- Fixed a problem for Flask to run on jython. -- Added a ``PROPAGATE_EXCEPTIONS`` configuration variable that can be - used to flip the setting of exception propagation which previously - was linked to ``DEBUG`` alone and is now linked to either ``DEBUG`` - or ``TESTING``. -- Flask no longer internally depends on rules being added through the - ``add_url_rule`` function and can now also accept regular werkzeug - rules added to the url map. -- Added an ``endpoint`` method to the flask application object which - allows one to register a callback to an arbitrary endpoint with a - decorator. -- Use Last-Modified for static file sending instead of Date which was - incorrectly introduced in 0.6. -- Added ``create_jinja_loader`` to override the loader creation - process. -- Implemented a silent flag for ``config.from_pyfile``. -- Added ``teardown_request`` decorator, for functions that should run - at the end of a request regardless of whether an exception occurred. - Also the behavior for ``after_request`` was changed. It's now no - longer executed when an exception is raised. -- Implemented ``has_request_context``. -- Deprecated ``init_jinja_globals``. Override the - ``Flask.create_jinja_environment`` method instead to achieve the - same functionality. -- Added ``safe_join``. -- The automatic JSON request data unpacking now looks at the charset - mimetype parameter. -- Don't modify the session on ``get_flashed_messages`` if there are no - messages in the session. -- ``before_request`` handlers are now able to abort requests with - errors. -- It is not possible to define user exception handlers. That way you - can provide custom error messages from a central hub for certain - errors that might occur during request processing (for instance - database connection errors, timeouts from remote resources etc.). -- Blueprints can provide blueprint specific error handlers. -- Implemented generic class-based views. - - -Version 0.6.1 -------------- - -Released 2010-12-31 - -- Fixed an issue where the default ``OPTIONS`` response was not - exposing all valid methods in the ``Allow`` header. -- Jinja2 template loading syntax now allows "./" in front of a - template load path. Previously this caused issues with module - setups. -- Fixed an issue where the subdomain setting for modules was ignored - for the static folder. -- Fixed a security problem that allowed clients to download arbitrary - files if the host server was a windows based operating system and - the client uses backslashes to escape the directory the files where - exposed from. - - -Version 0.6 ------------ - -Released 2010-07-27, codename Whisky - -- After request functions are now called in reverse order of - registration. -- OPTIONS is now automatically implemented by Flask unless the - application explicitly adds 'OPTIONS' as method to the URL rule. In - this case no automatic OPTIONS handling kicks in. -- Static rules are now even in place if there is no static folder for - the module. This was implemented to aid GAE which will remove the - static folder if it's part of a mapping in the .yml file. -- ``Flask.config`` is now available in the templates as ``config``. -- Context processors will no longer override values passed directly to - the render function. -- Added the ability to limit the incoming request data with the new - ``MAX_CONTENT_LENGTH`` configuration value. -- The endpoint for the ``Module.add_url_rule`` method is now optional - to be consistent with the function of the same name on the - application object. -- Added a ``make_response`` function that simplifies creating response - object instances in views. -- Added signalling support based on blinker. This feature is currently - optional and supposed to be used by extensions and applications. If - you want to use it, make sure to have ``blinker`` installed. -- Refactored the way URL adapters are created. This process is now - fully customizable with the ``Flask.create_url_adapter`` method. -- Modules can now register for a subdomain instead of just an URL - prefix. This makes it possible to bind a whole module to a - configurable subdomain. - - -Version 0.5.2 -------------- - -Released 2010-07-15 - -- Fixed another issue with loading templates from directories when - modules were used. - - -Version 0.5.1 -------------- - -Released 2010-07-06 - -- Fixes an issue with template loading from directories when modules - where used. - - -Version 0.5 ------------ - -Released 2010-07-06, codename Calvados - -- Fixed a bug with subdomains that was caused by the inability to - specify the server name. The server name can now be set with the - ``SERVER_NAME`` config key. This key is now also used to set the - session cookie cross-subdomain wide. -- Autoescaping is no longer active for all templates. Instead it is - only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside - templates this behavior can be changed with the ``autoescape`` tag. -- Refactored Flask internally. It now consists of more than a single - file. -- ``send_file`` now emits etags and has the ability to do conditional - responses builtin. -- (temporarily) dropped support for zipped applications. This was a - rarely used feature and led to some confusing behavior. -- Added support for per-package template and static-file directories. -- Removed support for ``create_jinja_loader`` which is no longer used - in 0.5 due to the improved module support. -- Added a helper function to expose files from any directory. - - -Version 0.4 ------------ - -Released 2010-06-18, codename Rakia - -- Added the ability to register application wide error handlers from - modules. -- ``Flask.after_request`` handlers are now also invoked if the request - dies with an exception and an error handling page kicks in. -- Test client has not the ability to preserve the request context for - a little longer. This can also be used to trigger custom requests - that do not pop the request stack for testing. -- Because the Python standard library caches loggers, the name of the - logger is configurable now to better support unittests. -- Added ``TESTING`` switch that can activate unittesting helpers. -- The logger switches to ``DEBUG`` mode now if debug is enabled. - - -Version 0.3.1 -------------- - -Released 2010-05-28 - -- Fixed a error reporting bug with ``Config.from_envvar``. -- Removed some unused code. -- Release does no longer include development leftover files (.git - folder for themes, built documentation in zip and pdf file and some - .pyc files) - - -Version 0.3 ------------ - -Released 2010-05-28, codename Schnaps - -- Added support for categories for flashed messages. -- The application now configures a ``logging.Handler`` and will log - request handling exceptions to that logger when not in debug mode. - This makes it possible to receive mails on server errors for - example. -- Added support for context binding that does not require the use of - the with statement for playing in the console. -- The request context is now available within the with statement - making it possible to further push the request context or pop it. -- Added support for configurations. - - -Version 0.2 ------------ - -Released 2010-05-12, codename J?germeister - -- Various bugfixes -- Integrated JSON support -- Added ``get_template_attribute`` helper function. -- ``Flask.add_url_rule`` can now also register a view function. -- Refactored internal request dispatching. -- Server listens on 127.0.0.1 by default now to fix issues with - chrome. -- Added external URL support. -- Added support for ``send_file``. -- Module support and internal request handling refactoring to better - support pluggable applications. -- Sessions can be set to be permanent now on a per-session basis. -- Better error reporting on missing secret keys. -- Added support for Google Appengine. - - -Version 0.1 ------------ - -Released 2010-04-16 - -- First public preview release. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f4ba197d..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at report@palletsprojects.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index fed44978..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,238 +0,0 @@ -How to contribute to Flask -========================== - -Thank you for considering contributing to Flask! - - -Support questions ------------------ - -Please don't use the issue tracker for this. The issue tracker is a tool -to address bugs and feature requests in Flask itself. Use one of the -following resources for questions about using Flask or issues with your -own code: - -- The ``#questions`` channel on our Discord chat: - https://discord.gg/pallets -- Ask on `Stack Overflow`_. Search with Google first using: - ``site:stackoverflow.com flask {search term, exception message, etc.}`` -- Ask on our `GitHub Discussions`_ for long term discussion or larger - questions. - -.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent -.. _GitHub Discussions: https://github.com/pallets/flask/discussions - - -Reporting issues ----------------- - -Include the following information in your post: - -- Describe what you expected to happen. -- If possible, include a `minimal reproducible example`_ to help us - identify the issue. This also helps check that the issue is not with - your own code. -- Describe what actually happened. Include the full traceback if there - was an exception. -- List your Python and Flask versions. If possible, check if this - issue is already fixed in the latest releases or the latest code in - the repository. - -.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example - - -Submitting patches ------------------- - -If there is not an open issue for what you want to submit, prefer -opening one for discussion before working on a PR. You can work on any -issue that doesn't have an open PR linked to it or a maintainer assigned -to it. These show up in the sidebar. No need to ask if you can work on -an issue that interests you. - -Include the following in your patch: - -- Use `Black`_ to format your code. This and other tools will run - automatically if you install `pre-commit`_ using the instructions - below. -- Include tests if your patch adds or changes code. Make sure the test - fails without your patch. -- Update any relevant docs pages and docstrings. Docs pages and - docstrings should be wrapped at 72 characters. -- Add an entry in ``CHANGES.rst``. Use the same style as other - entries. Also include ``.. versionchanged::`` inline changelogs in - relevant docstrings. - -.. _Black: https://black.readthedocs.io -.. _pre-commit: https://pre-commit.com - - -First time setup using GitHub Codespaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`GitHub Codespaces`_ creates a development environment that is already set up for the -project. By default it opens in Visual Studio Code for the Web, but this can -be changed in your GitHub profile settings to use Visual Studio Code or JetBrains -PyCharm on your local computer. - -- Make sure you have a `GitHub account`_. -- From the project's repository page, click the green "Code" button and then "Create - codespace on main". -- The codespace will be set up, then Visual Studio Code will open. However, you'll - need to wait a bit longer for the Python extension to be installed. You'll know it's - ready when the terminal at the bottom shows that the virtualenv was activated. -- Check out a branch and `start coding`_. - -.. _GitHub Codespaces: https://docs.github.com/en/codespaces -.. _devcontainer: https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers - -First time setup in your local environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Make sure you have a `GitHub account`_. -- Download and install the `latest version of git`_. -- Configure git with your `username`_ and `email`_. - - .. code-block:: text - - $ git config --global user.name 'your name' - $ git config --global user.email 'your email' - -- Fork Flask to your GitHub account by clicking the `Fork`_ button. -- `Clone`_ your fork locally, replacing ``your-username`` in the command below with - your actual username. - - .. code-block:: text - - $ git clone https://github.com/your-username/flask - $ cd flask - -- Create a virtualenv. Use the latest version of Python. - - - Linux/macOS - - .. code-block:: text - - $ python3 -m venv .venv - $ . .venv/bin/activate - - - Windows - - .. code-block:: text - - > py -3 -m venv .venv - > .venv\Scripts\activate - -- Install the development dependencies, then install Flask in editable mode. - - .. code-block:: text - - $ python -m pip install -U pip - $ pip install -r requirements/dev.txt && pip install -e . - -- Install the pre-commit hooks. - - .. code-block:: text - - $ pre-commit install --install-hooks - -.. _GitHub account: https://github.com/join -.. _latest version of git: https://git-scm.com/downloads -.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git -.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address -.. _Fork: https://github.com/pallets/flask/fork -.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork - -.. _start coding: - -Start coding -~~~~~~~~~~~~ - -- Create a branch to identify the issue you would like to work on. If you're - submitting a bug or documentation fix, branch off of the latest ".x" branch. - - .. code-block:: text - - $ git fetch origin - $ git checkout -b your-branch-name origin/2.0.x - - If you're submitting a feature addition or change, branch off of the "main" branch. - - .. code-block:: text - - $ git fetch origin - $ git checkout -b your-branch-name origin/main - -- Using your favorite editor, make your changes, `committing as you go`_. - - - If you are in a codespace, you will be prompted to `create a fork`_ the first - time you make a commit. Enter ``Y`` to continue. - -- Include tests that cover any code changes you make. Make sure the test fails without - your patch. Run the tests as described below. -- Push your commits to your fork on GitHub and `create a pull request`_. Link to the - issue being addressed with ``fixes #123`` in the pull request description. - - .. code-block:: text - - $ git push --set-upstream origin your-branch-name - -.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes -.. _create a fork: https://docs.github.com/en/codespaces/developing-in-codespaces/using-source-control-in-your-codespace#about-automatic-forking -.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request - -.. _Running the tests: - -Running the tests -~~~~~~~~~~~~~~~~~ - -Run the basic test suite with pytest. - -.. code-block:: text - - $ pytest - -This runs the tests for the current environment, which is usually -sufficient. CI will run the full suite when you submit your pull -request. You can run the full test suite with tox if you don't want to -wait. - -.. code-block:: text - - $ tox - - -Running test coverage -~~~~~~~~~~~~~~~~~~~~~ - -Generating a report of lines that do not have test coverage can indicate -where to start contributing. Run ``pytest`` using ``coverage`` and -generate a report. - -If you are using GitHub Codespaces, ``coverage`` is already installed -so you can skip the installation command. - -.. code-block:: text - - $ pip install coverage - $ coverage run -m pytest - $ coverage html - -Open ``htmlcov/index.html`` in your browser to explore the report. - -Read more about `coverage `__. - - -Building the docs -~~~~~~~~~~~~~~~~~ - -Build the docs in the ``docs`` directory using Sphinx. - -.. code-block:: text - - $ cd docs - $ make html - -Open ``_build/html/index.html`` in your browser to view the docs. - -Read more about `Sphinx `__. diff --git a/examples/javascript/LICENSE.rst b/LICENSE.rst similarity index 100% rename from examples/javascript/LICENSE.rst rename to LICENSE.rst diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 9d227a0c..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md deleted file mode 100644 index df4d41cb..00000000 --- a/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Flask - -Flask is a lightweight [WSGI][] web application framework. It is designed -to make getting started quick and easy, with the ability to scale up to -complex applications. It began as a simple wrapper around [Werkzeug][] -and [Jinja][], and has become one of the most popular Python web -application frameworks. - -Flask offers suggestions, but doesn't enforce any dependencies or -project layout. It is up to the developer to choose the tools and -libraries they want to use. There are many extensions provided by the -community that make adding new functionality easy. - -[WSGI]: https://wsgi.readthedocs.io/ -[Werkzeug]: https://werkzeug.palletsprojects.com/ -[Jinja]: https://jinja.palletsprojects.com/ - - -## A Simple Example - -```python -# save this as app.py -from flask import Flask - -app = Flask(__name__) - -@app.route("/") -def hello(): - return "Hello, World!" -``` - -``` -$ flask run - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) -``` - - -## Donate - -The Pallets organization develops and supports Flask and the libraries -it uses. In order to grow the community of contributors and users, and -allow the maintainers to devote more time to the projects, [please -donate today][]. - -[please donate today]: https://palletsprojects.com/donate diff --git a/examples/tutorial/README.rst b/README.rst similarity index 100% rename from examples/tutorial/README.rst rename to README.rst diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cbb..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/debugger.png b/docs/_static/debugger.png deleted file mode 100644 index 7d4181f6..00000000 Binary files a/docs/_static/debugger.png and /dev/null differ diff --git a/docs/_static/flask-horizontal.png b/docs/_static/flask-horizontal.png deleted file mode 100644 index a0df2c61..00000000 Binary files a/docs/_static/flask-horizontal.png and /dev/null differ diff --git a/docs/_static/flask-vertical.png b/docs/_static/flask-vertical.png deleted file mode 100644 index d1fd1499..00000000 Binary files a/docs/_static/flask-vertical.png and /dev/null differ diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png deleted file mode 100644 index ad025545..00000000 Binary files a/docs/_static/pycharm-run-config.png and /dev/null differ diff --git a/docs/_static/shortcut-icon.png b/docs/_static/shortcut-icon.png deleted file mode 100644 index 4d3e6c37..00000000 Binary files a/docs/_static/shortcut-icon.png and /dev/null differ diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 1aa8048f..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,717 +0,0 @@ -API -=== - -.. module:: flask - -This part of the documentation covers all the interfaces of Flask. For -parts where Flask depends on external libraries, we document the most -important right here and provide links to the canonical documentation. - - -Application Object ------------------- - -.. autoclass:: Flask - :members: - :inherited-members: - - -Blueprint Objects ------------------ - -.. autoclass:: Blueprint - :members: - :inherited-members: - -Incoming Request Data ---------------------- - -.. autoclass:: Request - :members: - :inherited-members: - :exclude-members: json_module - -.. attribute:: request - - To access incoming request data, you can use the global `request` - object. Flask parses incoming request data for you and gives you - access to it through that global object. Internally Flask makes - sure that you always get the correct data for the active thread if you - are in a multithreaded environment. - - This is a proxy. See :ref:`notes-on-proxies` for more information. - - The request object is an instance of a :class:`~flask.Request`. - - -Response Objects ----------------- - -.. autoclass:: flask.Response - :members: - :inherited-members: - :exclude-members: json_module - -Sessions --------- - -If you have set :attr:`Flask.secret_key` (or configured it from -:data:`SECRET_KEY`) you can use sessions in Flask applications. A session makes -it possible to remember information from one request to another. The way Flask -does this is by using a signed cookie. The user can look at the session -contents, but can't modify it unless they know the secret key, so make sure to -set that to something complex and unguessable. - -To access the current session you can use the :class:`session` object: - -.. class:: session - - The session object works pretty much like an ordinary dict, with the - difference that it keeps track of modifications. - - This is a proxy. See :ref:`notes-on-proxies` for more information. - - The following attributes are interesting: - - .. attribute:: new - - ``True`` if the session is new, ``False`` otherwise. - - .. attribute:: modified - - ``True`` if the session object detected a modification. Be advised - that modifications on mutable structures are not picked up - automatically, in that situation you have to explicitly set the - attribute to ``True`` yourself. Here an example:: - - # this change is not picked up because a mutable object (here - # a list) is changed. - session['objects'].append(42) - # so mark it as modified yourself - session.modified = True - - .. attribute:: permanent - - If set to ``True`` the session lives for - :attr:`~flask.Flask.permanent_session_lifetime` seconds. The - default is 31 days. If set to ``False`` (which is the default) the - session will be deleted when the user closes the browser. - - -Session Interface ------------------ - -.. versionadded:: 0.8 - -The session interface provides a simple way to replace the session -implementation that Flask is using. - -.. currentmodule:: flask.sessions - -.. autoclass:: SessionInterface - :members: - -.. autoclass:: SecureCookieSessionInterface - :members: - -.. autoclass:: SecureCookieSession - :members: - -.. autoclass:: NullSession - :members: - -.. autoclass:: SessionMixin - :members: - -.. admonition:: Notice - - The :data:`PERMANENT_SESSION_LIFETIME` config can be an integer or ``timedelta``. - The :attr:`~flask.Flask.permanent_session_lifetime` attribute is always a - ``timedelta``. - - -Test Client ------------ - -.. currentmodule:: flask.testing - -.. autoclass:: FlaskClient - :members: - - -Test CLI Runner ---------------- - -.. currentmodule:: flask.testing - -.. autoclass:: FlaskCliRunner - :members: - - -Application Globals -------------------- - -.. currentmodule:: flask - -To share data that is valid for one request only from one function to -another, a global variable is not good enough because it would break in -threaded environments. Flask provides you with a special object that -ensures it is only valid for the active request and that will return -different values for each request. In a nutshell: it does the right -thing, like it does for :class:`request` and :class:`session`. - -.. data:: g - - A namespace object that can store data during an - :doc:`application context `. This is an instance of - :attr:`Flask.app_ctx_globals_class`, which defaults to - :class:`ctx._AppCtxGlobals`. - - This is a good place to store resources during a request. For - example, a ``before_request`` function could load a user object from - a session id, then set ``g.user`` to be used in the view function. - - This is a proxy. See :ref:`notes-on-proxies` for more information. - - .. versionchanged:: 0.10 - Bound to the application context instead of the request context. - -.. autoclass:: flask.ctx._AppCtxGlobals - :members: - - -Useful Functions and Classes ----------------------------- - -.. data:: current_app - - A proxy to the application handling the current request. This is - useful to access the application without needing to import it, or if - it can't be imported, such as when using the application factory - pattern or in blueprints and extensions. - - This is only available when an - :doc:`application context ` is pushed. This happens - automatically during requests and CLI commands. It can be controlled - manually with :meth:`~flask.Flask.app_context`. - - This is a proxy. See :ref:`notes-on-proxies` for more information. - -.. autofunction:: has_request_context - -.. autofunction:: copy_current_request_context - -.. autofunction:: has_app_context - -.. autofunction:: url_for - -.. autofunction:: abort - -.. autofunction:: redirect - -.. autofunction:: make_response - -.. autofunction:: after_this_request - -.. autofunction:: send_file - -.. autofunction:: send_from_directory - - -Message Flashing ----------------- - -.. autofunction:: flash - -.. autofunction:: get_flashed_messages - - -JSON Support ------------- - -.. module:: flask.json - -Flask uses Python's built-in :mod:`json` module for handling JSON by -default. The JSON implementation can be changed by assigning a different -provider to :attr:`flask.Flask.json_provider_class` or -:attr:`flask.Flask.json`. The functions provided by ``flask.json`` will -use methods on ``app.json`` if an app context is active. - -Jinja's ``|tojson`` filter is configured to use the app's JSON provider. -The filter marks the output with ``|safe``. Use it to render data inside -HTML `` - -.. autofunction:: jsonify - -.. autofunction:: dumps - -.. autofunction:: dump - -.. autofunction:: loads - -.. autofunction:: load - -.. autoclass:: flask.json.provider.JSONProvider - :members: - :member-order: bysource - -.. autoclass:: flask.json.provider.DefaultJSONProvider - :members: - :member-order: bysource - -.. automodule:: flask.json.tag - - -Template Rendering ------------------- - -.. currentmodule:: flask - -.. autofunction:: render_template - -.. autofunction:: render_template_string - -.. autofunction:: stream_template - -.. autofunction:: stream_template_string - -.. autofunction:: get_template_attribute - -Configuration -------------- - -.. autoclass:: Config - :members: - - -Stream Helpers --------------- - -.. autofunction:: stream_with_context - -Useful Internals ----------------- - -.. autoclass:: flask.ctx.RequestContext - :members: - -.. data:: flask.globals.request_ctx - - The current :class:`~flask.ctx.RequestContext`. If a request context - is not active, accessing attributes on this proxy will raise a - ``RuntimeError``. - - This is an internal object that is essential to how Flask handles - requests. Accessing this should not be needed in most cases. Most - likely you want :data:`request` and :data:`session` instead. - -.. autoclass:: flask.ctx.AppContext - :members: - -.. data:: flask.globals.app_ctx - - The current :class:`~flask.ctx.AppContext`. If an app context is not - active, accessing attributes on this proxy will raise a - ``RuntimeError``. - - This is an internal object that is essential to how Flask handles - requests. Accessing this should not be needed in most cases. Most - likely you want :data:`current_app` and :data:`g` instead. - -.. autoclass:: flask.blueprints.BlueprintSetupState - :members: - -.. _core-signals-list: - -Signals -------- - -Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. - -.. _blinker: https://blinker.readthedocs.io/ - -.. data:: template_rendered - - This signal is sent when a template was successfully rendered. The - signal is invoked with the instance of the template as `template` - and the context as dictionary (named `context`). - - Example subscriber:: - - def log_template_renders(sender, template, context, **extra): - sender.logger.debug('Rendering template "%s" with context %s', - template.name or 'string template', - context) - - from flask import template_rendered - template_rendered.connect(log_template_renders, app) - -.. data:: flask.before_render_template - :noindex: - - This signal is sent before template rendering process. The - signal is invoked with the instance of the template as `template` - and the context as dictionary (named `context`). - - Example subscriber:: - - def log_template_renders(sender, template, context, **extra): - sender.logger.debug('Rendering template "%s" with context %s', - template.name or 'string template', - context) - - from flask import before_render_template - before_render_template.connect(log_template_renders, app) - -.. data:: request_started - - This signal is sent when the request context is set up, before - any request processing happens. Because the request context is already - bound, the subscriber can access the request with the standard global - proxies such as :class:`~flask.request`. - - Example subscriber:: - - def log_request(sender, **extra): - sender.logger.debug('Request context is set up') - - from flask import request_started - request_started.connect(log_request, app) - -.. data:: request_finished - - This signal is sent right before the response is sent to the client. - It is passed the response to be sent named `response`. - - Example subscriber:: - - def log_response(sender, response, **extra): - sender.logger.debug('Request context is about to close down. ' - 'Response: %s', response) - - from flask import request_finished - request_finished.connect(log_response, app) - -.. data:: got_request_exception - - This signal is sent when an unhandled exception happens during - request processing, including when debugging. The exception is - passed to the subscriber as ``exception``. - - This signal is not sent for - :exc:`~werkzeug.exceptions.HTTPException`, or other exceptions that - have error handlers registered, unless the exception was raised from - an error handler. - - This example shows how to do some extra logging if a theoretical - ``SecurityException`` was raised: - - .. code-block:: python - - from flask import got_request_exception - - def log_security_exception(sender, exception, **extra): - if not isinstance(exception, SecurityException): - return - - security_logger.exception( - f"SecurityException at {request.url!r}", - exc_info=exception, - ) - - got_request_exception.connect(log_security_exception, app) - -.. data:: request_tearing_down - - This signal is sent when the request is tearing down. This is always - called, even if an exception is caused. Currently functions listening - to this signal are called after the regular teardown handlers, but this - is not something you can rely on. - - Example subscriber:: - - def close_db_connection(sender, **extra): - session.close() - - from flask import request_tearing_down - request_tearing_down.connect(close_db_connection, app) - - As of Flask 0.9, this will also be passed an `exc` keyword argument - that has a reference to the exception that caused the teardown if - there was one. - -.. data:: appcontext_tearing_down - - This signal is sent when the app context is tearing down. This is always - called, even if an exception is caused. Currently functions listening - to this signal are called after the regular teardown handlers, but this - is not something you can rely on. - - Example subscriber:: - - def close_db_connection(sender, **extra): - session.close() - - from flask import appcontext_tearing_down - appcontext_tearing_down.connect(close_db_connection, app) - - This will also be passed an `exc` keyword argument that has a reference - to the exception that caused the teardown if there was one. - -.. data:: appcontext_pushed - - This signal is sent when an application context is pushed. The sender - is the application. This is usually useful for unittests in order to - temporarily hook in information. For instance it can be used to - set a resource early onto the `g` object. - - Example usage:: - - from contextlib import contextmanager - from flask import appcontext_pushed - - @contextmanager - def user_set(app, user): - def handler(sender, **kwargs): - g.user = user - with appcontext_pushed.connected_to(handler, app): - yield - - And in the testcode:: - - def test_user_me(self): - with user_set(app, 'john'): - c = app.test_client() - resp = c.get('/users/me') - assert resp.data == 'username=john' - - .. versionadded:: 0.10 - -.. data:: appcontext_popped - - This signal is sent when an application context is popped. The sender - is the application. This usually falls in line with the - :data:`appcontext_tearing_down` signal. - - .. versionadded:: 0.10 - -.. data:: message_flashed - - This signal is sent when the application is flashing a message. The - messages is sent as `message` keyword argument and the category as - `category`. - - Example subscriber:: - - recorded = [] - def record(sender, message, category, **extra): - recorded.append((message, category)) - - from flask import message_flashed - message_flashed.connect(record, app) - - .. versionadded:: 0.10 - - -Class-Based Views ------------------ - -.. versionadded:: 0.7 - -.. currentmodule:: None - -.. autoclass:: flask.views.View - :members: - -.. autoclass:: flask.views.MethodView - :members: - -.. _url-route-registrations: - -URL Route Registrations ------------------------ - -Generally there are three ways to define rules for the routing system: - -1. You can use the :meth:`flask.Flask.route` decorator. -2. You can use the :meth:`flask.Flask.add_url_rule` function. -3. You can directly access the underlying Werkzeug routing system - which is exposed as :attr:`flask.Flask.url_map`. - -Variable parts in the route can be specified with angular brackets -(``/user/``). By default a variable part in the URL accepts any -string without a slash however a different converter can be specified as -well by using ````. - -Variable parts are passed to the view function as keyword arguments. - -The following converters are available: - -=========== =============================================== -`string` accepts any text without a slash (the default) -`int` accepts integers -`float` like `int` but for floating point values -`path` like the default but also accepts slashes -`any` matches one of the items provided -`uuid` accepts UUID strings -=========== =============================================== - -Custom converters can be defined using :attr:`flask.Flask.url_map`. - -Here are some examples:: - - @app.route('/') - def index(): - pass - - @app.route('/') - def show_user(username): - pass - - @app.route('/post/') - def show_post(post_id): - pass - -An important detail to keep in mind is how Flask deals with trailing -slashes. The idea is to keep each URL unique so the following rules -apply: - -1. If a rule ends with a slash and is requested without a slash by the - user, the user is automatically redirected to the same page with a - trailing slash attached. -2. If a rule does not end with a trailing slash and the user requests the - page with a trailing slash, a 404 not found is raised. - -This is consistent with how web servers deal with static files. This -also makes it possible to use relative link targets safely. - -You can also define multiple rules for the same function. They have to be -unique however. Defaults can also be specified. Here for example is a -definition for a URL that accepts an optional page:: - - @app.route('/users/', defaults={'page': 1}) - @app.route('/users/page/') - def show_users(page): - pass - -This specifies that ``/users/`` will be the URL for page one and -``/users/page/N`` will be the URL for page ``N``. - -If a URL contains a default value, it will be redirected to its simpler -form with a 301 redirect. In the above example, ``/users/page/1`` will -be redirected to ``/users/``. If your route handles ``GET`` and ``POST`` -requests, make sure the default route only handles ``GET``, as redirects -can't preserve form data. :: - - @app.route('/region/', defaults={'id': 1}) - @app.route('/region/', methods=['GET', 'POST']) - def region(id): - pass - -Here are the parameters that :meth:`~flask.Flask.route` and -:meth:`~flask.Flask.add_url_rule` accept. The only difference is that -with the route parameter the view function is defined with the decorator -instead of the `view_func` parameter. - -=============== ========================================================== -`rule` the URL rule as string -`endpoint` the endpoint for the registered URL rule. Flask itself - assumes that the name of the view function is the name - of the endpoint if not explicitly stated. -`view_func` the function to call when serving a request to the - provided endpoint. If this is not provided one can - specify the function later by storing it in the - :attr:`~flask.Flask.view_functions` dictionary with the - endpoint as key. -`defaults` A dictionary with defaults for this rule. See the - example above for how defaults work. -`subdomain` specifies the rule for the subdomain in case subdomain - matching is in use. If not specified the default - subdomain is assumed. -`**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. They have to be specified as keyword arguments. -=============== ========================================================== - - -View Function Options ---------------------- - -For internal usage the view functions can have some attributes attached to -customize behavior the view function would normally not have control over. -The following attributes can be provided optionally to either override -some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: - -- `__name__`: The name of a function is by default used as endpoint. If - endpoint is provided explicitly this value is used. Additionally this - will be prefixed with the name of the blueprint by default which - cannot be customized from the function itself. - -- `methods`: If methods are not provided when the URL rule is added, - Flask will look on the view function object itself if a `methods` - attribute exists. If it does, it will pull the information for the - methods from there. - -- `provide_automatic_options`: if this attribute is set Flask will - either force enable or disable the automatic implementation of the - HTTP ``OPTIONS`` response. This can be useful when working with - decorators that want to customize the ``OPTIONS`` response on a per-view - basis. - -- `required_methods`: if this attribute is set, Flask will always add - these methods when registering a URL rule even if the methods were - explicitly overridden in the ``route()`` call. - -Full example:: - - def index(): - if request.method == 'OPTIONS': - # custom options handling here - ... - return 'Hello World!' - index.provide_automatic_options = False - index.methods = ['GET', 'OPTIONS'] - - app.add_url_rule('/', index) - -.. versionadded:: 0.8 - The `provide_automatic_options` functionality was added. - -Command Line Interface ----------------------- - -.. currentmodule:: flask.cli - -.. autoclass:: FlaskGroup - :members: - -.. autoclass:: AppGroup - :members: - -.. autoclass:: ScriptInfo - :members: - -.. autofunction:: load_dotenv - -.. autofunction:: with_appcontext - -.. autofunction:: pass_script_info - - Marks a function so that an instance of :class:`ScriptInfo` is passed - as first argument to the click callback. - -.. autodata:: run_command - -.. autodata:: shell_command diff --git a/docs/appcontext.rst b/docs/appcontext.rst deleted file mode 100644 index 5509a9a7..00000000 --- a/docs/appcontext.rst +++ /dev/null @@ -1,147 +0,0 @@ -.. currentmodule:: flask - -The Application Context -======================= - -The application context keeps track of the application-level data during -a request, CLI command, or other activity. Rather than passing the -application around to each function, the :data:`current_app` and -:data:`g` proxies are accessed instead. - -This is similar to :doc:`/reqcontext`, which keeps track of -request-level data during a request. A corresponding application context -is pushed when a request context is pushed. - -Purpose of the Context ----------------------- - -The :class:`Flask` application object has attributes, such as -:attr:`~Flask.config`, that are useful to access within views and -:doc:`CLI commands `. However, importing the ``app`` instance -within the modules in your project is prone to circular import issues. -When using the :doc:`app factory pattern ` or -writing reusable :doc:`blueprints ` or -:doc:`extensions ` there won't be an ``app`` instance to -import at all. - -Flask solves this issue with the *application context*. Rather than -referring to an ``app`` directly, you use the :data:`current_app` -proxy, which points to the application handling the current activity. - -Flask automatically *pushes* an application context when handling a -request. View functions, error handlers, and other functions that run -during a request will have access to :data:`current_app`. - -Flask will also automatically push an app context when running CLI -commands registered with :attr:`Flask.cli` using ``@app.cli.command()``. - - -Lifetime of the Context ------------------------ - -The application context is created and destroyed as necessary. When a -Flask application begins handling a request, it pushes an application -context and a :doc:`request context `. When the request -ends it pops the request context then the application context. -Typically, an application context will have the same lifetime as a -request. - -See :doc:`/reqcontext` for more information about how the contexts work -and the full life cycle of a request. - - -Manually Push a Context ------------------------ - -If you try to access :data:`current_app`, or anything that uses it, -outside an application context, you'll get this error message: - -.. code-block:: pytb - - RuntimeError: Working outside of application context. - - This typically means that you attempted to use functionality that - needed to interface with the current application object in some way. - To solve this, set up an application context with app.app_context(). - -If you see that error while configuring your application, such as when -initializing an extension, you can push a context manually since you -have direct access to the ``app``. Use :meth:`~Flask.app_context` in a -``with`` block, and everything that runs in the block will have access -to :data:`current_app`. :: - - def create_app(): - app = Flask(__name__) - - with app.app_context(): - init_db() - - return app - -If you see that error somewhere else in your code not related to -configuring the application, it most likely indicates that you should -move that code into a view function or CLI command. - - -Storing Data ------------- - -The application context is a good place to store common data during a -request or CLI command. Flask provides the :data:`g object ` for this -purpose. It is a simple namespace object that has the same lifetime as -an application context. - -.. note:: - The ``g`` name stands for "global", but that is referring to the - data being global *within a context*. The data on ``g`` is lost - after the context ends, and it is not an appropriate place to store - data between requests. Use the :data:`session` or a database to - store data across requests. - -A common use for :data:`g` is to manage resources during a request. - -1. ``get_X()`` creates resource ``X`` if it does not exist, caching it - as ``g.X``. -2. ``teardown_X()`` closes or otherwise deallocates the resource if it - exists. It is registered as a :meth:`~Flask.teardown_appcontext` - handler. - -For example, you can manage a database connection using this pattern:: - - from flask import g - - def get_db(): - if 'db' not in g: - g.db = connect_to_database() - - return g.db - - @app.teardown_appcontext - def teardown_db(exception): - db = g.pop('db', None) - - if db is not None: - db.close() - -During a request, every call to ``get_db()`` will return the same -connection, and it will be closed automatically at the end of the -request. - -You can use :class:`~werkzeug.local.LocalProxy` to make a new context -local from ``get_db()``:: - - from werkzeug.local import LocalProxy - db = LocalProxy(get_db) - -Accessing ``db`` will call ``get_db`` internally, in the same way that -:data:`current_app` works. - - -Events and Signals ------------------- - -The application will call functions registered with :meth:`~Flask.teardown_appcontext` -when the application context is popped. - -The following signals are sent: :data:`appcontext_pushed`, -:data:`appcontext_tearing_down`, and :data:`appcontext_popped`. diff --git a/docs/async-await.rst b/docs/async-await.rst deleted file mode 100644 index 06a29fcc..00000000 --- a/docs/async-await.rst +++ /dev/null @@ -1,131 +0,0 @@ -.. _async_await: - -Using ``async`` and ``await`` -============================= - -.. versionadded:: 2.0 - -Routes, error handlers, before request, after request, and teardown -functions can all be coroutine functions if Flask is installed with the -``async`` extra (``pip install flask[async]``). This allows views to be -defined with ``async def`` and use ``await``. - -.. code-block:: python - - @app.route("/get-data") - async def get_data(): - data = await async_db_query(...) - return jsonify(data) - -Pluggable class-based views also support handlers that are implemented as -coroutines. This applies to the :meth:`~flask.views.View.dispatch_request` -method in views that inherit from the :class:`flask.views.View` class, as -well as all the HTTP method handlers in views that inherit from the -:class:`flask.views.MethodView` class. - -.. admonition:: Using ``async`` on Windows on Python 3.8 - - Python 3.8 has a bug related to asyncio on Windows. If you encounter - something like ``ValueError: set_wakeup_fd only works in main thread``, - please upgrade to Python 3.9. - -.. admonition:: Using ``async`` with greenlet - - When using gevent or eventlet to serve an application or patch the - runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is - required. - - -Performance ------------ - -Async functions require an event loop to run. Flask, as a WSGI -application, uses one worker to handle one request/response cycle. -When a request comes in to an async view, Flask will start an event loop -in a thread, run the view function there, then return the result. - -Each request still ties up one worker, even for async views. The upside -is that you can run async code within a view, for example to make -multiple concurrent database queries, HTTP requests to an external API, -etc. However, the number of requests your application can handle at one -time will remain the same. - -**Async is not inherently faster than sync code.** Async is beneficial -when performing concurrent IO-bound tasks, but will probably not improve -CPU-bound tasks. Traditional Flask views will still be appropriate for -most use cases, but Flask's async support enables writing and using -code that wasn't possible natively before. - - -Background tasks ----------------- - -Async functions will run in an event loop until they complete, at -which stage the event loop will stop. This means any additional -spawned tasks that haven't completed when the async function completes -will be cancelled. Therefore you cannot spawn background tasks, for -example via ``asyncio.create_task``. - -If you wish to use background tasks it is best to use a task queue to -trigger background work, rather than spawn tasks in a view -function. With that in mind you can spawn asyncio tasks by serving -Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter -as described in :doc:`deploying/asgi`. This works as the adapter creates -an event loop that runs continually. - - -When to use Quart instead -------------------------- - -Flask's async support is less performant than async-first frameworks due -to the way it is implemented. If you have a mainly async codebase it -would make sense to consider `Quart`_. Quart is a reimplementation of -Flask based on the `ASGI`_ standard instead of WSGI. This allows it to -handle many concurrent requests, long running requests, and websockets -without requiring multiple worker processes or threads. - -It has also already been possible to run Flask with Gevent or Eventlet -to get many of the benefits of async request handling. These libraries -patch low-level Python functions to accomplish this, whereas ``async``/ -``await`` and ASGI use standard, modern Python capabilities. Deciding -whether you should use Flask, Quart, or something else is ultimately up -to understanding the specific needs of your project. - -.. _Quart: https://github.com/pallets/quart -.. _ASGI: https://asgi.readthedocs.io/en/latest/ - - -Extensions ----------- - -Flask extensions predating Flask's async support do not expect async views. -If they provide decorators to add functionality to views, those will probably -not work with async views because they will not await the function or be -awaitable. Other functions they provide will not be awaitable either and -will probably be blocking if called within an async view. - -Extension authors can support async functions by utilising the -:meth:`flask.Flask.ensure_sync` method. For example, if the extension -provides a view function decorator add ``ensure_sync`` before calling -the decorated function, - -.. code-block:: python - - def extension(func): - @wraps(func) - def wrapper(*args, **kwargs): - ... # Extension logic - return current_app.ensure_sync(func)(*args, **kwargs) - - return wrapper - -Check the changelog of the extension you want to use to see if they've -implemented async support, or make a feature request or PR to them. - - -Other event loops ------------------ - -At the moment Flask only supports :mod:`asyncio`. It's possible to -override :meth:`flask.Flask.ensure_sync` to change how async functions -are wrapped to use a different library. diff --git a/docs/blueprints.rst b/docs/blueprints.rst deleted file mode 100644 index d5cf3d82..00000000 --- a/docs/blueprints.rst +++ /dev/null @@ -1,315 +0,0 @@ -Modular Applications with Blueprints -==================================== - -.. currentmodule:: flask - -.. versionadded:: 0.7 - -Flask uses a concept of *blueprints* for making application components and -supporting common patterns within an application or across applications. -Blueprints can greatly simplify how large applications work and provide a -central means for Flask extensions to register operations on applications. -A :class:`Blueprint` object works similarly to a :class:`Flask` -application object, but it is not actually an application. Rather it is a -*blueprint* of how to construct or extend an application. - -Why Blueprints? ---------------- - -Blueprints in Flask are intended for these cases: - -* Factor an application into a set of blueprints. This is ideal for - larger applications; a project could instantiate an application object, - initialize several extensions, and register a collection of blueprints. -* Register a blueprint on an application at a URL prefix and/or subdomain. - Parameters in the URL prefix/subdomain become common view arguments - (with defaults) across all view functions in the blueprint. -* Register a blueprint multiple times on an application with different URL - rules. -* Provide template filters, static files, templates, and other utilities - through blueprints. A blueprint does not have to implement applications - or view functions. -* Register a blueprint on an application for any of these cases when - initializing a Flask extension. - -A blueprint in Flask is not a pluggable app because it is not actually an -application -- it's a set of operations which can be registered on an -application, even multiple times. Why not have multiple application -objects? You can do that (see :doc:`/patterns/appdispatch`), but your -applications will have separate configs and will be managed at the WSGI -layer. - -Blueprints instead provide separation at the Flask level, share -application config, and can change an application object as necessary with -being registered. The downside is that you cannot unregister a blueprint -once an application was created without having to destroy the whole -application object. - -The Concept of Blueprints -------------------------- - -The basic concept of blueprints is that they record operations to execute -when registered on an application. Flask associates view functions with -blueprints when dispatching requests and generating URLs from one endpoint -to another. - -My First Blueprint ------------------- - -This is what a very basic blueprint looks like. In this case we want to -implement a blueprint that does simple rendering of static templates:: - - from flask import Blueprint, render_template, abort - from jinja2 import TemplateNotFound - - simple_page = Blueprint('simple_page', __name__, - template_folder='templates') - - @simple_page.route('/', defaults={'page': 'index'}) - @simple_page.route('/') - def show(page): - try: - return render_template(f'pages/{page}.html') - except TemplateNotFound: - abort(404) - -When you bind a function with the help of the ``@simple_page.route`` -decorator, the blueprint will record the intention of registering the -function ``show`` on the application when it's later registered. -Additionally it will prefix the endpoint of the function with the -name of the blueprint which was given to the :class:`Blueprint` -constructor (in this case also ``simple_page``). The blueprint's name -does not modify the URL, only the endpoint. - -Registering Blueprints ----------------------- - -So how do you register that blueprint? Like this:: - - from flask import Flask - from yourapplication.simple_page import simple_page - - app = Flask(__name__) - app.register_blueprint(simple_page) - -If you check the rules registered on the application, you will find -these:: - - >>> app.url_map - Map([' (HEAD, OPTIONS, GET) -> static>, - ' (HEAD, OPTIONS, GET) -> simple_page.show>, - simple_page.show>]) - -The first one is obviously from the application itself for the static -files. The other two are for the `show` function of the ``simple_page`` -blueprint. As you can see, they are also prefixed with the name of the -blueprint and separated by a dot (``.``). - -Blueprints however can also be mounted at different locations:: - - app.register_blueprint(simple_page, url_prefix='/pages') - -And sure enough, these are the generated rules:: - - >>> app.url_map - Map([' (HEAD, OPTIONS, GET) -> static>, - ' (HEAD, OPTIONS, GET) -> simple_page.show>, - simple_page.show>]) - -On top of that you can register blueprints multiple times though not every -blueprint might respond properly to that. In fact it depends on how the -blueprint is implemented if it can be mounted more than once. - -Nesting Blueprints ------------------- - -It is possible to register a blueprint on another blueprint. - -.. code-block:: python - - parent = Blueprint('parent', __name__, url_prefix='/parent') - child = Blueprint('child', __name__, url_prefix='/child') - parent.register_blueprint(child) - app.register_blueprint(parent) - -The child blueprint will gain the parent's name as a prefix to its -name, and child URLs will be prefixed with the parent's URL prefix. - -.. code-block:: python - - url_for('parent.child.create') - /parent/child/create - -In addition a child blueprint's will gain their parent's subdomain, -with their subdomain as prefix if present i.e. - -.. code-block:: python - - parent = Blueprint('parent', __name__, subdomain='parent') - child = Blueprint('child', __name__, subdomain='child') - parent.register_blueprint(child) - app.register_blueprint(parent) - - url_for('parent.child.create', _external=True) - "child.parent.domain.tld" - -Blueprint-specific before request functions, etc. registered with the -parent will trigger for the child. If a child does not have an error -handler that can handle a given exception, the parent's will be tried. - - -Blueprint Resources -------------------- - -Blueprints can provide resources as well. Sometimes you might want to -introduce a blueprint only for the resources it provides. - -Blueprint Resource Folder -````````````````````````` - -Like for regular applications, blueprints are considered to be contained -in a folder. While multiple blueprints can originate from the same folder, -it does not have to be the case and it's usually not recommended. - -The folder is inferred from the second argument to :class:`Blueprint` which -is usually `__name__`. This argument specifies what logical Python -module or package corresponds to the blueprint. If it points to an actual -Python package that package (which is a folder on the filesystem) is the -resource folder. If it's a module, the package the module is contained in -will be the resource folder. You can access the -:attr:`Blueprint.root_path` property to see what the resource folder is:: - - >>> simple_page.root_path - '/Users/username/TestProject/yourapplication' - -To quickly open sources from this folder you can use the -:meth:`~Blueprint.open_resource` function:: - - with simple_page.open_resource('static/style.css') as f: - code = f.read() - -Static Files -```````````` - -A blueprint can expose a folder with static files by providing the path -to the folder on the filesystem with the ``static_folder`` argument. -It is either an absolute path or relative to the blueprint's location:: - - admin = Blueprint('admin', __name__, static_folder='static') - -By default the rightmost part of the path is where it is exposed on the -web. This can be changed with the ``static_url_path`` argument. Because the -folder is called ``static`` here it will be available at the -``url_prefix`` of the blueprint + ``/static``. If the blueprint -has the prefix ``/admin``, the static URL will be ``/admin/static``. - -The endpoint is named ``blueprint_name.static``. You can generate URLs -to it with :func:`url_for` like you would with the static folder of the -application:: - - url_for('admin.static', filename='style.css') - -However, if the blueprint does not have a ``url_prefix``, it is not -possible to access the blueprint's static folder. This is because the -URL would be ``/static`` in this case, and the application's ``/static`` -route takes precedence. Unlike template folders, blueprint static -folders are not searched if the file does not exist in the application -static folder. - -Templates -````````` - -If you want the blueprint to expose templates you can do that by providing -the `template_folder` parameter to the :class:`Blueprint` constructor:: - - admin = Blueprint('admin', __name__, template_folder='templates') - -For static files, the path can be absolute or relative to the blueprint -resource folder. - -The template folder is added to the search path of templates but with a lower -priority than the actual application's template folder. That way you can -easily override templates that a blueprint provides in the actual application. -This also means that if you don't want a blueprint template to be accidentally -overridden, make sure that no other blueprint or actual application template -has the same relative path. When multiple blueprints provide the same relative -template path the first blueprint registered takes precedence over the others. - - -So if you have a blueprint in the folder ``yourapplication/admin`` and you -want to render the template ``'admin/index.html'`` and you have provided -``templates`` as a `template_folder` you will have to create a file like -this: :file:`yourapplication/admin/templates/admin/index.html`. The reason -for the extra ``admin`` folder is to avoid getting our template overridden -by a template named ``index.html`` in the actual application template -folder. - -To further reiterate this: if you have a blueprint named ``admin`` and you -want to render a template called :file:`index.html` which is specific to this -blueprint, the best idea is to lay out your templates like this:: - - yourpackage/ - blueprints/ - admin/ - templates/ - admin/ - index.html - __init__.py - -And then when you want to render the template, use :file:`admin/index.html` as -the name to look up the template by. If you encounter problems loading -the correct templates enable the ``EXPLAIN_TEMPLATE_LOADING`` config -variable which will instruct Flask to print out the steps it goes through -to locate templates on every ``render_template`` call. - -Building URLs -------------- - -If you want to link from one page to another you can use the -:func:`url_for` function just like you normally would do just that you -prefix the URL endpoint with the name of the blueprint and a dot (``.``):: - - url_for('admin.index') - -Additionally if you are in a view function of a blueprint or a rendered -template and you want to link to another endpoint of the same blueprint, -you can use relative redirects by prefixing the endpoint with a dot only:: - - url_for('.index') - -This will link to ``admin.index`` for instance in case the current request -was dispatched to any other admin blueprint endpoint. - - -Blueprint Error Handlers ------------------------- - -Blueprints support the ``errorhandler`` decorator just like the :class:`Flask` -application object, so it is easy to make Blueprint-specific custom error -pages. - -Here is an example for a "404 Page Not Found" exception:: - - @simple_page.errorhandler(404) - def page_not_found(e): - return render_template('pages/404.html') - -Most errorhandlers will simply work as expected; however, there is a caveat -concerning handlers for 404 and 405 exceptions. These errorhandlers are only -invoked from an appropriate ``raise`` statement or a call to ``abort`` in another -of the blueprint's view functions; they are not invoked by, e.g., an invalid URL -access. This is because the blueprint does not "own" a certain URL space, so -the application instance has no way of knowing which blueprint error handler it -should run if given an invalid URL. If you would like to execute different -handling strategies for these errors based on URL prefixes, they may be defined -at the application level using the ``request`` proxy object:: - - @app.errorhandler(404) - @app.errorhandler(405) - def _handle_api_error(ex): - if request.path.startswith('/api/'): - return jsonify(error=str(ex)), ex.code - else: - return ex - -See :doc:`/errorhandling`. diff --git a/docs/changes.rst b/docs/changes.rst deleted file mode 100644 index 955deaf2..00000000 --- a/docs/changes.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changes -======= - -.. include:: ../CHANGES.rst diff --git a/docs/cli.rst b/docs/cli.rst deleted file mode 100644 index a72e6d51..00000000 --- a/docs/cli.rst +++ /dev/null @@ -1,556 +0,0 @@ -.. currentmodule:: flask - -Command Line Interface -====================== - -Installing Flask installs the ``flask`` script, a `Click`_ command line -interface, in your virtualenv. Executed from the terminal, this script gives -access to built-in, extension, and application-defined commands. The ``--help`` -option will give more information about any commands and options. - -.. _Click: https://click.palletsprojects.com/ - - -Application Discovery ---------------------- - -The ``flask`` command is installed by Flask, not your application; it must be -told where to find your application in order to use it. The ``--app`` -option is used to specify how to load the application. - -While ``--app`` supports a variety of options for specifying your -application, most use cases should be simple. Here are the typical values: - -(nothing) - The name "app" or "wsgi" is imported (as a ".py" file, or package), - automatically detecting an app (``app`` or ``application``) or - factory (``create_app`` or ``make_app``). - -``--app hello`` - The given name is imported, automatically detecting an app (``app`` - or ``application``) or factory (``create_app`` or ``make_app``). - ----- - -``--app`` has three parts: an optional path that sets the current working -directory, a Python file or dotted import path, and an optional variable -name of the instance or factory. If the name is a factory, it can optionally -be followed by arguments in parentheses. The following values demonstrate these -parts: - -``--app src/hello`` - Sets the current working directory to ``src`` then imports ``hello``. - -``--app hello.web`` - Imports the path ``hello.web``. - -``--app hello:app2`` - Uses the ``app2`` Flask instance in ``hello``. - -``--app 'hello:create_app("dev")'`` - The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` - as the argument. - -If ``--app`` is not set, the command will try to import "app" or -"wsgi" (as a ".py" file, or package) and try to detect an application -instance or factory. - -Within the given import, the command looks for an application instance named -``app`` or ``application``, then any application instance. If no instance is -found, the command looks for a factory function named ``create_app`` or -``make_app`` that returns an instance. - -If parentheses follow the factory name, their contents are parsed as -Python literals and passed as arguments and keyword arguments to the -function. This means that strings must still be in quotes. - - -Run the Development Server --------------------------- - -The :func:`run ` command will start the development server. It -replaces the :meth:`Flask.run` method in most cases. :: - - $ flask --app hello run - * Serving Flask app "hello" - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - -.. warning:: Do not use this command to run your application in production. - Only use the development server during development. The development server - is provided for convenience, but is not designed to be particularly secure, - stable, or efficient. See :doc:`/deploying/index` for how to run in production. - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. - - -Debug Mode -~~~~~~~~~~ - -In debug mode, the ``flask run`` command will enable the interactive debugger and the -reloader by default, and make errors easier to see and debug. To enable debug mode, use -the ``--debug`` option. - -.. code-block:: console - - $ flask --app hello run --debug - * Serving Flask app "hello" - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 - -The ``--debug`` option can also be passed to the top level ``flask`` command to enable -debug mode for any command. The following two ``run`` calls are equivalent. - -.. code-block:: console - - $ flask --app hello --debug run - $ flask --app hello run --debug - - -Watch and Ignore Files with the Reloader -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using debug mode, the reloader will trigger whenever your Python code or imported -modules change. The reloader can watch additional files with the ``--extra-files`` -option. Multiple paths are separated with ``:``, or ``;`` on Windows. - -.. code-block:: text - - $ flask run --extra-files file1:dirA/file2:dirB/ - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading - -The reloader can also ignore files using :mod:`fnmatch` patterns with the -``--exclude-patterns`` option. Multiple patterns are separated with ``:``, or ``;`` on -Windows. - - -Open a Shell ------------- - -To explore the data in your application, you can start an interactive Python -shell with the :func:`shell ` command. An application -context will be active, and the app instance will be imported. :: - - $ flask shell - Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux - App: example [production] - Instance: /home/david/Projects/pallets/flask/instance - >>> - -Use :meth:`~Flask.shell_context_processor` to add other automatic imports. - - -.. _dotenv: - -Environment Variables From dotenv ---------------------------------- - -The ``flask`` command supports setting any option for any command with -environment variables. The variables are named like ``FLASK_OPTION`` or -``FLASK_COMMAND_OPTION``, for example ``FLASK_APP`` or -``FLASK_RUN_PORT``. - -Rather than passing options every time you run a command, or environment -variables every time you open a new terminal, you can use Flask's dotenv -support to set environment variables automatically. - -If `python-dotenv`_ is installed, running the ``flask`` command will set -environment variables defined in the files ``.env`` and ``.flaskenv``. -You can also specify an extra file to load with the ``--env-file`` -option. Dotenv files can be used to avoid having to set ``--app`` or -``FLASK_APP`` manually, and to set configuration using environment -variables similar to how some deployment services work. - -Variables set on the command line are used over those set in :file:`.env`, -which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be -used for public variables, such as ``FLASK_APP``, while :file:`.env` should not -be committed to your repository so that it can set private variables. - -Directories are scanned upwards from the directory you call ``flask`` -from to locate the files. - -The files are only loaded by the ``flask`` command or calling -:meth:`~Flask.run`. If you would like to load these files when running in -production, you should call :func:`~cli.load_dotenv` manually. - -.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - - -Setting Command Options -~~~~~~~~~~~~~~~~~~~~~~~ - -Click is configured to load default values for command options from -environment variables. The variables use the pattern -``FLASK_COMMAND_OPTION``. For example, to set the port for the run -command, instead of ``flask run --port 8000``: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_RUN_PORT=8000 - $ flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_RUN_PORT 8000 - $ flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_RUN_PORT=8000 - > flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_RUN_PORT = 8000 - > flask run - * Running on http://127.0.0.1:8000/ - -These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to -control default command options. - - -Disable dotenv -~~~~~~~~~~~~~~ - -The ``flask`` command will show a message if it detects dotenv files but -python-dotenv is not installed. - -.. code-block:: bash - - $ flask run - * Tip: There are .env files present. Do "pip install python-dotenv" to use them. - -You can tell Flask not to load dotenv files even when python-dotenv is -installed by setting the ``FLASK_SKIP_DOTENV`` environment variable. -This can be useful if you want to load them manually, or if you're using -a project runner that loads them already. Keep in mind that the -environment variables must be set before the app loads or it won't -configure as expected. - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_SKIP_DOTENV=1 - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_SKIP_DOTENV 1 - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_SKIP_DOTENV=1 - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_SKIP_DOTENV = 1 - > flask run - - -Environment Variables From virtualenv -------------------------------------- - -If you do not want to install dotenv support, you can still set environment -variables by adding them to the end of the virtualenv's :file:`activate` -script. Activating the virtualenv will set the variables. - -.. tabs:: - - .. group-tab:: Bash - - Unix Bash, :file:`.venv/bin/activate`:: - - $ export FLASK_APP=hello - - .. group-tab:: Fish - - Fish, :file:`.venv/bin/activate.fish`:: - - $ set -x FLASK_APP hello - - .. group-tab:: CMD - - Windows CMD, :file:`.venv\\Scripts\\activate.bat`:: - - > set FLASK_APP=hello - - .. group-tab:: Powershell - - Windows Powershell, :file:`.venv\\Scripts\\activate.ps1`:: - - > $env:FLASK_APP = "hello" - -It is preferred to use dotenv support over this, since :file:`.flaskenv` can be -committed to the repository so that it works automatically wherever the project -is checked out. - - -Custom Commands ---------------- - -The ``flask`` command is implemented using `Click`_. See that project's -documentation for full information about writing commands. - -This example adds the command ``create-user`` that takes the argument -``name``. :: - - import click - from flask import Flask - - app = Flask(__name__) - - @app.cli.command("create-user") - @click.argument("name") - def create_user(name): - ... - -:: - - $ flask create-user admin - -This example adds the same command, but as ``user create``, a command in a -group. This is useful if you want to organize multiple related commands. :: - - import click - from flask import Flask - from flask.cli import AppGroup - - app = Flask(__name__) - user_cli = AppGroup('user') - - @user_cli.command('create') - @click.argument('name') - def create_user(name): - ... - - app.cli.add_command(user_cli) - -:: - - $ flask user create demo - -See :ref:`testing-cli` for an overview of how to test your custom -commands. - - -Registering Commands with Blueprints -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your application uses blueprints, you can optionally register CLI -commands directly onto them. When your blueprint is registered onto your -application, the associated commands will be available to the ``flask`` -command. By default, those commands will be nested in a group matching -the name of the blueprint. - -.. code-block:: python - - from flask import Blueprint - - bp = Blueprint('students', __name__) - - @bp.cli.command('create') - @click.argument('name') - def create(name): - ... - - app.register_blueprint(bp) - -.. code-block:: text - - $ flask students create alice - -You can alter the group name by specifying the ``cli_group`` parameter -when creating the :class:`Blueprint` object, or later with -:meth:`app.register_blueprint(bp, cli_group='...') `. -The following are equivalent: - -.. code-block:: python - - bp = Blueprint('students', __name__, cli_group='other') - # or - app.register_blueprint(bp, cli_group='other') - -.. code-block:: text - - $ flask other create alice - -Specifying ``cli_group=None`` will remove the nesting and merge the -commands directly to the application's level: - -.. code-block:: python - - bp = Blueprint('students', __name__, cli_group=None) - # or - app.register_blueprint(bp, cli_group=None) - -.. code-block:: text - - $ flask create alice - - -Application Context -~~~~~~~~~~~~~~~~~~~ - -Commands added using the Flask app's :attr:`~Flask.cli` or -:class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator -will be executed with an application context pushed, so your custom -commands and parameters have access to the app and its configuration. The -:func:`~cli.with_appcontext` decorator can be used to get the same -behavior, but is not needed in most cases. - -.. code-block:: python - - import click - from flask.cli import with_appcontext - - @click.command() - @with_appcontext - def do_work(): - ... - - app.cli.add_command(do_work) - - -Plugins -------- - -Flask will automatically load commands specified in the ``flask.commands`` -`entry point`_. This is useful for extensions that want to add commands when -they are installed. Entry points are specified in :file:`pyproject.toml`: - -.. code-block:: toml - - [project.entry-points."flask.commands"] - my-command = "my_extension.commands:cli" - -.. _entry point: https://packaging.python.org/tutorials/packaging-projects/#entry-points - -Inside :file:`my_extension/commands.py` you can then export a Click -object:: - - import click - - @click.command() - def cli(): - ... - -Once that package is installed in the same virtualenv as your Flask project, -you can run ``flask my-command`` to invoke the command. - - -.. _custom-scripts: - -Custom Scripts --------------- - -When you are using the app factory pattern, it may be more convenient to define -your own Click script. Instead of using ``--app`` and letting Flask load -your application, you can create your own Click object and export it as a -`console script`_ entry point. - -Create an instance of :class:`~cli.FlaskGroup` and pass it the factory:: - - import click - from flask import Flask - from flask.cli import FlaskGroup - - def create_app(): - app = Flask('wiki') - # other setup - return app - - @click.group(cls=FlaskGroup, create_app=create_app) - def cli(): - """Management script for the Wiki application.""" - -Define the entry point in :file:`pyproject.toml`: - -.. code-block:: toml - - [project.scripts] - wiki = "wiki:cli" - -Install the application in the virtualenv in editable mode and the custom -script is available. Note that you don't need to set ``--app``. :: - - $ pip install -e . - $ wiki run - -.. admonition:: Errors in Custom Scripts - - When using a custom script, if you introduce an error in your - module-level code, the reloader will fail because it can no longer - load the entry point. - - The ``flask`` command, being separate from your code, does not have - this issue and is recommended in most cases. - -.. _console script: https://packaging.python.org/tutorials/packaging-projects/#console-scripts - - -PyCharm Integration -------------------- - -PyCharm Professional provides a special Flask run configuration to run the development -server. For the Community Edition, and for other commands besides ``run``, you need to -create a custom run configuration. These instructions should be similar for any other -IDE you use. - -In PyCharm, with your project open, click on *Run* from the menu bar and go to *Edit -Configurations*. You'll see a screen similar to this: - -.. image:: _static/pycharm-run-config.png - :align: center - :class: screenshot - :alt: Screenshot of PyCharm run configuration. - -Once you create a configuration for the ``flask run``, you can copy and change it to -call any other command. - -Click the *+ (Add New Configuration)* button and select *Python*. Give the configuration -a name such as "flask run". - -Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. - -The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello run --debug``, which will run the development server in -debug mode. ``--app hello`` should be the import or file with your Flask app. - -If you installed your project as a package in your virtualenv, you may uncheck the -*PYTHONPATH* options. This will more accurately match how you deploy later. - -Click *OK* to save and close the configuration. Select the configuration in the main -PyCharm window and click the play button next to it to run the server. - -Now that you have a configuration for ``flask run``, you can copy that configuration and -change the *Parameters* argument to run a different CLI command. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 25b8f004..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,97 +0,0 @@ -import packaging.version -from pallets_sphinx_themes import get_version -from pallets_sphinx_themes import ProjectLink - -# Project -------------------------------------------------------------- - -project = "Flask" -copyright = "2010 Pallets" -author = "Pallets" -release, version = get_version("Flask") - -# General -------------------------------------------------------------- - -default_role = "code" -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.extlinks", - "sphinx.ext.intersphinx", - "sphinxcontrib.log_cabinet", - "sphinx_tabs.tabs", - "pallets_sphinx_themes", -] -autodoc_member_order = "bysource" -autodoc_typehints = "description" -autodoc_preserve_defaults = True -extlinks = { - "issue": ("https://github.com/pallets/flask/issues/%s", "#%s"), - "pr": ("https://github.com/pallets/flask/pull/%s", "#%s"), -} -intersphinx_mapping = { - "python": ("https://docs.python.org/3/", None), - "werkzeug": ("https://werkzeug.palletsprojects.com/", None), - "click": ("https://click.palletsprojects.com/", None), - "jinja": ("https://jinja.palletsprojects.com/", None), - "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), - "sqlalchemy": ("https://docs.sqlalchemy.org/", None), - "wtforms": ("https://wtforms.readthedocs.io/", None), - "blinker": ("https://blinker.readthedocs.io/", None), -} - -# HTML ----------------------------------------------------------------- - -html_theme = "flask" -html_theme_options = {"index_sidebar_logo": False} -html_context = { - "project_links": [ - ProjectLink("Donate", "https://palletsprojects.com/donate"), - ProjectLink("PyPI Releases", "https://pypi.org/project/Flask/"), - ProjectLink("Source Code", "https://github.com/pallets/flask/"), - ProjectLink("Issue Tracker", "https://github.com/pallets/flask/issues/"), - ProjectLink("Chat", "https://discord.gg/pallets"), - ] -} -html_sidebars = { - "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], - "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], -} -singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} -html_static_path = ["_static"] -html_favicon = "_static/shortcut-icon.png" -html_logo = "_static/flask-vertical.png" -html_title = f"Flask Documentation ({version})" -html_show_sourcelink = False - -# Local Extensions ----------------------------------------------------- - - -def github_link(name, rawtext, text, lineno, inliner, options=None, content=None): - app = inliner.document.settings.env.app - release = app.config.release - base_url = "https://github.com/pallets/flask/tree/" - - if text.endswith(">"): - words, text = text[:-1].rsplit("<", 1) - words = words.strip() - else: - words = None - - if packaging.version.parse(release).is_devrelease: - url = f"{base_url}main/{text}" - else: - url = f"{base_url}{release}/{text}" - - if words is None: - words = url - - from docutils.nodes import reference - from docutils.parsers.rst.roles import set_classes - - options = options or {} - set_classes(options) - node = reference(rawtext, words, refuri=url, **options) - return [node], [] - - -def setup(app): - app.add_role("gh", github_link) diff --git a/docs/config.rst b/docs/config.rst deleted file mode 100644 index f9e71774..00000000 --- a/docs/config.rst +++ /dev/null @@ -1,737 +0,0 @@ -Configuration Handling -====================== - -Applications need some kind of configuration. There are different settings -you might want to change depending on the application environment like -toggling the debug mode, setting the secret key, and other such -environment-specific things. - -The way Flask is designed usually requires the configuration to be -available when the application starts up. You can hard code the -configuration in the code, which for many small applications is not -actually that bad, but there are better ways. - -Independent of how you load your config, there is a config object -available which holds the loaded configuration values: -The :attr:`~flask.Flask.config` attribute of the :class:`~flask.Flask` -object. This is the place where Flask itself puts certain configuration -values and also where extensions can put their configuration values. But -this is also where you can have your own configuration. - - -Configuration Basics --------------------- - -The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and -can be modified just like any dictionary:: - - app = Flask(__name__) - app.config['TESTING'] = True - -Certain configuration values are also forwarded to the -:attr:`~flask.Flask` object so you can read and write them from there:: - - app.testing = True - -To update multiple keys at once you can use the :meth:`dict.update` -method:: - - app.config.update( - TESTING=True, - SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - ) - - -Debug Mode ----------- - -The :data:`DEBUG` config value is special because it may behave inconsistently if -changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the -interactive debugger and reloader by default in debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -Using the option is recommended. While it is possible to set :data:`DEBUG` in your -config or code, this is strongly discouraged. It can't be read early by the -``flask run`` command, and some systems or extensions may have already configured -themselves based on a previous value. - - -Builtin Configuration Values ----------------------------- - -The following configuration values are used internally by Flask: - -.. py:data:: DEBUG - - Whether debug mode is enabled. When using ``flask run`` to start the development - server, an interactive debugger will be shown for unhandled exceptions, and the - server will be reloaded when code changes. The :attr:`~flask.Flask.debug` attribute - maps to this config key. This is set with the ``FLASK_DEBUG`` environment variable. - It may not behave as expected if set in code. - - **Do not enable debug mode when deploying in production.** - - Default: ``False`` - -.. py:data:: TESTING - - Enable testing mode. Exceptions are propagated rather than handled by the - the app's error handlers. Extensions may also change their behavior to - facilitate easier testing. You should enable this in your own tests. - - Default: ``False`` - -.. py:data:: PROPAGATE_EXCEPTIONS - - Exceptions are re-raised rather than being handled by the app's error - handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG`` - is enabled. - - Default: ``None`` - -.. py:data:: TRAP_HTTP_EXCEPTIONS - - If there is no handler for an ``HTTPException``-type exception, re-raise it - to be handled by the interactive debugger instead of returning it as a - simple error response. - - Default: ``False`` - -.. py:data:: TRAP_BAD_REQUEST_ERRORS - - Trying to access a key that doesn't exist from request dicts like ``args`` - and ``form`` will return a 400 Bad Request error page. Enable this to treat - the error as an unhandled exception instead so that you get the interactive - debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If - unset, it is enabled in debug mode. - - Default: ``None`` - -.. py:data:: SECRET_KEY - - A secret key that will be used for securely signing the session cookie - and can be used for any other security related needs by extensions or your - application. It should be a long random ``bytes`` or ``str``. For - example, copy the output of this to your config:: - - $ python -c 'import secrets; print(secrets.token_hex())' - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - - **Do not reveal the secret key when posting questions or committing code.** - - Default: ``None`` - -.. py:data:: SESSION_COOKIE_NAME - - The name of the session cookie. Can be changed in case you already have a - cookie with the same name. - - Default: ``'session'`` - -.. py:data:: SESSION_COOKIE_DOMAIN - - The value of the ``Domain`` parameter on the session cookie. If not set, browsers - will only send the cookie to the exact domain it was set from. Otherwise, they - will send it to any subdomain of the given value as well. - - Not setting this value is more restricted and secure than setting it. - - Default: ``None`` - - .. warning:: - If this is changed after the browser created a cookie is created with - one setting, it may result in another being created. Browsers may send - send both in an undefined order. In that case, you may want to change - :data:`SESSION_COOKIE_NAME` as well or otherwise invalidate old sessions. - - .. versionchanged:: 2.3 - Not set by default, does not fall back to ``SERVER_NAME``. - -.. py:data:: SESSION_COOKIE_PATH - - The path that the session cookie will be valid for. If not set, the cookie - will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set. - - Default: ``None`` - -.. py:data:: SESSION_COOKIE_HTTPONLY - - Browsers will not allow JavaScript access to cookies marked as "HTTP only" - for security. - - Default: ``True`` - -.. py:data:: SESSION_COOKIE_SECURE - - Browsers will only send cookies with requests over HTTPS if the cookie is - marked "secure". The application must be served over HTTPS for this to make - sense. - - Default: ``False`` - -.. py:data:: SESSION_COOKIE_SAMESITE - - Restrict how cookies are sent with requests from external sites. Can - be set to ``'Lax'`` (recommended) or ``'Strict'``. - See :ref:`security-cookie`. - - Default: ``None`` - - .. versionadded:: 1.0 - -.. py:data:: PERMANENT_SESSION_LIFETIME - - If ``session.permanent`` is true, the cookie's expiration will be set this - number of seconds in the future. Can either be a - :class:`datetime.timedelta` or an ``int``. - - Flask's default cookie implementation validates that the cryptographic - signature is not older than this value. - - Default: ``timedelta(days=31)`` (``2678400`` seconds) - -.. py:data:: SESSION_REFRESH_EACH_REQUEST - - Control whether the cookie is sent with every response when - ``session.permanent`` is true. Sending the cookie every time (the default) - can more reliably keep the session from expiring, but uses more bandwidth. - Non-permanent sessions are not affected. - - Default: ``True`` - -.. py:data:: USE_X_SENDFILE - - When serving files, set the ``X-Sendfile`` header instead of serving the - data with Flask. Some web servers, such as Apache, recognize this and serve - the data more efficiently. This only makes sense when using such a server. - - Default: ``False`` - -.. py:data:: SEND_FILE_MAX_AGE_DEFAULT - - When serving files, set the cache control max age to this number of - seconds. Can be a :class:`datetime.timedelta` or an ``int``. - Override this value on a per-file basis using - :meth:`~flask.Flask.get_send_file_max_age` on the application or - blueprint. - - If ``None``, ``send_file`` tells the browser to use conditional - requests will be used instead of a timed cache, which is usually - preferable. - - Default: ``None`` - -.. py:data:: SERVER_NAME - - Inform the application what host and port it is bound to. Required - for subdomain route matching support. - - If set, ``url_for`` can generate external URLs with only an application - context instead of a request context. - - Default: ``None`` - - .. versionchanged:: 2.3 - Does not affect ``SESSION_COOKIE_DOMAIN``. - -.. py:data:: APPLICATION_ROOT - - Inform the application what path it is mounted under by the application / - web server. This is used for generating URLs outside the context of a - request (inside a request, the dispatcher is responsible for setting - ``SCRIPT_NAME`` instead; see :doc:`/patterns/appdispatch` - for examples of dispatch configuration). - - Will be used for the session cookie path if ``SESSION_COOKIE_PATH`` is not - set. - - Default: ``'/'`` - -.. py:data:: PREFERRED_URL_SCHEME - - Use this scheme for generating external URLs when not in a request context. - - Default: ``'http'`` - -.. py:data:: MAX_CONTENT_LENGTH - - Don't read more than this many bytes from the incoming request data. If not - set and the request does not specify a ``CONTENT_LENGTH``, no data will be - read for security. - - Default: ``None`` - -.. py:data:: TEMPLATES_AUTO_RELOAD - - Reload templates when they are changed. If not set, it will be enabled in - debug mode. - - Default: ``None`` - -.. py:data:: EXPLAIN_TEMPLATE_LOADING - - Log debugging information tracing how a template file was loaded. This can - be useful to figure out why a template was not loaded or the wrong file - appears to be loaded. - - Default: ``False`` - -.. py:data:: MAX_COOKIE_SIZE - - Warn if cookie headers are larger than this many bytes. Defaults to - ``4093``. Larger cookies may be silently ignored by browsers. Set to - ``0`` to disable the warning. - -.. py:data:: PROVIDE_AUTOMATIC_OPTIONS - - Set to ``False`` to disable the automatic addition of OPTIONS - responses. This can be overridden per route by altering the - ``provide_automatic_options`` attribute. - -.. versionadded:: 0.4 - ``LOGGER_NAME`` - -.. versionadded:: 0.5 - ``SERVER_NAME`` - -.. versionadded:: 0.6 - ``MAX_CONTENT_LENGTH`` - -.. versionadded:: 0.7 - ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` - -.. versionadded:: 0.8 - ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, - ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, - ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, - ``SESSION_COOKIE_SECURE`` - -.. versionadded:: 0.9 - ``PREFERRED_URL_SCHEME`` - -.. versionadded:: 0.10 - ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` - -.. versionadded:: 0.11 - ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``, - ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` - -.. versionchanged:: 1.0 - ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See - :doc:`/logging` for information about configuration. - - Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment - variable. - - Added :data:`SESSION_COOKIE_SAMESITE` to control the session - cookie's ``SameSite`` option. - - Added :data:`MAX_COOKIE_SIZE` to control a warning from Werkzeug. - -.. versionchanged:: 2.2 - Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``. - -.. versionchanged:: 2.3 - ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and - ``JSONIFY_PRETTYPRINT_REGULAR`` were removed. The default ``app.json`` provider has - equivalent attributes instead. - -.. versionchanged:: 2.3 - ``ENV`` was removed. - -.. versionadded:: 3.10 - Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default - addition of autogenerated OPTIONS responses. - - -Configuring from Python Files ------------------------------ - -Configuration becomes more useful if you can store it in a separate file, ideally -located outside the actual application package. You can deploy your application, then -separately configure it for the specific deployment. - -A common pattern is this:: - - app = Flask(__name__) - app.config.from_object('yourapplication.default_settings') - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - -This first loads the configuration from the -`yourapplication.default_settings` module and then overrides the values -with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` -environment variable points to. This environment variable can be set -in the shell before starting the server: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x YOURAPPLICATION_SETTINGS /path/to/settings.cfg - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg - > flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:YOURAPPLICATION_SETTINGS = "\path\to\settings.cfg" - > flask run - * Running on http://127.0.0.1:5000/ - -The configuration files themselves are actual Python files. Only values -in uppercase are actually stored in the config object later on. So make -sure to use uppercase letters for your config keys. - -Here is an example of a configuration file:: - - # Example configuration - SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -Make sure to load the configuration very early on, so that extensions have -the ability to access the configuration when starting up. There are other -methods on the config object as well to load from individual files. For a -complete reference, read the :class:`~flask.Config` object's -documentation. - - -Configuring from Data Files ---------------------------- - -It is also possible to load configuration from a file in a format of -your choice using :meth:`~flask.Config.from_file`. For example to load -from a TOML file: - -.. code-block:: python - - import tomllib - app.config.from_file("config.toml", load=tomllib.load, text=False) - -Or from a JSON file: - -.. code-block:: python - - import json - app.config.from_file("config.json", load=json.load) - - -Configuring from Environment Variables --------------------------------------- - -In addition to pointing to configuration files using environment -variables, you may find it useful (or necessary) to control your -configuration values directly from the environment. Flask can be -instructed to load all environment variables starting with a specific -prefix into the config using :meth:`~flask.Config.from_prefixed_env`. - -Environment variables can be set in the shell before starting the -server: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" - $ export FLASK_MAIL_ENABLED=false - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_SECRET_KEY "5f352379324c22463451387a0aec5d2f" - $ set -x FLASK_MAIL_ENABLED false - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" - > set FLASK_MAIL_ENABLED=false - > flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_SECRET_KEY = "5f352379324c22463451387a0aec5d2f" - > $env:FLASK_MAIL_ENABLED = "false" - > flask run - * Running on http://127.0.0.1:5000/ - -The variables can then be loaded and accessed via the config with a key -equal to the environment variable name without the prefix i.e. - -.. code-block:: python - - app.config.from_prefixed_env() - app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f" - -The prefix is ``FLASK_`` by default. This is configurable via the -``prefix`` argument of :meth:`~flask.Config.from_prefixed_env`. - -Values will be parsed to attempt to convert them to a more specific type -than strings. By default :func:`json.loads` is used, so any valid JSON -value is possible, including lists and dicts. This is configurable via -the ``loads`` argument of :meth:`~flask.Config.from_prefixed_env`. - -When adding a boolean value with the default JSON parsing, only "true" -and "false", lowercase, are valid values. Keep in mind that any -non-empty string is considered ``True`` by Python. - -It is possible to set keys in nested dictionaries by separating the -keys with double underscore (``__``). Any intermediate keys that don't -exist on the parent dict will be initialized to an empty dict. - -.. code-block:: text - - $ export FLASK_MYAPI__credentials__username=user123 - -.. code-block:: python - - app.config["MYAPI"]["credentials"]["username"] # Is "user123" - -On Windows, environment variable keys are always uppercase, therefore -the above example would end up as ``MYAPI__CREDENTIALS__USERNAME``. - -For even more config loading features, including merging and -case-insensitive Windows support, try a dedicated library such as -Dynaconf_, which includes integration with Flask. - -.. _Dynaconf: https://www.dynaconf.com/ - - -Configuration Best Practices ----------------------------- - -The downside with the approach mentioned earlier is that it makes testing -a little harder. There is no single 100% solution for this problem in -general, but there are a couple of things you can keep in mind to improve -that experience: - -1. Create your application in a function and register blueprints on it. - That way you can create multiple instances of your application with - different configurations attached which makes unit testing a lot - easier. You can use this to pass in configuration as needed. - -2. Do not write code that needs the configuration at import time. If you - limit yourself to request-only accesses to the configuration you can - reconfigure the object later on as needed. - -3. Make sure to load the configuration very early on, so that - extensions can access the configuration when calling ``init_app``. - - -.. _config-dev-prod: - -Development / Production ------------------------- - -Most applications need more than one configuration. There should be at -least separate configurations for the production server and the one used -during development. The easiest way to handle this is to use a default -configuration that is always loaded and part of the version control, and a -separate configuration that overrides the values as necessary as mentioned -in the example above:: - - app = Flask(__name__) - app.config.from_object('yourapplication.default_settings') - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - -Then you just have to add a separate :file:`config.py` file and export -``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However -there are alternative ways as well. For example you could use imports or -subclassing. - -What is very popular in the Django world is to make the import explicit in -the config file by adding ``from yourapplication.default_settings -import *`` to the top of the file and then overriding the changes by hand. -You could also inspect an environment variable like -``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc -and import different hard-coded files based on that. - -An interesting pattern is also to use classes and inheritance for -configuration:: - - class Config(object): - TESTING = False - - class ProductionConfig(Config): - DATABASE_URI = 'mysql://user@localhost/foo' - - class DevelopmentConfig(Config): - DATABASE_URI = "sqlite:////tmp/foo.db" - - class TestingConfig(Config): - DATABASE_URI = 'sqlite:///:memory:' - TESTING = True - -To enable such a config you just have to call into -:meth:`~flask.Config.from_object`:: - - app.config.from_object('configmodule.ProductionConfig') - -Note that :meth:`~flask.Config.from_object` does not instantiate the class -object. If you need to instantiate the class, such as to access a property, -then you must do so before calling :meth:`~flask.Config.from_object`:: - - from configmodule import ProductionConfig - app.config.from_object(ProductionConfig()) - - # Alternatively, import via string: - from werkzeug.utils import import_string - cfg = import_string('configmodule.ProductionConfig')() - app.config.from_object(cfg) - -Instantiating the configuration object allows you to use ``@property`` in -your configuration classes:: - - class Config(object): - """Base config, uses staging database server.""" - TESTING = False - DB_SERVER = '192.168.1.56' - - @property - def DATABASE_URI(self): # Note: all caps - return f"mysql://user@{self.DB_SERVER}/foo" - - class ProductionConfig(Config): - """Uses production database server.""" - DB_SERVER = '192.168.19.32' - - class DevelopmentConfig(Config): - DB_SERVER = 'localhost' - - class TestingConfig(Config): - DB_SERVER = 'localhost' - DATABASE_URI = 'sqlite:///:memory:' - -There are many different ways and it's up to you how you want to manage -your configuration files. However here a list of good recommendations: - -- Keep a default configuration in version control. Either populate the - config with this default configuration or import it in your own - configuration files before overriding values. -- Use an environment variable to switch between the configurations. - This can be done from outside the Python interpreter and makes - development and deployment much easier because you can quickly and - easily switch between different configs without having to touch the - code at all. If you are working often on different projects you can - even create your own script for sourcing that activates a virtualenv - and exports the development configuration for you. -- Use a tool like `fabric`_ to push code and configuration separately - to the production server(s). - -.. _fabric: https://www.fabfile.org/ - - -.. _instance-folders: - -Instance Folders ----------------- - -.. versionadded:: 0.8 - -Flask 0.8 introduces instance folders. Flask for a long time made it -possible to refer to paths relative to the application's folder directly -(via :attr:`Flask.root_path`). This was also how many developers loaded -configurations stored next to the application. Unfortunately however this -only works well if applications are not packages in which case the root -path refers to the contents of the package. - -With Flask 0.8 a new attribute was introduced: -:attr:`Flask.instance_path`. It refers to a new concept called the -“instance folder”. The instance folder is designed to not be under -version control and be deployment specific. It's the perfect place to -drop things that either change at runtime or configuration files. - -You can either explicitly provide the path of the instance folder when -creating the Flask application or you can let Flask autodetect the -instance folder. For explicit configuration use the `instance_path` -parameter:: - - app = Flask(__name__, instance_path='/path/to/instance/folder') - -Please keep in mind that this path *must* be absolute when provided. - -If the `instance_path` parameter is not provided the following default -locations are used: - -- Uninstalled module:: - - /myapp.py - /instance - -- Uninstalled package:: - - /myapp - /__init__.py - /instance - -- Installed module or package:: - - $PREFIX/lib/pythonX.Y/site-packages/myapp - $PREFIX/var/myapp-instance - - ``$PREFIX`` is the prefix of your Python installation. This can be - ``/usr`` or the path to your virtualenv. You can print the value of - ``sys.prefix`` to see what the prefix is set to. - -Since the config object provided loading of configuration files from -relative filenames we made it possible to change the loading via filenames -to be relative to the instance path if wanted. The behavior of relative -paths in config files can be flipped between “relative to the application -root” (the default) to “relative to instance folder” via the -`instance_relative_config` switch to the application constructor:: - - app = Flask(__name__, instance_relative_config=True) - -Here is a full example of how to configure Flask to preload the config -from a module and then override the config from a file in the instance -folder if it exists:: - - app = Flask(__name__, instance_relative_config=True) - app.config.from_object('yourapplication.default_settings') - app.config.from_pyfile('application.cfg', silent=True) - -The path to the instance folder can be found via the -:attr:`Flask.instance_path`. Flask also provides a shortcut to open a -file from the instance folder with :meth:`Flask.open_instance_resource`. - -Example usage for both:: - - filename = os.path.join(app.instance_path, 'application.cfg') - with open(filename) as f: - config = f.read() - - # or via open_instance_resource: - with app.open_instance_resource('application.cfg') as f: - config = f.read() diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053e..00000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/debugging.rst b/docs/debugging.rst deleted file mode 100644 index f6b56cab..00000000 --- a/docs/debugging.rst +++ /dev/null @@ -1,99 +0,0 @@ -Debugging Application Errors -============================ - - -In Production -------------- - -**Do not run the development server, or enable the built-in debugger, in -a production environment.** The debugger allows executing arbitrary -Python code from the browser. It's protected by a pin, but that should -not be relied on for security. - -Use an error logging tool, such as Sentry, as described in -:ref:`error-logging-tools`, or enable logging and notifications as -described in :doc:`/logging`. - -If you have access to the server, you could add some code to start an -external debugger if ``request.remote_addr`` matches your IP. Some IDE -debuggers also have a remote mode so breakpoints on the server can be -interacted with locally. Only enable a debugger temporarily. - - -The Built-In Debugger ---------------------- - -The built-in Werkzeug development server provides a debugger which shows -an interactive traceback in the browser when an unhandled error occurs -during a request. This debugger should only be used during development. - -.. image:: _static/debugger.png - :align: center - :class: screenshot - :alt: screenshot of debugger in action - -.. warning:: - - The debugger allows executing arbitrary Python code from the - browser. It is protected by a pin, but still represents a major - security risk. Do not run the development server or debugger in a - production environment. - -The debugger is enabled by default when the development server is run in debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -When running from Python code, passing ``debug=True`` enables debug mode, which is -mostly equivalent. - -.. code-block:: python - - app.run(debug=True) - -:doc:`/server` and :doc:`/cli` have more information about running the debugger and -debug mode. More information about the debugger can be found in the `Werkzeug -documentation `__. - - -External Debuggers ------------------- - -External debuggers, such as those provided by IDEs, can offer a more -powerful debugging experience than the built-in debugger. They can also -be used to step through code during a request before an error is raised, -or if no error is raised. Some even have a remote mode so you can debug -code running on another machine. - -When using an external debugger, the app should still be in debug mode, otherwise Flask -turns unhandled errors into generic 500 error pages. However, the built-in debugger and -reloader should be disabled so they don't interfere with the external debugger. - -.. code-block:: text - - $ flask --app hello run --debug --no-debugger --no-reload - -When running from Python: - -.. code-block:: python - - app.run(debug=True, use_debugger=False, use_reloader=False) - -Disabling these isn't required, an external debugger will continue to work with the -following caveats. - -- If the built-in debugger is not disabled, it will catch unhandled exceptions before - the external debugger can. -- If the reloader is not disabled, it could cause an unexpected reload if code changes - during a breakpoint. -- The development server will still catch unhandled exceptions if the built-in - debugger is disabled, otherwise it would crash on any error. If you want that (and - usually you don't) pass ``passthrough_errors=True`` to ``app.run``. - - .. code-block:: python - - app.run( - debug=True, passthrough_errors=True, - use_debugger=False, use_reloader=False - ) diff --git a/docs/deploying/apache-httpd.rst b/docs/deploying/apache-httpd.rst deleted file mode 100644 index bdeaf626..00000000 --- a/docs/deploying/apache-httpd.rst +++ /dev/null @@ -1,66 +0,0 @@ -Apache httpd -============ - -`Apache httpd`_ is a fast, production level HTTP server. When serving -your application with one of the WSGI servers listed in :doc:`index`, it -is often good or necessary to put a dedicated HTTP server in front of -it. This "reverse proxy" can handle incoming requests, TLS, and other -security and performance concerns better than the WSGI server. - -httpd can be installed using your system package manager, or a pre-built -executable for Windows. Installing and running httpd itself is outside -the scope of this doc. This page outlines the basics of configuring -httpd to proxy your application. Be sure to read its documentation to -understand what features are available. - -.. _Apache httpd: https://httpd.apache.org/ - - -Domain Name ------------ - -Acquiring and configuring a domain name is outside the scope of this -doc. In general, you will buy a domain name from a registrar, pay for -server space with a hosting provider, and then point your registrar -at the hosting provider's name servers. - -To simulate this, you can also edit your ``hosts`` file, located at -``/etc/hosts`` on Linux. Add a line that associates a name with the -local IP. - -Modern Linux systems may be configured to treat any domain name that -ends with ``.localhost`` like this without adding it to the ``hosts`` -file. - -.. code-block:: python - :caption: ``/etc/hosts`` - - 127.0.0.1 hello.localhost - - -Configuration -------------- - -The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on -Linux. It may be different depending on your operating system. Check the -docs and look for ``httpd.conf``. - -Remove or comment out any existing ``DocumentRoot`` directive. Add the -config lines below. We'll assume the WSGI server is listening locally at -``http://127.0.0.1:8000``. - -.. code-block:: apache - :caption: ``/etc/httpd/conf/httpd.conf`` - - LoadModule proxy_module modules/mod_proxy.so - LoadModule proxy_http_module modules/mod_proxy_http.so - ProxyPass / http://127.0.0.1:8000/ - RequestHeader set X-Forwarded-Proto http - RequestHeader set X-Forwarded-Prefix / - -The ``LoadModule`` lines might already exist. If so, make sure they are -uncommented instead of adding them manually. - -Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded`` -headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically -set by ``ProxyPass``. diff --git a/docs/deploying/asgi.rst b/docs/deploying/asgi.rst deleted file mode 100644 index 1dc0aa24..00000000 --- a/docs/deploying/asgi.rst +++ /dev/null @@ -1,27 +0,0 @@ -ASGI -==== - -If you'd like to use an ASGI server you will need to utilise WSGI to -ASGI middleware. The asgiref -`WsgiToAsgi `_ -adapter is recommended as it integrates with the event loop used for -Flask's :ref:`async_await` support. You can use the adapter by -wrapping the Flask app, - -.. code-block:: python - - from asgiref.wsgi import WsgiToAsgi - from flask import Flask - - app = Flask(__name__) - - ... - - asgi_app = WsgiToAsgi(app) - -and then serving the ``asgi_app`` with the ASGI server, e.g. using -`Hypercorn `_, - -.. sourcecode:: text - - $ hypercorn module:asgi_app diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst deleted file mode 100644 index 8a718b22..00000000 --- a/docs/deploying/eventlet.rst +++ /dev/null @@ -1,80 +0,0 @@ -eventlet -======== - -Prefer using :doc:`gunicorn` with eventlet workers rather than using -`eventlet`_ directly. Gunicorn provides a much more configurable and -production-tested server. - -`eventlet`_ allows writing asynchronous, coroutine-based code that looks -like standard synchronous Python. It uses `greenlet`_ to enable task -switching without writing ``async/await`` or using ``asyncio``. - -:doc:`gevent` is another library that does the same thing. Certain -dependencies you have, or other considerations, may affect which of the -two you choose to use. - -eventlet provides a WSGI server that can handle many connections at once -instead of one per worker process. You must actually use eventlet in -your own code to see any benefit to using the server. - -.. _eventlet: https://eventlet.net/ -.. _greenlet: https://greenlet.readthedocs.io/en/latest/ - - -Installing ----------- - -When using eventlet, greenlet>=1.0 is required, otherwise context locals -such as ``request`` will not work as expected. When using PyPy, -PyPy>=7.3.7 is required. - -Create a virtualenv, install your application, then install -``eventlet``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install eventlet - - -Running -------- - -To use eventlet to serve your application, write a script that imports -its ``wsgi.server``, as well as your app or app factory. - -.. code-block:: python - :caption: ``wsgi.py`` - - import eventlet - from eventlet import wsgi - from hello import create_app - - app = create_app() - wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app) - -.. code-block:: text - - $ python wsgi.py - (x) wsgi starting up on http://127.0.0.1:8000 - - -Binding Externally ------------------- - -eventlet should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of eventlet. - -You can bind to all external IPs on a non-privileged port by using -``0.0.0.0`` in the server arguments shown in the previous section. -Don't do this when using a reverse proxy setup, otherwise it will be -possible to bypass the proxy. - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. diff --git a/docs/deploying/gevent.rst b/docs/deploying/gevent.rst deleted file mode 100644 index 448b93e7..00000000 --- a/docs/deploying/gevent.rst +++ /dev/null @@ -1,80 +0,0 @@ -gevent -====== - -Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather -than using `gevent`_ directly. Gunicorn and uWSGI provide much more -configurable and production-tested servers. - -`gevent`_ allows writing asynchronous, coroutine-based code that looks -like standard synchronous Python. It uses `greenlet`_ to enable task -switching without writing ``async/await`` or using ``asyncio``. - -:doc:`eventlet` is another library that does the same thing. Certain -dependencies you have, or other considerations, may affect which of the -two you choose to use. - -gevent provides a WSGI server that can handle many connections at once -instead of one per worker process. You must actually use gevent in your -own code to see any benefit to using the server. - -.. _gevent: https://www.gevent.org/ -.. _greenlet: https://greenlet.readthedocs.io/en/latest/ - - -Installing ----------- - -When using gevent, greenlet>=1.0 is required, otherwise context locals -such as ``request`` will not work as expected. When using PyPy, -PyPy>=7.3.7 is required. - -Create a virtualenv, install your application, then install ``gevent``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install gevent - - -Running -------- - -To use gevent to serve your application, write a script that imports its -``WSGIServer``, as well as your app or app factory. - -.. code-block:: python - :caption: ``wsgi.py`` - - from gevent.pywsgi import WSGIServer - from hello import create_app - - app = create_app() - http_server = WSGIServer(("127.0.0.1", 8000), app) - http_server.serve_forever() - -.. code-block:: text - - $ python wsgi.py - -No output is shown when the server starts. - - -Binding Externally ------------------- - -gevent should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of gevent. - -You can bind to all external IPs on a non-privileged port by using -``0.0.0.0`` in the server arguments shown in the previous section. Don't -do this when using a reverse proxy setup, otherwise it will be possible -to bypass the proxy. - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. diff --git a/docs/deploying/gunicorn.rst b/docs/deploying/gunicorn.rst deleted file mode 100644 index c50edc23..00000000 --- a/docs/deploying/gunicorn.rst +++ /dev/null @@ -1,130 +0,0 @@ -Gunicorn -======== - -`Gunicorn`_ is a pure Python WSGI server with simple configuration and -multiple worker implementations for performance tuning. - -* It tends to integrate easily with hosting platforms. -* It does not support Windows (but does run on WSL). -* It is easy to install as it does not require additional dependencies - or compilation. -* It has built-in async worker support using gevent or eventlet. - -This page outlines the basics of running Gunicorn. Be sure to read its -`documentation`_ and use ``gunicorn --help`` to understand what features -are available. - -.. _Gunicorn: https://gunicorn.org/ -.. _documentation: https://docs.gunicorn.org/ - - -Installing ----------- - -Gunicorn is easy to install, as it does not require external -dependencies or compilation. It runs on Windows only under WSL. - -Create a virtualenv, install your application, then install -``gunicorn``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install gunicorn - - -Running -------- - -The only required argument to Gunicorn tells it how to load your Flask -application. The syntax is ``{module_import}:{app_variable}``. -``module_import`` is the dotted import name to the module with your -application. ``app_variable`` is the variable with the application. It -can also be a function call (with any arguments) if you're using the -app factory pattern. - -.. code-block:: text - - # equivalent to 'from hello import app' - $ gunicorn -w 4 'hello:app' - - # equivalent to 'from hello import create_app; create_app()' - $ gunicorn -w 4 'hello:create_app()' - - Starting gunicorn 20.1.0 - Listening at: http://127.0.0.1:8000 (x) - Using worker: sync - Booting worker with pid: x - Booting worker with pid: x - Booting worker with pid: x - Booting worker with pid: x - -The ``-w`` option specifies the number of processes to run; a starting -value could be ``CPU * 2``. The default is only 1 worker, which is -probably not what you want for the default worker type. - -Logs for each request aren't shown by default, only worker info and -errors are shown. To show access logs on stdout, use the -``--access-logfile=-`` option. - - -Binding Externally ------------------- - -Gunicorn should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of Gunicorn. - -You can bind to all external IPs on a non-privileged port using the -``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup, -otherwise it will be possible to bypass the proxy. - -.. code-block:: text - - $ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()' - Listening at: http://0.0.0.0:8000 (x) - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. - - -Async with gevent or eventlet ------------------------------ - -The default sync worker is appropriate for many use cases. If you need -asynchronous support, Gunicorn provides workers using either `gevent`_ -or `eventlet`_. This is not the same as Python's ``async/await``, or the -ASGI server spec. You must actually use gevent/eventlet in your own code -to see any benefit to using the workers. - -When using either gevent or eventlet, greenlet>=1.0 is required, -otherwise context locals such as ``request`` will not work as expected. -When using PyPy, PyPy>=7.3.7 is required. - -To use gevent: - -.. code-block:: text - - $ gunicorn -k gevent 'hello:create_app()' - Starting gunicorn 20.1.0 - Listening at: http://127.0.0.1:8000 (x) - Using worker: gevent - Booting worker with pid: x - -To use eventlet: - -.. code-block:: text - - $ gunicorn -k eventlet 'hello:create_app()' - Starting gunicorn 20.1.0 - Listening at: http://127.0.0.1:8000 (x) - Using worker: eventlet - Booting worker with pid: x - -.. _gevent: https://www.gevent.org/ -.. _eventlet: https://eventlet.net/ diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst deleted file mode 100644 index 4135596a..00000000 --- a/docs/deploying/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -Deploying to Production -======================= - -After developing your application, you'll want to make it available -publicly to other users. When you're developing locally, you're probably -using the built-in development server, debugger, and reloader. These -should not be used in production. Instead, you should use a dedicated -WSGI server or hosting platform, some of which will be described here. - -"Production" means "not development", which applies whether you're -serving your application publicly to millions of users or privately / -locally to a single user. **Do not use the development server when -deploying to production. It is intended for use only during local -development. It is not designed to be particularly secure, stable, or -efficient.** - -Self-Hosted Options -------------------- - -Flask is a WSGI *application*. A WSGI *server* is used to run the -application, converting incoming HTTP requests to the standard WSGI -environ, and converting outgoing WSGI responses to HTTP responses. - -The primary goal of these docs is to familiarize you with the concepts -involved in running a WSGI application using a production WSGI server -and HTTP server. There are many WSGI servers and HTTP servers, with many -configuration possibilities. The pages below discuss the most common -servers, and show the basics of running each one. The next section -discusses platforms that can manage this for you. - -.. toctree:: - :maxdepth: 1 - - gunicorn - waitress - mod_wsgi - uwsgi - gevent - eventlet - asgi - -WSGI servers have HTTP servers built-in. However, a dedicated HTTP -server may be safer, more efficient, or more capable. Putting an HTTP -server in front of the WSGI server is called a "reverse proxy." - -.. toctree:: - :maxdepth: 1 - - proxy_fix - nginx - apache-httpd - -This list is not exhaustive, and you should evaluate these and other -servers based on your application's needs. Different servers will have -different capabilities, configuration, and support. - - -Hosting Platforms ------------------ - -There are many services available for hosting web applications without -needing to maintain your own server, networking, domain, etc. Some -services may have a free tier up to a certain time or bandwidth. Many of -these services use one of the WSGI servers described above, or a similar -interface. The links below are for some of the most common platforms, -which have instructions for Flask, WSGI, or Python. - -- `PythonAnywhere `_ -- `Google App Engine `_ -- `Google Cloud Run `_ -- `AWS Elastic Beanstalk `_ -- `Microsoft Azure `_ - -This list is not exhaustive, and you should evaluate these and other -services based on your application's needs. Different services will have -different capabilities, configuration, pricing, and support. - -You'll probably need to :doc:`proxy_fix` when using most hosting -platforms. diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst deleted file mode 100644 index 23e82279..00000000 --- a/docs/deploying/mod_wsgi.rst +++ /dev/null @@ -1,94 +0,0 @@ -mod_wsgi -======== - -`mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server. -The modern `mod_wsgi-express`_ command makes it easy to configure and -start the server without needing to write Apache httpd configuration. - -* Tightly integrated with Apache httpd. -* Supports Windows directly. -* Requires a compiler and the Apache development headers to install. -* Does not require a reverse proxy setup. - -This page outlines the basics of running mod_wsgi-express, not the more -complex installation and configuration with httpd. Be sure to read the -`mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to -understand what features are available. - -.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ -.. _mod_wsgi: https://modwsgi.readthedocs.io/ -.. _Apache httpd: https://httpd.apache.org/ - - -Installing ----------- - -Installing mod_wsgi requires a compiler and the Apache server and -development headers installed. You will get an error if they are not. -How to install them depends on the OS and package manager that you use. - -Create a virtualenv, install your application, then install -``mod_wsgi``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install mod_wsgi - - -Running -------- - -The only argument to ``mod_wsgi-express`` specifies a script containing -your Flask application, which must be called ``application``. You can -write a small script to import your app with this name, or to create it -if using the app factory pattern. - -.. code-block:: python - :caption: ``wsgi.py`` - - from hello import app - - application = app - -.. code-block:: python - :caption: ``wsgi.py`` - - from hello import create_app - - application = create_app() - -Now run the ``mod_wsgi-express start-server`` command. - -.. code-block:: text - - $ mod_wsgi-express start-server wsgi.py --processes 4 - -The ``--processes`` option specifies the number of worker processes to -run; a starting value could be ``CPU * 2``. - -Logs for each request aren't show in the terminal. If an error occurs, -its information is written to the error log file shown when starting the -server. - - -Binding Externally ------------------- - -Unlike the other WSGI servers in these docs, mod_wsgi can be run as -root to bind to privileged ports like 80 and 443. However, it must be -configured to drop permissions to a different user and group for the -worker processes. - -For example, if you created a ``hello`` user and group, you should -install your virtualenv and application as that user, then tell -mod_wsgi to drop to that user after starting. - -.. code-block:: text - - $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ - /home/hello/wsgi.py \ - --user hello --group hello --port 80 --processes 4 diff --git a/docs/deploying/nginx.rst b/docs/deploying/nginx.rst deleted file mode 100644 index 6b25c073..00000000 --- a/docs/deploying/nginx.rst +++ /dev/null @@ -1,69 +0,0 @@ -nginx -===== - -`nginx`_ is a fast, production level HTTP server. When serving your -application with one of the WSGI servers listed in :doc:`index`, it is -often good or necessary to put a dedicated HTTP server in front of it. -This "reverse proxy" can handle incoming requests, TLS, and other -security and performance concerns better than the WSGI server. - -Nginx can be installed using your system package manager, or a pre-built -executable for Windows. Installing and running Nginx itself is outside -the scope of this doc. This page outlines the basics of configuring -Nginx to proxy your application. Be sure to read its documentation to -understand what features are available. - -.. _nginx: https://nginx.org/ - - -Domain Name ------------ - -Acquiring and configuring a domain name is outside the scope of this -doc. In general, you will buy a domain name from a registrar, pay for -server space with a hosting provider, and then point your registrar -at the hosting provider's name servers. - -To simulate this, you can also edit your ``hosts`` file, located at -``/etc/hosts`` on Linux. Add a line that associates a name with the -local IP. - -Modern Linux systems may be configured to treat any domain name that -ends with ``.localhost`` like this without adding it to the ``hosts`` -file. - -.. code-block:: python - :caption: ``/etc/hosts`` - - 127.0.0.1 hello.localhost - - -Configuration -------------- - -The nginx configuration is located at ``/etc/nginx/nginx.conf`` on -Linux. It may be different depending on your operating system. Check the -docs and look for ``nginx.conf``. - -Remove or comment out any existing ``server`` section. Add a ``server`` -section and use the ``proxy_pass`` directive to point to the address the -WSGI server is listening on. We'll assume the WSGI server is listening -locally at ``http://127.0.0.1:8000``. - -.. code-block:: nginx - :caption: ``/etc/nginx.conf`` - - server { - listen 80; - server_name _; - - location / { - proxy_pass http://127.0.0.1:8000/; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Prefix /; - } - } - -Then :doc:`proxy_fix` so that your application uses these headers. diff --git a/docs/deploying/proxy_fix.rst b/docs/deploying/proxy_fix.rst deleted file mode 100644 index e2c42e82..00000000 --- a/docs/deploying/proxy_fix.rst +++ /dev/null @@ -1,33 +0,0 @@ -Tell Flask it is Behind a Proxy -=============================== - -When using a reverse proxy, or many Python hosting platforms, the proxy -will intercept and forward all external requests to the local WSGI -server. - -From the WSGI server and Flask application's perspectives, requests are -now coming from the HTTP server to the local address, rather than from -the remote address to the external server address. - -HTTP servers should set ``X-Forwarded-`` headers to pass on the real -values to the application. The application can then be told to trust and -use those values by wrapping it with the -:doc:`werkzeug:middleware/proxy_fix` middleware provided by Werkzeug. - -This middleware should only be used if the application is actually -behind a proxy, and should be configured with the number of proxies that -are chained in front of it. Not all proxies set all the headers. Since -incoming headers can be faked, you must set how many proxies are setting -each header so the middleware knows what to trust. - -.. code-block:: python - - from werkzeug.middleware.proxy_fix import ProxyFix - - app.wsgi_app = ProxyFix( - app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 - ) - -Remember, only apply this middleware if you are behind a proxy, and set -the correct number of proxies that set each header. It can be a security -issue if you get this configuration wrong. diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst deleted file mode 100644 index 1f9d5eca..00000000 --- a/docs/deploying/uwsgi.rst +++ /dev/null @@ -1,145 +0,0 @@ -uWSGI -===== - -`uWSGI`_ is a fast, compiled server suite with extensive configuration -and capabilities beyond a basic server. - -* It can be very performant due to being a compiled program. -* It is complex to configure beyond the basic application, and has so - many options that it can be difficult for beginners to understand. -* It does not support Windows (but does run on WSL). -* It requires a compiler to install in some cases. - -This page outlines the basics of running uWSGI. Be sure to read its -documentation to understand what features are available. - -.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ - - -Installing ----------- - -uWSGI has multiple ways to install it. The most straightforward is to -install the ``pyuwsgi`` package, which provides precompiled wheels for -common platforms. However, it does not provide SSL support, which can be -provided with a reverse proxy instead. - -Create a virtualenv, install your application, then install ``pyuwsgi``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install pyuwsgi - -If you have a compiler available, you can install the ``uwsgi`` package -instead. Or install the ``pyuwsgi`` package from sdist instead of wheel. -Either method will include SSL support. - -.. code-block:: text - - $ pip install uwsgi - - # or - $ pip install --no-binary pyuwsgi pyuwsgi - - -Running -------- - -The most basic way to run uWSGI is to tell it to start an HTTP server -and import your application. - -.. code-block:: text - - $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app - - *** Starting uWSGI 2.0.20 (64bit) on [x] *** - *** Operational MODE: preforking *** - mounting hello:app on / - spawned uWSGI master process (pid: x) - spawned uWSGI worker 1 (pid: x, cores: 1) - spawned uWSGI worker 2 (pid: x, cores: 1) - spawned uWSGI worker 3 (pid: x, cores: 1) - spawned uWSGI worker 4 (pid: x, cores: 1) - spawned uWSGI http 1 (pid: x) - -If you're using the app factory pattern, you'll need to create a small -Python file to create the app, then point uWSGI at that. - -.. code-block:: python - :caption: ``wsgi.py`` - - from hello import create_app - - app = create_app() - -.. code-block:: text - - $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app - -The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The -``--master`` option specifies the standard worker manager. The ``-p`` -option starts 4 worker processes; a starting value could be ``CPU * 2``. -The ``-w`` option tells uWSGI how to import your application - - -Binding Externally ------------------- - -uWSGI should not be run as root with the configuration shown in this doc -because it would cause your application code to run as root, which is -not secure. However, this means it will not be possible to bind to port -80 or 443. Instead, a reverse proxy such as :doc:`nginx` or -:doc:`apache-httpd` should be used in front of uWSGI. It is possible to -run uWSGI as root securely, but that is beyond the scope of this doc. - -uWSGI has optimized integration with `Nginx uWSGI`_ and -`Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using -a standard HTTP proxy. That configuration is beyond the scope of this -doc, see the links for more information. - -.. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html -.. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi - -You can bind to all external IPs on a non-privileged port using the -``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy -setup, otherwise it will be possible to bypass the proxy. - -.. code-block:: text - - $ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. - - -Async with gevent ------------------ - -The default sync worker is appropriate for many use cases. If you need -asynchronous support, uWSGI provides a `gevent`_ worker. This is not the -same as Python's ``async/await``, or the ASGI server spec. You must -actually use gevent in your own code to see any benefit to using the -worker. - -When using gevent, greenlet>=1.0 is required, otherwise context locals -such as ``request`` will not work as expected. When using PyPy, -PyPy>=7.3.7 is required. - -.. code-block:: text - - $ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app - - *** Starting uWSGI 2.0.20 (64bit) on [x] *** - *** Operational MODE: async *** - mounting hello:app on / - spawned uWSGI master process (pid: x) - spawned uWSGI worker 1 (pid: x, cores: 100) - spawned uWSGI http 1 (pid: x) - *** running gevent loop engine [addr:x] *** - - -.. _gevent: https://www.gevent.org/ diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst deleted file mode 100644 index 7bdd695b..00000000 --- a/docs/deploying/waitress.rst +++ /dev/null @@ -1,75 +0,0 @@ -Waitress -======== - -`Waitress`_ is a pure Python WSGI server. - -* It is easy to configure. -* It supports Windows directly. -* It is easy to install as it does not require additional dependencies - or compilation. -* It does not support streaming requests, full request data is always - buffered. -* It uses a single process with multiple thread workers. - -This page outlines the basics of running Waitress. Be sure to read its -documentation and ``waitress-serve --help`` to understand what features -are available. - -.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ - - -Installing ----------- - -Create a virtualenv, install your application, then install -``waitress``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install waitress - - -Running -------- - -The only required argument to ``waitress-serve`` tells it how to load -your Flask application. The syntax is ``{module}:{app}``. ``module`` is -the dotted import name to the module with your application. ``app`` is -the variable with the application. If you're using the app factory -pattern, use ``--call {module}:{factory}`` instead. - -.. code-block:: text - - # equivalent to 'from hello import app' - $ waitress-serve --host 127.0.0.1 hello:app - - # equivalent to 'from hello import create_app; create_app()' - $ waitress-serve --host 127.0.0.1 --call hello:create_app - - Serving on http://127.0.0.1:8080 - -The ``--host`` option binds the server to local ``127.0.0.1`` only. - -Logs for each request aren't shown, only errors are shown. Logging can -be configured through the Python interface instead of the command line. - - -Binding Externally ------------------- - -Waitress should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of Waitress. - -You can bind to all external IPs on a non-privileged port by not -specifying the ``--host`` option. Don't do this when using a reverse -proxy setup, otherwise it will be possible to bypass the proxy. - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. diff --git a/docs/design.rst b/docs/design.rst deleted file mode 100644 index 066cf107..00000000 --- a/docs/design.rst +++ /dev/null @@ -1,228 +0,0 @@ -Design Decisions in Flask -========================= - -If you are curious why Flask does certain things the way it does and not -differently, this section is for you. This should give you an idea about -some of the design decisions that may appear arbitrary and surprising at -first, especially in direct comparison with other frameworks. - - -The Explicit Application Object -------------------------------- - -A Python web application based on WSGI has to have one central callable -object that implements the actual application. In Flask this is an -instance of the :class:`~flask.Flask` class. Each Flask application has -to create an instance of this class itself and pass it the name of the -module, but why can't Flask do that itself? - -Without such an explicit application object the following code:: - - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def index(): - return 'Hello World!' - -Would look like this instead:: - - from hypothetical_flask import route - - @route('/') - def index(): - return 'Hello World!' - -There are three major reasons for this. The most important one is that -implicit application objects require that there may only be one instance at -the time. There are ways to fake multiple applications with a single -application object, like maintaining a stack of applications, but this -causes some problems I won't outline here in detail. Now the question is: -when does a microframework need more than one application at the same -time? A good example for this is unit testing. When you want to test -something it can be very helpful to create a minimal application to test -specific behavior. When the application object is deleted everything it -allocated will be freed again. - -Another thing that becomes possible when you have an explicit object lying -around in your code is that you can subclass the base class -(:class:`~flask.Flask`) to alter specific behavior. This would not be -possible without hacks if the object were created ahead of time for you -based on a class that is not exposed to you. - -But there is another very important reason why Flask depends on an -explicit instantiation of that class: the package name. Whenever you -create a Flask instance you usually pass it `__name__` as package name. -Flask depends on that information to properly load resources relative -to your module. With Python's outstanding support for reflection it can -then access the package to figure out where the templates and static files -are stored (see :meth:`~flask.Flask.open_resource`). Now obviously there -are frameworks around that do not need any configuration and will still be -able to load templates relative to your application module. But they have -to use the current working directory for that, which is a very unreliable -way to determine where the application is. The current working directory -is process-wide and if you are running multiple applications in one -process (which could happen in a webserver without you knowing) the paths -will be off. Worse: many webservers do not set the working directory to -the directory of your application but to the document root which does not -have to be the same folder. - -The third reason is "explicit is better than implicit". That object is -your WSGI application, you don't have to remember anything else. If you -want to apply a WSGI middleware, just wrap it and you're done (though -there are better ways to do that so that you do not lose the reference -to the application object :meth:`~flask.Flask.wsgi_app`). - -Furthermore this design makes it possible to use a factory function to -create the application which is very helpful for unit testing and similar -things (:doc:`/patterns/appfactories`). - -The Routing System ------------------- - -Flask uses the Werkzeug routing system which was designed to -automatically order routes by complexity. This means that you can declare -routes in arbitrary order and they will still work as expected. This is a -requirement if you want to properly implement decorator based routing -since decorators could be fired in undefined order when the application is -split into multiple modules. - -Another design decision with the Werkzeug routing system is that routes -in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far -with that in that it will automatically redirect to a canonical URL if a route -is ambiguous. - - -One Template Engine -------------------- - -Flask decides on one template engine: Jinja2. Why doesn't Flask have a -pluggable template engine interface? You can obviously use a different -template engine, but Flask will still configure Jinja2 for you. While -that limitation that Jinja2 is *always* configured will probably go away, -the decision to bundle one template engine and use that will not. - -Template engines are like programming languages and each of those engines -has a certain understanding about how things work. On the surface they -all work the same: you tell the engine to evaluate a template with a set -of variables and take the return value as string. - -But that's about where similarities end. Jinja2 for example has an -extensive filter system, a certain way to do template inheritance, -support for reusable blocks (macros) that can be used from inside -templates and also from Python code, supports iterative template -rendering, configurable syntax and more. On the other hand an engine -like Genshi is based on XML stream evaluation, template inheritance by -taking the availability of XPath into account and more. Mako on the -other hand treats templates similar to Python modules. - -When it comes to connecting a template engine with an application or -framework there is more than just rendering templates. For instance, -Flask uses Jinja2's extensive autoescaping support. Also it provides -ways to access macros from Jinja2 templates. - -A template abstraction layer that would not take the unique features of -the template engines away is a science on its own and a too large -undertaking for a microframework like Flask. - -Furthermore extensions can then easily depend on one template language -being present. You can easily use your own templating language, but an -extension could still depend on Jinja itself. - - -What does "micro" mean? ------------------------ - -“Micro” does not mean that your whole web application has to fit into a single -Python file (although it certainly can), nor does it mean that Flask is lacking -in functionality. The "micro" in microframework means Flask aims to keep the -core simple but extensible. Flask won't make many decisions for you, such as -what database to use. Those decisions that it does make, such as what -templating engine to use, are easy to change. Everything else is up to you, so -that Flask can be everything you need and nothing you don't. - -By default, Flask does not include a database abstraction layer, form -validation or anything else where different libraries already exist that can -handle that. Instead, Flask supports extensions to add such functionality to -your application as if it was implemented in Flask itself. Numerous extensions -provide database integration, form validation, upload handling, various open -authentication technologies, and more. Flask may be "micro", but it's ready for -production use on a variety of needs. - -Why does Flask call itself a microframework and yet it depends on two -libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look -over to the Ruby side of web development there we have a protocol very -similar to WSGI. Just that it's called Rack there, but besides that it -looks very much like a WSGI rendition for Ruby. But nearly all -applications in Ruby land do not work with Rack directly, but on top of a -library with the same name. This Rack library has two equivalents in -Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but -from my understanding it's sort of deprecated in favour of WebOb. The -development of WebOb and Werkzeug started side by side with similar ideas -in mind: be a good implementation of WSGI for other applications to take -advantage. - -Flask is a framework that takes advantage of the work already done by -Werkzeug to properly interface WSGI (which can be a complex task at -times). Thanks to recent developments in the Python package -infrastructure, packages with dependencies are no longer an issue and -there are very few reasons against having libraries that depend on others. - - -Thread Locals -------------- - -Flask uses thread local objects (context local objects in fact, they -support greenlet contexts as well) for request, session and an extra -object you can put your own things on (:data:`~flask.g`). Why is that and -isn't that a bad idea? - -Yes it is usually not such a bright idea to use thread locals. They cause -troubles for servers that are not based on the concept of threads and make -large applications harder to maintain. However Flask is just not designed -for large applications or asynchronous servers. Flask wants to make it -quick and easy to write a traditional web application. - - -Async/await and ASGI support ----------------------------- - -Flask supports ``async`` coroutines for view functions by executing the -coroutine on a separate thread instead of using an event loop on the -main thread as an async-first (ASGI) framework would. This is necessary -for Flask to remain backwards compatible with extensions and code built -before ``async`` was introduced into Python. This compromise introduces -a performance cost compared with the ASGI frameworks, due to the -overhead of the threads. - -Due to how tied to WSGI Flask's code is, it's not clear if it's possible -to make the ``Flask`` class support ASGI and WSGI at the same time. Work -is currently being done in Werkzeug to work with ASGI, which may -eventually enable support in Flask as well. - -See :doc:`/async-await` for more discussion. - - -What Flask is, What Flask is Not --------------------------------- - -Flask will never have a database layer. It will not have a form library -or anything else in that direction. Flask itself just bridges to Werkzeug -to implement a proper WSGI application and to Jinja2 to handle templating. -It also binds to a few common standard library packages such as logging. -Everything else is up for extensions. - -Why is this the case? Because people have different preferences and -requirements and Flask could not meet those if it would force any of this -into the core. The majority of web applications will need a template -engine in some sort. However not every application needs a SQL database. - -As your codebase grows, you are free to make the design decisions appropriate -for your project. Flask will continue to provide a very simple glue layer to -the best that Python has to offer. You can implement advanced patterns in -SQLAlchemy or another database tool, introduce non-relational data persistence -as appropriate, and take advantage of framework-agnostic tools built for WSGI, -the Python web interface. - -The idea of Flask is to build a good foundation for all applications. -Everything else is up to you or extensions. diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst deleted file mode 100644 index faca58c2..00000000 --- a/docs/errorhandling.rst +++ /dev/null @@ -1,523 +0,0 @@ -Handling Application Errors -=========================== - -Applications fail, servers fail. Sooner or later you will see an exception -in production. Even if your code is 100% correct, you will still see -exceptions from time to time. Why? Because everything else involved will -fail. Here are some situations where perfectly fine code can lead to server -errors: - -- the client terminated the request early and the application was still - reading from the incoming data -- the database server was overloaded and could not handle the query -- a filesystem is full -- a harddrive crashed -- a backend server overloaded -- a programming error in a library you are using -- network connection of the server to another system failed - -And that's just a small sample of issues you could be facing. So how do we -deal with that sort of problem? By default if your application runs in -production mode, and an exception is raised Flask will display a very simple -page for you and log the exception to the :attr:`~flask.Flask.logger`. - -But there is more you can do, and we will cover some better setups to deal -with errors including custom exceptions and 3rd party tools. - - -.. _error-logging-tools: - -Error Logging Tools -------------------- - -Sending error mails, even if just for critical ones, can become -overwhelming if enough users are hitting the error and log files are -typically never looked at. This is why we recommend using `Sentry -`_ for dealing with application errors. It's -available as a source-available project `on GitHub -`_ and is also available as a `hosted version -`_ which you can try for free. Sentry -aggregates duplicate errors, captures the full stack trace and local -variables for debugging, and sends you mails based on new errors or -frequency thresholds. - -To use Sentry you need to install the ``sentry-sdk`` client with extra -``flask`` dependencies. - -.. code-block:: text - - $ pip install sentry-sdk[flask] - -And then add this to your Flask app: - -.. code-block:: python - - import sentry_sdk - from sentry_sdk.integrations.flask import FlaskIntegration - - sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()]) - -The ``YOUR_DSN_HERE`` value needs to be replaced with the DSN value you -get from your Sentry installation. - -After installation, failures leading to an Internal Server Error -are automatically reported to Sentry and from there you can -receive error notifications. - -See also: - -- Sentry also supports catching errors from a worker queue - (RQ, Celery, etc.) in a similar fashion. See the `Python SDK docs - `__ for more information. -- `Flask-specific documentation `__ - - -Error Handlers --------------- - -When an error occurs in Flask, an appropriate `HTTP status code -`__ will be -returned. 400-499 indicate errors with the client's request data, or -about the data requested. 500-599 indicate errors with the server or -application itself. - -You might want to show custom error pages to the user when an error occurs. -This can be done by registering error handlers. - -An error handler is a function that returns a response when a type of error is -raised, similar to how a view is a function that returns a response when a -request URL is matched. It is passed the instance of the error being handled, -which is most likely a :exc:`~werkzeug.exceptions.HTTPException`. - -The status code of the response will not be set to the handler's code. Make -sure to provide the appropriate HTTP status code when returning a response from -a handler. - - -Registering -``````````` - -Register handlers by decorating a function with -:meth:`~flask.Flask.errorhandler`. Or use -:meth:`~flask.Flask.register_error_handler` to register the function later. -Remember to set the error code when returning the response. - -.. code-block:: python - - @app.errorhandler(werkzeug.exceptions.BadRequest) - def handle_bad_request(e): - return 'bad request!', 400 - - # or, without the decorator - app.register_error_handler(400, handle_bad_request) - -:exc:`werkzeug.exceptions.HTTPException` subclasses like -:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable -when registering handlers. (``BadRequest.code == 400``) - -Non-standard HTTP codes cannot be registered by code because they are not known -by Werkzeug. Instead, define a subclass of -:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and -register and raise that exception class. - -.. code-block:: python - - class InsufficientStorage(werkzeug.exceptions.HTTPException): - code = 507 - description = 'Not enough storage space.' - - app.register_error_handler(InsufficientStorage, handle_507) - - raise InsufficientStorage() - -Handlers can be registered for any exception class, not just -:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status -codes. Handlers can be registered for a specific class, or for all subclasses -of a parent class. - - -Handling -```````` - -When building a Flask application you *will* run into exceptions. If some part -of your code breaks while handling a request (and you have no error handlers -registered), a "500 Internal Server Error" -(:exc:`~werkzeug.exceptions.InternalServerError`) will be returned by default. -Similarly, "404 Not Found" -(:exc:`~werkzeug.exceptions.NotFound`) error will occur if a request is sent to an unregistered route. -If a route receives an unallowed request method, a "405 Method Not Allowed" -(:exc:`~werkzeug.exceptions.MethodNotAllowed`) will be raised. These are all -subclasses of :class:`~werkzeug.exceptions.HTTPException` and are provided by -default in Flask. - -Flask gives you the ability to raise any HTTP exception registered by -Werkzeug. However, the default HTTP exceptions return simple exception -pages. You might want to show custom error pages to the user when an error occurs. -This can be done by registering error handlers. - -When Flask catches an exception while handling a request, it is first looked up by code. -If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen. -If no handler is registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a -generic message about their code, while other exceptions are converted to a -generic "500 Internal Server Error". - -For example, if an instance of :exc:`ConnectionRefusedError` is raised, -and a handler is registered for :exc:`ConnectionError` and -:exc:`ConnectionRefusedError`, the more specific :exc:`ConnectionRefusedError` -handler is called with the exception instance to generate the response. - -Handlers registered on the blueprint take precedence over those registered -globally on the application, assuming a blueprint is handling the request that -raises the exception. However, the blueprint cannot handle 404 routing errors -because the 404 occurs at the routing level before the blueprint can be -determined. - - -Generic Exception Handlers -`````````````````````````` - -It is possible to register error handlers for very generic base classes -such as ``HTTPException`` or even ``Exception``. However, be aware that -these will catch more than you might expect. - -For example, an error handler for ``HTTPException`` might be useful for turning -the default HTML errors pages into JSON. However, this -handler will trigger for things you don't cause directly, such as 404 -and 405 errors during routing. Be sure to craft your handler carefully -so you don't lose information about the HTTP error. - -.. code-block:: python - - from flask import json - from werkzeug.exceptions import HTTPException - - @app.errorhandler(HTTPException) - def handle_exception(e): - """Return JSON instead of HTML for HTTP errors.""" - # start with the correct headers and status code from the error - response = e.get_response() - # replace the body with JSON - response.data = json.dumps({ - "code": e.code, - "name": e.name, - "description": e.description, - }) - response.content_type = "application/json" - return response - -An error handler for ``Exception`` might seem useful for changing how -all errors, even unhandled ones, are presented to the user. However, -this is similar to doing ``except Exception:`` in Python, it will -capture *all* otherwise unhandled errors, including all HTTP status -codes. - -In most cases it will be safer to register handlers for more -specific exceptions. Since ``HTTPException`` instances are valid WSGI -responses, you could also pass them through directly. - -.. code-block:: python - - from werkzeug.exceptions import HTTPException - - @app.errorhandler(Exception) - def handle_exception(e): - # pass through HTTP errors - if isinstance(e, HTTPException): - return e - - # now you're handling non-HTTP exceptions only - return render_template("500_generic.html", e=e), 500 - -Error handlers still respect the exception class hierarchy. If you -register handlers for both ``HTTPException`` and ``Exception``, the -``Exception`` handler will not handle ``HTTPException`` subclasses -because the ``HTTPException`` handler is more specific. - - -Unhandled Exceptions -```````````````````` - -When there is no error handler registered for an exception, a 500 -Internal Server Error will be returned instead. See -:meth:`flask.Flask.handle_exception` for information about this -behavior. - -If there is an error handler registered for ``InternalServerError``, -this will be invoked. As of Flask 1.1.0, this error handler will always -be passed an instance of ``InternalServerError``, not the original -unhandled error. - -The original error is available as ``e.original_exception``. - -An error handler for "500 Internal Server Error" will be passed uncaught -exceptions in addition to explicit 500 errors. In debug mode, a handler -for "500 Internal Server Error" will not be used. Instead, the -interactive debugger will be shown. - - -Custom Error Pages ------------------- - -Sometimes when building a Flask application, you might want to raise a -:exc:`~werkzeug.exceptions.HTTPException` to signal to the user that -something is wrong with the request. Fortunately, Flask comes with a handy -:func:`~flask.abort` function that aborts a request with a HTTP error from -werkzeug as desired. It will also provide a plain black and white error page -for you with a basic description, but nothing fancy. - -Depending on the error code it is less or more likely for the user to -actually see such an error. - -Consider the code below, we might have a user profile route, and if the user -fails to pass a username we can raise a "400 Bad Request". If the user passes a -username and we can't find it, we raise a "404 Not Found". - -.. code-block:: python - - from flask import abort, render_template, request - - # a username needs to be supplied in the query args - # a successful request would be like /profile?username=jack - @app.route("/profile") - def user_profile(): - username = request.arg.get("username") - # if a username isn't supplied in the request, return a 400 bad request - if username is None: - abort(400) - - user = get_user(username=username) - # if a user can't be found by their username, return 404 not found - if user is None: - abort(404) - - return render_template("profile.html", user=user) - -Here is another example implementation for a "404 Page Not Found" exception: - -.. code-block:: python - - from flask import render_template - - @app.errorhandler(404) - def page_not_found(e): - # note that we set the 404 status explicitly - return render_template('404.html'), 404 - -When using :doc:`/patterns/appfactories`: - -.. code-block:: python - - from flask import Flask, render_template - - def page_not_found(e): - return render_template('404.html'), 404 - - def create_app(config_filename): - app = Flask(__name__) - app.register_error_handler(404, page_not_found) - return app - -An example template might be this: - -.. code-block:: html+jinja - - {% extends "layout.html" %} - {% block title %}Page Not Found{% endblock %} - {% block body %} -

Page Not Found

-

What you were looking for is just not there. -

go somewhere nice - {% endblock %} - - -Further Examples -```````````````` - -The above examples wouldn't actually be an improvement on the default -exception pages. We can create a custom 500.html template like this: - -.. code-block:: html+jinja - - {% extends "layout.html" %} - {% block title %}Internal Server Error{% endblock %} - {% block body %} -

Internal Server Error

-

Oops... we seem to have made a mistake, sorry!

-

Go somewhere nice instead - {% endblock %} - -It can be implemented by rendering the template on "500 Internal Server Error": - -.. code-block:: python - - from flask import render_template - - @app.errorhandler(500) - def internal_server_error(e): - # note that we set the 500 status explicitly - return render_template('500.html'), 500 - -When using :doc:`/patterns/appfactories`: - -.. code-block:: python - - from flask import Flask, render_template - - def internal_server_error(e): - return render_template('500.html'), 500 - - def create_app(): - app = Flask(__name__) - app.register_error_handler(500, internal_server_error) - return app - -When using :doc:`/blueprints`: - -.. code-block:: python - - from flask import Blueprint - - blog = Blueprint('blog', __name__) - - # as a decorator - @blog.errorhandler(500) - def internal_server_error(e): - return render_template('500.html'), 500 - - # or with register_error_handler - blog.register_error_handler(500, internal_server_error) - - -Blueprint Error Handlers ------------------------- - -In :doc:`/blueprints`, most error handlers will work as expected. -However, there is a caveat concerning handlers for 404 and 405 -exceptions. These error handlers are only invoked from an appropriate -``raise`` statement or a call to ``abort`` in another of the blueprint's -view functions; they are not invoked by, e.g., an invalid URL access. - -This is because the blueprint does not "own" a certain URL space, so -the application instance has no way of knowing which blueprint error -handler it should run if given an invalid URL. If you would like to -execute different handling strategies for these errors based on URL -prefixes, they may be defined at the application level using the -``request`` proxy object. - -.. code-block:: python - - from flask import jsonify, render_template - - # at the application level - # not the blueprint level - @app.errorhandler(404) - def page_not_found(e): - # if a request is in our blog URL space - if request.path.startswith('/blog/'): - # we return a custom blog 404 page - return render_template("blog/404.html"), 404 - else: - # otherwise we return our generic site-wide 404 page - return render_template("404.html"), 404 - - @app.errorhandler(405) - def method_not_allowed(e): - # if a request has the wrong method to our API - if request.path.startswith('/api/'): - # we return a json saying so - return jsonify(message="Method Not Allowed"), 405 - else: - # otherwise we return a generic site-wide 405 page - return render_template("405.html"), 405 - - -Returning API Errors as JSON ----------------------------- - -When building APIs in Flask, some developers realise that the built-in -exceptions are not expressive enough for APIs and that the content type of -:mimetype:`text/html` they are emitting is not very useful for API consumers. - -Using the same techniques as above and :func:`~flask.json.jsonify` we can return JSON -responses to API errors. :func:`~flask.abort` is called -with a ``description`` parameter. The error handler will -use that as the JSON error message, and set the status code to 404. - -.. code-block:: python - - from flask import abort, jsonify - - @app.errorhandler(404) - def resource_not_found(e): - return jsonify(error=str(e)), 404 - - @app.route("/cheese") - def get_one_cheese(): - resource = get_resource() - - if resource is None: - abort(404, description="Resource not found") - - return jsonify(resource) - -We can also create custom exception classes. For instance, we can -introduce a new custom exception for an API that can take a proper human readable message, -a status code for the error and some optional payload to give more context -for the error. - -This is a simple example: - -.. code-block:: python - - from flask import jsonify, request - - class InvalidAPIUsage(Exception): - status_code = 400 - - def __init__(self, message, status_code=None, payload=None): - super().__init__() - self.message = message - if status_code is not None: - self.status_code = status_code - self.payload = payload - - def to_dict(self): - rv = dict(self.payload or ()) - rv['message'] = self.message - return rv - - @app.errorhandler(InvalidAPIUsage) - def invalid_api_usage(e): - return jsonify(e.to_dict()), e.status_code - - # an API app route for getting user information - # a correct request might be /api/user?user_id=420 - @app.route("/api/user") - def user_api(user_id): - user_id = request.arg.get("user_id") - if not user_id: - raise InvalidAPIUsage("No user id provided!") - - user = get_user(user_id=user_id) - if not user: - raise InvalidAPIUsage("No such user!", status_code=404) - - return jsonify(user.to_dict()) - -A view can now raise that exception with an error message. Additionally -some extra payload can be provided as a dictionary through the `payload` -parameter. - - -Logging -------- - -See :doc:`/logging` for information about how to log exceptions, such as -by emailing them to admins. - - -Debugging ---------- - -See :doc:`/debugging` for information about how to debug errors in -development and production. diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst deleted file mode 100644 index c9dee5ff..00000000 --- a/docs/extensiondev.rst +++ /dev/null @@ -1,303 +0,0 @@ -Flask Extension Development -=========================== - -.. currentmodule:: flask - -Extensions are extra packages that add functionality to a Flask -application. While `PyPI`_ contains many Flask extensions, you may not -find one that fits your need. If this is the case, you can create your -own, and publish it for others to use as well. - -This guide will show how to create a Flask extension, and some of the -common patterns and requirements involved. Since extensions can do -anything, this guide won't be able to cover every possibility. - -The best ways to learn about extensions are to look at how other -extensions you use are written, and discuss with others. Discuss your -design ideas with others on our `Discord Chat`_ or -`GitHub Discussions`_. - -The best extensions share common patterns, so that anyone familiar with -using one extension won't feel completely lost with another. This can -only work if collaboration happens early. - - -Naming ------- - -A Flask extension typically has ``flask`` in its name as a prefix or -suffix. If it wraps another library, it should include the library name -as well. This makes it easy to search for extensions, and makes their -purpose clearer. - -A general Python packaging recommendation is that the install name from -the package index and the name used in ``import`` statements should be -related. The import name is lowercase, with words separated by -underscores (``_``). The install name is either lower case or title -case, with words separated by dashes (``-``). If it wraps another -library, prefer using the same case as that library's name. - -Here are some example install and import names: - -- ``Flask-Name`` imported as ``flask_name`` -- ``flask-name-lower`` imported as ``flask_name_lower`` -- ``Flask-ComboName`` imported as ``flask_comboname`` -- ``Name-Flask`` imported as ``name_flask`` - - -The Extension Class and Initialization --------------------------------------- - -All extensions will need some entry point that initializes the -extension with the application. The most common pattern is to create a -class that represents the extension's configuration and behavior, with -an ``init_app`` method to apply the extension instance to the given -application instance. - -.. code-block:: python - - class HelloExtension: - def __init__(self, app=None): - if app is not None: - self.init_app(app) - - def init_app(self, app): - app.before_request(...) - -It is important that the app is not stored on the extension, don't do -``self.app = app``. The only time the extension should have direct -access to an app is during ``init_app``, otherwise it should use -:data:`current_app`. - -This allows the extension to support the application factory pattern, -avoids circular import issues when importing the extension instance -elsewhere in a user's code, and makes testing with different -configurations easier. - -.. code-block:: python - - hello = HelloExtension() - - def create_app(): - app = Flask(__name__) - hello.init_app(app) - return app - -Above, the ``hello`` extension instance exists independently of the -application. This means that other modules in a user's project can do -``from project import hello`` and use the extension in blueprints before -the app exists. - -The :attr:`Flask.extensions` dict can be used to store a reference to -the extension on the application, or some other state specific to the -application. Be aware that this is a single namespace, so use a name -unique to your extension, such as the extension's name without the -"flask" prefix. - - -Adding Behavior ---------------- - -There are many ways that an extension can add behavior. Any setup -methods that are available on the :class:`Flask` object can be used -during an extension's ``init_app`` method. - -A common pattern is to use :meth:`~Flask.before_request` to initialize -some data or a connection at the beginning of each request, then -:meth:`~Flask.teardown_request` to clean it up at the end. This can be -stored on :data:`g`, discussed more below. - -A more lazy approach is to provide a method that initializes and caches -the data or connection. For example, a ``ext.get_db`` method could -create a database connection the first time it's called, so that a view -that doesn't use the database doesn't create a connection. - -Besides doing something before and after every view, your extension -might want to add some specific views as well. In this case, you could -define a :class:`Blueprint`, then call :meth:`~Flask.register_blueprint` -during ``init_app`` to add the blueprint to the app. - - -Configuration Techniques ------------------------- - -There can be multiple levels and sources of configuration for an -extension. You should consider what parts of your extension fall into -each one. - -- Configuration per application instance, through ``app.config`` - values. This is configuration that could reasonably change for each - deployment of an application. A common example is a URL to an - external resource, such as a database. Configuration keys should - start with the extension's name so that they don't interfere with - other extensions. -- Configuration per extension instance, through ``__init__`` - arguments. This configuration usually affects how the extension - is used, such that it wouldn't make sense to change it per - deployment. -- Configuration per extension instance, through instance attributes - and decorator methods. It might be more ergonomic to assign to - ``ext.value``, or use a ``@ext.register`` decorator to register a - function, after the extension instance has been created. -- Global configuration through class attributes. Changing a class - attribute like ``Ext.connection_class`` can customize default - behavior without making a subclass. This could be combined - per-extension configuration to override defaults. -- Subclassing and overriding methods and attributes. Making the API of - the extension itself something that can be overridden provides a - very powerful tool for advanced customization. - -The :class:`~flask.Flask` object itself uses all of these techniques. - -It's up to you to decide what configuration is appropriate for your -extension, based on what you need and what you want to support. - -Configuration should not be changed after the application setup phase is -complete and the server begins handling requests. Configuration is -global, any changes to it are not guaranteed to be visible to other -workers. - - -Data During a Request ---------------------- - -When writing a Flask application, the :data:`~flask.g` object is used to -store information during a request. For example the -:doc:`tutorial ` stores a connection to a SQLite -database as ``g.db``. Extensions can also use this, with some care. -Since ``g`` is a single global namespace, extensions must use unique -names that won't collide with user data. For example, use the extension -name as a prefix, or as a namespace. - -.. code-block:: python - - # an internal prefix with the extension name - g._hello_user_id = 2 - - # or an internal prefix as a namespace - from types import SimpleNamespace - g._hello = SimpleNamespace() - g._hello.user_id = 2 - -The data in ``g`` lasts for an application context. An application -context is active when a request context is, or when a CLI command is -run. If you're storing something that should be closed, use -:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed -when the application context ends. If it should only be valid during a -request, or would not be used in the CLI outside a request, use -:meth:`~flask.Flask.teardown_request`. - - -Views and Models ----------------- - -Your extension views might want to interact with specific models in your -database, or some other extension or data connected to your application. -For example, let's consider a ``Flask-SimpleBlog`` extension that works -with Flask-SQLAlchemy to provide a ``Post`` model and views to write -and read posts. - -The ``Post`` model needs to subclass the Flask-SQLAlchemy ``db.Model`` -object, but that's only available once you've created an instance of -that extension, not when your extension is defining its views. So how -can the view code, defined before the model exists, access the model? - -One method could be to use :doc:`views`. During ``__init__``, create -the model, then create the views by passing the model to the view -class's :meth:`~views.View.as_view` method. - -.. code-block:: python - - class PostAPI(MethodView): - def __init__(self, model): - self.model = model - - def get(self, id): - post = self.model.query.get(id) - return jsonify(post.to_json()) - - class BlogExtension: - def __init__(self, db): - class Post(db.Model): - id = db.Column(primary_key=True) - title = db.Column(db.String, nullable=False) - - self.post_model = Post - - def init_app(self, app): - api_view = PostAPI.as_view(model=self.post_model) - - db = SQLAlchemy() - blog = BlogExtension(db) - db.init_app(app) - blog.init_app(app) - -Another technique could be to use an attribute on the extension, such as -``self.post_model`` from above. Add the extension to ``app.extensions`` -in ``init_app``, then access -``current_app.extensions["simple_blog"].post_model`` from views. - -You may also want to provide base classes so that users can provide -their own ``Post`` model that conforms to the API your extension -expects. So they could implement ``class Post(blog.BasePost)``, then -set it as ``blog.post_model``. - -As you can see, this can get a bit complex. Unfortunately, there's no -perfect solution here, only different strategies and tradeoffs depending -on your needs and how much customization you want to offer. Luckily, -this sort of resource dependency is not a common need for most -extensions. Remember, if you need help with design, ask on our -`Discord Chat`_ or `GitHub Discussions`_. - - -Recommended Extension Guidelines --------------------------------- - -Flask previously had the concept of "approved extensions", where the -Flask maintainers evaluated the quality, support, and compatibility of -the extensions before listing them. While the list became too difficult -to maintain over time, the guidelines are still relevant to all -extensions maintained and developed today, as they help the Flask -ecosystem remain consistent and compatible. - -1. An extension requires a maintainer. In the event an extension author - would like to move beyond the project, the project should find a new - maintainer and transfer access to the repository, documentation, - PyPI, and any other services. The `Pallets-Eco`_ organization on - GitHub allows for community maintenance with oversight from the - Pallets maintainers. -2. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. - It must provide exactly one package or module named - ``flask_extension_name``. -3. The extension must use an open source license. The Python web - ecosystem tends to prefer BSD or MIT. It must be open source and - publicly available. -4. The extension's API must have the following characteristics: - - - It must support multiple applications running in the same Python - process. Use ``current_app`` instead of ``self.app``, store - configuration and state per application instance. - - It must be possible to use the factory pattern for creating - applications. Use the ``ext.init_app()`` pattern. - -5. From a clone of the repository, an extension with its dependencies - must be installable in editable mode with ``pip install -e .``. -6. It must ship tests that can be invoked with a common tool like - ``tox -e py``, ``nox -s test`` or ``pytest``. If not using ``tox``, - the test dependencies should be specified in a requirements file. - The tests must be part of the sdist distribution. -7. A link to the documentation or project website must be in the PyPI - metadata or the readme. The documentation should use the Flask theme - from the `Official Pallets Themes`_. -8. The extension's dependencies should not use upper bounds or assume - any particular version scheme, but should use lower bounds to - indicate minimum compatibility support. For example, - ``sqlalchemy>=1.4``. -9. Indicate the versions of Python supported using ``python_requires=">=version"``. - Flask itself supports Python >=3.8 as of April 2023, but this will update over time. - -.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask -.. _Discord Chat: https://discord.gg/pallets -.. _GitHub Discussions: https://github.com/pallets/flask/discussions -.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ -.. _Pallets-Eco: https://github.com/pallets-eco diff --git a/docs/extensions.rst b/docs/extensions.rst deleted file mode 100644 index 4713ec8e..00000000 --- a/docs/extensions.rst +++ /dev/null @@ -1,48 +0,0 @@ -Extensions -========== - -Extensions are extra packages that add functionality to a Flask -application. For example, an extension might add support for sending -email or connecting to a database. Some extensions add entire new -frameworks to help build certain types of applications, like a REST API. - - -Finding Extensions ------------------- - -Flask extensions are usually named "Flask-Foo" or "Foo-Flask". You can -search PyPI for packages tagged with `Framework :: Flask `_. - - -Using Extensions ----------------- - -Consult each extension's documentation for installation, configuration, -and usage instructions. Generally, extensions pull their own -configuration from :attr:`app.config ` and are -passed an application instance during initialization. For example, -an extension called "Flask-Foo" might be used like this:: - - from flask_foo import Foo - - foo = Foo() - - app = Flask(__name__) - app.config.update( - FOO_BAR='baz', - FOO_SPAM='eggs', - ) - - foo.init_app(app) - - -Building Extensions -------------------- - -While `PyPI `_ contains many Flask extensions, you may not find -an extension that fits your need. If this is the case, you can create -your own, and publish it for others to use as well. Read -:doc:`extensiondev` to develop your own Flask extension. - - -.. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index f9ab9bd9..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,88 +0,0 @@ -.. rst-class:: hide-header - -Welcome to Flask -================ - -.. image:: _static/flask-horizontal.png - :align: center - -Welcome to Flask's documentation. Flask is a lightweight WSGI web application framework. -It is designed to make getting started quick and easy, with the ability to scale up to -complex applications. - -Get started with :doc:`installation` -and then get an overview with the :doc:`quickstart`. There is also a -more detailed :doc:`tutorial/index` that shows how to create a small but -complete application with Flask. Common patterns are described in the -:doc:`patterns/index` section. The rest of the docs describe each -component of Flask in detail, with a full reference in the :doc:`api` -section. - -Flask depends on the `Werkzeug`_ WSGI toolkit, the `Jinja`_ template engine, and the -`Click`_ CLI toolkit. Be sure to check their documentation as well as Flask's when -looking for information. - -.. _Werkzeug: https://werkzeug.palletsprojects.com -.. _Jinja: https://jinja.palletsprojects.com -.. _Click: https://click.palletsprojects.com - - -User's Guide ------------- - -Flask provides configuration and conventions, with sensible defaults, to get started. -This section of the documentation explains the different parts of the Flask framework -and how they can be used, customized, and extended. Beyond Flask itself, look for -community-maintained extensions to add even more functionality. - -.. toctree:: - :maxdepth: 2 - - installation - quickstart - tutorial/index - templating - testing - errorhandling - debugging - logging - config - signals - views - lifecycle - appcontext - reqcontext - blueprints - extensions - cli - server - shell - patterns/index - web-security - deploying/index - async-await - - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you. - -.. toctree:: - :maxdepth: 2 - - api - - -Additional Notes ----------------- - -.. toctree:: - :maxdepth: 2 - - design - extensiondev - contributing - license - changes diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index aeb00ce1..00000000 --- a/docs/installation.rst +++ /dev/null @@ -1,144 +0,0 @@ -Installation -============ - - -Python Version --------------- - -We recommend using the latest version of Python. Flask supports Python 3.8 and newer. - - -Dependencies ------------- - -These distributions will be installed automatically when installing Flask. - -* `Werkzeug`_ implements WSGI, the standard Python interface between - applications and servers. -* `Jinja`_ is a template language that renders the pages your application - serves. -* `MarkupSafe`_ comes with Jinja. It escapes untrusted input when rendering - templates to avoid injection attacks. -* `ItsDangerous`_ securely signs data to ensure its integrity. This is used - to protect Flask's session cookie. -* `Click`_ is a framework for writing command line applications. It provides - the ``flask`` command and allows adding custom management commands. -* `Blinker`_ provides support for :doc:`signals`. - -.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ -.. _Jinja: https://palletsprojects.com/p/jinja/ -.. _MarkupSafe: https://palletsprojects.com/p/markupsafe/ -.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ -.. _Click: https://palletsprojects.com/p/click/ -.. _Blinker: https://blinker.readthedocs.io/ - - -Optional dependencies -~~~~~~~~~~~~~~~~~~~~~ - -These distributions will not be installed automatically. Flask will detect and -use them if you install them. - -* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` - commands. -* `Watchdog`_ provides a faster, more efficient reloader for the development - server. - -.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme -.. _watchdog: https://pythonhosted.org/watchdog/ - - -greenlet -~~~~~~~~ - -You may choose to use gevent or eventlet with your application. In this -case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is -required. - -These are not minimum supported versions, they only indicate the first -versions that added necessary features. You should use the latest -versions of each. - - -Virtual environments --------------------- - -Use a virtual environment to manage the dependencies for your project, both in -development and in production. - -What problem does a virtual environment solve? The more Python projects you -have, the more likely it is that you need to work with different versions of -Python libraries, or even Python itself. Newer versions of libraries for one -project can break compatibility in another project. - -Virtual environments are independent groups of Python libraries, one for each -project. Packages installed for one project will not affect other projects or -the operating system's packages. - -Python comes bundled with the :mod:`venv` module to create virtual -environments. - - -.. _install-create-env: - -Create an environment -~~~~~~~~~~~~~~~~~~~~~ - -Create a project folder and a :file:`.venv` folder within: - -.. tabs:: - - .. group-tab:: macOS/Linux - - .. code-block:: text - - $ mkdir myproject - $ cd myproject - $ python3 -m venv .venv - - .. group-tab:: Windows - - .. code-block:: text - - > mkdir myproject - > cd myproject - > py -3 -m venv .venv - - -.. _install-activate-env: - -Activate the environment -~~~~~~~~~~~~~~~~~~~~~~~~ - -Before you work on your project, activate the corresponding environment: - -.. tabs:: - - .. group-tab:: macOS/Linux - - .. code-block:: text - - $ . .venv/bin/activate - - .. group-tab:: Windows - - .. code-block:: text - - > .venv\Scripts\activate - -Your shell prompt will change to show the name of the activated -environment. - - -Install Flask -------------- - -Within the activated environment, use the following command to install -Flask: - -.. code-block:: sh - - $ pip install Flask - -Flask is now installed. Check out the :doc:`/quickstart` or go to the -:doc:`Documentation Overview `. diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index 2a445f9c..00000000 --- a/docs/license.rst +++ /dev/null @@ -1,5 +0,0 @@ -BSD-3-Clause License -==================== - -.. literalinclude:: ../LICENSE.txt - :language: text diff --git a/docs/lifecycle.rst b/docs/lifecycle.rst deleted file mode 100644 index 2344d98a..00000000 --- a/docs/lifecycle.rst +++ /dev/null @@ -1,168 +0,0 @@ -Application Structure and Lifecycle -=================================== - -Flask makes it pretty easy to write a web application. But there are quite a few -different parts to an application and to each request it handles. Knowing what happens -during application setup, serving, and handling requests will help you know what's -possible in Flask and how to structure your application. - - -Application Setup ------------------ - -The first step in creating a Flask application is creating the application object. Each -Flask application is an instance of the :class:`.Flask` class, which collects all -configuration, extensions, and views. - -.. code-block:: python - - from flask import Flask - - app = Flask(__name__) - app.config.from_mapping( - SECRET_KEY="dev", - ) - app.config.from_prefixed_env() - - @app.route("/") - def index(): - return "Hello, World!" - -This is known as the "application setup phase", it's the code you write that's outside -any view functions or other handlers. It can be split up between different modules and -sub-packages, but all code that you want to be part of your application must be imported -in order for it to be registered. - -All application setup must be completed before you start serving your application and -handling requests. This is because WSGI servers divide work between multiple workers, or -can be distributed across multiple machines. If the configuration changed in one worker, -there's no way for Flask to ensure consistency between other workers. - -Flask tries to help developers catch some of these setup ordering issues by showing an -error if setup-related methods are called after requests are handled. In that case -you'll see this error: - - The setup method 'route' can no longer be called on the application. It has already - handled its first request, any changes will not be applied consistently. - Make sure all imports, decorators, functions, etc. needed to set up the application - are done before running it. - -However, it is not possible for Flask to detect all cases of out-of-order setup. In -general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects -from within view functions that run during requests. This includes: - -- Adding routes, view functions, and other request handlers with ``@app.route``, - ``@app.errorhandler``, ``@app.before_request``, etc. -- Registering blueprints. -- Loading configuration with ``app.config``. -- Setting up the Jinja template environment with ``app.jinja_env``. -- Setting a session interface, instead of the default itsdangerous cookie. -- Setting a JSON provider with ``app.json``, instead of the default provider. -- Creating and initializing Flask extensions. - - -Serving the Application ------------------------ - -Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During -development, Flask, through Werkzeug, provides a development WSGI server with the -``flask run`` CLI command. When you are done with development, use a production server -to serve your application, see :doc:`deploying/index`. - -Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The -WSGI server will be told how to access your Flask application object, which is the WSGI -application. Then it will start listening for HTTP requests, translate the request data -into a WSGI environ, and call the WSGI application with that data. The WSGI application -will return data that is translated into an HTTP response. - -#. Browser or other client makes HTTP request. -#. WSGI server receives request. -#. WSGI server converts HTTP data to WSGI ``environ`` dict. -#. WSGI server calls WSGI application with the ``environ``. -#. Flask, the WSGI application, does all its internal processing to route the request - to a view function, handle errors, etc. -#. Flask translates View function return into WSGI response data, passes it to WSGI - server. -#. WSGI server creates and send an HTTP response. -#. Client receives the HTTP response. - - -Middleware -~~~~~~~~~~ - -The WSGI application above is a callable that behaves in a certain way. Middleware -is a WSGI application that wraps another WSGI application. It's a similar concept to -Python decorators. The outermost middleware will be called by the server. It can modify -the data passed to it, then call the WSGI application (or further middleware) that it -wraps, and so on. And it can take the return value of that call and modify it further. - -From the WSGI server's perspective, there is one WSGI application, the one it calls -directly. Typically, Flask is the "real" application at the end of the chain of -middleware. But even Flask can call further WSGI applications, although that's an -advanced, uncommon use case. - -A common middleware you'll see used with Flask is Werkzeug's -:class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look -like it came directly from a client even if it passed through HTTP proxies on the way. -There are other middleware that can handle serving static files, authentication, etc. - - -How a Request is Handled ------------------------- - -For us, the interesting part of the steps above is when Flask gets called by the WSGI -server (or middleware). At that point, it will do quite a lot to handle the request and -generate the response. At the most basic, it will match the URL to a view function, call -the view function, and pass the return value back to the server. But there are many more -parts that you can use to customize its behavior. - -#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. -#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` - dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. -#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and - :data:`.g` available. -#. The :data:`.appcontext_pushed` signal is sent. -#. The :doc:`request context ` is pushed, which makes :attr:`.request` and - :class:`.session` available. -#. The session is opened, loading any existing session data using the app's - :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. -#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` - decorator during application setup. If there is no match, the error - usually a 404, - 405, or redirect - is stored to be handled later. -#. The :data:`.request_started` signal is sent. -#. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. -#. Any :meth:`~.Flask.before_request` decorated functions are called. If any of - these function returns a value it is treated as the response immediately. -#. If the URL didn't match a route a few steps ago, that error is raised now. -#. The :meth:`~.Flask.route` decorated view function associated with the matched URL - is called and returns a value to be used as the response. -#. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` - decorated function that matches the exception class or HTTP error code, it is - called to handle the error and return a response. -#. Whatever returned a response value - a before request function, the view, or an - error handler, that value is converted to a :class:`.Response` object. -#. Any :func:`~.after_this_request` decorated functions are called, then cleared. -#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify - the response object. -#. The session is saved, persisting any modified session data using the app's - :attr:`~.Flask.session_interface`. -#. The :data:`.request_finished` signal is sent. -#. If any step so far raised an exception, and it was not handled by an error handler - function, it is handled now. HTTP exceptions are treated as responses with their - corresponding status code, other exceptions are converted to a generic 500 response. - The :data:`.got_request_exception` signal is sent. -#. The response object's status, headers, and body are returned to the WSGI server. -#. Any :meth:`~.Flask.teardown_request` decorated functions are called. -#. The :data:`.request_tearing_down` signal is sent. -#. The request context is popped, :attr:`.request` and :class:`.session` are no longer - available. -#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. -#. The :data:`.appcontext_tearing_down` signal is sent. -#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer - available. -#. The :data:`.appcontext_popped` signal is sent. - -There are even more decorators and customization points than this, but that aren't part -of every request lifecycle. They're more specific to certain things you might use during -a request, such as templates, building URLs, or handling JSON data. See the rest of this -documentation, as well as the :doc:`api` to explore further. diff --git a/docs/logging.rst b/docs/logging.rst deleted file mode 100644 index 39588242..00000000 --- a/docs/logging.rst +++ /dev/null @@ -1,183 +0,0 @@ -Logging -======= - -Flask uses standard Python :mod:`logging`. Messages about your Flask -application are logged with :meth:`app.logger `, -which takes the same name as :attr:`app.name `. This -logger can also be used to log your own messages. - -.. code-block:: python - - @app.route('/login', methods=['POST']) - def login(): - user = get_user(request.form['username']) - - if user.check_password(request.form['password']): - login_user(user) - app.logger.info('%s logged in successfully', user.username) - return redirect(url_for('index')) - else: - app.logger.info('%s failed to log in', user.username) - abort(401) - -If you don't configure logging, Python's default log level is usually -'warning'. Nothing below the configured level will be visible. - - -Basic Configuration -------------------- - -When you want to configure logging for your project, you should do it as soon -as possible when the program starts. If :meth:`app.logger ` -is accessed before logging is configured, it will add a default handler. If -possible, configure logging before creating the application object. - -This example uses :func:`~logging.config.dictConfig` to create a logging -configuration similar to Flask's default, except for all logs:: - - from logging.config import dictConfig - - dictConfig({ - 'version': 1, - 'formatters': {'default': { - 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', - }}, - 'handlers': {'wsgi': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://flask.logging.wsgi_errors_stream', - 'formatter': 'default' - }}, - 'root': { - 'level': 'INFO', - 'handlers': ['wsgi'] - } - }) - - app = Flask(__name__) - - -Default Configuration -````````````````````` - -If you do not configure logging yourself, Flask will add a -:class:`~logging.StreamHandler` to :meth:`app.logger ` -automatically. During requests, it will write to the stream specified by the -WSGI server in ``environ['wsgi.errors']`` (which is usually -:data:`sys.stderr`). Outside a request, it will log to :data:`sys.stderr`. - - -Removing the Default Handler -```````````````````````````` - -If you configured logging after accessing -:meth:`app.logger `, and need to remove the default -handler, you can import and remove it:: - - from flask.logging import default_handler - - app.logger.removeHandler(default_handler) - - -Email Errors to Admins ----------------------- - -When running the application on a remote server for production, you probably -won't be looking at the log messages very often. The WSGI server will probably -send log messages to a file, and you'll only check that file if a user tells -you something went wrong. - -To be proactive about discovering and fixing bugs, you can configure a -:class:`logging.handlers.SMTPHandler` to send an email when errors and higher -are logged. :: - - import logging - from logging.handlers import SMTPHandler - - mail_handler = SMTPHandler( - mailhost='127.0.0.1', - fromaddr='server-error@example.com', - toaddrs=['admin@example.com'], - subject='Application Error' - ) - mail_handler.setLevel(logging.ERROR) - mail_handler.setFormatter(logging.Formatter( - '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' - )) - - if not app.debug: - app.logger.addHandler(mail_handler) - -This requires that you have an SMTP server set up on the same server. See the -Python docs for more information about configuring the handler. - - -Injecting Request Information ------------------------------ - -Seeing more information about the request, such as the IP address, may help -debugging some errors. You can subclass :class:`logging.Formatter` to inject -your own fields that can be used in messages. You can change the formatter for -Flask's default handler, the mail handler defined above, or any other -handler. :: - - from flask import has_request_context, request - from flask.logging import default_handler - - class RequestFormatter(logging.Formatter): - def format(self, record): - if has_request_context(): - record.url = request.url - record.remote_addr = request.remote_addr - else: - record.url = None - record.remote_addr = None - - return super().format(record) - - formatter = RequestFormatter( - '[%(asctime)s] %(remote_addr)s requested %(url)s\n' - '%(levelname)s in %(module)s: %(message)s' - ) - default_handler.setFormatter(formatter) - mail_handler.setFormatter(formatter) - - -Other Libraries ---------------- - -Other libraries may use logging extensively, and you want to see relevant -messages from those logs too. The simplest way to do this is to add handlers -to the root logger instead of only the app logger. :: - - from flask.logging import default_handler - - root = logging.getLogger() - root.addHandler(default_handler) - root.addHandler(mail_handler) - -Depending on your project, it may be more useful to configure each logger you -care about separately, instead of configuring only the root logger. :: - - for logger in ( - logging.getLogger(app.name), - logging.getLogger('sqlalchemy'), - logging.getLogger('other_package'), - ): - logger.addHandler(default_handler) - logger.addHandler(mail_handler) - - -Werkzeug -```````` - -Werkzeug logs basic request/response information to the ``'werkzeug'`` logger. -If the root logger has no handlers configured, Werkzeug adds a -:class:`~logging.StreamHandler` to its logger. - - -Flask Extensions -```````````````` - -Depending on the situation, an extension may choose to log to -:meth:`app.logger ` or its own named logger. Consult each -extension's documentation for details. diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 922152e9..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst deleted file mode 100644 index f22c8060..00000000 --- a/docs/patterns/appdispatch.rst +++ /dev/null @@ -1,189 +0,0 @@ -Application Dispatching -======================= - -Application dispatching is the process of combining multiple Flask -applications on the WSGI level. You can combine not only Flask -applications but any WSGI application. This would allow you to run a -Django and a Flask application in the same interpreter side by side if -you want. The usefulness of this depends on how the applications work -internally. - -The fundamental difference from :doc:`packages` is that in this case you -are running the same or different Flask applications that are entirely -isolated from each other. They run different configurations and are -dispatched on the WSGI level. - - -Working with this Document --------------------------- - -Each of the techniques and examples below results in an ``application`` -object that can be run with any WSGI server. For development, use the -``flask run`` command to start a development server. For production, see -:doc:`/deploying/index`. - -.. code-block:: python - - from flask import Flask - - app = Flask(__name__) - - @app.route('/') - def hello_world(): - return 'Hello World!' - - -Combining Applications ----------------------- - -If you have entirely separated applications and you want them to work next -to each other in the same Python interpreter process you can take -advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea -here is that each Flask application is a valid WSGI application and they -are combined by the dispatcher middleware into a larger one that is -dispatched based on prefix. - -For example you could have your main application run on ``/`` and your -backend interface on ``/backend``. - -.. code-block:: python - - from werkzeug.middleware.dispatcher import DispatcherMiddleware - from frontend_app import application as frontend - from backend_app import application as backend - - application = DispatcherMiddleware(frontend, { - '/backend': backend - }) - - -Dispatch by Subdomain ---------------------- - -Sometimes you might want to use multiple instances of the same application -with different configurations. Assuming the application is created inside -a function and you can call that function to instantiate it, that is -really easy to implement. In order to develop your application to support -creating new instances in functions have a look at the -:doc:`appfactories` pattern. - -A very common example would be creating applications per subdomain. For -instance you configure your webserver to dispatch all requests for all -subdomains to your application and you then use the subdomain information -to create user-specific instances. Once you have your server set up to -listen on all subdomains you can use a very simple WSGI application to do -the dynamic application creation. - -The perfect level for abstraction in that regard is the WSGI layer. You -write your own WSGI application that looks at the request that comes and -delegates it to your Flask application. If that application does not -exist yet, it is dynamically created and remembered. - -.. code-block:: python - - from threading import Lock - - class SubdomainDispatcher: - - def __init__(self, domain, create_app): - self.domain = domain - self.create_app = create_app - self.lock = Lock() - self.instances = {} - - def get_application(self, host): - host = host.split(':')[0] - assert host.endswith(self.domain), 'Configuration error' - subdomain = host[:-len(self.domain)].rstrip('.') - with self.lock: - app = self.instances.get(subdomain) - if app is None: - app = self.create_app(subdomain) - self.instances[subdomain] = app - return app - - def __call__(self, environ, start_response): - app = self.get_application(environ['HTTP_HOST']) - return app(environ, start_response) - - -This dispatcher can then be used like this: - -.. code-block:: python - - from myapplication import create_app, get_user_for_subdomain - from werkzeug.exceptions import NotFound - - def make_app(subdomain): - user = get_user_for_subdomain(subdomain) - if user is None: - # if there is no user for that subdomain we still have - # to return a WSGI application that handles that request. - # We can then just return the NotFound() exception as - # application which will render a default 404 page. - # You might also redirect the user to the main page then - return NotFound() - - # otherwise create the application for the specific user - return create_app(user) - - application = SubdomainDispatcher('example.com', make_app) - - -Dispatch by Path ----------------- - -Dispatching by a path on the URL is very similar. Instead of looking at -the ``Host`` header to figure out the subdomain one simply looks at the -request path up to the first slash. - -.. code-block:: python - - from threading import Lock - from wsgiref.util import shift_path_info - - class PathDispatcher: - - def __init__(self, default_app, create_app): - self.default_app = default_app - self.create_app = create_app - self.lock = Lock() - self.instances = {} - - def get_application(self, prefix): - with self.lock: - app = self.instances.get(prefix) - if app is None: - app = self.create_app(prefix) - if app is not None: - self.instances[prefix] = app - return app - - def __call__(self, environ, start_response): - app = self.get_application(_peek_path_info(environ)) - if app is not None: - shift_path_info(environ) - else: - app = self.default_app - return app(environ, start_response) - - def _peek_path_info(environ): - segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) - if segments: - return segments[0] - - return None - -The big difference between this and the subdomain one is that this one -falls back to another application if the creator function returns ``None``. - -.. code-block:: python - - from myapplication import create_app, default_app, get_user_for_prefix - - def make_app(prefix): - user = get_user_for_prefix(prefix) - if user is not None: - return create_app(user) - - application = PathDispatcher(default_app, make_app) diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst deleted file mode 100644 index 32fd062b..00000000 --- a/docs/patterns/appfactories.rst +++ /dev/null @@ -1,118 +0,0 @@ -Application Factories -===================== - -If you are already using packages and blueprints for your application -(:doc:`/blueprints`) there are a couple of really nice ways to further improve -the experience. A common pattern is creating the application object when -the blueprint is imported. But if you move the creation of this object -into a function, you can then create multiple instances of this app later. - -So why would you want to do this? - -1. Testing. You can have instances of the application with different - settings to test every case. -2. Multiple instances. Imagine you want to run different versions of the - same application. Of course you could have multiple instances with - different configs set up in your webserver, but if you use factories, - you can have multiple instances of the same application running in the - same application process which can be handy. - -So how would you then actually implement that? - -Basic Factories ---------------- - -The idea is to set up the application in a function. Like this:: - - def create_app(config_filename): - app = Flask(__name__) - app.config.from_pyfile(config_filename) - - from yourapplication.model import db - db.init_app(app) - - from yourapplication.views.admin import admin - from yourapplication.views.frontend import frontend - app.register_blueprint(admin) - app.register_blueprint(frontend) - - return app - -The downside is that you cannot use the application object in the blueprints -at import time. You can however use it from within a request. How do you -get access to the application with the config? Use -:data:`~flask.current_app`:: - - from flask import current_app, Blueprint, render_template - admin = Blueprint('admin', __name__, url_prefix='/admin') - - @admin.route('/') - def index(): - return render_template(current_app.config['INDEX_TEMPLATE']) - -Here we look up the name of a template in the config. - -Factories & Extensions ----------------------- - -It's preferable to create your extensions and app factories so that the -extension object does not initially get bound to the application. - -Using `Flask-SQLAlchemy `_, -as an example, you should not do something along those lines:: - - def create_app(config_filename): - app = Flask(__name__) - app.config.from_pyfile(config_filename) - - db = SQLAlchemy(app) - -But, rather, in model.py (or equivalent):: - - db = SQLAlchemy() - -and in your application.py (or equivalent):: - - def create_app(config_filename): - app = Flask(__name__) - app.config.from_pyfile(config_filename) - - from yourapplication.model import db - db.init_app(app) - -Using this design pattern, no application-specific state is stored on the -extension object, so one extension object can be used for multiple apps. -For more information about the design of extensions refer to :doc:`/extensiondev`. - -Using Applications ------------------- - -To run such an application, you can use the :command:`flask` command: - -.. code-block:: text - - $ flask --app hello run - -Flask will automatically detect the factory if it is named -``create_app`` or ``make_app`` in ``hello``. You can also pass arguments -to the factory like this: - -.. code-block:: text - - $ flask --app hello:create_app(local_auth=True) run - -Then the ``create_app`` factory in ``myapp`` is called with the keyword -argument ``local_auth=True``. See :doc:`/cli` for more detail. - -Factory Improvements --------------------- - -The factory function above is not very clever, but you can improve it. -The following changes are straightforward to implement: - -1. Make it possible to pass in configuration values for unit tests so that - you don't have to create config files on the filesystem. -2. Call a function from a blueprint when the application is setting up so - that you have a place to modify attributes of the application (like - hooking in before/after request handlers etc.) -3. Add in WSGI middlewares when the application is being created if necessary. diff --git a/docs/patterns/caching.rst b/docs/patterns/caching.rst deleted file mode 100644 index 9bf7b72a..00000000 --- a/docs/patterns/caching.rst +++ /dev/null @@ -1,16 +0,0 @@ -Caching -======= - -When your application runs slow, throw some caches in. Well, at least -it's the easiest way to speed up things. What does a cache do? Say you -have a function that takes some time to complete but the results would -still be good enough if they were 5 minutes old. So then the idea is that -you actually put the result of that calculation into a cache for some -time. - -Flask itself does not provide caching for you, but `Flask-Caching`_, an -extension for Flask does. Flask-Caching supports various backends, and it is -even possible to develop your own caching backend. - - -.. _Flask-Caching: https://flask-caching.readthedocs.io/en/latest/ diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst deleted file mode 100644 index 2e9a43a7..00000000 --- a/docs/patterns/celery.rst +++ /dev/null @@ -1,242 +0,0 @@ -Background Tasks with Celery -============================ - -If your application has a long running task, such as processing some uploaded data or -sending email, you don't want to wait for it to finish during a request. Instead, use a -task queue to send the necessary data to another process that will run the task in the -background while the request returns immediately. - -`Celery`_ is a powerful task queue that can be used for simple background tasks as well -as complex multi-stage programs and schedules. This guide will show you how to configure -Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use -Celery itself. - -.. _Celery: https://celery.readthedocs.io -.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html - -The Flask repository contains `an example `_ -based on the information on this page, which also shows how to use JavaScript to submit -tasks and poll for progress and results. - - -Install -------- - -Install Celery from PyPI, for example using pip: - -.. code-block:: text - - $ pip install celery - - -Integrate Celery with Flask ---------------------------- - -You can use Celery without any integration with Flask, but it's convenient to configure -it through Flask's config, and to let tasks access the Flask application. - -Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration -and registers tasks. While creating a Flask app, use the following code to create and -configure a Celery app as well. - -.. code-block:: python - - from celery import Celery, Task - - def celery_init_app(app: Flask) -> Celery: - class FlaskTask(Task): - def __call__(self, *args: object, **kwargs: object) -> object: - with app.app_context(): - return self.run(*args, **kwargs) - - celery_app = Celery(app.name, task_cls=FlaskTask) - celery_app.config_from_object(app.config["CELERY"]) - celery_app.set_default() - app.extensions["celery"] = celery_app - return celery_app - -This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from -the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so -that it is seen during each request. The ``Task`` subclass automatically runs task -functions with a Flask app context active, so that services like your database -connections are available. - -.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html - -Here's a basic ``example.py`` that configures Celery to use Redis for communication. We -enable a result backend, but ignore results by default. This allows us to store results -only for tasks where we care about the result. - -.. code-block:: python - - from flask import Flask - - app = Flask(__name__) - app.config.from_mapping( - CELERY=dict( - broker_url="redis://localhost", - result_backend="redis://localhost", - task_ignore_result=True, - ), - ) - celery_app = celery_init_app(app) - -Point the ``celery worker`` command at this and it will find the ``celery_app`` object. - -.. code-block:: text - - $ celery -A example worker --loglevel INFO - -You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's -docs for more information about defining schedules. - -.. code-block:: text - - $ celery -A example beat --loglevel INFO - - -Application Factory -------------------- - -When using the Flask application factory pattern, call the ``celery_init_app`` function -inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which -can be used to get the Celery app from the Flask app returned by the factory. - -.. code-block:: python - - def create_app() -> Flask: - app = Flask(__name__) - app.config.from_mapping( - CELERY=dict( - broker_url="redis://localhost", - result_backend="redis://localhost", - task_ignore_result=True, - ), - ) - app.config.from_prefixed_env() - celery_init_app(app) - return app - -To use ``celery`` commands, Celery needs an app object, but that's no longer directly -available. Create a ``make_celery.py`` file that calls the Flask app factory and gets -the Celery app from the returned Flask app. - -.. code-block:: python - - from example import create_app - - flask_app = create_app() - celery_app = flask_app.extensions["celery"] - -Point the ``celery`` command to this file. - -.. code-block:: text - - $ celery -A make_celery worker --loglevel INFO - $ celery -A make_celery beat --loglevel INFO - - -Defining Tasks --------------- - -Using ``@celery_app.task`` to decorate task functions requires access to the -``celery_app`` object, which won't be available when using the factory pattern. It also -means that the decorated tasks are tied to the specific Flask and Celery app instances, -which could be an issue during testing if you change configuration for a test. - -Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will -access whatever the "current app" is, which is a similar concept to Flask's blueprints -and app context. This is why we called ``celery_app.set_default()`` above. - -Here's an example task that adds two numbers together and returns the result. - -.. code-block:: python - - from celery import shared_task - - @shared_task(ignore_result=False) - def add_together(a: int, b: int) -> int: - return a + b - -Earlier, we configured Celery to ignore task results by default. Since we want to know -the return value of this task, we set ``ignore_result=False``. On the other hand, a task -that didn't need a result, such as sending an email, wouldn't set this. - - -Calling Tasks -------------- - -The decorated function becomes a task object with methods to call it in the background. -The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for -more methods. - -A Celery worker must be running to run the task. Starting a worker is shown in the -previous sections. - -.. code-block:: python - - from flask import request - - @app.post("/add") - def start_add() -> dict[str, object]: - a = request.form.get("a", type=int) - b = request.form.get("b", type=int) - result = add_together.delay(a, b) - return {"result_id": result.id} - -The route doesn't get the task's result immediately. That would defeat the purpose by -blocking the response. Instead, we return the running task's result id, which we can use -later to get the result. - - -Getting Results ---------------- - -To fetch the result of the task we started above, we'll add another route that takes the -result id we returned before. We return whether the task is finished (ready), whether it -finished successfully, and what the return value (or error) was if it is finished. - -.. code-block:: python - - from celery.result import AsyncResult - - @app.get("/result/") - def task_result(id: str) -> dict[str, object]: - result = AsyncResult(id) - return { - "ready": result.ready(), - "successful": result.successful(), - "value": result.result if result.ready() else None, - } - -Now you can start the task using the first route, then poll for the result using the -second route. This keeps the Flask request workers from being blocked waiting for tasks -to finish. - -The Flask repository contains `an example `_ -using JavaScript to submit tasks and poll for progress and results. - - -Passing Data to Tasks ---------------------- - -The "add" task above took two integers as arguments. To pass arguments to tasks, Celery -has to serialize them to a format that it can pass to other processes. Therefore, -passing complex objects is not recommended. For example, it would be impossible to pass -a SQLAlchemy model object, since that object is probably not serializable and is tied to -the session that queried it. - -Pass the minimal amount of data necessary to fetch or recreate any complex data within -the task. Consider a task that will run when the logged in user asks for an archive of -their data. The Flask request knows the logged in user, and has the user object queried -from the database. It got that by querying the database for a given id, so the task can -do the same thing. Pass the user's id rather than the user object. - -.. code-block:: python - - @shared_task - def generate_user_archive(user_id: str) -> None: - user = db.session.get(User, user_id) - ... - - generate_user_archive.delay(current_user.id) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst deleted file mode 100644 index 4ff8814b..00000000 --- a/docs/patterns/deferredcallbacks.rst +++ /dev/null @@ -1,44 +0,0 @@ -Deferred Request Callbacks -========================== - -One of the design principles of Flask is that response objects are created and -passed down a chain of potential callbacks that can modify them or replace -them. When the request handling starts, there is no response object yet. It is -created as necessary either by a view function or by some other component in -the system. - -What happens if you want to modify the response at a point where the response -does not exist yet? A common example for that would be a -:meth:`~flask.Flask.before_request` callback that wants to set a cookie on the -response object. - -One way is to avoid the situation. Very often that is possible. For instance -you can try to move that logic into a :meth:`~flask.Flask.after_request` -callback instead. However, sometimes moving code there makes it -more complicated or awkward to reason about. - -As an alternative, you can use :func:`~flask.after_this_request` to register -callbacks that will execute after only the current request. This way you can -defer code execution from anywhere in the application, based on the current -request. - -At any time during a request, we can register a function to be called at the -end of the request. For example you can remember the current language of the -user in a cookie in a :meth:`~flask.Flask.before_request` callback:: - - from flask import request, after_this_request - - @app.before_request - def detect_user_language(): - language = request.cookies.get('user_lang') - - if language is None: - language = guess_language_from_request() - - # when the response exists, set a cookie with the language - @after_this_request - def remember_language(response): - response.set_cookie('user_lang', language) - return response - - g.language = language diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst deleted file mode 100644 index 21ea767f..00000000 --- a/docs/patterns/favicon.rst +++ /dev/null @@ -1,53 +0,0 @@ -Adding a favicon -================ - -A "favicon" is an icon used by browsers for tabs and bookmarks. This helps -to distinguish your website and to give it a unique brand. - -A common question is how to add a favicon to a Flask application. First, of -course, you need an icon. It should be 16 × 16 pixels and in the ICO file -format. This is not a requirement but a de-facto standard supported by all -relevant browsers. Put the icon in your static directory as -:file:`favicon.ico`. - -Now, to get browsers to find your icon, the correct way is to add a link -tag in your HTML. So, for example: - -.. sourcecode:: html+jinja - - - -That's all you need for most browsers, however some really old ones do not -support this standard. The old de-facto standard is to serve this file, -with this name, at the website root. If your application is not mounted at -the root path of the domain you either need to configure the web server to -serve the icon at the root or if you can't do that you're out of luck. If -however your application is the root you can simply route a redirect:: - - app.add_url_rule('/favicon.ico', - redirect_to=url_for('static', filename='favicon.ico')) - -If you want to save the extra redirect request you can also write a view -using :func:`~flask.send_from_directory`:: - - import os - from flask import send_from_directory - - @app.route('/favicon.ico') - def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') - -We can leave out the explicit mimetype and it will be guessed, but we may -as well specify it to avoid the extra guessing, as it will always be the -same. - -The above will serve the icon via your application and if possible it's -better to configure your dedicated web server to serve it; refer to the -web server's documentation. - -See also --------- - -* The `Favicon `_ article on - Wikipedia diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst deleted file mode 100644 index 304f57dc..00000000 --- a/docs/patterns/fileuploads.rst +++ /dev/null @@ -1,182 +0,0 @@ -Uploading Files -=============== - -Ah yes, the good old problem of file uploads. The basic idea of file -uploads is actually quite simple. It basically works like this: - -1. A ``

`` tag is marked with ``enctype=multipart/form-data`` - and an ```` is placed in that form. -2. The application accesses the file from the :attr:`~flask.request.files` - dictionary on the request object. -3. use the :meth:`~werkzeug.datastructures.FileStorage.save` method of the file to save - the file permanently somewhere on the filesystem. - -A Gentle Introduction ---------------------- - -Let's start with a very basic application that uploads a file to a -specific upload folder and displays a file to the user. Let's look at the -bootstrapping code for our application:: - - import os - from flask import Flask, flash, request, redirect, url_for - from werkzeug.utils import secure_filename - - UPLOAD_FOLDER = '/path/to/the/uploads' - ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} - - app = Flask(__name__) - app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER - -So first we need a couple of imports. Most should be straightforward, the -:func:`werkzeug.secure_filename` is explained a little bit later. The -``UPLOAD_FOLDER`` is where we will store the uploaded files and the -``ALLOWED_EXTENSIONS`` is the set of allowed file extensions. - -Why do we limit the extensions that are allowed? You probably don't want -your users to be able to upload everything there if the server is directly -sending out the data to the client. That way you can make sure that users -are not able to upload HTML files that would cause XSS problems (see -:ref:`security-xss`). Also make sure to disallow ``.php`` files if the server -executes them, but who has PHP installed on their server, right? :) - -Next the functions that check if an extension is valid and that uploads -the file and redirects the user to the URL for the uploaded file:: - - def allowed_file(filename): - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - - @app.route('/', methods=['GET', 'POST']) - def upload_file(): - if request.method == 'POST': - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') - return redirect(request.url) - file = request.files['file'] - # If the user does not select a file, the browser submits an - # empty file without a filename. - if file.filename == '': - flash('No selected file') - return redirect(request.url) - if file and allowed_file(file.filename): - filename = secure_filename(file.filename) - file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) - return redirect(url_for('download_file', name=filename)) - return ''' - - Upload new File -

Upload new File

- - - -
- ''' - -So what does that :func:`~werkzeug.utils.secure_filename` function actually do? -Now the problem is that there is that principle called "never trust user -input". This is also true for the filename of an uploaded file. All -submitted form data can be forged, and filenames can be dangerous. For -the moment just remember: always use that function to secure a filename -before storing it directly on the filesystem. - -.. admonition:: Information for the Pros - - So you're interested in what that :func:`~werkzeug.utils.secure_filename` - function does and what the problem is if you're not using it? So just - imagine someone would send the following information as `filename` to - your application:: - - filename = "../../../../home/username/.bashrc" - - Assuming the number of ``../`` is correct and you would join this with - the ``UPLOAD_FOLDER`` the user might have the ability to modify a file on - the server's filesystem he or she should not modify. This does require some - knowledge about how the application looks like, but trust me, hackers - are patient :) - - Now let's look how that function works: - - >>> secure_filename('../../../../home/username/.bashrc') - 'home_username_.bashrc' - -We want to be able to serve the uploaded files so they can be downloaded -by users. We'll define a ``download_file`` view to serve files in the -upload folder by name. ``url_for("download_file", name=name)`` generates -download URLs. - -.. code-block:: python - - from flask import send_from_directory - - @app.route('/uploads/') - def download_file(name): - return send_from_directory(app.config["UPLOAD_FOLDER"], name) - -If you're using middleware or the HTTP server to serve files, you can -register the ``download_file`` endpoint as ``build_only`` so ``url_for`` -will work without a view function. - -.. code-block:: python - - app.add_url_rule( - "/uploads/", endpoint="download_file", build_only=True - ) - - -Improving Uploads ------------------ - -.. versionadded:: 0.6 - -So how exactly does Flask handle uploads? Well it will store them in the -webserver's memory if the files are reasonably small, otherwise in a -temporary location (as returned by :func:`tempfile.gettempdir`). But how -do you specify the maximum file size after which an upload is aborted? By -default Flask will happily accept file uploads with an unlimited amount of -memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` -config key:: - - from flask import Flask, Request - - app = Flask(__name__) - app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 - -The code above will limit the maximum allowed payload to 16 megabytes. -If a larger file is transmitted, Flask will raise a -:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. - -.. admonition:: Connection Reset Issue - - When using the local development server, you may get a connection - reset error instead of a 413 response. You will get the correct - status response when running the app with a production WSGI server. - -This feature was added in Flask 0.6 but can be achieved in older versions -as well by subclassing the request object. For more information on that -consult the Werkzeug documentation on file handling. - - -Upload Progress Bars --------------------- - -A while ago many developers had the idea to read the incoming file in -small chunks and store the upload progress in the database to be able to -poll the progress with JavaScript from the client. The client asks the -server every 5 seconds how much it has transmitted, but this is -something it should already know. - -An Easier Solution ------------------- - -Now there are better solutions that work faster and are more reliable. There -are JavaScript libraries like jQuery_ that have form plugins to ease the -construction of progress bar. - -Because the common pattern for file uploads exists almost unchanged in all -applications dealing with uploads, there are also some Flask extensions that -implement a full fledged upload mechanism that allows controlling which -file extensions are allowed to be uploaded. - -.. _jQuery: https://jquery.com/ diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst deleted file mode 100644 index 8eb6b3ac..00000000 --- a/docs/patterns/flashing.rst +++ /dev/null @@ -1,148 +0,0 @@ -Message Flashing -================ - -Good applications and user interfaces are all about feedback. If the user -does not get enough feedback they will probably end up hating the -application. Flask provides a really simple way to give feedback to a -user with the flashing system. The flashing system basically makes it -possible to record a message at the end of a request and access it next -request and only next request. This is usually combined with a layout -template that does this. Note that browsers and sometimes web servers enforce -a limit on cookie sizes. This means that flashing messages that are too -large for session cookies causes message flashing to fail silently. - -Simple Flashing ---------------- - -So here is a full example:: - - from flask import Flask, flash, redirect, render_template, \ - request, url_for - - app = Flask(__name__) - app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' - - @app.route('/') - def index(): - return render_template('index.html') - - @app.route('/login', methods=['GET', 'POST']) - def login(): - error = None - if request.method == 'POST': - if request.form['username'] != 'admin' or \ - request.form['password'] != 'secret': - error = 'Invalid credentials' - else: - flash('You were successfully logged in') - return redirect(url_for('index')) - return render_template('login.html', error=error) - -And here is the :file:`layout.html` template which does the magic: - -.. sourcecode:: html+jinja - - - My Application - {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} - {% block body %}{% endblock %} - -Here is the :file:`index.html` template which inherits from :file:`layout.html`: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

Overview

-

Do you want to log in? - {% endblock %} - -And here is the :file:`login.html` template which also inherits from -:file:`layout.html`: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

Login

- {% if error %} -

Error: {{ error }} - {% endif %} -

-
-
Username: -
-
Password: -
-
-

-

- {% endblock %} - -Flashing With Categories ------------------------- - -.. versionadded:: 0.3 - -It is also possible to provide categories when flashing a message. The -default category if nothing is provided is ``'message'``. Alternative -categories can be used to give the user better feedback. For example -error messages could be displayed with a red background. - -To flash a message with a different category, just use the second argument -to the :func:`~flask.flash` function:: - - flash('Invalid password provided', 'error') - -Inside the template you then have to tell the -:func:`~flask.get_flashed_messages` function to also return the -categories. The loop looks slightly different in that situation then: - -.. sourcecode:: html+jinja - - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
    - {% for category, message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} - -This is just one example of how to render these flashed messages. One -might also use the category to add a prefix such as -``Error:`` to the message. - -Filtering Flash Messages ------------------------- - -.. versionadded:: 0.9 - -Optionally you can pass a list of categories which filters the results of -:func:`~flask.get_flashed_messages`. This is useful if you wish to -render each category in a separate block. - -.. sourcecode:: html+jinja - - {% with errors = get_flashed_messages(category_filter=["error"]) %} - {% if errors %} -
- × -
    - {%- for msg in errors %} -
  • {{ msg }}
  • - {% endfor -%} -
-
- {% endif %} - {% endwith %} diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst deleted file mode 100644 index 1f2c07dd..00000000 --- a/docs/patterns/index.rst +++ /dev/null @@ -1,40 +0,0 @@ -Patterns for Flask -================== - -Certain features and interactions are common enough that you will find -them in most web applications. For example, many applications use a -relational database and user authentication. They will open a database -connection at the beginning of the request and get the information for -the logged in user. At the end of the request, the database connection -is closed. - -These types of patterns may be a bit outside the scope of Flask itself, -but Flask makes it easy to implement them. Some common patterns are -collected in the following pages. - -.. toctree:: - :maxdepth: 2 - - packages - appfactories - appdispatch - urlprocessors - sqlite3 - sqlalchemy - fileuploads - caching - viewdecorators - wtforms - templateinheritance - flashing - javascript - lazyloading - mongoengine - favicon - streaming - deferredcallbacks - methodoverrides - requestchecksum - celery - subclassing - singlepageapplications diff --git a/docs/patterns/javascript.rst b/docs/patterns/javascript.rst deleted file mode 100644 index d58a3eb6..00000000 --- a/docs/patterns/javascript.rst +++ /dev/null @@ -1,259 +0,0 @@ -JavaScript, ``fetch``, and JSON -=============================== - -You may want to make your HTML page dynamic, by changing data without -reloading the entire page. Instead of submitting an HTML ``
`` and -performing a redirect to re-render the template, you can add -`JavaScript`_ that calls |fetch|_ and replaces content on the page. - -|fetch|_ is the modern, built-in JavaScript solution to making -requests from a page. You may have heard of other "AJAX" methods and -libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in -modern browsers, although you may choose to use them or another library -depending on your application's requirements. These docs will only focus -on built-in JavaScript features. - -.. _JavaScript: https://developer.mozilla.org/Web/JavaScript -.. |fetch| replace:: ``fetch()`` -.. _fetch: https://developer.mozilla.org/Web/API/Fetch_API -.. |XHR| replace:: ``XMLHttpRequest()`` -.. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest -.. _jQuery: https://jquery.com/ - - -Rendering Templates -------------------- - -It is important to understand the difference between templates and -JavaScript. Templates are rendered on the server, before the response is -sent to the user's browser. JavaScript runs in the user's browser, after -the template is rendered and sent. Therefore, it is impossible to use -JavaScript to affect how the Jinja template is rendered, but it is -possible to render data into the JavaScript that will run. - -To provide data to JavaScript when rendering the template, use the -:func:`~jinja-filters.tojson` filter in a `` - -A less common pattern is to add the data to a ``data-`` attribute on an -HTML tag. In this case, you must use single quotes around the value, not -double quotes, otherwise you will produce invalid or unsafe HTML. - -.. code-block:: jinja - -
- - -Generating URLs ---------------- - -The other way to get data from the server to JavaScript is to make a -request for it. First, you need to know the URL to request. - -The simplest way to generate URLs is to continue to use -:func:`~flask.url_for` when rendering the template. For example: - -.. code-block:: javascript - - const user_url = {{ url_for("user", id=current_user.id)|tojson }} - fetch(user_url).then(...) - -However, you might need to generate a URL based on information you only -know in JavaScript. As discussed above, JavaScript runs in the user's -browser, not as part of the template rendering, so you can't use -``url_for`` at that point. - -In this case, you need to know the "root URL" under which your -application is served. In simple setups, this is ``/``, but it might -also be something else, like ``https://example.com/myapp/``. - -A simple way to tell your JavaScript code about this root is to set it -as a global variable when rendering the template. Then you can use it -when generating URLs from JavaScript. - -.. code-block:: javascript - - const SCRIPT_ROOT = {{ request.script_root|tojson }} - let user_id = ... // do something to get a user id from the page - let user_url = `${SCRIPT_ROOT}/user/${user_id}` - fetch(user_url).then(...) - - -Making a Request with ``fetch`` -------------------------------- - -|fetch|_ takes two arguments, a URL and an object with other options, -and returns a |Promise|_. We won't cover all the available options, and -will only use ``then()`` on the promise, not other callbacks or -``await`` syntax. Read the linked MDN docs for more information about -those features. - -By default, the GET method is used. If the response contains JSON, it -can be used with a ``then()`` callback chain. - -.. code-block:: javascript - - const room_url = {{ url_for("room_detail", id=room.id)|tojson }} - fetch(room_url) - .then(response => response.json()) - .then(data => { - // data is a parsed JSON object - }) - -To send data, use a data method such as POST, and pass the ``body`` -option. The most common types for data are form data or JSON data. - -To send form data, pass a populated |FormData|_ object. This uses the -same format as an HTML form, and would be accessed with ``request.form`` -in a Flask view. - -.. code-block:: javascript - - let data = new FormData() - data.append("name", "Flask Room") - data.append("description", "Talk about Flask here.") - fetch(room_url, { - "method": "POST", - "body": data, - }).then(...) - -In general, prefer sending request data as form data, as would be used -when submitting an HTML form. JSON can represent more complex data, but -unless you need that it's better to stick with the simpler format. When -sending JSON data, the ``Content-Type: application/json`` header must be -sent as well, otherwise Flask will return a 400 error. - -.. code-block:: javascript - - let data = { - "name": "Flask Room", - "description": "Talk about Flask here.", - } - fetch(room_url, { - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "body": JSON.stringify(data), - }).then(...) - -.. |Promise| replace:: ``Promise`` -.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise -.. |FormData| replace:: ``FormData`` -.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData - - -Following Redirects -------------------- - -A response might be a redirect, for example if you logged in with -JavaScript instead of a traditional HTML form, and your view returned -a redirect instead of JSON. JavaScript requests do follow redirects, but -they don't change the page. If you want to make the page change you can -inspect the response and apply the redirect manually. - -.. code-block:: javascript - - fetch("/login", {"body": ...}).then( - response => { - if (response.redirected) { - window.location = response.url - } else { - showLoginError() - } - } - ) - - -Replacing Content ------------------ - -A response might be new HTML, either a new section of the page to add or -replace, or an entirely new page. In general, if you're returning the -entire page, it would be better to handle that with a redirect as shown -in the previous section. The following example shows how to replace a -``
`` with the HTML returned by a request. - -.. code-block:: html - -
- {{ include "geology_fact.html" }} -
- - - -Return JSON from Views ----------------------- - -To return a JSON object from your API view, you can directly return a -dict from the view. It will be serialized to JSON automatically. - -.. code-block:: python - - @app.route("/user/") - def user_detail(id): - user = User.query.get_or_404(id) - return { - "username": User.username, - "email": User.email, - "picture": url_for("static", filename=f"users/{id}/profile.png"), - } - -If you want to return another JSON type, use the -:func:`~flask.json.jsonify` function, which creates a response object -with the given data serialized to JSON. - -.. code-block:: python - - from flask import jsonify - - @app.route("/users") - def user_list(): - users = User.query.order_by(User.name).all() - return jsonify([u.to_json() for u in users]) - -It is usually not a good idea to return file data in a JSON response. -JSON cannot represent binary data directly, so it must be base64 -encoded, which can be slow, takes more bandwidth to send, and is not as -easy to cache. Instead, serve files using one view, and generate a URL -to the desired file to include in the JSON. Then the client can make a -separate request to get the linked resource after getting the JSON. - - -Receiving JSON in Views ------------------------ - -Use the :attr:`~flask.Request.json` property of the -:data:`~flask.request` object to decode the request's body as JSON. If -the body is not valid JSON, or the ``Content-Type`` header is not set to -``application/json``, a 400 Bad Request error will be raised. - -.. code-block:: python - - from flask import request - - @app.post("/user/") - def user_update(id): - user = User.query.get_or_404(id) - user.update_from_json(request.json) - db.session.commit() - return user.to_json() diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst deleted file mode 100644 index 7ac6856e..00000000 --- a/docs/patterns/jquery.rst +++ /dev/null @@ -1,6 +0,0 @@ -:orphan: - -AJAX with jQuery -================ - -Obsolete, see :doc:`/patterns/javascript` instead. diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst deleted file mode 100644 index 658a1cd4..00000000 --- a/docs/patterns/lazyloading.rst +++ /dev/null @@ -1,109 +0,0 @@ -Lazily Loading Views -==================== - -Flask is usually used with the decorators. Decorators are simple and you -have the URL right next to the function that is called for that specific -URL. However there is a downside to this approach: it means all your code -that uses decorators has to be imported upfront or Flask will never -actually find your function. - -This can be a problem if your application has to import quick. It might -have to do that on systems like Google's App Engine or other systems. So -if you suddenly notice that your application outgrows this approach you -can fall back to a centralized URL mapping. - -The system that enables having a central URL map is the -:meth:`~flask.Flask.add_url_rule` function. Instead of using decorators, -you have a file that sets up the application with all URLs. - -Converting to Centralized URL Map ---------------------------------- - -Imagine the current application looks somewhat like this:: - - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def index(): - pass - - @app.route('/user/') - def user(username): - pass - -Then, with the centralized approach you would have one file with the views -(:file:`views.py`) but without any decorator:: - - def index(): - pass - - def user(username): - pass - -And then a file that sets up an application which maps the functions to -URLs:: - - from flask import Flask - from yourapplication import views - app = Flask(__name__) - app.add_url_rule('/', view_func=views.index) - app.add_url_rule('/user/', view_func=views.user) - -Loading Late ------------- - -So far we only split up the views and the routing, but the module is still -loaded upfront. The trick is to actually load the view function as needed. -This can be accomplished with a helper class that behaves just like a -function but internally imports the real function on first use:: - - from werkzeug.utils import import_string, cached_property - - class LazyView(object): - - def __init__(self, import_name): - self.__module__, self.__name__ = import_name.rsplit('.', 1) - self.import_name = import_name - - @cached_property - def view(self): - return import_string(self.import_name) - - def __call__(self, *args, **kwargs): - return self.view(*args, **kwargs) - -What's important here is is that `__module__` and `__name__` are properly -set. This is used by Flask internally to figure out how to name the -URL rules in case you don't provide a name for the rule yourself. - -Then you can define your central place to combine the views like this:: - - from flask import Flask - from yourapplication.helpers import LazyView - app = Flask(__name__) - app.add_url_rule('/', - view_func=LazyView('yourapplication.views.index')) - app.add_url_rule('/user/', - view_func=LazyView('yourapplication.views.user')) - -You can further optimize this in terms of amount of keystrokes needed to -write this by having a function that calls into -:meth:`~flask.Flask.add_url_rule` by prefixing a string with the project -name and a dot, and by wrapping `view_func` in a `LazyView` as needed. :: - - def url(import_name, url_rules=[], **options): - view = LazyView(f"yourapplication.{import_name}") - for url_rule in url_rules: - app.add_url_rule(url_rule, view_func=view, **options) - - # add a single route to the index view - url('views.index', ['/']) - - # add two routes to a single function endpoint - url_rules = ['/user/','/user/'] - url('views.user', url_rules) - -One thing to keep in mind is that before and after request handlers have -to be in a file that is imported upfront to work properly on the first -request. The same goes for any kind of remaining decorator. diff --git a/docs/patterns/methodoverrides.rst b/docs/patterns/methodoverrides.rst deleted file mode 100644 index 45dbb87e..00000000 --- a/docs/patterns/methodoverrides.rst +++ /dev/null @@ -1,42 +0,0 @@ -Adding HTTP Method Overrides -============================ - -Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP -methods (such as PATCH). In that case it's possible to "proxy" HTTP -methods through another HTTP method in total violation of the protocol. - -The way this works is by letting the client do an HTTP POST request and -set the ``X-HTTP-Method-Override`` header. Then the method is replaced -with the header value before being passed to Flask. - -This can be accomplished with an HTTP middleware:: - - class HTTPMethodOverrideMiddleware(object): - allowed_methods = frozenset([ - 'GET', - 'HEAD', - 'POST', - 'DELETE', - 'PUT', - 'PATCH', - 'OPTIONS' - ]) - bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) - - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() - if method in self.allowed_methods: - environ['REQUEST_METHOD'] = method - if method in self.bodyless_methods: - environ['CONTENT_LENGTH'] = '0' - return self.app(environ, start_response) - -To use this with Flask, wrap the app object with the middleware:: - - from flask import Flask - - app = Flask(__name__) - app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) diff --git a/docs/patterns/mongoengine.rst b/docs/patterns/mongoengine.rst deleted file mode 100644 index 015e7b61..00000000 --- a/docs/patterns/mongoengine.rst +++ /dev/null @@ -1,103 +0,0 @@ -MongoDB with MongoEngine -======================== - -Using a document database like MongoDB is a common alternative to -relational SQL databases. This pattern shows how to use -`MongoEngine`_, a document mapper library, to integrate with MongoDB. - -A running MongoDB server and `Flask-MongoEngine`_ are required. :: - - pip install flask-mongoengine - -.. _MongoEngine: http://mongoengine.org -.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io - - -Configuration -------------- - -Basic setup can be done by defining ``MONGODB_SETTINGS`` on -``app.config`` and creating a ``MongoEngine`` instance. :: - - from flask import Flask - from flask_mongoengine import MongoEngine - - app = Flask(__name__) - app.config['MONGODB_SETTINGS'] = { - "db": "myapp", - } - db = MongoEngine(app) - - -Mapping Documents ------------------ - -To declare a model that represents a Mongo document, create a class that -inherits from ``Document`` and declare each of the fields. :: - - import mongoengine as me - - class Movie(me.Document): - title = me.StringField(required=True) - year = me.IntField() - rated = me.StringField() - director = me.StringField() - actors = me.ListField() - -If the document has nested fields, use ``EmbeddedDocument`` to -defined the fields of the embedded document and -``EmbeddedDocumentField`` to declare it on the parent document. :: - - class Imdb(me.EmbeddedDocument): - imdb_id = me.StringField() - rating = me.DecimalField() - votes = me.IntField() - - class Movie(me.Document): - ... - imdb = me.EmbeddedDocumentField(Imdb) - - -Creating Data -------------- - -Instantiate your document class with keyword arguments for the fields. -You can also assign values to the field attributes after instantiation. -Then call ``doc.save()``. :: - - bttf = Movie(title="Back To The Future", year=1985) - bttf.actors = [ - "Michael J. Fox", - "Christopher Lloyd" - ] - bttf.imdb = Imdb(imdb_id="tt0088763", rating=8.5) - bttf.save() - - -Queries -------- - -Use the class ``objects`` attribute to make queries. A keyword argument -looks for an equal value on the field. :: - - bttf = Movies.objects(title="Back To The Future").get_or_404() - -Query operators may be used by concatenating them with the field name -using a double-underscore. ``objects``, and queries returned by -calling it, are iterable. :: - - some_theron_movie = Movie.objects(actors__in=["Charlize Theron"]).first() - - for recents in Movie.objects(year__gte=2017): - print(recents.title) - - -Documentation -------------- - -There are many more ways to define and query documents with MongoEngine. -For more information, check out the `official documentation -`_. - -Flask-MongoEngine adds helpful utilities on top of MongoEngine. Check -out their `documentation `_ as well. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst deleted file mode 100644 index 90fa8a8f..00000000 --- a/docs/patterns/packages.rst +++ /dev/null @@ -1,133 +0,0 @@ -Large Applications as Packages -============================== - -Imagine a simple flask application structure that looks like this:: - - /yourapplication - yourapplication.py - /static - style.css - /templates - layout.html - index.html - login.html - ... - -While this is fine for small applications, for larger applications -it's a good idea to use a package instead of a module. -The :doc:`/tutorial/index` is structured to use the package pattern, -see the :gh:`example code `. - -Simple Packages ---------------- - -To convert that into a larger one, just create a new folder -:file:`yourapplication` inside the existing one and move everything below it. -Then rename :file:`yourapplication.py` to :file:`__init__.py`. (Make sure to delete -all ``.pyc`` files first, otherwise things would most likely break) - -You should then end up with something like that:: - - /yourapplication - /yourapplication - __init__.py - /static - style.css - /templates - layout.html - index.html - login.html - ... - -But how do you run your application now? The naive ``python -yourapplication/__init__.py`` will not work. Let's just say that Python -does not want modules in packages to be the startup file. But that is not -a big problem, just add a new file called :file:`pyproject.toml` next to the inner -:file:`yourapplication` folder with the following contents: - -.. code-block:: toml - - [project] - name = "yourapplication" - dependencies = [ - "flask", - ] - - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" - -Install your application so it is importable: - -.. code-block:: text - - $ pip install -e . - -To use the ``flask`` command and run your application you need to set -the ``--app`` option that tells Flask where to find the application -instance: - -.. code-block:: text - - $ flask --app yourapplication run - -What did we gain from this? Now we can restructure the application a bit -into multiple modules. The only thing you have to remember is the -following quick checklist: - -1. the `Flask` application object creation has to be in the - :file:`__init__.py` file. That way each module can import it safely and the - `__name__` variable will resolve to the correct package. -2. all the view functions (the ones with a :meth:`~flask.Flask.route` - decorator on top) have to be imported in the :file:`__init__.py` file. - Not the object itself, but the module it is in. Import the view module - **after the application object is created**. - -Here's an example :file:`__init__.py`:: - - from flask import Flask - app = Flask(__name__) - - import yourapplication.views - -And this is what :file:`views.py` would look like:: - - from yourapplication import app - - @app.route('/') - def index(): - return 'Hello World!' - -You should then end up with something like that:: - - /yourapplication - pyproject.toml - /yourapplication - __init__.py - views.py - /static - style.css - /templates - layout.html - index.html - login.html - ... - -.. admonition:: Circular Imports - - Every Python programmer hates them, and yet we just added some: - circular imports (That's when two modules depend on each other. In this - case :file:`views.py` depends on :file:`__init__.py`). Be advised that this is a - bad idea in general but here it is actually fine. The reason for this is - that we are not actually using the views in :file:`__init__.py` and just - ensuring the module is imported and we are doing that at the bottom of - the file. - - -Working with Blueprints ------------------------ - -If you have larger applications it's recommended to divide them into -smaller groups where each group is implemented with the help of a -blueprint. For a gentle introduction into this topic refer to the -:doc:`/blueprints` chapter of the documentation. diff --git a/docs/patterns/requestchecksum.rst b/docs/patterns/requestchecksum.rst deleted file mode 100644 index 25bc38b2..00000000 --- a/docs/patterns/requestchecksum.rst +++ /dev/null @@ -1,55 +0,0 @@ -Request Content Checksums -========================= - -Various pieces of code can consume the request data and preprocess it. -For instance JSON data ends up on the request object already read and -processed, form data ends up there as well but goes through a different -code path. This seems inconvenient when you want to calculate the -checksum of the incoming request data. This is necessary sometimes for -some APIs. - -Fortunately this is however very simple to change by wrapping the input -stream. - -The following example calculates the SHA1 checksum of the incoming data as -it gets read and stores it in the WSGI environment:: - - import hashlib - - class ChecksumCalcStream(object): - - def __init__(self, stream): - self._stream = stream - self._hash = hashlib.sha1() - - def read(self, bytes): - rv = self._stream.read(bytes) - self._hash.update(rv) - return rv - - def readline(self, size_hint): - rv = self._stream.readline(size_hint) - self._hash.update(rv) - return rv - - def generate_checksum(request): - env = request.environ - stream = ChecksumCalcStream(env['wsgi.input']) - env['wsgi.input'] = stream - return stream._hash - -To use this, all you need to do is to hook the calculating stream in -before the request starts consuming data. (Eg: be careful accessing -``request.form`` or anything of that nature. ``before_request_handlers`` -for instance should be careful not to access it). - -Example usage:: - - @app.route('/special-api', methods=['POST']) - def special_api(): - hash = generate_checksum(request) - # Accessing this parses the input stream - files = request.files - # At this point the hash is fully constructed. - checksum = hash.hexdigest() - return f"Hash was: {checksum}" diff --git a/docs/patterns/singlepageapplications.rst b/docs/patterns/singlepageapplications.rst deleted file mode 100644 index 1cb779b3..00000000 --- a/docs/patterns/singlepageapplications.rst +++ /dev/null @@ -1,24 +0,0 @@ -Single-Page Applications -======================== - -Flask can be used to serve Single-Page Applications (SPA) by placing static -files produced by your frontend framework in a subfolder inside of your -project. You will also need to create a catch-all endpoint that routes all -requests to your SPA. - -The following example demonstrates how to serve an SPA along with an API:: - - from flask import Flask, jsonify - - app = Flask(__name__, static_folder='app', static_url_path="/app") - - - @app.route("/heartbeat") - def heartbeat(): - return jsonify({"status": "healthy"}) - - - @app.route('/', defaults={'path': ''}) - @app.route('/') - def catch_all(path): - return app.send_static_file("index.html") diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst deleted file mode 100644 index 7e4108d0..00000000 --- a/docs/patterns/sqlalchemy.rst +++ /dev/null @@ -1,214 +0,0 @@ -SQLAlchemy in Flask -=================== - -Many people prefer `SQLAlchemy`_ for database access. In this case it's -encouraged to use a package instead of a module for your flask application -and drop the models into a separate module (:doc:`packages`). While that -is not necessary, it makes a lot of sense. - -There are four very common ways to use SQLAlchemy. I will outline each -of them here: - -Flask-SQLAlchemy Extension --------------------------- - -Because SQLAlchemy is a common database abstraction layer and object -relational mapper that requires a little bit of configuration effort, -there is a Flask extension that handles that for you. This is recommended -if you want to get started quickly. - -You can download `Flask-SQLAlchemy`_ from `PyPI -`_. - -.. _Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/ - - -Declarative ------------ - -The declarative extension in SQLAlchemy is the most recent method of using -SQLAlchemy. It allows you to define tables and models in one go, similar -to how Django works. In addition to the following text I recommend the -official documentation on the `declarative`_ extension. - -Here's the example :file:`database.py` module for your application:: - - from sqlalchemy import create_engine - from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base - - engine = create_engine('sqlite:////tmp/test.db') - db_session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) - Base = declarative_base() - Base.query = db_session.query_property() - - def init_db(): - # import all modules here that might define models so that - # they will be registered properly on the metadata. Otherwise - # you will have to import them first before calling init_db() - import yourapplication.models - Base.metadata.create_all(bind=engine) - -To define your models, just subclass the `Base` class that was created by -the code above. If you are wondering why we don't have to care about -threads here (like we did in the SQLite3 example above with the -:data:`~flask.g` object): that's because SQLAlchemy does that for us -already with the :class:`~sqlalchemy.orm.scoped_session`. - -To use SQLAlchemy in a declarative way with your application, you just -have to put the following code into your application module. Flask will -automatically remove database sessions at the end of the request or -when the application shuts down:: - - from yourapplication.database import db_session - - @app.teardown_appcontext - def shutdown_session(exception=None): - db_session.remove() - -Here is an example model (put this into :file:`models.py`, e.g.):: - - from sqlalchemy import Column, Integer, String - from yourapplication.database import Base - - class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(String(50), unique=True) - email = Column(String(120), unique=True) - - def __init__(self, name=None, email=None): - self.name = name - self.email = email - - def __repr__(self): - return f'' - -To create the database you can use the `init_db` function: - ->>> from yourapplication.database import init_db ->>> init_db() - -You can insert entries into the database like this: - ->>> from yourapplication.database import db_session ->>> from yourapplication.models import User ->>> u = User('admin', 'admin@localhost') ->>> db_session.add(u) ->>> db_session.commit() - -Querying is simple as well: - ->>> User.query.all() -[] ->>> User.query.filter(User.name == 'admin').first() - - -.. _SQLAlchemy: https://www.sqlalchemy.org/ -.. _declarative: https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ - -Manual Object Relational Mapping --------------------------------- - -Manual object relational mapping has a few upsides and a few downsides -versus the declarative approach from above. The main difference is that -you define tables and classes separately and map them together. It's more -flexible but a little more to type. In general it works like the -declarative approach, so make sure to also split up your application into -multiple modules in a package. - -Here is an example :file:`database.py` module for your application:: - - from sqlalchemy import create_engine, MetaData - from sqlalchemy.orm import scoped_session, sessionmaker - - engine = create_engine('sqlite:////tmp/test.db') - metadata = MetaData() - db_session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) - def init_db(): - metadata.create_all(bind=engine) - -As in the declarative approach, you need to close the session after -each request or application context shutdown. Put this into your -application module:: - - from yourapplication.database import db_session - - @app.teardown_appcontext - def shutdown_session(exception=None): - db_session.remove() - -Here is an example table and model (put this into :file:`models.py`):: - - from sqlalchemy import Table, Column, Integer, String - from sqlalchemy.orm import mapper - from yourapplication.database import metadata, db_session - - class User(object): - query = db_session.query_property() - - def __init__(self, name=None, email=None): - self.name = name - self.email = email - - def __repr__(self): - return f'' - - users = Table('users', metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50), unique=True), - Column('email', String(120), unique=True) - ) - mapper(User, users) - -Querying and inserting works exactly the same as in the example above. - - -SQL Abstraction Layer ---------------------- - -If you just want to use the database system (and SQL) abstraction layer -you basically only need the engine:: - - from sqlalchemy import create_engine, MetaData, Table - - engine = create_engine('sqlite:////tmp/test.db') - metadata = MetaData(bind=engine) - -Then you can either declare the tables in your code like in the examples -above, or automatically load them:: - - from sqlalchemy import Table - - users = Table('users', metadata, autoload=True) - -To insert data you can use the `insert` method. We have to get a -connection first so that we can use a transaction: - ->>> con = engine.connect() ->>> con.execute(users.insert(), name='admin', email='admin@localhost') - -SQLAlchemy will automatically commit for us. - -To query your database, you use the engine directly or use a connection: - ->>> users.select(users.c.id == 1).execute().first() -(1, 'admin', 'admin@localhost') - -These results are also dict-like tuples: - ->>> r = users.select(users.c.id == 1).execute().first() ->>> r['name'] -'admin' - -You can also pass strings of SQL statements to the -:meth:`~sqlalchemy.engine.base.Connection.execute` method: - ->>> engine.execute('select * from users where id = :1', [1]).first() -(1, 'admin', 'admin@localhost') - -For more information about SQLAlchemy, head over to the -`website `_. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst deleted file mode 100644 index 5932589f..00000000 --- a/docs/patterns/sqlite3.rst +++ /dev/null @@ -1,147 +0,0 @@ -Using SQLite 3 with Flask -========================= - -In Flask you can easily implement the opening of database connections on -demand and closing them when the context dies (usually at the end of the -request). - -Here is a simple example of how you can use SQLite 3 with Flask:: - - import sqlite3 - from flask import g - - DATABASE = '/path/to/database.db' - - def get_db(): - db = getattr(g, '_database', None) - if db is None: - db = g._database = sqlite3.connect(DATABASE) - return db - - @app.teardown_appcontext - def close_connection(exception): - db = getattr(g, '_database', None) - if db is not None: - db.close() - -Now, to use the database, the application must either have an active -application context (which is always true if there is a request in flight) -or create an application context itself. At that point the ``get_db`` -function can be used to get the current database connection. Whenever the -context is destroyed the database connection will be terminated. - -Example:: - - @app.route('/') - def index(): - cur = get_db().cursor() - ... - - -.. note:: - - Please keep in mind that the teardown request and appcontext functions - are always executed, even if a before-request handler failed or was - never executed. Because of this we have to make sure here that the - database is there before we close it. - -Connect on Demand ------------------ - -The upside of this approach (connecting on first use) is that this will -only open the connection if truly necessary. If you want to use this -code outside a request context you can use it in a Python shell by opening -the application context by hand:: - - with app.app_context(): - # now you can use get_db() - - -Easy Querying -------------- - -Now in each request handling function you can access `get_db()` to get the -current open database connection. To simplify working with SQLite, a -row factory function is useful. It is executed for every result returned -from the database to convert the result. For instance, in order to get -dictionaries instead of tuples, this could be inserted into the ``get_db`` -function we created above:: - - def make_dicts(cursor, row): - return dict((cursor.description[idx][0], value) - for idx, value in enumerate(row)) - - db.row_factory = make_dicts - -This will make the sqlite3 module return dicts for this database connection, which are much nicer to deal with. Even more simply, we could place this in ``get_db`` instead:: - - db.row_factory = sqlite3.Row - -This would use Row objects rather than dicts to return the results of queries. These are ``namedtuple`` s, so we can access them either by index or by key. For example, assuming we have a ``sqlite3.Row`` called ``r`` for the rows ``id``, ``FirstName``, ``LastName``, and ``MiddleInitial``:: - - >>> # You can get values based on the row's name - >>> r['FirstName'] - John - >>> # Or, you can get them based on index - >>> r[1] - John - # Row objects are also iterable: - >>> for value in r: - ... print(value) - 1 - John - Doe - M - -Additionally, it is a good idea to provide a query function that combines -getting the cursor, executing and fetching the results:: - - def query_db(query, args=(), one=False): - cur = get_db().execute(query, args) - rv = cur.fetchall() - cur.close() - return (rv[0] if rv else None) if one else rv - -This handy little function, in combination with a row factory, makes -working with the database much more pleasant than it is by just using the -raw cursor and connection objects. - -Here is how you can use it:: - - for user in query_db('select * from users'): - print(user['username'], 'has the id', user['user_id']) - -Or if you just want a single result:: - - user = query_db('select * from users where username = ?', - [the_username], one=True) - if user is None: - print('No such user') - else: - print(the_username, 'has the id', user['user_id']) - -To pass variable parts to the SQL statement, use a question mark in the -statement and pass in the arguments as a list. Never directly add them to -the SQL statement with string formatting because this makes it possible -to attack the application using `SQL Injections -`_. - -Initial Schemas ---------------- - -Relational databases need schemas, so applications often ship a -`schema.sql` file that creates the database. It's a good idea to provide -a function that creates the database based on that schema. This function -can do that for you:: - - def init_db(): - with app.app_context(): - db = get_db() - with app.open_resource('schema.sql', mode='r') as f: - db.cursor().executescript(f.read()) - db.commit() - -You can then create such a database from the Python shell: - ->>> from yourapplication import init_db ->>> init_db() diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst deleted file mode 100644 index c9e6ef22..00000000 --- a/docs/patterns/streaming.rst +++ /dev/null @@ -1,85 +0,0 @@ -Streaming Contents -================== - -Sometimes you want to send an enormous amount of data to the client, much -more than you want to keep in memory. When you are generating the data on -the fly though, how do you send that back to the client without the -roundtrip to the filesystem? - -The answer is by using generators and direct responses. - -Basic Usage ------------ - -This is a basic view function that generates a lot of CSV data on the fly. -The trick is to have an inner function that uses a generator to generate -data and to then invoke that function and pass it to a response object:: - - @app.route('/large.csv') - def generate_large_csv(): - def generate(): - for row in iter_all_rows(): - yield f"{','.join(row)}\n" - return generate(), {"Content-Type": "text/csv"} - -Each ``yield`` expression is directly sent to the browser. Note though -that some WSGI middlewares might break streaming, so be careful there in -debug environments with profilers and other things you might have enabled. - -Streaming from Templates ------------------------- - -The Jinja2 template engine supports rendering a template piece by -piece, returning an iterator of strings. Flask provides the -:func:`~flask.stream_template` and :func:`~flask.stream_template_string` -functions to make this easier to use. - -.. code-block:: python - - from flask import stream_template - - @app.get("/timeline") - def timeline(): - return stream_template("timeline.html") - -The parts yielded by the render stream tend to match statement blocks in -the template. - - -Streaming with Context ----------------------- - -The :data:`~flask.request` will not be active while the generator is -running, because the view has already returned at that point. If you try -to access ``request``, you'll get a ``RuntimeError``. - -If your generator function relies on data in ``request``, use the -:func:`~flask.stream_with_context` wrapper. This will keep the request -context active during the generator. - -.. code-block:: python - - from flask import stream_with_context, request - from markupsafe import escape - - @app.route('/stream') - def streamed_response(): - def generate(): - yield '

Hello ' - yield escape(request.args['name']) - yield '!

' - return stream_with_context(generate()) - -It can also be used as a decorator. - -.. code-block:: python - - @stream_with_context - def generate(): - ... - - return generate() - -The :func:`~flask.stream_template` and -:func:`~flask.stream_template_string` functions automatically -use :func:`~flask.stream_with_context` if a request is active. diff --git a/docs/patterns/subclassing.rst b/docs/patterns/subclassing.rst deleted file mode 100644 index d8de2335..00000000 --- a/docs/patterns/subclassing.rst +++ /dev/null @@ -1,17 +0,0 @@ -Subclassing Flask -================= - -The :class:`~flask.Flask` class is designed for subclassing. - -For example, you may want to override how request parameters are handled to preserve their order:: - - from flask import Flask, Request - from werkzeug.datastructures import ImmutableOrderedMultiDict - class MyRequest(Request): - """Request subclass to override request parameter storage""" - parameter_storage_class = ImmutableOrderedMultiDict - class MyFlask(Flask): - """Flask subclass using the custom request class""" - request_class = MyRequest - -This is the recommended approach for overriding or augmenting Flask's internal functionality. diff --git a/docs/patterns/templateinheritance.rst b/docs/patterns/templateinheritance.rst deleted file mode 100644 index bb5cba27..00000000 --- a/docs/patterns/templateinheritance.rst +++ /dev/null @@ -1,68 +0,0 @@ -Template Inheritance -==================== - -The most powerful part of Jinja is template inheritance. Template inheritance -allows you to build a base "skeleton" template that contains all the common -elements of your site and defines **blocks** that child templates can override. - -Sounds complicated but is very basic. It's easiest to understand it by starting -with an example. - - -Base Template -------------- - -This template, which we'll call :file:`layout.html`, defines a simple HTML skeleton -document that you might use for a simple two-column page. It's the job of -"child" templates to fill the empty blocks with content: - -.. sourcecode:: html+jinja - - - - - {% block head %} - - {% block title %}{% endblock %} - My Webpage - {% endblock %} - - -
{% block content %}{% endblock %}
- - - - -In this example, the ``{% block %}`` tags define four blocks that child templates -can fill in. All the `block` tag does is tell the template engine that a -child template may override those portions of the template. - -Child Template --------------- - -A child template might look like this: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block title %}Index{% endblock %} - {% block head %} - {{ super() }} - - {% endblock %} - {% block content %} -

Index

-

- Welcome on my awesome homepage. - {% endblock %} - -The ``{% extends %}`` tag is the key here. It tells the template engine that -this template "extends" another template. When the template system evaluates -this template, first it locates the parent. The extends tag must be the -first tag in the template. To render the contents of a block defined in -the parent template, use ``{{ super() }}``. diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst deleted file mode 100644 index 0d743205..00000000 --- a/docs/patterns/urlprocessors.rst +++ /dev/null @@ -1,126 +0,0 @@ -Using URL Processors -==================== - -.. versionadded:: 0.7 - -Flask 0.7 introduces the concept of URL processors. The idea is that you -might have a bunch of resources with common parts in the URL that you -don't always explicitly want to provide. For instance you might have a -bunch of URLs that have the language code in it but you don't want to have -to handle it in every single function yourself. - -URL processors are especially helpful when combined with blueprints. We -will handle both application specific URL processors here as well as -blueprint specifics. - -Internationalized Application URLs ----------------------------------- - -Consider an application like this:: - - from flask import Flask, g - - app = Flask(__name__) - - @app.route('//') - def index(lang_code): - g.lang_code = lang_code - ... - - @app.route('//about') - def about(lang_code): - g.lang_code = lang_code - ... - -This is an awful lot of repetition as you have to handle the language code -setting on the :data:`~flask.g` object yourself in every single function. -Sure, a decorator could be used to simplify this, but if you want to -generate URLs from one function to another you would have to still provide -the language code explicitly which can be annoying. - -For the latter, this is where :func:`~flask.Flask.url_defaults` functions -come in. They can automatically inject values into a call to -:func:`~flask.url_for`. The code below checks if the -language code is not yet in the dictionary of URL values and if the -endpoint wants a value named ``'lang_code'``:: - - @app.url_defaults - def add_language_code(endpoint, values): - if 'lang_code' in values or not g.lang_code: - return - if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): - values['lang_code'] = g.lang_code - -The method :meth:`~werkzeug.routing.Map.is_endpoint_expecting` of the URL -map can be used to figure out if it would make sense to provide a language -code for the given endpoint. - -The reverse of that function are -:meth:`~flask.Flask.url_value_preprocessor`\s. They are executed right -after the request was matched and can execute code based on the URL -values. The idea is that they pull information out of the values -dictionary and put it somewhere else:: - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - g.lang_code = values.pop('lang_code', None) - -That way you no longer have to do the `lang_code` assignment to -:data:`~flask.g` in every function. You can further improve that by -writing your own decorator that prefixes URLs with the language code, but -the more beautiful solution is using a blueprint. Once the -``'lang_code'`` is popped from the values dictionary and it will no longer -be forwarded to the view function reducing the code to this:: - - from flask import Flask, g - - app = Flask(__name__) - - @app.url_defaults - def add_language_code(endpoint, values): - if 'lang_code' in values or not g.lang_code: - return - if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): - values['lang_code'] = g.lang_code - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - g.lang_code = values.pop('lang_code', None) - - @app.route('//') - def index(): - ... - - @app.route('//about') - def about(): - ... - -Internationalized Blueprint URLs --------------------------------- - -Because blueprints can automatically prefix all URLs with a common string -it's easy to automatically do that for every function. Furthermore -blueprints can have per-blueprint URL processors which removes a whole lot -of logic from the :meth:`~flask.Flask.url_defaults` function because it no -longer has to check if the URL is really interested in a ``'lang_code'`` -parameter:: - - from flask import Blueprint, g - - bp = Blueprint('frontend', __name__, url_prefix='/') - - @bp.url_defaults - def add_language_code(endpoint, values): - values.setdefault('lang_code', g.lang_code) - - @bp.url_value_preprocessor - def pull_lang_code(endpoint, values): - g.lang_code = values.pop('lang_code') - - @bp.route('/') - def index(): - ... - - @bp.route('/about') - def about(): - ... diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst deleted file mode 100644 index 0b0479ef..00000000 --- a/docs/patterns/viewdecorators.rst +++ /dev/null @@ -1,171 +0,0 @@ -View Decorators -=============== - -Python has a really interesting feature called function decorators. This -allows some really neat things for web applications. Because each view in -Flask is a function, decorators can be used to inject additional -functionality to one or more functions. The :meth:`~flask.Flask.route` -decorator is the one you probably used already. But there are use cases -for implementing your own decorator. For instance, imagine you have a -view that should only be used by people that are logged in. If a user -goes to the site and is not logged in, they should be redirected to the -login page. This is a good example of a use case where a decorator is an -excellent solution. - -Login Required Decorator ------------------------- - -So let's implement such a decorator. A decorator is a function that -wraps and replaces another function. Since the original function is -replaced, you need to remember to copy the original function's information -to the new function. Use :func:`functools.wraps` to handle this for you. - -This example assumes that the login page is called ``'login'`` and that -the current user is stored in ``g.user`` and is ``None`` if there is no-one -logged in. :: - - from functools import wraps - from flask import g, request, redirect, url_for - - def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if g.user is None: - return redirect(url_for('login', next=request.url)) - return f(*args, **kwargs) - return decorated_function - -To use the decorator, apply it as innermost decorator to a view function. -When applying further decorators, always remember -that the :meth:`~flask.Flask.route` decorator is the outermost. :: - - @app.route('/secret_page') - @login_required - def secret_page(): - pass - -.. note:: - The ``next`` value will exist in ``request.args`` after a ``GET`` request for - the login page. You'll have to pass it along when sending the ``POST`` request - from the login form. You can do this with a hidden input tag, then retrieve it - from ``request.form`` when logging the user in. :: - - - - -Caching Decorator ------------------ - -Imagine you have a view function that does an expensive calculation and -because of that you would like to cache the generated results for a -certain amount of time. A decorator would be nice for that. We're -assuming you have set up a cache like mentioned in :doc:`caching`. - -Here is an example cache function. It generates the cache key from a -specific prefix (actually a format string) and the current path of the -request. Notice that we are using a function that first creates the -decorator that then decorates the function. Sounds awful? Unfortunately -it is a little bit more complex, but the code should still be -straightforward to read. - -The decorated function will then work as follows - -1. get the unique cache key for the current request based on the current - path. -2. get the value for that key from the cache. If the cache returned - something we will return that value. -3. otherwise the original function is called and the return value is - stored in the cache for the timeout provided (by default 5 minutes). - -Here the code:: - - from functools import wraps - from flask import request - - def cached(timeout=5 * 60, key='view/{}'): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - cache_key = key.format(request.path) - rv = cache.get(cache_key) - if rv is not None: - return rv - rv = f(*args, **kwargs) - cache.set(cache_key, rv, timeout=timeout) - return rv - return decorated_function - return decorator - -Notice that this assumes an instantiated ``cache`` object is available, see -:doc:`caching`. - - -Templating Decorator --------------------- - -A common pattern invented by the TurboGears guys a while back is a -templating decorator. The idea of that decorator is that you return a -dictionary with the values passed to the template from the view function -and the template is automatically rendered. With that, the following -three examples do exactly the same:: - - @app.route('/') - def index(): - return render_template('index.html', value=42) - - @app.route('/') - @templated('index.html') - def index(): - return dict(value=42) - - @app.route('/') - @templated() - def index(): - return dict(value=42) - -As you can see, if no template name is provided it will use the endpoint -of the URL map with dots converted to slashes + ``'.html'``. Otherwise -the provided template name is used. When the decorated function returns, -the dictionary returned is passed to the template rendering function. If -``None`` is returned, an empty dictionary is assumed, if something else than -a dictionary is returned we return it from the function unchanged. That -way you can still use the redirect function or return simple strings. - -Here is the code for that decorator:: - - from functools import wraps - from flask import request, render_template - - def templated(template=None): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - template_name = template - if template_name is None: - template_name = f"{request.endpoint.replace('.', '/')}.html" - ctx = f(*args, **kwargs) - if ctx is None: - ctx = {} - elif not isinstance(ctx, dict): - return ctx - return render_template(template_name, **ctx) - return decorated_function - return decorator - - -Endpoint Decorator ------------------- - -When you want to use the werkzeug routing system for more flexibility you -need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` -to a view function. This is possible with this decorator. For example:: - - from flask import Flask - from werkzeug.routing import Rule - - app = Flask(__name__) - app.url_map.add(Rule('/', endpoint='index')) - - @app.endpoint('index') - def my_index(): - return "Hello world" diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst deleted file mode 100644 index 3d626f50..00000000 --- a/docs/patterns/wtforms.rst +++ /dev/null @@ -1,126 +0,0 @@ -Form Validation with WTForms -============================ - -When you have to work with form data submitted by a browser view, code -quickly becomes very hard to read. There are libraries out there designed -to make this process easier to manage. One of them is `WTForms`_ which we -will handle here. If you find yourself in the situation of having many -forms, you might want to give it a try. - -When you are working with WTForms you have to define your forms as classes -first. I recommend breaking up the application into multiple modules -(:doc:`packages`) for that and adding a separate module for the -forms. - -.. admonition:: Getting the most out of WTForms with an Extension - - The `Flask-WTF`_ extension expands on this pattern and adds a - few little helpers that make working with forms and Flask more - fun. You can get it from `PyPI - `_. - -.. _Flask-WTF: https://flask-wtf.readthedocs.io/ - -The Forms ---------- - -This is an example form for a typical registration page:: - - from wtforms import Form, BooleanField, StringField, PasswordField, validators - - class RegistrationForm(Form): - username = StringField('Username', [validators.Length(min=4, max=25)]) - email = StringField('Email Address', [validators.Length(min=6, max=35)]) - password = PasswordField('New Password', [ - validators.DataRequired(), - validators.EqualTo('confirm', message='Passwords must match') - ]) - confirm = PasswordField('Repeat Password') - accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) - -In the View ------------ - -In the view function, the usage of this form looks like this:: - - @app.route('/register', methods=['GET', 'POST']) - def register(): - form = RegistrationForm(request.form) - if request.method == 'POST' and form.validate(): - user = User(form.username.data, form.email.data, - form.password.data) - db_session.add(user) - flash('Thanks for registering') - return redirect(url_for('login')) - return render_template('register.html', form=form) - -Notice we're implying that the view is using SQLAlchemy here -(:doc:`sqlalchemy`), but that's not a requirement, of course. Adapt -the code as necessary. - -Things to remember: - -1. create the form from the request :attr:`~flask.request.form` value if - the data is submitted via the HTTP ``POST`` method and - :attr:`~flask.request.args` if the data is submitted as ``GET``. -2. to validate the data, call the :func:`~wtforms.form.Form.validate` - method, which will return ``True`` if the data validates, ``False`` - otherwise. -3. to access individual values from the form, access `form..data`. - -Forms in Templates ------------------- - -Now to the template side. When you pass the form to the templates, you can -easily render them there. Look at the following example template to see -how easy this is. WTForms does half the form generation for us already. -To make it even nicer, we can write a macro that renders a field with -label and a list of errors if there are any. - -Here's an example :file:`_formhelpers.html` template with such a macro: - -.. sourcecode:: html+jinja - - {% macro render_field(field) %} -

{{ field.label }} -
{{ field(**kwargs)|safe }} - {% if field.errors %} -
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} -
- {% endmacro %} - -This macro accepts a couple of keyword arguments that are forwarded to -WTForm's field function, which renders the field for us. The keyword -arguments will be inserted as HTML attributes. So, for example, you can -call ``render_field(form.username, class='username')`` to add a class to -the input element. Note that WTForms returns standard Python strings, -so we have to tell Jinja2 that this data is already HTML-escaped with -the ``|safe`` filter. - -Here is the :file:`register.html` template for the function we used above, which -takes advantage of the :file:`_formhelpers.html` template: - -.. sourcecode:: html+jinja - - {% from "_formhelpers.html" import render_field %} - -
- {{ render_field(form.username) }} - {{ render_field(form.email) }} - {{ render_field(form.password) }} - {{ render_field(form.confirm) }} - {{ render_field(form.accept_tos) }} -
-

-

- -For more information about WTForms, head over to the `WTForms -website`_. - -.. _WTForms: https://wtforms.readthedocs.io/ -.. _WTForms website: https://wtforms.readthedocs.io/ diff --git a/docs/quickstart.rst b/docs/quickstart.rst deleted file mode 100644 index f763bb1e..00000000 --- a/docs/quickstart.rst +++ /dev/null @@ -1,907 +0,0 @@ -Quickstart -========== - -Eager to get started? This page gives a good introduction to Flask. -Follow :doc:`installation` to set up a project and install Flask first. - - -A Minimal Application ---------------------- - -A minimal Flask application looks something like this: - -.. code-block:: python - - from flask import Flask - - app = Flask(__name__) - - @app.route("/") - def hello_world(): - return "

Hello, World!

" - -So what did that code do? - -1. First we imported the :class:`~flask.Flask` class. An instance of - this class will be our WSGI application. -2. Next we create an instance of this class. The first argument is the - name of the application's module or package. ``__name__`` is a - convenient shortcut for this that is appropriate for most cases. - This is needed so that Flask knows where to look for resources such - as templates and static files. -3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask - what URL should trigger our function. -4. The function returns the message we want to display in the user's - browser. The default content type is HTML, so HTML in the string - will be rendered by the browser. - -Save it as :file:`hello.py` or something similar. Make sure to not call -your application :file:`flask.py` because this would conflict with Flask -itself. - -To run the application, use the ``flask`` command or -``python -m flask``. You need to tell the Flask where your application -is with the ``--app`` option. - -.. code-block:: text - - $ flask --app hello run - * Serving Flask app 'hello' - * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) - -.. admonition:: Application Discovery Behavior - - As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you - don't have to use ``--app``. See :doc:`/cli` for more details. - -This launches a very simple builtin server, which is good enough for -testing but probably not what you want to use in production. For -deployment options see :doc:`deploying/index`. - -Now head over to http://127.0.0.1:5000/, and you should see your hello -world greeting. - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. - -.. _public-server: - -.. admonition:: Externally Visible Server - - If you run the server you will notice that the server is only accessible - from your own computer, not from any other in the network. This is the - default because in debugging mode a user of the application can execute - arbitrary Python code on your computer. - - If you have the debugger disabled or trust the users on your network, - you can make the server publicly available simply by adding - ``--host=0.0.0.0`` to the command line:: - - $ flask run --host=0.0.0.0 - - This tells your operating system to listen on all public IPs. - - -Debug Mode ----------- - -The ``flask run`` command can do more than just start the development -server. By enabling debug mode, the server will automatically reload if -code changes, and will show an interactive debugger in the browser if an -error occurs during a request. - -.. image:: _static/debugger.png - :align: center - :class: screenshot - :alt: The interactive debugger in action. - -.. warning:: - - The debugger allows executing arbitrary Python code from the - browser. It is protected by a pin, but still represents a major - security risk. Do not run the development server or debugger in a - production environment. - -To enable debug mode, use the ``--debug`` option. - -.. code-block:: text - - $ flask --app hello run --debug - * Serving Flask app 'hello' - * Debug mode: on - * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) - * Restarting with stat - * Debugger is active! - * Debugger PIN: nnn-nnn-nnn - -See also: - -- :doc:`/server` and :doc:`/cli` for information about running in debug mode. -- :doc:`/debugging` for information about using the built-in debugger - and other debuggers. -- :doc:`/logging` and :doc:`/errorhandling` to log errors and display - nice error pages. - - -HTML Escaping -------------- - -When returning HTML (the default response type in Flask), any -user-provided values rendered in the output must be escaped to protect -from injection attacks. HTML templates rendered with Jinja, introduced -later, will do this automatically. - -:func:`~markupsafe.escape`, shown here, can be used manually. It is -omitted in most examples for brevity, but you should always be aware of -how you're using untrusted data. - -.. code-block:: python - - from markupsafe import escape - - @app.route("/") - def hello(name): - return f"Hello, {escape(name)}!" - -If a user managed to submit the name ````, -escaping causes it to be rendered as text, rather than running the -script in the user's browser. - -```` in the route captures a value from the URL and passes it to -the view function. These variable rules are explained below. - - -Routing -------- - -Modern web applications use meaningful URLs to help users. Users are more -likely to like a page and come back if the page uses a meaningful URL they can -remember and use to directly visit a page. - -Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: - - @app.route('/') - def index(): - return 'Index Page' - - @app.route('/hello') - def hello(): - return 'Hello, World' - -You can do more! You can make parts of the URL dynamic and attach multiple -rules to a function. - -Variable Rules -`````````````` - -You can add variable sections to a URL by marking sections with -````. Your function then receives the ```` -as a keyword argument. Optionally, you can use a converter to specify the type -of the argument like ````. :: - - from markupsafe import escape - - @app.route('/user/') - def show_user_profile(username): - # show the user profile for that user - return f'User {escape(username)}' - - @app.route('/post/') - def show_post(post_id): - # show the post with the given id, the id is an integer - return f'Post {post_id}' - - @app.route('/path/') - def show_subpath(subpath): - # show the subpath after /path/ - return f'Subpath {escape(subpath)}' - -Converter types: - -========== ========================================== -``string`` (default) accepts any text without a slash -``int`` accepts positive integers -``float`` accepts positive floating point values -``path`` like ``string`` but also accepts slashes -``uuid`` accepts UUID strings -========== ========================================== - - -Unique URLs / Redirection Behavior -`````````````````````````````````` - -The following two rules differ in their use of a trailing slash. :: - - @app.route('/projects/') - def projects(): - return 'The project page' - - @app.route('/about') - def about(): - return 'The about page' - -The canonical URL for the ``projects`` endpoint has a trailing slash. -It's similar to a folder in a file system. If you access the URL without -a trailing slash (``/projects``), Flask redirects you to the canonical URL -with the trailing slash (``/projects/``). - -The canonical URL for the ``about`` endpoint does not have a trailing -slash. It's similar to the pathname of a file. Accessing the URL with a -trailing slash (``/about/``) produces a 404 "Not Found" error. This helps -keep URLs unique for these resources, which helps search engines avoid -indexing the same page twice. - - -.. _url-building: - -URL Building -```````````` - -To build a URL to a specific function, use the :func:`~flask.url_for` function. -It accepts the name of the function as its first argument and any number of -keyword arguments, each corresponding to a variable part of the URL rule. -Unknown variable parts are appended to the URL as query parameters. - -Why would you want to build URLs using the URL reversing function -:func:`~flask.url_for` instead of hard-coding them into your templates? - -1. Reversing is often more descriptive than hard-coding the URLs. -2. You can change your URLs in one go instead of needing to remember to - manually change hard-coded URLs. -3. URL building handles escaping of special characters transparently. -4. The generated paths are always absolute, avoiding unexpected behavior - of relative paths in browsers. -5. If your application is placed outside the URL root, for example, in - ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly - handles that for you. - -For example, here we use the :meth:`~flask.Flask.test_request_context` method -to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` -tells Flask to behave as though it's handling a request even while we use a -Python shell. See :ref:`context-locals`. - -.. code-block:: python - - from flask import url_for - - @app.route('/') - def index(): - return 'index' - - @app.route('/login') - def login(): - return 'login' - - @app.route('/user/') - def profile(username): - return f'{username}\'s profile' - - with app.test_request_context(): - print(url_for('index')) - print(url_for('login')) - print(url_for('login', next='/')) - print(url_for('profile', username='John Doe')) - -.. code-block:: text - - / - /login - /login?next=/ - /user/John%20Doe - - -HTTP Methods -```````````` - -Web applications use different HTTP methods when accessing URLs. You should -familiarize yourself with the HTTP methods as you work with Flask. By default, -a route only answers to ``GET`` requests. You can use the ``methods`` argument -of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. -:: - - from flask import request - - @app.route('/login', methods=['GET', 'POST']) - def login(): - if request.method == 'POST': - return do_the_login() - else: - return show_the_login_form() - -The example above keeps all methods for the route within one function, -which can be useful if each part uses some common data. - -You can also separate views for different methods into different -functions. Flask provides a shortcut for decorating such routes with -:meth:`~flask.Flask.get`, :meth:`~flask.Flask.post`, etc. for each -common HTTP method. - -.. code-block:: python - - @app.get('/login') - def login_get(): - return show_the_login_form() - - @app.post('/login') - def login_post(): - return do_the_login() - -If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method -and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise, -``OPTIONS`` is automatically implemented for you. - -.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt - -Static Files ------------- - -Dynamic web applications also need static files. That's usually where -the CSS and JavaScript files are coming from. Ideally your web server is -configured to serve them for you, but during development Flask can do that -as well. Just create a folder called :file:`static` in your package or next to -your module and it will be available at ``/static`` on the application. - -To generate URLs for static files, use the special ``'static'`` endpoint name:: - - url_for('static', filename='style.css') - -The file has to be stored on the filesystem as :file:`static/style.css`. - -Rendering Templates -------------------- - -Generating HTML from within Python is not fun, and actually pretty -cumbersome because you have to do the HTML escaping on your own to keep -the application secure. Because of that Flask configures the `Jinja2 -`_ template engine for you automatically. - -Templates can be used to generate any type of text file. For web applications, you'll -primarily be generating HTML pages, but you can also generate markdown, plain text for -emails, and anything else. - -For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. - -.. _MDN Web Docs: https://developer.mozilla.org/ - -To render a template you can use the :func:`~flask.render_template` -method. All you have to do is provide the name of the template and the -variables you want to pass to the template engine as keyword arguments. -Here's a simple example of how to render a template:: - - from flask import render_template - - @app.route('/hello/') - @app.route('/hello/') - def hello(name=None): - return render_template('hello.html', person=name) - -Flask will look for templates in the :file:`templates` folder. So if your -application is a module, this folder is next to that module, if it's a -package it's actually inside your package: - -**Case 1**: a module:: - - /application.py - /templates - /hello.html - -**Case 2**: a package:: - - /application - /__init__.py - /templates - /hello.html - -For templates you can use the full power of Jinja2 templates. Head over -to the official `Jinja2 Template Documentation -`_ for more information. - -Here is an example template: - -.. sourcecode:: html+jinja - - - Hello from Flask - {% if person %} -

Hello {{ person }}!

- {% else %} -

Hello, World!

- {% endif %} - -Inside templates you also have access to the :data:`~flask.Flask.config`, -:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects -as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions. - -Templates are especially useful if inheritance is used. If you want to -know how that works, see :doc:`patterns/templateinheritance`. Basically -template inheritance makes it possible to keep certain elements on each -page (like header, navigation and footer). - -Automatic escaping is enabled, so if ``person`` contains HTML it will be escaped -automatically. If you can trust a variable and you know that it will be -safe HTML (for example because it came from a module that converts wiki -markup to HTML) you can mark it as safe by using the -:class:`~markupsafe.Markup` class or by using the ``|safe`` filter in the -template. Head over to the Jinja 2 documentation for more examples. - -Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:: - - >>> from markupsafe import Markup - >>> Markup('Hello %s!') % 'hacker' - Markup('Hello <blink>hacker</blink>!') - >>> Markup.escape('hacker') - Markup('<blink>hacker</blink>') - >>> Markup('Marked up » HTML').striptags() - 'Marked up » HTML' - -.. versionchanged:: 0.5 - - Autoescaping is no longer enabled for all templates. The following - extensions for templates trigger autoescaping: ``.html``, ``.htm``, - ``.xml``, ``.xhtml``. Templates loaded from a string will have - autoescaping disabled. - -.. [#] Unsure what that :class:`~flask.g` object is? It's something in which - you can store information for your own needs. See the documentation - for :class:`flask.g` and :doc:`patterns/sqlite3`. - - -Accessing Request Data ----------------------- - -For web applications it's crucial to react to the data a client sends to -the server. In Flask this information is provided by the global -:class:`~flask.request` object. If you have some experience with Python -you might be wondering how that object can be global and how Flask -manages to still be threadsafe. The answer is context locals: - - -.. _context-locals: - -Context Locals -`````````````` - -.. admonition:: Insider Information - - If you want to understand how that works and how you can implement - tests with context locals, read this section, otherwise just skip it. - -Certain objects in Flask are global objects, but not of the usual kind. -These objects are actually proxies to objects that are local to a specific -context. What a mouthful. But that is actually quite easy to understand. - -Imagine the context being the handling thread. A request comes in and the -web server decides to spawn a new thread (or something else, the -underlying object is capable of dealing with concurrency systems other -than threads). When Flask starts its internal request handling it -figures out that the current thread is the active context and binds the -current application and the WSGI environments to that context (thread). -It does that in an intelligent way so that one application can invoke another -application without breaking. - -So what does this mean to you? Basically you can completely ignore that -this is the case unless you are doing something like unit testing. You -will notice that code which depends on a request object will suddenly break -because there is no request object. The solution is creating a request -object yourself and binding it to the context. The easiest solution for -unit testing is to use the :meth:`~flask.Flask.test_request_context` -context manager. In combination with the ``with`` statement it will bind a -test request so that you can interact with it. Here is an example:: - - from flask import request - - with app.test_request_context('/hello', method='POST'): - # now you can do something with the request until the - # end of the with block, such as basic assertions: - assert request.path == '/hello' - assert request.method == 'POST' - -The other possibility is passing a whole WSGI environment to the -:meth:`~flask.Flask.request_context` method:: - - with app.request_context(environ): - assert request.method == 'POST' - -The Request Object -`````````````````` - -The request object is documented in the API section and we will not cover -it here in detail (see :class:`~flask.Request`). Here is a broad overview of -some of the most common operations. First of all you have to import it from -the ``flask`` module:: - - from flask import request - -The current request method is available by using the -:attr:`~flask.Request.method` attribute. To access form data (data -transmitted in a ``POST`` or ``PUT`` request) you can use the -:attr:`~flask.Request.form` attribute. Here is a full example of the two -attributes mentioned above:: - - @app.route('/login', methods=['POST', 'GET']) - def login(): - error = None - if request.method == 'POST': - if valid_login(request.form['username'], - request.form['password']): - return log_the_user_in(request.form['username']) - else: - error = 'Invalid username/password' - # the code below is executed if the request method - # was GET or the credentials were invalid - return render_template('login.html', error=error) - -What happens if the key does not exist in the ``form`` attribute? In that -case a special :exc:`KeyError` is raised. You can catch it like a -standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request -error page is shown instead. So for many situations you don't have to -deal with that problem. - -To access parameters submitted in the URL (``?key=value``) you can use the -:attr:`~flask.Request.args` attribute:: - - searchword = request.args.get('key', '') - -We recommend accessing URL parameters with `get` or by catching the -:exc:`KeyError` because users might change the URL and presenting them a 400 -bad request page in that case is not user friendly. - -For a full list of methods and attributes of the request object, head over -to the :class:`~flask.Request` documentation. - - -File Uploads -```````````` - -You can handle uploaded files with Flask easily. Just make sure not to -forget to set the ``enctype="multipart/form-data"`` attribute on your HTML -form, otherwise the browser will not transmit your files at all. - -Uploaded files are stored in memory or at a temporary location on the -filesystem. You can access those files by looking at the -:attr:`~flask.request.files` attribute on the request object. Each -uploaded file is stored in that dictionary. It behaves just like a -standard Python :class:`file` object, but it also has a -:meth:`~werkzeug.datastructures.FileStorage.save` method that -allows you to store that file on the filesystem of the server. -Here is a simple example showing how that works:: - - from flask import request - - @app.route('/upload', methods=['GET', 'POST']) - def upload_file(): - if request.method == 'POST': - f = request.files['the_file'] - f.save('/var/www/uploads/uploaded_file.txt') - ... - -If you want to know how the file was named on the client before it was -uploaded to your application, you can access the -:attr:`~werkzeug.datastructures.FileStorage.filename` attribute. -However please keep in mind that this value can be forged -so never ever trust that value. If you want to use the filename -of the client to store the file on the server, pass it through the -:func:`~werkzeug.utils.secure_filename` function that -Werkzeug provides for you:: - - from werkzeug.utils import secure_filename - - @app.route('/upload', methods=['GET', 'POST']) - def upload_file(): - if request.method == 'POST': - file = request.files['the_file'] - file.save(f"/var/www/uploads/{secure_filename(file.filename)}") - ... - -For some better examples, see :doc:`patterns/fileuploads`. - -Cookies -``````` - -To access cookies you can use the :attr:`~flask.Request.cookies` -attribute. To set cookies you can use the -:attr:`~flask.Response.set_cookie` method of response objects. The -:attr:`~flask.Request.cookies` attribute of request objects is a -dictionary with all the cookies the client transmits. If you want to use -sessions, do not use the cookies directly but instead use the -:ref:`sessions` in Flask that add some security on top of cookies for you. - -Reading cookies:: - - from flask import request - - @app.route('/') - def index(): - username = request.cookies.get('username') - # use cookies.get(key) instead of cookies[key] to not get a - # KeyError if the cookie is missing. - -Storing cookies:: - - from flask import make_response - - @app.route('/') - def index(): - resp = make_response(render_template(...)) - resp.set_cookie('username', 'the username') - return resp - -Note that cookies are set on response objects. Since you normally -just return strings from the view functions Flask will convert them into -response objects for you. If you explicitly want to do that you can use -the :meth:`~flask.make_response` function and then modify it. - -Sometimes you might want to set a cookie at a point where the response -object does not exist yet. This is possible by utilizing the -:doc:`patterns/deferredcallbacks` pattern. - -For this also see :ref:`about-responses`. - -Redirects and Errors --------------------- - -To redirect a user to another endpoint, use the :func:`~flask.redirect` -function; to abort a request early with an error code, use the -:func:`~flask.abort` function:: - - from flask import abort, redirect, url_for - - @app.route('/') - def index(): - return redirect(url_for('login')) - - @app.route('/login') - def login(): - abort(401) - this_is_never_executed() - -This is a rather pointless example because a user will be redirected from -the index to a page they cannot access (401 means access denied) but it -shows how that works. - -By default a black and white error page is shown for each error code. If -you want to customize the error page, you can use the -:meth:`~flask.Flask.errorhandler` decorator:: - - from flask import render_template - - @app.errorhandler(404) - def page_not_found(error): - return render_template('page_not_found.html'), 404 - -Note the ``404`` after the :func:`~flask.render_template` call. This -tells Flask that the status code of that page should be 404 which means -not found. By default 200 is assumed which translates to: all went well. - -See :doc:`errorhandling` for more details. - -.. _about-responses: - -About Responses ---------------- - -The return value from a view function is automatically converted into -a response object for you. If the return value is a string it's -converted into a response object with the string as response body, a -``200 OK`` status code and a :mimetype:`text/html` mimetype. If the -return value is a dict or list, :func:`jsonify` is called to produce a -response. The logic that Flask applies to converting return values into -response objects is as follows: - -1. If a response object of the correct type is returned it's directly - returned from the view. -2. If it's a string, a response object is created with that data and - the default parameters. -3. If it's an iterator or generator returning strings or bytes, it is - treated as a streaming response. -4. If it's a dict or list, a response object is created using - :func:`~flask.json.jsonify`. -5. If a tuple is returned the items in the tuple can provide extra - information. Such tuples have to be in the form - ``(response, status)``, ``(response, headers)``, or - ``(response, status, headers)``. The ``status`` value will override - the status code and ``headers`` can be a list or dictionary of - additional header values. -6. If none of that works, Flask will assume the return value is a - valid WSGI application and convert that into a response object. - -If you want to get hold of the resulting response object inside the view -you can use the :func:`~flask.make_response` function. - -Imagine you have a view like this:: - - from flask import render_template - - @app.errorhandler(404) - def not_found(error): - return render_template('error.html'), 404 - -You just need to wrap the return expression with -:func:`~flask.make_response` and get the response object to modify it, then -return it:: - - from flask import make_response - - @app.errorhandler(404) - def not_found(error): - resp = make_response(render_template('error.html'), 404) - resp.headers['X-Something'] = 'A value' - return resp - - -APIs with JSON -`````````````` - -A common response format when writing an API is JSON. It's easy to get -started writing such an API with Flask. If you return a ``dict`` or -``list`` from a view, it will be converted to a JSON response. - -.. code-block:: python - - @app.route("/me") - def me_api(): - user = get_current_user() - return { - "username": user.username, - "theme": user.theme, - "image": url_for("user_image", filename=user.image), - } - - @app.route("/users") - def users_api(): - users = get_all_users() - return [user.to_json() for user in users] - -This is a shortcut to passing the data to the -:func:`~flask.json.jsonify` function, which will serialize any supported -JSON data type. That means that all the data in the dict or list must be -JSON serializable. - -For complex types such as database models, you'll want to use a -serialization library to convert the data to valid JSON types first. -There are many serialization libraries and Flask API extensions -maintained by the community that support more complex applications. - - -.. _sessions: - -Sessions --------- - -In addition to the request object there is also a second object called -:class:`~flask.session` which allows you to store information specific to a -user from one request to the next. This is implemented on top of cookies -for you and signs the cookies cryptographically. What this means is that -the user could look at the contents of your cookie but not modify it, -unless they know the secret key used for signing. - -In order to use sessions you have to set a secret key. Here is how -sessions work:: - - from flask import session - - # Set the secret key to some random bytes. Keep this really secret! - app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' - - @app.route('/') - def index(): - if 'username' in session: - return f'Logged in as {session["username"]}' - return 'You are not logged in' - - @app.route('/login', methods=['GET', 'POST']) - def login(): - if request.method == 'POST': - session['username'] = request.form['username'] - return redirect(url_for('index')) - return ''' -
-

-

-

- ''' - - @app.route('/logout') - def logout(): - # remove the username from the session if it's there - session.pop('username', None) - return redirect(url_for('index')) - -.. admonition:: How to generate good secret keys - - A secret key should be as random as possible. Your operating system has - ways to generate pretty random data based on a cryptographic random - generator. Use the following command to quickly generate a value for - :attr:`Flask.secret_key` (or :data:`SECRET_KEY`):: - - $ python -c 'import secrets; print(secrets.token_hex())' - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -A note on cookie-based sessions: Flask will take the values you put into the -session object and serialize them into a cookie. If you are finding some -values do not persist across requests, cookies are indeed enabled, and you are -not getting a clear error message, check the size of the cookie in your page -responses compared to the size supported by web browsers. - -Besides the default client-side based sessions, if you want to handle -sessions on the server-side instead, there are several -Flask extensions that support this. - -Message Flashing ----------------- - -Good applications and user interfaces are all about feedback. If the user -does not get enough feedback they will probably end up hating the -application. Flask provides a really simple way to give feedback to a -user with the flashing system. The flashing system basically makes it -possible to record a message at the end of a request and access it on the next -(and only the next) request. This is usually combined with a layout -template to expose the message. - -To flash a message use the :func:`~flask.flash` method, to get hold of the -messages you can use :func:`~flask.get_flashed_messages` which is also -available in the templates. See :doc:`patterns/flashing` for a full -example. - -Logging -------- - -.. versionadded:: 0.3 - -Sometimes you might be in a situation where you deal with data that -should be correct, but actually is not. For example you may have -some client-side code that sends an HTTP request to the server -but it's obviously malformed. This might be caused by a user tampering -with the data, or the client code failing. Most of the time it's okay -to reply with ``400 Bad Request`` in that situation, but sometimes -that won't do and the code has to continue working. - -You may still want to log that something fishy happened. This is where -loggers come in handy. As of Flask 0.3 a logger is preconfigured for you -to use. - -Here are some example log calls:: - - app.logger.debug('A value for debugging') - app.logger.warning('A warning occurred (%d apples)', 42) - app.logger.error('An error occurred') - -The attached :attr:`~flask.Flask.logger` is a standard logging -:class:`~logging.Logger`, so head over to the official :mod:`logging` -docs for more information. - -See :doc:`errorhandling`. - - -Hooking in WSGI Middleware --------------------------- - -To add WSGI middleware to your Flask application, wrap the application's -``wsgi_app`` attribute. For example, to apply Werkzeug's -:class:`~werkzeug.middleware.proxy_fix.ProxyFix` middleware for running -behind Nginx: - -.. code-block:: python - - from werkzeug.middleware.proxy_fix import ProxyFix - app.wsgi_app = ProxyFix(app.wsgi_app) - -Wrapping ``app.wsgi_app`` instead of ``app`` means that ``app`` still -points at your Flask application, not at the middleware, so you can -continue to use and configure ``app`` directly. - -Using Flask Extensions ----------------------- - -Extensions are packages that help you accomplish common tasks. For -example, Flask-SQLAlchemy provides SQLAlchemy support that makes it simple -and easy to use with Flask. - -For more on Flask extensions, see :doc:`extensions`. - -Deploying to a Web Server -------------------------- - -Ready to deploy your new Flask app? See :doc:`deploying/index`. diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst deleted file mode 100644 index 4f1846a3..00000000 --- a/docs/reqcontext.rst +++ /dev/null @@ -1,243 +0,0 @@ -.. currentmodule:: flask - -The Request Context -=================== - -The request context keeps track of the request-level data during a -request. Rather than passing the request object to each function that -runs during a request, the :data:`request` and :data:`session` proxies -are accessed instead. - -This is similar to :doc:`/appcontext`, which keeps track of the -application-level data independent of a request. A corresponding -application context is pushed when a request context is pushed. - - -Purpose of the Context ----------------------- - -When the :class:`Flask` application handles a request, it creates a -:class:`Request` object based on the environment it received from the -WSGI server. Because a *worker* (thread, process, or coroutine depending -on the server) handles only one request at a time, the request data can -be considered global to that worker during that request. Flask uses the -term *context local* for this. - -Flask automatically *pushes* a request context when handling a request. -View functions, error handlers, and other functions that run during a -request will have access to the :data:`request` proxy, which points to -the request object for the current request. - - -Lifetime of the Context ------------------------ - -When a Flask application begins handling a request, it pushes a request -context, which also pushes an :doc:`app context `. When the -request ends it pops the request context then the application context. - -The context is unique to each thread (or other worker type). -:data:`request` cannot be passed to another thread, the other thread has -a different context space and will not know about the request the parent -thread was pointing to. - -Context locals are implemented using Python's :mod:`contextvars` and -Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the -lifetime of context vars automatically, and local proxy wraps that -low-level interface to make the data easier to work with. - - -Manually Push a Context ------------------------ - -If you try to access :data:`request`, or anything that uses it, outside -a request context, you'll get this error message: - -.. code-block:: pytb - - RuntimeError: Working outside of request context. - - This typically means that you attempted to use functionality that - needed an active HTTP request. Consult the documentation on testing - for information about how to avoid this problem. - -This should typically only happen when testing code that expects an -active request. One option is to use the -:meth:`test client ` to simulate a full request. Or -you can use :meth:`~Flask.test_request_context` in a ``with`` block, and -everything that runs in the block will have access to :data:`request`, -populated with your test data. :: - - def generate_report(year): - format = request.args.get("format") - ... - - with app.test_request_context( - "/make_report/2017", query_string={"format": "short"} - ): - generate_report() - -If you see that error somewhere else in your code not related to -testing, it most likely indicates that you should move that code into a -view function. - -For information on how to use the request context from the interactive -Python shell, see :doc:`/shell`. - - -How the Context Works ---------------------- - -The :meth:`Flask.wsgi_app` method is called to handle each request. It -manages the contexts during the request. Internally, the request and -application contexts work like stacks. When contexts are pushed, the -proxies that depend on them are available and point at information from -the top item. - -When the request starts, a :class:`~ctx.RequestContext` is created and -pushed, which creates and pushes an :class:`~ctx.AppContext` first if -a context for that application is not already the top context. While -these contexts are pushed, the :data:`current_app`, :data:`g`, -:data:`request`, and :data:`session` proxies are available to the -original thread handling the request. - -Other contexts may be pushed to change the proxies during a request. -While this is not a common pattern, it can be used in advanced -applications to, for example, do internal redirects or chain different -applications together. - -After the request is dispatched and a response is generated and sent, -the request context is popped, which then pops the application context. -Immediately before they are popped, the :meth:`~Flask.teardown_request` -and :meth:`~Flask.teardown_appcontext` functions are executed. These -execute even if an unhandled exception occurred during dispatch. - - -.. _callbacks-and-errors: - -Callbacks and Errors --------------------- - -Flask dispatches a request in multiple stages which can affect the -request, response, and how errors are handled. The contexts are active -during all of these stages. - -A :class:`Blueprint` can add handlers for these events that are specific -to the blueprint. The handlers for a blueprint will run if the blueprint -owns the route that matches the request. - -#. Before each request, :meth:`~Flask.before_request` functions are - called. If one of these functions return a value, the other - functions are skipped. The return value is treated as the response - and the view function is not called. - -#. If the :meth:`~Flask.before_request` functions did not return a - response, the view function for the matched route is called and - returns a response. - -#. The return value of the view is converted into an actual response - object and passed to the :meth:`~Flask.after_request` - functions. Each function returns a modified or new response object. - -#. After the response is returned, the contexts are popped, which calls - the :meth:`~Flask.teardown_request` and - :meth:`~Flask.teardown_appcontext` functions. These functions are - called even if an unhandled exception was raised at any point above. - -If an exception is raised before the teardown functions, Flask tries to -match it with an :meth:`~Flask.errorhandler` function to handle the -exception and return a response. If no error handler is found, or the -handler itself raises an exception, Flask returns a generic -``500 Internal Server Error`` response. The teardown functions are still -called, and are passed the exception object. - -If debug mode is enabled, unhandled exceptions are not converted to a -``500`` response and instead are propagated to the WSGI server. This -allows the development server to present the interactive debugger with -the traceback. - - -Teardown Callbacks -~~~~~~~~~~~~~~~~~~ - -The teardown callbacks are independent of the request dispatch, and are -instead called by the contexts when they are popped. The functions are -called even if there is an unhandled exception during dispatch, and for -manually pushed contexts. This means there is no guarantee that any -other parts of the request dispatch have run first. Be sure to write -these functions in a way that does not depend on other callbacks and -will not fail. - -During testing, it can be useful to defer popping the contexts after the -request ends, so that their data can be accessed in the test function. -Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the -contexts until the ``with`` block exits. - -.. code-block:: python - - from flask import Flask, request - - app = Flask(__name__) - - @app.route('/') - def hello(): - print('during view') - return 'Hello, World!' - - @app.teardown_request - def show_teardown(exception): - print('after with block') - - with app.test_request_context(): - print('during with block') - - # teardown functions are called after the context with block exits - - with app.test_client() as client: - client.get('/') - # the contexts are not popped even though the request ended - print(request.path) - - # the contexts are popped and teardown functions are called after - # the client with block exits - -Signals -~~~~~~~ - -The following signals are sent: - -#. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions - are called. -#. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions - are called. -#. :data:`got_request_exception` is sent when an exception begins to be handled, but - before an :meth:`~Flask.errorhandler` is looked up or called. -#. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request` - functions are called. - - -.. _notes-on-proxies: - -Notes On Proxies ----------------- - -Some of the objects provided by Flask are proxies to other objects. The -proxies are accessed in the same way for each worker thread, but -point to the unique object bound to each worker behind the scenes as -described on this page. - -Most of the time you don't have to care about that, but there are some -exceptions where it is good to know that this object is actually a proxy: - -- The proxy objects cannot fake their type as the actual object types. - If you want to perform instance checks, you have to do that on the - object being proxied. -- The reference to the proxied object is needed in some situations, - such as sending :doc:`signals` or passing data to a background - thread. - -If you need to access the underlying object that is proxied, use the -:meth:`~werkzeug.local.LocalProxy._get_current_object` method:: - - app = current_app._get_current_object() - my_signal.send(app) diff --git a/docs/server.rst b/docs/server.rst deleted file mode 100644 index 11e976bc..00000000 --- a/docs/server.rst +++ /dev/null @@ -1,115 +0,0 @@ -.. currentmodule:: flask - -Development Server -================== - -Flask provides a ``run`` command to run the application with a development server. In -debug mode, this server provides an interactive debugger and will reload when code is -changed. - -.. warning:: - - Do not use the development server when deploying to production. It - is intended for use only during local development. It is not - designed to be particularly efficient, stable, or secure. - - See :doc:`/deploying/index` for deployment options. - -Command Line ------------- - -The ``flask run`` CLI command is the recommended way to run the development server. Use -the ``--app`` option to point to your application, and the ``--debug`` option to enable -debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -This enables debug mode, including the interactive debugger and reloader, and then -starts the server on http://localhost:5000/. Use ``flask run --help`` to see the -available options, and :doc:`/cli` for detailed instructions about configuring and using -the CLI. - - -.. _address-already-in-use: - -Address already in use -~~~~~~~~~~~~~~~~~~~~~~ - -If another program is already using port 5000, you'll see an ``OSError`` -when the server tries to start. It may have one of the following -messages: - -- ``OSError: [Errno 98] Address already in use`` -- ``OSError: [WinError 10013] An attempt was made to access a socket - in a way forbidden by its access permissions`` - -Either identify and stop the other program, or use -``flask run --port 5001`` to pick a different port. - -You can use ``netstat`` or ``lsof`` to identify what process id is using -a port, then use other operating system tools stop that process. The -following example shows that process id 6847 is using port 5000. - -.. tabs:: - - .. tab:: ``netstat`` (Linux) - - .. code-block:: text - - $ netstat -nlp | grep 5000 - tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python - - .. tab:: ``lsof`` (macOS / Linux) - - .. code-block:: text - - $ lsof -P -i :5000 - Python 6847 IPv4 TCP localhost:5000 (LISTEN) - - .. tab:: ``netstat`` (Windows) - - .. code-block:: text - - > netstat -ano | findstr 5000 - TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847 - -macOS Monterey and later automatically starts a service that uses port -5000. You can choose to disable this service instead of using a different port by -searching for "AirPlay Receiver" in System Preferences and toggling it off. - - -Deferred Errors on Reload -~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using the ``flask run`` command with the reloader, the server will -continue to run even if you introduce syntax errors or other -initialization errors into the code. Accessing the site will show the -interactive debugger for the error, rather than crashing the server. - -If a syntax error is already present when calling ``flask run``, it will -fail immediately and show the traceback rather than waiting until the -site is accessed. This is intended to make errors more visible initially -while still allowing the server to handle errors on reload. - - -In Code -------- - -The development server can also be started from Python with the :meth:`Flask.run` -method. This method takes arguments similar to the CLI options to control the server. -The main difference from the CLI command is that the server will crash if there are -errors when reloading. ``debug=True`` can be passed to enable debug mode. - -Place the call in a main block, otherwise it will interfere when trying to import and -run the application with a production server later. - -.. code-block:: python - - if __name__ == "__main__": - app.run(debug=True) - -.. code-block:: text - - $ python hello.py diff --git a/docs/shell.rst b/docs/shell.rst deleted file mode 100644 index 7e42e285..00000000 --- a/docs/shell.rst +++ /dev/null @@ -1,100 +0,0 @@ -Working with the Shell -====================== - -.. versionadded:: 0.3 - -One of the reasons everybody loves Python is the interactive shell. It -basically allows you to execute Python commands in real time and -immediately get results back. Flask itself does not come with an -interactive shell, because it does not require any specific setup upfront, -just import your application and start playing around. - -There are however some handy helpers to make playing around in the shell a -more pleasant experience. The main issue with interactive console -sessions is that you're not triggering a request like a browser does which -means that :data:`~flask.g`, :data:`~flask.request` and others are not -available. But the code you want to test might depend on them, so what -can you do? - -This is where some helper functions come in handy. Keep in mind however -that these functions are not only there for interactive shell usage, but -also for unit testing and other situations that require a faked request -context. - -Generally it's recommended that you read :doc:`reqcontext` first. - -Command Line Interface ----------------------- - -Starting with Flask 0.11 the recommended way to work with the shell is the -``flask shell`` command which does a lot of this automatically for you. -For instance the shell is automatically initialized with a loaded -application context. - -For more information see :doc:`/cli`. - -Creating a Request Context --------------------------- - -The easiest way to create a proper request context from the shell is by -using the :attr:`~flask.Flask.test_request_context` method which creates -us a :class:`~flask.ctx.RequestContext`: - ->>> ctx = app.test_request_context() - -Normally you would use the ``with`` statement to make this request object -active, but in the shell it's easier to use the -:meth:`~flask.ctx.RequestContext.push` and -:meth:`~flask.ctx.RequestContext.pop` methods by hand: - ->>> ctx.push() - -From that point onwards you can work with the request object until you -call `pop`: - ->>> ctx.pop() - -Firing Before/After Request ---------------------------- - -By just creating a request context, you still don't have run the code that -is normally run before a request. This might result in your database -being unavailable if you are connecting to the database in a -before-request callback or the current user not being stored on the -:data:`~flask.g` object etc. - -This however can easily be done yourself. Just call -:meth:`~flask.Flask.preprocess_request`: - ->>> ctx = app.test_request_context() ->>> ctx.push() ->>> app.preprocess_request() - -Keep in mind that the :meth:`~flask.Flask.preprocess_request` function -might return a response object, in that case just ignore it. - -To shutdown a request, you need to trick a bit before the after request -functions (triggered by :meth:`~flask.Flask.process_response`) operate on -a response object: - ->>> app.process_response(app.response_class()) - ->>> ctx.pop() - -The functions registered as :meth:`~flask.Flask.teardown_request` are -automatically called when the context is popped. So this is the perfect -place to automatically tear down resources that were needed by the request -context (such as database connections). - - -Further Improving the Shell Experience --------------------------------------- - -If you like the idea of experimenting in a shell, create yourself a module -with stuff you want to star import into your interactive session. There -you could also define some more helper methods for common things such as -initializing the database, dropping tables etc. - -Just put them into a module (like `shelltools`) and import from there: - ->>> from shelltools import * diff --git a/docs/signals.rst b/docs/signals.rst deleted file mode 100644 index 739bb0b5..00000000 --- a/docs/signals.rst +++ /dev/null @@ -1,167 +0,0 @@ -Signals -======= - -Signals are a lightweight way to notify subscribers of certain events during the -lifecycle of the application and each request. When an event occurs, it emits the -signal, which calls each subscriber. - -Signals are implemented by the `Blinker`_ library. See its documentation for detailed -information. Flask provides some built-in signals. Extensions may provide their own. - -Many signals mirror Flask's decorator-based callbacks with similar names. For example, -the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` -decorator. The advantage of signals over handlers is that they can be subscribed to -temporarily, and can't directly affect the application. This is useful for testing, -metrics, auditing, and more. For example, if you want to know what templates were -rendered at what parts of what requests, there is a signal that will notify you of that -information. - - -Core Signals ------------- - -See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` -page also describes the order that signals and decorators execute. - - -Subscribing to Signals ----------------------- - -To subscribe to a signal, you can use the -:meth:`~blinker.base.Signal.connect` method of a signal. The first -argument is the function that should be called when the signal is emitted, -the optional second argument specifies a sender. To unsubscribe from a -signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. - -For all core Flask signals, the sender is the application that issued the -signal. When you subscribe to a signal, be sure to also provide a sender -unless you really want to listen for signals from all applications. This is -especially true if you are developing an extension. - -For example, here is a helper context manager that can be used in a unit test -to determine which templates were rendered and what variables were passed -to the template:: - - from flask import template_rendered - from contextlib import contextmanager - - @contextmanager - def captured_templates(app): - recorded = [] - def record(sender, template, context, **extra): - recorded.append((template, context)) - template_rendered.connect(record, app) - try: - yield recorded - finally: - template_rendered.disconnect(record, app) - -This can now easily be paired with a test client:: - - with captured_templates(app) as templates: - rv = app.test_client().get('/') - assert rv.status_code == 200 - assert len(templates) == 1 - template, context = templates[0] - assert template.name == 'index.html' - assert len(context['items']) == 10 - -Make sure to subscribe with an extra ``**extra`` argument so that your -calls don't fail if Flask introduces new arguments to the signals. - -All the template rendering in the code issued by the application `app` -in the body of the ``with`` block will now be recorded in the `templates` -variable. Whenever a template is rendered, the template object as well as -context are appended to it. - -Additionally there is a convenient helper method -(:meth:`~blinker.base.Signal.connected_to`) that allows you to -temporarily subscribe a function to a signal with a context manager on -its own. Because the return value of the context manager cannot be -specified that way, you have to pass the list in as an argument:: - - from flask import template_rendered - - def captured_templates(app, recorded, **extra): - def record(sender, template, context): - recorded.append((template, context)) - return template_rendered.connected_to(record, app) - -The example above would then look like this:: - - templates = [] - with captured_templates(app, templates, **extra): - ... - template, context = templates[0] - -Creating Signals ----------------- - -If you want to use signals in your own application, you can use the -blinker library directly. The most common use case are named signals in a -custom :class:`~blinker.base.Namespace`. This is what is recommended -most of the time:: - - from blinker import Namespace - my_signals = Namespace() - -Now you can create new signals like this:: - - model_saved = my_signals.signal('model-saved') - -The name for the signal here makes it unique and also simplifies -debugging. You can access the name of the signal with the -:attr:`~blinker.base.NamedSignal.name` attribute. - -.. _signals-sending: - -Sending Signals ---------------- - -If you want to emit a signal, you can do so by calling the -:meth:`~blinker.base.Signal.send` method. It accepts a sender as first -argument and optionally some keyword arguments that are forwarded to the -signal subscribers:: - - class Model(object): - ... - - def save(self): - model_saved.send(self) - -Try to always pick a good sender. If you have a class that is emitting a -signal, pass ``self`` as sender. If you are emitting a signal from a random -function, you can pass ``current_app._get_current_object()`` as sender. - -.. admonition:: Passing Proxies as Senders - - Never pass :data:`~flask.current_app` as sender to a signal. Use - ``current_app._get_current_object()`` instead. The reason for this is - that :data:`~flask.current_app` is a proxy and not the real application - object. - - -Signals and Flask's Request Context ------------------------------------ - -Signals fully support :doc:`reqcontext` when receiving signals. -Context-local variables are consistently available between -:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can -rely on :class:`flask.g` and others as needed. Note the limitations described -in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. - - -Decorator Based Signal Subscriptions ------------------------------------- - -You can also easily subscribe to signals by using the -:meth:`~blinker.base.NamedSignal.connect_via` decorator:: - - from flask import template_rendered - - @template_rendered.connect_via(app) - def when_template_rendered(sender, template, context, **extra): - print(f'Template {template.name} is rendered with {context}') - - -.. _blinker: https://pypi.org/project/blinker/ diff --git a/docs/templating.rst b/docs/templating.rst deleted file mode 100644 index 23cfee4c..00000000 --- a/docs/templating.rst +++ /dev/null @@ -1,229 +0,0 @@ -Templates -========= - -Flask leverages Jinja2 as its template engine. You are obviously free to use -a different template engine, but you still have to install Jinja2 to run -Flask itself. This requirement is necessary to enable rich extensions. -An extension can depend on Jinja2 being present. - -This section only gives a very quick introduction into how Jinja2 -is integrated into Flask. If you want information on the template -engine's syntax itself, head over to the official `Jinja2 Template -Documentation `_ for -more information. - -Jinja Setup ------------ - -Unless customized, Jinja2 is configured by Flask as follows: - -- autoescaping is enabled for all templates ending in ``.html``, - ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using - :func:`~flask.templating.render_template`. -- autoescaping is enabled for all strings when using - :func:`~flask.templating.render_template_string`. -- a template has the ability to opt in/out autoescaping with the - ``{% autoescape %}`` tag. -- Flask inserts a couple of global functions and helpers into the - Jinja2 context, additionally to the values that are present by - default. - -Standard Context ----------------- - -The following global variables are available within Jinja2 templates -by default: - -.. data:: config - :noindex: - - The current configuration object (:data:`flask.Flask.config`) - - .. versionadded:: 0.6 - - .. versionchanged:: 0.10 - This is now always available, even in imported templates. - -.. data:: request - :noindex: - - The current request object (:class:`flask.request`). This variable is - unavailable if the template was rendered without an active request - context. - -.. data:: session - :noindex: - - The current session object (:class:`flask.session`). This variable - is unavailable if the template was rendered without an active request - context. - -.. data:: g - :noindex: - - The request-bound object for global variables (:data:`flask.g`). This - variable is unavailable if the template was rendered without an active - request context. - -.. function:: url_for - :noindex: - - The :func:`flask.url_for` function. - -.. function:: get_flashed_messages - :noindex: - - The :func:`flask.get_flashed_messages` function. - -.. admonition:: The Jinja Context Behavior - - These variables are added to the context of variables, they are not - global variables. The difference is that by default these will not - show up in the context of imported templates. This is partially caused - by performance considerations, partially to keep things explicit. - - What does this mean for you? If you have a macro you want to import, - that needs to access the request object you have two possibilities: - - 1. you explicitly pass the request to the macro as parameter, or - the attribute of the request object you are interested in. - 2. you import the macro "with context". - - Importing with context looks like this: - - .. sourcecode:: jinja - - {% from '_helpers.html' import my_macro with context %} - - -Controlling Autoescaping ------------------------- - -Autoescaping is the concept of automatically escaping special characters -for you. Special characters in the sense of HTML (or XML, and thus XHTML) -are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters -carry specific meanings in documents on their own you have to replace them -by so called "entities" if you want to use them for text. Not doing so -would not only cause user frustration by the inability to use these -characters in text, but can also lead to security problems. (see -:ref:`security-xss`) - -Sometimes however you will need to disable autoescaping in templates. -This can be the case if you want to explicitly inject HTML into pages, for -example if they come from a system that generates secure HTML like a -markdown to HTML converter. - -There are three ways to accomplish that: - -- In the Python code, wrap the HTML string in a :class:`~markupsafe.Markup` - object before passing it to the template. This is in general the - recommended way. -- Inside the template, use the ``|safe`` filter to explicitly mark a - string as safe HTML (``{{ myvariable|safe }}``) -- Temporarily disable the autoescape system altogether. - -To disable the autoescape system in templates, you can use the ``{% -autoescape %}`` block: - -.. sourcecode:: html+jinja - - {% autoescape false %} -

autoescaping is disabled here -

{{ will_not_be_escaped }} - {% endautoescape %} - -Whenever you do this, please be very cautious about the variables you are -using in this block. - -.. _registering-filters: - -Registering Filters -------------------- - -If you want to register your own filters in Jinja2 you have two ways to do -that. You can either put them by hand into the -:attr:`~flask.Flask.jinja_env` of the application or use the -:meth:`~flask.Flask.template_filter` decorator. - -The two following examples work the same and both reverse an object:: - - @app.template_filter('reverse') - def reverse_filter(s): - return s[::-1] - - def reverse_filter(s): - return s[::-1] - app.jinja_env.filters['reverse'] = reverse_filter - -In case of the decorator the argument is optional if you want to use the -function name as name of the filter. Once registered, you can use the filter -in your templates in the same way as Jinja2's builtin filters, for example if -you have a Python list in context called `mylist`:: - - {% for x in mylist | reverse %} - {% endfor %} - - -Context Processors ------------------- - -To inject new variables automatically into the context of a template, -context processors exist in Flask. Context processors run before the -template is rendered and have the ability to inject new values into the -template context. A context processor is a function that returns a -dictionary. The keys and values of this dictionary are then merged with -the template context, for all templates in the app:: - - @app.context_processor - def inject_user(): - return dict(user=g.user) - -The context processor above makes a variable called `user` available in -the template with the value of `g.user`. This example is not very -interesting because `g` is available in templates anyways, but it gives an -idea how this works. - -Variables are not limited to values; a context processor can also make -functions available to templates (since Python allows passing around -functions):: - - @app.context_processor - def utility_processor(): - def format_price(amount, currency="€"): - return f"{amount:.2f}{currency}" - return dict(format_price=format_price) - -The context processor above makes the `format_price` function available to all -templates:: - - {{ format_price(0.33) }} - -You could also build `format_price` as a template filter (see -:ref:`registering-filters`), but this demonstrates how to pass functions in a -context processor. - -Streaming ---------- - -It can be useful to not render the whole template as one complete -string, instead render it as a stream, yielding smaller incremental -strings. This can be used for streaming HTML in chunks to speed up -initial page load, or to save memory when rendering a very large -template. - -The Jinja2 template engine supports rendering a template piece -by piece, returning an iterator of strings. Flask provides the -:func:`~flask.stream_template` and :func:`~flask.stream_template_string` -functions to make this easier to use. - -.. code-block:: python - - from flask import stream_template - - @app.get("/timeline") - def timeline(): - return stream_template("timeline.html") - -These functions automatically apply the -:func:`~flask.stream_with_context` wrapper if a request is active, so -that it remains available in the template. diff --git a/docs/testing.rst b/docs/testing.rst deleted file mode 100644 index b1d52f9a..00000000 --- a/docs/testing.rst +++ /dev/null @@ -1,319 +0,0 @@ -Testing Flask Applications -========================== - -Flask provides utilities for testing an application. This documentation -goes over techniques for working with different parts of the application -in tests. - -We will use the `pytest`_ framework to set up and run our tests. - -.. code-block:: text - - $ pip install pytest - -.. _pytest: https://docs.pytest.org/ - -The :doc:`tutorial ` goes over how to write tests for -100% coverage of the sample Flaskr blog application. See -:doc:`the tutorial on tests ` for a detailed -explanation of specific tests for an application. - - -Identifying Tests ------------------ - -Tests are typically located in the ``tests`` folder. Tests are functions -that start with ``test_``, in Python modules that start with ``test_``. -Tests can also be further grouped in classes that start with ``Test``. - -It can be difficult to know what to test. Generally, try to test the -code that you write, not the code of libraries that you use, since they -are already tested. Try to extract complex behaviors as separate -functions to test individually. - - -Fixtures --------- - -Pytest *fixtures* allow writing pieces of code that are reusable across -tests. A simple fixture returns a value, but a fixture can also do -setup, yield a value, then do teardown. Fixtures for the application, -test client, and CLI runner are shown below, they can be placed in -``tests/conftest.py``. - -If you're using an -:doc:`application factory `, define an ``app`` -fixture to create and configure an app instance. You can add code before -and after the ``yield`` to set up and tear down other resources, such as -creating and clearing a database. - -If you're not using a factory, you already have an app object you can -import and configure directly. You can still use an ``app`` fixture to -set up and tear down resources. - -.. code-block:: python - - import pytest - from my_project import create_app - - @pytest.fixture() - def app(): - app = create_app() - app.config.update({ - "TESTING": True, - }) - - # other setup can go here - - yield app - - # clean up / reset resources here - - - @pytest.fixture() - def client(app): - return app.test_client() - - - @pytest.fixture() - def runner(app): - return app.test_cli_runner() - - -Sending Requests with the Test Client -------------------------------------- - -The test client makes requests to the application without running a live -server. Flask's client extends -:doc:`Werkzeug's client `, see those docs for additional -information. - -The ``client`` has methods that match the common HTTP request methods, -such as ``client.get()`` and ``client.post()``. They take many arguments -for building the request; you can find the full documentation in -:class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, -``query_string``, ``headers``, and ``data`` or ``json``. - -To make a request, call the method the request should use with the path -to the route to test. A :class:`~werkzeug.test.TestResponse` is returned -to examine the response data. It has all the usual properties of a -response object. You'll usually look at ``response.data``, which is the -bytes returned by the view. If you want to use text, Werkzeug 2.1 -provides ``response.text``, or use ``response.get_data(as_text=True)``. - -.. code-block:: python - - def test_request_example(client): - response = client.get("/posts") - assert b"

Hello, World!

" in response.data - - -Pass a dict ``query_string={"key": "value", ...}`` to set arguments in -the query string (after the ``?`` in the URL). Pass a dict -``headers={}`` to set request headers. - -To send a request body in a POST or PUT request, pass a value to -``data``. If raw bytes are passed, that exact body is used. Usually, -you'll pass a dict to set form data. - - -Form Data -~~~~~~~~~ - -To send form data, pass a dict to ``data``. The ``Content-Type`` header -will be set to ``multipart/form-data`` or -``application/x-www-form-urlencoded`` automatically. - -If a value is a file object opened for reading bytes (``"rb"`` mode), it -will be treated as an uploaded file. To change the detected filename and -content type, pass a ``(file, filename, content_type)`` tuple. File -objects will be closed after making the request, so they do not need to -use the usual ``with open() as f:`` pattern. - -It can be useful to store files in a ``tests/resources`` folder, then -use ``pathlib.Path`` to get files relative to the current test file. - -.. code-block:: python - - from pathlib import Path - - # get the resources folder in the tests folder - resources = Path(__file__).parent / "resources" - - def test_edit_user(client): - response = client.post("/user/2/edit", data={ - "name": "Flask", - "theme": "dark", - "picture": (resources / "picture.png").open("rb"), - }) - assert response.status_code == 200 - - -JSON Data -~~~~~~~~~ - -To send JSON data, pass an object to ``json``. The ``Content-Type`` -header will be set to ``application/json`` automatically. - -Similarly, if the response contains JSON data, the ``response.json`` -attribute will contain the deserialized object. - -.. code-block:: python - - def test_json_data(client): - response = client.post("/graphql", json={ - "query": """ - query User($id: String!) { - user(id: $id) { - name - theme - picture_url - } - } - """, - variables={"id": 2}, - }) - assert response.json["data"]["user"]["name"] == "Flask" - - -Following Redirects -------------------- - -By default, the client does not make additional requests if the response -is a redirect. By passing ``follow_redirects=True`` to a request method, -the client will continue to make requests until a non-redirect response -is returned. - -:attr:`TestResponse.history ` is -a tuple of the responses that led up to the final response. Each -response has a :attr:`~werkzeug.test.TestResponse.request` attribute -which records the request that produced that response. - -.. code-block:: python - - def test_logout_redirect(client): - response = client.get("/logout", follow_redirects=True) - # Check that there was one redirect response. - assert len(response.history) == 1 - # Check that the second request was to the index page. - assert response.request.path == "/index" - - -Accessing and Modifying the Session ------------------------------------ - -To access Flask's context variables, mainly -:data:`~flask.session`, use the client in a ``with`` statement. -The app and request context will remain active *after* making a request, -until the ``with`` block ends. - -.. code-block:: python - - from flask import session - - def test_access_session(client): - with client: - client.post("/auth/login", data={"username": "flask"}) - # session is still accessible - assert session["user_id"] == 1 - - # session is no longer accessible - -If you want to access or set a value in the session *before* making a -request, use the client's -:meth:`~flask.testing.FlaskClient.session_transaction` method in a -``with`` statement. It returns a session object, and will save the -session once the block ends. - -.. code-block:: python - - from flask import session - - def test_modify_session(client): - with client.session_transaction() as session: - # set a user id without going through the login route - session["user_id"] = 1 - - # session is saved now - - response = client.get("/users/me") - assert response.json["username"] == "flask" - - -.. _testing-cli: - -Running Commands with the CLI Runner ------------------------------------- - -Flask provides :meth:`~flask.Flask.test_cli_runner` to create a -:class:`~flask.testing.FlaskCliRunner`, which runs CLI commands in -isolation and captures the output in a :class:`~click.testing.Result` -object. Flask's runner extends :doc:`Click's runner `, -see those docs for additional information. - -Use the runner's :meth:`~flask.testing.FlaskCliRunner.invoke` method to -call commands in the same way they would be called with the ``flask`` -command from the command line. - -.. code-block:: python - - import click - - @app.cli.command("hello") - @click.option("--name", default="World") - def hello_command(name): - click.echo(f"Hello, {name}!") - - def test_hello_command(runner): - result = runner.invoke(args="hello") - assert "World" in result.output - - result = runner.invoke(args=["hello", "--name", "Flask"]) - assert "Flask" in result.output - - -Tests that depend on an Active Context --------------------------------------- - -You may have functions that are called from views or commands, that -expect an active :doc:`application context ` or -:doc:`request context ` because they access ``request``, -``session``, or ``current_app``. Rather than testing them by making a -request or invoking the command, you can create and activate a context -directly. - -Use ``with app.app_context()`` to push an application context. For -example, database extensions usually require an active app context to -make queries. - -.. code-block:: python - - def test_db_post_model(app): - with app.app_context(): - post = db.session.query(Post).get(1) - -Use ``with app.test_request_context()`` to push a request context. It -takes the same arguments as the test client's request methods. - -.. code-block:: python - - def test_validate_user_edit(app): - with app.test_request_context( - "/user/2/edit", method="POST", data={"name": ""} - ): - # call a function that accesses `request` - messages = validate_edit_user() - - assert messages["name"][0] == "Name cannot be empty." - -Creating a test request context doesn't run any of the Flask dispatching -code, so ``before_request`` functions are not called. If you need to -call these, usually it's better to make a full request instead. However, -it's possible to call them manually. - -.. code-block:: python - - def test_auth_token(app): - with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}): - app.preprocess_request() - assert g.user.name == "Flask" diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst deleted file mode 100644 index b06329ea..00000000 --- a/docs/tutorial/blog.rst +++ /dev/null @@ -1,336 +0,0 @@ -.. currentmodule:: flask - -Blog Blueprint -============== - -You'll use the same techniques you learned about when writing the -authentication blueprint to write the blog blueprint. The blog should -list all posts, allow logged in users to create posts, and allow the -author of a post to edit or delete it. - -As you implement each view, keep the development server running. As you -save your changes, try going to the URL in your browser and testing them -out. - -The Blueprint -------------- - -Define the blueprint and register it in the application factory. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - from flask import ( - Blueprint, flash, g, redirect, render_template, request, url_for - ) - from werkzeug.exceptions import abort - - from flaskr.auth import login_required - from flaskr.db import get_db - - bp = Blueprint('blog', __name__) - -Import and register the blueprint from the factory using -:meth:`app.register_blueprint() `. Place the -new code at the end of the factory function before returning the app. - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - def create_app(): - app = ... - # existing code omitted - - from . import blog - app.register_blueprint(blog.bp) - app.add_url_rule('/', endpoint='index') - - return app - - -Unlike the auth blueprint, the blog blueprint does not have a -``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` -view at ``/create``, and so on. The blog is the main feature of Flaskr, -so it makes sense that the blog index will be the main index. - -However, the endpoint for the ``index`` view defined below will be -``blog.index``. Some of the authentication views referred to a plain -``index`` endpoint. :meth:`app.add_url_rule() ` -associates the endpoint name ``'index'`` with the ``/`` url so that -``url_for('index')`` or ``url_for('blog.index')`` will both work, -generating the same ``/`` URL either way. - -In another application you might give the blog blueprint a -``url_prefix`` and define a separate ``index`` view in the application -factory, similar to the ``hello`` view. Then the ``index`` and -``blog.index`` endpoints and URLs would be different. - - -Index ------ - -The index will show all of the posts, most recent first. A ``JOIN`` is -used so that the author information from the ``user`` table is -available in the result. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('/') - def index(): - db = get_db() - posts = db.execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' ORDER BY created DESC' - ).fetchall() - return render_template('blog/index.html', posts=posts) - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/index.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Posts{% endblock %}

- {% if g.user %} - New - {% endif %} - {% endblock %} - - {% block content %} - {% for post in posts %} -
-
-
-

{{ post['title'] }}

-
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
-
- {% if g.user['id'] == post['author_id'] %} - Edit - {% endif %} -
-

{{ post['body'] }}

-
- {% if not loop.last %} -
- {% endif %} - {% endfor %} - {% endblock %} - -When a user is logged in, the ``header`` block adds a link to the -``create`` view. When the user is the author of a post, they'll see an -"Edit" link to the ``update`` view for that post. ``loop.last`` is a -special variable available inside `Jinja for loops`_. It's used to -display a line after each post except the last one, to visually separate -them. - -.. _Jinja for loops: https://jinja.palletsprojects.com/templates/#for - - -Create ------- - -The ``create`` view works the same as the auth ``register`` view. Either -the form is displayed, or the posted data is validated and the post is -added to the database or an error is shown. - -The ``login_required`` decorator you wrote earlier is used on the blog -views. A user must be logged in to visit these views, otherwise they -will be redirected to the login page. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('/create', methods=('GET', 'POST')) - @login_required - def create(): - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'INSERT INTO post (title, body, author_id)' - ' VALUES (?, ?, ?)', - (title, body, g.user['id']) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/create.html') - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/create.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}New Post{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
- {% endblock %} - - -Update ------- - -Both the ``update`` and ``delete`` views will need to fetch a ``post`` -by ``id`` and check if the author matches the logged in user. To avoid -duplicating code, you can write a function to get the ``post`` and call -it from each view. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - def get_post(id, check_author=True): - post = get_db().execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' WHERE p.id = ?', - (id,) - ).fetchone() - - if post is None: - abort(404, f"Post id {id} doesn't exist.") - - if check_author and post['author_id'] != g.user['id']: - abort(403) - - return post - -:func:`abort` will raise a special exception that returns an HTTP status -code. It takes an optional message to show with the error, otherwise a -default message is used. ``404`` means "Not Found", and ``403`` means -"Forbidden". (``401`` means "Unauthorized", but you redirect to the -login page instead of returning that status.) - -The ``check_author`` argument is defined so that the function can be -used to get a ``post`` without checking the author. This would be useful -if you wrote a view to show an individual post on a page, where the user -doesn't matter because they're not modifying the post. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('//update', methods=('GET', 'POST')) - @login_required - def update(id): - post = get_post(id) - - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'UPDATE post SET title = ?, body = ?' - ' WHERE id = ?', - (title, body, id) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/update.html', post=post) - -Unlike the views you've written so far, the ``update`` function takes -an argument, ``id``. That corresponds to the ```` in the route. -A real URL will look like ``/1/update``. Flask will capture the ``1``, -ensure it's an :class:`int`, and pass it as the ``id`` argument. If you -don't specify ``int:`` and instead do ````, it will be a string. -To generate a URL to the update page, :func:`url_for` needs to be passed -the ``id`` so it knows what to fill in: -``url_for('blog.update', id=post['id'])``. This is also in the -``index.html`` file above. - -The ``create`` and ``update`` views look very similar. The main -difference is that the ``update`` view uses a ``post`` object and an -``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, -you could use one view and template for both actions, but for the -tutorial it's clearer to keep them separate. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/update.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
-
-
- -
- {% endblock %} - -This template has two forms. The first posts the edited data to the -current page (``//update``). The other form contains only a button -and specifies an ``action`` attribute that posts to the delete view -instead. The button uses some JavaScript to show a confirmation dialog -before submitting. - -The pattern ``{{ request.form['title'] or post['title'] }}`` is used to -choose what data appears in the form. When the form hasn't been -submitted, the original ``post`` data appears, but if invalid form data -was posted you want to display that so the user can fix the error, so -``request.form`` is used instead. :data:`request` is another variable -that's automatically available in templates. - - -Delete ------- - -The delete view doesn't have its own template, the delete button is part -of ``update.html`` and posts to the ``//delete`` URL. Since there -is no template, it will only handle the ``POST`` method and then redirect -to the ``index`` view. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('//delete', methods=('POST',)) - @login_required - def delete(id): - get_post(id) - db = get_db() - db.execute('DELETE FROM post WHERE id = ?', (id,)) - db.commit() - return redirect(url_for('blog.index')) - -Congratulations, you've now finished writing your application! Take some -time to try out everything in the browser. However, there's still more -to do before the project is complete. - -Continue to :doc:`install`. diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst deleted file mode 100644 index 934f6008..00000000 --- a/docs/tutorial/database.rst +++ /dev/null @@ -1,209 +0,0 @@ -.. currentmodule:: flask - -Define and Access the Database -============================== - -The application will use a `SQLite`_ database to store users and posts. -Python comes with built-in support for SQLite in the :mod:`sqlite3` -module. - -SQLite is convenient because it doesn't require setting up a separate -database server and is built-in to Python. However, if concurrent -requests try to write to the database at the same time, they will slow -down as each write happens sequentially. Small applications won't notice -this. Once you become big, you may want to switch to a different -database. - -The tutorial doesn't go into detail about SQL. If you are not familiar -with it, the SQLite docs describe the `language`_. - -.. _SQLite: https://sqlite.org/about.html -.. _language: https://sqlite.org/lang.html - - -Connect to the Database ------------------------ - -The first thing to do when working with a SQLite database (and most -other Python database libraries) is to create a connection to it. Any -queries and operations are performed using the connection, which is -closed after the work is finished. - -In web applications this connection is typically tied to the request. It -is created at some point when handling a request, and closed before the -response is sent. - -.. code-block:: python - :caption: ``flaskr/db.py`` - - import sqlite3 - - import click - from flask import current_app, g - - - def get_db(): - if 'db' not in g: - g.db = sqlite3.connect( - current_app.config['DATABASE'], - detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - - return g.db - - - def close_db(e=None): - db = g.pop('db', None) - - if db is not None: - db.close() - -:data:`g` is a special object that is unique for each request. It is -used to store data that might be accessed by multiple functions during -the request. The connection is stored and reused instead of creating a -new connection if ``get_db`` is called a second time in the same -request. - -:data:`current_app` is another special object that points to the Flask -application handling the request. Since you used an application factory, -there is no application object when writing the rest of your code. -``get_db`` will be called when the application has been created and is -handling a request, so :data:`current_app` can be used. - -:func:`sqlite3.connect` establishes a connection to the file pointed at -by the ``DATABASE`` configuration key. This file doesn't have to exist -yet, and won't until you initialize the database later. - -:class:`sqlite3.Row` tells the connection to return rows that behave -like dicts. This allows accessing the columns by name. - -``close_db`` checks if a connection was created by checking if ``g.db`` -was set. If the connection exists, it is closed. Further down you will -tell your application about the ``close_db`` function in the application -factory so that it is called after each request. - - -Create the Tables ------------------ - -In SQLite, data is stored in *tables* and *columns*. These need to be -created before you can store and retrieve data. Flaskr will store users -in the ``user`` table, and posts in the ``post`` table. Create a file -with the SQL commands needed to create empty tables: - -.. code-block:: sql - :caption: ``flaskr/schema.sql`` - - DROP TABLE IF EXISTS user; - DROP TABLE IF EXISTS post; - - CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL - ); - - CREATE TABLE post ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - FOREIGN KEY (author_id) REFERENCES user (id) - ); - -Add the Python functions that will run these SQL commands to the -``db.py`` file: - -.. code-block:: python - :caption: ``flaskr/db.py`` - - def init_db(): - db = get_db() - - with current_app.open_resource('schema.sql') as f: - db.executescript(f.read().decode('utf8')) - - - @click.command('init-db') - def init_db_command(): - """Clear the existing data and create new tables.""" - init_db() - click.echo('Initialized the database.') - -:meth:`open_resource() ` opens a file relative to -the ``flaskr`` package, which is useful since you won't necessarily know -where that location is when deploying the application later. ``get_db`` -returns a database connection, which is used to execute the commands -read from the file. - -:func:`click.command` defines a command line command called ``init-db`` -that calls the ``init_db`` function and shows a success message to the -user. You can read :doc:`/cli` to learn more about writing commands. - - -Register with the Application ------------------------------ - -The ``close_db`` and ``init_db_command`` functions need to be registered -with the application instance; otherwise, they won't be used by the -application. However, since you're using a factory function, that -instance isn't available when writing the functions. Instead, write a -function that takes an application and does the registration. - -.. code-block:: python - :caption: ``flaskr/db.py`` - - def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) - -:meth:`app.teardown_appcontext() ` tells -Flask to call that function when cleaning up after returning the -response. - -:meth:`app.cli.add_command() ` adds a new -command that can be called with the ``flask`` command. - -Import and call this function from the factory. Place the new code at -the end of the factory function before returning the app. - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - def create_app(): - app = ... - # existing code omitted - - from . import db - db.init_app(app) - - return app - - -Initialize the Database File ----------------------------- - -Now that ``init-db`` has been registered with the app, it can be called -using the ``flask`` command, similar to the ``run`` command from the -previous page. - -.. note:: - - If you're still running the server from the previous page, you can - either stop the server, or run this command in a new terminal. If - you use a new terminal, remember to change to your project directory - and activate the env as described in :doc:`/installation`. - -Run the ``init-db`` command: - -.. code-block:: none - - $ flask --app flaskr init-db - Initialized the database. - -There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in -your project. - -Continue to :doc:`views`. diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst deleted file mode 100644 index eb3a53ac..00000000 --- a/docs/tutorial/deploy.rst +++ /dev/null @@ -1,111 +0,0 @@ -Deploy to Production -==================== - -This part of the tutorial assumes you have a server that you want to -deploy your application to. It gives an overview of how to create the -distribution file and install it, but won't go into specifics about -what server or software to use. You can set up a new environment on your -development computer to try out the instructions below, but probably -shouldn't use it for hosting a real public application. See -:doc:`/deploying/index` for a list of many different ways to host your -application. - - -Build and Install ------------------ - -When you want to deploy your application elsewhere, you build a *wheel* -(``.whl``) file. Install and use the ``build`` tool to do this. - -.. code-block:: none - - $ pip install build - $ python -m build --wheel - -You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The -file name is in the format of {project name}-{version}-{python tag} --{abi tag}-{platform tag}. - -Copy this file to another machine, -:ref:`set up a new virtualenv `, then install the -file with ``pip``. - -.. code-block:: none - - $ pip install flaskr-1.0.0-py3-none-any.whl - -Pip will install your project along with its dependencies. - -Since this is a different machine, you need to run ``init-db`` again to -create the database in the instance folder. - - .. code-block:: text - - $ flask --app flaskr init-db - -When Flask detects that it's installed (not in editable mode), it uses -a different directory for the instance folder. You can find it at -``.venv/var/flaskr-instance`` instead. - - -Configure the Secret Key ------------------------- - -In the beginning of the tutorial that you gave a default value for -:data:`SECRET_KEY`. This should be changed to some random bytes in -production. Otherwise, attackers could use the public ``'dev'`` key to -modify the session cookie, or anything else that uses the secret key. - -You can use the following command to output a random secret key: - -.. code-block:: none - - $ python -c 'import secrets; print(secrets.token_hex())' - - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -Create the ``config.py`` file in the instance folder, which the factory -will read from if it exists. Copy the generated value into it. - -.. code-block:: python - :caption: ``.venv/var/flaskr-instance/config.py`` - - SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -You can also set any other necessary configuration here, although -``SECRET_KEY`` is the only one needed for Flaskr. - - -Run with a Production Server ----------------------------- - -When running publicly rather than in development, you should not use the -built-in development server (``flask run``). The development server is -provided by Werkzeug for convenience, but is not designed to be -particularly efficient, stable, or secure. - -Instead, use a production WSGI server. For example, to use `Waitress`_, -first install it in the virtual environment: - -.. code-block:: none - - $ pip install waitress - -You need to tell Waitress about your application, but it doesn't use -``--app`` like ``flask run`` does. You need to tell it to import and -call the application factory to get an application object. - -.. code-block:: none - - $ waitress-serve --call 'flaskr:create_app' - - Serving on http://0.0.0.0:8080 - -See :doc:`/deploying/index` for a list of many different ways to host -your application. Waitress is just an example, chosen for the tutorial -because it supports both Windows and Linux. There are many more WSGI -servers and deployment options that you may choose for your project. - -.. _Waitress: https://docs.pylonsproject.org/projects/waitress/en/stable/ - -Continue to :doc:`next`. diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst deleted file mode 100644 index 39febd13..00000000 --- a/docs/tutorial/factory.rst +++ /dev/null @@ -1,162 +0,0 @@ -.. currentmodule:: flask - -Application Setup -================= - -A Flask application is an instance of the :class:`Flask` class. -Everything about the application, such as configuration and URLs, will -be registered with this class. - -The most straightforward way to create a Flask application is to create -a global :class:`Flask` instance directly at the top of your code, like -how the "Hello, World!" example did on the previous page. While this is -simple and useful in some cases, it can cause some tricky issues as the -project grows. - -Instead of creating a :class:`Flask` instance globally, you will create -it inside a function. This function is known as the *application -factory*. Any configuration, registration, and other setup the -application needs will happen inside the function, then the application -will be returned. - - -The Application Factory ------------------------ - -It's time to start coding! Create the ``flaskr`` directory and add the -``__init__.py`` file. The ``__init__.py`` serves double duty: it will -contain the application factory, and it tells Python that the ``flaskr`` -directory should be treated as a package. - -.. code-block:: none - - $ mkdir flaskr - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - import os - - from flask import Flask - - - def create_app(test_config=None): - # create and configure the app - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - SECRET_KEY='dev', - DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), - ) - - if test_config is None: - # load the instance config, if it exists, when not testing - app.config.from_pyfile('config.py', silent=True) - else: - # load the test config if passed in - app.config.from_mapping(test_config) - - # ensure the instance folder exists - try: - os.makedirs(app.instance_path) - except OSError: - pass - - # a simple page that says hello - @app.route('/hello') - def hello(): - return 'Hello, World!' - - return app - -``create_app`` is the application factory function. You'll add to it -later in the tutorial, but it already does a lot. - -#. ``app = Flask(__name__, instance_relative_config=True)`` creates the - :class:`Flask` instance. - - * ``__name__`` is the name of the current Python module. The app - needs to know where it's located to set up some paths, and - ``__name__`` is a convenient way to tell it that. - - * ``instance_relative_config=True`` tells the app that - configuration files are relative to the - :ref:`instance folder `. The instance folder - is located outside the ``flaskr`` package and can hold local - data that shouldn't be committed to version control, such as - configuration secrets and the database file. - -#. :meth:`app.config.from_mapping() ` sets - some default configuration that the app will use: - - * :data:`SECRET_KEY` is used by Flask and extensions to keep data - safe. It's set to ``'dev'`` to provide a convenient value - during development, but it should be overridden with a random - value when deploying. - - * ``DATABASE`` is the path where the SQLite database file will be - saved. It's under - :attr:`app.instance_path `, which is the - path that Flask has chosen for the instance folder. You'll learn - more about the database in the next section. - -#. :meth:`app.config.from_pyfile() ` overrides - the default configuration with values taken from the ``config.py`` - file in the instance folder if it exists. For example, when - deploying, this can be used to set a real ``SECRET_KEY``. - - * ``test_config`` can also be passed to the factory, and will be - used instead of the instance configuration. This is so the tests - you'll write later in the tutorial can be configured - independently of any development values you have configured. - -#. :func:`os.makedirs` ensures that - :attr:`app.instance_path ` exists. Flask - doesn't create the instance folder automatically, but it needs to be - created because your project will create the SQLite database file - there. - -#. :meth:`@app.route() ` creates a simple route so you can - see the application working before getting into the rest of the - tutorial. It creates a connection between the URL ``/hello`` and a - function that returns a response, the string ``'Hello, World!'`` in - this case. - - -Run The Application -------------------- - -Now you can run your application using the ``flask`` command. From the -terminal, tell Flask where to find your application, then run it in -debug mode. Remember, you should still be in the top-level -``flask-tutorial`` directory, not the ``flaskr`` package. - -Debug mode shows an interactive debugger whenever a page raises an -exception, and restarts the server whenever you make changes to the -code. You can leave it running and just reload the browser page as you -follow the tutorial. - -.. code-block:: text - - $ flask --app flaskr run --debug - -You'll see output similar to this: - -.. code-block:: text - - * Serving Flask app "flaskr" - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with stat - * Debugger is active! - * Debugger PIN: nnn-nnn-nnn - -Visit http://127.0.0.1:5000/hello in a browser and you should see the -"Hello, World!" message. Congratulations, you're now running your Flask -web application! - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. - -Continue to :doc:`database`. diff --git a/docs/tutorial/flaskr_edit.png b/docs/tutorial/flaskr_edit.png deleted file mode 100644 index 6cd6e398..00000000 Binary files a/docs/tutorial/flaskr_edit.png and /dev/null differ diff --git a/docs/tutorial/flaskr_index.png b/docs/tutorial/flaskr_index.png deleted file mode 100644 index aa2b50f5..00000000 Binary files a/docs/tutorial/flaskr_index.png and /dev/null differ diff --git a/docs/tutorial/flaskr_login.png b/docs/tutorial/flaskr_login.png deleted file mode 100644 index d482c645..00000000 Binary files a/docs/tutorial/flaskr_login.png and /dev/null differ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst deleted file mode 100644 index d5dc5b3c..00000000 --- a/docs/tutorial/index.rst +++ /dev/null @@ -1,64 +0,0 @@ -Tutorial -======== - -.. toctree:: - :caption: Contents: - :maxdepth: 1 - - layout - factory - database - views - templates - static - blog - install - tests - deploy - next - -This tutorial will walk you through creating a basic blog application -called Flaskr. Users will be able to register, log in, create posts, -and edit or delete their own posts. You will be able to package and -install the application on other computers. - -.. image:: flaskr_index.png - :align: center - :class: screenshot - :alt: screenshot of index page - -It's assumed that you're already familiar with Python. The `official -tutorial`_ in the Python docs is a great way to learn or review first. - -.. _official tutorial: https://docs.python.org/3/tutorial/ - -While it's designed to give a good starting point, the tutorial doesn't -cover all of Flask's features. Check out the :doc:`/quickstart` for an -overview of what Flask can do, then dive into the docs to find out more. -The tutorial only uses what's provided by Flask and Python. In another -project, you might decide to use :doc:`/extensions` or other libraries -to make some tasks simpler. - -.. image:: flaskr_login.png - :align: center - :class: screenshot - :alt: screenshot of login page - -Flask is flexible. It doesn't require you to use any particular project -or code layout. However, when first starting, it's helpful to use a more -structured approach. This means that the tutorial will require a bit of -boilerplate up front, but it's done to avoid many common pitfalls that -new developers encounter, and it creates a project that's easy to expand -on. Once you become more comfortable with Flask, you can step out of -this structure and take full advantage of Flask's flexibility. - -.. image:: flaskr_edit.png - :align: center - :class: screenshot - :alt: screenshot of edit page - -:gh:`The tutorial project is available as an example in the Flask -repository `, if you want to compare your project -with the final product as you follow the tutorial. - -Continue to :doc:`layout`. diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst deleted file mode 100644 index db83e106..00000000 --- a/docs/tutorial/install.rst +++ /dev/null @@ -1,89 +0,0 @@ -Make the Project Installable -============================ - -Making your project installable means that you can build a *wheel* file and install that -in another environment, just like you installed Flask in your project's environment. -This makes deploying your project the same as installing any other library, so you're -using all the standard Python tools to manage everything. - -Installing also comes with other benefits that might not be obvious from -the tutorial or as a new Python user, including: - -* Currently, Python and Flask understand how to use the ``flaskr`` - package only because you're running from your project's directory. - Installing means you can import it no matter where you run from. - -* You can manage your project's dependencies just like other packages - do, so ``pip install yourproject.whl`` installs them. - -* Test tools can isolate your test environment from your development - environment. - -.. note:: - This is being introduced late in the tutorial, but in your future - projects you should always start with this. - - -Describe the Project --------------------- - -The ``pyproject.toml`` file describes your project and how to build it. - -.. code-block:: toml - :caption: ``pyproject.toml`` - - [project] - name = "flaskr" - version = "1.0.0" - description = "The basic blog app built in the Flask tutorial." - dependencies = [ - "flask", - ] - - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" - -See the official `Packaging tutorial `_ for more -explanation of the files and options used. - -.. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ - - -Install the Project -------------------- - -Use ``pip`` to install your project in the virtual environment. - -.. code-block:: none - - $ pip install -e . - -This tells pip to find ``pyproject.toml`` in the current directory and install the -project in *editable* or *development* mode. Editable mode means that as you make -changes to your local code, you'll only need to re-install if you change the metadata -about the project, such as its dependencies. - -You can observe that the project is now installed with ``pip list``. - -.. code-block:: none - - $ pip list - - Package Version Location - -------------- --------- ---------------------------------- - click 6.7 - Flask 1.0 - flaskr 1.0.0 /home/user/Projects/flask-tutorial - itsdangerous 0.24 - Jinja2 2.10 - MarkupSafe 1.0 - pip 9.0.3 - Werkzeug 0.14.1 - -Nothing changes from how you've been running your project so far. -``--app`` is still set to ``flaskr`` and ``flask run`` still runs -the application, but you can call it from anywhere, not just the -``flask-tutorial`` directory. - -Continue to :doc:`tests`. diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst deleted file mode 100644 index 6f8e59f4..00000000 --- a/docs/tutorial/layout.rst +++ /dev/null @@ -1,110 +0,0 @@ -Project Layout -============== - -Create a project directory and enter it: - -.. code-block:: none - - $ mkdir flask-tutorial - $ cd flask-tutorial - -Then follow the :doc:`installation instructions ` to set -up a Python virtual environment and install Flask for your project. - -The tutorial will assume you're working from the ``flask-tutorial`` -directory from now on. The file names at the top of each code block are -relative to this directory. - ----- - -A Flask application can be as simple as a single file. - -.. code-block:: python - :caption: ``hello.py`` - - from flask import Flask - - app = Flask(__name__) - - - @app.route('/') - def hello(): - return 'Hello, World!' - -However, as a project gets bigger, it becomes overwhelming to keep all -the code in one file. Python projects use *packages* to organize code -into multiple modules that can be imported where needed, and the -tutorial will do this as well. - -The project directory will contain: - -* ``flaskr/``, a Python package containing your application code and - files. -* ``tests/``, a directory containing test modules. -* ``.venv/``, a Python virtual environment where Flask and other - dependencies are installed. -* Installation files telling Python how to install your project. -* Version control config, such as `git`_. You should make a habit of - using some type of version control for all your projects, no matter - the size. -* Any other project files you might add in the future. - -.. _git: https://git-scm.com/ - -By the end, your project layout will look like this: - -.. code-block:: none - - /home/user/Projects/flask-tutorial - ├── flaskr/ - │ ├── __init__.py - │ ├── db.py - │ ├── schema.sql - │ ├── auth.py - │ ├── blog.py - │ ├── templates/ - │ │ ├── base.html - │ │ ├── auth/ - │ │ │ ├── login.html - │ │ │ └── register.html - │ │ └── blog/ - │ │ ├── create.html - │ │ ├── index.html - │ │ └── update.html - │ └── static/ - │ └── style.css - ├── tests/ - │ ├── conftest.py - │ ├── data.sql - │ ├── test_factory.py - │ ├── test_db.py - │ ├── test_auth.py - │ └── test_blog.py - ├── .venv/ - ├── pyproject.toml - └── MANIFEST.in - -If you're using version control, the following files that are generated -while running your project should be ignored. There may be other files -based on the editor you use. In general, ignore files that you didn't -write. For example, with git: - -.. code-block:: none - :caption: ``.gitignore`` - - .venv/ - - *.pyc - __pycache__/ - - instance/ - - .pytest_cache/ - .coverage - htmlcov/ - - dist/ - build/ - *.egg-info/ - -Continue to :doc:`factory`. diff --git a/docs/tutorial/next.rst b/docs/tutorial/next.rst deleted file mode 100644 index d41e8ef2..00000000 --- a/docs/tutorial/next.rst +++ /dev/null @@ -1,38 +0,0 @@ -Keep Developing! -================ - -You've learned about quite a few Flask and Python concepts throughout -the tutorial. Go back and review the tutorial and compare your code with -the steps you took to get there. Compare your project to the -:gh:`example project `, which might look a bit -different due to the step-by-step nature of the tutorial. - -There's a lot more to Flask than what you've seen so far. Even so, -you're now equipped to start developing your own web applications. Check -out the :doc:`/quickstart` for an overview of what Flask can do, then -dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, -`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have -their own documentation too. You'll also be interested in -:doc:`/extensions` which make tasks like working with the database or -validating form data easier and more powerful. - -If you want to keep developing your Flaskr project, here are some ideas -for what to try next: - -* A detail view to show a single post. Click a post's title to go to - its page. -* Like / unlike a post. -* Comments. -* Tags. Clicking a tag shows all the posts with that tag. -* A search box that filters the index page by name. -* Paged display. Only show 5 posts per page. -* Upload an image to go along with a post. -* Format posts using Markdown. -* An RSS feed of new posts. - -Have fun and make awesome applications! - -.. _Jinja: https://palletsprojects.com/p/jinja/ -.. _Click: https://palletsprojects.com/p/click/ -.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ -.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ diff --git a/docs/tutorial/static.rst b/docs/tutorial/static.rst deleted file mode 100644 index 8e76c409..00000000 --- a/docs/tutorial/static.rst +++ /dev/null @@ -1,72 +0,0 @@ -Static Files -============ - -The authentication views and templates work, but they look very plain -right now. Some `CSS`_ can be added to add style to the HTML layout you -constructed. The style won't change, so it's a *static* file rather than -a template. - -Flask automatically adds a ``static`` view that takes a path relative -to the ``flaskr/static`` directory and serves it. The ``base.html`` -template already has a link to the ``style.css`` file: - -.. code-block:: html+jinja - - {{ url_for('static', filename='style.css') }} - -Besides CSS, other types of static files might be files with JavaScript -functions, or a logo image. They are all placed under the -``flaskr/static`` directory and referenced with -``url_for('static', filename='...')``. - -This tutorial isn't focused on how to write CSS, so you can just copy -the following into the ``flaskr/static/style.css`` file: - -.. code-block:: css - :caption: ``flaskr/static/style.css`` - - html { font-family: sans-serif; background: #eee; padding: 1rem; } - body { max-width: 960px; margin: 0 auto; background: white; } - h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } - a { color: #377ba8; } - hr { border: none; border-top: 1px solid lightgray; } - nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } - nav h1 { flex: auto; margin: 0; } - nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } - nav ul { display: flex; list-style: none; margin: 0; padding: 0; } - nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } - .content { padding: 0 1rem 1rem; } - .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } - .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } - .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } - .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } - .post > header > div:first-of-type { flex: auto; } - .post > header h1 { font-size: 1.5em; margin-bottom: 0; } - .post .about { color: slategray; font-style: italic; } - .post .body { white-space: pre-line; } - .content:last-child { margin-bottom: 0; } - .content form { margin: 1em 0; display: flex; flex-direction: column; } - .content label { font-weight: bold; margin-bottom: 0.5em; } - .content input, .content textarea { margin-bottom: 1em; } - .content textarea { min-height: 12em; resize: vertical; } - input.danger { color: #cc2f2e; } - input[type=submit] { align-self: start; min-width: 10em; } - -You can find a less compact version of ``style.css`` in the -:gh:`example code `. - -Go to http://127.0.0.1:5000/auth/login and the page should look like the -screenshot below. - -.. image:: flaskr_login.png - :align: center - :class: screenshot - :alt: screenshot of login page - -You can read more about CSS from `Mozilla's documentation `_. If -you change a static file, refresh the browser page. If the change -doesn't show up, try clearing your browser's cache. - -.. _CSS: https://developer.mozilla.org/docs/Web/CSS - -Continue to :doc:`blog`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst deleted file mode 100644 index 1a5535cc..00000000 --- a/docs/tutorial/templates.rst +++ /dev/null @@ -1,187 +0,0 @@ -.. currentmodule:: flask - -Templates -========= - -You've written the authentication views for your application, but if -you're running the server and try to go to any of the URLs, you'll see a -``TemplateNotFound`` error. That's because the views are calling -:func:`render_template`, but you haven't written the templates yet. -The template files will be stored in the ``templates`` directory inside -the ``flaskr`` package. - -Templates are files that contain static data as well as placeholders -for dynamic data. A template is rendered with specific data to produce a -final document. Flask uses the `Jinja`_ template library to render -templates. - -In your application, you will use templates to render `HTML`_ which -will display in the user's browser. In Flask, Jinja is configured to -*autoescape* any data that is rendered in HTML templates. This means -that it's safe to render user input; any characters they've entered that -could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with -*safe* values that look the same in the browser but don't cause unwanted -effects. - -Jinja looks and behaves mostly like Python. Special delimiters are used -to distinguish Jinja syntax from the static data in the template. -Anything between ``{{`` and ``}}`` is an expression that will be output -to the final document. ``{%`` and ``%}`` denotes a control flow -statement like ``if`` and ``for``. Unlike Python, blocks are denoted -by start and end tags rather than indentation since static text within -a block could change indentation. - -.. _Jinja: https://jinja.palletsprojects.com/templates/ -.. _HTML: https://developer.mozilla.org/docs/Web/HTML - - -The Base Layout ---------------- - -Each page in the application will have the same basic layout around a -different body. Instead of writing the entire HTML structure in each -template, each template will *extend* a base template and override -specific sections. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/base.html`` - - - {% block title %}{% endblock %} - Flaskr - - -
-
- {% block header %}{% endblock %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block content %}{% endblock %} -
- -:data:`g` is automatically available in templates. Based on if -``g.user`` is set (from ``load_logged_in_user``), either the username -and a log out link are displayed, or links to register and log in -are displayed. :func:`url_for` is also automatically available, and is -used to generate URLs to views instead of writing them out manually. - -After the page title, and before the content, the template loops over -each message returned by :func:`get_flashed_messages`. You used -:func:`flash` in the views to show error messages, and this is the code -that will display them. - -There are three blocks defined here that will be overridden in the other -templates: - -#. ``{% block title %}`` will change the title displayed in the - browser's tab and window title. - -#. ``{% block header %}`` is similar to ``title`` but will change the - title displayed on the page. - -#. ``{% block content %}`` is where the content of each page goes, such - as the login form or a blog post. - -The base template is directly in the ``templates`` directory. To keep -the others organized, the templates for a blueprint will be placed in a -directory with the same name as the blueprint. - - -Register --------- - -.. code-block:: html+jinja - :caption: ``flaskr/templates/auth/register.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Register{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
- {% endblock %} - -``{% extends 'base.html' %}`` tells Jinja that this template should -replace the blocks from the base template. All the rendered content must -appear inside ``{% block %}`` tags that override blocks from the base -template. - -A useful pattern used here is to place ``{% block title %}`` inside -``{% block header %}``. This will set the title block and then output -the value of it into the header block, so that both the window and page -share the same title without writing it twice. - -The ``input`` tags are using the ``required`` attribute here. This tells -the browser not to submit the form until those fields are filled in. If -the user is using an older browser that doesn't support that attribute, -or if they are using something besides a browser to make requests, you -still want to validate the data in the Flask view. It's important to -always fully validate the data on the server, even if the client does -some validation as well. - - -Log In ------- - -This is identical to the register template except for the title and -submit button. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/auth/login.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Log In{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
- {% endblock %} - - -Register A User ---------------- - -Now that the authentication templates are written, you can register a -user. Make sure the server is still running (``flask run`` if it's not), -then go to http://127.0.0.1:5000/auth/register. - -Try clicking the "Register" button without filling out the form and see -that the browser shows an error message. Try removing the ``required`` -attributes from the ``register.html`` template and click "Register" -again. Instead of the browser showing an error, the page will reload and -the error from :func:`flash` in the view will be shown. - -Fill out a username and password and you'll be redirected to the login -page. Try entering an incorrect username, or the correct username and -incorrect password. If you log in you'll get an error because there's -no ``index`` view to redirect to yet. - -Continue to :doc:`static`. diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst deleted file mode 100644 index f4744cda..00000000 --- a/docs/tutorial/tests.rst +++ /dev/null @@ -1,559 +0,0 @@ -.. currentmodule:: flask - -Test Coverage -============= - -Writing unit tests for your application lets you check that the code -you wrote works the way you expect. Flask provides a test client that -simulates requests to the application and returns the response data. - -You should test as much of your code as possible. Code in functions only -runs when the function is called, and code in branches, such as ``if`` -blocks, only runs when the condition is met. You want to make sure that -each function is tested with data that covers each branch. - -The closer you get to 100% coverage, the more comfortable you can be -that making a change won't unexpectedly change other behavior. However, -100% coverage doesn't guarantee that your application doesn't have bugs. -In particular, it doesn't test how the user interacts with the -application in the browser. Despite this, test coverage is an important -tool to use during development. - -.. note:: - This is being introduced late in the tutorial, but in your future - projects you should test as you develop. - -You'll use `pytest`_ and `coverage`_ to test and measure your code. -Install them both: - -.. code-block:: none - - $ pip install pytest coverage - -.. _pytest: https://pytest.readthedocs.io/ -.. _coverage: https://coverage.readthedocs.io/ - - -Setup and Fixtures ------------------- - -The test code is located in the ``tests`` directory. This directory is -*next to* the ``flaskr`` package, not inside it. The -``tests/conftest.py`` file contains setup functions called *fixtures* -that each test will use. Tests are in Python modules that start with -``test_``, and each test function in those modules also starts with -``test_``. - -Each test will create a new temporary database file and populate some -data that will be used in the tests. Write a SQL file to insert that -data. - -.. code-block:: sql - :caption: ``tests/data.sql`` - - INSERT INTO user (username, password) - VALUES - ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), - ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); - - INSERT INTO post (title, body, author_id, created) - VALUES - ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); - -The ``app`` fixture will call the factory and pass ``test_config`` to -configure the application and database for testing instead of using your -local development configuration. - -.. code-block:: python - :caption: ``tests/conftest.py`` - - import os - import tempfile - - import pytest - from flaskr import create_app - from flaskr.db import get_db, init_db - - with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: - _data_sql = f.read().decode('utf8') - - - @pytest.fixture - def app(): - db_fd, db_path = tempfile.mkstemp() - - app = create_app({ - 'TESTING': True, - 'DATABASE': db_path, - }) - - with app.app_context(): - init_db() - get_db().executescript(_data_sql) - - yield app - - os.close(db_fd) - os.unlink(db_path) - - - @pytest.fixture - def client(app): - return app.test_client() - - - @pytest.fixture - def runner(app): - return app.test_cli_runner() - -:func:`tempfile.mkstemp` creates and opens a temporary file, returning -the file descriptor and the path to it. The ``DATABASE`` path is -overridden so it points to this temporary path instead of the instance -folder. After setting the path, the database tables are created and the -test data is inserted. After the test is over, the temporary file is -closed and removed. - -:data:`TESTING` tells Flask that the app is in test mode. Flask changes -some internal behavior so it's easier to test, and other extensions can -also use the flag to make testing them easier. - -The ``client`` fixture calls -:meth:`app.test_client() ` with the application -object created by the ``app`` fixture. Tests will use the client to make -requests to the application without running the server. - -The ``runner`` fixture is similar to ``client``. -:meth:`app.test_cli_runner() ` creates a runner -that can call the Click commands registered with the application. - -Pytest uses fixtures by matching their function names with the names -of arguments in the test functions. For example, the ``test_hello`` -function you'll write next takes a ``client`` argument. Pytest matches -that with the ``client`` fixture function, calls it, and passes the -returned value to the test function. - - -Factory -------- - -There's not much to test about the factory itself. Most of the code will -be executed for each test already, so if something fails the other tests -will notice. - -The only behavior that can change is passing test config. If config is -not passed, there should be some default configuration, otherwise the -configuration should be overridden. - -.. code-block:: python - :caption: ``tests/test_factory.py`` - - from flaskr import create_app - - - def test_config(): - assert not create_app().testing - assert create_app({'TESTING': True}).testing - - - def test_hello(client): - response = client.get('/hello') - assert response.data == b'Hello, World!' - -You added the ``hello`` route as an example when writing the factory at -the beginning of the tutorial. It returns "Hello, World!", so the test -checks that the response data matches. - - -Database --------- - -Within an application context, ``get_db`` should return the same -connection each time it's called. After the context, the connection -should be closed. - -.. code-block:: python - :caption: ``tests/test_db.py`` - - import sqlite3 - - import pytest - from flaskr.db import get_db - - - def test_get_close_db(app): - with app.app_context(): - db = get_db() - assert db is get_db() - - with pytest.raises(sqlite3.ProgrammingError) as e: - db.execute('SELECT 1') - - assert 'closed' in str(e.value) - -The ``init-db`` command should call the ``init_db`` function and output -a message. - -.. code-block:: python - :caption: ``tests/test_db.py`` - - def test_init_db_command(runner, monkeypatch): - class Recorder(object): - called = False - - def fake_init_db(): - Recorder.called = True - - monkeypatch.setattr('flaskr.db.init_db', fake_init_db) - result = runner.invoke(args=['init-db']) - assert 'Initialized' in result.output - assert Recorder.called - -This test uses Pytest's ``monkeypatch`` fixture to replace the -``init_db`` function with one that records that it's been called. The -``runner`` fixture you wrote above is used to call the ``init-db`` -command by name. - - -Authentication --------------- - -For most of the views, a user needs to be logged in. The easiest way to -do this in tests is to make a ``POST`` request to the ``login`` view -with the client. Rather than writing that out every time, you can write -a class with methods to do that, and use a fixture to pass it the client -for each test. - -.. code-block:: python - :caption: ``tests/conftest.py`` - - class AuthActions(object): - def __init__(self, client): - self._client = client - - def login(self, username='test', password='test'): - return self._client.post( - '/auth/login', - data={'username': username, 'password': password} - ) - - def logout(self): - return self._client.get('/auth/logout') - - - @pytest.fixture - def auth(client): - return AuthActions(client) - -With the ``auth`` fixture, you can call ``auth.login()`` in a test to -log in as the ``test`` user, which was inserted as part of the test -data in the ``app`` fixture. - -The ``register`` view should render successfully on ``GET``. On ``POST`` -with valid form data, it should redirect to the login URL and the user's -data should be in the database. Invalid data should display error -messages. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - import pytest - from flask import g, session - from flaskr.db import get_db - - - def test_register(client, app): - assert client.get('/auth/register').status_code == 200 - response = client.post( - '/auth/register', data={'username': 'a', 'password': 'a'} - ) - assert response.headers["Location"] == "/auth/login" - - with app.app_context(): - assert get_db().execute( - "SELECT * FROM user WHERE username = 'a'", - ).fetchone() is not None - - - @pytest.mark.parametrize(('username', 'password', 'message'), ( - ('', '', b'Username is required.'), - ('a', '', b'Password is required.'), - ('test', 'test', b'already registered'), - )) - def test_register_validate_input(client, username, password, message): - response = client.post( - '/auth/register', - data={'username': username, 'password': password} - ) - assert message in response.data - -:meth:`client.get() ` makes a ``GET`` request -and returns the :class:`Response` object returned by Flask. Similarly, -:meth:`client.post() ` makes a ``POST`` -request, converting the ``data`` dict into form data. - -To test that the page renders successfully, a simple request is made and -checked for a ``200 OK`` :attr:`~Response.status_code`. If -rendering failed, Flask would return a ``500 Internal Server Error`` -code. - -:attr:`~Response.headers` will have a ``Location`` header with the login -URL when the register view redirects to the login view. - -:attr:`~Response.data` contains the body of the response as bytes. If -you expect a certain value to render on the page, check that it's in -``data``. Bytes must be compared to bytes. If you want to compare text, -use :meth:`get_data(as_text=True) ` -instead. - -``pytest.mark.parametrize`` tells Pytest to run the same test function -with different arguments. You use it here to test different invalid -input and error messages without writing the same code three times. - -The tests for the ``login`` view are very similar to those for -``register``. Rather than testing the data in the database, -:data:`session` should have ``user_id`` set after logging in. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - def test_login(client, auth): - assert client.get('/auth/login').status_code == 200 - response = auth.login() - assert response.headers["Location"] == "/" - - with client: - client.get('/') - assert session['user_id'] == 1 - assert g.user['username'] == 'test' - - - @pytest.mark.parametrize(('username', 'password', 'message'), ( - ('a', 'test', b'Incorrect username.'), - ('test', 'a', b'Incorrect password.'), - )) - def test_login_validate_input(auth, username, password, message): - response = auth.login(username, password) - assert message in response.data - -Using ``client`` in a ``with`` block allows accessing context variables -such as :data:`session` after the response is returned. Normally, -accessing ``session`` outside of a request would raise an error. - -Testing ``logout`` is the opposite of ``login``. :data:`session` should -not contain ``user_id`` after logging out. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - def test_logout(client, auth): - auth.login() - - with client: - auth.logout() - assert 'user_id' not in session - - -Blog ----- - -All the blog views use the ``auth`` fixture you wrote earlier. Call -``auth.login()`` and subsequent requests from the client will be logged -in as the ``test`` user. - -The ``index`` view should display information about the post that was -added with the test data. When logged in as the author, there should be -a link to edit the post. - -You can also test some more authentication behavior while testing the -``index`` view. When not logged in, each page shows links to log in or -register. When logged in, there's a link to log out. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - import pytest - from flaskr.db import get_db - - - def test_index(client, auth): - response = client.get('/') - assert b"Log In" in response.data - assert b"Register" in response.data - - auth.login() - response = client.get('/') - assert b'Log Out' in response.data - assert b'test title' in response.data - assert b'by test on 2018-01-01' in response.data - assert b'test\nbody' in response.data - assert b'href="/1/update"' in response.data - -A user must be logged in to access the ``create``, ``update``, and -``delete`` views. The logged in user must be the author of the post to -access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status -is returned. If a ``post`` with the given ``id`` doesn't exist, -``update`` and ``delete`` should return ``404 Not Found``. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - @pytest.mark.parametrize('path', ( - '/create', - '/1/update', - '/1/delete', - )) - def test_login_required(client, path): - response = client.post(path) - assert response.headers["Location"] == "/auth/login" - - - def test_author_required(app, client, auth): - # change the post author to another user - with app.app_context(): - db = get_db() - db.execute('UPDATE post SET author_id = 2 WHERE id = 1') - db.commit() - - auth.login() - # current user can't modify other user's post - assert client.post('/1/update').status_code == 403 - assert client.post('/1/delete').status_code == 403 - # current user doesn't see edit link - assert b'href="/1/update"' not in client.get('/').data - - - @pytest.mark.parametrize('path', ( - '/2/update', - '/2/delete', - )) - def test_exists_required(client, auth, path): - auth.login() - assert client.post(path).status_code == 404 - -The ``create`` and ``update`` views should render and return a -``200 OK`` status for a ``GET`` request. When valid data is sent in a -``POST`` request, ``create`` should insert the new post data into the -database, and ``update`` should modify the existing data. Both pages -should show an error message on invalid data. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - def test_create(client, auth, app): - auth.login() - assert client.get('/create').status_code == 200 - client.post('/create', data={'title': 'created', 'body': ''}) - - with app.app_context(): - db = get_db() - count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] - assert count == 2 - - - def test_update(client, auth, app): - auth.login() - assert client.get('/1/update').status_code == 200 - client.post('/1/update', data={'title': 'updated', 'body': ''}) - - with app.app_context(): - db = get_db() - post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() - assert post['title'] == 'updated' - - - @pytest.mark.parametrize('path', ( - '/create', - '/1/update', - )) - def test_create_update_validate(client, auth, path): - auth.login() - response = client.post(path, data={'title': '', 'body': ''}) - assert b'Title is required.' in response.data - -The ``delete`` view should redirect to the index URL and the post should -no longer exist in the database. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - def test_delete(client, auth, app): - auth.login() - response = client.post('/1/delete') - assert response.headers["Location"] == "/" - - with app.app_context(): - db = get_db() - post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() - assert post is None - - -Running the Tests ------------------ - -Some extra configuration, which is not required but makes running tests with coverage -less verbose, can be added to the project's ``pyproject.toml`` file. - -.. code-block:: toml - :caption: ``pyproject.toml`` - - [tool.pytest.ini_options] - testpaths = ["tests"] - - [tool.coverage.run] - branch = true - source = ["flaskr"] - -To run the tests, use the ``pytest`` command. It will find and run all -the test functions you've written. - -.. code-block:: none - - $ pytest - - ========================= test session starts ========================== - platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 - rootdir: /home/user/Projects/flask-tutorial - collected 23 items - - tests/test_auth.py ........ [ 34%] - tests/test_blog.py ............ [ 86%] - tests/test_db.py .. [ 95%] - tests/test_factory.py .. [100%] - - ====================== 24 passed in 0.64 seconds ======================= - -If any tests fail, pytest will show the error that was raised. You can -run ``pytest -v`` to get a list of each test function rather than dots. - -To measure the code coverage of your tests, use the ``coverage`` command -to run pytest instead of running it directly. - -.. code-block:: none - - $ coverage run -m pytest - -You can either view a simple coverage report in the terminal: - -.. code-block:: none - - $ coverage report - - Name Stmts Miss Branch BrPart Cover - ------------------------------------------------------ - flaskr/__init__.py 21 0 2 0 100% - flaskr/auth.py 54 0 22 0 100% - flaskr/blog.py 54 0 16 0 100% - flaskr/db.py 24 0 4 0 100% - ------------------------------------------------------ - TOTAL 153 0 44 0 100% - -An HTML report allows you to see which lines were covered in each file: - -.. code-block:: none - - $ coverage html - -This generates files in the ``htmlcov`` directory. Open -``htmlcov/index.html`` in your browser to see the report. - -Continue to :doc:`deploy`. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst deleted file mode 100644 index 7092dbc2..00000000 --- a/docs/tutorial/views.rst +++ /dev/null @@ -1,305 +0,0 @@ -.. currentmodule:: flask - -Blueprints and Views -==================== - -A view function is the code you write to respond to requests to your -application. Flask uses patterns to match the incoming request URL to -the view that should handle it. The view returns data that Flask turns -into an outgoing response. Flask can also go the other direction and -generate a URL to a view based on its name and arguments. - - -Create a Blueprint ------------------- - -A :class:`Blueprint` is a way to organize a group of related views and -other code. Rather than registering views and other code directly with -an application, they are registered with a blueprint. Then the blueprint -is registered with the application when it is available in the factory -function. - -Flaskr will have two blueprints, one for authentication functions and -one for the blog posts functions. The code for each blueprint will go -in a separate module. Since the blog needs to know about authentication, -you'll write the authentication one first. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - import functools - - from flask import ( - Blueprint, flash, g, redirect, render_template, request, session, url_for - ) - from werkzeug.security import check_password_hash, generate_password_hash - - from flaskr.db import get_db - - bp = Blueprint('auth', __name__, url_prefix='/auth') - -This creates a :class:`Blueprint` named ``'auth'``. Like the application -object, the blueprint needs to know where it's defined, so ``__name__`` -is passed as the second argument. The ``url_prefix`` will be prepended -to all the URLs associated with the blueprint. - -Import and register the blueprint from the factory using -:meth:`app.register_blueprint() `. Place the -new code at the end of the factory function before returning the app. - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - def create_app(): - app = ... - # existing code omitted - - from . import auth - app.register_blueprint(auth.bp) - - return app - -The authentication blueprint will have views to register new users and -to log in and log out. - - -The First View: Register ------------------------- - -When the user visits the ``/auth/register`` URL, the ``register`` view -will return `HTML`_ with a form for them to fill out. When they submit -the form, it will validate their input and either show the form again -with an error message or create the new user and go to the login page. - -.. _HTML: https://developer.mozilla.org/docs/Web/HTML - -For now you will just write the view code. On the next page, you'll -write templates to generate the HTML form. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/register', methods=('GET', 'POST')) - def register(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - - if not username: - error = 'Username is required.' - elif not password: - error = 'Password is required.' - - if error is None: - try: - db.execute( - "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), - ) - db.commit() - except db.IntegrityError: - error = f"User {username} is already registered." - else: - return redirect(url_for("auth.login")) - - flash(error) - - return render_template('auth/register.html') - -Here's what the ``register`` view function is doing: - -#. :meth:`@bp.route ` associates the URL ``/register`` - with the ``register`` view function. When Flask receives a request - to ``/auth/register``, it will call the ``register`` view and use - the return value as the response. - -#. If the user submitted the form, - :attr:`request.method ` will be ``'POST'``. In this - case, start validating the input. - -#. :attr:`request.form ` is a special type of - :class:`dict` mapping submitted form keys and values. The user will - input their ``username`` and ``password``. - -#. Validate that ``username`` and ``password`` are not empty. - -#. If validation succeeds, insert the new user data into the database. - - - :meth:`db.execute ` takes a SQL - query with ``?`` placeholders for any user input, and a tuple of - values to replace the placeholders with. The database library - will take care of escaping the values so you are not vulnerable - to a *SQL injection attack*. - - - For security, passwords should never be stored in the database - directly. Instead, - :func:`~werkzeug.security.generate_password_hash` is used to - securely hash the password, and that hash is stored. Since this - query modifies data, - :meth:`db.commit() ` needs to be - called afterwards to save the changes. - - - An :exc:`sqlite3.IntegrityError` will occur if the username - already exists, which should be shown to the user as another - validation error. - -#. After storing the user, they are redirected to the login page. - :func:`url_for` generates the URL for the login view based on its - name. This is preferable to writing the URL directly as it allows - you to change the URL later without changing all code that links to - it. :func:`redirect` generates a redirect response to the generated - URL. - -#. If validation fails, the error is shown to the user. :func:`flash` - stores messages that can be retrieved when rendering the template. - -#. When the user initially navigates to ``auth/register``, or - there was a validation error, an HTML page with the registration - form should be shown. :func:`render_template` will render a template - containing the HTML, which you'll write in the next step of the - tutorial. - - -Login ------ - -This view follows the same pattern as the ``register`` view above. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/login', methods=('GET', 'POST')) - def login(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - user = db.execute( - 'SELECT * FROM user WHERE username = ?', (username,) - ).fetchone() - - if user is None: - error = 'Incorrect username.' - elif not check_password_hash(user['password'], password): - error = 'Incorrect password.' - - if error is None: - session.clear() - session['user_id'] = user['id'] - return redirect(url_for('index')) - - flash(error) - - return render_template('auth/login.html') - -There are a few differences from the ``register`` view: - -#. The user is queried first and stored in a variable for later use. - - :meth:`~sqlite3.Cursor.fetchone` returns one row from the query. - If the query returned no results, it returns ``None``. Later, - :meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list - of all results. - -#. :func:`~werkzeug.security.check_password_hash` hashes the submitted - password in the same way as the stored hash and securely compares - them. If they match, the password is valid. - -#. :data:`session` is a :class:`dict` that stores data across requests. - When validation succeeds, the user's ``id`` is stored in a new - session. The data is stored in a *cookie* that is sent to the - browser, and the browser then sends it back with subsequent requests. - Flask securely *signs* the data so that it can't be tampered with. - -Now that the user's ``id`` is stored in the :data:`session`, it will be -available on subsequent requests. At the beginning of each request, if -a user is logged in their information should be loaded and made -available to other views. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.before_app_request - def load_logged_in_user(): - user_id = session.get('user_id') - - if user_id is None: - g.user = None - else: - g.user = get_db().execute( - 'SELECT * FROM user WHERE id = ?', (user_id,) - ).fetchone() - -:meth:`bp.before_app_request() ` registers -a function that runs before the view function, no matter what URL is -requested. ``load_logged_in_user`` checks if a user id is stored in the -:data:`session` and gets that user's data from the database, storing it -on :data:`g.user `, which lasts for the length of the request. If -there is no user id, or if the id doesn't exist, ``g.user`` will be -``None``. - - -Logout ------- - -To log out, you need to remove the user id from the :data:`session`. -Then ``load_logged_in_user`` won't load a user on subsequent requests. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/logout') - def logout(): - session.clear() - return redirect(url_for('index')) - - -Require Authentication in Other Views -------------------------------------- - -Creating, editing, and deleting blog posts will require a user to be -logged in. A *decorator* can be used to check this for each view it's -applied to. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - def login_required(view): - @functools.wraps(view) - def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for('auth.login')) - - return view(**kwargs) - - return wrapped_view - -This decorator returns a new view function that wraps the original view -it's applied to. The new function checks if a user is loaded and -redirects to the login page otherwise. If a user is loaded the original -view is called and continues normally. You'll use this decorator when -writing the blog views. - -Endpoints and URLs ------------------- - -The :func:`url_for` function generates the URL to a view based on a name -and arguments. The name associated with a view is also called the -*endpoint*, and by default it's the same as the name of the view -function. - -For example, the ``hello()`` view that was added to the app -factory earlier in the tutorial has the name ``'hello'`` and can be -linked to with ``url_for('hello')``. If it took an argument, which -you'll see later, it would be linked to using -``url_for('hello', who='World')``. - -When using a blueprint, the name of the blueprint is prepended to the -name of the function, so the endpoint for the ``login`` function you -wrote above is ``'auth.login'`` because you added it to the ``'auth'`` -blueprint. - -Continue to :doc:`templates`. diff --git a/docs/views.rst b/docs/views.rst deleted file mode 100644 index f2210270..00000000 --- a/docs/views.rst +++ /dev/null @@ -1,324 +0,0 @@ -Class-based Views -================= - -.. currentmodule:: flask.views - -This page introduces using the :class:`View` and :class:`MethodView` -classes to write class-based views. - -A class-based view is a class that acts as a view function. Because it -is a class, different instances of the class can be created with -different arguments, to change the behavior of the view. This is also -known as generic, reusable, or pluggable views. - -An example of where this is useful is defining a class that creates an -API based on the database model it is initialized with. - -For more complex API behavior and customization, look into the various -API extensions for Flask. - - -Basic Reusable View -------------------- - -Let's walk through an example converting a view function to a view -class. We start with a view function that queries a list of users then -renders a template to show the list. - -.. code-block:: python - - @app.route("/users/") - def user_list(): - users = User.query.all() - return render_template("users.html", users=users) - -This works for the user model, but let's say you also had more models -that needed list pages. You'd need to write another view function for -each model, even though the only thing that would change is the model -and template name. - -Instead, you can write a :class:`View` subclass that will query a model -and render a template. As the first step, we'll convert the view to a -class without any customization. - -.. code-block:: python - - from flask.views import View - - class UserList(View): - def dispatch_request(self): - users = User.query.all() - return render_template("users.html", objects=users) - - app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) - -The :meth:`View.dispatch_request` method is the equivalent of the view -function. Calling :meth:`View.as_view` method will create a view -function that can be registered on the app with its -:meth:`~flask.Flask.add_url_rule` method. The first argument to -``as_view`` is the name to use to refer to the view with -:func:`~flask.url_for`. - -.. note:: - - You can't decorate the class with ``@app.route()`` the way you'd - do with a basic view function. - -Next, we need to be able to register the same view class for different -models and templates, to make it more useful than the original function. -The class will take two arguments, the model and template, and store -them on ``self``. Then ``dispatch_request`` can reference these instead -of hard-coded values. - -.. code-block:: python - - class ListView(View): - def __init__(self, model, template): - self.model = model - self.template = template - - def dispatch_request(self): - items = self.model.query.all() - return render_template(self.template, items=items) - -Remember, we create the view function with ``View.as_view()`` instead of -creating the class directly. Any extra arguments passed to ``as_view`` -are then passed when creating the class. Now we can register the same -view to handle multiple models. - -.. code-block:: python - - app.add_url_rule( - "/users/", - view_func=ListView.as_view("user_list", User, "users.html"), - ) - app.add_url_rule( - "/stories/", - view_func=ListView.as_view("story_list", Story, "stories.html"), - ) - - -URL Variables -------------- - -Any variables captured by the URL are passed as keyword arguments to the -``dispatch_request`` method, as they would be for a regular view -function. - -.. code-block:: python - - class DetailView(View): - def __init__(self, model): - self.model = model - self.template = f"{model.__name__.lower()}/detail.html" - - def dispatch_request(self, id) - item = self.model.query.get_or_404(id) - return render_template(self.template, item=item) - - app.add_url_rule( - "/users/", - view_func=DetailView.as_view("user_detail", User) - ) - - -View Lifetime and ``self`` --------------------------- - -By default, a new instance of the view class is created every time a -request is handled. This means that it is safe to write other data to -``self`` during the request, since the next request will not see it, -unlike other forms of global state. - -However, if your view class needs to do a lot of complex initialization, -doing it for every request is unnecessary and can be inefficient. To -avoid this, set :attr:`View.init_every_request` to ``False``, which will -only create one instance of the class and use it for every request. In -this case, writing to ``self`` is not safe. If you need to store data -during the request, use :data:`~flask.g` instead. - -In the ``ListView`` example, nothing writes to ``self`` during the -request, so it is more efficient to create a single instance. - -.. code-block:: python - - class ListView(View): - init_every_request = False - - def __init__(self, model, template): - self.model = model - self.template = template - - def dispatch_request(self): - items = self.model.query.all() - return render_template(self.template, items=items) - -Different instances will still be created each for each ``as_view`` -call, but not for each request to those views. - - -View Decorators ---------------- - -The view class itself is not the view function. View decorators need to -be applied to the view function returned by ``as_view``, not the class -itself. Set :attr:`View.decorators` to a list of decorators to apply. - -.. code-block:: python - - class UserList(View): - decorators = [cache(minutes=2), login_required] - - app.add_url_rule('/users/', view_func=UserList.as_view()) - -If you didn't set ``decorators``, you could apply them manually instead. -This is equivalent to: - -.. code-block:: python - - view = UserList.as_view("users_list") - view = cache(minutes=2)(view) - view = login_required(view) - app.add_url_rule('/users/', view_func=view) - -Keep in mind that order matters. If you're used to ``@decorator`` style, -this is equivalent to: - -.. code-block:: python - - @app.route("/users/") - @login_required - @cache(minutes=2) - def user_list(): - ... - - -Method Hints ------------- - -A common pattern is to register a view with ``methods=["GET", "POST"]``, -then check ``request.method == "POST"`` to decide what to do. Setting -:attr:`View.methods` is equivalent to passing the list of methods to -``add_url_rule`` or ``route``. - -.. code-block:: python - - class MyView(View): - methods = ["GET", "POST"] - - def dispatch_request(self): - if request.method == "POST": - ... - ... - - app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) - -This is equivalent to the following, except further subclasses can -inherit or change the methods. - -.. code-block:: python - - app.add_url_rule( - "/my-view", - view_func=MyView.as_view("my-view"), - methods=["GET", "POST"], - ) - - -Method Dispatching and APIs ---------------------------- - -For APIs it can be helpful to use a different function for each HTTP -method. :class:`MethodView` extends the basic :class:`View` to dispatch -to different methods of the class based on the request method. Each HTTP -method maps to a method of the class with the same (lowercase) name. - -:class:`MethodView` automatically sets :attr:`View.methods` based on the -methods defined by the class. It even knows how to handle subclasses -that override or define other methods. - -We can make a generic ``ItemAPI`` class that provides get (detail), -patch (edit), and delete methods for a given model. A ``GroupAPI`` can -provide get (list) and post (create) methods. - -.. code-block:: python - - from flask.views import MethodView - - class ItemAPI(MethodView): - init_every_request = False - - def __init__(self, model): - self.model = model - self.validator = generate_validator(model) - - def _get_item(self, id): - return self.model.query.get_or_404(id) - - def get(self, id): - item = self._get_item(id) - return jsonify(item.to_json()) - - def patch(self, id): - item = self._get_item(id) - errors = self.validator.validate(item, request.json) - - if errors: - return jsonify(errors), 400 - - item.update_from_json(request.json) - db.session.commit() - return jsonify(item.to_json()) - - def delete(self, id): - item = self._get_item(id) - db.session.delete(item) - db.session.commit() - return "", 204 - - class GroupAPI(MethodView): - init_every_request = False - - def __init__(self, model): - self.model = model - self.validator = generate_validator(model, create=True) - - def get(self): - items = self.model.query.all() - return jsonify([item.to_json() for item in items]) - - def post(self): - errors = self.validator.validate(request.json) - - if errors: - return jsonify(errors), 400 - - db.session.add(self.model.from_json(request.json)) - db.session.commit() - return jsonify(item.to_json()) - - def register_api(app, model, name): - item = ItemAPI.as_view(f"{name}-item", model) - group = GroupAPI.as_view(f"{name}-group", model) - app.add_url_rule(f"/{name}/", view_func=item) - app.add_url_rule(f"/{name}/", view_func=group) - - register_api(app, User, "users") - register_api(app, Story, "stories") - -This produces the following views, a standard REST API! - -================= ========== =================== -URL Method Description ------------------ ---------- ------------------- -``/users/`` ``GET`` List all users -``/users/`` ``POST`` Create a new user -``/users/`` ``GET`` Show a single user -``/users/`` ``PATCH`` Update a user -``/users/`` ``DELETE`` Delete a user -``/stories/`` ``GET`` List all stories -``/stories/`` ``POST`` Create a new story -``/stories/`` ``GET`` Show a single story -``/stories/`` ``PATCH`` Update a story -``/stories/`` ``DELETE`` Delete a story -================= ========== =================== diff --git a/docs/web-security.rst b/docs/web-security.rst deleted file mode 100644 index 3992e8da..00000000 --- a/docs/web-security.rst +++ /dev/null @@ -1,274 +0,0 @@ -Security Considerations -======================= - -Web applications usually face all kinds of security problems and it's very -hard to get everything right. Flask tries to solve a few of these things -for you, but there are a couple more you have to take care of yourself. - -.. _security-xss: - -Cross-Site Scripting (XSS) --------------------------- - -Cross site scripting is the concept of injecting arbitrary HTML (and with -it JavaScript) into the context of a website. To remedy this, developers -have to properly escape text so that it cannot include arbitrary HTML -tags. For more information on that have a look at the Wikipedia article -on `Cross-Site Scripting -`_. - -Flask configures Jinja2 to automatically escape all values unless -explicitly told otherwise. This should rule out all XSS problems caused -in templates, but there are still other places where you have to be -careful: - -- generating HTML without the help of Jinja2 -- calling :class:`~markupsafe.Markup` on data submitted by users -- sending out HTML from uploaded files, never do that, use the - ``Content-Disposition: attachment`` header to prevent that problem. -- sending out textfiles from uploaded files. Some browsers are using - content-type guessing based on the first few bytes so users could - trick a browser to execute HTML. - -Another thing that is very important are unquoted attributes. While -Jinja2 can protect you from XSS issues by escaping HTML, there is one -thing it cannot protect you from: XSS by attribute injection. To counter -this possible attack vector, be sure to always quote your attributes with -either double or single quotes when using Jinja expressions in them: - -.. sourcecode:: html+jinja - - - -Why is this necessary? Because if you would not be doing that, an -attacker could easily inject custom JavaScript handlers. For example an -attacker could inject this piece of HTML+JavaScript: - -.. sourcecode:: html - - onmouseover=alert(document.cookie) - -When the user would then move with the mouse over the input, the cookie -would be presented to the user in an alert window. But instead of showing -the cookie to the user, a good attacker might also execute any other -JavaScript code. In combination with CSS injections the attacker might -even make the element fill out the entire page so that the user would -just have to have the mouse anywhere on the page to trigger the attack. - -There is one class of XSS issues that Jinja's escaping does not protect -against. The ``a`` tag's ``href`` attribute can contain a `javascript:` URI, -which the browser will execute when clicked if not secured properly. - -.. sourcecode:: html - - click here - click here - -To prevent this, you'll need to set the :ref:`security-csp` response header. - -Cross-Site Request Forgery (CSRF) ---------------------------------- - -Another big problem is CSRF. This is a very complex topic and I won't -outline it here in detail just mention what it is and how to theoretically -prevent it. - -If your authentication information is stored in cookies, you have implicit -state management. The state of "being logged in" is controlled by a -cookie, and that cookie is sent with each request to a page. -Unfortunately that includes requests triggered by 3rd party sites. If you -don't keep that in mind, some people might be able to trick your -application's users with social engineering to do stupid things without -them knowing. - -Say you have a specific URL that, when you sent ``POST`` requests to will -delete a user's profile (say ``http://example.com/user/delete``). If an -attacker now creates a page that sends a post request to that page with -some JavaScript they just have to trick some users to load that page and -their profiles will end up being deleted. - -Imagine you were to run Facebook with millions of concurrent users and -someone would send out links to images of little kittens. When users -would go to that page, their profiles would get deleted while they are -looking at images of fluffy cats. - -How can you prevent that? Basically for each request that modifies -content on the server you would have to either use a one-time token and -store that in the cookie **and** also transmit it with the form data. -After receiving the data on the server again, you would then have to -compare the two tokens and ensure they are equal. - -Why does Flask not do that for you? The ideal place for this to happen is -the form validation framework, which does not exist in Flask. - -.. _security-json: - -JSON Security -------------- - -In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level -arrays to JSON. This was because of a security vulnerability in ECMAScript 4. - -ECMAScript 5 closed this vulnerability, so only extremely old browsers are -still vulnerable. All of these browsers have `other more serious -vulnerabilities -`_, so -this behavior was changed and :func:`~flask.jsonify` now supports serializing -arrays. - -Security Headers ----------------- - -Browsers recognize various response headers in order to control security. We -recommend reviewing each of the headers below for use in your application. -The `Flask-Talisman`_ extension can be used to manage HTTPS and the security -headers for you. - -.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman - -HTTP Strict Transport Security (HSTS) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tells the browser to convert all HTTP requests to HTTPS, preventing -man-in-the-middle (MITM) attacks. :: - - response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security - -.. _security-csp: - -Content Security Policy (CSP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tell the browser where it can load various types of resource from. This header -should be used whenever possible, but requires some work to define the correct -policy for your site. A very strict policy would be:: - - response.headers['Content-Security-Policy'] = "default-src 'self'" - -- https://csp.withgoogle.com/docs/index.html -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - -X-Content-Type-Options -~~~~~~~~~~~~~~~~~~~~~~ - -Forces the browser to honor the response content type instead of trying to -detect it, which can be abused to generate a cross-site scripting (XSS) -attack. :: - - response.headers['X-Content-Type-Options'] = 'nosniff' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - -X-Frame-Options -~~~~~~~~~~~~~~~ - -Prevents external sites from embedding your site in an ``iframe``. This -prevents a class of attacks where clicks in the outer frame can be translated -invisibly to clicks on your page's elements. This is also known as -"clickjacking". :: - - response.headers['X-Frame-Options'] = 'SAMEORIGIN' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - -.. _security-cookie: - -Set-Cookie options -~~~~~~~~~~~~~~~~~~ - -These options can be added to a ``Set-Cookie`` header to improve their -security. Flask has configuration options to set these on the session cookie. -They can be set on other cookies too. - -- ``Secure`` limits cookies to HTTPS traffic only. -- ``HttpOnly`` protects the contents of cookies from being read with - JavaScript. -- ``SameSite`` restricts how cookies are sent with requests from - external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``. - ``Lax`` prevents sending cookies with CSRF-prone requests from - external sites, such as submitting a form. ``Strict`` prevents sending - cookies with all external requests, including following regular links. - -:: - - app.config.update( - SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='Lax', - ) - - response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax') - -Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after -the given time, or the current time plus the age, respectively. If neither -option is set, the cookie will be removed when the browser is closed. :: - - # cookie expires after 10 minutes - response.set_cookie('snakes', '3', max_age=600) - -For the session cookie, if :attr:`session.permanent ` -is set, then :data:`PERMANENT_SESSION_LIFETIME` is used to set the expiration. -Flask's default cookie implementation validates that the cryptographic -signature is not older than this value. Lowering this value may help mitigate -replay attacks, where intercepted cookies can be sent at a later time. :: - - app.config.update( - PERMANENT_SESSION_LIFETIME=600 - ) - - @app.route('/login', methods=['POST']) - def login(): - ... - session.clear() - session['user_id'] = user.id - session.permanent = True - ... - -Use :class:`itsdangerous.TimedSerializer` to sign and validate other cookie -values (or any values that need secure signatures). - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - -.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute - - -HTTP Public Key Pinning (HPKP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This tells the browser to authenticate with the server using only the specific -certificate key to prevent MITM attacks. - -.. warning:: - Be careful when enabling this, as it is very difficult to undo if you set up - or upgrade your key incorrectly. - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning - - -Copy/Paste to Terminal ----------------------- - -Hidden characters such as the backspace character (``\b``, ``^H``) can -cause text to render differently in HTML than how it is interpreted if -`pasted into a terminal `__. - -For example, ``import y\bose\bm\bi\bt\be\b`` renders as -``import yosemite`` in HTML, but the backspaces are applied when pasted -into a terminal, and it becomes ``import os``. - -If you expect users to copy and paste untrusted code from your site, -such as from comments posted by users on a technical blog, consider -applying extra filtering, such as replacing all ``\b`` characters. - -.. code-block:: python - - body = body.replace("\b", "") - -Most modern terminals will warn about and remove hidden characters when -pasting, so this isn't strictly necessary. It's also possible to craft -dangerous commands in other ways that aren't possible to filter. -Depending on your site's use case, it may be good to show a warning -about copying code in general. diff --git a/examples/celery/README.md b/examples/celery/README.md deleted file mode 100644 index 038eb51e..00000000 --- a/examples/celery/README.md +++ /dev/null @@ -1,27 +0,0 @@ -Background Tasks with Celery -============================ - -This example shows how to configure Celery with Flask, how to set up an API for -submitting tasks and polling results, and how to use that API with JavaScript. See -[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). - -From this directory, create a virtualenv and install the application into it. Then run a -Celery worker. - -```shell -$ python3 -m venv .venv -$ . ./.venv/bin/activate -$ pip install -r requirements.txt && pip install -e . -$ celery -A make_celery worker --loglevel INFO -``` - -In a separate terminal, activate the virtualenv and run the Flask development server. - -```shell -$ . ./.venv/bin/activate -$ flask -A task_app run --debug -``` - -Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling -requests in the browser dev tools and the Flask logs. You can see the tasks submitting -and completing in the Celery logs. diff --git a/examples/celery/make_celery.py b/examples/celery/make_celery.py deleted file mode 100644 index f7d138e6..00000000 --- a/examples/celery/make_celery.py +++ /dev/null @@ -1,4 +0,0 @@ -from task_app import create_app - -flask_app = create_app() -celery_app = flask_app.extensions["celery"] diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml deleted file mode 100644 index 25887ca2..00000000 --- a/examples/celery/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[project] -name = "flask-example-celery" -version = "1.0.0" -description = "Example Flask application with Celery background tasks." -readme = "README.md" -requires-python = ">=3.8" -dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "task_app" - -[tool.ruff] -src = ["src"] diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt deleted file mode 100644 index 29075ab5..00000000 --- a/examples/celery/requirements.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --resolver=backtracking pyproject.toml -# -amqp==5.1.1 - # via kombu -async-timeout==4.0.2 - # via redis -billiard==3.6.4.0 - # via celery -blinker==1.6.2 - # via flask -celery[redis]==5.2.7 - # via flask-example-celery (pyproject.toml) -click==8.1.3 - # via - # celery - # click-didyoumean - # click-plugins - # click-repl - # flask -click-didyoumean==0.3.0 - # via celery -click-plugins==1.1.1 - # via celery -click-repl==0.2.0 - # via celery -flask==2.3.2 - # via flask-example-celery (pyproject.toml) -itsdangerous==2.1.2 - # via flask -jinja2==3.1.2 - # via flask -kombu==5.2.4 - # via celery -markupsafe==2.1.2 - # via - # jinja2 - # werkzeug -prompt-toolkit==3.0.38 - # via click-repl -pytz==2023.3 - # via celery -redis==4.5.4 - # via celery -six==1.16.0 - # via click-repl -vine==5.0.0 - # via - # amqp - # celery - # kombu -wcwidth==0.2.6 - # via prompt-toolkit -werkzeug==2.3.3 - # via flask diff --git a/examples/celery/src/task_app/__init__.py b/examples/celery/src/task_app/__init__.py deleted file mode 100644 index dafff8aa..00000000 --- a/examples/celery/src/task_app/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from celery import Celery -from celery import Task -from flask import Flask -from flask import render_template - - -def create_app() -> Flask: - app = Flask(__name__) - app.config.from_mapping( - CELERY=dict( - broker_url="redis://localhost", - result_backend="redis://localhost", - task_ignore_result=True, - ), - ) - app.config.from_prefixed_env() - celery_init_app(app) - - @app.route("/") - def index() -> str: - return render_template("index.html") - - from . import views - - app.register_blueprint(views.bp) - return app - - -def celery_init_app(app: Flask) -> Celery: - class FlaskTask(Task): - def __call__(self, *args: object, **kwargs: object) -> object: - with app.app_context(): - return self.run(*args, **kwargs) - - celery_app = Celery(app.name, task_cls=FlaskTask) - celery_app.config_from_object(app.config["CELERY"]) - celery_app.set_default() - app.extensions["celery"] = celery_app - return celery_app diff --git a/examples/celery/src/task_app/tasks.py b/examples/celery/src/task_app/tasks.py deleted file mode 100644 index b6b3595d..00000000 --- a/examples/celery/src/task_app/tasks.py +++ /dev/null @@ -1,23 +0,0 @@ -import time - -from celery import shared_task -from celery import Task - - -@shared_task(ignore_result=False) -def add(a: int, b: int) -> int: - return a + b - - -@shared_task() -def block() -> None: - time.sleep(5) - - -@shared_task(bind=True, ignore_result=False) -def process(self: Task, total: int) -> object: - for i in range(total): - self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) - time.sleep(1) - - return {"current": total, "total": total} diff --git a/examples/celery/src/task_app/templates/index.html b/examples/celery/src/task_app/templates/index.html deleted file mode 100644 index 4e1145cb..00000000 --- a/examples/celery/src/task_app/templates/index.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - Celery Example - - -

Celery Example

-Execute background tasks with Celery. Submits tasks and shows results using JavaScript. - -
-

Add

-

Start a task to add two numbers, then poll for the result. -

-
-
- -
-

Result:

- -
-

Block

-

Start a task that takes 5 seconds. However, the response will return immediately. -

- -
-

- -
-

Process

-

Start a task that counts, waiting one second each time, showing progress. -

-
- -
-

- - - - diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py deleted file mode 100644 index 99cf92dc..00000000 --- a/examples/celery/src/task_app/views.py +++ /dev/null @@ -1,38 +0,0 @@ -from celery.result import AsyncResult -from flask import Blueprint -from flask import request - -from . import tasks - -bp = Blueprint("tasks", __name__, url_prefix="/tasks") - - -@bp.get("/result/") -def result(id: str) -> dict[str, object]: - result = AsyncResult(id) - ready = result.ready() - return { - "ready": ready, - "successful": result.successful() if ready else None, - "value": result.get() if ready else result.result, - } - - -@bp.post("/add") -def add() -> dict[str, object]: - a = request.form.get("a", type=int) - b = request.form.get("b", type=int) - result = tasks.add.delay(a, b) - return {"result_id": result.id} - - -@bp.post("/block") -def block() -> dict[str, object]: - result = tasks.block.delay() - return {"result_id": result.id} - - -@bp.post("/process") -def process() -> dict[str, object]: - result = tasks.process.delay(total=request.form.get("total", type=int)) - return {"result_id": result.id} diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore deleted file mode 100644 index a306afbc..00000000 --- a/examples/javascript/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -.venv/ -*.pyc -__pycache__/ -instance/ -.cache/ -.pytest_cache/ -.coverage -htmlcov/ -dist/ -build/ -*.egg-info/ -.idea/ -*.swp -*~ diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst deleted file mode 100644 index f5f66912..00000000 --- a/examples/javascript/README.rst +++ /dev/null @@ -1,48 +0,0 @@ -JavaScript Ajax Example -======================= - -Demonstrates how to post form data and process a JSON response using -JavaScript. This allows making requests without navigating away from the -page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and -|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax. - -.. |fetch| replace:: ``fetch`` -.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch - -.. |XMLHttpRequest| replace:: ``XMLHttpRequest`` -.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest - -.. |jQuery.ajax| replace:: ``jQuery.ajax`` -.. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ - -.. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/ - - -Install -------- - -.. code-block:: text - - $ python3 -m venv .venv - $ . .venv/bin/activate - $ pip install -e . - - -Run ---- - -.. code-block:: text - - $ flask --app js_example run - -Open http://127.0.0.1:5000 in a browser. - - -Test ----- - -.. code-block:: text - - $ pip install -e '.[test]' - $ coverage run -m pytest - $ coverage report diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py deleted file mode 100644 index 0ec3ca21..00000000 --- a/examples/javascript/js_example/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - -from js_example import views # noqa: E402, F401 diff --git a/examples/javascript/js_example/templates/base.html b/examples/javascript/js_example/templates/base.html deleted file mode 100644 index a4d35bd7..00000000 --- a/examples/javascript/js_example/templates/base.html +++ /dev/null @@ -1,33 +0,0 @@ - -JavaScript Example - - - - -
-

{% block intro %}{% endblock %}

-
-
- - + - - -
-= -{% block script %}{% endblock %} diff --git a/examples/javascript/js_example/templates/fetch.html b/examples/javascript/js_example/templates/fetch.html deleted file mode 100644 index e2944b85..00000000 --- a/examples/javascript/js_example/templates/fetch.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - fetch - is the modern plain JavaScript way to make requests. It's - supported in all modern browsers. -{% endblock %} - -{% block script %} - -{% endblock %} diff --git a/examples/javascript/js_example/templates/jquery.html b/examples/javascript/js_example/templates/jquery.html deleted file mode 100644 index 48f0c11c..00000000 --- a/examples/javascript/js_example/templates/jquery.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - jQuery is a popular library that - adds cross browser APIs for common tasks. However, it requires loading - an extra library. -{% endblock %} - -{% block script %} - - -{% endblock %} diff --git a/examples/javascript/js_example/templates/xhr.html b/examples/javascript/js_example/templates/xhr.html deleted file mode 100644 index 1672d4d6..00000000 --- a/examples/javascript/js_example/templates/xhr.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - XMLHttpRequest - is the original JavaScript way to make requests. It's natively supported - by all browsers, but has been superseded by - fetch. -{% endblock %} - -{% block script %} - -{% endblock %} diff --git a/examples/javascript/js_example/views.py b/examples/javascript/js_example/views.py deleted file mode 100644 index 9f0d26c5..00000000 --- a/examples/javascript/js_example/views.py +++ /dev/null @@ -1,18 +0,0 @@ -from flask import jsonify -from flask import render_template -from flask import request - -from . import app - - -@app.route("/", defaults={"js": "fetch"}) -@app.route("/") -def index(js): - return render_template(f"{js}.html", js=js) - - -@app.route("/add", methods=["POST"]) -def add(): - a = request.form.get("a", 0, type=float) - b = request.form.get("b", 0, type=float) - return jsonify(result=a + b) diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml deleted file mode 100644 index f584e5c8..00000000 --- a/examples/javascript/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -[project] -name = "js_example" -version = "1.1.0" -description = "Demonstrates making AJAX requests to Flask." -readme = "README.rst" -license = {file = "LICENSE.rst"} -maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -dependencies = ["flask"] - -[project.urls] -Documentation = "https://flask.palletsprojects.com/patterns/javascript/" - -[project.optional-dependencies] -test = ["pytest"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "js_example" - -[tool.pytest.ini_options] -testpaths = ["tests"] -filterwarnings = ["error"] - -[tool.coverage.run] -branch = true -source = ["js_example", "tests"] - -[tool.ruff] -src = ["src"] diff --git a/examples/javascript/tests/conftest.py b/examples/javascript/tests/conftest.py deleted file mode 100644 index e0cabbfd..00000000 --- a/examples/javascript/tests/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from js_example import app - - -@pytest.fixture(name="app") -def fixture_app(): - app.testing = True - yield app - app.testing = False - - -@pytest.fixture -def client(app): - return app.test_client() diff --git a/examples/javascript/tests/test_js_example.py b/examples/javascript/tests/test_js_example.py deleted file mode 100644 index d155ad5c..00000000 --- a/examples/javascript/tests/test_js_example.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -from flask import template_rendered - - -@pytest.mark.parametrize( - ("path", "template_name"), - ( - ("/", "xhr.html"), - ("/plain", "xhr.html"), - ("/fetch", "fetch.html"), - ("/jquery", "jquery.html"), - ), -) -def test_index(app, client, path, template_name): - def check(sender, template, context): - assert template.name == template_name - - with template_rendered.connected_to(check, app): - client.get(path) - - -@pytest.mark.parametrize( - ("a", "b", "result"), ((2, 3, 5), (2.5, 3, 5.5), (2, None, 2), (2, "b", 2)) -) -def test_add(client, a, b, result): - response = client.post("/add", data={"a": a, "b": b}) - assert response.get_json()["result"] == result diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore deleted file mode 100644 index a306afbc..00000000 --- a/examples/tutorial/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -.venv/ -*.pyc -__pycache__/ -instance/ -.cache/ -.pytest_cache/ -.coverage -htmlcov/ -dist/ -build/ -*.egg-info/ -.idea/ -*.swp -*~ diff --git a/examples/tutorial/LICENSE.rst b/examples/tutorial/LICENSE.rst deleted file mode 100644 index 9d227a0c..00000000 --- a/examples/tutorial/LICENSE.rst +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml deleted file mode 100644 index 73a674ce..00000000 --- a/examples/tutorial/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -[project] -name = "flaskr" -version = "1.0.0" -description = "The basic blog app built in the Flask tutorial." -readme = "README.rst" -license = {text = "BSD-3-Clause"} -maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -dependencies = [ - "flask", -] - -[project.urls] -Documentation = "https://flask.palletsprojects.com/tutorial/" - -[project.optional-dependencies] -test = ["pytest"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "flaskr" - -[tool.flit.sdist] -include = [ - "tests/", -] - -[tool.pytest.ini_options] -testpaths = ["tests"] -filterwarnings = ["error"] - -[tool.coverage.run] -branch = true -source = ["flaskr", "tests"] - -[tool.ruff] -src = ["src"] diff --git a/examples/tutorial/tests/conftest.py b/examples/tutorial/tests/conftest.py deleted file mode 100644 index 6bf62f0a..00000000 --- a/examples/tutorial/tests/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import tempfile - -import pytest - -from flaskr import create_app -from flaskr.db import get_db -from flaskr.db import init_db - -# read in SQL for populating test data -with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: - _data_sql = f.read().decode("utf8") - - -@pytest.fixture -def app(): - """Create and configure a new app instance for each test.""" - # create a temporary file to isolate the database for each test - db_fd, db_path = tempfile.mkstemp() - # create the app with common test config - app = create_app({"TESTING": True, "DATABASE": db_path}) - - # create the database and load test data - with app.app_context(): - init_db() - get_db().executescript(_data_sql) - - yield app - - # close and remove the temporary database - os.close(db_fd) - os.unlink(db_path) - - -@pytest.fixture -def client(app): - """A test client for the app.""" - return app.test_client() - - -@pytest.fixture -def runner(app): - """A test runner for the app's Click commands.""" - return app.test_cli_runner() - - -class AuthActions: - def __init__(self, client): - self._client = client - - def login(self, username="test", password="test"): - return self._client.post( - "/auth/login", data={"username": username, "password": password} - ) - - def logout(self): - return self._client.get("/auth/logout") - - -@pytest.fixture -def auth(client): - return AuthActions(client) diff --git a/examples/tutorial/flaskr/__init__.py b/flaskr/__init__.py similarity index 100% rename from examples/tutorial/flaskr/__init__.py rename to flaskr/__init__.py diff --git a/examples/tutorial/flaskr/auth.py b/flaskr/auth.py similarity index 100% rename from examples/tutorial/flaskr/auth.py rename to flaskr/auth.py diff --git a/examples/tutorial/flaskr/blog.py b/flaskr/blog.py similarity index 100% rename from examples/tutorial/flaskr/blog.py rename to flaskr/blog.py diff --git a/examples/tutorial/flaskr/db.py b/flaskr/db.py similarity index 100% rename from examples/tutorial/flaskr/db.py rename to flaskr/db.py diff --git a/examples/tutorial/flaskr/schema.sql b/flaskr/schema.sql similarity index 100% rename from examples/tutorial/flaskr/schema.sql rename to flaskr/schema.sql diff --git a/examples/tutorial/flaskr/static/style.css b/flaskr/static/style.css similarity index 100% rename from examples/tutorial/flaskr/static/style.css rename to flaskr/static/style.css diff --git a/examples/tutorial/flaskr/templates/auth/login.html b/flaskr/templates/auth/login.html similarity index 100% rename from examples/tutorial/flaskr/templates/auth/login.html rename to flaskr/templates/auth/login.html diff --git a/examples/tutorial/flaskr/templates/auth/register.html b/flaskr/templates/auth/register.html similarity index 100% rename from examples/tutorial/flaskr/templates/auth/register.html rename to flaskr/templates/auth/register.html diff --git a/examples/tutorial/flaskr/templates/base.html b/flaskr/templates/base.html similarity index 100% rename from examples/tutorial/flaskr/templates/base.html rename to flaskr/templates/base.html diff --git a/examples/tutorial/flaskr/templates/blog/create.html b/flaskr/templates/blog/create.html similarity index 100% rename from examples/tutorial/flaskr/templates/blog/create.html rename to flaskr/templates/blog/create.html diff --git a/examples/tutorial/flaskr/templates/blog/index.html b/flaskr/templates/blog/index.html similarity index 100% rename from examples/tutorial/flaskr/templates/blog/index.html rename to flaskr/templates/blog/index.html diff --git a/examples/tutorial/flaskr/templates/blog/update.html b/flaskr/templates/blog/update.html similarity index 100% rename from examples/tutorial/flaskr/templates/blog/update.html rename to flaskr/templates/blog/update.html diff --git a/pyproject.toml b/pyproject.toml index cddf28cd..73a674ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,124 +1,39 @@ [project] -name = "Flask" -version = "3.1.0.dev" -description = "A simple framework for building complex web applications." -readme = "README.md" -license = {file = "LICENSE.txt"} +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {text = "BSD-3-Clause"} maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Flask", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Typing :: Typed", -] -requires-python = ">=3.8" dependencies = [ - "Werkzeug>=3.0.0", - "Jinja2>=3.1.2", - "itsdangerous>=2.1.2", - "click>=8.1.3", - "blinker>=1.6.2", - "importlib-metadata>=3.6.0; python_version < '3.10'", + "flask", ] [project.urls] -Donate = "https://palletsprojects.com/donate" -Documentation = "https://flask.palletsprojects.com/" -Changes = "https://flask.palletsprojects.com/changes/" -Source = "https://github.com/pallets/flask/" -Chat = "https://discord.gg/pallets" +Documentation = "https://flask.palletsprojects.com/tutorial/" [project.optional-dependencies] -async = ["asgiref>=3.2"] -dotenv = ["python-dotenv"] - -[project.scripts] -flask = "flask.cli:main" +test = ["pytest"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] -name = "flask" +name = "flaskr" [tool.flit.sdist] include = [ - "docs/", - "examples/", - "requirements/", "tests/", - "CHANGES.rst", - "CONTRIBUTING.rst", - "tox.ini", -] -exclude = [ - "docs/_build/", ] [tool.pytest.ini_options] testpaths = ["tests"] -filterwarnings = [ - "error", -] +filterwarnings = ["error"] [tool.coverage.run] branch = true -source = ["flask", "tests"] - -[tool.coverage.paths] -source = ["src", "*/site-packages"] - -[tool.mypy] -python_version = "3.8" -files = ["src/flask", "tests/typing"] -show_error_codes = true -pretty = true -strict = true - -[[tool.mypy.overrides]] -module = [ - "asgiref.*", - "dotenv.*", - "cryptography.*", - "importlib_metadata", -] -ignore_missing_imports = true - -[tool.pyright] -pythonVersion = "3.8" -include = ["src/flask", "tests"] -typeCheckingMode = "basic" +source = ["flaskr", "tests"] [tool.ruff] src = ["src"] -fix = true -show-fixes = true -output-format = "full" - -[tool.ruff.lint] -select = [ - "B", # flake8-bugbear - "E", # pycodestyle error - "F", # pyflakes - "I", # isort - "UP", # pyupgrade - "W", # pycodestyle warning -] - -[tool.ruff.lint.isort] -force-single-line = true -order-by-type = false - -[tool.gha-update] -tag-only = [ - "slsa-framework/slsa-github-generator", -] diff --git a/requirements-skip/README.md b/requirements-skip/README.md deleted file mode 100644 index 675ca4ab..00000000 --- a/requirements-skip/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Dependabot will only update files in the `requirements` directory. This directory is -separate because the pins in here should not be updated automatically. diff --git a/requirements-skip/tests-dev.txt b/requirements-skip/tests-dev.txt deleted file mode 100644 index 3e7f028e..00000000 --- a/requirements-skip/tests-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz -https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz -https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz -https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz -https://github.com/pallets/click/archive/refs/heads/main.tar.gz -https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz diff --git a/requirements-skip/tests-min.in b/requirements-skip/tests-min.in deleted file mode 100644 index c7ec9969..00000000 --- a/requirements-skip/tests-min.in +++ /dev/null @@ -1,6 +0,0 @@ -werkzeug==3.0.0 -jinja2==3.1.2 -markupsafe==2.1.1 -itsdangerous==2.1.2 -click==8.1.3 -blinker==1.6.2 diff --git a/requirements-skip/tests-min.txt b/requirements-skip/tests-min.txt deleted file mode 100644 index 8a6cbf02..00000000 --- a/requirements-skip/tests-min.txt +++ /dev/null @@ -1,21 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile tests-min.in -# -blinker==1.6.2 - # via -r tests-min.in -click==8.1.3 - # via -r tests-min.in -itsdangerous==2.1.2 - # via -r tests-min.in -jinja2==3.1.2 - # via -r tests-min.in -markupsafe==2.1.1 - # via - # -r tests-min.in - # jinja2 - # werkzeug -werkzeug==3.0.0 - # via -r tests-min.in diff --git a/requirements/build.in b/requirements/build.in deleted file mode 100644 index 378eac25..00000000 --- a/requirements/build.in +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/requirements/build.txt b/requirements/build.txt deleted file mode 100644 index 4b289ca7..00000000 --- a/requirements/build.txt +++ /dev/null @@ -1,12 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile build.in -# -build==1.2.1 - # via -r build.in -packaging==24.1 - # via build -pyproject-hooks==1.1.0 - # via build diff --git a/requirements/dev.in b/requirements/dev.in deleted file mode 100644 index 1efde82b..00000000 --- a/requirements/dev.in +++ /dev/null @@ -1,5 +0,0 @@ --r docs.txt --r tests.txt --r typing.txt -pre-commit -tox diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index cb7a407c..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,192 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile dev.in -# -alabaster==1.0.0 - # via - # -r docs.txt - # sphinx -asgiref==3.8.1 - # via - # -r tests.txt - # -r typing.txt -babel==2.16.0 - # via - # -r docs.txt - # sphinx -cachetools==5.5.0 - # via tox -certifi==2024.7.4 - # via - # -r docs.txt - # requests -cffi==1.17.0 - # via - # -r typing.txt - # cryptography -cfgv==3.4.0 - # via pre-commit -chardet==5.2.0 - # via tox -charset-normalizer==3.3.2 - # via - # -r docs.txt - # requests -colorama==0.4.6 - # via tox -cryptography==43.0.0 - # via -r typing.txt -distlib==0.3.8 - # via virtualenv -docutils==0.21.2 - # via - # -r docs.txt - # sphinx - # sphinx-tabs -filelock==3.15.4 - # via - # tox - # virtualenv -identify==2.6.0 - # via pre-commit -idna==3.8 - # via - # -r docs.txt - # requests -imagesize==1.4.1 - # via - # -r docs.txt - # sphinx -iniconfig==2.0.0 - # via - # -r tests.txt - # -r typing.txt - # pytest -jinja2==3.1.4 - # via - # -r docs.txt - # sphinx -markupsafe==2.1.5 - # via - # -r docs.txt - # jinja2 -mypy==1.11.1 - # via -r typing.txt -mypy-extensions==1.0.0 - # via - # -r typing.txt - # mypy -nodeenv==1.9.1 - # via - # -r typing.txt - # pre-commit - # pyright -packaging==24.1 - # via - # -r docs.txt - # -r tests.txt - # -r typing.txt - # pallets-sphinx-themes - # pyproject-api - # pytest - # sphinx - # tox -pallets-sphinx-themes==2.1.3 - # via -r docs.txt -platformdirs==4.2.2 - # via - # tox - # virtualenv -pluggy==1.5.0 - # via - # -r tests.txt - # -r typing.txt - # pytest - # tox -pre-commit==3.8.0 - # via -r dev.in -pycparser==2.22 - # via - # -r typing.txt - # cffi -pygments==2.18.0 - # via - # -r docs.txt - # sphinx - # sphinx-tabs -pyproject-api==1.7.1 - # via tox -pyright==1.1.377 - # via -r typing.txt -pytest==8.3.2 - # via - # -r tests.txt - # -r typing.txt -python-dotenv==1.0.1 - # via - # -r tests.txt - # -r typing.txt -pyyaml==6.0.2 - # via pre-commit -requests==2.32.3 - # via - # -r docs.txt - # sphinx -snowballstemmer==2.2.0 - # via - # -r docs.txt - # sphinx -sphinx==8.0.2 - # via - # -r docs.txt - # pallets-sphinx-themes - # sphinx-tabs - # sphinxcontrib-log-cabinet -sphinx-tabs==3.4.5 - # via -r docs.txt -sphinxcontrib-applehelp==2.0.0 - # via - # -r docs.txt - # sphinx -sphinxcontrib-devhelp==2.0.0 - # via - # -r docs.txt - # sphinx -sphinxcontrib-htmlhelp==2.1.0 - # via - # -r docs.txt - # sphinx -sphinxcontrib-jsmath==1.0.1 - # via - # -r docs.txt - # sphinx -sphinxcontrib-log-cabinet==1.0.1 - # via -r docs.txt -sphinxcontrib-qthelp==2.0.0 - # via - # -r docs.txt - # sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via - # -r docs.txt - # sphinx -tox==4.18.0 - # via -r dev.in -types-contextvars==2.4.7.3 - # via -r typing.txt -types-dataclasses==0.6.6 - # via -r typing.txt -typing-extensions==4.12.2 - # via - # -r typing.txt - # mypy -urllib3==2.2.2 - # via - # -r docs.txt - # requests -virtualenv==20.26.3 - # via - # pre-commit - # tox diff --git a/requirements/docs.in b/requirements/docs.in deleted file mode 100644 index fd5708f7..00000000 --- a/requirements/docs.in +++ /dev/null @@ -1,4 +0,0 @@ -pallets-sphinx-themes -sphinx -sphinxcontrib-log-cabinet -sphinx-tabs diff --git a/requirements/docs.txt b/requirements/docs.txt deleted file mode 100644 index 651e9b2d..00000000 --- a/requirements/docs.txt +++ /dev/null @@ -1,64 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile docs.in -# -alabaster==1.0.0 - # via sphinx -babel==2.16.0 - # via sphinx -certifi==2024.7.4 - # via requests -charset-normalizer==3.3.2 - # via requests -docutils==0.21.2 - # via - # sphinx - # sphinx-tabs -idna==3.8 - # via requests -imagesize==1.4.1 - # via sphinx -jinja2==3.1.4 - # via sphinx -markupsafe==2.1.5 - # via jinja2 -packaging==24.1 - # via - # pallets-sphinx-themes - # sphinx -pallets-sphinx-themes==2.1.3 - # via -r docs.in -pygments==2.18.0 - # via - # sphinx - # sphinx-tabs -requests==2.32.3 - # via sphinx -snowballstemmer==2.2.0 - # via sphinx -sphinx==8.0.2 - # via - # -r docs.in - # pallets-sphinx-themes - # sphinx-tabs - # sphinxcontrib-log-cabinet -sphinx-tabs==3.4.5 - # via -r docs.in -sphinxcontrib-applehelp==2.0.0 - # via sphinx -sphinxcontrib-devhelp==2.0.0 - # via sphinx -sphinxcontrib-htmlhelp==2.1.0 - # via sphinx -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-log-cabinet==1.0.1 - # via -r docs.in -sphinxcontrib-qthelp==2.0.0 - # via sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via sphinx -urllib3==2.2.2 - # via requests diff --git a/requirements/tests.in b/requirements/tests.in deleted file mode 100644 index f4b3dad8..00000000 --- a/requirements/tests.in +++ /dev/null @@ -1,4 +0,0 @@ -pytest -asgiref -greenlet ; python_version < "3.11" -python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt deleted file mode 100644 index 3a24fc51..00000000 --- a/requirements/tests.txt +++ /dev/null @@ -1,18 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile tests.in -# -asgiref==3.8.1 - # via -r tests.in -iniconfig==2.0.0 - # via pytest -packaging==24.1 - # via pytest -pluggy==1.5.0 - # via pytest -pytest==8.3.2 - # via -r tests.in -python-dotenv==1.0.1 - # via -r tests.in diff --git a/requirements/typing.in b/requirements/typing.in deleted file mode 100644 index 59128f34..00000000 --- a/requirements/typing.in +++ /dev/null @@ -1,8 +0,0 @@ -mypy -pyright -pytest -types-contextvars -types-dataclasses -asgiref -cryptography -python-dotenv diff --git a/requirements/typing.txt b/requirements/typing.txt deleted file mode 100644 index 99753c70..00000000 --- a/requirements/typing.txt +++ /dev/null @@ -1,38 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile typing.in -# -asgiref==3.8.1 - # via -r typing.in -cffi==1.17.0 - # via cryptography -cryptography==43.0.0 - # via -r typing.in -iniconfig==2.0.0 - # via pytest -mypy==1.11.1 - # via -r typing.in -mypy-extensions==1.0.0 - # via mypy -nodeenv==1.9.1 - # via pyright -packaging==24.1 - # via pytest -pluggy==1.5.0 - # via pytest -pycparser==2.22 - # via cffi -pyright==1.1.377 - # via -r typing.in -pytest==8.3.2 - # via -r typing.in -python-dotenv==1.0.1 - # via -r typing.in -types-contextvars==2.4.7.3 - # via -r typing.in -types-dataclasses==0.6.6 - # via -r typing.in -typing-extensions==4.12.2 - # via mypy diff --git a/src/flask/__init__.py b/src/flask/__init__.py deleted file mode 100644 index e86eb43e..00000000 --- a/src/flask/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import typing as t - -from . import json as json -from .app import Flask as Flask -from .blueprints import Blueprint as Blueprint -from .config import Config as Config -from .ctx import after_this_request as after_this_request -from .ctx import copy_current_request_context as copy_current_request_context -from .ctx import has_app_context as has_app_context -from .ctx import has_request_context as has_request_context -from .globals import current_app as current_app -from .globals import g as g -from .globals import request as request -from .globals import session as session -from .helpers import abort as abort -from .helpers import flash as flash -from .helpers import get_flashed_messages as get_flashed_messages -from .helpers import get_template_attribute as get_template_attribute -from .helpers import make_response as make_response -from .helpers import redirect as redirect -from .helpers import send_file as send_file -from .helpers import send_from_directory as send_from_directory -from .helpers import stream_with_context as stream_with_context -from .helpers import url_for as url_for -from .json import jsonify as jsonify -from .signals import appcontext_popped as appcontext_popped -from .signals import appcontext_pushed as appcontext_pushed -from .signals import appcontext_tearing_down as appcontext_tearing_down -from .signals import before_render_template as before_render_template -from .signals import got_request_exception as got_request_exception -from .signals import message_flashed as message_flashed -from .signals import request_finished as request_finished -from .signals import request_started as request_started -from .signals import request_tearing_down as request_tearing_down -from .signals import template_rendered as template_rendered -from .templating import render_template as render_template -from .templating import render_template_string as render_template_string -from .templating import stream_template as stream_template -from .templating import stream_template_string as stream_template_string -from .wrappers import Request as Request -from .wrappers import Response as Response - - -def __getattr__(name: str) -> t.Any: - if name == "__version__": - import importlib.metadata - import warnings - - warnings.warn( - "The '__version__' attribute is deprecated and will be removed in" - " Flask 3.1. Use feature detection or" - " 'importlib.metadata.version(\"flask\")' instead.", - DeprecationWarning, - stacklevel=2, - ) - return importlib.metadata.version("flask") - - raise AttributeError(name) diff --git a/src/flask/__main__.py b/src/flask/__main__.py deleted file mode 100644 index 4e28416e..00000000 --- a/src/flask/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .cli import main - -main() diff --git a/src/flask/app.py b/src/flask/app.py deleted file mode 100644 index 53eb602c..00000000 --- a/src/flask/app.py +++ /dev/null @@ -1,1515 +0,0 @@ -from __future__ import annotations - -import collections.abc as cabc -import os -import sys -import typing as t -import weakref -from datetime import timedelta -from inspect import iscoroutinefunction -from itertools import chain -from types import TracebackType -from urllib.parse import quote as _url_quote - -import click -from werkzeug.datastructures import Headers -from werkzeug.datastructures import ImmutableDict -from werkzeug.exceptions import BadRequestKeyError -from werkzeug.exceptions import HTTPException -from werkzeug.exceptions import InternalServerError -from werkzeug.routing import BuildError -from werkzeug.routing import MapAdapter -from werkzeug.routing import RequestRedirect -from werkzeug.routing import RoutingException -from werkzeug.routing import Rule -from werkzeug.serving import is_running_from_reloader -from werkzeug.wrappers import Response as BaseResponse - -from . import cli -from . import typing as ft -from .ctx import AppContext -from .ctx import RequestContext -from .globals import _cv_app -from .globals import _cv_request -from .globals import current_app -from .globals import g -from .globals import request -from .globals import request_ctx -from .globals import session -from .helpers import get_debug_flag -from .helpers import get_flashed_messages -from .helpers import get_load_dotenv -from .helpers import send_from_directory -from .sansio.app import App -from .sansio.scaffold import _sentinel -from .sessions import SecureCookieSessionInterface -from .sessions import SessionInterface -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 Environment -from .wrappers import Request -from .wrappers import Response - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIEnvironment - - from .testing import FlaskClient - from .testing import FlaskCliRunner - -T_shell_context_processor = t.TypeVar( - "T_shell_context_processor", bound=ft.ShellContextProcessorCallable -) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) - - -def _make_timedelta(value: timedelta | int | None) -> timedelta | None: - if value is None or isinstance(value, timedelta): - return value - - return timedelta(seconds=value) - - -class Flask(App): - """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 - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the :file:`__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea of what - belongs to your application. This name is used to find resources - on the filesystem, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in :file:`yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplication.app` and not - `yourapplication.views.frontend`) - - .. versionadded:: 0.7 - The `static_url_path`, `static_folder`, and `template_folder` - parameters were added. - - .. versionadded:: 0.8 - The `instance_path` and `instance_relative_config` parameters were - added. - - .. versionadded:: 0.11 - The `root_path` parameter was added. - - .. versionadded:: 1.0 - The ``host_matching`` and ``static_host`` parameters were added. - - .. versionadded:: 1.0 - The ``subdomain_matching`` parameter was added. Subdomain - matching needs to be enabled manually now. Setting - :data:`SERVER_NAME` does not implicitly enable it. - - :param import_name: the name of the application package - :param static_url_path: can be used to specify a different path for the - static files on the web. Defaults to the name - of the `static_folder` folder. - :param static_folder: The folder with static files that is served at - ``static_url_path``. Relative to the application ``root_path`` - or an absolute path. Defaults to ``'static'``. - :param static_host: the host to use when adding the static route. - Defaults to None. Required when using ``host_matching=True`` - with a ``static_folder`` configured. - :param host_matching: set ``url_map.host_matching`` attribute. - Defaults to False. - :param subdomain_matching: consider the subdomain relative to - :data:`SERVER_NAME` when matching routes. Defaults to False. - :param template_folder: the folder that contains the templates that should - be used by the application. Defaults to - ``'templates'`` folder in the root path of the - application. - :param instance_path: An alternative instance path for the application. - By default the folder ``'instance'`` next to the - package or module is assumed to be the instance - path. - :param instance_relative_config: if set to ``True`` relative filenames - for loading the config are assumed to - be relative to the instance path instead - of the application root. - :param root_path: The path to the root of the application files. - This should only be set manually when it can't be detected - automatically, such as for namespace packages. - """ - - default_config = ImmutableDict( - { - "DEBUG": None, - "TESTING": False, - "PROPAGATE_EXCEPTIONS": None, - "SECRET_KEY": None, - "PERMANENT_SESSION_LIFETIME": timedelta(days=31), - "USE_X_SENDFILE": False, - "SERVER_NAME": None, - "APPLICATION_ROOT": "/", - "SESSION_COOKIE_NAME": "session", - "SESSION_COOKIE_DOMAIN": None, - "SESSION_COOKIE_PATH": None, - "SESSION_COOKIE_HTTPONLY": True, - "SESSION_COOKIE_SECURE": False, - "SESSION_COOKIE_SAMESITE": None, - "SESSION_REFRESH_EACH_REQUEST": True, - "MAX_CONTENT_LENGTH": None, - "SEND_FILE_MAX_AGE_DEFAULT": None, - "TRAP_BAD_REQUEST_ERRORS": None, - "TRAP_HTTP_EXCEPTIONS": False, - "EXPLAIN_TEMPLATE_LOADING": False, - "PREFERRED_URL_SCHEME": "http", - "TEMPLATES_AUTO_RELOAD": None, - "MAX_COOKIE_SIZE": 4093, - "PROVIDE_AUTOMATIC_OPTIONS": True, - } - ) - - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class: type[Request] = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class: type[Response] = Response - - #: the session interface to use. By default an instance of - #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. - #: - #: .. versionadded:: 0.8 - session_interface: SessionInterface = SecureCookieSessionInterface() - - def __init__( - self, - import_name: str, - static_url_path: str | None = None, - static_folder: str | os.PathLike[str] | None = "static", - static_host: str | None = None, - host_matching: bool = False, - subdomain_matching: bool = False, - template_folder: str | os.PathLike[str] | None = "templates", - instance_path: str | None = None, - instance_relative_config: bool = False, - root_path: str | None = None, - ): - super().__init__( - import_name=import_name, - static_url_path=static_url_path, - static_folder=static_folder, - static_host=static_host, - host_matching=host_matching, - subdomain_matching=subdomain_matching, - template_folder=template_folder, - instance_path=instance_path, - instance_relative_config=instance_relative_config, - root_path=root_path, - ) - - #: The Click command group for registering CLI commands for this - #: object. The commands are available from the ``flask`` command - #: once the application has been discovered and blueprints have - #: been registered. - self.cli = cli.AppGroup() - - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name - - # Add a static route using the provided static_url_path, static_host, - # and static_folder if there is a configured static_folder. - # Note we do this without checking if static_folder exists. - # For one, it might be created while the server is running (e.g. during - # development). Also, Google App Engine stores static files somewhere - if self.has_static_folder: - assert ( - bool(static_host) == host_matching - ), "Invalid static_host/host_matching combination" - # Use a weakref to avoid creating a reference cycle between the app - # and the view function (see #3761). - self_ref = weakref.ref(self) - self.add_url_rule( - f"{self.static_url_path}/", - endpoint="static", - host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 - ) - - def get_send_file_max_age(self, filename: str | None) -> int | None: - """Used by :func:`send_file` to determine the ``max_age`` cache - value for a given file path if it wasn't passed. - - By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from - the configuration of :data:`~flask.current_app`. This defaults - to ``None``, which tells the browser to use conditional requests - instead of a timed cache, which is usually preferable. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. - - .. versionadded:: 0.9 - """ - value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] - - if value is None: - return None - - if isinstance(value, timedelta): - return int(value.total_seconds()) - - return value # type: ignore[no-any-return] - - def send_static_file(self, filename: str) -> Response: - """The view function used to serve files from - :attr:`static_folder`. A route is automatically registered for - this view at :attr:`static_url_path` if :attr:`static_folder` is - set. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionadded:: 0.5 - - """ - if not self.has_static_folder: - raise RuntimeError("'static_folder' must be set to serve static_files.") - - # send_file only knows to call get_send_file_max_age on the app, - # call it here so it works for blueprints too. - max_age = self.get_send_file_max_age(filename) - return send_from_directory( - t.cast(str, self.static_folder), filename, max_age=max_age - ) - - def open_resource( - self, resource: str, mode: str = "rb", encoding: str | None = None - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to :attr:`root_path` for reading. - - For example, if the file ``schema.sql`` is next to the file - ``app.py`` where the ``Flask`` app is defined, it can be opened - with: - - .. code-block:: python - - with app.open_resource("schema.sql") as f: - conn.executescript(f.read()) - - :param resource: Path to the resource relative to :attr:`root_path`. - :param mode: Open the file in this mode. Only reading is supported, - valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") - - path = os.path.join(self.root_path, resource) - - if mode == "rb": - return open(path, mode) - - return open(path, mode, encoding=encoding) - - def open_instance_resource( - self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to the application's instance folder - :attr:`instance_path`. Unlike :meth:`open_resource`, files in the - instance folder can be opened for writing. - - :param resource: Path to the resource relative to :attr:`instance_path`. - :param mode: Open the file in this mode. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - path = os.path.join(self.instance_path, resource) - - if "b" in mode: - return open(path, mode) - - return open(path, mode, encoding=encoding) - - def create_jinja_environment(self) -> Environment: - """Create the Jinja environment based on :attr:`jinja_options` - and the various Jinja-related methods of the app. Changing - :attr:`jinja_options` after this will have no effect. Also adds - Flask-related globals and filters to the environment. - - .. versionchanged:: 0.11 - ``Environment.auto_reload`` set in accordance with - ``TEMPLATES_AUTO_RELOAD`` configuration option. - - .. versionadded:: 0.5 - """ - options = dict(self.jinja_options) - - if "autoescape" not in options: - options["autoescape"] = self.select_jinja_autoescape - - if "auto_reload" not in options: - auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] - - if auto_reload is None: - auto_reload = self.debug - - options["auto_reload"] = auto_reload - - rv = self.jinja_environment(self, **options) - rv.globals.update( - url_for=self.url_for, - get_flashed_messages=get_flashed_messages, - config=self.config, - # request, session and g are normally added with the - # context processor for efficiency reasons but for imported - # templates we also want the proxies in there. - request=request, - session=session, - g=g, - ) - rv.policies["json.dumps_function"] = self.json.dumps - return rv - - def create_url_adapter(self, request: Request | None) -> MapAdapter | None: - """Creates a URL adapter for the given request. The URL adapter - is created at a point where the request context is not yet set - up so the request is passed explicitly. - - .. versionadded:: 0.6 - - .. versionchanged:: 0.9 - This can now also be called without a request object when the - URL adapter is created for the application context. - - .. versionchanged:: 1.0 - :data:`SERVER_NAME` no longer implicitly enables subdomain - matching. Use :attr:`subdomain_matching` instead. - """ - if request is not None: - # If subdomain matching is disabled (the default), use the - # default subdomain in all cases. This should be the default - # in Werkzeug but it currently does not have that feature. - if not self.subdomain_matching: - subdomain = self.url_map.default_subdomain or None - else: - subdomain = None - - return self.url_map.bind_to_environ( - request.environ, - server_name=self.config["SERVER_NAME"], - subdomain=subdomain, - ) - # We need at the very least the server name to be set for this - # to work. - if self.config["SERVER_NAME"] is not None: - return self.url_map.bind( - self.config["SERVER_NAME"], - script_name=self.config["APPLICATION_ROOT"], - url_scheme=self.config["PREFERRED_URL_SCHEME"], - ) - - return None - - def raise_routing_exception(self, request: Request) -> t.NoReturn: - """Intercept routing exceptions and possibly do something else. - - In debug mode, intercept a routing redirect and replace it with - an error if the body will be discarded. - - With modern Werkzeug this shouldn't occur, since it now uses a - 308 status which tells the browser to resend the method and - body. - - .. versionchanged:: 2.1 - Don't intercept 307 and 308 redirects. - - :meta private: - :internal: - """ - if ( - not self.debug - or not isinstance(request.routing_exception, RequestRedirect) - or request.routing_exception.code in {307, 308} - or request.method in {"GET", "HEAD", "OPTIONS"} - ): - raise request.routing_exception # type: ignore[misc] - - from .debughelpers import FormDataRoutingRedirect - - raise FormDataRoutingRedirect(request) - - def update_template_context(self, context: dict[str, t.Any]) -> None: - """Update the template context with some commonly used variables. - This injects request, session, config and g into the template - context as well as everything template context processors want - to inject. Note that the as of Flask 0.6, the original values - in the context will not be overridden if a context processor - decides to return a value with the same key. - - :param context: the context as a dictionary that is updated in place - to add extra variables. - """ - names: t.Iterable[str | None] = (None,) - - # A template may be rendered outside a request context. - if request: - names = chain(names, reversed(request.blueprints)) - - # The values passed to render_template take precedence. Keep a - # copy to re-apply after all context functions. - orig_ctx = context.copy() - - for name in names: - if name in self.template_context_processors: - for func in self.template_context_processors[name]: - context.update(self.ensure_sync(func)()) - - context.update(orig_ctx) - - def make_shell_context(self) -> dict[str, t.Any]: - """Returns the shell context for an interactive shell for this - application. This runs all the registered shell context - processors. - - .. versionadded:: 0.11 - """ - rv = {"app": self, "g": g} - for processor in self.shell_context_processors: - rv.update(processor()) - return rv - - def run( - self, - host: str | None = None, - port: int | None = None, - debug: bool | None = None, - load_dotenv: bool = True, - **options: t.Any, - ) -> None: - """Runs the application on a local development server. - - Do not use ``run()`` in a production setting. It is not intended to - meet security and performance requirements for a production server. - Instead, see :doc:`/deploying/index` for WSGI server recommendations. - - If the :attr:`debug` flag is set the server will automatically reload - for code changes and show a debugger in case an exception happened. - - If you want to run the application in debug mode, but disable the - code execution on the interactive debugger, you can pass - ``use_evalex=False`` as parameter. This will keep the debugger's - traceback screen active, but disable code execution. - - It is not recommended to use this function for development with - automatic reloading as this is badly supported. Instead you should - be using the :command:`flask` command line script's ``run`` support. - - .. admonition:: Keep in Mind - - Flask will suppress any server error with a generic error page - unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you have to - invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. - Setting ``use_debugger`` to ``True`` without being in debug mode - won't catch any exceptions because there won't be any to - catch. - - :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to - have the server available externally as well. Defaults to - ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable - if present. - :param port: the port of the webserver. Defaults to ``5000`` or the - port defined in the ``SERVER_NAME`` config variable if present. - :param debug: if given, enable or disable debug mode. See - :attr:`debug`. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param options: the options to be forwarded to the underlying Werkzeug - server. See :func:`werkzeug.serving.run_simple` for more - information. - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment - variables from :file:`.env` and :file:`.flaskenv` files. - - The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. - - Threaded mode is enabled by default. - - .. versionchanged:: 0.10 - The default port is now picked from the ``SERVER_NAME`` - variable. - """ - # Ignore this call so that it doesn't start another server if - # the 'flask run' command is used. - if os.environ.get("FLASK_RUN_FROM_CLI") == "true": - if not is_running_from_reloader(): - click.secho( - " * Ignoring a call to 'app.run()' that would block" - " the current 'flask' CLI command.\n" - " Only call 'app.run()' in an 'if __name__ ==" - ' "__main__"\' guard.', - fg="red", - ) - - return - - if get_load_dotenv(load_dotenv): - cli.load_dotenv() - - # if set, env var overrides existing value - if "FLASK_DEBUG" in os.environ: - self.debug = get_debug_flag() - - # debug passed to method overrides all other sources - if debug is not None: - self.debug = bool(debug) - - server_name = self.config.get("SERVER_NAME") - sn_host = sn_port = None - - if server_name: - sn_host, _, sn_port = server_name.partition(":") - - if not host: - if sn_host: - host = sn_host - else: - host = "127.0.0.1" - - if port or port == 0: - port = int(port) - elif sn_port: - port = int(sn_port) - else: - port = 5000 - - options.setdefault("use_reloader", self.debug) - options.setdefault("use_debugger", self.debug) - options.setdefault("threaded", True) - - cli.show_server_banner(self.debug, self.name) - - from werkzeug.serving import run_simple - - try: - run_simple(t.cast(str, host), port, self, **options) - finally: - # reset the first request information if the development server - # reset normally. This makes it possible to restart the server - # without reloader and that stuff from an interactive shell. - self._got_first_request = False - - def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: - """Creates a test client for this application. For information - about unit testing head over to :doc:`/testing`. - - Note that if you are testing for assertions or exceptions in your - application code, you must set ``app.testing = True`` in order for the - exceptions to propagate to the test client. Otherwise, the exception - will be handled by the application (not visible to the test client) and - the only indication of an AssertionError or other exception will be a - 500 status code response to the test client. See the :attr:`testing` - attribute. For example:: - - app.testing = True - client = app.test_client() - - The test client can be used in a ``with`` block to defer the closing down - of the context until the end of the ``with`` block. This is useful if - you want to access the context locals for testing:: - - with app.test_client() as c: - rv = c.get('/?vodka=42') - assert request.args['vodka'] == '42' - - Additionally, you may pass optional keyword arguments that will then - be passed to the application's :attr:`test_client_class` constructor. - For example:: - - from flask.testing import FlaskClient - - class CustomClient(FlaskClient): - def __init__(self, *args, **kwargs): - self._authentication = kwargs.pop("authentication") - super(CustomClient,self).__init__( *args, **kwargs) - - app.test_client_class = CustomClient - client = app.test_client(authentication='Basic ....') - - See :class:`~flask.testing.FlaskClient` for more information. - - .. versionchanged:: 0.4 - added support for ``with`` block usage for the client. - - .. versionadded:: 0.7 - The `use_cookies` parameter was added as well as the ability - to override the client to be used by setting the - :attr:`test_client_class` attribute. - - .. versionchanged:: 0.11 - Added `**kwargs` to support passing additional keyword arguments to - the constructor of :attr:`test_client_class`. - """ - cls = self.test_client_class - if cls is None: - from .testing import FlaskClient as cls - return cls( # type: ignore - self, self.response_class, use_cookies=use_cookies, **kwargs - ) - - def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: - """Create a CLI runner for testing CLI commands. - See :ref:`testing-cli`. - - Returns an instance of :attr:`test_cli_runner_class`, by default - :class:`~flask.testing.FlaskCliRunner`. The Flask app object is - passed as the first argument. - - .. versionadded:: 1.0 - """ - cls = self.test_cli_runner_class - - if cls is None: - from .testing import FlaskCliRunner as cls - - return cls(self, **kwargs) # type: ignore - - def handle_http_exception( - self, e: HTTPException - ) -> HTTPException | ft.ResponseReturnValue: - """Handles an HTTP exception. By default this will invoke the - registered error handlers and fall back to returning the - exception as response. - - .. versionchanged:: 1.0.3 - ``RoutingException``, used internally for actions such as - slash redirects during routing, is not passed to error - handlers. - - .. versionchanged:: 1.0 - Exceptions are looked up by code *and* by MRO, so - ``HTTPException`` subclasses can be handled with a catch-all - handler for the base ``HTTPException``. - - .. versionadded:: 0.3 - """ - # Proxy exceptions don't have error codes. We want to always return - # those unchanged as errors - if e.code is None: - return e - - # RoutingExceptions are used internally to trigger routing - # actions, such as slash redirects raising RequestRedirect. They - # are not raised or handled in user code. - if isinstance(e, RoutingException): - return e - - handler = self._find_error_handler(e, request.blueprints) - if handler is None: - return e - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] - - def handle_user_exception( - self, e: Exception - ) -> HTTPException | ft.ResponseReturnValue: - """This method is called whenever an exception occurs that - should be handled. A special case is :class:`~werkzeug - .exceptions.HTTPException` which is forwarded to the - :meth:`handle_http_exception` method. This function will either - return a response value or reraise the exception with the same - traceback. - - .. versionchanged:: 1.0 - Key errors raised from request data like ``form`` show the - bad key in debug mode rather than a generic bad request - message. - - .. versionadded:: 0.7 - """ - if isinstance(e, BadRequestKeyError) and ( - self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] - ): - e.show_exception = True - - if isinstance(e, HTTPException) and not self.trap_http_exception(e): - return self.handle_http_exception(e) - - handler = self._find_error_handler(e, request.blueprints) - - if handler is None: - raise - - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] - - def handle_exception(self, e: Exception) -> Response: - """Handle an exception that did not have an error handler - associated with it, or that was raised from an error handler. - This always causes a 500 ``InternalServerError``. - - Always sends the :data:`got_request_exception` signal. - - If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug - mode, the error will be re-raised so that the debugger can - display it. Otherwise, the original exception is logged, and - an :exc:`~werkzeug.exceptions.InternalServerError` is returned. - - If an error handler is registered for ``InternalServerError`` or - ``500``, it will be used. For consistency, the handler will - always receive the ``InternalServerError``. The original - unhandled exception is available as ``e.original_exception``. - - .. versionchanged:: 1.1.0 - Always passes the ``InternalServerError`` instance to the - handler, setting ``original_exception`` to the unhandled - error. - - .. versionchanged:: 1.1.0 - ``after_request`` functions and other finalization is done - even for the default 500 response when there is no handler. - - .. versionadded:: 0.3 - """ - exc_info = sys.exc_info() - got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) - propagate = self.config["PROPAGATE_EXCEPTIONS"] - - if propagate is None: - propagate = self.testing or self.debug - - if propagate: - # Re-raise if called with an active exception, otherwise - # raise the passed in exception. - if exc_info[1] is e: - raise - - raise e - - self.log_exception(exc_info) - server_error: InternalServerError | ft.ResponseReturnValue - server_error = InternalServerError(original_exception=e) - handler = self._find_error_handler(server_error, request.blueprints) - - if handler is not None: - server_error = self.ensure_sync(handler)(server_error) - - return self.finalize_request(server_error, from_error_handler=True) - - def log_exception( - self, - exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), - ) -> None: - """Logs an exception. This is called by :meth:`handle_exception` - if debugging is disabled and right before the handler is called. - The default implementation logs the exception as error on the - :attr:`logger`. - - .. versionadded:: 0.8 - """ - self.logger.error( - f"Exception on {request.path} [{request.method}]", exc_info=exc_info - ) - - def dispatch_request(self) -> ft.ResponseReturnValue: - """Does the request dispatching. Matches the URL and returns the - return value of the view or error handler. This does not have to - be a response object. In order to convert the return value to a - proper response object, call :func:`make_response`. - - .. versionchanged:: 0.7 - This no longer does the exception handling, this code was - moved to the new :meth:`full_dispatch_request`. - """ - req = request_ctx.request - if req.routing_exception is not None: - self.raise_routing_exception(req) - rule: Rule = req.url_rule # type: ignore[assignment] - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if ( - getattr(rule, "provide_automatic_options", False) - and req.method == "OPTIONS" - ): - return self.make_default_options_response() - # otherwise dispatch to the handler for that endpoint - view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] - - def full_dispatch_request(self) -> Response: - """Dispatches the request and on top of that performs request - pre and postprocessing as well as HTTP exception catching and - error handling. - - .. versionadded:: 0.7 - """ - self._got_first_request = True - - try: - request_started.send(self, _async_wrapper=self.ensure_sync) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - except Exception as e: - rv = self.handle_user_exception(e) - return self.finalize_request(rv) - - def finalize_request( - self, - rv: ft.ResponseReturnValue | HTTPException, - from_error_handler: bool = False, - ) -> Response: - """Given the return value from a view function this finalizes - the request by converting it into a response and invoking the - postprocessing functions. This is invoked for both normal - request dispatching as well as error handlers. - - Because this means that it might be called as a result of a - failure a special safe mode is available which can be enabled - with the `from_error_handler` flag. If enabled, failures in - response processing will be logged and otherwise ignored. - - :internal: - """ - response = self.make_response(rv) - try: - response = self.process_response(response) - request_finished.send( - self, _async_wrapper=self.ensure_sync, response=response - ) - except Exception: - if not from_error_handler: - raise - self.logger.exception( - "Request finalizing failed with an error while handling an error" - ) - return response - - def make_default_options_response(self) -> Response: - """This method is called to create the default ``OPTIONS`` response. - This can be changed through subclassing to change the default - behavior of ``OPTIONS`` responses. - - .. versionadded:: 0.7 - """ - adapter = request_ctx.url_adapter - methods = adapter.allowed_methods() # type: ignore[union-attr] - rv = self.response_class() - rv.allow.update(methods) - return rv - - def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: - """Ensure that the function is synchronous for WSGI workers. - Plain ``def`` functions are returned as-is. ``async def`` - functions are wrapped to run and wait for the response. - - Override this method to change how the app runs async views. - - .. versionadded:: 2.0 - """ - if iscoroutinefunction(func): - return self.async_to_sync(func) - - return func - - def async_to_sync( - self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] - ) -> t.Callable[..., t.Any]: - """Return a sync function that will run the coroutine function. - - .. code-block:: python - - result = app.async_to_sync(func)(*args, **kwargs) - - Override this method to change how the app converts async code - to be synchronously callable. - - .. versionadded:: 2.0 - """ - try: - from asgiref.sync import async_to_sync as asgiref_async_to_sync - except ImportError: - raise RuntimeError( - "Install Flask with the 'async' extra in order to use async views." - ) from None - - return asgiref_async_to_sync(func) - - def url_for( - self, - /, - endpoint: str, - *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, - **values: t.Any, - ) -> str: - """Generate a URL to the given endpoint with the given values. - - This is called by :func:`flask.url_for`, and can be called - directly as well. - - An *endpoint* is the name of a URL rule, usually added with - :meth:`@app.route() `, and usually the same name as the - view function. A route defined in a :class:`~flask.Blueprint` - will prepend the blueprint's name separated by a ``.`` to the - endpoint. - - In some cases, such as email messages, you want URLs to include - the scheme and domain, like ``https://example.com/hello``. When - not in an active request, URLs will be external by default, but - this requires setting :data:`SERVER_NAME` so Flask knows what - domain to use. :data:`APPLICATION_ROOT` and - :data:`PREFERRED_URL_SCHEME` should also be configured as - needed. This config is only used when not in an active request. - - Functions can be decorated with :meth:`url_defaults` to modify - keyword arguments before the URL is built. - - If building fails for some reason, such as an unknown endpoint - or incorrect values, the app's :meth:`handle_url_build_error` - method is called. If that returns a string, that is returned, - otherwise a :exc:`~werkzeug.routing.BuildError` is raised. - - :param endpoint: The endpoint name associated with the URL to - generate. If this starts with a ``.``, the current blueprint - name (if any) will be used. - :param _anchor: If given, append this as ``#anchor`` to the URL. - :param _method: If given, generate the URL associated with this - method for the endpoint. - :param _scheme: If given, the URL will have this scheme if it - is external. - :param _external: If given, prefer the URL to be internal - (False) or require it to be external (True). External URLs - include the scheme and domain. When not in an active - request, URLs are external by default. - :param values: Values to use for the variable parts of the URL - rule. Unknown keys are appended as query string arguments, - like ``?a=b&c=d``. - - .. versionadded:: 2.2 - Moved from ``flask.url_for``, which calls this method. - """ - req_ctx = _cv_request.get(None) - - if req_ctx is not None: - url_adapter = req_ctx.url_adapter - blueprint_name = req_ctx.request.blueprint - - # If the endpoint starts with "." and the request matches a - # blueprint, the endpoint is relative to the blueprint. - if endpoint[:1] == ".": - if blueprint_name is not None: - endpoint = f"{blueprint_name}{endpoint}" - else: - endpoint = endpoint[1:] - - # When in a request, generate a URL without scheme and - # domain by default, unless a scheme is given. - if _external is None: - _external = _scheme is not None - else: - app_ctx = _cv_app.get(None) - - # If called by helpers.url_for, an app context is active, - # use its url_adapter. Otherwise, app.url_for was called - # directly, build an adapter. - if app_ctx is not None: - url_adapter = app_ctx.url_adapter - else: - url_adapter = self.create_url_adapter(None) - - if url_adapter is None: - raise RuntimeError( - "Unable to build URLs outside an active request" - " without 'SERVER_NAME' configured. Also configure" - " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" - " needed." - ) - - # When outside a request, generate a URL with scheme and - # domain by default. - if _external is None: - _external = True - - # It is an error to set _scheme when _external=False, in order - # to avoid accidental insecure URLs. - if _scheme is not None and not _external: - raise ValueError("When specifying '_scheme', '_external' must be True.") - - self.inject_url_defaults(endpoint, values) - - try: - rv = url_adapter.build( # type: ignore[union-attr] - endpoint, - values, - method=_method, - url_scheme=_scheme, - force_external=_external, - ) - except BuildError as error: - values.update( - _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external - ) - return self.handle_url_build_error(error, endpoint, values) - - if _anchor is not None: - _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") - rv = f"{rv}#{_anchor}" - - return rv - - def make_response(self, rv: ft.ResponseReturnValue) -> Response: - """Convert the return value from a view function to an instance of - :attr:`response_class`. - - :param rv: the return value from the view function. The view function - must return a response. Returning ``None``, or the view ending - without returning, is not allowed. The following types are allowed - for ``view_rv``: - - ``str`` - A response object is created with the string encoded to UTF-8 - as the body. - - ``bytes`` - A response object is created with the bytes as the body. - - ``dict`` - A dictionary that will be jsonify'd before being returned. - - ``list`` - A list that will be jsonify'd before being returned. - - ``generator`` or ``iterator`` - A generator that returns ``str`` or ``bytes`` to be - streamed as the response. - - ``tuple`` - Either ``(body, status, headers)``, ``(body, status)``, or - ``(body, headers)``, where ``body`` is any of the other types - allowed here, ``status`` is a string or an integer, and - ``headers`` is a dictionary or a list of ``(key, value)`` - tuples. If ``body`` is a :attr:`response_class` instance, - ``status`` overwrites the exiting value and ``headers`` are - extended. - - :attr:`response_class` - The object is returned unchanged. - - other :class:`~werkzeug.wrappers.Response` class - The object is coerced to :attr:`response_class`. - - :func:`callable` - The function is called as a WSGI application. The result is - used to create a response object. - - .. versionchanged:: 2.2 - A generator will be converted to a streaming response. - A list will be converted to a JSON response. - - .. versionchanged:: 1.1 - A dict will be converted to a JSON response. - - .. versionchanged:: 0.9 - Previously a tuple was interpreted as the arguments for the - response object. - """ - - status = headers = None - - # unpack tuple returns - if isinstance(rv, tuple): - len_rv = len(rv) - - # a 3-tuple is unpacked directly - if len_rv == 3: - rv, status, headers = rv # type: ignore[misc] - # decide if a 2-tuple has status or headers - elif len_rv == 2: - if isinstance(rv[1], (Headers, dict, tuple, list)): - rv, headers = rv - else: - rv, status = rv # type: ignore[assignment,misc] - # other sized tuples are not allowed - else: - raise TypeError( - "The view function did not return a valid response tuple." - " The tuple must have the form (body, status, headers)," - " (body, status), or (body, headers)." - ) - - # the body must not be None - if rv is None: - raise TypeError( - f"The view function for {request.endpoint!r} did not" - " return a valid response. The function either returned" - " None or ended without a return statement." - ) - - # make sure the body is an instance of the response class - if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): - # let the response class set the status and headers instead of - # waiting to do it manually, so that the class can handle any - # special logic - rv = self.response_class( - rv, - status=status, - headers=headers, # type: ignore[arg-type] - ) - status = headers = None - elif isinstance(rv, (dict, list)): - rv = self.json.response(rv) - elif isinstance(rv, BaseResponse) or callable(rv): - # evaluate a WSGI callable, or coerce a different response - # class to the correct type - try: - rv = self.response_class.force_type( - rv, # type: ignore[arg-type] - request.environ, - ) - except TypeError as e: - raise TypeError( - f"{e}\nThe view function did not return a valid" - " response. The return type must be a string," - " dict, list, tuple with headers or status," - " Response instance, or WSGI callable, but it" - f" was a {type(rv).__name__}." - ).with_traceback(sys.exc_info()[2]) from None - else: - raise TypeError( - "The view function did not return a valid" - " response. The return type must be a string," - " dict, list, tuple with headers or status," - " Response instance, or WSGI callable, but it was a" - f" {type(rv).__name__}." - ) - - rv = t.cast(Response, rv) - # prefer the status if it was provided - if status is not None: - if isinstance(status, (str, bytes, bytearray)): - rv.status = status - else: - rv.status_code = status - - # extend existing headers with provided headers - if headers: - rv.headers.update(headers) # type: ignore[arg-type] - - return rv - - def preprocess_request(self) -> ft.ResponseReturnValue | None: - """Called before the request is dispatched. Calls - :attr:`url_value_preprocessors` registered with the app and the - current blueprint (if any). Then calls :attr:`before_request_funcs` - registered with the app and the blueprint. - - If any :meth:`before_request` handler 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. - """ - names = (None, *reversed(request.blueprints)) - - for name in names: - if name in self.url_value_preprocessors: - for url_func in self.url_value_preprocessors[name]: - url_func(request.endpoint, request.view_args) - - for name in names: - if name in self.before_request_funcs: - for before_func in self.before_request_funcs[name]: - rv = self.ensure_sync(before_func)() - - if rv is not None: - return rv # type: ignore[no-any-return] - - return None - - def process_response(self, response: Response) -> Response: - """Can be overridden in order to modify the response object - before it's sent to the WSGI server. By default this will - call all the :meth:`after_request` decorated functions. - - .. versionchanged:: 0.5 - As of Flask 0.5 the functions registered for after request - execution are called in reverse order of registration. - - :param response: a :attr:`response_class` object. - :return: a new response object or the same, has to be an - instance of :attr:`response_class`. - """ - ctx = request_ctx._get_current_object() # type: ignore[attr-defined] - - for func in ctx._after_request_functions: - response = self.ensure_sync(func)(response) - - for name in chain(request.blueprints, (None,)): - if name in self.after_request_funcs: - for func in reversed(self.after_request_funcs[name]): - response = self.ensure_sync(func)(response) - - if not self.session_interface.is_null_session(ctx.session): - self.session_interface.save_session(self, ctx.session, response) - - return response - - def do_teardown_request( - self, - exc: BaseException | None = _sentinel, # type: ignore[assignment] - ) -> None: - """Called after the request is dispatched and the response is - returned, right before the request context is popped. - - This calls all functions decorated with - :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` - if a blueprint handled the request. Finally, the - :data:`request_tearing_down` signal is sent. - - This is called by - :meth:`RequestContext.pop() `, - which may be delayed during testing to maintain access to - resources. - - :param exc: An unhandled exception raised while dispatching the - request. Detected from the current exception information if - not passed. Passed to each teardown function. - - .. versionchanged:: 0.9 - Added the ``exc`` argument. - """ - if exc is _sentinel: - exc = sys.exc_info()[1] - - for name in chain(request.blueprints, (None,)): - if name in self.teardown_request_funcs: - for func in reversed(self.teardown_request_funcs[name]): - self.ensure_sync(func)(exc) - - request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - def do_teardown_appcontext( - self, - exc: BaseException | None = _sentinel, # type: ignore[assignment] - ) -> None: - """Called right before the application context is popped. - - When handling a request, the application context is popped - after the request context. See :meth:`do_teardown_request`. - - This calls all functions decorated with - :meth:`teardown_appcontext`. Then the - :data:`appcontext_tearing_down` signal is sent. - - This is called by - :meth:`AppContext.pop() `. - - .. versionadded:: 0.9 - """ - if exc is _sentinel: - exc = sys.exc_info()[1] - - for func in reversed(self.teardown_appcontext_funcs): - self.ensure_sync(func)(exc) - - appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - def app_context(self) -> AppContext: - """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` - block to push the context, which will make :data:`current_app` - point at this application. - - An application context is automatically pushed by - :meth:`RequestContext.push() ` - when handling a request, and when running a CLI command. Use - this to manually create a context outside of these situations. - - :: - - with app.app_context(): - init_db() - - See :doc:`/appcontext`. - - .. versionadded:: 0.9 - """ - return AppContext(self) - - def request_context(self, environ: WSGIEnvironment) -> RequestContext: - """Create a :class:`~flask.ctx.RequestContext` representing a - WSGI environment. Use a ``with`` block to push the context, - which will make :data:`request` point at this request. - - See :doc:`/reqcontext`. - - Typically you should not call this from your own code. A request - context is automatically pushed by the :meth:`wsgi_app` when - handling a request. Use :meth:`test_request_context` to create - an environment and context instead of this method. - - :param environ: a WSGI environment - """ - return RequestContext(self, environ) - - def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: - """Create a :class:`~flask.ctx.RequestContext` for a WSGI - environment created from the given values. This is mostly useful - during testing, where you may want to run a function that uses - request data without dispatching a full request. - - See :doc:`/reqcontext`. - - Use a ``with`` block to push the context, which will make - :data:`request` point at the request for the created - environment. :: - - with app.test_request_context(...): - generate_report() - - When using the shell, it may be easier to push and pop the - context manually to avoid indentation. :: - - ctx = app.test_request_context(...) - ctx.push() - ... - ctx.pop() - - Takes the same arguments as Werkzeug's - :class:`~werkzeug.test.EnvironBuilder`, with some defaults from - the application. See the linked Werkzeug docs for most of the - available arguments. Flask-specific behavior is listed here. - - :param path: URL path being requested. - :param base_url: Base URL where the app is being served, which - ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, - :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to append to - :data:`SERVER_NAME`. - :param url_scheme: Scheme to use instead of - :data:`PREFERRED_URL_SCHEME`. - :param data: The request body, either as a string or a dict of - form keys and values. - :param json: If given, this is serialized as JSON and passed as - ``data``. Also defaults ``content_type`` to - ``application/json``. - :param args: other positional arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: other keyword arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - """ - from .testing import EnvironBuilder - - builder = EnvironBuilder(self, *args, **kwargs) - - try: - return self.request_context(builder.get_environ()) - finally: - builder.close() - - def wsgi_app( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - """The actual WSGI application. This is not implemented in - :meth:`__call__` so that middlewares can be applied without - losing a reference to the app object. Instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.7 - Teardown events for the request and app contexts are called - even if an unhandled error occurs. Other events may not be - called depending on when an error occurs during dispatch. - See :ref:`callbacks-and-errors`. - - :param environ: A WSGI environment. - :param start_response: A callable accepting a status code, - a list of headers, and an optional exception context to - start the response. - """ - ctx = self.request_context(environ) - error: BaseException | None = None - try: - try: - ctx.push() - response = self.full_dispatch_request() - except Exception as e: - error = e - response = self.handle_exception(e) - except: # noqa: B001 - error = sys.exc_info()[1] - raise - return response(environ, start_response) - finally: - if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](_cv_app.get()) - environ["werkzeug.debug.preserve_context"](_cv_request.get()) - - if error is not None and self.should_ignore_error(error): - error = None - - ctx.pop(error) - - def __call__( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - """The WSGI server calls the Flask application object as the - WSGI application. This calls :meth:`wsgi_app`, which can be - wrapped to apply middleware. - """ - return self.wsgi_app(environ, start_response) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py deleted file mode 100644 index 86c5d59a..00000000 --- a/src/flask/blueprints.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import os -import typing as t -from datetime import timedelta - -from .cli import AppGroup -from .globals import current_app -from .helpers import send_from_directory -from .sansio.blueprints import Blueprint as SansioBlueprint -from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa -from .sansio.scaffold import _sentinel - -if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response - - -class Blueprint(SansioBlueprint): - def __init__( - self, - name: str, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - url_prefix: str | None = None, - subdomain: str | None = None, - url_defaults: dict[str, t.Any] | None = None, - root_path: str | None = None, - cli_group: str | None = _sentinel, # type: ignore - ) -> None: - super().__init__( - name, - import_name, - static_folder, - static_url_path, - template_folder, - url_prefix, - subdomain, - url_defaults, - root_path, - cli_group, - ) - - #: The Click command group for registering CLI commands for this - #: object. The commands are available from the ``flask`` command - #: once the application has been discovered and blueprints have - #: been registered. - self.cli = AppGroup() - - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name - - def get_send_file_max_age(self, filename: str | None) -> int | None: - """Used by :func:`send_file` to determine the ``max_age`` cache - value for a given file path if it wasn't passed. - - By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from - the configuration of :data:`~flask.current_app`. This defaults - to ``None``, which tells the browser to use conditional requests - instead of a timed cache, which is usually preferable. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. - - .. versionadded:: 0.9 - """ - value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] - - if value is None: - return None - - if isinstance(value, timedelta): - return int(value.total_seconds()) - - return value # type: ignore[no-any-return] - - def send_static_file(self, filename: str) -> Response: - """The view function used to serve files from - :attr:`static_folder`. A route is automatically registered for - this view at :attr:`static_url_path` if :attr:`static_folder` is - set. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionadded:: 0.5 - - """ - if not self.has_static_folder: - raise RuntimeError("'static_folder' must be set to serve static_files.") - - # send_file only knows to call get_send_file_max_age on the app, - # call it here so it works for blueprints too. - max_age = self.get_send_file_max_age(filename) - return send_from_directory( - t.cast(str, self.static_folder), filename, max_age=max_age - ) - - def open_resource( - self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to :attr:`root_path` for reading. The - blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` - method. - - :param resource: Path to the resource relative to :attr:`root_path`. - :param mode: Open the file in this mode. Only reading is supported, - valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") - - path = os.path.join(self.root_path, resource) - - if mode == "rb": - return open(path, mode) - - return open(path, mode, encoding=encoding) diff --git a/src/flask/cli.py b/src/flask/cli.py deleted file mode 100644 index ecb292a0..00000000 --- a/src/flask/cli.py +++ /dev/null @@ -1,1109 +0,0 @@ -from __future__ import annotations - -import ast -import collections.abc as cabc -import importlib.metadata -import inspect -import os -import platform -import re -import sys -import traceback -import typing as t -from functools import update_wrapper -from operator import itemgetter -from types import ModuleType - -import click -from click.core import ParameterSource -from werkzeug import run_simple -from werkzeug.serving import is_running_from_reloader -from werkzeug.utils import import_string - -from .globals import current_app -from .helpers import get_debug_flag -from .helpers import get_load_dotenv - -if t.TYPE_CHECKING: - import ssl - - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIApplication - from _typeshed.wsgi import WSGIEnvironment - - from .app import Flask - - -class NoAppException(click.UsageError): - """Raised if an application cannot be found or loaded.""" - - -def find_best_app(module: ModuleType) -> Flask: - """Given a module instance this tries to find the best possible - application in the module or raises an exception. - """ - from . import Flask - - # Search for the most common names first. - for attr_name in ("app", "application"): - app = getattr(module, attr_name, None) - - if isinstance(app, Flask): - return app - - # Otherwise find the only object that is a Flask instance. - matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] - - if len(matches) == 1: - return matches[0] - elif len(matches) > 1: - raise NoAppException( - "Detected multiple Flask applications in module" - f" '{module.__name__}'. Use '{module.__name__}:name'" - " to specify the correct one." - ) - - # Search for app factory functions. - for attr_name in ("create_app", "make_app"): - app_factory = getattr(module, attr_name, None) - - if inspect.isfunction(app_factory): - try: - app = app_factory() - - if isinstance(app, Flask): - return app - except TypeError as e: - if not _called_with_wrong_args(app_factory): - raise - - raise NoAppException( - f"Detected factory '{attr_name}' in module '{module.__name__}'," - " but could not call it without arguments. Use" - f" '{module.__name__}:{attr_name}(args)'" - " to specify arguments." - ) from e - - raise NoAppException( - "Failed to find Flask application or factory in module" - f" '{module.__name__}'. Use '{module.__name__}:name'" - " to specify one." - ) - - -def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: - """Check whether calling a function raised a ``TypeError`` because - the call failed or because something in the factory raised the - error. - - :param f: The function that was called. - :return: ``True`` if the call failed. - """ - tb = sys.exc_info()[2] - - try: - while tb is not None: - if tb.tb_frame.f_code is f.__code__: - # In the function, it was called successfully. - return False - - tb = tb.tb_next - - # Didn't reach the function. - return True - finally: - # Delete tb to break a circular reference. - # https://docs.python.org/2/library/sys.html#sys.exc_info - del tb - - -def find_app_by_string(module: ModuleType, app_name: str) -> Flask: - """Check if the given string is a variable name or a function. Call - a function to get the app instance, or return the variable directly. - """ - from . import Flask - - # Parse app_name as a single expression to determine if it's a valid - # attribute name or function call. - try: - expr = ast.parse(app_name.strip(), mode="eval").body - except SyntaxError: - raise NoAppException( - f"Failed to parse {app_name!r} as an attribute name or function call." - ) from None - - if isinstance(expr, ast.Name): - name = expr.id - args = [] - kwargs = {} - elif isinstance(expr, ast.Call): - # Ensure the function name is an attribute name only. - if not isinstance(expr.func, ast.Name): - raise NoAppException( - f"Function reference must be a simple name: {app_name!r}." - ) - - name = expr.func.id - - # Parse the positional and keyword arguments as literals. - try: - args = [ast.literal_eval(arg) for arg in expr.args] - kwargs = { - kw.arg: ast.literal_eval(kw.value) - for kw in expr.keywords - if kw.arg is not None - } - except ValueError: - # literal_eval gives cryptic error messages, show a generic - # message with the full expression instead. - raise NoAppException( - f"Failed to parse arguments as literal values: {app_name!r}." - ) from None - else: - raise NoAppException( - f"Failed to parse {app_name!r} as an attribute name or function call." - ) - - try: - attr = getattr(module, name) - except AttributeError as e: - raise NoAppException( - f"Failed to find attribute {name!r} in {module.__name__!r}." - ) from e - - # If the attribute is a function, call it with any args and kwargs - # to get the real application. - if inspect.isfunction(attr): - try: - app = attr(*args, **kwargs) - except TypeError as e: - if not _called_with_wrong_args(attr): - raise - - raise NoAppException( - f"The factory {app_name!r} in module" - f" {module.__name__!r} could not be called with the" - " specified arguments." - ) from e - else: - app = attr - - if isinstance(app, Flask): - return app - - raise NoAppException( - "A valid Flask application was not obtained from" - f" '{module.__name__}:{app_name}'." - ) - - -def prepare_import(path: str) -> str: - """Given a filename this will try to calculate the python path, add it - to the search path and return the actual module name that is expected. - """ - path = os.path.realpath(path) - - fname, ext = os.path.splitext(path) - if ext == ".py": - path = fname - - if os.path.basename(path) == "__init__": - path = os.path.dirname(path) - - module_name = [] - - # move up until outside package structure (no __init__.py) - while True: - path, name = os.path.split(path) - module_name.append(name) - - if not os.path.exists(os.path.join(path, "__init__.py")): - break - - if sys.path[0] != path: - sys.path.insert(0, path) - - return ".".join(module_name[::-1]) - - -@t.overload -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True -) -> Flask: ... - - -@t.overload -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... -) -> Flask | None: ... - - -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: bool = True -) -> Flask | None: - try: - __import__(module_name) - except ImportError: - # Reraise the ImportError if it occurred within the imported module. - # Determine this by checking whether the trace has a depth > 1. - if sys.exc_info()[2].tb_next: # type: ignore[union-attr] - raise NoAppException( - f"While importing {module_name!r}, an ImportError was" - f" raised:\n\n{traceback.format_exc()}" - ) from None - elif raise_if_not_found: - raise NoAppException(f"Could not import {module_name!r}.") from None - else: - return None - - module = sys.modules[module_name] - - if app_name is None: - return find_best_app(module) - else: - return find_app_by_string(module, app_name) - - -def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: - if not value or ctx.resilient_parsing: - return - - flask_version = importlib.metadata.version("flask") - werkzeug_version = importlib.metadata.version("werkzeug") - - click.echo( - f"Python {platform.python_version()}\n" - f"Flask {flask_version}\n" - f"Werkzeug {werkzeug_version}", - color=ctx.color, - ) - ctx.exit() - - -version_option = click.Option( - ["--version"], - help="Show the Flask version.", - expose_value=False, - callback=get_version, - is_flag=True, - is_eager=True, -) - - -class ScriptInfo: - """Helper object to deal with Flask applications. This is usually not - necessary to interface with as it's used internally in the dispatching - to click. In future versions of Flask this object will most likely play - a bigger role. Typically it's created automatically by the - :class:`FlaskGroup` but you can also manually create it and pass it - onwards as click object. - """ - - def __init__( - self, - app_import_path: str | None = None, - create_app: t.Callable[..., Flask] | None = None, - set_debug_flag: bool = True, - ) -> None: - #: Optionally the import path for the Flask application. - self.app_import_path = app_import_path - #: Optionally a function that is passed the script info to create - #: the instance of the application. - self.create_app = create_app - #: A dictionary with arbitrary data that can be associated with - #: this script info. - self.data: dict[t.Any, t.Any] = {} - self.set_debug_flag = set_debug_flag - self._loaded_app: Flask | None = None - - def load_app(self) -> Flask: - """Loads the Flask app (if not yet loaded) and returns it. Calling - this multiple times will just result in the already loaded app to - be returned. - """ - if self._loaded_app is not None: - return self._loaded_app - - if self.create_app is not None: - app: Flask | None = self.create_app() - else: - if self.app_import_path: - path, name = ( - re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] - )[:2] - import_name = prepare_import(path) - app = locate_app(import_name, name) - else: - for path in ("wsgi.py", "app.py"): - import_name = prepare_import(path) - app = locate_app(import_name, None, raise_if_not_found=False) - - if app is not None: - break - - if app is None: - raise NoAppException( - "Could not locate a Flask application. Use the" - " 'flask --app' option, 'FLASK_APP' environment" - " variable, or a 'wsgi.py' or 'app.py' file in the" - " current directory." - ) - - if self.set_debug_flag: - # Update the app's debug flag through the descriptor so that - # other values repopulate as well. - app.debug = get_debug_flag() - - self._loaded_app = app - return app - - -pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -def with_appcontext(f: F) -> F: - """Wraps a callback so that it's guaranteed to be executed with the - script's application context. - - Custom commands (and their options) registered under ``app.cli`` or - ``blueprint.cli`` will always have an app context available, this - decorator is not required in that case. - - .. versionchanged:: 2.2 - The app context is active for subcommands as well as the - decorated callback. The app context is always available to - ``app.cli`` command and parameter callbacks. - """ - - @click.pass_context - def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: - if not current_app: - app = ctx.ensure_object(ScriptInfo).load_app() - ctx.with_resource(app.app_context()) - - return ctx.invoke(f, *args, **kwargs) - - return update_wrapper(decorator, f) # type: ignore[return-value] - - -class AppGroup(click.Group): - """This works similar to a regular click :class:`~click.Group` but it - changes the behavior of the :meth:`command` decorator so that it - automatically wraps the functions in :func:`with_appcontext`. - - Not to be confused with :class:`FlaskGroup`. - """ - - def command( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` - unless it's disabled by passing ``with_appcontext=False``. - """ - wrap_for_ctx = kwargs.pop("with_appcontext", True) - - def decorator(f: t.Callable[..., t.Any]) -> click.Command: - if wrap_for_ctx: - f = with_appcontext(f) - return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] - - return decorator - - def group( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it defaults the group class to - :class:`AppGroup`. - """ - kwargs.setdefault("cls", AppGroup) - return super().group(*args, **kwargs) # type: ignore[no-any-return] - - -def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: - if value is None: - return None - - info = ctx.ensure_object(ScriptInfo) - info.app_import_path = value - return value - - -# This option is eager so the app will be available if --help is given. -# --help is also eager, so --app must be before it in the param list. -# no_args_is_help bypasses eager processing, so this option must be -# processed manually in that case to ensure FLASK_APP gets picked up. -_app_option = click.Option( - ["-A", "--app"], - metavar="IMPORT", - help=( - "The Flask application or factory function to load, in the form 'module:name'." - " Module can be a dotted import or file path. Name is not required if it is" - " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" - " pass arguments." - ), - is_eager=True, - expose_value=False, - callback=_set_app, -) - - -def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: - # If the flag isn't provided, it will default to False. Don't use - # that, let debug be set by env in that case. - source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] - - if source is not None and source in ( - ParameterSource.DEFAULT, - ParameterSource.DEFAULT_MAP, - ): - return None - - # Set with env var instead of ScriptInfo.load so that it can be - # accessed early during a factory function. - os.environ["FLASK_DEBUG"] = "1" if value else "0" - return value - - -_debug_option = click.Option( - ["--debug/--no-debug"], - help="Set debug mode.", - expose_value=False, - callback=_set_debug, -) - - -def _env_file_callback( - ctx: click.Context, param: click.Option, value: str | None -) -> str | None: - if value is None: - return None - - import importlib - - try: - importlib.import_module("dotenv") - except ImportError: - raise click.BadParameter( - "python-dotenv must be installed to load an env file.", - ctx=ctx, - param=param, - ) from None - - # Don't check FLASK_SKIP_DOTENV, that only disables automatically - # loading .env and .flaskenv files. - load_dotenv(value) - return value - - -# This option is eager so env vars are loaded as early as possible to be -# used by other options. -_env_file_option = click.Option( - ["-e", "--env-file"], - type=click.Path(exists=True, dir_okay=False), - help="Load environment variables from this file. python-dotenv must be installed.", - is_eager=True, - expose_value=False, - callback=_env_file_callback, -) - - -class FlaskGroup(AppGroup): - """Special subclass of the :class:`AppGroup` group that supports - loading more commands from the configured Flask app. Normally a - developer does not have to interface with this class but there are - some very advanced use cases for which it makes sense to create an - instance of this. see :ref:`custom-scripts`. - - :param add_default_commands: if this is True then the default run and - shell commands will be added. - :param add_version_option: adds the ``--version`` option. - :param create_app: an optional callback that is passed the script info and - returns the loaded app. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param set_debug_flag: Set the app's debug flag. - - .. versionchanged:: 2.2 - Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. - - .. versionchanged:: 2.2 - An app context is pushed when running ``app.cli`` commands, so - ``@with_appcontext`` is no longer required for those commands. - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment variables - from :file:`.env` and :file:`.flaskenv` files. - """ - - def __init__( - self, - add_default_commands: bool = True, - create_app: t.Callable[..., Flask] | None = None, - add_version_option: bool = True, - load_dotenv: bool = True, - set_debug_flag: bool = True, - **extra: t.Any, - ) -> None: - params = list(extra.pop("params", None) or ()) - # Processing is done with option callbacks instead of a group - # callback. This allows users to make a custom group callback - # without losing the behavior. --env-file must come first so - # that it is eagerly evaluated before --app. - params.extend((_env_file_option, _app_option, _debug_option)) - - if add_version_option: - params.append(version_option) - - if "context_settings" not in extra: - extra["context_settings"] = {} - - extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") - - super().__init__(params=params, **extra) - - self.create_app = create_app - self.load_dotenv = load_dotenv - self.set_debug_flag = set_debug_flag - - if add_default_commands: - self.add_command(run_command) - self.add_command(shell_command) - self.add_command(routes_command) - - self._loaded_plugin_commands = False - - def _load_plugin_commands(self) -> None: - if self._loaded_plugin_commands: - return - - if sys.version_info >= (3, 10): - from importlib import metadata - else: - # Use a backport on Python < 3.10. We technically have - # importlib.metadata on 3.8+, but the API changed in 3.10, - # so use the backport for consistency. - import importlib_metadata as metadata - - for ep in metadata.entry_points(group="flask.commands"): - self.add_command(ep.load(), ep.name) - - self._loaded_plugin_commands = True - - def get_command(self, ctx: click.Context, name: str) -> click.Command | None: - self._load_plugin_commands() - # Look up built-in and plugin commands, which should be - # available even if the app fails to load. - rv = super().get_command(ctx, name) - - if rv is not None: - return rv - - info = ctx.ensure_object(ScriptInfo) - - # Look up commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - app = info.load_app() - except NoAppException as e: - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - return None - - # Push an app context for the loaded app unless it is already - # active somehow. This makes the context available to parameter - # and command callbacks without needing @with_appcontext. - if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] - ctx.with_resource(app.app_context()) - - return app.cli.get_command(ctx, name) - - def list_commands(self, ctx: click.Context) -> list[str]: - self._load_plugin_commands() - # Start with the built-in and plugin commands. - rv = set(super().list_commands(ctx)) - info = ctx.ensure_object(ScriptInfo) - - # Add commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - rv.update(info.load_app().cli.list_commands(ctx)) - except NoAppException as e: - # When an app couldn't be loaded, show the error message - # without the traceback. - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - except Exception: - # When any other errors occurred during loading, show the - # full traceback. - click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") - - return sorted(rv) - - def make_context( - self, - info_name: str | None, - args: list[str], - parent: click.Context | None = None, - **extra: t.Any, - ) -> click.Context: - # Set a flag to tell app.run to become a no-op. If app.run was - # not in a __name__ == __main__ guard, it would start the server - # when importing, blocking whatever command is being called. - os.environ["FLASK_RUN_FROM_CLI"] = "true" - - # Attempt to load .env and .flask env files. The --env-file - # option can cause another file to be loaded. - if get_load_dotenv(self.load_dotenv): - load_dotenv() - - if "obj" not in extra and "obj" not in self.context_settings: - extra["obj"] = ScriptInfo( - create_app=self.create_app, set_debug_flag=self.set_debug_flag - ) - - return super().make_context(info_name, args, parent=parent, **extra) - - def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: - if not args and self.no_args_is_help: - # Attempt to load --env-file and --app early in case they - # were given as env vars. Otherwise no_args_is_help will not - # see commands from app.cli. - _env_file_option.handle_parse_result(ctx, {}, []) - _app_option.handle_parse_result(ctx, {}, []) - - return super().parse_args(ctx, args) - - -def _path_is_ancestor(path: str, other: str) -> bool: - """Take ``other`` and remove the length of ``path`` from it. Then join it - to ``path``. If it is the original value, ``path`` is an ancestor of - ``other``.""" - return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other - - -def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool: - """Load "dotenv" files in order of precedence to set environment variables. - - If an env var is already set it is not overwritten, so earlier files in the - list are preferred over later files. - - This is a no-op if `python-dotenv`_ is not installed. - - .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - - :param path: Load the file at this location instead of searching. - :return: ``True`` if a file was loaded. - - .. versionchanged:: 2.0 - The current directory is not changed to the location of the - loaded file. - - .. versionchanged:: 2.0 - When loading the env files, set the default encoding to UTF-8. - - .. versionchanged:: 1.1.0 - Returns ``False`` when python-dotenv is not installed, or when - the given path isn't a file. - - .. versionadded:: 1.0 - """ - try: - import dotenv - except ImportError: - if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): - click.secho( - " * Tip: There are .env or .flaskenv files present." - ' Do "pip install python-dotenv" to use them.', - fg="yellow", - err=True, - ) - - return False - - # Always return after attempting to load a given path, don't load - # the default files. - if path is not None: - if os.path.isfile(path): - return dotenv.load_dotenv(path, encoding="utf-8") - - return False - - loaded = False - - for name in (".env", ".flaskenv"): - path = dotenv.find_dotenv(name, usecwd=True) - - if not path: - continue - - dotenv.load_dotenv(path, encoding="utf-8") - loaded = True - - return loaded # True if at least one file was located and loaded. - - -def show_server_banner(debug: bool, app_import_path: str | None) -> None: - """Show extra startup messages the first time the server is run, - ignoring the reloader. - """ - if is_running_from_reloader(): - return - - if app_import_path is not None: - click.echo(f" * Serving Flask app '{app_import_path}'") - - if debug is not None: - click.echo(f" * Debug mode: {'on' if debug else 'off'}") - - -class CertParamType(click.ParamType): - """Click option type for the ``--cert`` option. Allows either an - existing file, the string ``'adhoc'``, or an import for a - :class:`~ssl.SSLContext` object. - """ - - name = "path" - - def __init__(self) -> None: - self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) - - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: - try: - import ssl - except ImportError: - raise click.BadParameter( - 'Using "--cert" requires Python to be compiled with SSL support.', - ctx, - param, - ) from None - - try: - return self.path_type(value, param, ctx) - except click.BadParameter: - value = click.STRING(value, param, ctx).lower() - - if value == "adhoc": - try: - import cryptography # noqa: F401 - except ImportError: - raise click.BadParameter( - "Using ad-hoc certificates requires the cryptography library.", - ctx, - param, - ) from None - - return value - - obj = import_string(value, silent=True) - - if isinstance(obj, ssl.SSLContext): - return obj - - raise - - -def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: - """The ``--key`` option must be specified when ``--cert`` is a file. - Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. - """ - cert = ctx.params.get("cert") - is_adhoc = cert == "adhoc" - - try: - import ssl - except ImportError: - is_context = False - else: - is_context = isinstance(cert, ssl.SSLContext) - - if value is not None: - if is_adhoc: - raise click.BadParameter( - 'When "--cert" is "adhoc", "--key" is not used.', ctx, param - ) - - if is_context: - raise click.BadParameter( - 'When "--cert" is an SSLContext object, "--key" is not used.', - ctx, - param, - ) - - if not cert: - raise click.BadParameter('"--cert" must also be specified.', ctx, param) - - ctx.params["cert"] = cert, value - - else: - if cert and not (is_adhoc or is_context): - raise click.BadParameter('Required when using "--cert".', ctx, param) - - return value - - -class SeparatedPathType(click.Path): - """Click option type that accepts a list of values separated by the - OS's path separator (``:``, ``;`` on Windows). Each value is - validated as a :class:`click.Path` type. - """ - - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: - items = self.split_envvar_value(value) - # can't call no-arg super() inside list comprehension until Python 3.12 - super_convert = super().convert - return [super_convert(item, param, ctx) for item in items] - - -@click.command("run", short_help="Run a development server.") -@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") -@click.option("--port", "-p", default=5000, help="The port to bind to.") -@click.option( - "--cert", - type=CertParamType(), - help="Specify a certificate file to use HTTPS.", - is_eager=True, -) -@click.option( - "--key", - type=click.Path(exists=True, dir_okay=False, resolve_path=True), - callback=_validate_key, - expose_value=False, - help="The key file to use when specifying a certificate.", -) -@click.option( - "--reload/--no-reload", - default=None, - help="Enable or disable the reloader. By default the reloader " - "is active if debug is enabled.", -) -@click.option( - "--debugger/--no-debugger", - default=None, - help="Enable or disable the debugger. By default the debugger " - "is active if debug is enabled.", -) -@click.option( - "--with-threads/--without-threads", - default=True, - help="Enable or disable multithreading.", -) -@click.option( - "--extra-files", - default=None, - type=SeparatedPathType(), - help=( - "Extra files that trigger a reload on change. Multiple paths" - f" are separated by {os.path.pathsep!r}." - ), -) -@click.option( - "--exclude-patterns", - default=None, - type=SeparatedPathType(), - help=( - "Files matching these fnmatch patterns will not trigger a reload" - " on change. Multiple patterns are separated by" - f" {os.path.pathsep!r}." - ), -) -@pass_script_info -def run_command( - info: ScriptInfo, - host: str, - port: int, - reload: bool, - debugger: bool, - with_threads: bool, - cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, - extra_files: list[str] | None, - exclude_patterns: list[str] | None, -) -> None: - """Run a local development server. - - This server is for development purposes only. It does not provide - the stability, security, or performance of production WSGI servers. - - The reloader and debugger are enabled by default with the '--debug' - option. - """ - try: - app: WSGIApplication = info.load_app() - except Exception as e: - if is_running_from_reloader(): - # When reloading, print out the error immediately, but raise - # it later so the debugger or server can handle it. - traceback.print_exc() - err = e - - def app( - environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - raise err from None - - else: - # When not reloading, raise the error immediately so the - # command fails. - raise e from None - - debug = get_debug_flag() - - if reload is None: - reload = debug - - if debugger is None: - debugger = debug - - show_server_banner(debug, info.app_import_path) - - run_simple( - host, - port, - app, - use_reloader=reload, - use_debugger=debugger, - threaded=with_threads, - ssl_context=cert, - extra_files=extra_files, - exclude_patterns=exclude_patterns, - ) - - -run_command.params.insert(0, _debug_option) - - -@click.command("shell", short_help="Run a shell in the app context.") -@with_appcontext -def shell_command() -> None: - """Run an interactive Python shell in the context of a given - Flask application. The application will populate the default - namespace of this shell according to its configuration. - - This is useful for executing small snippets of management code - without having to manually configure the application. - """ - import code - - banner = ( - f"Python {sys.version} on {sys.platform}\n" - f"App: {current_app.import_name}\n" - f"Instance: {current_app.instance_path}" - ) - ctx: dict[str, t.Any] = {} - - # Support the regular Python interpreter startup script if someone - # is using it. - startup = os.environ.get("PYTHONSTARTUP") - if startup and os.path.isfile(startup): - with open(startup) as f: - eval(compile(f.read(), startup, "exec"), ctx) - - ctx.update(current_app.make_shell_context()) - - # Site, customize, or startup script can set a hook to call when - # entering interactive mode. The default one sets up readline with - # tab and history completion. - interactive_hook = getattr(sys, "__interactivehook__", None) - - if interactive_hook is not None: - try: - import readline - from rlcompleter import Completer - except ImportError: - pass - else: - # rlcompleter uses __main__.__dict__ by default, which is - # flask.__main__. Use the shell context instead. - readline.set_completer(Completer(ctx).complete) - - interactive_hook() - - code.interact(banner=banner, local=ctx) - - -@click.command("routes", short_help="Show the routes for the app.") -@click.option( - "--sort", - "-s", - type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), - default="endpoint", - help=( - "Method to sort routes by. 'match' is the order that Flask will match routes" - " when dispatching a request." - ), -) -@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") -@with_appcontext -def routes_command(sort: str, all_methods: bool) -> None: - """Show all registered routes with endpoints and methods.""" - rules = list(current_app.url_map.iter_rules()) - - if not rules: - click.echo("No routes were registered.") - return - - ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} - host_matching = current_app.url_map.host_matching - has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) - rows = [] - - for rule in rules: - row = [ - rule.endpoint, - ", ".join(sorted((rule.methods or set()) - ignored_methods)), - ] - - if has_domain: - row.append((rule.host if host_matching else rule.subdomain) or "") - - row.append(rule.rule) - rows.append(row) - - headers = ["Endpoint", "Methods"] - sorts = ["endpoint", "methods"] - - if has_domain: - headers.append("Host" if host_matching else "Subdomain") - sorts.append("domain") - - headers.append("Rule") - sorts.append("rule") - - try: - rows.sort(key=itemgetter(sorts.index(sort))) - except ValueError: - pass - - rows.insert(0, headers) - widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] - rows.insert(1, ["-" * w for w in widths]) - template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) - - for row in rows: - click.echo(template.format(*row)) - - -cli = FlaskGroup( - name="flask", - help="""\ -A general utility script for Flask applications. - -An application to load must be given with the '--app' option, -'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file -in the current directory. -""", -) - - -def main() -> None: - cli.main() - - -if __name__ == "__main__": - main() diff --git a/src/flask/config.py b/src/flask/config.py deleted file mode 100644 index 7e3ba179..00000000 --- a/src/flask/config.py +++ /dev/null @@ -1,370 +0,0 @@ -from __future__ import annotations - -import errno -import json -import os -import types -import typing as t - -from werkzeug.utils import import_string - -if t.TYPE_CHECKING: - import typing_extensions as te - - from .sansio.app import App - - -T = t.TypeVar("T") - - -class ConfigAttribute(t.Generic[T]): - """Makes an attribute forward to the config""" - - def __init__( - self, name: str, get_converter: t.Callable[[t.Any], T] | None = None - ) -> None: - self.__name__ = name - self.get_converter = get_converter - - @t.overload - def __get__(self, obj: None, owner: None) -> te.Self: ... - - @t.overload - def __get__(self, obj: App, owner: type[App]) -> T: ... - - def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: - if obj is None: - return self - - rv = obj.config[self.__name__] - - if self.get_converter is not None: - rv = self.get_converter(rv) - - return rv # type: ignore[no-any-return] - - def __set__(self, obj: App, value: t.Any) -> None: - obj.config[self.__name__] = value - - -class Config(dict): # type: ignore[type-arg] - """Works exactly like a dict but provides ways to fill it from files - or special dictionaries. There are two common patterns to populate the - config. - - Either you can fill the config from a config file:: - - app.config.from_pyfile('yourconfig.cfg') - - Or alternatively you can define the configuration options in the - module that calls :meth:`from_object` or provide an import path to - a module that should be loaded. It is also possible to tell it to - use the same module and with that provide the configuration values - just before the call:: - - DEBUG = True - SECRET_KEY = 'development key' - app.config.from_object(__name__) - - In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. This makes it possible to use - lowercase values in the config file for temporary values that are not added - to the config or to define the config keys in the same file that implements - the application. - - Probably the most interesting way to load configurations is from an - environment variable pointing to a file:: - - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - - In this case before launching the application you have to set this - environment variable to the file you want to use. On Linux and OS X - use the export statement:: - - export YOURAPPLICATION_SETTINGS='/path/to/config/file' - - On windows use `set` instead. - - :param root_path: path to which files are read relative from. When the - config object is created by the application, this is - the application's :attr:`~flask.Flask.root_path`. - :param defaults: an optional dictionary of default values - """ - - def __init__( - self, - root_path: str | os.PathLike[str], - defaults: dict[str, t.Any] | None = None, - ) -> None: - super().__init__(defaults or {}) - self.root_path = root_path - - def from_envvar(self, variable_name: str, silent: bool = False) -> bool: - """Loads a configuration from an environment variable pointing to - a configuration file. This is basically just a shortcut with nicer - error messages for this line of code:: - - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) - - :param variable_name: name of the environment variable - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError( - f"The environment variable {variable_name!r} is not set" - " and as such configuration could not be loaded. Set" - " this variable and make it point to a configuration" - " file" - ) - return self.from_pyfile(rv, silent=silent) - - def from_prefixed_env( - self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads - ) -> bool: - """Load any environment variables that start with ``FLASK_``, - dropping the prefix from the env key for the config key. Values - are passed through a loading function to attempt to convert them - to more specific types than strings. - - Keys are loaded in :func:`sorted` order. - - The default loading function attempts to parse values as any - valid JSON type, including dicts and lists. - - Specific items in nested dicts can be set by separating the - keys with double underscores (``__``). If an intermediate key - doesn't exist, it will be initialized to an empty dict. - - :param prefix: Load env vars that start with this prefix, - separated with an underscore (``_``). - :param loads: Pass each string value to this function and use - the returned value as the config value. If any error is - raised it is ignored and the value remains a string. The - default is :func:`json.loads`. - - .. versionadded:: 2.1 - """ - prefix = f"{prefix}_" - len_prefix = len(prefix) - - for key in sorted(os.environ): - if not key.startswith(prefix): - continue - - value = os.environ[key] - - try: - value = loads(value) - except Exception: - # Keep the value as a string if loading failed. - pass - - # Change to key.removeprefix(prefix) on Python >= 3.9. - key = key[len_prefix:] - - if "__" not in key: - # A non-nested key, set directly. - self[key] = value - continue - - # Traverse nested dictionaries with keys separated by "__". - current = self - *parts, tail = key.split("__") - - for part in parts: - # If an intermediate dict does not exist, create it. - if part not in current: - current[part] = {} - - current = current[part] - - current[tail] = value - - return True - - def from_pyfile( - self, filename: str | os.PathLike[str], silent: bool = False - ) -> bool: - """Updates the values in the config from a Python file. This function - behaves as if the file was imported as module with the - :meth:`from_object` function. - - :param filename: the filename of the config. This can either be an - absolute filename or a filename relative to the - root path. - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - - .. versionadded:: 0.7 - `silent` parameter. - """ - filename = os.path.join(self.root_path, filename) - d = types.ModuleType("config") - d.__file__ = filename - try: - with open(filename, mode="rb") as config_file: - exec(compile(config_file.read(), filename, "exec"), d.__dict__) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): - return False - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - self.from_object(d) - return True - - def from_object(self, obj: object | str) -> None: - """Updates the values from the given object. An object can be of one - of the following two types: - - - a string: in this case the object with that name will be imported - - an actual object reference: that object is used directly - - Objects are usually either modules or classes. :meth:`from_object` - loads only the uppercase attributes of the module/class. A ``dict`` - object will not work with :meth:`from_object` because the keys of a - ``dict`` are not attributes of the ``dict`` class. - - Example of module-based configuration:: - - app.config.from_object('yourapplication.default_config') - from yourapplication import default_config - app.config.from_object(default_config) - - Nothing is done to the object before loading. If the object is a - class and has ``@property`` attributes, it needs to be - instantiated before being passed to this method. - - You should not use this function to load the actual configuration but - rather configuration defaults. The actual config should be loaded - with :meth:`from_pyfile` and ideally from a location not within the - package because the package might be installed system wide. - - See :ref:`config-dev-prod` for an example of class-based configuration - using :meth:`from_object`. - - :param obj: an import name or object - """ - if isinstance(obj, str): - obj = import_string(obj) - for key in dir(obj): - if key.isupper(): - self[key] = getattr(obj, key) - - def from_file( - self, - filename: str | os.PathLike[str], - load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], - silent: bool = False, - text: bool = True, - ) -> bool: - """Update the values in the config from a file that is loaded - using the ``load`` parameter. The loaded data is passed to the - :meth:`from_mapping` method. - - .. code-block:: python - - import json - app.config.from_file("config.json", load=json.load) - - import tomllib - app.config.from_file("config.toml", load=tomllib.load, text=False) - - :param filename: The path to the data file. This can be an - absolute path or relative to the config root path. - :param load: A callable that takes a file handle and returns a - mapping of loaded data from the file. - :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` - implements a ``read`` method. - :param silent: Ignore the file if it doesn't exist. - :param text: Open the file in text or binary mode. - :return: ``True`` if the file was loaded successfully. - - .. versionchanged:: 2.3 - The ``text`` parameter was added. - - .. versionadded:: 2.0 - """ - filename = os.path.join(self.root_path, filename) - - try: - with open(filename, "r" if text else "rb") as f: - obj = load(f) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR): - return False - - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - - return self.from_mapping(obj) - - def from_mapping( - self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any - ) -> bool: - """Updates the config like :meth:`update` ignoring items with - non-upper keys. - - :return: Always returns ``True``. - - .. versionadded:: 0.11 - """ - mappings: dict[str, t.Any] = {} - if mapping is not None: - mappings.update(mapping) - mappings.update(kwargs) - for key, value in mappings.items(): - if key.isupper(): - self[key] = value - return True - - def get_namespace( - self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> dict[str, t.Any]: - """Returns a dictionary containing a subset of configuration options - that match the specified namespace/prefix. Example usage:: - - app.config['IMAGE_STORE_TYPE'] = 'fs' - app.config['IMAGE_STORE_PATH'] = '/var/app/images' - app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' - image_store_config = app.config.get_namespace('IMAGE_STORE_') - - The resulting dictionary `image_store_config` would look like:: - - { - 'type': 'fs', - 'path': '/var/app/images', - 'base_url': 'http://img.website.com' - } - - This is often useful when configuration options map directly to - keyword arguments in functions or class constructors. - - :param namespace: a configuration namespace - :param lowercase: a flag indicating if the keys of the resulting - dictionary should be lowercase - :param trim_namespace: a flag indicating if the keys of the resulting - dictionary should not include the namespace - - .. versionadded:: 0.11 - """ - rv = {} - for k, v in self.items(): - if not k.startswith(namespace): - continue - if trim_namespace: - key = k[len(namespace) :] - else: - key = k - if lowercase: - key = key.lower() - rv[key] = v - return rv - - def __repr__(self) -> str: - return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/src/flask/ctx.py b/src/flask/ctx.py deleted file mode 100644 index 9b164d39..00000000 --- a/src/flask/ctx.py +++ /dev/null @@ -1,449 +0,0 @@ -from __future__ import annotations - -import contextvars -import sys -import typing as t -from functools import update_wrapper -from types import TracebackType - -from werkzeug.exceptions import HTTPException - -from . import typing as ft -from .globals import _cv_app -from .globals import _cv_request -from .signals import appcontext_popped -from .signals import appcontext_pushed - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import WSGIEnvironment - - from .app import Flask - from .sessions import SessionMixin - from .wrappers import Request - - -# a singleton sentinel value for parameter defaults -_sentinel = object() - - -class _AppCtxGlobals: - """A plain object. Used as a namespace for storing data during an - application context. - - Creating an app context automatically creates this object, which is - made available as the :data:`g` proxy. - - .. describe:: 'key' in g - - Check whether an attribute is present. - - .. versionadded:: 0.10 - - .. describe:: iter(g) - - Return an iterator over the attribute names. - - .. versionadded:: 0.10 - """ - - # Define attr methods to let mypy know this is a namespace object - # that has arbitrary attributes. - - def __getattr__(self, name: str) -> t.Any: - try: - return self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def __setattr__(self, name: str, value: t.Any) -> None: - self.__dict__[name] = value - - def __delattr__(self, name: str) -> None: - try: - del self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def get(self, name: str, default: t.Any | None = None) -> t.Any: - """Get an attribute by name, or a default value. Like - :meth:`dict.get`. - - :param name: Name of attribute to get. - :param default: Value to return if the attribute is not present. - - .. versionadded:: 0.10 - """ - return self.__dict__.get(name, default) - - def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: - """Get and remove an attribute by name. Like :meth:`dict.pop`. - - :param name: Name of attribute to pop. - :param default: Value to return if the attribute is not present, - instead of raising a ``KeyError``. - - .. versionadded:: 0.11 - """ - if default is _sentinel: - return self.__dict__.pop(name) - else: - return self.__dict__.pop(name, default) - - def setdefault(self, name: str, default: t.Any = None) -> t.Any: - """Get the value of an attribute if it is present, otherwise - set and return a default value. Like :meth:`dict.setdefault`. - - :param name: Name of attribute to get. - :param default: Value to set and return if the attribute is not - present. - - .. versionadded:: 0.11 - """ - return self.__dict__.setdefault(name, default) - - def __contains__(self, item: str) -> bool: - return item in self.__dict__ - - def __iter__(self) -> t.Iterator[str]: - return iter(self.__dict__) - - def __repr__(self) -> str: - ctx = _cv_app.get(None) - if ctx is not None: - return f"" - return object.__repr__(self) - - -def after_this_request( - f: ft.AfterRequestCallable[t.Any], -) -> ft.AfterRequestCallable[t.Any]: - """Executes a function after this request. This is useful to modify - response objects. The function is passed the response object and has - to return the same or a new one. - - Example:: - - @app.route('/') - def index(): - @after_this_request - def add_header(response): - response.headers['X-Foo'] = 'Parachute' - return response - return 'Hello World!' - - This is more useful if a function other than the view function wants to - modify a response. For instance think of a decorator that wants to add - some headers without converting the return value into a response object. - - .. versionadded:: 0.9 - """ - ctx = _cv_request.get(None) - - if ctx is None: - raise RuntimeError( - "'after_this_request' can only be used when a request" - " context is active, such as in a view function." - ) - - ctx._after_request_functions.append(f) - return f - - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -def copy_current_request_context(f: F) -> F: - """A helper function that decorates a function to retain the current - request context. This is useful when working with greenlets. The moment - the function is decorated a copy of the request context is created and - then pushed when the function is called. The current session is also - included in the copied request context. - - Example:: - - import gevent - from flask import copy_current_request_context - - @app.route('/') - def index(): - @copy_current_request_context - def do_some_work(): - # do some work here, it can access flask.request or - # flask.session like you would otherwise in the view function. - ... - gevent.spawn(do_some_work) - return 'Regular response' - - .. versionadded:: 0.10 - """ - ctx = _cv_request.get(None) - - if ctx is None: - raise RuntimeError( - "'copy_current_request_context' can only be used when a" - " request context is active, such as in a view function." - ) - - ctx = ctx.copy() - - def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: - with ctx: # type: ignore[union-attr] - return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -def has_request_context() -> bool: - """If you have code that wants to test if a request context is there or - not this function can be used. For instance, you may want to take advantage - of request information if the request object is available, but fail - silently if it is unavailable. - - :: - - class User(db.Model): - - def __init__(self, username, remote_addr=None): - self.username = username - if remote_addr is None and has_request_context(): - remote_addr = request.remote_addr - self.remote_addr = remote_addr - - Alternatively you can also just test any of the context bound objects - (such as :class:`request` or :class:`g`) for truthness:: - - class User(db.Model): - - def __init__(self, username, remote_addr=None): - self.username = username - if remote_addr is None and request: - remote_addr = request.remote_addr - self.remote_addr = remote_addr - - .. versionadded:: 0.7 - """ - return _cv_request.get(None) is not None - - -def has_app_context() -> bool: - """Works like :func:`has_request_context` but for the application - context. You can also just do a boolean check on the - :data:`current_app` object instead. - - .. versionadded:: 0.9 - """ - return _cv_app.get(None) is not None - - -class AppContext: - """The app context contains application-specific information. An app - context is created and pushed at the beginning of each request if - one is not already active. An app context is also pushed when - running CLI commands. - """ - - def __init__(self, app: Flask) -> None: - self.app = app - self.url_adapter = app.create_url_adapter(None) - self.g: _AppCtxGlobals = app.app_ctx_globals_class() - self._cv_tokens: list[contextvars.Token[AppContext]] = [] - - def push(self) -> None: - """Binds the app context to the current context.""" - self._cv_tokens.append(_cv_app.set(self)) - appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) - - def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore - """Pops the app context.""" - try: - if len(self._cv_tokens) == 1: - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_appcontext(exc) - finally: - ctx = _cv_app.get() - _cv_app.reset(self._cv_tokens.pop()) - - if ctx is not self: - raise AssertionError( - f"Popped wrong app context. ({ctx!r} instead of {self!r})" - ) - - appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) - - def __enter__(self) -> AppContext: - self.push() - return self - - def __exit__( - self, - exc_type: type | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.pop(exc_value) - - -class RequestContext: - """The request context contains per-request information. The Flask - app creates and pushes it at the beginning of the request, then pops - it at the end of the request. It will create the URL adapter and - request object for the WSGI environment provided. - - Do not attempt to use this class directly, instead use - :meth:`~flask.Flask.test_request_context` and - :meth:`~flask.Flask.request_context` to create this object. - - When the request context is popped, it will evaluate all the - functions registered on the application for teardown execution - (:meth:`~flask.Flask.teardown_request`). - - The request context is automatically popped at the end of the - request. When using the interactive debugger, the context will be - restored so ``request`` is still accessible. Similarly, the test - client can preserve the context after the request ends. However, - teardown functions may already have closed some resources such as - database connections. - """ - - def __init__( - self, - app: Flask, - environ: WSGIEnvironment, - request: Request | None = None, - session: SessionMixin | None = None, - ) -> None: - self.app = app - if request is None: - request = app.request_class(environ) - request.json_module = app.json - self.request: Request = request - self.url_adapter = None - try: - self.url_adapter = app.create_url_adapter(self.request) - except HTTPException as e: - self.request.routing_exception = e - self.flashes: list[tuple[str, str]] | None = None - self.session: SessionMixin | None = session - # Functions that should be executed after the request on the response - # object. These will be called before the regular "after_request" - # functions. - self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] - - self._cv_tokens: list[ - tuple[contextvars.Token[RequestContext], AppContext | None] - ] = [] - - def copy(self) -> RequestContext: - """Creates a copy of this request context with the same request object. - This can be used to move a request context to a different greenlet. - Because the actual request object is the same this cannot be used to - move a request context to a different thread unless access to the - request object is locked. - - .. versionadded:: 0.10 - - .. versionchanged:: 1.1 - The current session object is used instead of reloading the original - data. This prevents `flask.session` pointing to an out-of-date object. - """ - return self.__class__( - self.app, - environ=self.request.environ, - request=self.request, - session=self.session, - ) - - def match_request(self) -> None: - """Can be overridden by a subclass to hook into the matching - of the request. - """ - try: - result = self.url_adapter.match(return_rule=True) # type: ignore - self.request.url_rule, self.request.view_args = result # type: ignore - except HTTPException as e: - self.request.routing_exception = e - - def push(self) -> None: - # Before we push the request context we have to ensure that there - # is an application context. - app_ctx = _cv_app.get(None) - - if app_ctx is None or app_ctx.app is not self.app: - app_ctx = self.app.app_context() - app_ctx.push() - else: - app_ctx = None - - self._cv_tokens.append((_cv_request.set(self), app_ctx)) - - # Open the session at the moment that the request context is available. - # This allows a custom open_session method to use the request context. - # Only open a new session if this is the first time the request was - # pushed, otherwise stream_with_context loses the session. - if self.session is None: - session_interface = self.app.session_interface - self.session = session_interface.open_session(self.app, self.request) - - if self.session is None: - self.session = session_interface.make_null_session(self.app) - - # Match the request URL after loading the session, so that the - # session is available in custom URL converters. - if self.url_adapter is not None: - self.match_request() - - def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore - """Pops the request context and unbinds it by doing that. This will - also trigger the execution of functions registered by the - :meth:`~flask.Flask.teardown_request` decorator. - - .. versionchanged:: 0.9 - Added the `exc` argument. - """ - clear_request = len(self._cv_tokens) == 1 - - try: - if clear_request: - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_request(exc) - - request_close = getattr(self.request, "close", None) - if request_close is not None: - request_close() - finally: - ctx = _cv_request.get() - token, app_ctx = self._cv_tokens.pop() - _cv_request.reset(token) - - # get rid of circular dependencies at the end of the request - # so that we don't require the GC to be active. - if clear_request: - ctx.request.environ["werkzeug.request"] = None - - if app_ctx is not None: - app_ctx.pop(exc) - - if ctx is not self: - raise AssertionError( - f"Popped wrong request context. ({ctx!r} instead of {self!r})" - ) - - def __enter__(self) -> RequestContext: - self.push() - return self - - def __exit__( - self, - exc_type: type | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.pop(exc_value) - - def __repr__(self) -> str: - return ( - f"<{type(self).__name__} {self.request.url!r}" - f" [{self.request.method}] of {self.app.name}>" - ) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py deleted file mode 100644 index 2c8c4c48..00000000 --- a/src/flask/debughelpers.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -import typing as t - -from jinja2.loaders import BaseLoader -from werkzeug.routing import RequestRedirect - -from .blueprints import Blueprint -from .globals import request_ctx -from .sansio.app import App - -if t.TYPE_CHECKING: - from .sansio.scaffold import Scaffold - from .wrappers import Request - - -class UnexpectedUnicodeError(AssertionError, UnicodeError): - """Raised in places where we want some better error reporting for - unexpected unicode or binary data. - """ - - -class DebugFilesKeyError(KeyError, AssertionError): - """Raised from request.files during debugging. The idea is that it can - provide a better error message than just a generic KeyError/BadRequest. - """ - - def __init__(self, request: Request, key: str) -> None: - form_matches = request.form.getlist(key) - buf = [ - f"You tried to access the file {key!r} in the request.files" - " dictionary but it does not exist. The mimetype for the" - f" request is {request.mimetype!r} instead of" - " 'multipart/form-data' which means that no file contents" - " 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( - "\n\nThe browser instead transmitted some file names. " - f"This was submitted: {names}" - ) - self.msg = "".join(buf) - - def __str__(self) -> str: - return self.msg - - -class FormDataRoutingRedirect(AssertionError): - """This exception is raised in debug mode if a routing redirect - would cause the browser to drop the method or body. This happens - when method is not GET, HEAD or OPTIONS and the status code is not - 307 or 308. - """ - - def __init__(self, request: Request) -> None: - exc = request.routing_exception - assert isinstance(exc, RequestRedirect) - buf = [ - f"A request was sent to '{request.url}', but routing issued" - f" a redirect to the canonical URL '{exc.new_url}'." - ] - - if f"{request.base_url}/" == exc.new_url.partition("?")[0]: - buf.append( - " The URL was defined with a trailing slash. Flask" - " will redirect to the URL with a trailing slash if it" - " was accessed without one." - ) - - buf.append( - " Send requests to the canonical URL, or use 307 or 308 for" - " routing redirects. Otherwise, browsers will drop form" - " data.\n\n" - "This exception is only raised in debug mode." - ) - super().__init__("".join(buf)) - - -def attach_enctype_error_multidict(request: Request) -> None: - """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): # type: ignore[valid-type, misc] - def __getitem__(self, key: str) -> t.Any: - try: - return super().__getitem__(key) - except KeyError as e: - if key not in request.form: - raise - - raise DebugFilesKeyError(request, key).with_traceback( - e.__traceback__ - ) from None - - newcls.__name__ = oldcls.__name__ - newcls.__module__ = oldcls.__module__ - request.files.__class__ = newcls - - -def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: - 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}" - - -def explain_template_loading_attempts( - app: App, - template: str, - attempts: list[ - tuple[ - BaseLoader, - Scaffold, - tuple[str, str | None, t.Callable[[], bool] | None] | None, - ] - ], -) -> None: - """This should help developers understand what failed""" - info = [f"Locating template {template!r}:"] - total_found = 0 - blueprint = None - if request_ctx and request_ctx.request.blueprint is not None: - blueprint = request_ctx.request.blueprint - - for idx, (loader, srcobj, triple) in enumerate(attempts): - if isinstance(srcobj, App): - 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) - - info.append(f"{idx + 1:5}: trying loader of {src_info}") - - for line in _dump_loader_info(loader): - info.append(f" {line}") - - 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 - - if blueprint is not None and seems_fishy: - info.append( - " The template was looked up from an endpoint that belongs" - f" to the blueprint {blueprint!r}." - ) - info.append(" Maybe you did not place a template in the right folder?") - info.append(" See https://flask.palletsprojects.com/blueprints/#templates") - - app.logger.info("\n".join(info)) diff --git a/src/flask/globals.py b/src/flask/globals.py deleted file mode 100644 index e2c410cc..00000000 --- a/src/flask/globals.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import typing as t -from contextvars import ContextVar - -from werkzeug.local import LocalProxy - -if t.TYPE_CHECKING: # pragma: no cover - from .app import Flask - from .ctx import _AppCtxGlobals - from .ctx import AppContext - from .ctx import RequestContext - from .sessions import SessionMixin - from .wrappers import Request - - -_no_app_msg = """\ -Working outside of application context. - -This typically means that you attempted to use functionality that needed -the current application. To solve this, set up an application context -with app.app_context(). See the documentation for more information.\ -""" -_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") -app_ctx: AppContext = LocalProxy( # type: ignore[assignment] - _cv_app, unbound_message=_no_app_msg -) -current_app: Flask = LocalProxy( # type: ignore[assignment] - _cv_app, "app", unbound_message=_no_app_msg -) -g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] - _cv_app, "g", unbound_message=_no_app_msg -) - -_no_req_msg = """\ -Working outside of request context. - -This typically means that you attempted to use functionality that needed -an active HTTP request. Consult the documentation on testing for -information about how to avoid this problem.\ -""" -_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") -request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] - _cv_request, unbound_message=_no_req_msg -) -request: Request = LocalProxy( # type: ignore[assignment] - _cv_request, "request", unbound_message=_no_req_msg -) -session: SessionMixin = LocalProxy( # type: ignore[assignment] - _cv_request, "session", unbound_message=_no_req_msg -) diff --git a/src/flask/helpers.py b/src/flask/helpers.py deleted file mode 100644 index 2b805847..00000000 --- a/src/flask/helpers.py +++ /dev/null @@ -1,633 +0,0 @@ -from __future__ import annotations - -import importlib.util -import os -import sys -import typing as t -from datetime import datetime -from functools import lru_cache -from functools import update_wrapper - -import werkzeug.utils -from werkzeug.exceptions import abort as _wz_abort -from werkzeug.utils import redirect as _wz_redirect -from werkzeug.wrappers import Response as BaseResponse - -from .globals import _cv_request -from .globals import current_app -from .globals import request -from .globals import request_ctx -from .globals import session -from .signals import message_flashed - -if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response - - -def get_debug_flag() -> bool: - """Get whether debug mode should be enabled for the app, indicated by the - :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. - """ - val = os.environ.get("FLASK_DEBUG") - return bool(val and val.lower() not in {"0", "false", "no"}) - - -def get_load_dotenv(default: bool = True) -> bool: - """Get whether the user has disabled loading default dotenv files by - setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load - the files. - - :param default: What to return if the env var isn't set. - """ - val = os.environ.get("FLASK_SKIP_DOTENV") - - if not val: - return default - - return val.lower() in ("0", "false", "no") - - -@t.overload -def stream_with_context( - generator_or_function: t.Iterator[t.AnyStr], -) -> t.Iterator[t.AnyStr]: ... - - -@t.overload -def stream_with_context( - generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], -) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... - - -def stream_with_context( - generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], -) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: - """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 - memory leaks with badly written WSGI middlewares. The downside is that if - you are using streamed responses, the generator cannot access request bound - information any more. - - This function however can help you keep the context around for longer:: - - from flask import stream_with_context, request, Response - - @app.route('/stream') - def streamed_response(): - @stream_with_context - def generate(): - yield 'Hello ' - yield request.args['name'] - yield '!' - return Response(generate()) - - Alternatively it can also be used around a specific generator:: - - from flask import stream_with_context, request, Response - - @app.route('/stream') - def streamed_response(): - def generate(): - yield 'Hello ' - yield request.args['name'] - yield '!' - return Response(stream_with_context(generate())) - - .. versionadded:: 0.9 - """ - try: - gen = iter(generator_or_function) # type: ignore[arg-type] - except TypeError: - - def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: - gen = generator_or_function(*args, **kwargs) # type: ignore[operator] - return stream_with_context(gen) - - return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type, return-value] - - def generator() -> t.Iterator[t.AnyStr | None]: - ctx = _cv_request.get(None) - if ctx is None: - raise RuntimeError( - "'stream_with_context' can only be used when a request" - " context is active, such as in a view function." - ) - with ctx: - # Dummy sentinel. Has to be inside the context block or we're - # not actually keeping the context around. - yield None - - # The try/finally is here so that if someone passes a WSGI level - # iterator in we're still running the cleanup logic. Generators - # don't need that because they are closed on their destruction - # automatically. - try: - yield from gen - finally: - if hasattr(gen, "close"): - gen.close() - - # The trick is to start the generator. Then the code execution runs until - # the first dummy None is yielded at which point the context was already - # pushed. This item is discarded. Then when the iteration continues the - # real generator is executed. - wrapped_g = generator() - next(wrapped_g) - return wrapped_g # type: ignore[return-value] - - -def make_response(*args: t.Any) -> Response: - """Sometimes it is necessary to set additional headers in a view. Because - views do not have to return response objects but can return a value that - is converted into a response object by Flask itself, it becomes tricky to - add headers to it. This function can be called instead of using a return - and you will get a response object which you can use to attach headers. - - If view looked like this and you want to add a new header:: - - def index(): - return render_template('index.html', foo=42) - - You can now do something like this:: - - def index(): - response = make_response(render_template('index.html', foo=42)) - response.headers['X-Parachutes'] = 'parachutes are cool' - return response - - This function accepts the very same arguments you can return from a - view function. This for example creates a response with a 404 error - code:: - - response = make_response(render_template('not_found.html'), 404) - - The other use case of this function is to force the return value of a - view function into a response which is helpful with view - decorators:: - - response = make_response(view_function()) - response.headers['X-Parachutes'] = 'parachutes are cool' - - Internally this function does the following things: - - - if no arguments are passed, it creates a new response argument - - if one argument is passed, :meth:`flask.Flask.make_response` - is invoked with it. - - if more than one argument is passed, the arguments are passed - to the :meth:`flask.Flask.make_response` function as tuple. - - .. versionadded:: 0.6 - """ - if not args: - return current_app.response_class() - if len(args) == 1: - args = args[0] - return current_app.make_response(args) - - -def url_for( - endpoint: str, - *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, - **values: t.Any, -) -> str: - """Generate a URL to the given endpoint with the given values. - - This requires an active request or application context, and calls - :meth:`current_app.url_for() `. See that method - for full documentation. - - :param endpoint: The endpoint name associated with the URL to - generate. If this starts with a ``.``, the current blueprint - name (if any) will be used. - :param _anchor: If given, append this as ``#anchor`` to the URL. - :param _method: If given, generate the URL associated with this - method for the endpoint. - :param _scheme: If given, the URL will have this scheme if it is - external. - :param _external: If given, prefer the URL to be internal (False) or - require it to be external (True). External URLs include the - scheme and domain. When not in an active request, URLs are - external by default. - :param values: Values to use for the variable parts of the URL rule. - Unknown keys are appended as query string arguments, like - ``?a=b&c=d``. - - .. versionchanged:: 2.2 - Calls ``current_app.url_for``, allowing an app to override the - behavior. - - .. versionchanged:: 0.10 - The ``_scheme`` parameter was added. - - .. versionchanged:: 0.9 - The ``_anchor`` and ``_method`` parameters were added. - - .. versionchanged:: 0.9 - Calls ``app.handle_url_build_error`` on build errors. - """ - return current_app.url_for( - endpoint, - _anchor=_anchor, - _method=_method, - _scheme=_scheme, - _external=_external, - **values, - ) - - -def redirect( - location: str, code: int = 302, Response: type[BaseResponse] | None = None -) -> BaseResponse: - """Create a redirect response object. - - If :data:`~flask.current_app` is available, it will use its - :meth:`~flask.Flask.redirect` method, otherwise it will use - :func:`werkzeug.utils.redirect`. - - :param location: The URL to redirect to. - :param code: The status code for the redirect. - :param Response: The response class to use. Not used when - ``current_app`` is active, which uses ``app.response_class``. - - .. versionadded:: 2.2 - Calls ``current_app.redirect`` if available instead of always - using Werkzeug's default ``redirect``. - """ - if current_app: - return current_app.redirect(location, code=code) - - return _wz_redirect(location, code=code, Response=Response) - - -def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: - """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given - status code. - - If :data:`~flask.current_app` is available, it will call its - :attr:`~flask.Flask.aborter` object, otherwise it will use - :func:`werkzeug.exceptions.abort`. - - :param code: The status code for the exception, which must be - registered in ``app.aborter``. - :param args: Passed to the exception. - :param kwargs: Passed to the exception. - - .. versionadded:: 2.2 - Calls ``current_app.aborter`` if available instead of always - using Werkzeug's default ``abort``. - """ - if current_app: - current_app.aborter(code, *args, **kwargs) - - _wz_abort(code, *args, **kwargs) - - -def get_template_attribute(template_name: str, attribute: str) -> t.Any: - """Loads a macro (or variable) a template exports. This can be used to - invoke a macro from within Python code. If you for example have a - template named :file:`_cider.html` with the following contents: - - .. sourcecode:: html+jinja - - {% macro hello(name) %}Hello {{ name }}!{% endmacro %} - - You can access this from Python code like this:: - - hello = get_template_attribute('_cider.html', 'hello') - return hello('World') - - .. versionadded:: 0.2 - - :param template_name: the name of the template - :param attribute: the name of the variable of macro to access - """ - return getattr(current_app.jinja_env.get_template(template_name).module, attribute) - - -def flash(message: str, category: str = "message") -> None: - """Flashes a message to the next request. In order to remove the - flashed message from the session and to display it to the user, - the template has to call :func:`get_flashed_messages`. - - .. versionchanged:: 0.3 - `category` parameter added. - - :param message: the message to be flashed. - :param category: the category for the message. The following values - are recommended: ``'message'`` for any kind of message, - ``'error'`` for errors, ``'info'`` for information - messages and ``'warning'`` for warnings. However any - kind of string can be used as category. - """ - # Original implementation: - # - # session.setdefault('_flashes', []).append((category, message)) - # - # This assumed that changes made to mutable structures in the session are - # always in sync with the session object, which is not true for session - # implementations that use external storage for keeping their keys/values. - flashes = session.get("_flashes", []) - flashes.append((category, message)) - session["_flashes"] = flashes - app = current_app._get_current_object() # type: ignore - message_flashed.send( - app, - _async_wrapper=app.ensure_sync, - message=message, - category=category, - ) - - -def get_flashed_messages( - with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> list[str] | list[tuple[str, str]]: - """Pulls all flashed messages from the session and returns them. - Further calls in the same request to the function will return - the same messages. By default just the messages are returned, - but when `with_categories` is set to ``True``, the return value will - be a list of tuples in the form ``(category, message)`` instead. - - Filter the flashed messages to one or more categories by providing those - categories in `category_filter`. This allows rendering categories in - separate html blocks. The `with_categories` and `category_filter` - arguments are distinct: - - * `with_categories` controls whether categories are returned with message - text (``True`` gives a tuple, where ``False`` gives just the message text). - * `category_filter` filters the messages down to only those matching the - provided categories. - - See :doc:`/patterns/flashing` for examples. - - .. versionchanged:: 0.3 - `with_categories` parameter added. - - .. versionchanged:: 0.9 - `category_filter` parameter added. - - :param with_categories: set to ``True`` to also receive categories. - :param category_filter: filter of categories to limit return values. Only - categories in the list will be returned. - """ - flashes = request_ctx.flashes - if flashes is None: - flashes = session.pop("_flashes") if "_flashes" in session else [] - request_ctx.flashes = flashes - if category_filter: - flashes = list(filter(lambda f: f[0] in category_filter, flashes)) - if not with_categories: - return [x[1] for x in flashes] - return flashes - - -def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: - if kwargs.get("max_age") is None: - kwargs["max_age"] = current_app.get_send_file_max_age - - kwargs.update( - environ=request.environ, - use_x_sendfile=current_app.config["USE_X_SENDFILE"], - response_class=current_app.response_class, - _root_path=current_app.root_path, # type: ignore - ) - return kwargs - - -def send_file( - path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, - mimetype: str | None = None, - as_attachment: bool = False, - download_name: str | None = None, - conditional: bool = True, - etag: bool | str = True, - last_modified: datetime | int | float | None = None, - max_age: None | (int | t.Callable[[str | None], int | None]) = None, -) -> Response: - """Send the contents of a file to the client. - - The first argument can be a file path or a file-like object. Paths - are preferred in most cases because Werkzeug can manage the file and - get extra information from the path. Passing a file-like object - requires that the file is opened in binary mode, and is mostly - useful when building a file in memory with :class:`io.BytesIO`. - - Never pass file paths provided by a user. The path is assumed to be - trusted, so a user could craft a path to access a file you didn't - intend. Use :func:`send_from_directory` to safely serve - user-requested paths from within a directory. - - If the WSGI server sets a ``file_wrapper`` in ``environ``, it is - used, otherwise Werkzeug's built-in wrapper is used. Alternatively, - if the HTTP server supports ``X-Sendfile``, configuring Flask with - ``USE_X_SENDFILE = True`` will tell the server to send the given - path, which is much more efficient than reading it in Python. - - :param path_or_file: The path to the file to send, relative to the - current working directory if a relative path is given. - Alternatively, a file-like object opened in binary mode. Make - sure the file pointer is seeked to the start of the data. - :param mimetype: The MIME type to send for the file. If not - provided, it will try to detect it from the file name. - :param as_attachment: Indicate to a browser that it should offer to - save the file instead of displaying it. - :param download_name: The default name browsers will use when saving - the file. Defaults to the passed file name. - :param conditional: Enable conditional and range responses based on - request headers. Requires passing a file path and ``environ``. - :param etag: Calculate an ETag for the file, which requires passing - a file path. Can also be a string to use instead. - :param last_modified: The last modified time to send for the file, - in seconds. If not provided, it will try to detect it from the - file path. - :param max_age: How long the client should cache the file, in - seconds. If set, ``Cache-Control`` will be ``public``, otherwise - it will be ``no-cache`` to prefer conditional caching. - - .. versionchanged:: 2.0 - ``download_name`` replaces the ``attachment_filename`` - parameter. If ``as_attachment=False``, it is passed with - ``Content-Disposition: inline`` instead. - - .. versionchanged:: 2.0 - ``max_age`` replaces the ``cache_timeout`` parameter. - ``conditional`` is enabled and ``max_age`` is not set by - default. - - .. versionchanged:: 2.0 - ``etag`` replaces the ``add_etags`` parameter. It can be a - string to use instead of generating one. - - .. versionchanged:: 2.0 - Passing a file-like object that inherits from - :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather - than sending an empty file. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionchanged:: 1.1 - ``filename`` may be a :class:`~os.PathLike` object. - - .. versionchanged:: 1.1 - Passing a :class:`~io.BytesIO` object supports range requests. - - .. versionchanged:: 1.0.3 - Filenames are encoded with ASCII instead of Latin-1 for broader - compatibility with WSGI servers. - - .. versionchanged:: 1.0 - UTF-8 filenames as specified in :rfc:`2231` are supported. - - .. versionchanged:: 0.12 - The filename is no longer automatically inferred from file - objects. If you want to use automatic MIME and etag support, - pass a filename via ``filename_or_fp`` or - ``attachment_filename``. - - .. versionchanged:: 0.12 - ``attachment_filename`` is preferred over ``filename`` for MIME - detection. - - .. versionchanged:: 0.9 - ``cache_timeout`` defaults to - :meth:`Flask.get_send_file_max_age`. - - .. versionchanged:: 0.7 - MIME guessing and etag support for file-like objects was - removed because it was unreliable. Pass a filename if you are - able to, otherwise attach an etag yourself. - - .. versionchanged:: 0.5 - The ``add_etags``, ``cache_timeout`` and ``conditional`` - parameters were added. The default behavior is to add etags. - - .. versionadded:: 0.2 - """ - return werkzeug.utils.send_file( # type: ignore[return-value] - **_prepare_send_file_kwargs( - path_or_file=path_or_file, - environ=request.environ, - mimetype=mimetype, - as_attachment=as_attachment, - download_name=download_name, - conditional=conditional, - etag=etag, - last_modified=last_modified, - max_age=max_age, - ) - ) - - -def send_from_directory( - directory: os.PathLike[str] | str, - path: os.PathLike[str] | str, - **kwargs: t.Any, -) -> Response: - """Send a file from within a directory using :func:`send_file`. - - .. code-block:: python - - @app.route("/uploads/") - def download_file(name): - return send_from_directory( - app.config['UPLOAD_FOLDER'], name, as_attachment=True - ) - - This is a secure way to serve files from a folder, such as static - files or uploads. Uses :func:`~werkzeug.security.safe_join` to - ensure the path coming from the client is not maliciously crafted to - point outside the specified directory. - - If the final path does not point to an existing regular file, - raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. - - :param directory: The directory that ``path`` must be located under, - relative to the current application's root path. - :param path: The path to the file to send, relative to - ``directory``. - :param kwargs: Arguments to pass to :func:`send_file`. - - .. versionchanged:: 2.0 - ``path`` replaces the ``filename`` parameter. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionadded:: 0.5 - """ - return werkzeug.utils.send_from_directory( # type: ignore[return-value] - directory, path, **_prepare_send_file_kwargs(**kwargs) - ) - - -def get_root_path(import_name: str) -> str: - """Find the root path of a package, or the path that contains a - module. If it cannot be found, returns the current working - directory. - - Not to be confused with the value returned by :func:`find_package`. - - :meta private: - """ - # Module already imported and has a file attribute. Use that first. - mod = sys.modules.get(import_name) - - if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: - return os.path.dirname(os.path.abspath(mod.__file__)) - - # Next attempt: check the loader. - try: - spec = importlib.util.find_spec(import_name) - - if spec is None: - raise ValueError - except (ImportError, ValueError): - loader = None - else: - loader = spec.loader - - # Loader does not exist or we're referring to an unloaded main - # module or a main module without path (interactive sessions), go - # with the current working directory. - if loader is None: - return os.getcwd() - - if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) - else: - # Fall back to imports. - __import__(import_name) - mod = sys.modules[import_name] - filepath = getattr(mod, "__file__", None) - - # If we don't have a file path it might be because it is a - # namespace package. In this case pick the root path from the - # first module that is contained in the package. - if filepath is None: - raise RuntimeError( - "No root path can be found for the provided module" - f" {import_name!r}. This can happen because the module" - " came from an import hook that does not provide file" - " name information or because it's a namespace package." - " In this case the root path needs to be explicitly" - " provided." - ) - - # filepath is import_name.py for a module, or __init__.py for a package. - return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] - - -@lru_cache(maxsize=None) -def _split_blueprint_path(name: str) -> list[str]: - out: list[str] = [name] - - if "." in name: - out.extend(_split_blueprint_path(name.rpartition(".")[0])) - - return out diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py deleted file mode 100644 index c0941d04..00000000 --- a/src/flask/json/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing as t - -from ..globals import current_app -from .provider import _default - -if t.TYPE_CHECKING: # pragma: no cover - from ..wrappers import Response - - -def dumps(obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.dumps() ` - method, otherwise it will use :func:`json.dumps`. - - :param obj: The data to serialize. - :param kwargs: Arguments passed to the ``dumps`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.dumps``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0.2 - :class:`decimal.Decimal` is supported by converting to a string. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - if current_app: - return current_app.json.dumps(obj, **kwargs) - - kwargs.setdefault("default", _default) - return _json.dumps(obj, **kwargs) - - -def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: - """Serialize data as JSON and write to a file. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.dump() ` - method, otherwise it will use :func:`json.dump`. - - :param obj: The data to serialize. - :param fp: A file opened for writing text. Should use the UTF-8 - encoding to be valid JSON. - :param kwargs: Arguments passed to the ``dump`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.dump``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0 - Writing to a binary file, and the ``encoding`` argument, will be - removed in Flask 2.1. - """ - if current_app: - current_app.json.dump(obj, fp, **kwargs) - else: - kwargs.setdefault("default", _default) - _json.dump(obj, fp, **kwargs) - - -def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.loads() ` - method, otherwise it will use :func:`json.loads`. - - :param s: Text or UTF-8 bytes. - :param kwargs: Arguments passed to the ``loads`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.loads``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. The data must be a - string or UTF-8 bytes. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - if current_app: - return current_app.json.loads(s, **kwargs) - - return _json.loads(s, **kwargs) - - -def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON read from a file. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.load() ` - method, otherwise it will use :func:`json.load`. - - :param fp: A file opened for reading text or UTF-8 bytes. - :param kwargs: Arguments passed to the ``load`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.load``, allowing an app to override - the behavior. - - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. The file must be text - mode, or binary mode with UTF-8 bytes. - """ - if current_app: - return current_app.json.load(fp, **kwargs) - - return _json.load(fp, **kwargs) - - -def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with the ``application/json`` - mimetype. A dict or list returned from a view will be converted to a - JSON response automatically without needing to call this. - - This requires an active request or application context, and calls - :meth:`app.json.response() `. - - In debug mode, the output is formatted with indentation to make it - easier to read. This may also be controlled by the provider. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - - .. versionchanged:: 2.2 - Calls ``current_app.json.response``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0.2 - :class:`decimal.Decimal` is supported by converting to a string. - - .. versionchanged:: 0.11 - Added support for serializing top-level arrays. This was a - security risk in ancient browsers. See :ref:`security-json`. - - .. versionadded:: 0.2 - """ - return current_app.json.response(*args, **kwargs) # type: ignore[return-value] diff --git a/src/flask/json/provider.py b/src/flask/json/provider.py deleted file mode 100644 index b086e668..00000000 --- a/src/flask/json/provider.py +++ /dev/null @@ -1,215 +0,0 @@ -from __future__ import annotations - -import dataclasses -import decimal -import json -import typing as t -import uuid -import weakref -from datetime import date - -from werkzeug.http import http_date - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.sansio.response import Response - - from ..sansio.app import App - - -class JSONProvider: - """A standard set of JSON operations for an application. Subclasses - of this can be used to customize JSON behavior or use different - JSON libraries. - - To implement a provider for a specific library, subclass this base - class and implement at least :meth:`dumps` and :meth:`loads`. All - other methods have default implementations. - - To use a different provider, either subclass ``Flask`` and set - :attr:`~flask.Flask.json_provider_class` to a provider class, or set - :attr:`app.json ` to an instance of the class. - - :param app: An application instance. This will be stored as a - :class:`weakref.proxy` on the :attr:`_app` attribute. - - .. versionadded:: 2.2 - """ - - def __init__(self, app: App) -> None: - self._app: App = weakref.proxy(app) - - def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON. - - :param obj: The data to serialize. - :param kwargs: May be passed to the underlying JSON library. - """ - raise NotImplementedError - - def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: - """Serialize data as JSON and write to a file. - - :param obj: The data to serialize. - :param fp: A file opened for writing text. Should use the UTF-8 - encoding to be valid JSON. - :param kwargs: May be passed to the underlying JSON library. - """ - fp.write(self.dumps(obj, **kwargs)) - - def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON. - - :param s: Text or UTF-8 bytes. - :param kwargs: May be passed to the underlying JSON library. - """ - raise NotImplementedError - - def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON read from a file. - - :param fp: A file opened for reading text or UTF-8 bytes. - :param kwargs: May be passed to the underlying JSON library. - """ - return self.loads(fp.read(), **kwargs) - - def _prepare_response_obj( - self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] - ) -> t.Any: - if args and kwargs: - raise TypeError("app.json.response() takes either args or kwargs, not both") - - if not args and not kwargs: - return None - - if len(args) == 1: - return args[0] - - return args or kwargs - - def response(self, *args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with the ``application/json`` - mimetype. - - The :func:`~flask.json.jsonify` function calls this method for - the current application. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - """ - obj = self._prepare_response_obj(args, kwargs) - return self._app.response_class(self.dumps(obj), mimetype="application/json") - - -def _default(o: t.Any) -> t.Any: - if isinstance(o, date): - return http_date(o) - - if isinstance(o, (decimal.Decimal, uuid.UUID)): - return str(o) - - if dataclasses and dataclasses.is_dataclass(o): - return dataclasses.asdict(o) # type: ignore[call-overload] - - if hasattr(o, "__html__"): - return str(o.__html__()) - - raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") - - -class DefaultJSONProvider(JSONProvider): - """Provide JSON operations using Python's built-in :mod:`json` - library. Serializes the following additional data types: - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - """ - - default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] - """Apply this function to any object that :meth:`json.dumps` does - not know how to serialize. It should return a valid JSON type or - raise a ``TypeError``. - """ - - ensure_ascii = True - """Replace non-ASCII characters with escape sequences. This may be - more compatible with some clients, but can be disabled for better - performance and size. - """ - - sort_keys = True - """Sort the keys in any serialized dicts. This may be useful for - some caching situations, but can be disabled for better performance. - When enabled, keys must all be strings, they are not converted - before sorting. - """ - - compact: bool | None = None - """If ``True``, or ``None`` out of debug mode, the :meth:`response` - output will not add indentation, newlines, or spaces. If ``False``, - or ``None`` in debug mode, it will use a non-compact representation. - """ - - mimetype = "application/json" - """The mimetype set in :meth:`response`.""" - - def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON to a string. - - Keyword arguments are passed to :func:`json.dumps`. Sets some - parameter defaults from the :attr:`default`, - :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. - - :param obj: The data to serialize. - :param kwargs: Passed to :func:`json.dumps`. - """ - kwargs.setdefault("default", self.default) - kwargs.setdefault("ensure_ascii", self.ensure_ascii) - kwargs.setdefault("sort_keys", self.sort_keys) - return json.dumps(obj, **kwargs) - - def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON from a string or bytes. - - :param s: Text or UTF-8 bytes. - :param kwargs: Passed to :func:`json.loads`. - """ - return json.loads(s, **kwargs) - - def response(self, *args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with it. The response mimetype - will be "application/json" and can be changed with - :attr:`mimetype`. - - If :attr:`compact` is ``False`` or debug mode is enabled, the - output will be formatted to be easier to read. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - """ - obj = self._prepare_response_obj(args, kwargs) - dump_args: dict[str, t.Any] = {} - - if (self.compact is None and self._app.debug) or self.compact is False: - dump_args.setdefault("indent", 2) - else: - dump_args.setdefault("separators", (",", ":")) - - return self._app.response_class( - f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype - ) diff --git a/src/flask/json/tag.py b/src/flask/json/tag.py deleted file mode 100644 index 8dc3629b..00000000 --- a/src/flask/json/tag.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -Tagged JSON -~~~~~~~~~~~ - -A compact representation for lossless serialization of non-standard JSON -types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this -to serialize the session data, but it may be useful in other places. It -can be extended to support other types. - -.. autoclass:: TaggedJSONSerializer - :members: - -.. autoclass:: JSONTag - :members: - -Let's see an example that adds support for -:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so -to handle this we will dump the items as a list of ``[key, value]`` -pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to -identify the type. The session serializer processes dicts first, so -insert the new tag at the front of the order since ``OrderedDict`` must -be processed before ``dict``. - -.. code-block:: python - - from flask.json.tag import JSONTag - - class TagOrderedDict(JSONTag): - __slots__ = ('serializer',) - key = ' od' - - def check(self, value): - return isinstance(value, OrderedDict) - - def to_json(self, value): - return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] - - def to_python(self, value): - return OrderedDict(value) - - app.session_interface.serializer.register(TagOrderedDict, index=0) -""" - -from __future__ import annotations - -import typing as t -from base64 import b64decode -from base64 import b64encode -from datetime import datetime -from uuid import UUID - -from markupsafe import Markup -from werkzeug.http import http_date -from werkzeug.http import parse_date - -from ..json import dumps -from ..json import loads - - -class JSONTag: - """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" - - __slots__ = ("serializer",) - - #: The tag to mark the serialized object with. If empty, this tag is - #: only used as an intermediate step during tagging. - key: str = "" - - def __init__(self, serializer: TaggedJSONSerializer) -> None: - """Create a tagger for the given serializer.""" - self.serializer = serializer - - def check(self, value: t.Any) -> bool: - """Check if the given value should be tagged by this tag.""" - raise NotImplementedError - - def to_json(self, value: t.Any) -> t.Any: - """Convert the Python object to an object that is a valid JSON type. - The tag will be added later.""" - raise NotImplementedError - - def to_python(self, value: t.Any) -> t.Any: - """Convert the JSON representation back to the correct type. The tag - will already be removed.""" - raise NotImplementedError - - def tag(self, value: t.Any) -> dict[str, t.Any]: - """Convert the value to a valid JSON type and add the tag structure - around it.""" - return {self.key: self.to_json(value)} - - -class TagDict(JSONTag): - """Tag for 1-item dicts whose only key matches a registered tag. - - Internally, the dict key is suffixed with `__`, and the suffix is removed - when deserializing. - """ - - __slots__ = () - key = " di" - - def check(self, value: t.Any) -> bool: - return ( - isinstance(value, dict) - and len(value) == 1 - and next(iter(value)) in self.serializer.tags - ) - - def to_json(self, value: t.Any) -> t.Any: - key = next(iter(value)) - return {f"{key}__": self.serializer.tag(value[key])} - - def to_python(self, value: t.Any) -> t.Any: - key = next(iter(value)) - return {key[:-2]: value[key]} - - -class PassDict(JSONTag): - __slots__ = () - - def check(self, value: t.Any) -> bool: - return isinstance(value, dict) - - def to_json(self, value: t.Any) -> t.Any: - # JSON objects may only have string keys, so don't bother tagging the - # key here. - return {k: self.serializer.tag(v) for k, v in value.items()} - - tag = to_json - - -class TagTuple(JSONTag): - __slots__ = () - key = " t" - - def check(self, value: t.Any) -> bool: - return isinstance(value, tuple) - - def to_json(self, value: t.Any) -> t.Any: - return [self.serializer.tag(item) for item in value] - - def to_python(self, value: t.Any) -> t.Any: - return tuple(value) - - -class PassList(JSONTag): - __slots__ = () - - def check(self, value: t.Any) -> bool: - return isinstance(value, list) - - def to_json(self, value: t.Any) -> t.Any: - return [self.serializer.tag(item) for item in value] - - tag = to_json - - -class TagBytes(JSONTag): - __slots__ = () - key = " b" - - def check(self, value: t.Any) -> bool: - return isinstance(value, bytes) - - def to_json(self, value: t.Any) -> t.Any: - return b64encode(value).decode("ascii") - - def to_python(self, value: t.Any) -> t.Any: - return b64decode(value) - - -class TagMarkup(JSONTag): - """Serialize anything matching the :class:`~markupsafe.Markup` API by - having a ``__html__`` method to the result of that method. Always - deserializes to an instance of :class:`~markupsafe.Markup`.""" - - __slots__ = () - key = " m" - - def check(self, value: t.Any) -> bool: - return callable(getattr(value, "__html__", None)) - - def to_json(self, value: t.Any) -> t.Any: - return str(value.__html__()) - - def to_python(self, value: t.Any) -> t.Any: - return Markup(value) - - -class TagUUID(JSONTag): - __slots__ = () - key = " u" - - def check(self, value: t.Any) -> bool: - return isinstance(value, UUID) - - def to_json(self, value: t.Any) -> t.Any: - return value.hex - - def to_python(self, value: t.Any) -> t.Any: - return UUID(value) - - -class TagDateTime(JSONTag): - __slots__ = () - key = " d" - - def check(self, value: t.Any) -> bool: - return isinstance(value, datetime) - - def to_json(self, value: t.Any) -> t.Any: - return http_date(value) - - def to_python(self, value: t.Any) -> t.Any: - return parse_date(value) - - -class TaggedJSONSerializer: - """Serializer that uses a tag system to compactly represent objects that - are not JSON types. Passed as the intermediate serializer to - :class:`itsdangerous.Serializer`. - - The following extra types are supported: - - * :class:`dict` - * :class:`tuple` - * :class:`bytes` - * :class:`~markupsafe.Markup` - * :class:`~uuid.UUID` - * :class:`~datetime.datetime` - """ - - __slots__ = ("tags", "order") - - #: Tag classes to bind when creating the serializer. Other tags can be - #: added later using :meth:`~register`. - default_tags = [ - TagDict, - PassDict, - TagTuple, - PassList, - TagBytes, - TagMarkup, - TagUUID, - TagDateTime, - ] - - def __init__(self) -> None: - self.tags: dict[str, JSONTag] = {} - self.order: list[JSONTag] = [] - - for cls in self.default_tags: - self.register(cls) - - def register( - self, - tag_class: type[JSONTag], - force: bool = False, - index: int | None = None, - ) -> None: - """Register a new tag with this serializer. - - :param tag_class: tag class to register. Will be instantiated with this - serializer instance. - :param force: overwrite an existing tag. If false (default), a - :exc:`KeyError` is raised. - :param index: index to insert the new tag in the tag order. Useful when - the new tag is a special case of an existing tag. If ``None`` - (default), the tag is appended to the end of the order. - - :raise KeyError: if the tag key is already registered and ``force`` is - not true. - """ - tag = tag_class(self) - key = tag.key - - if key: - if not force and key in self.tags: - raise KeyError(f"Tag '{key}' is already registered.") - - self.tags[key] = tag - - if index is None: - self.order.append(tag) - else: - self.order.insert(index, tag) - - def tag(self, value: t.Any) -> t.Any: - """Convert a value to a tagged representation if necessary.""" - for tag in self.order: - if tag.check(value): - return tag.tag(value) - - return value - - def untag(self, value: dict[str, t.Any]) -> t.Any: - """Convert a tagged representation back to the original type.""" - if len(value) != 1: - return value - - key = next(iter(value)) - - if key not in self.tags: - return value - - return self.tags[key].to_python(value[key]) - - def _untag_scan(self, value: t.Any) -> t.Any: - if isinstance(value, dict): - # untag each item recursively - value = {k: self._untag_scan(v) for k, v in value.items()} - # untag the dict itself - value = self.untag(value) - elif isinstance(value, list): - # untag each item recursively - value = [self._untag_scan(item) for item in value] - - return value - - def dumps(self, value: t.Any) -> str: - """Tag the value and dump it to a compact JSON string.""" - return dumps(self.tag(value), separators=(",", ":")) - - def loads(self, value: str) -> t.Any: - """Load data from a JSON string and deserialized any tagged objects.""" - return self._untag_scan(loads(value)) diff --git a/src/flask/logging.py b/src/flask/logging.py deleted file mode 100644 index 0cb8f437..00000000 --- a/src/flask/logging.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -import logging -import sys -import typing as t - -from werkzeug.local import LocalProxy - -from .globals import request - -if t.TYPE_CHECKING: # pragma: no cover - from .sansio.app import App - - -@LocalProxy -def wsgi_errors_stream() -> t.TextIO: - """Find the most appropriate error stream for the application. If a request - is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. - - If you configure your own :class:`logging.StreamHandler`, you may want to - use this for the stream. If you are using file or dict configuration and - can't import this directly, you can refer to it as - ``ext://flask.logging.wsgi_errors_stream``. - """ - if request: - return request.environ["wsgi.errors"] # type: ignore[no-any-return] - - return sys.stderr - - -def has_level_handler(logger: logging.Logger) -> bool: - """Check if there is a handler in the logging chain that will handle the - given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. - """ - level = logger.getEffectiveLevel() - current = logger - - while current: - if any(handler.level <= level for handler in current.handlers): - return True - - if not current.propagate: - break - - current = current.parent # type: ignore - - return False - - -#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format -#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. -default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore -default_handler.setFormatter( - logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") -) - - -def create_logger(app: App) -> logging.Logger: - """Get the Flask app's logger and configure it if needed. - - The logger name will be the same as - :attr:`app.import_name `. - - When :attr:`~flask.Flask.debug` is enabled, set the logger level to - :data:`logging.DEBUG` if it is not set. - - If there is no handler for the logger's effective level, add a - :class:`~logging.StreamHandler` for - :func:`~flask.logging.wsgi_errors_stream` with a basic format. - """ - logger = logging.getLogger(app.name) - - if app.debug and not logger.level: - logger.setLevel(logging.DEBUG) - - if not has_level_handler(logger): - logger.addHandler(default_handler) - - return logger diff --git a/src/flask/py.typed b/src/flask/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/src/flask/sansio/README.md b/src/flask/sansio/README.md deleted file mode 100644 index 623ac198..00000000 --- a/src/flask/sansio/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Sansio - -This folder contains code that can be used by alternative Flask -implementations, for example Quart. The code therefore cannot do any -IO, nor be part of a likely IO path. Finally this code cannot use the -Flask globals. diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py deleted file mode 100644 index c091b5cb..00000000 --- a/src/flask/sansio/app.py +++ /dev/null @@ -1,964 +0,0 @@ -from __future__ import annotations - -import logging -import os -import sys -import typing as t -from datetime import timedelta -from itertools import chain - -from werkzeug.exceptions import Aborter -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import BadRequestKeyError -from werkzeug.routing import BuildError -from werkzeug.routing import Map -from werkzeug.routing import Rule -from werkzeug.sansio.response import Response -from werkzeug.utils import cached_property -from werkzeug.utils import redirect as _wz_redirect - -from .. import typing as ft -from ..config import Config -from ..config import ConfigAttribute -from ..ctx import _AppCtxGlobals -from ..helpers import _split_blueprint_path -from ..helpers import get_debug_flag -from ..json.provider import DefaultJSONProvider -from ..json.provider import JSONProvider -from ..logging import create_logger -from ..templating import DispatchingJinjaLoader -from ..templating import Environment -from .scaffold import _endpoint_from_view_func -from .scaffold import find_package -from .scaffold import Scaffold -from .scaffold import setupmethod - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.wrappers import Response as BaseResponse - - from ..testing import FlaskClient - from ..testing import FlaskCliRunner - from .blueprints import Blueprint - -T_shell_context_processor = t.TypeVar( - "T_shell_context_processor", bound=ft.ShellContextProcessorCallable -) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) - - -def _make_timedelta(value: timedelta | int | None) -> timedelta | None: - if value is None or isinstance(value, timedelta): - return value - - return timedelta(seconds=value) - - -class App(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 - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the :file:`__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea of what - belongs to your application. This name is used to find resources - on the filesystem, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in :file:`yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplication.app` and not - `yourapplication.views.frontend`) - - .. versionadded:: 0.7 - The `static_url_path`, `static_folder`, and `template_folder` - parameters were added. - - .. versionadded:: 0.8 - The `instance_path` and `instance_relative_config` parameters were - added. - - .. versionadded:: 0.11 - The `root_path` parameter was added. - - .. versionadded:: 1.0 - The ``host_matching`` and ``static_host`` parameters were added. - - .. versionadded:: 1.0 - The ``subdomain_matching`` parameter was added. Subdomain - matching needs to be enabled manually now. Setting - :data:`SERVER_NAME` does not implicitly enable it. - - :param import_name: the name of the application package - :param static_url_path: can be used to specify a different path for the - static files on the web. Defaults to the name - of the `static_folder` folder. - :param static_folder: The folder with static files that is served at - ``static_url_path``. Relative to the application ``root_path`` - or an absolute path. Defaults to ``'static'``. - :param static_host: the host to use when adding the static route. - Defaults to None. Required when using ``host_matching=True`` - with a ``static_folder`` configured. - :param host_matching: set ``url_map.host_matching`` attribute. - Defaults to False. - :param subdomain_matching: consider the subdomain relative to - :data:`SERVER_NAME` when matching routes. Defaults to False. - :param template_folder: the folder that contains the templates that should - be used by the application. Defaults to - ``'templates'`` folder in the root path of the - application. - :param instance_path: An alternative instance path for the application. - By default the folder ``'instance'`` next to the - package or module is assumed to be the instance - path. - :param instance_relative_config: if set to ``True`` relative filenames - for loading the config are assumed to - be relative to the instance path instead - of the application root. - :param root_path: The path to the root of the application files. - This should only be set manually when it can't be detected - automatically, such as for namespace packages. - """ - - #: The class of the object assigned to :attr:`aborter`, created by - #: :meth:`create_aborter`. That object is called by - #: :func:`flask.abort` to raise HTTP errors, and can be - #: called directly as well. - #: - #: Defaults to :class:`werkzeug.exceptions.Aborter`. - #: - #: .. versionadded:: 2.2 - aborter_class = Aborter - - #: The class that is used for the Jinja environment. - #: - #: .. versionadded:: 0.11 - jinja_environment = Environment - - #: The class that is used for the :data:`~flask.g` instance. - #: - #: Example use cases for a custom class: - #: - #: 1. Store arbitrary attributes on flask.g. - #: 2. Add a property for lazy per-request database connectors. - #: 3. Return None instead of AttributeError on unexpected attributes. - #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. - #: - #: In Flask 0.9 this property was called `request_globals_class` but it - #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the - #: flask.g object is now application context scoped. - #: - #: .. versionadded:: 0.10 - app_ctx_globals_class = _AppCtxGlobals - - #: The class that is used for the ``config`` attribute of this app. - #: Defaults to :class:`~flask.Config`. - #: - #: Example use cases for a custom class: - #: - #: 1. Default values for certain config options. - #: 2. Access to config values through attributes in addition to keys. - #: - #: .. versionadded:: 0.11 - config_class = Config - - #: The testing flag. Set this to ``True`` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate test helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the - #: default it's implicitly enabled. - #: - #: This attribute can also be configured from the config with the - #: ``TESTING`` configuration key. Defaults to ``False``. - testing = ConfigAttribute[bool]("TESTING") - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. - secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute[timedelta]( - "PERMANENT_SESSION_LIFETIME", - get_converter=_make_timedelta, # type: ignore[arg-type] - ) - - json_provider_class: type[JSONProvider] = DefaultJSONProvider - """A subclass of :class:`~flask.json.provider.JSONProvider`. An - instance is created and assigned to :attr:`app.json` when creating - the app. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses - Python's built-in :mod:`json` library. A different provider can use - a different JSON library. - - .. versionadded:: 2.2 - """ - - #: Options that are passed to the Jinja environment in - #: :meth:`create_jinja_environment`. Changing these options after - #: the environment is created (accessing :attr:`jinja_env`) will - #: have no effect. - #: - #: .. versionchanged:: 1.1.0 - #: This is a ``dict`` instead of an ``ImmutableDict`` to allow - #: easier configuration. - #: - jinja_options: dict[str, t.Any] = {} - - #: The rule object to use for URL rules created. This is used by - #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. - #: - #: .. versionadded:: 0.7 - url_rule_class = Rule - - #: The map object to use for storing the URL rules and routing - #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. - #: - #: .. versionadded:: 1.1.0 - url_map_class = Map - - #: The :meth:`test_client` method creates an instance of this test - #: client class. Defaults to :class:`~flask.testing.FlaskClient`. - #: - #: .. versionadded:: 0.7 - test_client_class: type[FlaskClient] | None = None - - #: The :class:`~click.testing.CliRunner` subclass, by default - #: :class:`~flask.testing.FlaskCliRunner` that is used by - #: :meth:`test_cli_runner`. Its ``__init__`` method should take a - #: Flask app object as the first argument. - #: - #: .. versionadded:: 1.0 - test_cli_runner_class: type[FlaskCliRunner] | None = None - - default_config: dict[str, t.Any] - response_class: type[Response] - - def __init__( - self, - import_name: str, - static_url_path: str | None = None, - static_folder: str | os.PathLike[str] | None = "static", - static_host: str | None = None, - host_matching: bool = False, - subdomain_matching: bool = False, - template_folder: str | os.PathLike[str] | None = "templates", - instance_path: str | None = None, - instance_relative_config: bool = False, - root_path: str | None = None, - ) -> None: - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if instance_path is None: - instance_path = self.auto_find_instance_path() - elif not os.path.isabs(instance_path): - raise ValueError( - "If an instance path is provided it must be absolute." - " A relative path was given instead." - ) - - #: Holds the path to the instance folder. - #: - #: .. versionadded:: 0.8 - self.instance_path = instance_path - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = self.make_config(instance_relative_config) - - #: An instance of :attr:`aborter_class` created by - #: :meth:`make_aborter`. This is called by :func:`flask.abort` - #: to raise HTTP errors, and can be called directly as well. - #: - #: .. versionadded:: 2.2 - #: Moved from ``flask.abort``, which calls this object. - self.aborter = self.make_aborter() - - self.json: JSONProvider = self.json_provider_class(self) - """Provides access to JSON methods. Functions in ``flask.json`` - will call methods on this provider when the application context - is active. Used for handling JSON requests and responses. - - An instance of :attr:`json_provider_class`. Can be customized by - changing that attribute on a subclass, or by assigning to this - attribute afterwards. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, - uses Python's built-in :mod:`json` library. A different provider - can use a different JSON library. - - .. versionadded:: 2.2 - """ - - #: A list of functions that are called by - #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function is called - #: with ``error``, ``endpoint`` and ``values``. If a function - #: returns ``None`` or raises a ``BuildError``, it is skipped. - #: Otherwise, its return value is returned by ``url_for``. - #: - #: .. versionadded:: 0.9 - self.url_build_error_handlers: list[ - t.Callable[[Exception, str, dict[str, t.Any]], str] - ] = [] - - #: 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 - #: from databases. - #: - #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] - - #: A list of shell context processor functions that should be run - #: when a shell context is created. - #: - #: .. versionadded:: 0.11 - self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] - - #: Maps registered blueprint names to blueprint objects. The - #: dict retains the order the blueprints were registered in. - #: Blueprints can be registered multiple times, this dict does - #: not track how often they were attached. - #: - #: .. versionadded:: 0.7 - self.blueprints: dict[str, Blueprint] = {} - - #: a place where extensions can store application specific state. For - #: example this is where an extension could store database engines and - #: similar things. - #: - #: The key must match the name of the extension module. For example in - #: case of a "Flask-Foo" extension in `flask_foo`, the key would be - #: ``'foo'``. - #: - #: .. versionadded:: 0.7 - self.extensions: dict[str, t.Any] = {} - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug.routing import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(super(ListConverter, self).to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = self.url_map_class(host_matching=host_matching) - - self.subdomain_matching = subdomain_matching - - # tracks internally if the application already handled at least one - # request. - self._got_first_request = False - - 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." - ) - - @cached_property - def name(self) -> str: # type: ignore - """The name of the application. This is usually the import name - with the difference that it's guessed from the run file if the - import name is main. This name is used as a display name when - Flask needs the name of the application. It can be set and overridden - to change the value. - - .. versionadded:: 0.8 - """ - if self.import_name == "__main__": - fn: str | None = getattr(sys.modules["__main__"], "__file__", None) - if fn is None: - return "__main__" - return os.path.splitext(os.path.basename(fn))[0] - return self.import_name - - @cached_property - def logger(self) -> logging.Logger: - """A standard Python :class:`~logging.Logger` for the app, with - the same name as :attr:`name`. - - In debug mode, the logger's :attr:`~logging.Logger.level` will - be set to :data:`~logging.DEBUG`. - - If there are no handlers configured, a default handler will be - added. See :doc:`/logging` for more information. - - .. versionchanged:: 1.1.0 - The logger takes the same name as :attr:`name` rather than - hard-coding ``"flask.app"``. - - .. versionchanged:: 1.0.0 - Behavior was simplified. The logger is always named - ``"flask.app"``. The level is only set during configuration, - it doesn't check ``app.debug`` each time. Only one format is - used, not different ones depending on ``app.debug``. No - handlers are removed, and a handler is only added if no - handlers are already configured. - - .. versionadded:: 0.3 - """ - return create_logger(self) - - @cached_property - def jinja_env(self) -> Environment: - """The Jinja environment used to load templates. - - The environment is created the first time this property is - accessed. Changing :attr:`jinja_options` after that will have no - effect. - """ - return self.create_jinja_environment() - - def create_jinja_environment(self) -> Environment: - raise NotImplementedError() - - def make_config(self, instance_relative: bool = False) -> Config: - """Used to create the config attribute by the Flask constructor. - The `instance_relative` parameter is passed in from the constructor - of Flask (there named `instance_relative_config`) and indicates if - the config should be relative to the instance path or the root path - of the application. - - .. versionadded:: 0.8 - """ - root_path = self.root_path - if instance_relative: - root_path = self.instance_path - defaults = dict(self.default_config) - defaults["DEBUG"] = get_debug_flag() - return self.config_class(root_path, defaults) - - def make_aborter(self) -> Aborter: - """Create the object to assign to :attr:`aborter`. That object - is called by :func:`flask.abort` to raise HTTP errors, and can - be called directly as well. - - By default, this creates an instance of :attr:`aborter_class`, - which defaults to :class:`werkzeug.exceptions.Aborter`. - - .. versionadded:: 2.2 - """ - return self.aborter_class() - - def auto_find_instance_path(self) -> str: - """Tries to locate the instance path if it was not provided to the - constructor of the application class. It will basically calculate - the path to a folder named ``instance`` next to your main file or - the package. - - .. versionadded:: 0.8 - """ - prefix, package_path = find_package(self.import_name) - if prefix is None: - return os.path.join(package_path, "instance") - return os.path.join(prefix, "var", f"{self.name}-instance") - - def create_global_jinja_loader(self) -> DispatchingJinjaLoader: - """Creates the loader for the Jinja2 environment. Can be used to - override just the loader and keeping the rest unchanged. It's - discouraged to override this function. Instead one should override - the :meth:`jinja_loader` function instead. - - The global loader dispatches between the loaders of the application - and the individual blueprints. - - .. versionadded:: 0.7 - """ - return DispatchingJinjaLoader(self) - - def select_jinja_autoescape(self, filename: str) -> bool: - """Returns ``True`` if autoescaping should be active for the given - template name. If no template name is given, returns `True`. - - .. versionchanged:: 2.2 - Autoescaping is now enabled by default for ``.svg`` files. - - .. versionadded:: 0.5 - """ - if filename is None: - return True - return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) - - @property - def debug(self) -> bool: - """Whether debug mode is enabled. When using ``flask run`` to start the - development server, an interactive debugger will be shown for unhandled - exceptions, and the server will be reloaded when code changes. This maps to the - :data:`DEBUG` config key. It may not behave as expected if set late. - - **Do not enable debug mode when deploying in production.** - - Default: ``False`` - """ - return self.config["DEBUG"] # type: ignore[no-any-return] - - @debug.setter - def debug(self, value: bool) -> None: - self.config["DEBUG"] = value - - if self.config["TEMPLATES_AUTO_RELOAD"] is None: - self.jinja_env.auto_reload = value - - @setupmethod - def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on the application. Keyword - arguments passed to this method will override the defaults set on the - blueprint. - - Calls the blueprint's :meth:`~flask.Blueprint.register` method after - recording the blueprint in the application's :attr:`blueprints`. - - :param blueprint: The blueprint to register. - :param url_prefix: Blueprint routes will be prefixed with this. - :param subdomain: Blueprint routes will match on this subdomain. - :param url_defaults: Blueprint routes will use these default values for - view arguments. - :param options: Additional keyword arguments are passed to - :class:`~flask.blueprints.BlueprintSetupState`. They can be - accessed in :meth:`~flask.Blueprint.record` callbacks. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 0.7 - """ - blueprint.register(self, options) - - def iter_blueprints(self) -> t.ValuesView[Blueprint]: - """Iterates over all blueprints by the order they were registered. - - .. versionadded:: 0.11 - """ - return self.blueprints.values() - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - options["endpoint"] = endpoint - methods = options.pop("methods", None) - - # if the methods are not given and the view_func object knows its - # methods we can use that instead. If neither exists, we go with - # a tuple of only ``GET`` as default. - if methods is None: - methods = getattr(view_func, "methods", None) or ("GET",) - if isinstance(methods, str): - raise TypeError( - "Allowed methods must be a list of strings, for" - ' example: @app.route(..., methods=["POST"])' - ) - methods = {item.upper() for item in methods} - - # Methods that should always be added - required_methods = set(getattr(view_func, "required_methods", ())) - - # starting with Flask 0.8 the view_func object can disable and - # force-enable the automatic options handling. - if provide_automatic_options is None: - provide_automatic_options = getattr( - view_func, "provide_automatic_options", None - ) - - if provide_automatic_options is None: - if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]: - provide_automatic_options = True - required_methods.add("OPTIONS") - else: - provide_automatic_options = False - - # Add the required methods now. - methods |= required_methods - - rule_obj = self.url_rule_class(rule, methods=methods, **options) - rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] - - self.url_map.add(rule_obj) - if view_func is not None: - old_func = self.view_functions.get(endpoint) - if old_func is not None and old_func != view_func: - raise AssertionError( - "View function mapping is overwriting an existing" - f" endpoint function: {endpoint}" - ) - self.view_functions[endpoint] = view_func - - @setupmethod - def template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: - """A decorator that is used to register custom template filter. - You can specify a name for the filter, otherwise the function - name will be used. Example:: - - @app.template_filter() - def reverse(s): - return s[::-1] - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_filter( - self, f: ft.TemplateFilterCallable, name: str | None = None - ) -> None: - """Register a custom template filter. Works exactly like the - :meth:`template_filter` decorator. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - self.jinja_env.filters[name or f.__name__] = f - - @setupmethod - def template_test( - self, name: str | None = None - ) -> t.Callable[[T_template_test], T_template_test]: - """A decorator that is used to register custom template test. - You can specify a name for the test, otherwise the function - name will be used. Example:: - - @app.template_test() - def is_prime(n): - if n == 2: - return True - for i in range(2, int(math.ceil(math.sqrt(n))) + 1): - if n % i == 0: - return False - return True - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: T_template_test) -> T_template_test: - self.add_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_test( - self, f: ft.TemplateTestCallable, name: str | None = None - ) -> None: - """Register a custom template test. Works exactly like the - :meth:`template_test` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - self.jinja_env.tests[name or f.__name__] = f - - @setupmethod - def template_global( - self, name: str | None = None - ) -> t.Callable[[T_template_global], T_template_global]: - """A decorator that is used to register a custom template global function. - You can specify a name for the global function, otherwise the function - name will be used. Example:: - - @app.template_global() - def double(n): - return 2 * n - - .. versionadded:: 0.10 - - :param name: the optional name of the global function, otherwise the - function name will be used. - """ - - def decorator(f: T_template_global) -> T_template_global: - self.add_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_global( - self, f: ft.TemplateGlobalCallable, name: str | None = None - ) -> None: - """Register a custom template global function. Works exactly like the - :meth:`template_global` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the global function, otherwise the - function name will be used. - """ - self.jinja_env.globals[name or f.__name__] = f - - @setupmethod - def teardown_appcontext(self, f: T_teardown) -> T_teardown: - """Registers a function to be called when the application - context is popped. The application context is typically popped - after the request context for each request, at the end of CLI - commands, or after a manually pushed context ends. - - .. code-block:: python - - with app.app_context(): - ... - - When the ``with`` block exits (or ``ctx.pop()`` is called), the - teardown functions are called just before the app context is - made inactive. Since a request context typically also manages an - application context it would also be called when you pop a - request context. - - When a teardown function was called because of an unhandled - exception it will be passed an error object. If an - :meth:`errorhandler` is registered, it will handle the exception - and the teardown will not receive it. - - Teardown functions must avoid raising exceptions. If they - execute code that might fail they must surround that code with a - ``try``/``except`` block and log any errors. - - The return values of teardown functions are ignored. - - .. versionadded:: 0.9 - """ - self.teardown_appcontext_funcs.append(f) - return f - - @setupmethod - def shell_context_processor( - self, f: T_shell_context_processor - ) -> T_shell_context_processor: - """Registers a shell context processor function. - - .. versionadded:: 0.11 - """ - self.shell_context_processors.append(f) - return f - - def _find_error_handler( - self, e: Exception, blueprints: list[str] - ) -> ft.ErrorHandlerCallable | None: - """Return a registered error handler for an exception in this order: - blueprint handler for a specific code, app handler for a specific code, - blueprint handler for an exception class, app handler for an exception - class, or ``None`` if a suitable handler is not found. - """ - exc_class, code = self._get_exc_class_and_code(type(e)) - names = (*blueprints, None) - - for c in (code, None) if code is not None else (None,): - for name in names: - handler_map = self.error_handler_spec[name][c] - - if not handler_map: - continue - - for cls in exc_class.__mro__: - handler = handler_map.get(cls) - - if handler is not None: - return handler - return None - - def trap_http_exception(self, e: Exception) -> bool: - """Checks if an HTTP exception should be trapped or not. By default - this will return ``False`` for all exceptions except for a bad request - key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It - also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. - - This is called for all HTTP exceptions raised by a view function. - If it returns ``True`` for any exception the error handler for this - exception is not called and it shows up as regular exception in the - traceback. This is helpful for debugging implicitly raised HTTP - exceptions. - - .. versionchanged:: 1.0 - Bad request errors are not trapped by default in debug mode. - - .. versionadded:: 0.8 - """ - if self.config["TRAP_HTTP_EXCEPTIONS"]: - return True - - trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] - - # if unset, trap key errors in debug mode - if ( - trap_bad_request is None - and self.debug - and isinstance(e, BadRequestKeyError) - ): - return True - - if trap_bad_request: - return isinstance(e, BadRequest) - - return False - - def should_ignore_error(self, error: BaseException | None) -> bool: - """This is called to figure out if an error should be ignored - or not as far as the teardown system is concerned. If this - function returns ``True`` then the teardown handlers will not be - passed the error. - - .. versionadded:: 0.10 - """ - return False - - def redirect(self, location: str, code: int = 302) -> BaseResponse: - """Create a redirect response object. - - This is called by :func:`flask.redirect`, and can be called - directly as well. - - :param location: The URL to redirect to. - :param code: The status code for the redirect. - - .. versionadded:: 2.2 - Moved from ``flask.redirect``, which calls this method. - """ - return _wz_redirect( - location, - code=code, - Response=self.response_class, # type: ignore[arg-type] - ) - - def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: - """Injects the URL defaults for the given endpoint directly into - the values dictionary passed. This is used internally and - automatically called on URL building. - - .. versionadded:: 0.7 - """ - names: t.Iterable[str | None] = (None,) - - # url_for may be called outside a request context, parse the - # passed endpoint instead of using request.blueprints. - if "." in endpoint: - names = chain( - names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) - ) - - for name in names: - if name in self.url_default_functions: - for func in self.url_default_functions[name]: - func(endpoint, values) - - def handle_url_build_error( - self, error: BuildError, endpoint: str, values: dict[str, t.Any] - ) -> str: - """Called by :meth:`.url_for` if a - :exc:`~werkzeug.routing.BuildError` was raised. If this returns - a value, it will be returned by ``url_for``, otherwise the error - will be re-raised. - - Each function in :attr:`url_build_error_handlers` is called with - ``error``, ``endpoint`` and ``values``. If a function returns - ``None`` or raises a ``BuildError``, it is skipped. Otherwise, - its return value is returned by ``url_for``. - - :param error: The active ``BuildError`` being handled. - :param endpoint: The endpoint being built. - :param values: The keyword arguments passed to ``url_for``. - """ - for handler in self.url_build_error_handlers: - try: - rv = handler(error, endpoint, values) - except BuildError as e: - # make error available outside except block - error = e - else: - if rv is not None: - return rv - - # Re-raise if called with an active exception, otherwise raise - # the passed in exception. - if error is sys.exc_info()[1]: - raise - - raise error diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py deleted file mode 100644 index 4f912cca..00000000 --- a/src/flask/sansio/blueprints.py +++ /dev/null @@ -1,632 +0,0 @@ -from __future__ import annotations - -import os -import typing as t -from collections import defaultdict -from functools import update_wrapper - -from .. import typing as ft -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import Scaffold -from .scaffold import setupmethod - -if t.TYPE_CHECKING: # pragma: no cover - from .app import App - -DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] -T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) -T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) -T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_context_processor = t.TypeVar( - "T_template_context_processor", bound=ft.TemplateContextProcessorCallable -) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) -T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) -T_url_value_preprocessor = t.TypeVar( - "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable -) - - -class BlueprintSetupState: - """Temporary holder object for registering a blueprint with the - application. An instance of this class is created by the - :meth:`~flask.Blueprint.make_setup_state` method and later passed - to all register callback functions. - """ - - def __init__( - self, - blueprint: Blueprint, - app: App, - options: t.Any, - first_registration: bool, - ) -> None: - #: a reference to the current application - self.app = app - - #: a reference to the blueprint that created this setup state. - self.blueprint = blueprint - - #: a dictionary with all options that were passed to the - #: :meth:`~flask.Flask.register_blueprint` method. - self.options = options - - #: as blueprints can be registered multiple times with the - #: application and not everything wants to be registered - #: multiple times on it, this attribute can be used to figure - #: out if the blueprint was registered in the past already. - self.first_registration = first_registration - - subdomain = self.options.get("subdomain") - if subdomain is None: - subdomain = self.blueprint.subdomain - - #: The subdomain that the blueprint should be active for, ``None`` - #: otherwise. - self.subdomain = subdomain - - url_prefix = self.options.get("url_prefix") - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - self.name = self.options.get("name", blueprint.name) - self.name_prefix = self.options.get("name_prefix", "") - - #: A dictionary with URL defaults that is added to each and every - #: URL that was defined with the blueprint. - 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, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - **options: t.Any, - ) -> None: - """A helper method to register a rule (and optionally a view function) - to the application. The endpoint is automatically prefixed with the - blueprint's name. - """ - 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")) - - self.app.add_url_rule( - rule, - f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), - view_func, - defaults=defaults, - **options, - ) - - -class Blueprint(Scaffold): - """Represents a blueprint, a collection of routes and other - app-related functions that can be registered on a real application - later. - - A blueprint is an object that allows defining application functions - without requiring an application object ahead of time. It uses the - same decorators as :class:`~flask.Flask`, but defers the need for an - application by recording them for later registration. - - Decorating a function with a blueprint creates a deferred function - that is called with :class:`~flask.blueprints.BlueprintSetupState` - when the blueprint is registered on an application. - - See :doc:`/blueprints` for more information. - - :param name: The name of the blueprint. Will be prepended to each - endpoint name. - :param import_name: The name of the blueprint package, usually - ``__name__``. This helps locate the ``root_path`` for the - blueprint. - :param static_folder: A folder with static files that should be - served by the blueprint's static route. The path is relative to - the blueprint's root path. Blueprint static files are disabled - by default. - :param static_url_path: The url to serve static files from. - Defaults to ``static_folder``. If the blueprint does not have - a ``url_prefix``, the app's static route will take precedence, - and the blueprint's static files won't be accessible. - :param template_folder: A folder with templates that should be added - to the app's template search path. The path is relative to the - blueprint's root path. Blueprint templates are disabled by - default. Blueprint templates have a lower precedence than those - in the app's templates folder. - :param url_prefix: A path to prepend to all of the blueprint's URLs, - to make them distinct from the rest of the app's routes. - :param subdomain: A subdomain that blueprint routes will match on by - default. - :param url_defaults: A dict of default values that blueprint routes - will receive by default. - :param root_path: By default, the blueprint will automatically set - this based on ``import_name``. In certain situations this - automatic detection can fail, so the path can be specified - manually instead. - - .. versionchanged:: 1.1.0 - Blueprints have a ``cli`` group to register nested CLI commands. - The ``cli_group`` parameter controls the name of the group under - the ``flask`` command. - - .. versionadded:: 0.7 - """ - - _got_registered_once = False - - def __init__( - self, - name: str, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - url_prefix: str | None = None, - subdomain: str | None = None, - url_defaults: dict[str, t.Any] | None = None, - root_path: str | None = None, - cli_group: str | None = _sentinel, # type: ignore[assignment] - ): - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if not name: - raise ValueError("'name' may not be empty.") - - if "." in name: - raise ValueError("'name' may not contain a dot '.' character.") - - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.deferred_functions: list[DeferredSetupFunction] = [] - - if url_defaults is None: - url_defaults = {} - - self.url_values_defaults = url_defaults - self.cli_group = cli_group - self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] - - def _check_setup_finished(self, f_name: str) -> None: - if self._got_registered_once: - raise AssertionError( - f"The setup method '{f_name}' can no longer be called on the blueprint" - f" '{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." - ) - - @setupmethod - def record(self, func: DeferredSetupFunction) -> 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. - """ - self.deferred_functions.append(func) - - @setupmethod - def record_once(self, func: DeferredSetupFunction) -> None: - """Works like :meth:`record` but wraps the function in another - function that will ensure the function is only called once. If the - 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) - - self.record(update_wrapper(wrapper, func)) - - def make_setup_state( - self, app: App, options: dict[str, t.Any], first_registration: bool = False - ) -> BlueprintSetupState: - """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` - object that is later passed to the register callback functions. - Subclasses can override this to return a subclass of the setup state. - """ - 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 - on the blueprint. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 2.0 - """ - if blueprint is self: - raise ValueError("Cannot register a blueprint on itself") - self._blueprints.append((blueprint, options)) - - def register(self, app: App, options: dict[str, t.Any]) -> None: - """Called by :meth:`Flask.register_blueprint` to register all - views and callbacks registered on the blueprint with the - application. Creates a :class:`.BlueprintSetupState` and calls - each :meth:`record` callback with it. - - :param app: The application this blueprint is being registered - with. - :param options: Keyword arguments forwarded from - :meth:`~Flask.register_blueprint`. - - .. versionchanged:: 2.3 - Nested blueprints now correctly apply subdomains. - - .. versionchanged:: 2.1 - Registering the same blueprint with the same name multiple - times is an error. - - .. versionchanged:: 2.0.1 - Nested blueprints are registered with their dotted name. - This allows different blueprints with the same name to be - nested at different locations. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - """ - name_prefix = options.get("name_prefix", "") - self_name = options.get("name", self.name) - name = f"{name_prefix}.{self_name}".lstrip(".") - - if name in app.blueprints: - bp_desc = "this" if app.blueprints[name] is self else "a different" - existing_at = f" '{name}'" if self_name != name else "" - - raise ValueError( - f"The name '{self_name}' is already registered for" - f" {bp_desc} blueprint{existing_at}. Use 'name=' to" - f" provide a unique name." - ) - - first_bp_registration = not any(bp is self for bp in app.blueprints.values()) - first_name_registration = name not in app.blueprints - - app.blueprints[name] = self - self._got_registered_once = True - state = self.make_setup_state(app, options, first_bp_registration) - - if self.has_static_folder: - state.add_url_rule( - f"{self.static_url_path}/", - view_func=self.send_static_file, # type: ignore[attr-defined] - endpoint="static", - ) - - # Merge blueprint data into parent. - if first_bp_registration or first_name_registration: - self._merge_blueprint_funcs(app, name) - - for deferred in self.deferred_functions: - deferred(state) - - cli_resolved_group = options.get("cli_group", self.cli_group) - - 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) - - for blueprint, bp_options in self._blueprints: - bp_options = bp_options.copy() - bp_url_prefix = bp_options.get("url_prefix") - bp_subdomain = bp_options.get("subdomain") - - if bp_subdomain is None: - bp_subdomain = blueprint.subdomain - - if state.subdomain is not None and bp_subdomain is not None: - bp_options["subdomain"] = bp_subdomain + "." + state.subdomain - elif bp_subdomain is not None: - bp_options["subdomain"] = bp_subdomain - elif state.subdomain is not None: - bp_options["subdomain"] = state.subdomain - - if bp_url_prefix is None: - bp_url_prefix = blueprint.url_prefix - - if state.url_prefix is not None and bp_url_prefix is not None: - 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) - - def _merge_blueprint_funcs(self, app: App, name: str) -> None: - def extend( - bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], - parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], - ) -> None: - 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( - dict, - { - code: {exc_class: func for exc_class, func in code_values.items()} - for code, code_values in value.items() - }, - ) - app.error_handler_spec[key] = value - - for endpoint, func in self.view_functions.items(): - app.view_functions[endpoint] = func - - extend(self.before_request_funcs, app.before_request_funcs) - extend(self.after_request_funcs, app.after_request_funcs) - extend( - self.teardown_request_funcs, - app.teardown_request_funcs, - ) - extend(self.url_default_functions, app.url_default_functions) - extend(self.url_value_preprocessors, app.url_value_preprocessors) - extend(self.template_context_processors, app.template_context_processors) - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for - full documentation. - - The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, - used with :func:`url_for`, is prefixed with the blueprint's name. - """ - if endpoint and "." in endpoint: - raise ValueError("'endpoint' may not contain a dot '.' character.") - - if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: - raise ValueError("'view_func' name may not contain a dot '.' character.") - - self.record( - lambda s: s.add_url_rule( - rule, - endpoint, - view_func, - provide_automatic_options=provide_automatic_options, - **options, - ) - ) - - @setupmethod - def app_template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: - """Register a template filter, available in any template rendered by the - application. Equivalent to :meth:`.Flask.template_filter`. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_app_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_filter( - self, f: ft.TemplateFilterCallable, name: str | None = None - ) -> None: - """Register a template filter, available in any template rendered by the - application. Works like the :meth:`app_template_filter` decorator. Equivalent to - :meth:`.Flask.add_template_filter`. - - :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: str | None = None - ) -> t.Callable[[T_template_test], T_template_test]: - """Register a template test, available in any template rendered by the - application. Equivalent to :meth:`.Flask.template_test`. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: T_template_test) -> T_template_test: - self.add_app_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_test( - self, f: ft.TemplateTestCallable, name: str | None = None - ) -> None: - """Register a template test, available in any template rendered by the - application. Works like the :meth:`app_template_test` decorator. Equivalent to - :meth:`.Flask.add_template_test`. - - .. versionadded:: 0.10 - - :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: str | None = None - ) -> t.Callable[[T_template_global], T_template_global]: - """Register a template global, available in any template rendered by the - application. Equivalent to :meth:`.Flask.template_global`. - - .. versionadded:: 0.10 - - :param name: the optional name of the global, otherwise the - function name will be used. - """ - - def decorator(f: T_template_global) -> T_template_global: - self.add_app_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_global( - self, f: ft.TemplateGlobalCallable, name: str | None = None - ) -> None: - """Register a template global, available in any template rendered by the - application. Works like the :meth:`app_template_global` decorator. Equivalent to - :meth:`.Flask.add_template_global`. - - .. versionadded:: 0.10 - - :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: T_before_request) -> T_before_request: - """Like :meth:`before_request`, but before every request, not only those handled - by the blueprint. Equivalent to :meth:`.Flask.before_request`. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def after_app_request(self, f: T_after_request) -> T_after_request: - """Like :meth:`after_request`, but after every request, not only those handled - by the blueprint. Equivalent to :meth:`.Flask.after_request`. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def teardown_app_request(self, f: T_teardown) -> T_teardown: - """Like :meth:`teardown_request`, but after every request, not only those - handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_context_processor( - self, f: T_template_context_processor - ) -> T_template_context_processor: - """Like :meth:`context_processor`, but for templates rendered by every view, not - only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_errorhandler( - self, code: type[Exception] | int - ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`errorhandler`, but for every request, not only those handled by - the blueprint. Equivalent to :meth:`.Flask.errorhandler`. - """ - - def decorator(f: T_error_handler) -> T_error_handler: - def from_blueprint(state: BlueprintSetupState) -> None: - state.app.errorhandler(code)(f) - - self.record_once(from_blueprint) - return f - - return decorator - - @setupmethod - def app_url_value_preprocessor( - self, f: T_url_value_preprocessor - ) -> T_url_value_preprocessor: - """Like :meth:`url_value_preprocessor`, but for every request, not only those - handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. - """ - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Like :meth:`url_defaults`, but for every request, not only those handled by - the blueprint. Equivalent to :meth:`.Flask.url_defaults`. - """ - self.record_once( - lambda s: s.app.url_default_functions.setdefault(None, []).append(f) - ) - return f diff --git a/src/flask/sansio/scaffold.py b/src/flask/sansio/scaffold.py deleted file mode 100644 index 69e33a09..00000000 --- a/src/flask/sansio/scaffold.py +++ /dev/null @@ -1,801 +0,0 @@ -from __future__ import annotations - -import importlib.util -import os -import pathlib -import sys -import typing as t -from collections import defaultdict -from functools import update_wrapper - -from jinja2 import BaseLoader -from jinja2 import FileSystemLoader -from werkzeug.exceptions import default_exceptions -from werkzeug.exceptions import HTTPException -from werkzeug.utils import cached_property - -from .. import typing as ft -from ..helpers import get_root_path -from ..templating import _default_template_ctx_processor - -if t.TYPE_CHECKING: # pragma: no cover - from click import Group - -# a singleton sentinel value for parameter defaults -_sentinel = object() - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) -T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) -T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_context_processor = t.TypeVar( - "T_template_context_processor", bound=ft.TemplateContextProcessorCallable -) -T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) -T_url_value_preprocessor = t.TypeVar( - "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable -) -T_route = t.TypeVar("T_route", bound=ft.RouteCallable) - - -def setupmethod(f: F) -> F: - f_name = f.__name__ - - def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: - self._check_setup_finished(f_name) - return f(self, *args, **kwargs) - - return t.cast(F, update_wrapper(wrapper_func, f)) - - -class Scaffold: - """Common behavior shared between :class:`~flask.Flask` and - :class:`~flask.blueprints.Blueprint`. - - :param import_name: The import name of the module where this object - is defined. Usually :attr:`__name__` should be used. - :param static_folder: Path to a folder of static files to serve. - If this is set, a static route will be added. - :param static_url_path: URL prefix for the static route. - :param template_folder: Path to a folder containing template files. - for rendering. If this is set, a Jinja loader will be added. - :param root_path: The path that static, template, and resource files - are relative to. Typically not set, it is discovered based on - the ``import_name``. - - .. versionadded:: 2.0 - """ - - cli: Group - name: str - _static_folder: str | None = None - _static_url_path: str | None = None - - def __init__( - self, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - root_path: str | None = None, - ): - #: The name of the package or module that this object belongs - #: to. Do not change this once it is set by the constructor. - self.import_name = import_name - - self.static_folder = static_folder # type: ignore - self.static_url_path = static_url_path - - #: The path to the templates folder, relative to - #: :attr:`root_path`, to add to the template loader. ``None`` if - #: templates should not be added. - self.template_folder = template_folder - - if root_path is None: - root_path = get_root_path(self.import_name) - - #: Absolute path to the package on the filesystem. Used to look - #: up resources contained in the package. - self.root_path = root_path - - #: A dictionary mapping endpoint names to view functions. - #: - #: To register a view function, use the :meth:`route` decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.view_functions: dict[str, ft.RouteCallable] = {} - - #: A data structure of registered error handlers, in the format - #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is - #: the name of a blueprint the handlers are active for, or - #: ``None`` for all requests. The ``code`` key is the HTTP - #: status code for ``HTTPException``, or ``None`` for - #: other exceptions. The innermost dictionary maps exception - #: classes to handler functions. - #: - #: To register an error handler, use the :meth:`errorhandler` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.error_handler_spec: dict[ - ft.AppOrBlueprintKey, - dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], - ] = defaultdict(lambda: defaultdict(dict)) - - #: A data structure of functions to call at the beginning of - #: each request, in the format ``{scope: [functions]}``. The - #: ``scope`` key is the name of a blueprint the functions are - #: active for, or ``None`` for all requests. - #: - #: To register a function, use the :meth:`before_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.before_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] - ] = defaultdict(list) - - #: A data structure of functions to call at the end of each - #: request, in the format ``{scope: [functions]}``. The - #: ``scope`` key is the name of a blueprint the functions are - #: active for, or ``None`` for all requests. - #: - #: To register a function, use the :meth:`after_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.after_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] - ] = defaultdict(list) - - #: A data structure of functions to call at the end of each - #: request even if an exception is raised, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`teardown_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.teardown_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.TeardownCallable] - ] = defaultdict(list) - - #: A data structure of functions to call to pass extra context - #: values when rendering templates, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`context_processor` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.template_context_processors: dict[ - ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] - ] = defaultdict(list, {None: [_default_template_ctx_processor]}) - - #: A data structure of functions to call to modify the keyword - #: arguments passed to the view function, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the - #: :meth:`url_value_preprocessor` decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.url_value_preprocessors: dict[ - ft.AppOrBlueprintKey, - list[ft.URLValuePreprocessorCallable], - ] = defaultdict(list) - - #: A data structure of functions to call to modify the keyword - #: arguments when generating URLs, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`url_defaults` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.url_default_functions: dict[ - ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] - ] = defaultdict(list) - - def __repr__(self) -> str: - return f"<{type(self).__name__} {self.name!r}>" - - def _check_setup_finished(self, f_name: str) -> None: - raise NotImplementedError - - @property - def static_folder(self) -> str | None: - """The absolute path to the configured static folder. ``None`` - if no static folder is set. - """ - if self._static_folder is not None: - return os.path.join(self.root_path, self._static_folder) - else: - return None - - @static_folder.setter - def static_folder(self, value: str | os.PathLike[str] | None) -> None: - if value is not None: - value = os.fspath(value).rstrip(r"\/") - - self._static_folder = value - - @property - def has_static_folder(self) -> bool: - """``True`` if :attr:`static_folder` is set. - - .. versionadded:: 0.5 - """ - return self.static_folder is not None - - @property - def static_url_path(self) -> str | None: - """The URL prefix that the static route will be accessible from. - - If it was not configured during init, it is derived from - :attr:`static_folder`. - """ - if self._static_url_path is not None: - return self._static_url_path - - if self.static_folder is not None: - basename = os.path.basename(self.static_folder) - return f"/{basename}".rstrip("/") - - return None - - @static_url_path.setter - def static_url_path(self, value: str | None) -> None: - if value is not None: - value = value.rstrip("/") - - self._static_url_path = value - - @cached_property - def jinja_loader(self) -> BaseLoader | None: - """The Jinja loader for this object's templates. By default this - is a class :class:`jinja2.loaders.FileSystemLoader` to - :attr:`template_folder` if it is set. - - .. versionadded:: 0.5 - """ - if self.template_folder is not None: - return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) - else: - return None - - def _method_route( - self, - method: str, - rule: str, - options: dict[str, t.Any], - ) -> t.Callable[[T_route], T_route]: - if "methods" in options: - raise TypeError("Use the 'route' decorator to use the 'methods' argument.") - - return self.route(rule, methods=[method], **options) - - @setupmethod - def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["GET"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("GET", rule, options) - - @setupmethod - def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["POST"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("POST", rule, options) - - @setupmethod - def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["PUT"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("PUT", rule, options) - - @setupmethod - def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["DELETE"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("DELETE", rule, options) - - @setupmethod - def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["PATCH"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("PATCH", rule, options) - - @setupmethod - def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Decorate a view function to register it with the given URL - rule and options. Calls :meth:`add_url_rule`, which has more - details about the implementation. - - .. code-block:: python - - @app.route("/") - def index(): - return "Hello, World!" - - See :ref:`url-route-registrations`. - - The endpoint name for the route defaults to the name of the view - function if the ``endpoint`` parameter isn't passed. - - The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and - ``OPTIONS`` are added automatically. - - :param rule: The URL rule string. - :param options: Extra options passed to the - :class:`~werkzeug.routing.Rule` object. - """ - - def decorator(f: T_route) -> T_route: - endpoint = options.pop("endpoint", None) - self.add_url_rule(rule, endpoint, f, **options) - return f - - return decorator - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - """Register a rule for routing incoming requests and building - URLs. The :meth:`route` decorator is a shortcut to call this - with the ``view_func`` argument. These are equivalent: - - .. code-block:: python - - @app.route("/") - def index(): - ... - - .. code-block:: python - - def index(): - ... - - app.add_url_rule("/", view_func=index) - - See :ref:`url-route-registrations`. - - The endpoint name for the route defaults to the name of the view - function if the ``endpoint`` parameter isn't passed. An error - will be raised if a function has already been registered for the - endpoint. - - The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is - always added automatically, and ``OPTIONS`` is added - automatically by default. - - ``view_func`` does not necessarily need to be passed, but if the - rule should participate in routing an endpoint name must be - associated with a view function at some point with the - :meth:`endpoint` decorator. - - .. code-block:: python - - app.add_url_rule("/", endpoint="index") - - @app.endpoint("index") - def index(): - ... - - If ``view_func`` has a ``required_methods`` attribute, those - methods are added to the passed and automatic methods. If it - has a ``provide_automatic_methods`` attribute, it is used as the - default if the parameter is not passed. - - :param rule: The URL rule string. - :param endpoint: The endpoint name to associate with the rule - and view function. Used when routing and building URLs. - Defaults to ``view_func.__name__``. - :param view_func: The view function to associate with the - endpoint name. - :param provide_automatic_options: Add the ``OPTIONS`` method and - respond to ``OPTIONS`` requests automatically. - :param options: Extra options passed to the - :class:`~werkzeug.routing.Rule` object. - """ - raise NotImplementedError - - @setupmethod - def endpoint(self, endpoint: str) -> t.Callable[[F], F]: - """Decorate a view function to register it for the given - endpoint. Used if a rule is added without a ``view_func`` with - :meth:`add_url_rule`. - - .. code-block:: python - - app.add_url_rule("/ex", endpoint="example") - - @app.endpoint("example") - def example(): - ... - - :param endpoint: The endpoint name to associate with the view - function. - """ - - def decorator(f: F) -> F: - self.view_functions[endpoint] = f - return f - - return decorator - - @setupmethod - def before_request(self, f: T_before_request) -> T_before_request: - """Register 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. - - .. code-block:: python - - @app.before_request - def load_user(): - if "user_id" in session: - g.user = db.session.get(session["user_id"]) - - 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. - - This is available on both app and blueprint objects. When used on an app, this - executes before every request. When used on a blueprint, this executes before - every request that the blueprint handles. To register with a blueprint and - execute before every request, use :meth:`.Blueprint.before_app_request`. - """ - self.before_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def after_request(self, f: T_after_request) -> T_after_request: - """Register a function to run after each request to this object. - - The function is called with the response object, and must return - a response object. This allows the functions to modify or - replace the response before it is sent. - - If a function raises an exception, any remaining - ``after_request`` functions will not be called. Therefore, this - should not be used for actions that must execute, such as to - close resources. Use :meth:`teardown_request` for that. - - This is available on both app and blueprint objects. When used on an app, this - executes after every request. When used on a blueprint, this executes after - every request that the blueprint handles. To register with a blueprint and - execute after every request, use :meth:`.Blueprint.after_app_request`. - """ - self.after_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def teardown_request(self, f: T_teardown) -> T_teardown: - """Register a function to be called when the request context is - popped. Typically this happens at the end of each request, but - contexts may be pushed manually as well during testing. - - .. code-block:: python - - with app.test_request_context(): - ... - - When the ``with`` block exits (or ``ctx.pop()`` is called), the - teardown functions are called just before the request context is - made inactive. - - When a teardown function was called because of an unhandled - exception it will be passed an error object. If an - :meth:`errorhandler` is registered, it will handle the exception - and the teardown will not receive it. - - Teardown functions must avoid raising exceptions. If they - execute code that might fail they must surround that code with a - ``try``/``except`` block and log any errors. - - The return values of teardown functions are ignored. - - This is available on both app and blueprint objects. When used on an app, this - executes after every request. When used on a blueprint, this executes after - every request that the blueprint handles. To register with a blueprint and - execute after every request, use :meth:`.Blueprint.teardown_app_request`. - """ - self.teardown_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def context_processor( - self, - f: T_template_context_processor, - ) -> T_template_context_processor: - """Registers a template context processor function. These functions run before - rendering a template. The keys of the returned dict are added as variables - available in the template. - - This is available on both app and blueprint objects. When used on an app, this - is called for every rendered template. When used on a blueprint, this is called - for templates rendered from the blueprint's views. To register with a blueprint - and affect every template, use :meth:`.Blueprint.app_context_processor`. - """ - self.template_context_processors[None].append(f) - return f - - @setupmethod - def url_value_preprocessor( - self, - f: T_url_value_preprocessor, - ) -> T_url_value_preprocessor: - """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. - - This is available on both app and blueprint objects. When used on an app, this - is called for every request. When used on a blueprint, this is called for - requests that the blueprint handles. To register with a blueprint and affect - every request, use :meth:`.Blueprint.app_url_value_preprocessor`. - """ - self.url_value_preprocessors[None].append(f) - return f - - @setupmethod - def url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """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. - - This is available on both app and blueprint objects. When used on an app, this - is called for every request. When used on a blueprint, this is called for - requests that the blueprint handles. To register with a blueprint and affect - every request, use :meth:`.Blueprint.app_url_defaults`. - """ - self.url_default_functions[None].append(f) - return f - - @setupmethod - def errorhandler( - self, code_or_exception: type[Exception] | int - ) -> t.Callable[[T_error_handler], T_error_handler]: - """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 - - This is available on both app and blueprint objects. When used on an app, this - can handle errors from every request. When used on a blueprint, this can handle - errors from requests that the blueprint handles. To register with a blueprint - and affect every request, use :meth:`.Blueprint.app_errorhandler`. - - .. 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: T_error_handler) -> T_error_handler: - self.register_error_handler(code_or_exception, f) - return f - - return decorator - - @setupmethod - def register_error_handler( - self, - code_or_exception: type[Exception] | int, - f: ft.ErrorHandlerCallable, - ) -> None: - """Alternative error attach function to the :meth:`errorhandler` - decorator that is more straightforward to use for non decorator - usage. - - .. versionadded:: 0.7 - """ - exc_class, code = self._get_exc_class_and_code(code_or_exception) - self.error_handler_spec[None][code][exc_class] = f - - @staticmethod - def _get_exc_class_and_code( - exc_class_or_code: type[Exception] | int, - ) -> tuple[type[Exception], int | None]: - """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. - """ - exc_class: type[Exception] - - if isinstance(exc_class_or_code, int): - try: - exc_class = default_exceptions[exc_class_or_code] - except KeyError: - raise ValueError( - f"'{exc_class_or_code}' is not a recognized HTTP" - " error code. Use a subclass of HTTPException with" - " that code instead." - ) from None - else: - exc_class = exc_class_or_code - - if isinstance(exc_class, Exception): - raise TypeError( - f"{exc_class!r} is an instance, not a class. Handlers" - " can only be registered for Exception classes or HTTP" - " error codes." - ) - - if not issubclass(exc_class, Exception): - raise ValueError( - f"'{exc_class.__name__}' is not a subclass of Exception." - " Handlers can only be registered for Exception classes" - " or HTTP error codes." - ) - - if issubclass(exc_class, HTTPException): - return exc_class, exc_class.code - else: - return exc_class, None - - -def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: - """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 _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: - # Path.is_relative_to doesn't exist until Python 3.9 - try: - path.relative_to(base) - return True - except ValueError: - return False - - -def _find_package_path(import_name: str) -> str: - """Find the path that contains the package or module.""" - root_mod_name, _, _ = import_name.partition(".") - - try: - root_spec = importlib.util.find_spec(root_mod_name) - - if root_spec is None: - raise ValueError("not found") - except (ImportError, ValueError): - # ImportError: the machinery told us it does not exist - # ValueError: - # - the module name was invalid - # - the module name is __main__ - # - we raised `ValueError` due to `root_spec` being `None` - return os.getcwd() - - if root_spec.submodule_search_locations: - if root_spec.origin is None or root_spec.origin == "namespace": - # namespace package - package_spec = importlib.util.find_spec(import_name) - - if package_spec is not None and package_spec.submodule_search_locations: - # Pick the path in the namespace that contains the submodule. - package_path = pathlib.Path( - os.path.commonpath(package_spec.submodule_search_locations) - ) - search_location = next( - location - for location in root_spec.submodule_search_locations - if _path_is_relative_to(package_path, location) - ) - else: - # Pick the first path. - search_location = root_spec.submodule_search_locations[0] - - return os.path.dirname(search_location) - else: - # package with __init__.py - return os.path.dirname(os.path.dirname(root_spec.origin)) - else: - # module - return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] - - -def find_package(import_name: str) -> tuple[str | None, str]: - """Find the prefix that a package is installed under, and the path - that it would be imported from. - - The prefix is the directory containing the standard directory - hierarchy (lib, bin, etc.). If the package is not installed to the - system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), - ``None`` is returned. - - The path is the entry in :attr:`sys.path` that contains the package - for import. If the package is not installed, it's assumed that the - package was imported from the current working directory. - """ - package_path = _find_package_path(import_name) - py_prefix = os.path.abspath(sys.prefix) - - # installed to the system - if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix): - return py_prefix, package_path - - site_parent, site_folder = os.path.split(package_path) - - # installed to a virtualenv - if site_folder.lower() == "site-packages": - parent, folder = os.path.split(site_parent) - - # Windows (prefix/lib/site-packages) - if folder.lower() == "lib": - return parent, package_path - - # Unix (prefix/lib/pythonX.Y/site-packages) - if os.path.basename(parent).lower() == "lib": - return os.path.dirname(parent), package_path - - # something else (prefix/site-packages) - return site_parent, package_path - - # not installed - return None, package_path diff --git a/src/flask/sessions.py b/src/flask/sessions.py deleted file mode 100644 index 05b367a2..00000000 --- a/src/flask/sessions.py +++ /dev/null @@ -1,379 +0,0 @@ -from __future__ import annotations - -import hashlib -import typing as t -from collections.abc import MutableMapping -from datetime import datetime -from datetime import timezone - -from itsdangerous import BadSignature -from itsdangerous import URLSafeTimedSerializer -from werkzeug.datastructures import CallbackDict - -from .json.tag import TaggedJSONSerializer - -if t.TYPE_CHECKING: # pragma: no cover - import typing_extensions as te - - from .app import Flask - from .wrappers import Request - from .wrappers import Response - - -# TODO generic when Python > 3.8 -class SessionMixin(MutableMapping): # type: ignore[type-arg] - """Expands a basic dictionary with session attributes.""" - - @property - def permanent(self) -> bool: - """This reflects the ``'_permanent'`` key in the dict.""" - return self.get("_permanent", False) - - @permanent.setter - def permanent(self, value: bool) -> None: - self["_permanent"] = bool(value) - - #: Some implementations can detect whether a session is newly - #: created, but that is not guaranteed. Use with caution. The mixin - # default is hard-coded ``False``. - new = False - - #: Some implementations can detect changes to the session and set - #: this when that happens. The mixin default is hard coded to - #: ``True``. - modified = True - - #: Some implementations can detect when session data is read or - #: written and set this when that happens. The mixin default is hard - #: coded to ``True``. - accessed = True - - -# TODO generic when Python > 3.8 -class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg] - """Base class for sessions based on signed cookies. - - This session backend will set the :attr:`modified` and - :attr:`accessed` attributes. It cannot reliably track whether a - session is new (vs. empty), so :attr:`new` remains hard coded to - ``False``. - """ - - #: When data is changed, this is set to ``True``. Only the session - #: dictionary itself is tracked; if the session contains mutable - #: data (for example a nested dict) then this must be set to - #: ``True`` manually when modifying that data. The session cookie - #: will only be written to the response if this is ``True``. - modified = False - - #: When data is read or written, this is set to ``True``. Used by - # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` - #: header, which allows caching proxies to cache different pages for - #: different users. - accessed = False - - def __init__(self, initial: t.Any = None) -> None: - def on_update(self: te.Self) -> None: - self.modified = True - self.accessed = True - - super().__init__(initial, on_update) - - def __getitem__(self, key: str) -> t.Any: - self.accessed = True - return super().__getitem__(key) - - def get(self, key: str, default: t.Any = None) -> t.Any: - self.accessed = True - return super().get(key, default) - - def setdefault(self, key: str, default: t.Any = None) -> t.Any: - self.accessed = True - return super().setdefault(key, default) - - -class NullSession(SecureCookieSession): - """Class used to generate nicer error messages if sessions are not - available. Will still allow read-only access to the empty session - but fail on setting. - """ - - def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: - raise RuntimeError( - "The session is unavailable because no secret " - "key was set. Set the secret_key on the " - "application to something unique and secret." - ) - - __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 - del _fail - - -class SessionInterface: - """The basic interface you have to implement in order to replace the - default session interface which uses werkzeug's securecookie - implementation. The only methods you have to implement are - :meth:`open_session` and :meth:`save_session`, the others have - useful defaults which you don't need to change. - - The session object returned by the :meth:`open_session` method has to - provide a dictionary like interface plus the properties and methods - from the :class:`SessionMixin`. We recommend just subclassing a dict - and adding that mixin:: - - class Session(dict, SessionMixin): - pass - - If :meth:`open_session` returns ``None`` Flask will call into - :meth:`make_null_session` to create a session that acts as replacement - if the session support cannot work because some requirement is not - fulfilled. The default :class:`NullSession` class that is created - will complain that the secret key was not set. - - To replace the session interface on an application all you have to do - is to assign :attr:`flask.Flask.session_interface`:: - - app = Flask(__name__) - app.session_interface = MySessionInterface() - - Multiple requests with the same session may be sent and handled - concurrently. When implementing a new session interface, consider - whether reads or writes to the backing store must be synchronized. - There is no guarantee on the order in which the session for each - request is opened or saved, it will occur in the order that requests - begin and end processing. - - .. versionadded:: 0.8 - """ - - #: :meth:`make_null_session` will look here for the class that should - #: be created when a null session is requested. Likewise the - #: :meth:`is_null_session` method will perform a typecheck against - #: this type. - null_session_class = NullSession - - #: A flag that indicates if the session interface is pickle based. - #: This can be used by Flask extensions to make a decision in regards - #: to how to deal with the session object. - #: - #: .. versionadded:: 0.10 - pickle_based = False - - def make_null_session(self, app: Flask) -> NullSession: - """Creates a null session which acts as a replacement object if the - real session support could not be loaded due to a configuration - error. This mainly aids the user experience because the job of the - null session is to still support lookup without complaining but - modifications are answered with a helpful error message of what - failed. - - This creates an instance of :attr:`null_session_class` by default. - """ - return self.null_session_class() - - def is_null_session(self, obj: object) -> bool: - """Checks if a given object is a null session. Null sessions are - not asked to be saved. - - This checks if the object is an instance of :attr:`null_session_class` - by default. - """ - return isinstance(obj, self.null_session_class) - - def get_cookie_name(self, app: Flask) -> str: - """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" - return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] - - def get_cookie_domain(self, app: Flask) -> str | None: - """The value of the ``Domain`` parameter on the session cookie. If not set, - browsers will only send the cookie to the exact domain it was set from. - Otherwise, they will send it to any subdomain of the given value as well. - - Uses the :data:`SESSION_COOKIE_DOMAIN` config. - - .. versionchanged:: 2.3 - Not set by default, does not fall back to ``SERVER_NAME``. - """ - return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] - - def get_cookie_path(self, app: Flask) -> str: - """Returns the path for which the cookie should be valid. The - default implementation uses the value from the ``SESSION_COOKIE_PATH`` - config var if it's set, and falls back to ``APPLICATION_ROOT`` or - uses ``/`` if it's ``None``. - """ - return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] - - def get_cookie_httponly(self, app: Flask) -> bool: - """Returns True if the session cookie should be httponly. This - currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` - config var. - """ - return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] - - def get_cookie_secure(self, app: Flask) -> bool: - """Returns True if the cookie should be secure. This currently - just returns the value of the ``SESSION_COOKIE_SECURE`` setting. - """ - return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] - - def get_cookie_samesite(self, app: Flask) -> str | None: - """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the - ``SameSite`` attribute. This currently just returns the value of - the :data:`SESSION_COOKIE_SAMESITE` setting. - """ - return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] - - def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: - """A helper method that returns an expiration date for the session - or ``None`` if the session is linked to the browser session. The - default implementation returns now + the permanent session - lifetime configured on the application. - """ - if session.permanent: - return datetime.now(timezone.utc) + app.permanent_session_lifetime - return None - - def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: - """Used by session backends to determine if a ``Set-Cookie`` header - should be set for this session cookie for this response. If the session - has been modified, the cookie is set. If the session is permanent and - the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is - always set. - - This check is usually skipped if the session was deleted. - - .. versionadded:: 0.11 - """ - - return session.modified or ( - session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] - ) - - def open_session(self, app: Flask, request: Request) -> SessionMixin | None: - """This is called at the beginning of each request, after - pushing the request context, before matching the URL. - - This must return an object which implements a dictionary-like - interface as well as the :class:`SessionMixin` interface. - - This will return ``None`` to indicate that loading failed in - some way that is not immediately an error. The request - context will fall back to using :meth:`make_null_session` - in this case. - """ - raise NotImplementedError() - - def save_session( - self, app: Flask, session: SessionMixin, response: Response - ) -> None: - """This is called at the end of each request, after generating - a response, before removing the request context. It is skipped - if :meth:`is_null_session` returns ``True``. - """ - raise NotImplementedError() - - -session_json_serializer = TaggedJSONSerializer() - - -def _lazy_sha1(string: bytes = b"") -> t.Any: - """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include - SHA-1, in which case the import and use as a default would fail before the - developer can configure something else. - """ - return hashlib.sha1(string) - - -class SecureCookieSessionInterface(SessionInterface): - """The default session interface that stores sessions in signed cookies - through the :mod:`itsdangerous` module. - """ - - #: the salt that should be applied on top of the secret key for the - #: signing of cookie based sessions. - salt = "cookie-session" - #: the hash function to use for the signature. The default is sha1 - digest_method = staticmethod(_lazy_sha1) - #: the name of the itsdangerous supported key derivation. The default - #: is hmac. - key_derivation = "hmac" - #: A python serializer for the payload. The default is a compact - #: JSON derived serializer with support for some extra Python types - #: such as datetime objects or tuples. - serializer = session_json_serializer - session_class = SecureCookieSession - - def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: - if not app.secret_key: - return None - signer_kwargs = dict( - key_derivation=self.key_derivation, digest_method=self.digest_method - ) - return URLSafeTimedSerializer( - app.secret_key, - salt=self.salt, - serializer=self.serializer, - signer_kwargs=signer_kwargs, - ) - - def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: - s = self.get_signing_serializer(app) - if s is None: - return None - val = request.cookies.get(self.get_cookie_name(app)) - if not val: - return self.session_class() - max_age = int(app.permanent_session_lifetime.total_seconds()) - try: - data = s.loads(val, max_age=max_age) - return self.session_class(data) - except BadSignature: - return self.session_class() - - def save_session( - self, app: Flask, session: SessionMixin, response: Response - ) -> None: - name = self.get_cookie_name(app) - domain = self.get_cookie_domain(app) - path = self.get_cookie_path(app) - secure = self.get_cookie_secure(app) - samesite = self.get_cookie_samesite(app) - httponly = self.get_cookie_httponly(app) - - # Add a "Vary: Cookie" header if the session was accessed at all. - if session.accessed: - response.vary.add("Cookie") - - # If the session is modified to be empty, remove the cookie. - # If the session is empty, return without setting the cookie. - if not session: - if session.modified: - response.delete_cookie( - name, - domain=domain, - path=path, - secure=secure, - samesite=samesite, - httponly=httponly, - ) - response.vary.add("Cookie") - - return - - if not self.should_set_cookie(app, session): - return - - expires = self.get_expiration_time(app, session) - val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] - response.set_cookie( - name, - val, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - samesite=samesite, - ) - response.vary.add("Cookie") diff --git a/src/flask/signals.py b/src/flask/signals.py deleted file mode 100644 index 444fda99..00000000 --- a/src/flask/signals.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from blinker import Namespace - -# This namespace is only for signals provided by Flask itself. -_signals = Namespace() - -template_rendered = _signals.signal("template-rendered") -before_render_template = _signals.signal("before-render-template") -request_started = _signals.signal("request-started") -request_finished = _signals.signal("request-finished") -request_tearing_down = _signals.signal("request-tearing-down") -got_request_exception = _signals.signal("got-request-exception") -appcontext_tearing_down = _signals.signal("appcontext-tearing-down") -appcontext_pushed = _signals.signal("appcontext-pushed") -appcontext_popped = _signals.signal("appcontext-popped") -message_flashed = _signals.signal("message-flashed") diff --git a/src/flask/templating.py b/src/flask/templating.py deleted file mode 100644 index 618a3b35..00000000 --- a/src/flask/templating.py +++ /dev/null @@ -1,219 +0,0 @@ -from __future__ import annotations - -import typing as t - -from jinja2 import BaseLoader -from jinja2 import Environment as BaseEnvironment -from jinja2 import Template -from jinja2 import TemplateNotFound - -from .globals import _cv_app -from .globals import _cv_request -from .globals import current_app -from .globals import request -from .helpers import stream_with_context -from .signals import before_render_template -from .signals import template_rendered - -if t.TYPE_CHECKING: # pragma: no cover - from .app import Flask - from .sansio.app import App - from .sansio.scaffold import Scaffold - - -def _default_template_ctx_processor() -> dict[str, t.Any]: - """Default template context processor. Injects `request`, - `session` and `g`. - """ - appctx = _cv_app.get(None) - reqctx = _cv_request.get(None) - rv: dict[str, t.Any] = {} - if appctx is not None: - rv["g"] = appctx.g - if reqctx is not None: - rv["request"] = reqctx.request - rv["session"] = reqctx.session - return rv - - -class Environment(BaseEnvironment): - """Works like a regular Jinja2 environment but has some additional - knowledge of how Flask's blueprint works so that it can prepend the - name of the blueprint to referenced templates if necessary. - """ - - def __init__(self, app: App, **options: t.Any) -> None: - if "loader" not in options: - options["loader"] = app.create_global_jinja_loader() - BaseEnvironment.__init__(self, **options) - self.app = app - - -class DispatchingJinjaLoader(BaseLoader): - """A loader that looks for templates in the application and all - the blueprint folders. - """ - - def __init__(self, app: App) -> None: - self.app = app - - def get_source( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: - return self._get_source_explained(environment, template) - return self._get_source_fast(environment, template) - - def _get_source_explained( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - attempts = [] - rv: tuple[str, str | None, t.Callable[[], bool] | None] | None - trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None - - for srcobj, loader in self._iter_loaders(template): - try: - rv = loader.get_source(environment, template) - if trv is None: - trv = rv - except TemplateNotFound: - rv = None - attempts.append((loader, srcobj, rv)) - - from .debughelpers import explain_template_loading_attempts - - explain_template_loading_attempts(self.app, template, attempts) - - if trv is not None: - return trv - raise TemplateNotFound(template) - - def _get_source_fast( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - for _srcobj, loader in self._iter_loaders(template): - try: - return loader.get_source(environment, template) - except TemplateNotFound: - continue - raise TemplateNotFound(template) - - def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: - loader = self.app.jinja_loader - if loader is not None: - yield self.app, loader - - for blueprint in self.app.iter_blueprints(): - loader = blueprint.jinja_loader - if loader is not None: - yield blueprint, loader - - def list_templates(self) -> list[str]: - result = set() - loader = self.app.jinja_loader - if loader is not None: - result.update(loader.list_templates()) - - for blueprint in self.app.iter_blueprints(): - loader = blueprint.jinja_loader - if loader is not None: - for template in loader.list_templates(): - result.add(template) - - return list(result) - - -def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: - app.update_template_context(context) - before_render_template.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - rv = template.render(context) - template_rendered.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - return rv - - -def render_template( - template_name_or_list: str | Template | list[str | Template], - **context: t.Any, -) -> str: - """Render a template by name with the given context. - - :param template_name_or_list: The name of the template to render. If - a list is given, the first name to exist will be rendered. - :param context: The variables to make available in the template. - """ - app = current_app._get_current_object() # type: ignore[attr-defined] - template = app.jinja_env.get_or_select_template(template_name_or_list) - return _render(app, template, context) - - -def render_template_string(source: str, **context: t.Any) -> str: - """Render a template from the given source string with the given - context. - - :param source: The source code of the template to render. - :param context: The variables to make available in the template. - """ - app = current_app._get_current_object() # type: ignore[attr-defined] - template = app.jinja_env.from_string(source) - return _render(app, template, context) - - -def _stream( - app: Flask, template: Template, context: dict[str, t.Any] -) -> t.Iterator[str]: - app.update_template_context(context) - before_render_template.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - - def generate() -> t.Iterator[str]: - yield from template.generate(context) - template_rendered.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - - rv = generate() - - # If a request context is active, keep it while generating. - if request: - rv = stream_with_context(rv) - - return rv - - -def stream_template( - template_name_or_list: str | Template | list[str | Template], - **context: t.Any, -) -> t.Iterator[str]: - """Render a template by name with the given context as a stream. - This returns an iterator of strings, which can be used as a - streaming response from a view. - - :param template_name_or_list: The name of the template to render. If - a list is given, the first name to exist will be rendered. - :param context: The variables to make available in the template. - - .. versionadded:: 2.2 - """ - app = current_app._get_current_object() # type: ignore[attr-defined] - template = app.jinja_env.get_or_select_template(template_name_or_list) - return _stream(app, template, context) - - -def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: - """Render a template from the given source string with the given - context as a stream. This returns an iterator of strings, which can - be used as a streaming response from a view. - - :param source: The source code of the template to render. - :param context: The variables to make available in the template. - - .. versionadded:: 2.2 - """ - app = current_app._get_current_object() # type: ignore[attr-defined] - template = app.jinja_env.from_string(source) - return _stream(app, template, context) diff --git a/src/flask/testing.py b/src/flask/testing.py deleted file mode 100644 index a27b7c8f..00000000 --- a/src/flask/testing.py +++ /dev/null @@ -1,298 +0,0 @@ -from __future__ import annotations - -import importlib.metadata -import typing as t -from contextlib import contextmanager -from contextlib import ExitStack -from copy import copy -from types import TracebackType -from urllib.parse import urlsplit - -import werkzeug.test -from click.testing import CliRunner -from werkzeug.test import Client -from werkzeug.wrappers import Request as BaseRequest - -from .cli import ScriptInfo -from .sessions import SessionMixin - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import WSGIEnvironment - from werkzeug.test import TestResponse - - from .app import Flask - - -class EnvironBuilder(werkzeug.test.EnvironBuilder): - """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the - application. - - :param app: The Flask application to configure the environment from. - :param path: URL path being requested. - :param base_url: Base URL where the app is being served, which - ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, - :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. - :param url_scheme: Scheme to use instead of - :data:`PREFERRED_URL_SCHEME`. - :param json: If given, this is serialized as JSON and passed as - ``data``. Also defaults ``content_type`` to - ``application/json``. - :param args: other positional arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: other keyword arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - """ - - def __init__( - self, - app: Flask, - path: str = "/", - base_url: str | None = None, - subdomain: str | None = None, - url_scheme: str | None = None, - *args: t.Any, - **kwargs: t.Any, - ) -> None: - assert not (base_url or subdomain or url_scheme) or ( - base_url is not None - ) != bool( - subdomain or url_scheme - ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' - - if base_url is None: - http_host = app.config.get("SERVER_NAME") or "localhost" - app_root = app.config["APPLICATION_ROOT"] - - if subdomain: - http_host = f"{subdomain}.{http_host}" - - if url_scheme is None: - url_scheme = app.config["PREFERRED_URL_SCHEME"] - - url = urlsplit(path) - base_url = ( - f"{url.scheme or url_scheme}://{url.netloc or http_host}" - f"/{app_root.lstrip('/')}" - ) - path = url.path - - if url.query: - sep = b"?" if isinstance(url.query, bytes) else "?" - path += sep + url.query - - self.app = app - super().__init__(path, base_url, *args, **kwargs) - - def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore - """Serialize ``obj`` to a JSON-formatted string. - - The serialization will be configured according to the config associated - with this EnvironBuilder's ``app``. - """ - return self.app.json.dumps(obj, **kwargs) - - -_werkzeug_version = "" - - -def _get_werkzeug_version() -> str: - global _werkzeug_version - - if not _werkzeug_version: - _werkzeug_version = importlib.metadata.version("werkzeug") - - return _werkzeug_version - - -class FlaskClient(Client): - """Works like a regular Werkzeug test client but has knowledge about - Flask's contexts to defer the cleanup of the request context until - the end of a ``with`` block. For general information about how to - use this class refer to :class:`werkzeug.test.Client`. - - .. versionchanged:: 0.12 - `app.test_client()` includes preset default environment, which can be - set after instantiation of the `app.test_client()` object in - `client.environ_base`. - - Basic usage is outlined in the :doc:`/testing` chapter. - """ - - application: Flask - - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: - super().__init__(*args, **kwargs) - self.preserve_context = False - self._new_contexts: list[t.ContextManager[t.Any]] = [] - self._context_stack = ExitStack() - self.environ_base = { - "REMOTE_ADDR": "127.0.0.1", - "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", - } - - @contextmanager - def session_transaction( - self, *args: t.Any, **kwargs: t.Any - ) -> t.Iterator[SessionMixin]: - """When used in combination with a ``with`` statement this opens a - session transaction. This can be used to modify the session that - the test client uses. Once the ``with`` block is left the session is - stored back. - - :: - - with client.session_transaction() as session: - session['value'] = 42 - - Internally this is implemented by going through a temporary test - request context and since session handling could depend on - request variables this function accepts the same arguments as - :meth:`~flask.Flask.test_request_context` which are directly - passed through. - """ - if self._cookies is None: - raise TypeError( - "Cookies are disabled. Create a client with 'use_cookies=True'." - ) - - app = self.application - ctx = app.test_request_context(*args, **kwargs) - self._add_cookies_to_wsgi(ctx.request.environ) - - with ctx: - sess = app.session_interface.open_session(app, ctx.request) - - if sess is None: - raise RuntimeError("Session backend did not open a session.") - - yield sess - resp = app.response_class() - - if app.session_interface.is_null_session(sess): - return - - with ctx: - app.session_interface.save_session(app, sess, resp) - - self._update_cookies_from_response( - ctx.request.host.partition(":")[0], - ctx.request.path, - resp.headers.getlist("Set-Cookie"), - ) - - def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: - out = {**self.environ_base, **other} - - if self.preserve_context: - out["werkzeug.debug.preserve_context"] = self._new_contexts.append - - return out - - def _request_from_builder_args( - self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] - ) -> BaseRequest: - kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) - builder = EnvironBuilder(self.application, *args, **kwargs) - - try: - return builder.get_request() - finally: - builder.close() - - def open( - self, - *args: t.Any, - buffered: bool = False, - follow_redirects: bool = False, - **kwargs: t.Any, - ) -> TestResponse: - if args and isinstance( - args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) - ): - if isinstance(args[0], werkzeug.test.EnvironBuilder): - builder = copy(args[0]) - builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] - request = builder.get_request() - elif isinstance(args[0], dict): - request = EnvironBuilder.from_environ( - args[0], app=self.application, environ_base=self._copy_environ({}) - ).get_request() - else: - # isinstance(args[0], BaseRequest) - request = copy(args[0]) - request.environ = self._copy_environ(request.environ) - else: - # request is None - request = self._request_from_builder_args(args, kwargs) - - # Pop any previously preserved contexts. This prevents contexts - # from being preserved across redirects or multiple requests - # within a single block. - self._context_stack.close() - - response = super().open( - request, - buffered=buffered, - follow_redirects=follow_redirects, - ) - response.json_module = self.application.json # type: ignore[assignment] - - # Re-push contexts that were preserved during the request. - while self._new_contexts: - cm = self._new_contexts.pop() - self._context_stack.enter_context(cm) - - return response - - def __enter__(self) -> FlaskClient: - if self.preserve_context: - raise RuntimeError("Cannot nest client invocations") - self.preserve_context = True - return self - - def __exit__( - self, - exc_type: type | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.preserve_context = False - self._context_stack.close() - - -class FlaskCliRunner(CliRunner): - """A :class:`~click.testing.CliRunner` for testing a Flask app's - CLI commands. Typically created using - :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. - """ - - def __init__(self, app: Flask, **kwargs: t.Any) -> None: - self.app = app - super().__init__(**kwargs) - - def invoke( # type: ignore - self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any - ) -> t.Any: - """Invokes a CLI command in an isolated environment. See - :meth:`CliRunner.invoke ` for - full method documentation. See :ref:`testing-cli` for examples. - - If the ``obj`` argument is not given, passes an instance of - :class:`~flask.cli.ScriptInfo` that knows how to load the Flask - app being tested. - - :param cli: Command object to invoke. Default is the app's - :attr:`~flask.app.Flask.cli` group. - :param args: List of strings to invoke the command with. - - :return: a :class:`~click.testing.Result` object. - """ - if cli is None: - cli = self.app.cli - - if "obj" not in kwargs: - kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) - - return super().invoke(cli, args, **kwargs) diff --git a/src/flask/typing.py b/src/flask/typing.py deleted file mode 100644 index cf6d4ae6..00000000 --- a/src/flask/typing.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import typing as t - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import WSGIApplication # noqa: F401 - from werkzeug.datastructures import Headers # noqa: F401 - from werkzeug.sansio.response import Response # noqa: F401 - -# The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union[ - "Response", - str, - bytes, - t.List[t.Any], - # Only dict is actually accepted, but Mapping allows for TypedDict. - t.Mapping[str, t.Any], - t.Iterator[str], - t.Iterator[bytes], -] - -# the possible types for an individual HTTP header -# This should be a Union, but mypy doesn't pass unless it's a TypeVar. -HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] - -# the possible types for HTTP headers -HeadersValue = t.Union[ - "Headers", - t.Mapping[str, HeaderValue], - t.Sequence[t.Tuple[str, HeaderValue]], -] - -# The possible types returned by a route function. -ResponseReturnValue = t.Union[ - ResponseValue, - t.Tuple[ResponseValue, HeadersValue], - t.Tuple[ResponseValue, int], - t.Tuple[ResponseValue, int, HeadersValue], - "WSGIApplication", -] - -# Allow any subclass of werkzeug.Response, such as the one from Flask, -# as a callback argument. Using werkzeug.Response directly makes a -# callback annotated with flask.Response fail type checking. -ResponseClass = t.TypeVar("ResponseClass", bound="Response") - -AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named -AfterRequestCallable = t.Union[ - t.Callable[[ResponseClass], ResponseClass], - t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], -] -BeforeFirstRequestCallable = t.Union[ - t.Callable[[], None], t.Callable[[], t.Awaitable[None]] -] -BeforeRequestCallable = t.Union[ - t.Callable[[], t.Optional[ResponseReturnValue]], - t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], -] -ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] -TeardownCallable = t.Union[ - t.Callable[[t.Optional[BaseException]], None], - t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], -] -TemplateContextProcessorCallable = t.Union[ - t.Callable[[], t.Dict[str, t.Any]], - t.Callable[[], t.Awaitable[t.Dict[str, t.Any]]], -] -TemplateFilterCallable = t.Callable[..., t.Any] -TemplateGlobalCallable = t.Callable[..., t.Any] -TemplateTestCallable = t.Callable[..., bool] -URLDefaultCallable = t.Callable[[str, t.Dict[str, t.Any]], None] -URLValuePreprocessorCallable = t.Callable[ - [t.Optional[str], t.Optional[t.Dict[str, t.Any]]], None -] - -# This should take Exception, but that either breaks typing the argument -# with a specific exception, or decorating multiple times with different -# exceptions (and using a union type on the argument). -# https://github.com/pallets/flask/issues/4095 -# https://github.com/pallets/flask/issues/4295 -# https://github.com/pallets/flask/issues/4297 -ErrorHandlerCallable = t.Union[ - t.Callable[[t.Any], ResponseReturnValue], - t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], -] - -RouteCallable = t.Union[ - t.Callable[..., ResponseReturnValue], - t.Callable[..., t.Awaitable[ResponseReturnValue]], -] diff --git a/src/flask/views.py b/src/flask/views.py deleted file mode 100644 index 794fdc06..00000000 --- a/src/flask/views.py +++ /dev/null @@ -1,191 +0,0 @@ -from __future__ import annotations - -import typing as t - -from . import typing as ft -from .globals import current_app -from .globals import request - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - -http_method_funcs = frozenset( - ["get", "post", "head", "options", "delete", "put", "trace", "patch"] -) - - -class View: - """Subclass this class and override :meth:`dispatch_request` to - create a generic class-based view. Call :meth:`as_view` to create a - view function that creates an instance of the class with the given - arguments and calls its ``dispatch_request`` method with any URL - variables. - - See :doc:`views` for a detailed guide. - - .. code-block:: python - - class Hello(View): - init_every_request = False - - def dispatch_request(self, name): - return f"Hello, {name}!" - - app.add_url_rule( - "/hello/", view_func=Hello.as_view("hello") - ) - - Set :attr:`methods` on the class to change what methods the view - accepts. - - Set :attr:`decorators` on the class to apply a list of decorators to - the generated view function. Decorators applied to the class itself - will not be applied to the generated view function! - - Set :attr:`init_every_request` to ``False`` for efficiency, unless - you need to store request-global data on ``self``. - """ - - #: The methods this view is registered for. Uses the same default - #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and - #: ``add_url_rule`` by default. - methods: t.ClassVar[t.Collection[str] | None] = None - - #: Control whether the ``OPTIONS`` method is handled automatically. - #: Uses the same default (``True``) as ``route`` and - #: ``add_url_rule`` by default. - provide_automatic_options: t.ClassVar[bool | None] = None - - #: A list of decorators to apply, in order, to the generated view - #: function. Remember that ``@decorator`` syntax is applied bottom - #: to top, so the first decorator in the list would be the bottom - #: decorator. - #: - #: .. versionadded:: 0.8 - decorators: t.ClassVar[list[t.Callable[[F], F]]] = [] - - #: Create a new instance of this view class for every request by - #: default. If a view subclass sets this to ``False``, the same - #: instance is used for every request. - #: - #: A single instance is more efficient, especially if complex setup - #: is done during init. However, storing data on ``self`` is no - #: longer safe across requests, and :data:`~flask.g` should be used - #: instead. - #: - #: .. versionadded:: 2.2 - init_every_request: t.ClassVar[bool] = True - - def dispatch_request(self) -> ft.ResponseReturnValue: - """The actual view function behavior. Subclasses must override - this and return a valid response. Any variables from the URL - rule are passed as keyword arguments. - """ - raise NotImplementedError() - - @classmethod - def as_view( - cls, name: str, *class_args: t.Any, **class_kwargs: t.Any - ) -> ft.RouteCallable: - """Convert the class into a view function that can be registered - for a route. - - By default, the generated view will create a new instance of the - view class for every request and call its - :meth:`dispatch_request` method. If the view class sets - :attr:`init_every_request` to ``False``, the same instance will - be used for every request. - - Except for ``name``, all other arguments passed to this method - are forwarded to the view class ``__init__`` method. - - .. versionchanged:: 2.2 - Added the ``init_every_request`` class attribute. - """ - if cls.init_every_request: - - def view(**kwargs: t.Any) -> ft.ResponseReturnValue: - self = view.view_class( # type: ignore[attr-defined] - *class_args, **class_kwargs - ) - return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] - - else: - self = cls(*class_args, **class_kwargs) - - def view(**kwargs: t.Any) -> ft.ResponseReturnValue: - return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] - - if cls.decorators: - view.__name__ = name - view.__module__ = cls.__module__ - for decorator in cls.decorators: - view = decorator(view) - - # We attach the view class to the view function for two reasons: - # first of all it allows us to easily figure out what class-based - # view this thing came from, secondly it's also used for instantiating - # the view class so you can actually replace it with something else - # for testing purposes and debugging. - view.view_class = cls # type: ignore - view.__name__ = name - view.__doc__ = cls.__doc__ - view.__module__ = cls.__module__ - view.methods = cls.methods # type: ignore - view.provide_automatic_options = cls.provide_automatic_options # type: ignore - return view - - -class MethodView(View): - """Dispatches request methods to the corresponding instance methods. - For example, if you implement a ``get`` method, it will be used to - handle ``GET`` requests. - - This can be useful for defining a REST API. - - :attr:`methods` is automatically set based on the methods defined on - the class. - - See :doc:`views` for a detailed guide. - - .. code-block:: python - - class CounterAPI(MethodView): - def get(self): - return str(session.get("counter", 0)) - - def post(self): - session["counter"] = session.get("counter", 0) + 1 - return redirect(url_for("counter")) - - app.add_url_rule( - "/counter", view_func=CounterAPI.as_view("counter") - ) - """ - - def __init_subclass__(cls, **kwargs: t.Any) -> None: - super().__init_subclass__(**kwargs) - - if "methods" not in cls.__dict__: - methods = set() - - for base in cls.__bases__: - if getattr(base, "methods", None): - methods.update(base.methods) # type: ignore[attr-defined] - - for key in http_method_funcs: - if hasattr(cls, key): - methods.add(key.upper()) - - if methods: - cls.methods = methods - - def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: - meth = getattr(self, request.method.lower(), None) - - # If the request method is HEAD and we don't have a handler for it - # retry with GET. - if meth is None and request.method == "HEAD": - meth = getattr(self, "get", None) - - assert meth is not None, f"Unimplemented method {request.method!r}" - return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py deleted file mode 100644 index db3118e6..00000000 --- a/src/flask/wrappers.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import annotations - -import typing as t - -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import HTTPException -from werkzeug.wrappers import Request as RequestBase -from werkzeug.wrappers import Response as ResponseBase - -from . import json -from .globals import current_app -from .helpers import _split_blueprint_path - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.routing import Rule - - -class Request(RequestBase): - """The request object used by default in Flask. Remembers the - matched endpoint and view arguments. - - It is what ends up as :class:`~flask.request`. If you want to replace - the request object used you can subclass this and set - :attr:`~flask.Flask.request_class` to your subclass. - - The request object is a :class:`~werkzeug.wrappers.Request` subclass and - provides all of the attributes Werkzeug defines plus a few Flask - specific ones. - """ - - json_module: t.Any = json - - #: The internal URL rule that matched the request. This can be - #: useful to inspect which methods are allowed for the URL from - #: a before/after handler (``request.url_rule.methods``) etc. - #: Though if the request's method was invalid for the URL rule, - #: the valid list is available in ``routing_exception.valid_methods`` - #: instead (an attribute of the Werkzeug exception - #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) - #: because the request was never internally bound. - #: - #: .. versionadded:: 0.6 - url_rule: Rule | None = None - - #: A dict of view arguments that matched the request. If an exception - #: happened when matching, this will be ``None``. - view_args: dict[str, t.Any] | None = None - - #: If matching the URL failed, this is the exception that will be - #: raised / was raised as part of the request handling. This is - #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or - #: something similar. - routing_exception: HTTPException | None = None - - @property - def max_content_length(self) -> int | None: # type: ignore[override] - """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" - if current_app: - return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] - else: - return None - - @property - def endpoint(self) -> str | None: - """The endpoint that matched the request URL. - - This will be ``None`` if matching failed or has not been - performed yet. - - This in combination with :attr:`view_args` can be used to - reconstruct the same URL or a modified URL. - """ - if self.url_rule is not None: - return self.url_rule.endpoint # type: ignore[no-any-return] - - return None - - @property - def blueprint(self) -> str | None: - """The registered name of the current blueprint. - - This will be ``None`` if the endpoint is not part of a - blueprint, or if URL matching failed or has not been performed - yet. - - This does not necessarily match the name the blueprint was - created with. It may have been nested, or registered with a - different name. - """ - endpoint = self.endpoint - - if endpoint is not None and "." in endpoint: - return endpoint.rpartition(".")[0] - - return None - - @property - def blueprints(self) -> list[str]: - """The registered names of the current blueprint upwards through - parent blueprints. - - This will be an empty list if there is no current blueprint, or - if URL matching failed. - - .. versionadded:: 2.0.1 - """ - name = self.blueprint - - if name is None: - return [] - - return _split_blueprint_path(name) - - def _load_form_data(self) -> None: - super()._load_form_data() - - # In debug mode we're replacing the files multidict with an ad-hoc - # subclass that raises a different error for key errors. - if ( - current_app - and current_app.debug - and self.mimetype != "multipart/form-data" - and not self.files - ): - from .debughelpers import attach_enctype_error_multidict - - attach_enctype_error_multidict(self) - - def on_json_loading_failed(self, e: ValueError | None) -> t.Any: - try: - return super().on_json_loading_failed(e) - except BadRequest as e: - if current_app and current_app.debug: - raise - - raise BadRequest() from e - - -class Response(ResponseBase): - """The response object that is used by default in Flask. Works like the - response object from Werkzeug but is set to have an HTML mimetype by - default. Quite often you don't have to create this object yourself because - :meth:`~flask.Flask.make_response` will take care of that for you. - - If you want to replace the response object used you can subclass this and - set :attr:`~flask.Flask.response_class` to your subclass. - - .. versionchanged:: 1.0 - JSON support is added to the response, like the request. This is useful - when testing to get the test client response data as JSON. - - .. versionchanged:: 1.0 - - Added :attr:`max_cookie_size`. - """ - - default_mimetype: str | None = "text/html" - - json_module = json - - autocorrect_location_header = False - - @property - def max_cookie_size(self) -> int: # type: ignore - """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. - - See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in - Werkzeug's docs. - """ - if current_app: - return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] - - # return Werkzeug's default when not in an app context - return super().max_cookie_size diff --git a/tests/conftest.py b/tests/conftest.py index 58cf85d8..6bf62f0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,160 +1,62 @@ import os -import pkgutil -import sys +import tempfile import pytest -from _pytest import monkeypatch -from flask import Flask -from flask.globals import request_ctx +from flaskr import create_app +from flaskr.db import get_db +from flaskr.db import init_db - -@pytest.fixture(scope="session", autouse=True) -def _standard_os_environ(): - """Set up ``os.environ`` at the start of the test session to have - standard values. Returns a list of operations that is used by - :func:`._reset_os_environ` after each test. - """ - mp = monkeypatch.MonkeyPatch() - out = ( - (os.environ, "FLASK_ENV_FILE", monkeypatch.notset), - (os.environ, "FLASK_APP", monkeypatch.notset), - (os.environ, "FLASK_DEBUG", monkeypatch.notset), - (os.environ, "FLASK_RUN_FROM_CLI", monkeypatch.notset), - (os.environ, "WERKZEUG_RUN_MAIN", monkeypatch.notset), - ) - - for _, key, value in out: - if value is monkeypatch.notset: - mp.delenv(key, False) - else: - mp.setenv(key, value) - - yield out - mp.undo() - - -@pytest.fixture(autouse=True) -def _reset_os_environ(monkeypatch, _standard_os_environ): - """Reset ``os.environ`` to the standard environ after each test, - in case a test changed something without cleaning up. - """ - monkeypatch._setitem.extend(_standard_os_environ) +# read in SQL for populating test data +with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: + _data_sql = f.read().decode("utf8") @pytest.fixture def app(): - app = Flask("flask_test", root_path=os.path.dirname(__file__)) - app.config.update( - TESTING=True, - SECRET_KEY="test key", - ) - return app + """Create and configure a new app instance for each test.""" + # create a temporary file to isolate the database for each test + db_fd, db_path = tempfile.mkstemp() + # create the app with common test config + app = create_app({"TESTING": True, "DATABASE": db_path}) + # create the database and load test data + with app.app_context(): + init_db() + get_db().executescript(_data_sql) -@pytest.fixture -def app_ctx(app): - with app.app_context() as ctx: - yield ctx + yield app - -@pytest.fixture -def req_ctx(app): - with app.test_request_context() as ctx: - yield ctx + # close and remove the temporary database + os.close(db_fd) + os.unlink(db_path) @pytest.fixture def client(app): + """A test client for the app.""" return app.test_client() @pytest.fixture -def test_apps(monkeypatch): - monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps")) - original_modules = set(sys.modules.keys()) - - yield - - # Remove any imports cached during the test. Otherwise "import app" - # will work in the next test even though it's no longer on the path. - for key in sys.modules.keys() - original_modules: - sys.modules.pop(key) +def runner(app): + """A test runner for the app's Click commands.""" + return app.test_cli_runner() -@pytest.fixture(autouse=True) -def leak_detector(): - yield +class AuthActions: + def __init__(self, client): + self._client = client - # make sure we're not leaking a request context since we are - # testing flask internally in debug mode in a few cases - leaks = [] - while request_ctx: - leaks.append(request_ctx._get_current_object()) - request_ctx.pop() + def login(self, username="test", password="test"): + return self._client.post( + "/auth/login", data={"username": username, "password": password} + ) - assert leaks == [] - - -@pytest.fixture(params=(True, False)) -def limit_loader(request, monkeypatch): - """Patch pkgutil.get_loader to give loader without get_filename or archive. - - This provides for tests where a system has custom loaders, e.g. Google App - Engine's HardenedModulesHook, which have neither the `get_filename` method - nor the `archive` attribute. - - This fixture will run the testcase twice, once with and once without the - limitation/mock. - """ - if not request.param: - return - - class LimitedLoader: - def __init__(self, loader): - self.loader = loader - - def __getattr__(self, name): - if name in {"archive", "get_filename"}: - raise AttributeError(f"Mocking a loader which does not have {name!r}.") - return getattr(self.loader, name) - - old_get_loader = pkgutil.get_loader - - def get_loader(*args, **kwargs): - return LimitedLoader(old_get_loader(*args, **kwargs)) - - monkeypatch.setattr(pkgutil, "get_loader", get_loader) + def logout(self): + return self._client.get("/auth/logout") @pytest.fixture -def modules_tmp_path(tmp_path, monkeypatch): - """A temporary directory added to sys.path.""" - rv = tmp_path / "modules_tmp" - rv.mkdir() - monkeypatch.syspath_prepend(os.fspath(rv)) - return rv - - -@pytest.fixture -def modules_tmp_path_prefix(modules_tmp_path, monkeypatch): - monkeypatch.setattr(sys, "prefix", os.fspath(modules_tmp_path)) - return modules_tmp_path - - -@pytest.fixture -def site_packages(modules_tmp_path, monkeypatch): - """Create a fake site-packages.""" - py_dir = f"python{sys.version_info.major}.{sys.version_info.minor}" - rv = modules_tmp_path / "lib" / py_dir / "site-packages" - rv.mkdir(parents=True) - monkeypatch.syspath_prepend(os.fspath(rv)) - return rv - - -@pytest.fixture -def purge_module(request): - def inner(name): - request.addfinalizer(lambda: sys.modules.pop(name, None)) - - return inner +def auth(client): + return AuthActions(client) diff --git a/examples/tutorial/tests/data.sql b/tests/data.sql similarity index 100% rename from examples/tutorial/tests/data.sql rename to tests/data.sql diff --git a/tests/static/config.json b/tests/static/config.json deleted file mode 100644 index 4eedab12..00000000 --- a/tests/static/config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_KEY": "foo", - "SECRET_KEY": "config" -} diff --git a/tests/static/config.toml b/tests/static/config.toml deleted file mode 100644 index 64acdbdd..00000000 --- a/tests/static/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -TEST_KEY="foo" -SECRET_KEY="config" diff --git a/tests/static/index.html b/tests/static/index.html deleted file mode 100644 index de8b69b6..00000000 --- a/tests/static/index.html +++ /dev/null @@ -1 +0,0 @@ -

Hello World!

diff --git a/tests/templates/_macro.html b/tests/templates/_macro.html deleted file mode 100644 index 3460ae2e..00000000 --- a/tests/templates/_macro.html +++ /dev/null @@ -1 +0,0 @@ -{% macro hello(name) %}Hello {{ name }}!{% endmacro %} diff --git a/tests/templates/context_template.html b/tests/templates/context_template.html deleted file mode 100644 index fadf3e5d..00000000 --- a/tests/templates/context_template.html +++ /dev/null @@ -1 +0,0 @@ -

{{ value }}|{{ injected_value }} diff --git a/tests/templates/escaping_template.html b/tests/templates/escaping_template.html deleted file mode 100644 index dc47644d..00000000 --- a/tests/templates/escaping_template.html +++ /dev/null @@ -1,6 +0,0 @@ -{{ text }} -{{ html }} -{% autoescape false %}{{ text }} -{{ html }}{% endautoescape %} -{% autoescape true %}{{ text }} -{{ html }}{% endautoescape %} diff --git a/tests/templates/mail.txt b/tests/templates/mail.txt deleted file mode 100644 index d6cb92ea..00000000 --- a/tests/templates/mail.txt +++ /dev/null @@ -1 +0,0 @@ -{{ foo}} Mail diff --git a/tests/templates/nested/nested.txt b/tests/templates/nested/nested.txt deleted file mode 100644 index 2c8634f9..00000000 --- a/tests/templates/nested/nested.txt +++ /dev/null @@ -1 +0,0 @@ -I'm nested diff --git a/tests/templates/non_escaping_template.txt b/tests/templates/non_escaping_template.txt deleted file mode 100644 index 542864e8..00000000 --- a/tests/templates/non_escaping_template.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ text }} -{{ html }} -{% autoescape false %}{{ text }} -{{ html }}{% endautoescape %} -{% autoescape true %}{{ text }} -{{ html }}{% endautoescape %} -{{ text }} -{{ html }} diff --git a/tests/templates/simple_template.html b/tests/templates/simple_template.html deleted file mode 100644 index c24612cb..00000000 --- a/tests/templates/simple_template.html +++ /dev/null @@ -1 +0,0 @@ -

{{ whiskey }}

diff --git a/tests/templates/template_filter.html b/tests/templates/template_filter.html deleted file mode 100644 index d51506a3..00000000 --- a/tests/templates/template_filter.html +++ /dev/null @@ -1 +0,0 @@ -{{ value|super_reverse }} diff --git a/tests/templates/template_test.html b/tests/templates/template_test.html deleted file mode 100644 index 92d5561b..00000000 --- a/tests/templates/template_test.html +++ /dev/null @@ -1,3 +0,0 @@ -{% if value is boolean %} - Success! -{% endif %} diff --git a/tests/test_appctx.py b/tests/test_appctx.py deleted file mode 100644 index ca9e079e..00000000 --- a/tests/test_appctx.py +++ /dev/null @@ -1,209 +0,0 @@ -import pytest - -import flask -from flask.globals import app_ctx -from flask.globals import request_ctx - - -def test_basic_url_generation(app): - app.config["SERVER_NAME"] = "localhost" - app.config["PREFERRED_URL_SCHEME"] = "https" - - @app.route("/") - def index(): - pass - - with app.app_context(): - rv = flask.url_for("index") - assert rv == "https://localhost/" - - -def test_url_generation_requires_server_name(app): - with app.app_context(): - with pytest.raises(RuntimeError): - flask.url_for("index") - - -def test_url_generation_without_context_fails(): - with pytest.raises(RuntimeError): - flask.url_for("index") - - -def test_request_context_means_app_context(app): - with app.test_request_context(): - assert flask.current_app._get_current_object() is app - assert not flask.current_app - - -def test_app_context_provides_current_app(app): - with app.app_context(): - assert flask.current_app._get_current_object() is app - assert not flask.current_app - - -def test_app_tearing_down(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - with app.app_context(): - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_previous_exception(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - try: - raise Exception("dummy") - except Exception: - pass - - with app.app_context(): - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_handled_exception_by_except_block(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - with app.app_context(): - try: - raise Exception("dummy") - except Exception: - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_handled_exception_by_app_handler(app, client): - app.config["PROPAGATE_EXCEPTIONS"] = True - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - @app.route("/") - def index(): - raise Exception("dummy") - - @app.errorhandler(Exception) - def handler(f): - return flask.jsonify(str(f)) - - with app.app_context(): - client.get("/") - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_unhandled_exception(app, client): - app.config["PROPAGATE_EXCEPTIONS"] = True - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - @app.route("/") - def index(): - raise ValueError("dummy") - - with pytest.raises(ValueError, match="dummy"): - with app.app_context(): - client.get("/") - - assert len(cleanup_stuff) == 1 - assert isinstance(cleanup_stuff[0], ValueError) - assert str(cleanup_stuff[0]) == "dummy" - - -def test_app_ctx_globals_methods(app, app_ctx): - # get - assert flask.g.get("foo") is None - assert flask.g.get("foo", "bar") == "bar" - # __contains__ - assert "foo" not in flask.g - flask.g.foo = "bar" - assert "foo" in flask.g - # setdefault - flask.g.setdefault("bar", "the cake is a lie") - flask.g.setdefault("bar", "hello world") - assert flask.g.bar == "the cake is a lie" - # pop - assert flask.g.pop("bar") == "the cake is a lie" - with pytest.raises(KeyError): - flask.g.pop("bar") - assert flask.g.pop("bar", "more cake") == "more cake" - # __iter__ - assert list(flask.g) == ["foo"] - # __repr__ - assert repr(flask.g) == "" - - -def test_custom_app_ctx_globals_class(app): - class CustomRequestGlobals: - def __init__(self): - self.spam = "eggs" - - app.app_ctx_globals_class = CustomRequestGlobals - with app.app_context(): - assert flask.render_template_string("{{ g.spam }}") == "eggs" - - -def test_context_refcounts(app, client): - called = [] - - @app.teardown_request - def teardown_req(error=None): - called.append("request") - - @app.teardown_appcontext - def teardown_app(error=None): - called.append("app") - - @app.route("/") - def index(): - with app_ctx: - with request_ctx: - pass - - assert flask.request.environ["werkzeug.request"] is not None - return "" - - res = client.get("/") - assert res.status_code == 200 - assert res.data == b"" - assert called == ["request", "app"] - - -def test_clean_pop(app): - app.testing = False - called = [] - - @app.teardown_request - def teardown_req(error=None): - raise ZeroDivisionError - - @app.teardown_appcontext - def teardown_app(error=None): - called.append("TEARDOWN") - - with app.app_context(): - called.append(flask.current_app.name) - - assert called == ["flask_test", "TEARDOWN"] - assert not flask.current_app diff --git a/tests/test_apps/.env b/tests/test_apps/.env deleted file mode 100644 index 0890b615..00000000 --- a/tests/test_apps/.env +++ /dev/null @@ -1,4 +0,0 @@ -FOO=env -SPAM=1 -EGGS=2 -HAM=火腿 diff --git a/tests/test_apps/.flaskenv b/tests/test_apps/.flaskenv deleted file mode 100644 index 59f96af7..00000000 --- a/tests/test_apps/.flaskenv +++ /dev/null @@ -1,3 +0,0 @@ -FOO=flaskenv -BAR=bar -EGGS=0 diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py deleted file mode 100644 index ad594cf1..00000000 --- a/tests/test_apps/blueprintapp/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import Flask - -app = Flask(__name__) -app.config["DEBUG"] = True -from blueprintapp.apps.admin import admin # noqa: E402 -from blueprintapp.apps.frontend import frontend # noqa: E402 - -app.register_blueprint(admin) -app.register_blueprint(frontend) diff --git a/tests/test_apps/blueprintapp/apps/__init__.py b/tests/test_apps/blueprintapp/apps/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_apps/blueprintapp/apps/admin/__init__.py b/tests/test_apps/blueprintapp/apps/admin/__init__.py deleted file mode 100644 index b197fad0..00000000 --- a/tests/test_apps/blueprintapp/apps/admin/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask import Blueprint -from flask import render_template - -admin = Blueprint( - "admin", - __name__, - url_prefix="/admin", - template_folder="templates", - static_folder="static", -) - - -@admin.route("/") -def index(): - return render_template("admin/index.html") - - -@admin.route("/index2") -def index2(): - return render_template("./admin/index.html") diff --git a/tests/test_apps/blueprintapp/apps/admin/static/css/test.css b/tests/test_apps/blueprintapp/apps/admin/static/css/test.css deleted file mode 100644 index b9f564de..00000000 --- a/tests/test_apps/blueprintapp/apps/admin/static/css/test.css +++ /dev/null @@ -1 +0,0 @@ -/* nested file */ diff --git a/tests/test_apps/blueprintapp/apps/admin/static/test.txt b/tests/test_apps/blueprintapp/apps/admin/static/test.txt deleted file mode 100644 index f220d22f..00000000 --- a/tests/test_apps/blueprintapp/apps/admin/static/test.txt +++ /dev/null @@ -1 +0,0 @@ -Admin File diff --git a/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html b/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html deleted file mode 100644 index eeec199a..00000000 --- a/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello from the Admin diff --git a/tests/test_apps/blueprintapp/apps/frontend/__init__.py b/tests/test_apps/blueprintapp/apps/frontend/__init__.py deleted file mode 100644 index 7cc5cd82..00000000 --- a/tests/test_apps/blueprintapp/apps/frontend/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import Blueprint -from flask import render_template - -frontend = Blueprint("frontend", __name__, template_folder="templates") - - -@frontend.route("/") -def index(): - return render_template("frontend/index.html") - - -@frontend.route("/missing") -def missing_template(): - return render_template("missing_template.html") diff --git a/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html b/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html deleted file mode 100644 index a062d713..00000000 --- a/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello from the Frontend diff --git a/tests/test_apps/cliapp/__init__.py b/tests/test_apps/cliapp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_apps/cliapp/app.py b/tests/test_apps/cliapp/app.py deleted file mode 100644 index 017ce280..00000000 --- a/tests/test_apps/cliapp/app.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -testapp = Flask("testapp") diff --git a/tests/test_apps/cliapp/factory.py b/tests/test_apps/cliapp/factory.py deleted file mode 100644 index 1d27396d..00000000 --- a/tests/test_apps/cliapp/factory.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Flask - - -def create_app(): - return Flask("app") - - -def create_app2(foo, bar): - return Flask("_".join(["app2", foo, bar])) - - -def no_app(): - pass diff --git a/tests/test_apps/cliapp/importerrorapp.py b/tests/test_apps/cliapp/importerrorapp.py deleted file mode 100644 index 2c96c9b4..00000000 --- a/tests/test_apps/cliapp/importerrorapp.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask - -raise ImportError() - -testapp = Flask("testapp") diff --git a/tests/test_apps/cliapp/inner1/__init__.py b/tests/test_apps/cliapp/inner1/__init__.py deleted file mode 100644 index 8330f6e0..00000000 --- a/tests/test_apps/cliapp/inner1/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -application = Flask(__name__) diff --git a/tests/test_apps/cliapp/inner1/inner2/__init__.py b/tests/test_apps/cliapp/inner1/inner2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_apps/cliapp/inner1/inner2/flask.py b/tests/test_apps/cliapp/inner1/inner2/flask.py deleted file mode 100644 index d7562aac..00000000 --- a/tests/test_apps/cliapp/inner1/inner2/flask.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -app = Flask(__name__) diff --git a/tests/test_apps/cliapp/message.txt b/tests/test_apps/cliapp/message.txt deleted file mode 100644 index fc2b2cf0..00000000 --- a/tests/test_apps/cliapp/message.txt +++ /dev/null @@ -1 +0,0 @@ -So long, and thanks for all the fish. diff --git a/tests/test_apps/cliapp/multiapp.py b/tests/test_apps/cliapp/multiapp.py deleted file mode 100644 index 4ed0f328..00000000 --- a/tests/test_apps/cliapp/multiapp.py +++ /dev/null @@ -1,4 +0,0 @@ -from flask import Flask - -app1 = Flask("app1") -app2 = Flask("app2") diff --git a/tests/test_apps/helloworld/hello.py b/tests/test_apps/helloworld/hello.py deleted file mode 100644 index 71a2f90c..00000000 --- a/tests/test_apps/helloworld/hello.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - - -@app.route("/") -def hello(): - return "Hello World!" diff --git a/tests/test_apps/helloworld/wsgi.py b/tests/test_apps/helloworld/wsgi.py deleted file mode 100644 index ab2d6e9f..00000000 --- a/tests/test_apps/helloworld/wsgi.py +++ /dev/null @@ -1 +0,0 @@ -from hello import app # noqa: F401 diff --git a/tests/test_apps/subdomaintestmodule/__init__.py b/tests/test_apps/subdomaintestmodule/__init__.py deleted file mode 100644 index b4ce4b16..00000000 --- a/tests/test_apps/subdomaintestmodule/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Module - -mod = Module(__name__, "foo", subdomain="foo") diff --git a/tests/test_apps/subdomaintestmodule/static/hello.txt b/tests/test_apps/subdomaintestmodule/static/hello.txt deleted file mode 100644 index 12e23c16..00000000 --- a/tests/test_apps/subdomaintestmodule/static/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello Subdomain diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index f52b0492..00000000 --- a/tests/test_async.py +++ /dev/null @@ -1,145 +0,0 @@ -import asyncio - -import pytest - -from flask import Blueprint -from flask import Flask -from flask import request -from flask.views import MethodView -from flask.views import View - -pytest.importorskip("asgiref") - - -class AppError(Exception): - pass - - -class BlueprintError(Exception): - pass - - -class AsyncView(View): - methods = ["GET", "POST"] - - async def dispatch_request(self): - await asyncio.sleep(0) - return request.method - - -class AsyncMethodView(MethodView): - async def get(self): - await asyncio.sleep(0) - return "GET" - - async def post(self): - await asyncio.sleep(0) - return "POST" - - -@pytest.fixture(name="async_app") -def _async_app(): - app = Flask(__name__) - - @app.route("/", methods=["GET", "POST"]) - @app.route("/home", methods=["GET", "POST"]) - async def index(): - await asyncio.sleep(0) - return request.method - - @app.errorhandler(AppError) - async def handle(_): - return "", 412 - - @app.route("/error") - async def error(): - raise AppError() - - blueprint = Blueprint("bp", __name__) - - @blueprint.route("/", methods=["GET", "POST"]) - async def bp_index(): - await asyncio.sleep(0) - return request.method - - @blueprint.errorhandler(BlueprintError) - async def bp_handle(_): - return "", 412 - - @blueprint.route("/error") - async def bp_error(): - raise BlueprintError() - - app.register_blueprint(blueprint, url_prefix="/bp") - - app.add_url_rule("/view", view_func=AsyncView.as_view("view")) - app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview")) - - return app - - -@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"]) -def test_async_route(path, async_app): - test_client = async_app.test_client() - response = test_client.get(path) - assert b"GET" in response.get_data() - response = test_client.post(path) - assert b"POST" in response.get_data() - - -@pytest.mark.parametrize("path", ["/error", "/bp/error"]) -def test_async_error_handler(path, async_app): - test_client = async_app.test_client() - response = test_client.get(path) - assert response.status_code == 412 - - -def test_async_before_after_request(): - app_before_called = False - app_after_called = False - bp_before_called = False - bp_after_called = False - - app = Flask(__name__) - - @app.route("/") - def index(): - return "" - - @app.before_request - async def before(): - nonlocal app_before_called - app_before_called = True - - @app.after_request - async def after(response): - nonlocal app_after_called - app_after_called = True - return response - - blueprint = Blueprint("bp", __name__) - - @blueprint.route("/") - def bp_index(): - return "" - - @blueprint.before_request - async def bp_before(): - nonlocal bp_before_called - bp_before_called = True - - @blueprint.after_request - async def bp_after(response): - nonlocal bp_after_called - bp_after_called = True - return response - - app.register_blueprint(blueprint, url_prefix="/bp") - - test_client = app.test_client() - test_client.get("/") - assert app_before_called - assert app_after_called - test_client.get("/bp/") - assert bp_before_called - assert bp_after_called diff --git a/examples/tutorial/tests/test_auth.py b/tests/test_auth.py similarity index 100% rename from examples/tutorial/tests/test_auth.py rename to tests/test_auth.py diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index 214cfee0..00000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,1890 +0,0 @@ -import gc -import re -import uuid -import warnings -import weakref -from datetime import datetime -from datetime import timezone -from platform import python_implementation - -import pytest -import werkzeug.serving -from markupsafe import Markup -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import Forbidden -from werkzeug.exceptions import NotFound -from werkzeug.http import parse_date -from werkzeug.routing import BuildError -from werkzeug.routing import RequestRedirect - -import flask - -require_cpython_gc = pytest.mark.skipif( - python_implementation() != "CPython", - reason="Requires CPython GC behavior", -) - - -def test_options_work(app, client): - @app.route("/", methods=["GET", "POST"]) - def index(): - return "Hello World" - - rv = client.open("/", method="OPTIONS") - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - assert rv.data == b"" - - -def test_options_on_multiple_rules(app, client): - @app.route("/", methods=["GET", "POST"]) - def index(): - return "Hello World" - - @app.route("/", methods=["PUT"]) - def index_put(): - return "Aha!" - - rv = client.open("/", method="OPTIONS") - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] - - -@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) -def test_method_route(app, client, method): - method_route = getattr(app, method) - client_method = getattr(client, method) - - @method_route("/") - def hello(): - return "Hello" - - assert client_method("/").data == b"Hello" - - -def test_method_route_no_methods(app): - with pytest.raises(TypeError): - app.get("/", methods=["GET", "POST"]) - - -def test_provide_automatic_options_attr(): - app = flask.Flask(__name__) - - def index(): - return "Hello World!" - - index.provide_automatic_options = False - app.route("/")(index) - rv = app.test_client().open("/", method="OPTIONS") - assert rv.status_code == 405 - - app = flask.Flask(__name__) - - def index2(): - return "Hello World!" - - index2.provide_automatic_options = True - app.route("/", methods=["OPTIONS"])(index2) - rv = app.test_client().open("/", method="OPTIONS") - assert sorted(rv.allow) == ["OPTIONS"] - - -def test_provide_automatic_options_kwarg(app, client): - def index(): - return flask.request.method - - def more(): - return flask.request.method - - app.add_url_rule("/", view_func=index, provide_automatic_options=False) - app.add_url_rule( - "/more", - view_func=more, - methods=["GET", "POST"], - provide_automatic_options=False, - ) - assert client.get("/").data == b"GET" - - rv = client.post("/") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD"] - - rv = client.open("/", method="OPTIONS") - assert rv.status_code == 405 - - rv = client.head("/") - assert rv.status_code == 200 - assert not rv.data # head truncates - assert client.post("/more").data == b"POST" - assert client.get("/more").data == b"GET" - - rv = client.delete("/more") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "POST"] - - rv = client.open("/more", method="OPTIONS") - assert rv.status_code == 405 - - -def test_request_dispatching(app, client): - @app.route("/") - def index(): - return flask.request.method - - @app.route("/more", methods=["GET", "POST"]) - def more(): - return flask.request.method - - assert client.get("/").data == b"GET" - rv = client.post("/") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] - rv = client.head("/") - assert rv.status_code == 200 - assert not rv.data # head truncates - assert client.post("/more").data == b"POST" - assert client.get("/more").data == b"GET" - rv = client.delete("/more") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - - -def test_disallow_string_for_allowed_methods(app): - with pytest.raises(TypeError): - app.add_url_rule("/", methods="GET POST", endpoint="test") - - -def test_url_mapping(app, client): - random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" - - def index(): - return flask.request.method - - def more(): - return flask.request.method - - def options(): - return random_uuid4 - - app.add_url_rule("/", "index", index) - app.add_url_rule("/more", "more", more, methods=["GET", "POST"]) - - # Issue 1288: Test that automatic options are not added - # when non-uppercase 'options' in methods - app.add_url_rule("/options", "options", options, methods=["options"]) - - assert client.get("/").data == b"GET" - rv = client.post("/") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] - rv = client.head("/") - assert rv.status_code == 200 - assert not rv.data # head truncates - assert client.post("/more").data == b"POST" - assert client.get("/more").data == b"GET" - rv = client.delete("/more") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - rv = client.open("/options", method="OPTIONS") - assert rv.status_code == 200 - assert random_uuid4 in rv.data.decode("utf-8") - - -def test_werkzeug_routing(app, client): - from werkzeug.routing import Rule - from werkzeug.routing import Submount - - app.url_map.add( - Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) - ) - - def bar(): - return "bar" - - def index(): - return "index" - - app.view_functions["bar"] = bar - app.view_functions["index"] = index - - assert client.get("/foo/").data == b"index" - assert client.get("/foo/bar").data == b"bar" - - -def test_endpoint_decorator(app, client): - from werkzeug.routing import Rule - from werkzeug.routing import Submount - - app.url_map.add( - Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) - ) - - @app.endpoint("bar") - def bar(): - return "bar" - - @app.endpoint("index") - def index(): - return "index" - - assert client.get("/foo/").data == b"index" - assert client.get("/foo/bar").data == b"bar" - - -def test_session(app, client): - @app.route("/set", methods=["POST"]) - def set(): - assert not flask.session.accessed - assert not flask.session.modified - flask.session["value"] = flask.request.form["value"] - assert flask.session.accessed - assert flask.session.modified - return "value set" - - @app.route("/get") - def get(): - assert not flask.session.accessed - assert not flask.session.modified - v = flask.session.get("value", "None") - assert flask.session.accessed - assert not flask.session.modified - return v - - assert client.post("/set", data={"value": "42"}).data == b"value set" - assert client.get("/get").data == b"42" - - -def test_session_path(app, client): - app.config.update(APPLICATION_ROOT="/foo") - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - rv = client.get("/", "http://example.com:8080/foo") - assert "path=/foo" in rv.headers["set-cookie"].lower() - - -def test_session_using_application_root(app, client): - class PrefixPathMiddleware: - def __init__(self, app, prefix): - self.app = app - self.prefix = prefix - - def __call__(self, environ, start_response): - environ["SCRIPT_NAME"] = self.prefix - return self.app(environ, start_response) - - app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, "/bar") - app.config.update(APPLICATION_ROOT="/bar") - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - rv = client.get("/", "http://example.com:8080/") - assert "path=/bar" in rv.headers["set-cookie"].lower() - - -def test_session_using_session_settings(app, client): - app.config.update( - SERVER_NAME="www.example.com:8080", - APPLICATION_ROOT="/test", - SESSION_COOKIE_DOMAIN=".example.com", - SESSION_COOKIE_HTTPONLY=False, - SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_SAMESITE="Lax", - SESSION_COOKIE_PATH="/", - ) - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - @app.route("/clear") - def clear(): - flask.session.pop("testing", None) - return "Goodbye World" - - rv = client.get("/", "http://www.example.com:8080/test/") - cookie = rv.headers["set-cookie"].lower() - # or condition for Werkzeug < 2.3 - assert "domain=example.com" in cookie or "domain=.example.com" in cookie - assert "path=/" in cookie - assert "secure" in cookie - assert "httponly" not in cookie - assert "samesite" in cookie - - rv = client.get("/clear", "http://www.example.com:8080/test/") - cookie = rv.headers["set-cookie"].lower() - assert "session=;" in cookie - # or condition for Werkzeug < 2.3 - assert "domain=example.com" in cookie or "domain=.example.com" in cookie - assert "path=/" in cookie - assert "secure" in cookie - assert "samesite" in cookie - - -def test_session_using_samesite_attribute(app, client): - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - app.config.update(SESSION_COOKIE_SAMESITE="invalid") - - with pytest.raises(ValueError): - client.get("/") - - app.config.update(SESSION_COOKIE_SAMESITE=None) - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite" not in cookie - - app.config.update(SESSION_COOKIE_SAMESITE="Strict") - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite=strict" in cookie - - app.config.update(SESSION_COOKIE_SAMESITE="Lax") - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite=lax" in cookie - - -def test_missing_session(app): - app.secret_key = None - - def expect_exception(f, *args, **kwargs): - e = pytest.raises(RuntimeError, f, *args, **kwargs) - assert e.value.args and "session is unavailable" in e.value.args[0] - - with app.test_request_context(): - assert flask.session.get("missing_key") is None - expect_exception(flask.session.__setitem__, "foo", 42) - expect_exception(flask.session.pop, "foo") - - -def test_session_expiration(app, client): - permanent = True - - @app.route("/") - def index(): - flask.session["test"] = 42 - flask.session.permanent = permanent - return "" - - @app.route("/test") - def test(): - return str(flask.session.permanent) - - rv = client.get("/") - assert "set-cookie" in rv.headers - match = re.search(r"(?i)\bexpires=([^;]+)", rv.headers["set-cookie"]) - expires = parse_date(match.group()) - expected = datetime.now(timezone.utc) + app.permanent_session_lifetime - assert expires.year == expected.year - assert expires.month == expected.month - assert expires.day == expected.day - - rv = client.get("/test") - assert rv.data == b"True" - - permanent = False - rv = client.get("/") - assert "set-cookie" in rv.headers - match = re.search(r"\bexpires=([^;]+)", rv.headers["set-cookie"]) - assert match is None - - -def test_session_stored_last(app, client): - @app.after_request - def modify_session(response): - flask.session["foo"] = 42 - return response - - @app.route("/") - def dump_session_contents(): - return repr(flask.session.get("foo")) - - assert client.get("/").data == b"None" - assert client.get("/").data == b"42" - - -def test_session_special_types(app, client): - now = datetime.now(timezone.utc).replace(microsecond=0) - the_uuid = uuid.uuid4() - - @app.route("/") - def dump_session_contents(): - flask.session["t"] = (1, 2, 3) - flask.session["b"] = b"\xff" - flask.session["m"] = Markup("") - flask.session["u"] = the_uuid - flask.session["d"] = now - flask.session["t_tag"] = {" t": "not-a-tuple"} - flask.session["di_t_tag"] = {" t__": "not-a-tuple"} - flask.session["di_tag"] = {" di": "not-a-dict"} - return "", 204 - - with client: - client.get("/") - s = flask.session - assert s["t"] == (1, 2, 3) - assert type(s["b"]) is bytes # noqa: E721 - assert s["b"] == b"\xff" - assert type(s["m"]) is Markup # noqa: E721 - assert s["m"] == Markup("") - assert s["u"] == the_uuid - assert s["d"] == now - assert s["t_tag"] == {" t": "not-a-tuple"} - assert s["di_t_tag"] == {" t__": "not-a-tuple"} - assert s["di_tag"] == {" di": "not-a-dict"} - - -def test_session_cookie_setting(app): - is_permanent = True - - @app.route("/bump") - def bump(): - rv = flask.session["foo"] = flask.session.get("foo", 0) + 1 - flask.session.permanent = is_permanent - return str(rv) - - @app.route("/read") - def read(): - return str(flask.session.get("foo", 0)) - - def run_test(expect_header): - with app.test_client() as c: - assert c.get("/bump").data == b"1" - assert c.get("/bump").data == b"2" - assert c.get("/bump").data == b"3" - - rv = c.get("/read") - set_cookie = rv.headers.get("set-cookie") - assert (set_cookie is not None) == expect_header - assert rv.data == b"3" - - is_permanent = True - app.config["SESSION_REFRESH_EACH_REQUEST"] = True - run_test(expect_header=True) - - is_permanent = True - app.config["SESSION_REFRESH_EACH_REQUEST"] = False - run_test(expect_header=False) - - is_permanent = False - app.config["SESSION_REFRESH_EACH_REQUEST"] = True - run_test(expect_header=False) - - is_permanent = False - app.config["SESSION_REFRESH_EACH_REQUEST"] = False - run_test(expect_header=False) - - -def test_session_vary_cookie(app, client): - @app.route("/set") - def set_session(): - flask.session["test"] = "test" - return "" - - @app.route("/get") - def get(): - return flask.session.get("test") - - @app.route("/getitem") - def getitem(): - return flask.session["test"] - - @app.route("/setdefault") - def setdefault(): - return flask.session.setdefault("test", "default") - - @app.route("/clear") - def clear(): - flask.session.clear() - return "" - - @app.route("/vary-cookie-header-set") - def vary_cookie_header_set(): - response = flask.Response() - response.vary.add("Cookie") - flask.session["test"] = "test" - return response - - @app.route("/vary-header-set") - def vary_header_set(): - response = flask.Response() - response.vary.update(("Accept-Encoding", "Accept-Language")) - flask.session["test"] = "test" - return response - - @app.route("/no-vary-header") - def no_vary_header(): - return "" - - def expect(path, header_value="Cookie"): - rv = client.get(path) - - if header_value: - # The 'Vary' key should exist in the headers only once. - assert len(rv.headers.get_all("Vary")) == 1 - assert rv.headers["Vary"] == header_value - else: - assert "Vary" not in rv.headers - - expect("/set") - expect("/get") - expect("/getitem") - expect("/setdefault") - expect("/clear") - expect("/vary-cookie-header-set") - expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie") - expect("/no-vary-header", None) - - -def test_session_refresh_vary(app, client): - @app.get("/login") - def login(): - flask.session["user_id"] = 1 - flask.session.permanent = True - return "" - - @app.get("/ignored") - def ignored(): - return "" - - rv = client.get("/login") - assert rv.headers["Vary"] == "Cookie" - rv = client.get("/ignored") - assert rv.headers["Vary"] == "Cookie" - - -def test_flashes(app, req_ctx): - assert not flask.session.modified - flask.flash("Zap") - flask.session.modified = False - flask.flash("Zip") - assert flask.session.modified - assert list(flask.get_flashed_messages()) == ["Zap", "Zip"] - - -def test_extended_flashing(app): - # Be sure app.testing=True below, else tests can fail silently. - # - # Specifically, if app.testing is not set to True, the AssertionErrors - # in the view functions will cause a 500 response to the test client - # instead of propagating exceptions. - - @app.route("/") - def index(): - flask.flash("Hello World") - flask.flash("Hello World", "error") - flask.flash(Markup("Testing"), "warning") - return "" - - @app.route("/test/") - def test(): - messages = flask.get_flashed_messages() - assert list(messages) == [ - "Hello World", - "Hello World", - Markup("Testing"), - ] - return "" - - @app.route("/test_with_categories/") - def test_with_categories(): - messages = flask.get_flashed_messages(with_categories=True) - assert len(messages) == 3 - assert list(messages) == [ - ("message", "Hello World"), - ("error", "Hello World"), - ("warning", Markup("Testing")), - ] - return "" - - @app.route("/test_filter/") - def test_filter(): - messages = flask.get_flashed_messages( - category_filter=["message"], with_categories=True - ) - assert list(messages) == [("message", "Hello World")] - return "" - - @app.route("/test_filters/") - def test_filters(): - messages = flask.get_flashed_messages( - category_filter=["message", "warning"], with_categories=True - ) - assert list(messages) == [ - ("message", "Hello World"), - ("warning", Markup("Testing")), - ] - return "" - - @app.route("/test_filters_without_returning_categories/") - def test_filters2(): - messages = flask.get_flashed_messages(category_filter=["message", "warning"]) - assert len(messages) == 2 - assert messages[0] == "Hello World" - assert messages[1] == Markup("Testing") - return "" - - # Create new test client on each test to clean flashed messages. - - client = app.test_client() - client.get("/") - client.get("/test_with_categories/") - - client = app.test_client() - client.get("/") - client.get("/test_filter/") - - client = app.test_client() - client.get("/") - client.get("/test_filters/") - - client = app.test_client() - client.get("/") - client.get("/test_filters_without_returning_categories/") - - -def test_request_processing(app, client): - evts = [] - - @app.before_request - def before_request(): - evts.append("before") - - @app.after_request - def after_request(response): - response.data += b"|after" - evts.append("after") - return response - - @app.route("/") - def index(): - assert "before" in evts - assert "after" not in evts - return "request" - - assert "after" not in evts - rv = client.get("/").data - assert "after" in evts - assert rv == b"request|after" - - -def test_request_preprocessing_early_return(app, client): - evts = [] - - @app.before_request - def before_request1(): - evts.append(1) - - @app.before_request - def before_request2(): - evts.append(2) - return "hello" - - @app.before_request - def before_request3(): - evts.append(3) - return "bye" - - @app.route("/") - def index(): - evts.append("index") - return "damnit" - - rv = client.get("/").data.strip() - assert rv == b"hello" - assert evts == [1, 2] - - -def test_after_request_processing(app, client): - @app.route("/") - def index(): - @flask.after_this_request - def foo(response): - response.headers["X-Foo"] = "a header" - return response - - return "Test" - - resp = client.get("/") - assert resp.status_code == 200 - assert resp.headers["X-Foo"] == "a header" - - -def test_teardown_request_handler(app, client): - called = [] - - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - - @app.route("/") - def root(): - return "Response" - - rv = client.get("/") - assert rv.status_code == 200 - assert b"Response" in rv.data - assert len(called) == 1 - - -def test_teardown_request_handler_debug_mode(app, client): - called = [] - - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - - @app.route("/") - def root(): - return "Response" - - rv = client.get("/") - assert rv.status_code == 200 - assert b"Response" in rv.data - assert len(called) == 1 - - -def test_teardown_request_handler_error(app, client): - called = [] - app.testing = False - - @app.teardown_request - def teardown_request1(exc): - assert type(exc) is ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError() - except Exception: - pass - - @app.teardown_request - def teardown_request2(exc): - assert type(exc) is ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError() - except Exception: - pass - - @app.route("/") - def fails(): - raise ZeroDivisionError - - rv = client.get("/") - assert rv.status_code == 500 - assert b"Internal Server Error" in rv.data - assert len(called) == 2 - - -def test_before_after_request_order(app, client): - called = [] - - @app.before_request - def before1(): - called.append(1) - - @app.before_request - def before2(): - called.append(2) - - @app.after_request - def after1(response): - called.append(4) - return response - - @app.after_request - def after2(response): - called.append(3) - return response - - @app.teardown_request - def finish1(exc): - called.append(6) - - @app.teardown_request - def finish2(exc): - called.append(5) - - @app.route("/") - def index(): - return "42" - - rv = client.get("/") - assert rv.data == b"42" - assert called == [1, 2, 3, 4, 5, 6] - - -def test_error_handling(app, client): - app.testing = False - - @app.errorhandler(404) - def not_found(e): - return "not found", 404 - - @app.errorhandler(500) - def internal_server_error(e): - return "internal server error", 500 - - @app.errorhandler(Forbidden) - def forbidden(e): - return "forbidden", 403 - - @app.route("/") - def index(): - flask.abort(404) - - @app.route("/error") - def error(): - raise ZeroDivisionError - - @app.route("/forbidden") - def error2(): - flask.abort(403) - - rv = client.get("/") - assert rv.status_code == 404 - assert rv.data == b"not found" - rv = client.get("/error") - assert rv.status_code == 500 - assert b"internal server error" == rv.data - rv = client.get("/forbidden") - assert rv.status_code == 403 - assert b"forbidden" == rv.data - - -def test_error_handling_processing(app, client): - app.testing = False - - @app.errorhandler(500) - def internal_server_error(e): - return "internal server error", 500 - - @app.route("/") - def broken_func(): - raise ZeroDivisionError - - @app.after_request - def after_request(resp): - resp.mimetype = "text/x-special" - return resp - - resp = client.get("/") - assert resp.mimetype == "text/x-special" - assert resp.data == b"internal server error" - - -def test_baseexception_error_handling(app, client): - app.testing = False - - @app.route("/") - def broken_func(): - raise KeyboardInterrupt() - - with pytest.raises(KeyboardInterrupt): - client.get("/") - - -def test_before_request_and_routing_errors(app, client): - @app.before_request - def attach_something(): - flask.g.something = "value" - - @app.errorhandler(404) - def return_something(error): - return flask.g.something, 404 - - rv = client.get("/") - assert rv.status_code == 404 - assert rv.data == b"value" - - -def test_user_error_handling(app, client): - class MyException(Exception): - pass - - @app.errorhandler(MyException) - def handle_my_exception(e): - assert isinstance(e, MyException) - return "42" - - @app.route("/") - def index(): - raise MyException() - - assert client.get("/").data == b"42" - - -def test_http_error_subclass_handling(app, client): - class ForbiddenSubclass(Forbidden): - pass - - @app.errorhandler(ForbiddenSubclass) - def handle_forbidden_subclass(e): - assert isinstance(e, ForbiddenSubclass) - return "banana" - - @app.errorhandler(403) - def handle_403(e): - assert not isinstance(e, ForbiddenSubclass) - assert isinstance(e, Forbidden) - return "apple" - - @app.route("/1") - def index1(): - raise ForbiddenSubclass() - - @app.route("/2") - def index2(): - flask.abort(403) - - @app.route("/3") - def index3(): - raise Forbidden() - - assert client.get("/1").data == b"banana" - assert client.get("/2").data == b"apple" - assert client.get("/3").data == b"apple" - - -def test_errorhandler_precedence(app, client): - class E1(Exception): - pass - - class E2(Exception): - pass - - class E3(E1, E2): - pass - - @app.errorhandler(E2) - def handle_e2(e): - return "E2" - - @app.errorhandler(Exception) - def handle_exception(e): - return "Exception" - - @app.route("/E1") - def raise_e1(): - raise E1 - - @app.route("/E3") - def raise_e3(): - raise E3 - - rv = client.get("/E1") - assert rv.data == b"Exception" - - rv = client.get("/E3") - assert rv.data == b"E2" - - -@pytest.mark.parametrize( - ("debug", "trap", "expect_key", "expect_abort"), - [(False, None, True, True), (True, None, False, True), (False, True, False, False)], -) -def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort): - app.config["DEBUG"] = debug - app.config["TRAP_BAD_REQUEST_ERRORS"] = trap - - @app.route("/key") - def fail(): - flask.request.form["missing_key"] - - @app.route("/abort") - def allow_abort(): - flask.abort(400) - - if expect_key: - rv = client.get("/key") - assert rv.status_code == 400 - assert b"missing_key" not in rv.data - else: - with pytest.raises(KeyError) as exc_info: - client.get("/key") - - assert exc_info.errisinstance(BadRequest) - assert "missing_key" in exc_info.value.get_description() - - if expect_abort: - rv = client.get("/abort") - assert rv.status_code == 400 - else: - with pytest.raises(BadRequest): - client.get("/abort") - - -def test_trapping_of_all_http_exceptions(app, client): - app.config["TRAP_HTTP_EXCEPTIONS"] = True - - @app.route("/fail") - def fail(): - flask.abort(404) - - with pytest.raises(NotFound): - client.get("/fail") - - -def test_error_handler_after_processor_error(app, client): - app.testing = False - - @app.before_request - def before_request(): - if _trigger == "before": - raise ZeroDivisionError - - @app.after_request - def after_request(response): - if _trigger == "after": - raise ZeroDivisionError - - return response - - @app.route("/") - def index(): - return "Foo" - - @app.errorhandler(500) - def internal_server_error(e): - return "Hello Server Error", 500 - - for _trigger in "before", "after": - rv = client.get("/") - assert rv.status_code == 500 - assert rv.data == b"Hello Server Error" - - -def test_enctype_debug_helper(app, client): - from flask.debughelpers import DebugFilesKeyError - - app.debug = True - - @app.route("/fail", methods=["POST"]) - def index(): - return flask.request.files["foo"].filename - - with pytest.raises(DebugFilesKeyError) as e: - client.post("/fail", data={"foo": "index.txt"}) - assert "no file contents were transmitted" in str(e.value) - assert "This was submitted: 'index.txt'" in str(e.value) - - -def test_response_types(app, client): - @app.route("/text") - def from_text(): - return "Hällo Wörld" - - @app.route("/bytes") - def from_bytes(): - return "Hällo Wörld".encode() - - @app.route("/full_tuple") - def from_full_tuple(): - return ( - "Meh", - 400, - {"X-Foo": "Testing", "Content-Type": "text/plain; charset=utf-8"}, - ) - - @app.route("/text_headers") - def from_text_headers(): - return "Hello", {"X-Foo": "Test", "Content-Type": "text/plain; charset=utf-8"} - - @app.route("/text_status") - def from_text_status(): - return "Hi, status!", 400 - - @app.route("/response_headers") - def from_response_headers(): - return ( - flask.Response( - "Hello world", 404, {"Content-Type": "text/html", "X-Foo": "Baz"} - ), - {"Content-Type": "text/plain", "X-Foo": "Bar", "X-Bar": "Foo"}, - ) - - @app.route("/response_status") - def from_response_status(): - return app.response_class("Hello world", 400), 500 - - @app.route("/wsgi") - def from_wsgi(): - return NotFound() - - @app.route("/dict") - def from_dict(): - return {"foo": "bar"}, 201 - - @app.route("/list") - def from_list(): - return ["foo", "bar"], 201 - - assert client.get("/text").data == "Hällo Wörld".encode() - assert client.get("/bytes").data == "Hällo Wörld".encode() - - rv = client.get("/full_tuple") - assert rv.data == b"Meh" - assert rv.headers["X-Foo"] == "Testing" - assert rv.status_code == 400 - assert rv.mimetype == "text/plain" - - rv = client.get("/text_headers") - assert rv.data == b"Hello" - assert rv.headers["X-Foo"] == "Test" - assert rv.status_code == 200 - assert rv.mimetype == "text/plain" - - rv = client.get("/text_status") - assert rv.data == b"Hi, status!" - assert rv.status_code == 400 - assert rv.mimetype == "text/html" - - rv = client.get("/response_headers") - assert rv.data == b"Hello world" - assert rv.content_type == "text/plain" - assert rv.headers.getlist("X-Foo") == ["Bar"] - assert rv.headers["X-Bar"] == "Foo" - assert rv.status_code == 404 - - rv = client.get("/response_status") - assert rv.data == b"Hello world" - assert rv.status_code == 500 - - rv = client.get("/wsgi") - assert b"Not Found" in rv.data - assert rv.status_code == 404 - - rv = client.get("/dict") - assert rv.json == {"foo": "bar"} - assert rv.status_code == 201 - - rv = client.get("/list") - assert rv.json == ["foo", "bar"] - assert rv.status_code == 201 - - -def test_response_type_errors(): - app = flask.Flask(__name__) - app.testing = True - - @app.route("/none") - def from_none(): - pass - - @app.route("/small_tuple") - def from_small_tuple(): - return ("Hello",) - - @app.route("/large_tuple") - def from_large_tuple(): - return "Hello", 234, {"X-Foo": "Bar"}, "???" - - @app.route("/bad_type") - def from_bad_type(): - return True - - @app.route("/bad_wsgi") - def from_bad_wsgi(): - return lambda: None - - c = app.test_client() - - with pytest.raises(TypeError) as e: - c.get("/none") - - assert "returned None" in str(e.value) - assert "from_none" in str(e.value) - - with pytest.raises(TypeError) as e: - c.get("/small_tuple") - - assert "tuple must have the form" in str(e.value) - - with pytest.raises(TypeError): - c.get("/large_tuple") - - with pytest.raises(TypeError) as e: - c.get("/bad_type") - - assert "it was a bool" in str(e.value) - - with pytest.raises(TypeError): - c.get("/bad_wsgi") - - -def test_make_response(app, req_ctx): - rv = flask.make_response() - assert rv.status_code == 200 - assert rv.data == b"" - assert rv.mimetype == "text/html" - - rv = flask.make_response("Awesome") - assert rv.status_code == 200 - assert rv.data == b"Awesome" - assert rv.mimetype == "text/html" - - rv = flask.make_response("W00t", 404) - assert rv.status_code == 404 - assert rv.data == b"W00t" - assert rv.mimetype == "text/html" - - rv = flask.make_response(c for c in "Hello") - assert rv.status_code == 200 - assert rv.data == b"Hello" - assert rv.mimetype == "text/html" - - -def test_make_response_with_response_instance(app, req_ctx): - rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) - assert rv.status_code == 400 - assert rv.data == b'{"msg":"W00t"}\n' - assert rv.mimetype == "application/json" - - rv = flask.make_response(flask.Response(""), 400) - assert rv.status_code == 400 - assert rv.data == b"" - assert rv.mimetype == "text/html" - - rv = flask.make_response( - flask.Response("", headers={"Content-Type": "text/html"}), - 400, - [("X-Foo", "bar")], - ) - assert rv.status_code == 400 - assert rv.headers["Content-Type"] == "text/html" - assert rv.headers["X-Foo"] == "bar" - - -@pytest.mark.parametrize("compact", [True, False]) -def test_jsonify_no_prettyprint(app, compact): - app.json.compact = compact - rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"}) - data = rv.data.strip() - assert (b" " not in data) is compact - assert (b"\n" not in data) is compact - - -def test_jsonify_mimetype(app, req_ctx): - app.json.mimetype = "application/vnd.api+json" - msg = {"msg": {"submsg": "W00t"}} - rv = flask.make_response(flask.jsonify(msg), 200) - assert rv.mimetype == "application/vnd.api+json" - - -def test_json_dump_dataclass(app, req_ctx): - from dataclasses import make_dataclass - - Data = make_dataclass("Data", [("name", str)]) - value = app.json.dumps(Data("Flask")) - value = app.json.loads(value) - assert value == {"name": "Flask"} - - -def test_jsonify_args_and_kwargs_check(app, req_ctx): - with pytest.raises(TypeError) as e: - flask.jsonify("fake args", kwargs="fake") - assert "args or kwargs" in str(e.value) - - -def test_url_generation(app, req_ctx): - @app.route("/hello/", methods=["POST"]) - def hello(): - pass - - assert flask.url_for("hello", name="test x") == "/hello/test%20x" - assert ( - flask.url_for("hello", name="test x", _external=True) - == "http://localhost/hello/test%20x" - ) - - -def test_build_error_handler(app): - # Test base case, a URL which results in a BuildError. - with app.test_request_context(): - pytest.raises(BuildError, flask.url_for, "spam") - - # Verify the error is re-raised if not the current exception. - try: - with app.test_request_context(): - flask.url_for("spam") - except BuildError as err: - error = err - try: - raise RuntimeError("Test case where BuildError is not current.") - except RuntimeError: - pytest.raises(BuildError, app.handle_url_build_error, error, "spam", {}) - - # Test a custom handler. - def handler(error, endpoint, values): - # Just a test. - return "/test_handler/" - - app.url_build_error_handlers.append(handler) - with app.test_request_context(): - assert flask.url_for("spam") == "/test_handler/" - - -def test_build_error_handler_reraise(app): - # Test a custom handler which reraises the BuildError - def handler_raises_build_error(error, endpoint, values): - raise error - - app.url_build_error_handlers.append(handler_raises_build_error) - - with app.test_request_context(): - pytest.raises(BuildError, flask.url_for, "not.existing") - - -def test_url_for_passes_special_values_to_build_error_handler(app): - @app.url_build_error_handlers.append - def handler(error, endpoint, values): - assert values == { - "_external": False, - "_anchor": None, - "_method": None, - "_scheme": None, - } - return "handled" - - with app.test_request_context(): - flask.url_for("/") - - -def test_static_files(app, client): - rv = client.get("/static/index.html") - assert rv.status_code == 200 - assert rv.data.strip() == b"

Hello World!

" - with app.test_request_context(): - assert flask.url_for("static", filename="index.html") == "/static/index.html" - rv.close() - - -def test_static_url_path(): - app = flask.Flask(__name__, static_url_path="/foo") - app.testing = True - rv = app.test_client().get("/foo/index.html") - assert rv.status_code == 200 - rv.close() - - with app.test_request_context(): - assert flask.url_for("static", filename="index.html") == "/foo/index.html" - - -def test_static_url_path_with_ending_slash(): - app = flask.Flask(__name__, static_url_path="/foo/") - app.testing = True - rv = app.test_client().get("/foo/index.html") - assert rv.status_code == 200 - rv.close() - - with app.test_request_context(): - assert flask.url_for("static", filename="index.html") == "/foo/index.html" - - -def test_static_url_empty_path(app): - app = flask.Flask(__name__, static_folder="", static_url_path="") - rv = app.test_client().open("/static/index.html", method="GET") - assert rv.status_code == 200 - rv.close() - - -def test_static_url_empty_path_default(app): - app = flask.Flask(__name__, static_folder="") - rv = app.test_client().open("/static/index.html", method="GET") - assert rv.status_code == 200 - rv.close() - - -def test_static_folder_with_pathlib_path(app): - from pathlib import Path - - app = flask.Flask(__name__, static_folder=Path("static")) - rv = app.test_client().open("/static/index.html", method="GET") - assert rv.status_code == 200 - rv.close() - - -def test_static_folder_with_ending_slash(): - app = flask.Flask(__name__, static_folder="static/") - - @app.route("/") - def catch_all(path): - return path - - rv = app.test_client().get("/catch/all") - assert rv.data == b"catch/all" - - -def test_static_route_with_host_matching(): - app = flask.Flask(__name__, host_matching=True, static_host="example.com") - c = app.test_client() - rv = c.get("http://example.com/static/index.html") - assert rv.status_code == 200 - rv.close() - with app.test_request_context(): - rv = flask.url_for("static", filename="index.html", _external=True) - assert rv == "http://example.com/static/index.html" - # Providing static_host without host_matching=True should error. - with pytest.raises(AssertionError): - flask.Flask(__name__, static_host="example.com") - # Providing host_matching=True with static_folder - # but without static_host should error. - with pytest.raises(AssertionError): - flask.Flask(__name__, host_matching=True) - # Providing host_matching=True without static_host - # but with static_folder=None should not error. - flask.Flask(__name__, host_matching=True, static_folder=None) - - -def test_request_locals(): - assert repr(flask.g) == "" - assert not flask.g - - -def test_server_name_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - client = app.test_client() - - @app.route("/") - def index(): - return "default" - - @app.route("/", subdomain="foo") - def subdomain(): - return "subdomain" - - app.config["SERVER_NAME"] = "dev.local:5000" - rv = client.get("/") - assert rv.data == b"default" - - rv = client.get("/", "http://dev.local:5000") - assert rv.data == b"default" - - rv = client.get("/", "https://dev.local:5000") - assert rv.data == b"default" - - app.config["SERVER_NAME"] = "dev.local:443" - rv = client.get("/", "https://dev.local") - - # Werkzeug 1.0 fixes matching https scheme with 443 port - if rv.status_code != 404: - assert rv.data == b"default" - - app.config["SERVER_NAME"] = "dev.local" - rv = client.get("/", "https://dev.local") - assert rv.data == b"default" - - # suppress Werkzeug 0.15 warning about name mismatch - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "Current server name", UserWarning, "flask.app" - ) - rv = client.get("/", "http://foo.localhost") - assert rv.status_code == 404 - - rv = client.get("/", "http://foo.dev.local") - assert rv.data == b"subdomain" - - -@pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) -def test_exception_propagation(app, client, key): - app.testing = False - - @app.route("/") - def index(): - raise ZeroDivisionError - - if key is not None: - app.config[key] = True - - with pytest.raises(ZeroDivisionError): - client.get("/") - else: - assert client.get("/").status_code == 500 - - -@pytest.mark.parametrize("debug", [True, False]) -@pytest.mark.parametrize("use_debugger", [True, False]) -@pytest.mark.parametrize("use_reloader", [True, False]) -@pytest.mark.parametrize("propagate_exceptions", [None, True, False]) -def test_werkzeug_passthrough_errors( - monkeypatch, debug, use_debugger, use_reloader, propagate_exceptions, app -): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(*args, **kwargs): - rv["passthrough_errors"] = kwargs.get("passthrough_errors") - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.config["PROPAGATE_EXCEPTIONS"] = propagate_exceptions - app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader) - - -def test_max_content_length(app, client): - app.config["MAX_CONTENT_LENGTH"] = 64 - - @app.before_request - def always_first(): - flask.request.form["myfile"] - AssertionError() - - @app.route("/accept", methods=["POST"]) - def accept_file(): - flask.request.form["myfile"] - AssertionError() - - @app.errorhandler(413) - def catcher(error): - return "42" - - rv = client.post("/accept", data={"myfile": "foo" * 100}) - assert rv.data == b"42" - - -def test_url_processors(app, client): - @app.url_defaults - def add_language_code(endpoint, values): - if flask.g.lang_code is not None and app.url_map.is_endpoint_expecting( - endpoint, "lang_code" - ): - values.setdefault("lang_code", flask.g.lang_code) - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code", None) - - @app.route("//") - def index(): - return flask.url_for("about") - - @app.route("//about") - def about(): - return flask.url_for("something_else") - - @app.route("/foo") - def something_else(): - return flask.url_for("about", lang_code="en") - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/foo" - assert client.get("/foo").data == b"/en/about" - - -def test_inject_blueprint_url_defaults(app): - bp = flask.Blueprint("foo", __name__, template_folder="template") - - @bp.url_defaults - def bp_defaults(endpoint, values): - values["page"] = "login" - - @bp.route("/") - def view(page): - pass - - app.register_blueprint(bp) - - values = dict() - app.inject_url_defaults("foo.view", values) - expected = dict(page="login") - assert values == expected - - with app.test_request_context("/somepage"): - url = flask.url_for("foo.view") - expected = "/login" - assert url == expected - - -def test_nonascii_pathinfo(app, client): - @app.route("/киртест") - def index(): - return "Hello World!" - - rv = client.get("/киртест") - assert rv.data == b"Hello World!" - - -def test_no_setup_after_first_request(app, client): - app.debug = True - - @app.route("/") - def index(): - return "Awesome" - - assert client.get("/").data == b"Awesome" - - with pytest.raises(AssertionError) as exc_info: - app.add_url_rule("/foo", endpoint="late") - - assert "setup method 'add_url_rule'" in str(exc_info.value) - - -def test_routing_redirect_debugging(monkeypatch, app, client): - app.config["DEBUG"] = True - - @app.route("/user/", methods=["GET", "POST"]) - def user(): - return flask.request.form["status"] - - # default redirect code preserves form data - rv = client.post("/user", data={"status": "success"}, follow_redirects=True) - assert rv.data == b"success" - - # 301 and 302 raise error - monkeypatch.setattr(RequestRedirect, "code", 301) - - with client, pytest.raises(AssertionError) as exc_info: - client.post("/user", data={"status": "error"}, follow_redirects=True) - - assert "canonical URL 'http://localhost/user/'" in str(exc_info.value) - - -def test_route_decorator_custom_endpoint(app, client): - app.debug = True - - @app.route("/foo/") - def foo(): - return flask.request.endpoint - - @app.route("/bar/", endpoint="bar") - def for_bar(): - return flask.request.endpoint - - @app.route("/bar/123", endpoint="123") - def for_bar_foo(): - return flask.request.endpoint - - with app.test_request_context(): - assert flask.url_for("foo") == "/foo/" - assert flask.url_for("bar") == "/bar/" - assert flask.url_for("123") == "/bar/123" - - assert client.get("/foo/").data == b"foo" - assert client.get("/bar/").data == b"bar" - assert client.get("/bar/123").data == b"123" - - -def test_get_method_on_g(app_ctx): - assert flask.g.get("x") is None - assert flask.g.get("x", 11) == 11 - flask.g.x = 42 - assert flask.g.get("x") == 42 - assert flask.g.x == 42 - - -def test_g_iteration_protocol(app_ctx): - flask.g.foo = 23 - flask.g.bar = 42 - assert "foo" in flask.g - assert "foos" not in flask.g - assert sorted(flask.g) == ["bar", "foo"] - - -def test_subdomain_basic_support(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "localhost.localdomain" - client = app.test_client() - - @app.route("/") - def normal_index(): - return "normal index" - - @app.route("/", subdomain="test") - def test_index(): - return "test index" - - rv = client.get("/", "http://localhost.localdomain/") - assert rv.data == b"normal index" - - rv = client.get("/", "http://test.localhost.localdomain/") - assert rv.data == b"test index" - - -def test_subdomain_matching(): - app = flask.Flask(__name__, subdomain_matching=True) - client = app.test_client() - app.config["SERVER_NAME"] = "localhost.localdomain" - - @app.route("/", subdomain="") - def index(user): - return f"index for {user}" - - rv = client.get("/", "http://mitsuhiko.localhost.localdomain/") - assert rv.data == b"index for mitsuhiko" - - -def test_subdomain_matching_with_ports(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "localhost.localdomain:3000" - client = app.test_client() - - @app.route("/", subdomain="") - def index(user): - return f"index for {user}" - - rv = client.get("/", "http://mitsuhiko.localhost.localdomain:3000/") - assert rv.data == b"index for mitsuhiko" - - -@pytest.mark.parametrize("matching", (False, True)) -def test_subdomain_matching_other_name(matching): - app = flask.Flask(__name__, subdomain_matching=matching) - app.config["SERVER_NAME"] = "localhost.localdomain:3000" - client = app.test_client() - - @app.route("/") - def index(): - return "", 204 - - # suppress Werkzeug 0.15 warning about name mismatch - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "Current server name", UserWarning, "flask.app" - ) - # ip address can't match name - rv = client.get("/", "http://127.0.0.1:3000/") - assert rv.status_code == 404 if matching else 204 - - # allow all subdomains if matching is disabled - rv = client.get("/", "http://www.localhost.localdomain:3000/") - assert rv.status_code == 404 if matching else 204 - - -def test_multi_route_rules(app, client): - @app.route("/") - @app.route("//") - def index(test="a"): - return test - - rv = client.open("/") - assert rv.data == b"a" - rv = client.open("/b/") - assert rv.data == b"b" - - -def test_multi_route_class_views(app, client): - class View: - def __init__(self, app): - app.add_url_rule("/", "index", self.index) - app.add_url_rule("//", "index", self.index) - - def index(self, test="a"): - return test - - _ = View(app) - rv = client.open("/") - assert rv.data == b"a" - rv = client.open("/b/") - assert rv.data == b"b" - - -def test_run_defaults(monkeypatch, app): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(*args, **kwargs): - rv["result"] = "running..." - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.run() - assert rv["result"] == "running..." - - -def test_run_server_port(monkeypatch, app): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(hostname, port, application, *args, **kwargs): - rv["result"] = f"running on {hostname}:{port} ..." - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - hostname, port = "localhost", 8000 - app.run(hostname, port, debug=True) - assert rv["result"] == f"running on {hostname}:{port} ..." - - -@pytest.mark.parametrize( - "host,port,server_name,expect_host,expect_port", - ( - (None, None, "pocoo.org:8080", "pocoo.org", 8080), - ("localhost", None, "pocoo.org:8080", "localhost", 8080), - (None, 80, "pocoo.org:8080", "pocoo.org", 80), - ("localhost", 80, "pocoo.org:8080", "localhost", 80), - ("localhost", 0, "localhost:8080", "localhost", 0), - (None, None, "localhost:8080", "localhost", 8080), - (None, None, "localhost:0", "localhost", 0), - ), -) -def test_run_from_config( - monkeypatch, host, port, server_name, expect_host, expect_port, app -): - def run_simple_mock(hostname, port, *args, **kwargs): - assert hostname == expect_host - assert port == expect_port - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.config["SERVER_NAME"] = server_name - app.run(host, port) - - -def test_max_cookie_size(app, client, recwarn): - app.config["MAX_COOKIE_SIZE"] = 100 - - # outside app context, default to Werkzeug static value, - # which is also the default config - response = flask.Response() - default = flask.Flask.default_config["MAX_COOKIE_SIZE"] - assert response.max_cookie_size == default - - # inside app context, use app config - with app.app_context(): - assert flask.Response().max_cookie_size == 100 - - @app.route("/") - def index(): - r = flask.Response("", status=204) - r.set_cookie("foo", "bar" * 100) - return r - - client.get("/") - assert len(recwarn) == 1 - w = recwarn.pop() - assert "cookie is too large" in str(w.message) - - app.config["MAX_COOKIE_SIZE"] = 0 - - client.get("/") - assert len(recwarn) == 0 - - -@require_cpython_gc -def test_app_freed_on_zero_refcount(): - # A Flask instance should not create a reference cycle that prevents CPython - # from freeing it when all external references to it are released (see #3761). - gc.disable() - try: - app = flask.Flask(__name__) - assert app.view_functions["static"] - weak = weakref.ref(app) - assert weak() is not None - del app - assert weak() is None - finally: - gc.enable() diff --git a/examples/tutorial/tests/test_blog.py b/tests/test_blog.py similarity index 100% rename from examples/tutorial/tests/test_blog.py rename to tests/test_blog.py diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py deleted file mode 100644 index 69bc71ad..00000000 --- a/tests/test_blueprints.py +++ /dev/null @@ -1,1054 +0,0 @@ -import pytest -from jinja2 import TemplateNotFound -from werkzeug.http import parse_cache_control_header - -import flask - - -def test_blueprint_specific_error_handling(app, client): - frontend = flask.Blueprint("frontend", __name__) - backend = flask.Blueprint("backend", __name__) - sideend = flask.Blueprint("sideend", __name__) - - @frontend.errorhandler(403) - def frontend_forbidden(e): - return "frontend says no", 403 - - @frontend.route("/frontend-no") - def frontend_no(): - flask.abort(403) - - @backend.errorhandler(403) - def backend_forbidden(e): - return "backend says no", 403 - - @backend.route("/backend-no") - def backend_no(): - flask.abort(403) - - @sideend.route("/what-is-a-sideend") - def sideend_no(): - flask.abort(403) - - app.register_blueprint(frontend) - app.register_blueprint(backend) - app.register_blueprint(sideend) - - @app.errorhandler(403) - def app_forbidden(e): - return "application itself says no", 403 - - assert client.get("/frontend-no").data == b"frontend says no" - assert client.get("/backend-no").data == b"backend says no" - assert client.get("/what-is-a-sideend").data == b"application itself says no" - - -def test_blueprint_specific_user_error_handling(app, client): - class MyDecoratorException(Exception): - pass - - class MyFunctionException(Exception): - pass - - blue = flask.Blueprint("blue", __name__) - - @blue.errorhandler(MyDecoratorException) - def my_decorator_exception_handler(e): - assert isinstance(e, MyDecoratorException) - return "boom" - - def my_function_exception_handler(e): - assert isinstance(e, MyFunctionException) - return "bam" - - blue.register_error_handler(MyFunctionException, my_function_exception_handler) - - @blue.route("/decorator") - def blue_deco_test(): - raise MyDecoratorException() - - @blue.route("/function") - def blue_func_test(): - raise MyFunctionException() - - app.register_blueprint(blue) - - assert client.get("/decorator").data == b"boom" - assert client.get("/function").data == b"bam" - - -def test_blueprint_app_error_handling(app, client): - errors = flask.Blueprint("errors", __name__) - - @errors.app_errorhandler(403) - def forbidden_handler(e): - return "you shall not pass", 403 - - @app.route("/forbidden") - def app_forbidden(): - flask.abort(403) - - forbidden_bp = flask.Blueprint("forbidden_bp", __name__) - - @forbidden_bp.route("/nope") - def bp_forbidden(): - flask.abort(403) - - app.register_blueprint(errors) - app.register_blueprint(forbidden_bp) - - assert client.get("/forbidden").data == b"you shall not pass" - assert client.get("/nope").data == b"you shall not pass" - - -@pytest.mark.parametrize( - ("prefix", "rule", "url"), - ( - ("", "/", "/"), - ("/", "", "/"), - ("/", "/", "/"), - ("/foo", "", "/foo"), - ("/foo/", "", "/foo/"), - ("", "/bar", "/bar"), - ("/foo/", "/bar", "/foo/bar"), - ("/foo/", "bar", "/foo/bar"), - ("/foo", "/bar", "/foo/bar"), - ("/foo/", "//bar", "/foo/bar"), - ("/foo//", "/bar", "/foo/bar"), - ), -) -def test_blueprint_prefix_slash(app, client, prefix, rule, url): - bp = flask.Blueprint("test", __name__, url_prefix=prefix) - - @bp.route(rule) - def index(): - return "", 204 - - app.register_blueprint(bp) - assert client.get(url).status_code == 204 - - -def test_blueprint_url_defaults(app, client): - bp = flask.Blueprint("test", __name__) - - @bp.route("/foo", defaults={"baz": 42}) - def foo(bar, baz): - return f"{bar}/{baz:d}" - - @bp.route("/bar") - def bar(bar): - return str(bar) - - app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23}) - app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19}) - - assert client.get("/1/foo").data == b"23/42" - assert client.get("/2/foo").data == b"19/42" - assert client.get("/1/bar").data == b"23" - assert client.get("/2/bar").data == b"19" - - -def test_blueprint_url_processors(app, client): - bp = flask.Blueprint("frontend", __name__, url_prefix="/") - - @bp.url_defaults - def add_language_code(endpoint, values): - values.setdefault("lang_code", flask.g.lang_code) - - @bp.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code") - - @bp.route("/") - def index(): - return flask.url_for(".about") - - @bp.route("/about") - def about(): - return flask.url_for(".index") - - app.register_blueprint(bp) - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/de/" - - -def test_templates_and_static(test_apps): - from blueprintapp import app - - client = app.test_client() - - rv = client.get("/") - assert rv.data == b"Hello from the Frontend" - rv = client.get("/admin/") - assert rv.data == b"Hello from the Admin" - rv = client.get("/admin/index2") - assert rv.data == b"Hello from the Admin" - rv = client.get("/admin/static/test.txt") - assert rv.data.strip() == b"Admin File" - rv.close() - rv = client.get("/admin/static/css/test.css") - assert rv.data.strip() == b"/* nested file */" - rv.close() - - # try/finally, in case other tests use this app for Blueprint tests. - max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] - try: - expected_max_age = 3600 - if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age: - expected_max_age = 7200 - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age - rv = client.get("/admin/static/css/test.css") - cc = parse_cache_control_header(rv.headers["Cache-Control"]) - assert cc.max_age == expected_max_age - rv.close() - finally: - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default - - with app.test_request_context(): - assert ( - flask.url_for("admin.static", filename="test.txt") - == "/admin/static/test.txt" - ) - - with app.test_request_context(): - with pytest.raises(TemplateNotFound) as e: - flask.render_template("missing.html") - assert e.value.name == "missing.html" - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template("nested/nested.txt") == "I'm nested" - - -def test_default_static_max_age(app): - class MyBlueprint(flask.Blueprint): - def get_send_file_max_age(self, filename): - return 100 - - blueprint = MyBlueprint("blueprint", __name__, static_folder="static") - app.register_blueprint(blueprint) - - # try/finally, in case other tests use this app for Blueprint tests. - max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] - try: - with app.test_request_context(): - unexpected_max_age = 3600 - if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age: - unexpected_max_age = 7200 - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age - rv = blueprint.send_static_file("index.html") - cc = parse_cache_control_header(rv.headers["Cache-Control"]) - assert cc.max_age == 100 - rv.close() - finally: - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default - - -def test_templates_list(test_apps): - from blueprintapp import app - - templates = sorted(app.jinja_env.list_templates()) - assert templates == ["admin/index.html", "frontend/index.html"] - - -def test_dotted_name_not_allowed(app, client): - with pytest.raises(ValueError): - flask.Blueprint("app.ui", __name__) - - -def test_empty_name_not_allowed(app, client): - with pytest.raises(ValueError): - flask.Blueprint("", __name__) - - -def test_dotted_names_from_app(app, client): - test = flask.Blueprint("test", __name__) - - @app.route("/") - def app_index(): - return flask.url_for("test.index") - - @test.route("/test/") - def index(): - return flask.url_for("app_index") - - app.register_blueprint(test) - - rv = client.get("/") - assert rv.data == b"/test/" - - -def test_empty_url_defaults(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.route("/", defaults={"page": 1}) - @bp.route("/page/") - def something(page): - return str(page) - - app.register_blueprint(bp) - - assert client.get("/").data == b"1" - assert client.get("/page/2").data == b"2" - - -def test_route_decorator_custom_endpoint(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.route("/foo") - def foo(): - return flask.request.endpoint - - @bp.route("/bar", endpoint="bar") - def foo_bar(): - return flask.request.endpoint - - @bp.route("/bar/123", endpoint="123") - def foo_bar_foo(): - return flask.request.endpoint - - @bp.route("/bar/foo") - def bar_foo(): - return flask.request.endpoint - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.request.endpoint - - assert client.get("/").data == b"index" - assert client.get("/py/foo").data == b"bp.foo" - assert client.get("/py/bar").data == b"bp.bar" - assert client.get("/py/bar/123").data == b"bp.123" - assert client.get("/py/bar/foo").data == b"bp.bar_foo" - - -def test_route_decorator_custom_endpoint_with_dots(app, client): - bp = flask.Blueprint("bp", __name__) - - with pytest.raises(ValueError): - bp.route("/", endpoint="a.b")(lambda: "") - - with pytest.raises(ValueError): - bp.add_url_rule("/", endpoint="a.b") - - def view(): - return "" - - view.__name__ = "a.b" - - with pytest.raises(ValueError): - bp.add_url_rule("/", view_func=view) - - -def test_endpoint_decorator(app, client): - from werkzeug.routing import Rule - - app.url_map.add(Rule("/foo", endpoint="bar")) - - bp = flask.Blueprint("bp", __name__) - - @bp.endpoint("bar") - def foobar(): - return flask.request.endpoint - - app.register_blueprint(bp, url_prefix="/bp_prefix") - - assert client.get("/foo").data == b"bar" - assert client.get("/bp_prefix/bar").status_code == 404 - - -def test_template_filter(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def my_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_add_template_filter(app): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse) - app.register_blueprint(bp, url_prefix="/py") - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_template_filter_with_name(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter("strrev") - def my_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_add_template_filter_with_name(app): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse, "strrev") - app.register_blueprint(bp, url_prefix="/py") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_template_filter_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def super_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_after_route_with_template(app, client): - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def super_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def super_reverse(s): - return s[::-1] - - bp.add_app_template_filter(super_reverse) - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter("super_reverse") - def my_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse, "super_reverse") - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_test(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def is_boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - assert "is_boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["is_boolean"] == is_boolean - assert app.jinja_env.tests["is_boolean"](False) - - -def test_add_template_test(app): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean) - app.register_blueprint(bp, url_prefix="/py") - assert "is_boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["is_boolean"] == is_boolean - assert app.jinja_env.tests["is_boolean"](False) - - -def test_template_test_with_name(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_add_template_test_with_name(app): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean, "boolean") - app.register_blueprint(bp, url_prefix="/py") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_after_route_with_template(app, client): - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(boolean) - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean, "boolean") - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_context_processing(app, client): - answer_bp = flask.Blueprint("answer_bp", __name__) - - def template_string(): - return flask.render_template_string( - "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" - "{% if answer %}{{ answer }} is the answer.{% endif %}" - ) - - # App global context processor - @answer_bp.app_context_processor - def not_answer_context_processor(): - return {"notanswer": 43} - - # Blueprint local context processor - @answer_bp.context_processor - def answer_context_processor(): - return {"answer": 42} - - # Setup endpoints for testing - @answer_bp.route("/bp") - def bp_page(): - return template_string() - - @app.route("/") - def app_page(): - return template_string() - - # Register the blueprint - app.register_blueprint(answer_bp) - - app_page_bytes = client.get("/").data - answer_page_bytes = client.get("/bp").data - - assert b"43" in app_page_bytes - assert b"42" not in app_page_bytes - - assert b"42" in answer_page_bytes - assert b"43" in answer_page_bytes - - -def test_template_global(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_global() - def get_answer(): - return 42 - - # Make sure the function is not in the jinja_env already - assert "get_answer" not in app.jinja_env.globals.keys() - app.register_blueprint(bp) - - # Tests - assert "get_answer" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_answer"] is get_answer - assert app.jinja_env.globals["get_answer"]() == 42 - - with app.app_context(): - rv = flask.render_template_string("{{ get_answer() }}") - assert rv == "42" - - -def test_request_processing(app, client): - bp = flask.Blueprint("bp", __name__) - evts = [] - - @bp.before_request - def before_bp(): - evts.append("before") - - @bp.after_request - def after_bp(response): - response.data += b"|after" - evts.append("after") - return response - - @bp.teardown_request - def teardown_bp(exc): - evts.append("teardown") - - # Setup routes for testing - @bp.route("/bp") - def bp_endpoint(): - return "request" - - app.register_blueprint(bp) - - assert evts == [] - rv = client.get("/bp") - assert rv.data == b"request|after" - assert evts == ["before", "after", "teardown"] - - -def test_app_request_processing(app, client): - bp = flask.Blueprint("bp", __name__) - evts = [] - - @bp.before_app_request - def before_app(): - evts.append("before") - - @bp.after_app_request - def after_app(response): - response.data += b"|after" - evts.append("after") - return response - - @bp.teardown_app_request - def teardown_app(exc): - evts.append("teardown") - - app.register_blueprint(bp) - - # Setup routes for testing - @app.route("/") - def bp_endpoint(): - return "request" - - # before first request - assert evts == [] - - # first request - resp = client.get("/").data - assert resp == b"request|after" - assert evts == ["before", "after", "teardown"] - - # second request - resp = client.get("/").data - assert resp == b"request|after" - assert evts == ["before", "after", "teardown"] * 2 - - -def test_app_url_processors(app, client): - bp = flask.Blueprint("bp", __name__) - - # Register app-wide url defaults and preprocessor on blueprint - @bp.app_url_defaults - def add_language_code(endpoint, values): - values.setdefault("lang_code", flask.g.lang_code) - - @bp.app_url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code") - - # Register route rules at the app level - @app.route("//") - def index(): - return flask.url_for("about") - - @app.route("//about") - def about(): - return flask.url_for("index") - - app.register_blueprint(bp) - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/de/" - - -def test_nested_blueprint(app, client): - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - grandchild = flask.Blueprint("grandchild", __name__) - - @parent.errorhandler(403) - def forbidden(e): - return "Parent no", 403 - - @parent.route("/") - def parent_index(): - return "Parent yes" - - @parent.route("/no") - def parent_no(): - flask.abort(403) - - @child.route("/") - def child_index(): - return "Child yes" - - @child.route("/no") - def child_no(): - flask.abort(403) - - @grandchild.errorhandler(403) - def grandchild_forbidden(e): - return "Grandchild no", 403 - - @grandchild.route("/") - def grandchild_index(): - return "Grandchild yes" - - @grandchild.route("/no") - def grandchild_no(): - flask.abort(403) - - child.register_blueprint(grandchild, url_prefix="/grandchild") - parent.register_blueprint(child, url_prefix="/child") - app.register_blueprint(parent, url_prefix="/parent") - - assert client.get("/parent/").data == b"Parent yes" - assert client.get("/parent/child/").data == b"Child yes" - assert client.get("/parent/child/grandchild/").data == b"Grandchild yes" - assert client.get("/parent/no").data == b"Parent no" - assert client.get("/parent/child/no").data == b"Parent no" - assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" - - -def test_nested_callback_order(app, client): - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - - @app.before_request - def app_before1(): - flask.g.setdefault("seen", []).append("app_1") - - @app.teardown_request - def app_teardown1(e=None): - assert flask.g.seen.pop() == "app_1" - - @app.before_request - def app_before2(): - flask.g.setdefault("seen", []).append("app_2") - - @app.teardown_request - def app_teardown2(e=None): - assert flask.g.seen.pop() == "app_2" - - @app.context_processor - def app_ctx(): - return dict(key="app") - - @parent.before_request - def parent_before1(): - flask.g.setdefault("seen", []).append("parent_1") - - @parent.teardown_request - def parent_teardown1(e=None): - assert flask.g.seen.pop() == "parent_1" - - @parent.before_request - def parent_before2(): - flask.g.setdefault("seen", []).append("parent_2") - - @parent.teardown_request - def parent_teardown2(e=None): - assert flask.g.seen.pop() == "parent_2" - - @parent.context_processor - def parent_ctx(): - return dict(key="parent") - - @child.before_request - def child_before1(): - flask.g.setdefault("seen", []).append("child_1") - - @child.teardown_request - def child_teardown1(e=None): - assert flask.g.seen.pop() == "child_1" - - @child.before_request - def child_before2(): - flask.g.setdefault("seen", []).append("child_2") - - @child.teardown_request - def child_teardown2(e=None): - assert flask.g.seen.pop() == "child_2" - - @child.context_processor - def child_ctx(): - return dict(key="child") - - @child.route("/a") - def a(): - return ", ".join(flask.g.seen) - - @child.route("/b") - def b(): - return flask.render_template_string("{{ key }}") - - parent.register_blueprint(child) - app.register_blueprint(parent) - assert ( - client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2" - ) - assert client.get("/b").data == b"child" - - -@pytest.mark.parametrize( - "parent_init, child_init, parent_registration, child_registration", - [ - ("/parent", "/child", None, None), - ("/parent", None, None, "/child"), - (None, None, "/parent", "/child"), - ("/other", "/something", "/parent", "/child"), - ], -) -def test_nesting_url_prefixes( - parent_init, - child_init, - parent_registration, - child_registration, - app, - client, -) -> None: - parent = flask.Blueprint("parent", __name__, url_prefix=parent_init) - child = flask.Blueprint("child", __name__, url_prefix=child_init) - - @child.route("/") - def index(): - return "index" - - parent.register_blueprint(child, url_prefix=child_registration) - app.register_blueprint(parent, url_prefix=parent_registration) - - response = client.get("/parent/child/") - assert response.status_code == 200 - - -def test_nesting_subdomains(app, client) -> None: - subdomain = "api" - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - - @child.route("/child/") - def index(): - return "child" - - parent.register_blueprint(child) - app.register_blueprint(parent, subdomain=subdomain) - - client.allow_subdomain_redirects = True - - domain_name = "domain.tld" - app.config["SERVER_NAME"] = domain_name - response = client.get("/child/", base_url="http://api." + domain_name) - - assert response.status_code == 200 - - -def test_child_and_parent_subdomain(app, client) -> None: - child_subdomain = "api" - parent_subdomain = "parent" - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__, subdomain=child_subdomain) - - @child.route("/") - def index(): - return "child" - - parent.register_blueprint(child) - app.register_blueprint(parent, subdomain=parent_subdomain) - - client.allow_subdomain_redirects = True - - domain_name = "domain.tld" - app.config["SERVER_NAME"] = domain_name - response = client.get( - "/", base_url=f"http://{child_subdomain}.{parent_subdomain}.{domain_name}" - ) - - assert response.status_code == 200 - - response = client.get("/", base_url=f"http://{parent_subdomain}.{domain_name}") - - assert response.status_code == 404 - - -def test_unique_blueprint_names(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - bp2 = flask.Blueprint("bp", __name__) - - app.register_blueprint(bp) - - with pytest.raises(ValueError): - app.register_blueprint(bp) # same bp, same name, error - - app.register_blueprint(bp, name="again") # same bp, different name, ok - - with pytest.raises(ValueError): - app.register_blueprint(bp2) # different bp, same name, error - - app.register_blueprint(bp2, name="alt") # different bp, different name, ok - - -def test_self_registration(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - with pytest.raises(ValueError): - bp.register_blueprint(bp) - - -def test_blueprint_renaming(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - bp2 = flask.Blueprint("bp2", __name__) - - @bp.get("/") - def index(): - return flask.request.endpoint - - @bp.get("/error") - def error(): - flask.abort(403) - - @bp.errorhandler(403) - def forbidden(_: Exception): - return "Error", 403 - - @bp2.get("/") - def index2(): - return flask.request.endpoint - - bp.register_blueprint(bp2, url_prefix="/a", name="sub") - app.register_blueprint(bp, url_prefix="/a") - app.register_blueprint(bp, url_prefix="/b", name="alt") - - assert client.get("/a/").data == b"bp.index" - assert client.get("/b/").data == b"alt.index" - assert client.get("/a/a/").data == b"bp.sub.index2" - assert client.get("/b/a/").data == b"alt.sub.index2" - assert client.get("/a/error").data == b"Error" - assert client.get("/b/error").data == b"Error" diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 09995488..00000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,686 +0,0 @@ -# This file was part of Flask-CLI and was modified under the terms of -# its Revised BSD License. Copyright © 2015 CERN. -import importlib.metadata -import os -import platform -import ssl -import sys -import types -from functools import partial -from pathlib import Path - -import click -import pytest -from _pytest.monkeypatch import notset -from click.testing import CliRunner - -from flask import Blueprint -from flask import current_app -from flask import Flask -from flask.cli import AppGroup -from flask.cli import find_best_app -from flask.cli import FlaskGroup -from flask.cli import get_version -from flask.cli import load_dotenv -from flask.cli import locate_app -from flask.cli import NoAppException -from flask.cli import prepare_import -from flask.cli import run_command -from flask.cli import ScriptInfo -from flask.cli import with_appcontext - -cwd = Path.cwd() -test_path = (Path(__file__) / ".." / "test_apps").resolve() - - -@pytest.fixture -def runner(): - return CliRunner() - - -def test_cli_name(test_apps): - """Make sure the CLI object's name is the app's name and not the app itself""" - from cliapp.app import testapp - - assert testapp.cli.name == testapp.name - - -def test_find_best_app(test_apps): - class Module: - app = Flask("appname") - - assert find_best_app(Module) == Module.app - - class Module: - application = Flask("appname") - - assert find_best_app(Module) == Module.application - - class Module: - myapp = Flask("appname") - - assert find_best_app(Module) == Module.myapp - - class Module: - @staticmethod - def create_app(): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - @staticmethod - def create_app(**kwargs): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - @staticmethod - def make_app(): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - myapp = Flask("appname1") - - @staticmethod - def create_app(): - return Flask("appname2") - - assert find_best_app(Module) == Module.myapp - - class Module: - myapp = Flask("appname1") - - @staticmethod - def create_app(): - return Flask("appname2") - - assert find_best_app(Module) == Module.myapp - - class Module: - pass - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - myapp1 = Flask("appname1") - myapp2 = Flask("appname2") - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - @staticmethod - def create_app(foo, bar): - return Flask("appname2") - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - @staticmethod - def create_app(): - raise TypeError("bad bad factory!") - - pytest.raises(TypeError, find_best_app, Module) - - -@pytest.mark.parametrize( - "value,path,result", - ( - ("test", cwd, "test"), - ("test.py", cwd, "test"), - ("a/test", cwd / "a", "test"), - ("test/__init__.py", cwd, "test"), - ("test/__init__", cwd, "test"), - # nested package - ( - test_path / "cliapp" / "inner1" / "__init__", - test_path, - "cliapp.inner1", - ), - ( - test_path / "cliapp" / "inner1" / "inner2", - test_path, - "cliapp.inner1.inner2", - ), - # dotted name - ("test.a.b", cwd, "test.a.b"), - (test_path / "cliapp.app", test_path, "cliapp.app"), - # not a Python file, will be caught during import - (test_path / "cliapp" / "message.txt", test_path, "cliapp.message.txt"), - ), -) -def test_prepare_import(request, value, path, result): - """Expect the correct path to be set and the correct import and app names - to be returned. - - :func:`prepare_exec_for_file` has a side effect where the parent directory - of the given import is added to :data:`sys.path`. This is reset after the - test runs. - """ - original_path = sys.path[:] - - def reset_path(): - sys.path[:] = original_path - - request.addfinalizer(reset_path) - - assert prepare_import(value) == result - assert sys.path[0] == str(path) - - -@pytest.mark.parametrize( - "iname,aname,result", - ( - ("cliapp.app", None, "testapp"), - ("cliapp.app", "testapp", "testapp"), - ("cliapp.factory", None, "app"), - ("cliapp.factory", "create_app", "app"), - ("cliapp.factory", "create_app()", "app"), - ("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"), - # trailing comma space - ("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"), - # strip whitespace - ("cliapp.factory", " create_app () ", "app"), - ), -) -def test_locate_app(test_apps, iname, aname, result): - assert locate_app(iname, aname).name == result - - -@pytest.mark.parametrize( - "iname,aname", - ( - ("notanapp.py", None), - ("cliapp/app", None), - ("cliapp.app", "notanapp"), - # not enough arguments - ("cliapp.factory", 'create_app2("foo")'), - # invalid identifier - ("cliapp.factory", "create_app("), - # no app returned - ("cliapp.factory", "no_app"), - # nested import error - ("cliapp.importerrorapp", None), - # not a Python file - ("cliapp.message.txt", None), - ), -) -def test_locate_app_raises(test_apps, iname, aname): - with pytest.raises(NoAppException): - locate_app(iname, aname) - - -def test_locate_app_suppress_raise(test_apps): - app = locate_app("notanapp.py", None, raise_if_not_found=False) - assert app is None - - # only direct import error is suppressed - with pytest.raises(NoAppException): - locate_app("cliapp.importerrorapp", None, raise_if_not_found=False) - - -def test_get_version(test_apps, capsys): - class MockCtx: - resilient_parsing = False - color = None - - def exit(self): - return - - ctx = MockCtx() - get_version(ctx, None, "test") - out, err = capsys.readouterr() - assert f"Python {platform.python_version()}" in out - assert f"Flask {importlib.metadata.version('flask')}" in out - assert f"Werkzeug {importlib.metadata.version('werkzeug')}" in out - - -def test_scriptinfo(test_apps, monkeypatch): - obj = ScriptInfo(app_import_path="cliapp.app:testapp") - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - - # import app with module's absolute path - cli_app_path = str(test_path / "cliapp" / "app.py") - - obj = ScriptInfo(app_import_path=cli_app_path) - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp") - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - - def create_app(): - return Flask("createapp") - - obj = ScriptInfo(create_app=create_app) - app = obj.load_app() - assert app.name == "createapp" - assert obj.load_app() is app - - obj = ScriptInfo() - pytest.raises(NoAppException, obj.load_app) - - # import app from wsgi.py in current directory - monkeypatch.chdir(test_path / "helloworld") - obj = ScriptInfo() - app = obj.load_app() - assert app.name == "hello" - - # import app from app.py in current directory - monkeypatch.chdir(test_path / "cliapp") - obj = ScriptInfo() - app = obj.load_app() - assert app.name == "testapp" - - -def test_app_cli_has_app_context(app, runner): - def _param_cb(ctx, param, value): - # current_app should be available in parameter callbacks - return bool(current_app) - - @app.cli.command() - @click.argument("value", callback=_param_cb) - def check(value): - app = click.get_current_context().obj.load_app() - # the loaded app should be the same as current_app - same_app = current_app._get_current_object() is app - return same_app, value - - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["check", "x"], standalone_mode=False) - assert result.return_value == (True, True) - - -def test_with_appcontext(runner): - @click.command() - @with_appcontext - def testcmd(): - click.echo(current_app.name) - - obj = ScriptInfo(create_app=lambda: Flask("testapp")) - - result = runner.invoke(testcmd, obj=obj) - assert result.exit_code == 0 - assert result.output == "testapp\n" - - -def test_appgroup_app_context(runner): - @click.group(cls=AppGroup) - def cli(): - pass - - @cli.command() - def test(): - click.echo(current_app.name) - - @cli.group() - def subgroup(): - pass - - @subgroup.command() - def test2(): - click.echo(current_app.name) - - obj = ScriptInfo(create_app=lambda: Flask("testappgroup")) - - result = runner.invoke(cli, ["test"], obj=obj) - assert result.exit_code == 0 - assert result.output == "testappgroup\n" - - result = runner.invoke(cli, ["subgroup", "test2"], obj=obj) - assert result.exit_code == 0 - assert result.output == "testappgroup\n" - - -def test_flaskgroup_app_context(runner): - def create_app(): - return Flask("flaskgroup") - - @click.group(cls=FlaskGroup, create_app=create_app) - def cli(**params): - pass - - @cli.command() - def test(): - click.echo(current_app.name) - - result = runner.invoke(cli, ["test"]) - assert result.exit_code == 0 - assert result.output == "flaskgroup\n" - - -@pytest.mark.parametrize("set_debug_flag", (True, False)) -def test_flaskgroup_debug(runner, set_debug_flag): - def create_app(): - app = Flask("flaskgroup") - app.debug = True - return app - - @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag) - def cli(**params): - pass - - @cli.command() - def test(): - click.echo(str(current_app.debug)) - - result = runner.invoke(cli, ["test"]) - assert result.exit_code == 0 - assert result.output == f"{not set_debug_flag}\n" - - -def test_flaskgroup_nested(app, runner): - cli = click.Group("cli") - flask_group = FlaskGroup(name="flask", create_app=lambda: app) - cli.add_command(flask_group) - - @flask_group.command() - def show(): - click.echo(current_app.name) - - result = runner.invoke(cli, ["flask", "show"]) - assert result.output == "flask_test\n" - - -def test_no_command_echo_loading_error(): - from flask.cli import cli - - runner = CliRunner(mix_stderr=False) - result = runner.invoke(cli, ["missing"]) - assert result.exit_code == 2 - assert "FLASK_APP" in result.stderr - assert "Usage:" in result.stderr - - -def test_help_echo_loading_error(): - from flask.cli import cli - - runner = CliRunner(mix_stderr=False) - result = runner.invoke(cli, ["--help"]) - assert result.exit_code == 0 - assert "FLASK_APP" in result.stderr - assert "Usage:" in result.stdout - - -def test_help_echo_exception(): - def create_app(): - raise Exception("oh no") - - cli = FlaskGroup(create_app=create_app) - runner = CliRunner(mix_stderr=False) - result = runner.invoke(cli, ["--help"]) - assert result.exit_code == 0 - assert "Exception: oh no" in result.stderr - assert "Usage:" in result.stdout - - -class TestRoutes: - @pytest.fixture - def app(self): - app = Flask(__name__) - app.add_url_rule( - "/get_post//", - methods=["GET", "POST"], - endpoint="yyy_get_post", - ) - app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post") - return app - - @pytest.fixture - def invoke(self, app, runner): - cli = FlaskGroup(create_app=lambda: app) - return partial(runner.invoke, cli) - - def expect_order(self, order, output): - # skip the header and match the start of each row - for expect, line in zip(order, output.splitlines()[2:]): - # do this instead of startswith for nicer pytest output - assert line[: len(expect)] == expect - - def test_simple(self, invoke): - result = invoke(["routes"]) - assert result.exit_code == 0 - self.expect_order(["aaa_post", "static", "yyy_get_post"], result.output) - - def test_sort(self, app, invoke): - default_output = invoke(["routes"]).output - endpoint_output = invoke(["routes", "-s", "endpoint"]).output - assert default_output == endpoint_output - self.expect_order( - ["static", "yyy_get_post", "aaa_post"], - invoke(["routes", "-s", "methods"]).output, - ) - self.expect_order( - ["yyy_get_post", "static", "aaa_post"], - invoke(["routes", "-s", "rule"]).output, - ) - match_order = [r.endpoint for r in app.url_map.iter_rules()] - self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) - - def test_all_methods(self, invoke): - output = invoke(["routes"]).output - assert "GET, HEAD, OPTIONS, POST" not in output - output = invoke(["routes", "--all-methods"]).output - assert "GET, HEAD, OPTIONS, POST" in output - - def test_no_routes(self, runner): - app = Flask(__name__, static_folder=None) - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "No routes were registered." in result.output - - def test_subdomain(self, runner): - app = Flask(__name__, static_folder=None) - app.add_url_rule("/a", subdomain="a", endpoint="a") - app.add_url_rule("/b", subdomain="b", endpoint="b") - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "Subdomain" in result.output - - def test_host(self, runner): - app = Flask(__name__, static_folder=None, host_matching=True) - app.add_url_rule("/a", host="a", endpoint="a") - app.add_url_rule("/b", host="b", endpoint="b") - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "Host" in result.output - - -def dotenv_not_available(): - try: - import dotenv # noqa: F401 - except ImportError: - return True - - return False - - -need_dotenv = pytest.mark.skipif( - dotenv_not_available(), reason="dotenv is not installed" -) - - -@need_dotenv -def test_load_dotenv(monkeypatch): - # can't use monkeypatch.delitem since the keys don't exist yet - for item in ("FOO", "BAR", "SPAM", "HAM"): - monkeypatch._setitem.append((os.environ, item, notset)) - - monkeypatch.setenv("EGGS", "3") - monkeypatch.chdir(test_path) - assert load_dotenv() - assert Path.cwd() == test_path - # .flaskenv doesn't overwrite .env - assert os.environ["FOO"] == "env" - # set only in .flaskenv - assert os.environ["BAR"] == "bar" - # set only in .env - assert os.environ["SPAM"] == "1" - # set manually, files don't overwrite - assert os.environ["EGGS"] == "3" - # test env file encoding - assert os.environ["HAM"] == "火腿" - # Non existent file should not load - assert not load_dotenv("non-existent-file") - - -@need_dotenv -def test_dotenv_path(monkeypatch): - for item in ("FOO", "BAR", "EGGS"): - monkeypatch._setitem.append((os.environ, item, notset)) - - load_dotenv(test_path / ".flaskenv") - assert Path.cwd() == cwd - assert "FOO" in os.environ - - -def test_dotenv_optional(monkeypatch): - monkeypatch.setitem(sys.modules, "dotenv", None) - monkeypatch.chdir(test_path) - load_dotenv() - assert "FOO" not in os.environ - - -@need_dotenv -def test_disable_dotenv_from_env(monkeypatch, runner): - monkeypatch.chdir(test_path) - monkeypatch.setitem(os.environ, "FLASK_SKIP_DOTENV", "1") - runner.invoke(FlaskGroup()) - assert "FOO" not in os.environ - - -def test_run_cert_path(): - # no key - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", __file__]) - - # no cert - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--key", __file__]) - - # cert specified first - ctx = run_command.make_context("run", ["--cert", __file__, "--key", __file__]) - assert ctx.params["cert"] == (__file__, __file__) - - # key specified first - ctx = run_command.make_context("run", ["--key", __file__, "--cert", __file__]) - assert ctx.params["cert"] == (__file__, __file__) - - -def test_run_cert_adhoc(monkeypatch): - monkeypatch.setitem(sys.modules, "cryptography", None) - - # cryptography not installed - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "adhoc"]) - - # cryptography installed - monkeypatch.setitem(sys.modules, "cryptography", types.ModuleType("cryptography")) - ctx = run_command.make_context("run", ["--cert", "adhoc"]) - assert ctx.params["cert"] == "adhoc" - - # no key with adhoc - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "adhoc", "--key", __file__]) - - -def test_run_cert_import(monkeypatch): - monkeypatch.setitem(sys.modules, "not_here", None) - - # ImportError - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "not_here"]) - - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "flask"]) - - # SSLContext - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - - monkeypatch.setitem(sys.modules, "ssl_context", ssl_context) - ctx = run_command.make_context("run", ["--cert", "ssl_context"]) - assert ctx.params["cert"] is ssl_context - - # no --key with SSLContext - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__]) - - -def test_run_cert_no_ssl(monkeypatch): - monkeypatch.setitem(sys.modules, "ssl", None) - - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "not_here"]) - - -def test_cli_blueprints(app): - """Test blueprint commands register correctly to the application""" - custom = Blueprint("custom", __name__, cli_group="customized") - nested = Blueprint("nested", __name__) - merged = Blueprint("merged", __name__, cli_group=None) - late = Blueprint("late", __name__) - - @custom.cli.command("custom") - def custom_command(): - click.echo("custom_result") - - @nested.cli.command("nested") - def nested_command(): - click.echo("nested_result") - - @merged.cli.command("merged") - def merged_command(): - click.echo("merged_result") - - @late.cli.command("late") - def late_command(): - click.echo("late_result") - - app.register_blueprint(custom) - app.register_blueprint(nested) - app.register_blueprint(merged) - app.register_blueprint(late, cli_group="late_registration") - - app_runner = app.test_cli_runner() - - result = app_runner.invoke(args=["customized", "custom"]) - assert "custom_result" in result.output - - result = app_runner.invoke(args=["nested", "nested"]) - assert "nested_result" in result.output - - result = app_runner.invoke(args=["merged"]) - assert "merged_result" in result.output - - result = app_runner.invoke(args=["late_registration", "late"]) - assert "late_result" in result.output - - -def test_cli_empty(app): - """If a Blueprint's CLI group is empty, do not register it.""" - bp = Blueprint("blue", __name__, cli_group="blue") - app.register_blueprint(bp) - - result = app.test_cli_runner().invoke(args=["blue", "--help"]) - assert result.exit_code == 2, f"Unexpected success:\n\n{result.output}" - - -def test_run_exclude_patterns(): - ctx = run_command.make_context("run", ["--exclude-patterns", __file__]) - assert ctx.params["exclude_patterns"] == [__file__] diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index e5b1906e..00000000 --- a/tests/test_config.py +++ /dev/null @@ -1,250 +0,0 @@ -import json -import os - -import pytest - -import flask - -# config keys used for the TestConfig -TEST_KEY = "foo" -SECRET_KEY = "config" - - -def common_object_test(app): - assert app.secret_key == "config" - assert app.config["TEST_KEY"] == "foo" - assert "TestConfig" not in app.config - - -def test_config_from_pyfile(): - app = flask.Flask(__name__) - app.config.from_pyfile(f"{__file__.rsplit('.', 1)[0]}.py") - common_object_test(app) - - -def test_config_from_object(): - app = flask.Flask(__name__) - app.config.from_object(__name__) - common_object_test(app) - - -def test_config_from_file_json(): - app = flask.Flask(__name__) - current_dir = os.path.dirname(os.path.abspath(__file__)) - app.config.from_file(os.path.join(current_dir, "static", "config.json"), json.load) - common_object_test(app) - - -def test_config_from_file_toml(): - tomllib = pytest.importorskip("tomllib", reason="tomllib added in 3.11") - app = flask.Flask(__name__) - current_dir = os.path.dirname(os.path.abspath(__file__)) - app.config.from_file( - os.path.join(current_dir, "static", "config.toml"), tomllib.load, text=False - ) - common_object_test(app) - - -def test_from_prefixed_env(monkeypatch): - monkeypatch.setenv("FLASK_STRING", "value") - monkeypatch.setenv("FLASK_BOOL", "true") - monkeypatch.setenv("FLASK_INT", "1") - monkeypatch.setenv("FLASK_FLOAT", "1.2") - monkeypatch.setenv("FLASK_LIST", "[1, 2]") - monkeypatch.setenv("FLASK_DICT", '{"k": "v"}') - monkeypatch.setenv("NOT_FLASK_OTHER", "other") - - app = flask.Flask(__name__) - app.config.from_prefixed_env() - - assert app.config["STRING"] == "value" - assert app.config["BOOL"] is True - assert app.config["INT"] == 1 - assert app.config["FLOAT"] == 1.2 - assert app.config["LIST"] == [1, 2] - assert app.config["DICT"] == {"k": "v"} - assert "OTHER" not in app.config - - -def test_from_prefixed_env_custom_prefix(monkeypatch): - monkeypatch.setenv("FLASK_A", "a") - monkeypatch.setenv("NOT_FLASK_A", "b") - - app = flask.Flask(__name__) - app.config.from_prefixed_env("NOT_FLASK") - - assert app.config["A"] == "b" - - -def test_from_prefixed_env_nested(monkeypatch): - monkeypatch.setenv("FLASK_EXIST__ok", "other") - monkeypatch.setenv("FLASK_EXIST__inner__ik", "2") - monkeypatch.setenv("FLASK_EXIST__new__more", '{"k": false}') - monkeypatch.setenv("FLASK_NEW__K", "v") - - app = flask.Flask(__name__) - app.config["EXIST"] = {"ok": "value", "flag": True, "inner": {"ik": 1}} - app.config.from_prefixed_env() - - if os.name != "nt": - assert app.config["EXIST"] == { - "ok": "other", - "flag": True, - "inner": {"ik": 2}, - "new": {"more": {"k": False}}, - } - else: - # Windows env var keys are always uppercase. - assert app.config["EXIST"] == { - "ok": "value", - "OK": "other", - "flag": True, - "inner": {"ik": 1}, - "INNER": {"IK": 2}, - "NEW": {"MORE": {"k": False}}, - } - - assert app.config["NEW"] == {"K": "v"} - - -def test_config_from_mapping(): - app = flask.Flask(__name__) - app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"}) - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping([("SECRET_KEY", "config"), ("TEST_KEY", "foo")]) - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo") - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip") - common_object_test(app) - - app = flask.Flask(__name__) - with pytest.raises(TypeError): - app.config.from_mapping({}, {}) - - -def test_config_from_class(): - class Base: - TEST_KEY = "foo" - - class Test(Base): - SECRET_KEY = "config" - - app = flask.Flask(__name__) - app.config.from_object(Test) - common_object_test(app) - - -def test_config_from_envvar(monkeypatch): - monkeypatch.setattr("os.environ", {}) - app = flask.Flask(__name__) - - with pytest.raises(RuntimeError) as e: - app.config.from_envvar("FOO_SETTINGS") - - assert "'FOO_SETTINGS' is not set" in str(e.value) - assert not app.config.from_envvar("FOO_SETTINGS", silent=True) - - monkeypatch.setattr( - "os.environ", {"FOO_SETTINGS": f"{__file__.rsplit('.', 1)[0]}.py"} - ) - assert app.config.from_envvar("FOO_SETTINGS") - common_object_test(app) - - -def test_config_from_envvar_missing(monkeypatch): - monkeypatch.setattr("os.environ", {"FOO_SETTINGS": "missing.cfg"}) - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_envvar("FOO_SETTINGS") - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.cfg'") - assert not app.config.from_envvar("FOO_SETTINGS", silent=True) - - -def test_config_missing(): - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_pyfile("missing.cfg") - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.cfg'") - assert not app.config.from_pyfile("missing.cfg", silent=True) - - -def test_config_missing_file(): - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_file("missing.json", load=json.load) - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.json'") - assert not app.config.from_file("missing.json", load=json.load, silent=True) - - -def test_custom_config_class(): - class Config(flask.Config): - pass - - class Flask(flask.Flask): - config_class = Config - - app = Flask(__name__) - assert isinstance(app.config, Config) - app.config.from_object(__name__) - common_object_test(app) - - -def test_session_lifetime(): - app = flask.Flask(__name__) - app.config["PERMANENT_SESSION_LIFETIME"] = 42 - assert app.permanent_session_lifetime.seconds == 42 - - -def test_get_namespace(): - app = flask.Flask(__name__) - app.config["FOO_OPTION_1"] = "foo option 1" - app.config["FOO_OPTION_2"] = "foo option 2" - app.config["BAR_STUFF_1"] = "bar stuff 1" - app.config["BAR_STUFF_2"] = "bar stuff 2" - foo_options = app.config.get_namespace("FOO_") - assert 2 == len(foo_options) - assert "foo option 1" == foo_options["option_1"] - assert "foo option 2" == foo_options["option_2"] - bar_options = app.config.get_namespace("BAR_", lowercase=False) - assert 2 == len(bar_options) - assert "bar stuff 1" == bar_options["STUFF_1"] - assert "bar stuff 2" == bar_options["STUFF_2"] - foo_options = app.config.get_namespace("FOO_", trim_namespace=False) - assert 2 == len(foo_options) - assert "foo option 1" == foo_options["foo_option_1"] - assert "foo option 2" == foo_options["foo_option_2"] - bar_options = app.config.get_namespace( - "BAR_", lowercase=False, trim_namespace=False - ) - assert 2 == len(bar_options) - assert "bar stuff 1" == bar_options["BAR_STUFF_1"] - assert "bar stuff 2" == bar_options["BAR_STUFF_2"] - - -@pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-15", "latin-1"]) -def test_from_pyfile_weird_encoding(tmp_path, encoding): - f = tmp_path / "my_config.py" - f.write_text(f'# -*- coding: {encoding} -*-\nTEST_VALUE = "föö"\n', encoding) - app = flask.Flask(__name__) - app.config.from_pyfile(os.fspath(f)) - value = app.config["TEST_VALUE"] - assert value == "föö" diff --git a/tests/test_converters.py b/tests/test_converters.py deleted file mode 100644 index d94a7658..00000000 --- a/tests/test_converters.py +++ /dev/null @@ -1,42 +0,0 @@ -from werkzeug.routing import BaseConverter - -from flask import request -from flask import session -from flask import url_for - - -def test_custom_converters(app, client): - class ListConverter(BaseConverter): - def to_python(self, value): - return value.split(",") - - def to_url(self, value): - base_to_url = super().to_url - return ",".join(base_to_url(x) for x in value) - - app.url_map.converters["list"] = ListConverter - - @app.route("/") - def index(args): - return "|".join(args) - - assert client.get("/1,2,3").data == b"1|2|3" - - with app.test_request_context(): - assert url_for("index", args=[4, 5, 6]) == "/4,5,6" - - -def test_context_available(app, client): - class ContextConverter(BaseConverter): - def to_python(self, value): - assert request is not None - assert session is not None - return value - - app.url_map.converters["ctx"] = ContextConverter - - @app.get("/") - def index(name): - return name - - assert client.get("/admin").data == b"admin" diff --git a/examples/tutorial/tests/test_db.py b/tests/test_db.py similarity index 100% rename from examples/tutorial/tests/test_db.py rename to tests/test_db.py diff --git a/examples/tutorial/tests/test_factory.py b/tests/test_factory.py similarity index 100% rename from examples/tutorial/tests/test_factory.py rename to tests/test_factory.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index ee77f176..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,360 +0,0 @@ -import io -import os - -import pytest -import werkzeug.exceptions - -import flask -from flask.helpers import get_debug_flag - - -class FakePath: - """Fake object to represent a ``PathLike object``. - - This represents a ``pathlib.Path`` object in python 3. - See: https://www.python.org/dev/peps/pep-0519/ - """ - - def __init__(self, path): - self.path = path - - def __fspath__(self): - return self.path - - -class PyBytesIO: - def __init__(self, *args, **kwargs): - self._io = io.BytesIO(*args, **kwargs) - - def __getattr__(self, name): - return getattr(self._io, name) - - -class TestSendfile: - def test_send_file(self, app, req_ctx): - rv = flask.send_file("static/index.html") - assert rv.direct_passthrough - assert rv.mimetype == "text/html" - - with app.open_resource("static/index.html") as f: - rv.direct_passthrough = False - assert rv.data == f.read() - - rv.close() - - def test_static_file(self, app, req_ctx): - # Default max_age is None. - - # Test with static file handler. - rv = app.send_static_file("index.html") - assert rv.cache_control.max_age is None - rv.close() - - # Test with direct use of send_file. - rv = flask.send_file("static/index.html") - assert rv.cache_control.max_age is None - rv.close() - - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 - - # Test with static file handler. - rv = app.send_static_file("index.html") - assert rv.cache_control.max_age == 3600 - rv.close() - - # Test with direct use of send_file. - rv = flask.send_file("static/index.html") - assert rv.cache_control.max_age == 3600 - rv.close() - - # Test with pathlib.Path. - rv = app.send_static_file(FakePath("index.html")) - assert rv.cache_control.max_age == 3600 - rv.close() - - class StaticFileApp(flask.Flask): - def get_send_file_max_age(self, filename): - return 10 - - app = StaticFileApp(__name__) - - with app.test_request_context(): - # Test with static file handler. - rv = app.send_static_file("index.html") - assert rv.cache_control.max_age == 10 - rv.close() - - # Test with direct use of send_file. - rv = flask.send_file("static/index.html") - assert rv.cache_control.max_age == 10 - rv.close() - - def test_send_from_directory(self, app, req_ctx): - app.root_path = os.path.join( - os.path.dirname(__file__), "test_apps", "subdomaintestmodule" - ) - rv = flask.send_from_directory("static", "hello.txt") - rv.direct_passthrough = False - assert rv.data.strip() == b"Hello Subdomain" - rv.close() - - -class TestUrlFor: - def test_url_for_with_anchor(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert flask.url_for("index", _anchor="x y") == "/#x%20y" - - def test_url_for_with_scheme(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert ( - flask.url_for("index", _external=True, _scheme="https") - == "https://localhost/" - ) - - def test_url_for_with_scheme_not_external(self, app, req_ctx): - app.add_url_rule("/", endpoint="index") - - # Implicit external with scheme. - url = flask.url_for("index", _scheme="https") - assert url == "https://localhost/" - - # Error when external=False with scheme - with pytest.raises(ValueError): - flask.url_for("index", _scheme="https", _external=False) - - def test_url_for_with_alternating_schemes(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert flask.url_for("index", _external=True) == "http://localhost/" - assert ( - flask.url_for("index", _external=True, _scheme="https") - == "https://localhost/" - ) - assert flask.url_for("index", _external=True) == "http://localhost/" - - def test_url_with_method(self, app, req_ctx): - from flask.views import MethodView - - class MyView(MethodView): - def get(self, id=None): - if id is None: - return "List" - return f"Get {id:d}" - - def post(self): - return "Create" - - myview = MyView.as_view("myview") - app.add_url_rule("/myview/", methods=["GET"], view_func=myview) - app.add_url_rule("/myview/", methods=["GET"], view_func=myview) - app.add_url_rule("/myview/create", methods=["POST"], view_func=myview) - - assert flask.url_for("myview", _method="GET") == "/myview/" - assert flask.url_for("myview", id=42, _method="GET") == "/myview/42" - assert flask.url_for("myview", _method="POST") == "/myview/create" - - def test_url_for_with_self(self, app, req_ctx): - @app.route("/") - def index(self): - return "42" - - assert flask.url_for("index", self="2") == "/2" - - -def test_redirect_no_app(): - response = flask.redirect("https://localhost", 307) - assert response.location == "https://localhost" - assert response.status_code == 307 - - -def test_redirect_with_app(app): - def redirect(location, code=302): - raise ValueError - - app.redirect = redirect - - with app.app_context(), pytest.raises(ValueError): - flask.redirect("other") - - -def test_abort_no_app(): - with pytest.raises(werkzeug.exceptions.Unauthorized): - flask.abort(401) - - with pytest.raises(LookupError): - flask.abort(900) - - -def test_app_aborter_class(): - class MyAborter(werkzeug.exceptions.Aborter): - pass - - class MyFlask(flask.Flask): - aborter_class = MyAborter - - app = MyFlask(__name__) - assert isinstance(app.aborter, MyAborter) - - -def test_abort_with_app(app): - class My900Error(werkzeug.exceptions.HTTPException): - code = 900 - - app.aborter.mapping[900] = My900Error - - with app.app_context(), pytest.raises(My900Error): - flask.abort(900) - - -class TestNoImports: - """Test Flasks are created without import. - - Avoiding ``__import__`` helps create Flask instances where there are errors - at import time. Those runtime errors will be apparent to the user soon - enough, but tools which build Flask instances meta-programmatically benefit - from a Flask which does not ``__import__``. Instead of importing to - retrieve file paths or metadata on a module or package, use the pkgutil and - imp modules in the Python standard library. - """ - - def test_name_with_import_error(self, modules_tmp_path): - (modules_tmp_path / "importerror.py").write_text("raise NotImplementedError()") - try: - flask.Flask("importerror") - except NotImplementedError: - AssertionError("Flask(import_name) is importing import_name.") - - -class TestStreaming: - def test_streaming_with_context(self, app, client): - @app.route("/") - def index(): - def generate(): - yield "Hello " - yield flask.request.args["name"] - yield "!" - - return flask.Response(flask.stream_with_context(generate())) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - - def test_streaming_with_context_as_decorator(self, app, client): - @app.route("/") - def index(): - @flask.stream_with_context - def generate(hello): - yield hello - yield flask.request.args["name"] - yield "!" - - return flask.Response(generate("Hello ")) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - - def test_streaming_with_context_and_custom_close(self, app, client): - called = [] - - class Wrapper: - def __init__(self, gen): - self._gen = gen - - def __iter__(self): - return self - - def close(self): - called.append(42) - - def __next__(self): - return next(self._gen) - - next = __next__ - - @app.route("/") - def index(): - def generate(): - yield "Hello " - yield flask.request.args["name"] - yield "!" - - return flask.Response(flask.stream_with_context(Wrapper(generate()))) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - assert called == [42] - - def test_stream_keeps_session(self, app, client): - @app.route("/") - def index(): - flask.session["test"] = "flask" - - @flask.stream_with_context - def gen(): - yield flask.session["test"] - - return flask.Response(gen()) - - rv = client.get("/") - assert rv.data == b"flask" - - -class TestHelpers: - @pytest.mark.parametrize( - ("debug", "expect"), - [ - ("", False), - ("0", False), - ("False", False), - ("No", False), - ("True", True), - ], - ) - def test_get_debug_flag(self, monkeypatch, debug, expect): - monkeypatch.setenv("FLASK_DEBUG", debug) - assert get_debug_flag() == expect - - def test_make_response(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.helpers.make_response() - assert rv.status_code == 200 - assert rv.mimetype == "text/html" - - rv = flask.helpers.make_response("Hello") - assert rv.status_code == 200 - assert rv.data == b"Hello" - assert rv.mimetype == "text/html" - - -@pytest.mark.parametrize("mode", ("r", "rb", "rt")) -def test_open_resource(mode): - app = flask.Flask(__name__) - - with app.open_resource("static/index.html", mode) as f: - assert "

Hello World!

" in str(f.read()) - - -@pytest.mark.parametrize("mode", ("w", "x", "a", "r+")) -def test_open_resource_exceptions(mode): - app = flask.Flask(__name__) - - with pytest.raises(ValueError): - app.open_resource("static/index.html", mode) - - -@pytest.mark.parametrize("encoding", ("utf-8", "utf-16-le")) -def test_open_resource_with_encoding(tmp_path, encoding): - app = flask.Flask(__name__, root_path=os.fspath(tmp_path)) - (tmp_path / "test").write_text("test", encoding=encoding) - - with app.open_resource("test", mode="rt", encoding=encoding) as f: - assert f.read() == "test" diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py deleted file mode 100644 index 1918bd99..00000000 --- a/tests/test_instance_config.py +++ /dev/null @@ -1,111 +0,0 @@ -import os - -import pytest - -import flask - - -def test_explicit_instance_paths(modules_tmp_path): - with pytest.raises(ValueError, match=".*must be absolute"): - flask.Flask(__name__, instance_path="instance") - - app = flask.Flask(__name__, instance_path=os.fspath(modules_tmp_path)) - assert app.instance_path == os.fspath(modules_tmp_path) - - -def test_uninstalled_module_paths(modules_tmp_path, purge_module): - (modules_tmp_path / "config_module_app.py").write_text( - "import os\n" - "import flask\n" - "here = os.path.abspath(os.path.dirname(__file__))\n" - "app = flask.Flask(__name__)\n" - ) - purge_module("config_module_app") - - from config_module_app import app - - assert app.instance_path == os.fspath(modules_tmp_path / "instance") - - -def test_uninstalled_package_paths(modules_tmp_path, purge_module): - app = modules_tmp_path / "config_package_app" - app.mkdir() - (app / "__init__.py").write_text( - "import os\n" - "import flask\n" - "here = os.path.abspath(os.path.dirname(__file__))\n" - "app = flask.Flask(__name__)\n" - ) - purge_module("config_package_app") - - from config_package_app import app - - assert app.instance_path == os.fspath(modules_tmp_path / "instance") - - -def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module): - def create_namespace(package): - project = tmp_path / f"project-{package}" - monkeypatch.syspath_prepend(os.fspath(project)) - ns = project / "namespace" / package - ns.mkdir(parents=True) - (ns / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - return project - - _ = create_namespace("package1") - project2 = create_namespace("package2") - purge_module("namespace.package2") - purge_module("namespace") - - from namespace.package2 import app - - assert app.instance_path == os.fspath(project2 / "instance") - - -def test_installed_module_paths( - modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages, limit_loader -): - (site_packages / "site_app.py").write_text( - "import flask\napp = flask.Flask(__name__)\n" - ) - purge_module("site_app") - - from site_app import app - - assert app.instance_path == os.fspath( - modules_tmp_path / "var" / "site_app-instance" - ) - - -def test_installed_package_paths( - limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch -): - installed_path = modules_tmp_path / "path" - installed_path.mkdir() - monkeypatch.syspath_prepend(installed_path) - - app = installed_path / "installed_package" - app.mkdir() - (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - purge_module("installed_package") - - from installed_package import app - - assert app.instance_path == os.fspath( - modules_tmp_path / "var" / "installed_package-instance" - ) - - -def test_prefix_package_paths( - limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages -): - app = site_packages / "site_package" - app.mkdir() - (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - purge_module("site_package") - - import site_package - - assert site_package.app.instance_path == os.fspath( - modules_tmp_path / "var" / "site_package-instance" - ) diff --git a/tests/test_json.py b/tests/test_json.py deleted file mode 100644 index 1e2b27dc..00000000 --- a/tests/test_json.py +++ /dev/null @@ -1,346 +0,0 @@ -import datetime -import decimal -import io -import uuid - -import pytest -from werkzeug.http import http_date - -import flask -from flask import json -from flask.json.provider import DefaultJSONProvider - - -@pytest.mark.parametrize("debug", (True, False)) -def test_bad_request_debug_message(app, client, debug): - app.config["DEBUG"] = debug - app.config["TRAP_BAD_REQUEST_ERRORS"] = False - - @app.route("/json", methods=["POST"]) - def post_json(): - flask.request.get_json() - return None - - rv = client.post("/json", data=None, content_type="application/json") - assert rv.status_code == 400 - contains = b"Failed to decode JSON object" in rv.data - assert contains == debug - - -def test_json_bad_requests(app, client): - @app.route("/json", methods=["POST"]) - def return_json(): - return flask.jsonify(foo=str(flask.request.get_json())) - - rv = client.post("/json", data="malformed", content_type="application/json") - assert rv.status_code == 400 - - -def test_json_custom_mimetypes(app, client): - @app.route("/json", methods=["POST"]) - def return_json(): - return flask.request.get_json() - - rv = client.post("/json", data='"foo"', content_type="application/x+json") - assert rv.data == b"foo" - - -@pytest.mark.parametrize( - "test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')] -) -def test_json_as_unicode(test_value, expected, app, app_ctx): - app.json.ensure_ascii = test_value - rv = app.json.dumps("\N{SNOWMAN}") - assert rv == expected - - -def test_json_dump_to_file(app, app_ctx): - test_data = {"name": "Flask"} - out = io.StringIO() - - flask.json.dump(test_data, out) - out.seek(0) - rv = flask.json.load(out) - assert rv == test_data - - -@pytest.mark.parametrize( - "test_value", [0, -1, 1, 23, 3.14, "s", "longer string", True, False, None] -) -def test_jsonify_basic_types(test_value, app, client): - url = "/jsonify_basic_types" - app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x)) - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == test_value - - -def test_jsonify_dicts(app, client): - d = { - "a": 0, - "b": 23, - "c": 3.14, - "d": "t", - "e": "Hi", - "f": True, - "g": False, - "h": ["test list", 10, False], - "i": {"test": "dict"}, - } - - @app.route("/kw") - def return_kwargs(): - return flask.jsonify(**d) - - @app.route("/dict") - def return_dict(): - return flask.jsonify(d) - - for url in "/kw", "/dict": - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == d - - -def test_jsonify_arrays(app, client): - """Test jsonify of lists and args unpacking.""" - a_list = [ - 0, - 42, - 3.14, - "t", - "hello", - True, - False, - ["test list", 2, False], - {"test": "dict"}, - ] - - @app.route("/args_unpack") - def return_args_unpack(): - return flask.jsonify(*a_list) - - @app.route("/array") - def return_array(): - return flask.jsonify(a_list) - - for url in "/args_unpack", "/array": - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == a_list - - -@pytest.mark.parametrize( - "value", [datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5)] -) -def test_jsonify_datetime(app, client, value): - @app.route("/") - def index(): - return flask.jsonify(value=value) - - r = client.get() - assert r.json["value"] == http_date(value) - - -class FixedOffset(datetime.tzinfo): - """Fixed offset in hours east from UTC. - - This is a slight adaptation of the ``FixedOffset`` example found in - https://docs.python.org/2.7/library/datetime.html. - """ - - def __init__(self, hours, name): - self.__offset = datetime.timedelta(hours=hours) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return datetime.timedelta() - - -@pytest.mark.parametrize("tz", (("UTC", 0), ("PST", -8), ("KST", 9))) -def test_jsonify_aware_datetimes(tz): - """Test if aware datetime.datetime objects are converted into GMT.""" - tzinfo = FixedOffset(hours=tz[1], name=tz[0]) - dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) - gmt = FixedOffset(hours=0, name="GMT") - expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') - assert flask.json.dumps(dt) == expected - - -def test_jsonify_uuid_types(app, client): - """Test jsonify with uuid.UUID types""" - - test_uuid = uuid.UUID(bytes=b"\xde\xad\xbe\xef" * 4) - url = "/uuid_test" - app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid)) - - rv = client.get(url) - - rv_x = flask.json.loads(rv.data)["x"] - assert rv_x == str(test_uuid) - rv_uuid = uuid.UUID(rv_x) - assert rv_uuid == test_uuid - - -def test_json_decimal(): - rv = flask.json.dumps(decimal.Decimal("0.003")) - assert rv == '"0.003"' - - -def test_json_attr(app, client): - @app.route("/add", methods=["POST"]) - def add(): - json = flask.request.get_json() - return str(json["a"] + json["b"]) - - rv = client.post( - "/add", - data=flask.json.dumps({"a": 1, "b": 2}), - content_type="application/json", - ) - assert rv.data == b"3" - - -def test_tojson_filter(app, req_ctx): - # The tojson filter is tested in Jinja, this confirms that it's - # using Flask's dumps. - rv = flask.render_template_string( - "const data = {{ data|tojson }};", - data={"name": "", "time": datetime.datetime(2021, 2, 1, 7, 15)}, - ) - assert rv == ( - 'const data = {"name": "\\u003c/script\\u003e",' - ' "time": "Mon, 01 Feb 2021 07:15:00 GMT"};' - ) - - -def test_json_customization(app, client): - class X: # noqa: B903, for Python2 compatibility - def __init__(self, val): - self.val = val - - def default(o): - if isinstance(o, X): - return f"<{o.val}>" - - return DefaultJSONProvider.default(o) - - class CustomProvider(DefaultJSONProvider): - def object_hook(self, obj): - if len(obj) == 1 and "_foo" in obj: - return X(obj["_foo"]) - - return obj - - def loads(self, s, **kwargs): - kwargs.setdefault("object_hook", self.object_hook) - return super().loads(s, **kwargs) - - app.json = CustomProvider(app) - app.json.default = default - - @app.route("/", methods=["POST"]) - def index(): - return flask.json.dumps(flask.request.get_json()["x"]) - - rv = client.post( - "/", - data=flask.json.dumps({"x": {"_foo": 42}}), - content_type="application/json", - ) - assert rv.data == b'"<42>"' - - -def _has_encoding(name): - try: - import codecs - - codecs.lookup(name) - return True - except LookupError: - return False - - -def test_json_key_sorting(app, client): - app.debug = True - assert app.json.sort_keys - d = dict.fromkeys(range(20), "foo") - - @app.route("/") - def index(): - return flask.jsonify(values=d) - - rv = client.get("/") - lines = [x.strip() for x in rv.data.strip().decode("utf-8").splitlines()] - sorted_by_str = [ - "{", - '"values": {', - '"0": "foo",', - '"1": "foo",', - '"10": "foo",', - '"11": "foo",', - '"12": "foo",', - '"13": "foo",', - '"14": "foo",', - '"15": "foo",', - '"16": "foo",', - '"17": "foo",', - '"18": "foo",', - '"19": "foo",', - '"2": "foo",', - '"3": "foo",', - '"4": "foo",', - '"5": "foo",', - '"6": "foo",', - '"7": "foo",', - '"8": "foo",', - '"9": "foo"', - "}", - "}", - ] - sorted_by_int = [ - "{", - '"values": {', - '"0": "foo",', - '"1": "foo",', - '"2": "foo",', - '"3": "foo",', - '"4": "foo",', - '"5": "foo",', - '"6": "foo",', - '"7": "foo",', - '"8": "foo",', - '"9": "foo",', - '"10": "foo",', - '"11": "foo",', - '"12": "foo",', - '"13": "foo",', - '"14": "foo",', - '"15": "foo",', - '"16": "foo",', - '"17": "foo",', - '"18": "foo",', - '"19": "foo"', - "}", - "}", - ] - - try: - assert lines == sorted_by_int - except AssertionError: - assert lines == sorted_by_str - - -def test_html_method(): - class ObjectWithHTML: - def __html__(self): - return "

test

" - - result = json.dumps(ObjectWithHTML()) - assert result == '"

test

"' diff --git a/tests/test_json_tag.py b/tests/test_json_tag.py deleted file mode 100644 index 677160a6..00000000 --- a/tests/test_json_tag.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime -from datetime import timezone -from uuid import uuid4 - -import pytest -from markupsafe import Markup - -from flask.json.tag import JSONTag -from flask.json.tag import TaggedJSONSerializer - - -@pytest.mark.parametrize( - "data", - ( - {" t": (1, 2, 3)}, - {" t__": b"a"}, - {" di": " di"}, - {"x": (1, 2, 3), "y": 4}, - (1, 2, 3), - [(1, 2, 3)], - b"\xff", - Markup(""), - uuid4(), - datetime.now(tz=timezone.utc).replace(microsecond=0), - ), -) -def test_dump_load_unchanged(data): - s = TaggedJSONSerializer() - assert s.loads(s.dumps(data)) == data - - -def test_duplicate_tag(): - class TagDict(JSONTag): - key = " d" - - s = TaggedJSONSerializer() - pytest.raises(KeyError, s.register, TagDict) - s.register(TagDict, force=True, index=0) - assert isinstance(s.tags[" d"], TagDict) - assert isinstance(s.order[0], TagDict) - - -def test_custom_tag(): - class Foo: # noqa: B903, for Python2 compatibility - def __init__(self, data): - self.data = data - - class TagFoo(JSONTag): - __slots__ = () - key = " f" - - def check(self, value): - return isinstance(value, Foo) - - def to_json(self, value): - return self.serializer.tag(value.data) - - def to_python(self, value): - return Foo(value) - - s = TaggedJSONSerializer() - s.register(TagFoo) - assert s.loads(s.dumps(Foo("bar"))).data == "bar" - - -def test_tag_interface(): - t = JSONTag(None) - pytest.raises(NotImplementedError, t.check, None) - pytest.raises(NotImplementedError, t.to_json, None) - pytest.raises(NotImplementedError, t.to_python, None) - - -def test_tag_order(): - class Tag1(JSONTag): - key = " 1" - - class Tag2(JSONTag): - key = " 2" - - s = TaggedJSONSerializer() - - s.register(Tag1, index=-1) - assert isinstance(s.order[-2], Tag1) - - s.register(Tag2, index=None) - assert isinstance(s.order[-1], Tag2) diff --git a/tests/test_logging.py b/tests/test_logging.py deleted file mode 100644 index a5f04636..00000000 --- a/tests/test_logging.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -import sys -from io import StringIO - -import pytest - -from flask.logging import default_handler -from flask.logging import has_level_handler -from flask.logging import wsgi_errors_stream - - -@pytest.fixture(autouse=True) -def reset_logging(pytestconfig): - root_handlers = logging.root.handlers[:] - logging.root.handlers = [] - root_level = logging.root.level - - logger = logging.getLogger("flask_test") - logger.handlers = [] - logger.setLevel(logging.NOTSET) - - logging_plugin = pytestconfig.pluginmanager.unregister(name="logging-plugin") - - yield - - logging.root.handlers[:] = root_handlers - logging.root.setLevel(root_level) - - logger.handlers = [] - logger.setLevel(logging.NOTSET) - - if logging_plugin: - pytestconfig.pluginmanager.register(logging_plugin, "logging-plugin") - - -def test_logger(app): - assert app.logger.name == "flask_test" - assert app.logger.level == logging.NOTSET - assert app.logger.handlers == [default_handler] - - -def test_logger_debug(app): - app.debug = True - assert app.logger.level == logging.DEBUG - assert app.logger.handlers == [default_handler] - - -def test_existing_handler(app): - logging.root.addHandler(logging.StreamHandler()) - assert app.logger.level == logging.NOTSET - assert not app.logger.handlers - - -def test_wsgi_errors_stream(app, client): - @app.route("/") - def index(): - app.logger.error("test") - return "" - - stream = StringIO() - client.get("/", errors_stream=stream) - assert "ERROR in test_logging: test" in stream.getvalue() - - assert wsgi_errors_stream._get_current_object() is sys.stderr - - with app.test_request_context(errors_stream=stream): - assert wsgi_errors_stream._get_current_object() is stream - - -def test_has_level_handler(): - logger = logging.getLogger("flask.app") - assert not has_level_handler(logger) - - handler = logging.StreamHandler() - logging.root.addHandler(handler) - assert has_level_handler(logger) - - logger.propagate = False - assert not has_level_handler(logger) - logger.propagate = True - - handler.setLevel(logging.ERROR) - assert not has_level_handler(logger) - - -def test_log_view_exception(app, client): - @app.route("/") - def index(): - raise Exception("test") - - app.testing = False - stream = StringIO() - rv = client.get("/", errors_stream=stream) - assert rv.status_code == 500 - assert rv.data - err = stream.getvalue() - assert "Exception on / [GET]" in err - assert "Exception: test" in err diff --git a/tests/test_regression.py b/tests/test_regression.py deleted file mode 100644 index 0ddcf972..00000000 --- a/tests/test_regression.py +++ /dev/null @@ -1,30 +0,0 @@ -import flask - - -def test_aborting(app): - class Foo(Exception): - whatever = 42 - - @app.errorhandler(Foo) - def handle_foo(e): - return str(e.whatever) - - @app.route("/") - def index(): - raise flask.abort(flask.redirect(flask.url_for("test"))) - - @app.route("/test") - def test(): - raise Foo() - - with app.test_client() as c: - rv = c.get("/") - location_parts = rv.headers["Location"].rpartition("/") - - if location_parts[0]: - # For older Werkzeug that used absolute redirects. - assert location_parts[0] == "http://localhost" - - assert location_parts[2] == "test" - rv = c.get("/test") - assert rv.data == b"42" diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py deleted file mode 100644 index 6c38b661..00000000 --- a/tests/test_reqctx.py +++ /dev/null @@ -1,325 +0,0 @@ -import warnings - -import pytest - -import flask -from flask.globals import request_ctx -from flask.sessions import SecureCookieSessionInterface -from flask.sessions import SessionInterface - -try: - from greenlet import greenlet -except ImportError: - greenlet = None - - -def test_teardown_on_pop(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - ctx = app.test_request_context() - ctx.push() - assert buffer == [] - ctx.pop() - assert buffer == [None] - - -def test_teardown_with_previous_exception(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - try: - raise Exception("dummy") - except Exception: - pass - - with app.test_request_context(): - assert buffer == [] - assert buffer == [None] - - -def test_teardown_with_handled_exception(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - with app.test_request_context(): - assert buffer == [] - try: - raise Exception("dummy") - except Exception: - pass - assert buffer == [None] - - -def test_proper_test_request_context(app): - app.config.update(SERVER_NAME="localhost.localdomain:5000") - - @app.route("/") - def index(): - return None - - @app.route("/", subdomain="foo") - def sub(): - return None - - with app.test_request_context("/"): - assert ( - flask.url_for("index", _external=True) - == "http://localhost.localdomain:5000/" - ) - - with app.test_request_context("/"): - assert ( - flask.url_for("sub", _external=True) - == "http://foo.localhost.localdomain:5000/" - ) - - # suppress Werkzeug 0.15 warning about name mismatch - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "Current server name", UserWarning, "flask.app" - ) - with app.test_request_context( - "/", environ_overrides={"HTTP_HOST": "localhost"} - ): - pass - - app.config.update(SERVER_NAME="localhost") - with app.test_request_context("/", environ_overrides={"SERVER_NAME": "localhost"}): - pass - - app.config.update(SERVER_NAME="localhost:80") - with app.test_request_context( - "/", environ_overrides={"SERVER_NAME": "localhost:80"} - ): - pass - - -def test_context_binding(app): - @app.route("/") - def index(): - return f"Hello {flask.request.args['name']}!" - - @app.route("/meh") - def meh(): - return flask.request.url - - with app.test_request_context("/?name=World"): - assert index() == "Hello World!" - with app.test_request_context("/meh"): - assert meh() == "http://localhost/meh" - assert not flask.request - - -def test_context_test(app): - assert not flask.request - assert not flask.has_request_context() - ctx = app.test_request_context() - ctx.push() - try: - assert flask.request - assert flask.has_request_context() - finally: - ctx.pop() - - -def test_manual_context_binding(app): - @app.route("/") - def index(): - return f"Hello {flask.request.args['name']}!" - - ctx = app.test_request_context("/?name=World") - ctx.push() - assert index() == "Hello World!" - ctx.pop() - with pytest.raises(RuntimeError): - index() - - -@pytest.mark.skipif(greenlet is None, reason="greenlet not installed") -class TestGreenletContextCopying: - def test_greenlet_context_copying(self, app, client): - greenlets = [] - - @app.route("/") - def index(): - flask.session["fizz"] = "buzz" - reqctx = request_ctx.copy() - - def g(): - assert not flask.request - assert not flask.current_app - with reqctx: - assert flask.request - assert flask.current_app == app - assert flask.request.path == "/" - assert flask.request.args["foo"] == "bar" - assert flask.session.get("fizz") == "buzz" - assert not flask.request - return 42 - - greenlets.append(greenlet(g)) - return "Hello World!" - - rv = client.get("/?foo=bar") - assert rv.data == b"Hello World!" - - result = greenlets[0].run() - assert result == 42 - - def test_greenlet_context_copying_api(self, app, client): - greenlets = [] - - @app.route("/") - def index(): - flask.session["fizz"] = "buzz" - - @flask.copy_current_request_context - def g(): - assert flask.request - assert flask.current_app == app - assert flask.request.path == "/" - assert flask.request.args["foo"] == "bar" - assert flask.session.get("fizz") == "buzz" - return 42 - - greenlets.append(greenlet(g)) - return "Hello World!" - - rv = client.get("/?foo=bar") - assert rv.data == b"Hello World!" - - result = greenlets[0].run() - assert result == 42 - - -def test_session_error_pops_context(): - class SessionError(Exception): - pass - - class FailingSessionInterface(SessionInterface): - def open_session(self, app, request): - raise SessionError() - - class CustomFlask(flask.Flask): - session_interface = FailingSessionInterface() - - app = CustomFlask(__name__) - - @app.route("/") - def index(): - # shouldn't get here - AssertionError() - - response = app.test_client().get("/") - assert response.status_code == 500 - assert not flask.request - assert not flask.current_app - - -def test_session_dynamic_cookie_name(): - # This session interface will use a cookie with a different name if the - # requested url ends with the string "dynamic_cookie" - class PathAwareSessionInterface(SecureCookieSessionInterface): - def get_cookie_name(self, app): - if flask.request.url.endswith("dynamic_cookie"): - return "dynamic_cookie_name" - else: - return super().get_cookie_name(app) - - class CustomFlask(flask.Flask): - session_interface = PathAwareSessionInterface() - - app = CustomFlask(__name__) - app.secret_key = "secret_key" - - @app.route("/set", methods=["POST"]) - def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") - def get(): - v = flask.session.get("value", "None") - return v - - @app.route("/set_dynamic_cookie", methods=["POST"]) - def set_dynamic_cookie(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get_dynamic_cookie") - def get_dynamic_cookie(): - v = flask.session.get("value", "None") - return v - - test_client = app.test_client() - - # first set the cookie in both /set urls but each with a different value - assert test_client.post("/set", data={"value": "42"}).data == b"value set" - assert ( - test_client.post("/set_dynamic_cookie", data={"value": "616"}).data - == b"value set" - ) - - # now check that the relevant values come back - meaning that different - # cookies are being used for the urls that end with "dynamic cookie" - assert test_client.get("/get").data == b"42" - assert test_client.get("/get_dynamic_cookie").data == b"616" - - -def test_bad_environ_raises_bad_request(): - app = flask.Flask(__name__) - - from flask.testing import EnvironBuilder - - builder = EnvironBuilder(app) - environ = builder.get_environ() - - # use a non-printable character in the Host - this is key to this test - environ["HTTP_HOST"] = "\x8a" - - with app.request_context(environ): - response = app.full_dispatch_request() - assert response.status_code == 400 - - -def test_environ_for_valid_idna_completes(): - app = flask.Flask(__name__) - - @app.route("/") - def index(): - return "Hello World!" - - from flask.testing import EnvironBuilder - - builder = EnvironBuilder(app) - environ = builder.get_environ() - - # these characters are all IDNA-compatible - environ["HTTP_HOST"] = "ąśźäüжŠßя.com" - - with app.request_context(environ): - response = app.full_dispatch_request() - - assert response.status_code == 200 - - -def test_normal_environ_completes(): - app = flask.Flask(__name__) - - @app.route("/") - def index(): - return "Hello World!" - - response = app.test_client().get("/", headers={"host": "xn--on-0ia.com"}) - assert response.status_code == 200 diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py deleted file mode 100644 index 613da37f..00000000 --- a/tests/test_session_interface.py +++ /dev/null @@ -1,28 +0,0 @@ -import flask -from flask.globals import request_ctx -from flask.sessions import SessionInterface - - -def test_open_session_with_endpoint(): - """If request.endpoint (or other URL matching behavior) is needed - while loading the session, RequestContext.match_request() can be - called manually. - """ - - class MySessionInterface(SessionInterface): - def save_session(self, app, session, response): - pass - - def open_session(self, app, request): - request_ctx.match_request() - assert request.endpoint is not None - - app = flask.Flask(__name__) - app.session_interface = MySessionInterface() - - @app.get("/") - def index(): - return "Hello, World!" - - response = app.test_client().get("/") - assert response.status_code == 200 diff --git a/tests/test_signals.py b/tests/test_signals.py deleted file mode 100644 index 32ab333e..00000000 --- a/tests/test_signals.py +++ /dev/null @@ -1,181 +0,0 @@ -import flask - - -def test_template_rendered(app, client): - @app.route("/") - def index(): - return flask.render_template("simple_template.html", whiskey=42) - - recorded = [] - - def record(sender, template, context): - recorded.append((template, context)) - - flask.template_rendered.connect(record, app) - try: - client.get("/") - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == "simple_template.html" - assert context["whiskey"] == 42 - finally: - flask.template_rendered.disconnect(record, app) - - -def test_before_render_template(): - app = flask.Flask(__name__) - - @app.route("/") - def index(): - return flask.render_template("simple_template.html", whiskey=42) - - recorded = [] - - def record(sender, template, context): - context["whiskey"] = 43 - recorded.append((template, context)) - - flask.before_render_template.connect(record, app) - try: - rv = app.test_client().get("/") - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == "simple_template.html" - assert context["whiskey"] == 43 - assert rv.data == b"

43

" - finally: - flask.before_render_template.disconnect(record, app) - - -def test_request_signals(): - app = flask.Flask(__name__) - calls = [] - - def before_request_signal(sender): - calls.append("before-signal") - - def after_request_signal(sender, response): - assert response.data == b"stuff" - calls.append("after-signal") - - @app.before_request - def before_request_handler(): - calls.append("before-handler") - - @app.after_request - def after_request_handler(response): - calls.append("after-handler") - response.data = "stuff" - return response - - @app.route("/") - def index(): - calls.append("handler") - return "ignored anyway" - - flask.request_started.connect(before_request_signal, app) - flask.request_finished.connect(after_request_signal, app) - - try: - rv = app.test_client().get("/") - assert rv.data == b"stuff" - - assert calls == [ - "before-signal", - "before-handler", - "handler", - "after-handler", - "after-signal", - ] - finally: - flask.request_started.disconnect(before_request_signal, app) - flask.request_finished.disconnect(after_request_signal, app) - - -def test_request_exception_signal(): - app = flask.Flask(__name__) - recorded = [] - - @app.route("/") - def index(): - raise ZeroDivisionError - - def record(sender, exception): - recorded.append(exception) - - flask.got_request_exception.connect(record, app) - try: - assert app.test_client().get("/").status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.got_request_exception.disconnect(record, app) - - -def test_appcontext_signals(app, client): - recorded = [] - - def record_push(sender, **kwargs): - recorded.append("push") - - def record_pop(sender, **kwargs): - recorded.append("pop") - - @app.route("/") - def index(): - return "Hello" - - flask.appcontext_pushed.connect(record_push, app) - flask.appcontext_popped.connect(record_pop, app) - try: - rv = client.get("/") - assert rv.data == b"Hello" - assert recorded == ["push", "pop"] - finally: - flask.appcontext_pushed.disconnect(record_push, app) - flask.appcontext_popped.disconnect(record_pop, app) - - -def test_flash_signal(app): - @app.route("/") - def index(): - flask.flash("This is a flash message", category="notice") - return flask.redirect("/other") - - recorded = [] - - def record(sender, message, category): - recorded.append((message, category)) - - flask.message_flashed.connect(record, app) - try: - client = app.test_client() - with client.session_transaction(): - client.get("/") - assert len(recorded) == 1 - message, category = recorded[0] - assert message == "This is a flash message" - assert category == "notice" - finally: - flask.message_flashed.disconnect(record, app) - - -def test_appcontext_tearing_down_signal(app, client): - app.testing = False - recorded = [] - - def record_teardown(sender, exc): - recorded.append(exc) - - @app.route("/") - def index(): - raise ZeroDivisionError - - flask.appcontext_tearing_down.connect(record_teardown, app) - try: - rv = client.get("/") - assert rv.status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.appcontext_tearing_down.disconnect(record_teardown, app) diff --git a/tests/test_subclassing.py b/tests/test_subclassing.py deleted file mode 100644 index 087c50dc..00000000 --- a/tests/test_subclassing.py +++ /dev/null @@ -1,21 +0,0 @@ -from io import StringIO - -import flask - - -def test_suppressed_exception_logging(): - class SuppressedFlask(flask.Flask): - def log_exception(self, exc_info): - pass - - out = StringIO() - app = SuppressedFlask(__name__) - - @app.route("/") - def index(): - raise Exception("test") - - rv = app.test_client().get("/", errors_stream=out) - assert rv.status_code == 500 - assert b"Internal Server Error" in rv.data - assert not out.getvalue() diff --git a/tests/test_templating.py b/tests/test_templating.py deleted file mode 100644 index c9fb3754..00000000 --- a/tests/test_templating.py +++ /dev/null @@ -1,451 +0,0 @@ -import logging - -import pytest -import werkzeug.serving -from jinja2 import TemplateNotFound -from markupsafe import Markup - -import flask - - -def test_context_processing(app, client): - @app.context_processor - def context_processor(): - return {"injected_value": 42} - - @app.route("/") - def index(): - return flask.render_template("context_template.html", value=23) - - rv = client.get("/") - assert rv.data == b"

23|42" - - -def test_original_win(app, client): - @app.route("/") - def index(): - return flask.render_template_string("{{ config }}", config=42) - - rv = client.get("/") - assert rv.data == b"42" - - -def test_simple_stream(app, client): - @app.route("/") - def index(): - return flask.stream_template_string("{{ config }}", config=42) - - rv = client.get("/") - assert rv.data == b"42" - - -def test_request_less_rendering(app, app_ctx): - app.config["WORLD_NAME"] = "Special World" - - @app.context_processor - def context_processor(): - return dict(foo=42) - - rv = flask.render_template_string("Hello {{ config.WORLD_NAME }} {{ foo }}") - assert rv == "Hello Special World 42" - - -def test_standard_context(app, client): - @app.route("/") - def index(): - flask.g.foo = 23 - flask.session["test"] = "aha" - return flask.render_template_string( - """ - {{ request.args.foo }} - {{ g.foo }} - {{ config.DEBUG }} - {{ session.test }} - """ - ) - - rv = client.get("/?foo=42") - assert rv.data.split() == [b"42", b"23", b"False", b"aha"] - - -def test_escaping(app, client): - text = "

Hello World!" - - @app.route("/") - def index(): - return flask.render_template( - "escaping_template.html", text=text, html=Markup(text) - ) - - lines = client.get("/").data.splitlines() - assert lines == [ - b"<p>Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"<p>Hello World!", - b"

Hello World!", - ] - - -def test_no_escaping(app, client): - text = "

Hello World!" - - @app.route("/") - def index(): - return flask.render_template( - "non_escaping_template.txt", text=text, html=Markup(text) - ) - - lines = client.get("/").data.splitlines() - assert lines == [ - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"<p>Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - ] - - -def test_escaping_without_template_filename(app, client, req_ctx): - assert flask.render_template_string("{{ foo }}", foo="") == "<test>" - assert flask.render_template("mail.txt", foo="") == " Mail" - - -def test_macros(app, req_ctx): - macro = flask.get_template_attribute("_macro.html", "hello") - assert macro("World") == "Hello World!" - - -def test_template_filter(app): - @app.template_filter() - def my_reverse(s): - return s[::-1] - - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_add_template_filter(app): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse) - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_template_filter_with_name(app): - @app.template_filter("strrev") - def my_reverse(s): - return s[::-1] - - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_add_template_filter_with_name(app): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse, "strrev") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_template_filter_with_template(app, client): - @app.template_filter() - def super_reverse(s): - return s[::-1] - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_template(app, client): - def super_reverse(s): - return s[::-1] - - app.add_template_filter(super_reverse) - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_with_name_and_template(app, client): - @app.template_filter("super_reverse") - def my_reverse(s): - return s[::-1] - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_name_and_template(app, client): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse, "super_reverse") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_test(app): - @app.template_test() - def boolean(value): - return isinstance(value, bool) - - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_add_template_test(app): - def boolean(value): - return isinstance(value, bool) - - app.add_template_test(boolean) - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_name(app): - @app.template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_add_template_test_with_name(app): - def is_boolean(value): - return isinstance(value, bool) - - app.add_template_test(is_boolean, "boolean") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_template(app, client): - @app.template_test() - def boolean(value): - return isinstance(value, bool) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_template(app, client): - def boolean(value): - return isinstance(value, bool) - - app.add_template_test(boolean) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_with_name_and_template(app, client): - @app.template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_name_and_template(app, client): - def is_boolean(value): - return isinstance(value, bool) - - app.add_template_test(is_boolean, "boolean") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_global(app, app_ctx): - @app.template_global() - def get_stuff(): - return 42 - - assert "get_stuff" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_stuff"] == get_stuff - assert app.jinja_env.globals["get_stuff"](), 42 - - rv = flask.render_template_string("{{ get_stuff() }}") - assert rv == "42" - - -def test_custom_template_loader(client): - class MyFlask(flask.Flask): - def create_global_jinja_loader(self): - from jinja2 import DictLoader - - return DictLoader({"index.html": "Hello Custom World!"}) - - app = MyFlask(__name__) - - @app.route("/") - def index(): - return flask.render_template("index.html") - - c = app.test_client() - rv = c.get("/") - assert rv.data == b"Hello Custom World!" - - -def test_iterable_loader(app, client): - @app.context_processor - def context_processor(): - return {"whiskey": "Jameson"} - - @app.route("/") - def index(): - return flask.render_template( - [ - "no_template.xml", # should skip this one - "simple_template.html", # should render this - "context_template.html", - ], - value=23, - ) - - rv = client.get("/") - assert rv.data == b"

Jameson

" - - -def test_templates_auto_reload(app): - # debug is False, config option is None - assert app.debug is False - assert app.config["TEMPLATES_AUTO_RELOAD"] is None - assert app.jinja_env.auto_reload is False - # debug is False, config option is False - app = flask.Flask(__name__) - app.config["TEMPLATES_AUTO_RELOAD"] = False - assert app.debug is False - assert app.jinja_env.auto_reload is False - # debug is False, config option is True - app = flask.Flask(__name__) - app.config["TEMPLATES_AUTO_RELOAD"] = True - assert app.debug is False - assert app.jinja_env.auto_reload is True - # debug is True, config option is None - app = flask.Flask(__name__) - app.config["DEBUG"] = True - assert app.config["TEMPLATES_AUTO_RELOAD"] is None - assert app.jinja_env.auto_reload is True - # debug is True, config option is False - app = flask.Flask(__name__) - app.config["DEBUG"] = True - app.config["TEMPLATES_AUTO_RELOAD"] = False - assert app.jinja_env.auto_reload is False - # debug is True, config option is True - app = flask.Flask(__name__) - app.config["DEBUG"] = True - app.config["TEMPLATES_AUTO_RELOAD"] = True - assert app.jinja_env.auto_reload is True - - -def test_templates_auto_reload_debug_run(app, monkeypatch): - def run_simple_mock(*args, **kwargs): - pass - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - - app.run() - assert not app.jinja_env.auto_reload - - app.run(debug=True) - assert app.jinja_env.auto_reload - - -def test_template_loader_debugging(test_apps, monkeypatch): - from blueprintapp import app - - called = [] - - class _TestHandler(logging.Handler): - def handle(self, record): - called.append(True) - text = str(record.msg) - assert "1: trying loader of application 'blueprintapp'" in text - assert ( - "2: trying loader of blueprint 'admin' (blueprintapp.apps.admin)" - ) in text - assert ( - "trying loader of blueprint 'frontend' (blueprintapp.apps.frontend)" - ) in text - assert "Error: the template could not be found" in text - assert ( - "looked up from an endpoint that belongs to the blueprint 'frontend'" - ) in text - assert "See https://flask.palletsprojects.com/blueprints/#templates" in text - - with app.test_client() as c: - monkeypatch.setitem(app.config, "EXPLAIN_TEMPLATE_LOADING", True) - monkeypatch.setattr( - logging.getLogger("blueprintapp"), "handlers", [_TestHandler()] - ) - - with pytest.raises(TemplateNotFound) as excinfo: - c.get("/missing") - - assert "missing_template.html" in str(excinfo.value) - - assert len(called) == 1 - - -def test_custom_jinja_env(): - class CustomEnvironment(flask.templating.Environment): - pass - - class CustomFlask(flask.Flask): - jinja_environment = CustomEnvironment - - app = CustomFlask(__name__) - assert isinstance(app.jinja_env, CustomEnvironment) diff --git a/tests/test_testing.py b/tests/test_testing.py deleted file mode 100644 index de052152..00000000 --- a/tests/test_testing.py +++ /dev/null @@ -1,396 +0,0 @@ -import importlib.metadata - -import click -import pytest - -import flask -from flask import appcontext_popped -from flask.cli import ScriptInfo -from flask.globals import _cv_request -from flask.json import jsonify -from flask.testing import EnvironBuilder -from flask.testing import FlaskCliRunner - - -def test_environ_defaults_from_config(app, client): - app.config["SERVER_NAME"] = "example.com:1234" - app.config["APPLICATION_ROOT"] = "/foo" - - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context() - assert ctx.request.url == "http://example.com:1234/foo/" - - rv = client.get("/") - assert rv.data == b"http://example.com:1234/foo/" - - -def test_environ_defaults(app, client, app_ctx, req_ctx): - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context() - assert ctx.request.url == "http://localhost/" - with client: - rv = client.get("/") - assert rv.data == b"http://localhost/" - - -def test_environ_base_default(app, client): - @app.route("/") - def index(): - flask.g.remote_addr = flask.request.remote_addr - flask.g.user_agent = flask.request.user_agent.string - return "" - - with client: - client.get("/") - assert flask.g.remote_addr == "127.0.0.1" - assert flask.g.user_agent == ( - f"Werkzeug/{importlib.metadata.version('werkzeug')}" - ) - - -def test_environ_base_modified(app, client): - @app.route("/") - def index(): - flask.g.remote_addr = flask.request.remote_addr - flask.g.user_agent = flask.request.user_agent.string - return "" - - client.environ_base["REMOTE_ADDR"] = "192.168.0.22" - client.environ_base["HTTP_USER_AGENT"] = "Foo" - - with client: - client.get("/") - assert flask.g.remote_addr == "192.168.0.22" - assert flask.g.user_agent == "Foo" - - -def test_client_open_environ(app, client, request): - @app.route("/index") - def index(): - return flask.request.remote_addr - - builder = EnvironBuilder(app, path="/index", method="GET") - request.addfinalizer(builder.close) - - rv = client.open(builder) - assert rv.data == b"127.0.0.1" - - environ = builder.get_environ() - client.environ_base["REMOTE_ADDR"] = "127.0.0.2" - rv = client.open(environ) - assert rv.data == b"127.0.0.2" - - -def test_specify_url_scheme(app, client): - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context(url_scheme="https") - assert ctx.request.url == "https://localhost/" - - rv = client.get("/", url_scheme="https") - assert rv.data == b"https://localhost/" - - -def test_path_is_url(app): - eb = EnvironBuilder(app, "https://example.com/") - assert eb.url_scheme == "https" - assert eb.host == "example.com" - assert eb.script_root == "" - assert eb.path == "/" - - -def test_environbuilder_json_dumps(app): - """EnvironBuilder.json_dumps() takes settings from the app.""" - app.json.ensure_ascii = False - eb = EnvironBuilder(app, json="\u20ac") - assert eb.input_stream.read().decode("utf8") == '"\u20ac"' - - -def test_blueprint_with_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "example.com:1234" - app.config["APPLICATION_ROOT"] = "/foo" - client = app.test_client() - - bp = flask.Blueprint("company", __name__, subdomain="xxx") - - @bp.route("/") - def index(): - return flask.request.url - - app.register_blueprint(bp) - - ctx = app.test_request_context("/", subdomain="xxx") - assert ctx.request.url == "http://xxx.example.com:1234/foo/" - - with ctx: - assert ctx.request.blueprint == bp.name - - rv = client.get("/", subdomain="xxx") - assert rv.data == b"http://xxx.example.com:1234/foo/" - - -def test_redirect_keep_session(app, client, app_ctx): - @app.route("/", methods=["GET", "POST"]) - def index(): - if flask.request.method == "POST": - return flask.redirect("/getsession") - flask.session["data"] = "foo" - return "index" - - @app.route("/getsession") - def get_session(): - return flask.session.get("data", "") - - with client: - rv = client.get("/getsession") - assert rv.data == b"" - - rv = client.get("/") - assert rv.data == b"index" - assert flask.session.get("data") == "foo" - - rv = client.post("/", data={}, follow_redirects=True) - assert rv.data == b"foo" - assert flask.session.get("data") == "foo" - - rv = client.get("/getsession") - assert rv.data == b"foo" - - -def test_session_transactions(app, client): - @app.route("/") - def index(): - return str(flask.session["foo"]) - - with client: - with client.session_transaction() as sess: - assert len(sess) == 0 - sess["foo"] = [42] - assert len(sess) == 1 - rv = client.get("/") - assert rv.data == b"[42]" - with client.session_transaction() as sess: - assert len(sess) == 1 - assert sess["foo"] == [42] - - -def test_session_transactions_no_null_sessions(): - app = flask.Flask(__name__) - - with app.test_client() as c: - with pytest.raises(RuntimeError) as e: - with c.session_transaction(): - pass - assert "Session backend did not open a session" in str(e.value) - - -def test_session_transactions_keep_context(app, client, req_ctx): - client.get("/") - req = flask.request._get_current_object() - assert req is not None - with client.session_transaction(): - assert req is flask.request._get_current_object() - - -def test_session_transaction_needs_cookies(app): - c = app.test_client(use_cookies=False) - - with pytest.raises(TypeError, match="Cookies are disabled."): - with c.session_transaction(): - pass - - -def test_test_client_context_binding(app, client): - app.testing = False - - @app.route("/") - def index(): - flask.g.value = 42 - return "Hello World!" - - @app.route("/other") - def other(): - raise ZeroDivisionError - - with client: - resp = client.get("/") - assert flask.g.value == 42 - assert resp.data == b"Hello World!" - assert resp.status_code == 200 - - with client: - resp = client.get("/other") - assert not hasattr(flask.g, "value") - assert b"Internal Server Error" in resp.data - assert resp.status_code == 500 - flask.g.value = 23 - - with pytest.raises(RuntimeError): - flask.g.value # noqa: B018 - - -def test_reuse_client(client): - c = client - - with c: - assert client.get("/").status_code == 404 - - with c: - assert client.get("/").status_code == 404 - - -def test_full_url_request(app, client): - @app.route("/action", methods=["POST"]) - def action(): - return "x" - - with client: - rv = client.post("http://domain.com/action?vodka=42", data={"gin": 43}) - assert rv.status_code == 200 - assert "gin" in flask.request.form - assert "vodka" in flask.request.args - - -def test_json_request_and_response(app, client): - @app.route("/echo", methods=["POST"]) - def echo(): - return jsonify(flask.request.get_json()) - - with client: - json_data = {"drink": {"gin": 1, "tonic": True}, "price": 10} - rv = client.post("/echo", json=json_data) - - # Request should be in JSON - assert flask.request.is_json - assert flask.request.get_json() == json_data - - # Response should be in JSON - assert rv.status_code == 200 - assert rv.is_json - assert rv.get_json() == json_data - - -def test_client_json_no_app_context(app, client): - @app.route("/hello", methods=["POST"]) - def hello(): - return f"Hello, {flask.request.json['name']}!" - - class Namespace: - count = 0 - - def add(self, app): - self.count += 1 - - ns = Namespace() - - with appcontext_popped.connected_to(ns.add, app): - rv = client.post("/hello", json={"name": "Flask"}) - - assert rv.get_data(as_text=True) == "Hello, Flask!" - assert ns.count == 1 - - -def test_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "example.com" - client = app.test_client() - - @app.route("/", subdomain="") - def view(company_id): - return company_id - - with app.test_request_context(): - url = flask.url_for("view", company_id="xxx") - - with client: - response = client.get(url) - - assert 200 == response.status_code - assert b"xxx" == response.data - - -def test_nosubdomain(app, client): - app.config["SERVER_NAME"] = "example.com" - - @app.route("/") - def view(company_id): - return company_id - - with app.test_request_context(): - url = flask.url_for("view", company_id="xxx") - - with client: - response = client.get(url) - - assert 200 == response.status_code - assert b"xxx" == response.data - - -def test_cli_runner_class(app): - runner = app.test_cli_runner() - assert isinstance(runner, FlaskCliRunner) - - class SubRunner(FlaskCliRunner): - pass - - app.test_cli_runner_class = SubRunner - runner = app.test_cli_runner() - assert isinstance(runner, SubRunner) - - -def test_cli_invoke(app): - @app.cli.command("hello") - def hello_command(): - click.echo("Hello, World!") - - runner = app.test_cli_runner() - # invoke with command name - result = runner.invoke(args=["hello"]) - assert "Hello" in result.output - # invoke with command object - result = runner.invoke(hello_command) - assert "Hello" in result.output - - -def test_cli_custom_obj(app): - class NS: - called = False - - def create_app(): - NS.called = True - return app - - @app.cli.command("hello") - def hello_command(): - click.echo("Hello, World!") - - script_info = ScriptInfo(create_app=create_app) - runner = app.test_cli_runner() - runner.invoke(hello_command, obj=script_info) - assert NS.called - - -def test_client_pop_all_preserved(app, req_ctx, client): - @app.route("/") - def index(): - # stream_with_context pushes a third context, preserved by response - return flask.stream_with_context("hello") - - # req_ctx fixture pushed an initial context - with client: - # request pushes a second request context, preserved by client - rv = client.get("/") - - # close the response, releasing the context held by stream_with_context - rv.close() - # only req_ctx fixture should still be pushed - assert _cv_request.get(None) is req_ctx diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py deleted file mode 100644 index 79c5a73c..00000000 --- a/tests/test_user_error_handler.py +++ /dev/null @@ -1,295 +0,0 @@ -import pytest -from werkzeug.exceptions import Forbidden -from werkzeug.exceptions import HTTPException -from werkzeug.exceptions import InternalServerError -from werkzeug.exceptions import NotFound - -import flask - - -def test_error_handler_no_match(app, client): - class CustomException(Exception): - pass - - @app.errorhandler(CustomException) - def custom_exception_handler(e): - assert isinstance(e, CustomException) - return "custom" - - with pytest.raises(TypeError) as exc_info: - app.register_error_handler(CustomException(), None) - - assert "CustomException() is an instance, not a class." in str(exc_info.value) - - with pytest.raises(ValueError) as exc_info: - app.register_error_handler(list, None) - - assert "'list' is not a subclass of Exception." in str(exc_info.value) - - @app.errorhandler(500) - def handle_500(e): - assert isinstance(e, InternalServerError) - - if e.original_exception is not None: - return f"wrapped {type(e.original_exception).__name__}" - - return "direct" - - with pytest.raises(ValueError) as exc_info: - app.register_error_handler(999, None) - - assert "Use a subclass of HTTPException" in str(exc_info.value) - - @app.route("/custom") - def custom_test(): - raise CustomException() - - @app.route("/keyerror") - def key_error(): - raise KeyError() - - @app.route("/abort") - def do_abort(): - flask.abort(500) - - app.testing = False - assert client.get("/custom").data == b"custom" - assert client.get("/keyerror").data == b"wrapped KeyError" - assert client.get("/abort").data == b"direct" - - -def test_error_handler_subclass(app): - class ParentException(Exception): - pass - - class ChildExceptionUnregistered(ParentException): - pass - - class ChildExceptionRegistered(ParentException): - pass - - @app.errorhandler(ParentException) - def parent_exception_handler(e): - assert isinstance(e, ParentException) - return "parent" - - @app.errorhandler(ChildExceptionRegistered) - def child_exception_handler(e): - assert isinstance(e, ChildExceptionRegistered) - return "child-registered" - - @app.route("/parent") - def parent_test(): - raise ParentException() - - @app.route("/child-unregistered") - def unregistered_test(): - raise ChildExceptionUnregistered() - - @app.route("/child-registered") - def registered_test(): - raise ChildExceptionRegistered() - - c = app.test_client() - - assert c.get("/parent").data == b"parent" - assert c.get("/child-unregistered").data == b"parent" - assert c.get("/child-registered").data == b"child-registered" - - -def test_error_handler_http_subclass(app): - class ForbiddenSubclassRegistered(Forbidden): - pass - - class ForbiddenSubclassUnregistered(Forbidden): - pass - - @app.errorhandler(403) - def code_exception_handler(e): - assert isinstance(e, Forbidden) - return "forbidden" - - @app.errorhandler(ForbiddenSubclassRegistered) - def subclass_exception_handler(e): - assert isinstance(e, ForbiddenSubclassRegistered) - return "forbidden-registered" - - @app.route("/forbidden") - def forbidden_test(): - raise Forbidden() - - @app.route("/forbidden-registered") - def registered_test(): - raise ForbiddenSubclassRegistered() - - @app.route("/forbidden-unregistered") - def unregistered_test(): - raise ForbiddenSubclassUnregistered() - - c = app.test_client() - - assert c.get("/forbidden").data == b"forbidden" - assert c.get("/forbidden-unregistered").data == b"forbidden" - assert c.get("/forbidden-registered").data == b"forbidden-registered" - - -def test_error_handler_blueprint(app): - bp = flask.Blueprint("bp", __name__) - - @bp.errorhandler(500) - def bp_exception_handler(e): - return "bp-error" - - @bp.route("/error") - def bp_test(): - raise InternalServerError() - - @app.errorhandler(500) - def app_exception_handler(e): - return "app-error" - - @app.route("/error") - def app_test(): - raise InternalServerError() - - app.register_blueprint(bp, url_prefix="/bp") - - c = app.test_client() - - assert c.get("/error").data == b"app-error" - assert c.get("/bp/error").data == b"bp-error" - - -def test_default_error_handler(): - bp = flask.Blueprint("bp", __name__) - - @bp.errorhandler(HTTPException) - def bp_exception_handler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return "bp-default" - - @bp.errorhandler(Forbidden) - def bp_forbidden_handler(e): - assert isinstance(e, Forbidden) - return "bp-forbidden" - - @bp.route("/undefined") - def bp_registered_test(): - raise NotFound() - - @bp.route("/forbidden") - def bp_forbidden_test(): - raise Forbidden() - - app = flask.Flask(__name__) - - @app.errorhandler(HTTPException) - def catchall_exception_handler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return "default" - - @app.errorhandler(Forbidden) - def catchall_forbidden_handler(e): - assert isinstance(e, Forbidden) - return "forbidden" - - @app.route("/forbidden") - def forbidden(): - raise Forbidden() - - @app.route("/slash/") - def slash(): - return "slash" - - app.register_blueprint(bp, url_prefix="/bp") - - c = app.test_client() - assert c.get("/bp/undefined").data == b"bp-default" - assert c.get("/bp/forbidden").data == b"bp-forbidden" - assert c.get("/undefined").data == b"default" - assert c.get("/forbidden").data == b"forbidden" - # Don't handle RequestRedirect raised when adding slash. - assert c.get("/slash", follow_redirects=True).data == b"slash" - - -class TestGenericHandlers: - """Test how very generic handlers are dispatched to.""" - - class Custom(Exception): - pass - - @pytest.fixture() - def app(self, app): - @app.route("/custom") - def do_custom(): - raise self.Custom() - - @app.route("/error") - def do_error(): - raise KeyError() - - @app.route("/abort") - def do_abort(): - flask.abort(500) - - @app.route("/raise") - def do_raise(): - raise InternalServerError() - - app.config["PROPAGATE_EXCEPTIONS"] = False - return app - - def report_error(self, e): - original = getattr(e, "original_exception", None) - - if original is not None: - return f"wrapped {type(original).__name__}" - - return f"direct {type(e).__name__}" - - @pytest.mark.parametrize("to_handle", (InternalServerError, 500)) - def test_handle_class_or_code(self, app, client, to_handle): - """``InternalServerError`` and ``500`` are aliases, they should - have the same behavior. Both should only receive - ``InternalServerError``, which might wrap another error. - """ - - @app.errorhandler(to_handle) - def handle_500(e): - assert isinstance(e, InternalServerError) - return self.report_error(e) - - assert client.get("/custom").data == b"wrapped Custom" - assert client.get("/error").data == b"wrapped KeyError" - assert client.get("/abort").data == b"direct InternalServerError" - assert client.get("/raise").data == b"direct InternalServerError" - - def test_handle_generic_http(self, app, client): - """``HTTPException`` should only receive ``HTTPException`` - subclasses. It will receive ``404`` routing exceptions. - """ - - @app.errorhandler(HTTPException) - def handle_http(e): - assert isinstance(e, HTTPException) - return str(e.code) - - assert client.get("/error").data == b"500" - assert client.get("/abort").data == b"500" - assert client.get("/not-found").data == b"404" - - def test_handle_generic(self, app, client): - """Generic ``Exception`` will handle all exceptions directly, - including ``HTTPExceptions``. - """ - - @app.errorhandler(Exception) - def handle_exception(e): - return self.report_error(e) - - assert client.get("/custom").data == b"direct Custom" - assert client.get("/error").data == b"direct KeyError" - assert client.get("/abort").data == b"direct InternalServerError" - assert client.get("/not-found").data == b"direct NotFound" diff --git a/tests/test_views.py b/tests/test_views.py deleted file mode 100644 index eab5eda2..00000000 --- a/tests/test_views.py +++ /dev/null @@ -1,260 +0,0 @@ -import pytest -from werkzeug.http import parse_set_header - -import flask.views - - -def common_test(app): - c = app.test_client() - - assert c.get("/").data == b"GET" - assert c.post("/").data == b"POST" - assert c.put("/").status_code == 405 - meths = parse_set_header(c.open("/", method="OPTIONS").headers["Allow"]) - assert sorted(meths) == ["GET", "HEAD", "OPTIONS", "POST"] - - -def test_basic_view(app): - class Index(flask.views.View): - methods = ["GET", "POST"] - - def dispatch_request(self): - return flask.request.method - - app.add_url_rule("/", view_func=Index.as_view("index")) - common_test(app) - - -def test_method_based_view(app): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def post(self): - return "POST" - - app.add_url_rule("/", view_func=Index.as_view("index")) - - common_test(app) - - -def test_view_patching(app): - class Index(flask.views.MethodView): - def get(self): - raise ZeroDivisionError - - def post(self): - raise ZeroDivisionError - - class Other(Index): - def get(self): - return "GET" - - def post(self): - return "POST" - - view = Index.as_view("index") - view.view_class = Other - app.add_url_rule("/", view_func=view) - common_test(app) - - -def test_view_inheritance(app, client): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def post(self): - return "POST" - - class BetterIndex(Index): - def delete(self): - return "DELETE" - - app.add_url_rule("/", view_func=BetterIndex.as_view("index")) - - meths = parse_set_header(client.open("/", method="OPTIONS").headers["Allow"]) - assert sorted(meths) == ["DELETE", "GET", "HEAD", "OPTIONS", "POST"] - - -def test_view_decorators(app, client): - def add_x_parachute(f): - def new_function(*args, **kwargs): - resp = flask.make_response(f(*args, **kwargs)) - resp.headers["X-Parachute"] = "awesome" - return resp - - return new_function - - class Index(flask.views.View): - decorators = [add_x_parachute] - - def dispatch_request(self): - return "Awesome" - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.headers["X-Parachute"] == "awesome" - assert rv.data == b"Awesome" - - -def test_view_provide_automatic_options_attr(): - app = flask.Flask(__name__) - - class Index1(flask.views.View): - provide_automatic_options = False - - def dispatch_request(self): - return "Hello World!" - - app.add_url_rule("/", view_func=Index1.as_view("index")) - c = app.test_client() - rv = c.open("/", method="OPTIONS") - assert rv.status_code == 405 - - app = flask.Flask(__name__) - - class Index2(flask.views.View): - methods = ["OPTIONS"] - provide_automatic_options = True - - def dispatch_request(self): - return "Hello World!" - - app.add_url_rule("/", view_func=Index2.as_view("index")) - c = app.test_client() - rv = c.open("/", method="OPTIONS") - assert sorted(rv.allow) == ["OPTIONS"] - - app = flask.Flask(__name__) - - class Index3(flask.views.View): - def dispatch_request(self): - return "Hello World!" - - app.add_url_rule("/", view_func=Index3.as_view("index")) - c = app.test_client() - rv = c.open("/", method="OPTIONS") - assert "OPTIONS" in rv.allow - - -def test_implicit_head(app, client): - class Index(flask.views.MethodView): - def get(self): - return flask.Response("Blub", headers={"X-Method": flask.request.method}) - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.data == b"Blub" - assert rv.headers["X-Method"] == "GET" - rv = client.head("/") - assert rv.data == b"" - assert rv.headers["X-Method"] == "HEAD" - - -def test_explicit_head(app, client): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def head(self): - return flask.Response("", headers={"X-Method": "HEAD"}) - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.data == b"GET" - rv = client.head("/") - assert rv.data == b"" - assert rv.headers["X-Method"] == "HEAD" - - -def test_endpoint_override(app): - app.debug = True - - class Index(flask.views.View): - methods = ["GET", "POST"] - - def dispatch_request(self): - return flask.request.method - - app.add_url_rule("/", view_func=Index.as_view("index")) - - with pytest.raises(AssertionError): - app.add_url_rule("/", view_func=Index.as_view("index")) - - # But these tests should still pass. We just log a warning. - common_test(app) - - -def test_methods_var_inheritance(app, client): - class BaseView(flask.views.MethodView): - methods = ["GET", "PROPFIND"] - - class ChildView(BaseView): - def get(self): - return "GET" - - def propfind(self): - return "PROPFIND" - - app.add_url_rule("/", view_func=ChildView.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.open("/", method="PROPFIND").data == b"PROPFIND" - assert ChildView.methods == {"PROPFIND", "GET"} - - -def test_multiple_inheritance(app, client): - class GetView(flask.views.MethodView): - def get(self): - return "GET" - - class DeleteView(flask.views.MethodView): - def delete(self): - return "DELETE" - - class GetDeleteView(GetView, DeleteView): - pass - - app.add_url_rule("/", view_func=GetDeleteView.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.delete("/").data == b"DELETE" - assert sorted(GetDeleteView.methods) == ["DELETE", "GET"] - - -def test_remove_method_from_parent(app, client): - class GetView(flask.views.MethodView): - def get(self): - return "GET" - - class OtherView(flask.views.MethodView): - def post(self): - return "POST" - - class View(GetView, OtherView): - methods = ["GET"] - - app.add_url_rule("/", view_func=View.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.post("/").status_code == 405 - assert sorted(View.methods) == ["GET"] - - -def test_init_once(app, client): - n = 0 - - class CountInit(flask.views.View): - init_every_request = False - - def __init__(self): - nonlocal n - n += 1 - - def dispatch_request(self): - return str(n) - - app.add_url_rule("/", view_func=CountInit.as_view("index")) - assert client.get("/").data == b"1" - assert client.get("/").data == b"1" diff --git a/tests/typing/typing_app_decorators.py b/tests/typing/typing_app_decorators.py deleted file mode 100644 index 0e25a30c..00000000 --- a/tests/typing/typing_app_decorators.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from flask import Flask -from flask import Response - -app = Flask(__name__) - - -@app.after_request -def after_sync(response: Response) -> Response: - return Response() - - -@app.after_request -async def after_async(response: Response) -> Response: - return Response() - - -@app.before_request -def before_sync() -> None: ... - - -@app.before_request -async def before_async() -> None: ... - - -@app.teardown_appcontext -def teardown_sync(exc: BaseException | None) -> None: ... - - -@app.teardown_appcontext -async def teardown_async(exc: BaseException | None) -> None: ... diff --git a/tests/typing/typing_error_handler.py b/tests/typing/typing_error_handler.py deleted file mode 100644 index ec9c886f..00000000 --- a/tests/typing/typing_error_handler.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from http import HTTPStatus - -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import NotFound - -from flask import Flask - -app = Flask(__name__) - - -@app.errorhandler(400) -@app.errorhandler(HTTPStatus.BAD_REQUEST) -@app.errorhandler(BadRequest) -def handle_400(e: BadRequest) -> str: - return "" - - -@app.errorhandler(ValueError) -def handle_custom(e: ValueError) -> str: - return "" - - -@app.errorhandler(ValueError) -def handle_accept_base(e: Exception) -> str: - return "" - - -@app.errorhandler(BadRequest) -@app.errorhandler(404) -def handle_multiple(e: BadRequest | NotFound) -> str: - return "" diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py deleted file mode 100644 index 8bc271b2..00000000 --- a/tests/typing/typing_route.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import typing as t -from http import HTTPStatus - -from flask import Flask -from flask import jsonify -from flask import stream_template -from flask.templating import render_template -from flask.views import View -from flask.wrappers import Response - -app = Flask(__name__) - - -@app.route("/str") -def hello_str() -> str: - return "

Hello, World!

" - - -@app.route("/bytes") -def hello_bytes() -> bytes: - return b"

Hello, World!

" - - -@app.route("/json") -def hello_json() -> Response: - return jsonify("Hello, World!") - - -@app.route("/json/dict") -def hello_json_dict() -> dict[str, t.Any]: - return {"response": "Hello, World!"} - - -@app.route("/json/dict") -def hello_json_list() -> list[t.Any]: - return [{"message": "Hello"}, {"message": "World"}] - - -class StatusJSON(t.TypedDict): - status: str - - -@app.route("/typed-dict") -def typed_dict() -> StatusJSON: - return {"status": "ok"} - - -@app.route("/generator") -def hello_generator() -> t.Generator[str, None, None]: - def show() -> t.Generator[str, None, None]: - for x in range(100): - yield f"data:{x}\n\n" - - return show() - - -@app.route("/generator-expression") -def hello_generator_expression() -> t.Iterator[bytes]: - return (f"data:{x}\n\n".encode() for x in range(100)) - - -@app.route("/iterator") -def hello_iterator() -> t.Iterator[str]: - return iter([f"data:{x}\n\n" for x in range(100)]) - - -@app.route("/status") -@app.route("/status/") -def tuple_status(code: int = 200) -> tuple[str, int]: - return "hello", code - - -@app.route("/status-enum") -def tuple_status_enum() -> tuple[str, int]: - return "hello", HTTPStatus.OK - - -@app.route("/headers") -def tuple_headers() -> tuple[str, dict[str, str]]: - return "Hello, World!", {"Content-Type": "text/plain"} - - -@app.route("/template") -@app.route("/template/") -def return_template(name: str | None = None) -> str: - return render_template("index.html", name=name) - - -@app.route("/template") -def return_template_stream() -> t.Iterator[str]: - return stream_template("index.html", name="Hello") - - -@app.route("/async") -async def async_route() -> str: - return "Hello" - - -class RenderTemplateView(View): - def __init__(self: RenderTemplateView, template_name: str) -> None: - self.template_name = template_name - - def dispatch_request(self: RenderTemplateView) -> str: - return render_template(self.template_name) - - -app.add_url_rule( - "/about", - view_func=RenderTemplateView.as_view("about_page", template_name="about.html"), -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 1217581c..00000000 --- a/tox.ini +++ /dev/null @@ -1,58 +0,0 @@ -[tox] -envlist = - py3{13,12,11,10,9,8} - pypy310 - py312-min - py38-dev - style - typing - docs -skip_missing_interpreters = true - -[testenv] -package = wheel -wheel_build_env = .pkg -envtmpdir = {toxworkdir}/tmp/{envname} -constrain_package_deps = true -use_frozen_constraints = true -deps = - -r requirements/tests.txt - min: -r requirements-skip/tests-min.txt - dev: -r requirements-skip/tests-dev.txt -commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} - -[testenv:style] -deps = pre-commit -skip_install = true -commands = pre-commit run --all-files - -[testenv:typing] -deps = -r requirements/typing.txt -commands = mypy - -[testenv:docs] -deps = -r requirements/docs.txt -commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml - -[testenv:update-actions] -labels = update -deps = gha-update -commands = gha-update - -[testenv:update-pre_commit] -labels = update -deps = pre-commit -skip_install = true -commands = pre-commit autoupdate -j4 - -[testenv:update-requirements] -labels = update -deps = pip-tools -skip_install = true -change_dir = requirements -commands = - pip-compile build.in -q {posargs:-U} - pip-compile docs.in -q {posargs:-U} - pip-compile tests.in -q {posargs:-U} - pip-compile typing.in -q {posargs:-U} - pip-compile dev.in -q {posargs:-U}