From 127440c727213f4bcf1aaf119e1ce5c7923b6bef Mon Sep 17 00:00:00 2001 From: Jose Palazon Date: Fri, 6 Sep 2024 00:18:20 +0100 Subject: [PATCH] I just want the tutorial --- CHANGES.rst | 1574 -------------- CODE_OF_CONDUCT.md | 76 - CONTRIBUTING.rst | 238 --- .../javascript/LICENSE.rst => LICENSE.rst | 0 LICENSE.txt | 28 - README.md | 45 - examples/tutorial/README.rst => README.rst | 0 docs/Makefile | 20 - docs/_static/debugger.png | Bin 207889 -> 0 bytes docs/_static/flask-horizontal.png | Bin 24796 -> 0 bytes docs/_static/flask-vertical.png | Bin 16021 -> 0 bytes docs/_static/pycharm-run-config.png | Bin 99654 -> 0 bytes docs/_static/shortcut-icon.png | Bin 4027 -> 0 bytes docs/api.rst | 717 ------- docs/appcontext.rst | 147 -- docs/async-await.rst | 131 -- docs/blueprints.rst | 315 --- docs/changes.rst | 4 - docs/cli.rst | 556 ----- docs/conf.py | 97 - docs/config.rst | 737 ------- docs/contributing.rst | 1 - docs/debugging.rst | 99 - docs/deploying/apache-httpd.rst | 66 - docs/deploying/asgi.rst | 27 - docs/deploying/eventlet.rst | 80 - docs/deploying/gevent.rst | 80 - docs/deploying/gunicorn.rst | 130 -- docs/deploying/index.rst | 79 - docs/deploying/mod_wsgi.rst | 94 - docs/deploying/nginx.rst | 69 - docs/deploying/proxy_fix.rst | 33 - docs/deploying/uwsgi.rst | 145 -- docs/deploying/waitress.rst | 75 - docs/design.rst | 228 -- docs/errorhandling.rst | 523 ----- docs/extensiondev.rst | 303 --- docs/extensions.rst | 48 - docs/index.rst | 88 - docs/installation.rst | 144 -- docs/license.rst | 5 - docs/lifecycle.rst | 168 -- docs/logging.rst | 183 -- docs/make.bat | 35 - docs/patterns/appdispatch.rst | 189 -- docs/patterns/appfactories.rst | 118 - docs/patterns/caching.rst | 16 - docs/patterns/celery.rst | 242 --- docs/patterns/deferredcallbacks.rst | 44 - docs/patterns/favicon.rst | 53 - docs/patterns/fileuploads.rst | 182 -- docs/patterns/flashing.rst | 148 -- docs/patterns/index.rst | 40 - docs/patterns/javascript.rst | 259 --- docs/patterns/jquery.rst | 6 - docs/patterns/lazyloading.rst | 109 - docs/patterns/methodoverrides.rst | 42 - docs/patterns/mongoengine.rst | 103 - docs/patterns/packages.rst | 133 -- docs/patterns/requestchecksum.rst | 55 - docs/patterns/singlepageapplications.rst | 24 - docs/patterns/sqlalchemy.rst | 214 -- docs/patterns/sqlite3.rst | 147 -- docs/patterns/streaming.rst | 85 - docs/patterns/subclassing.rst | 17 - docs/patterns/templateinheritance.rst | 68 - docs/patterns/urlprocessors.rst | 126 -- docs/patterns/viewdecorators.rst | 171 -- docs/patterns/wtforms.rst | 126 -- docs/quickstart.rst | 907 -------- docs/reqcontext.rst | 243 --- docs/server.rst | 115 - docs/shell.rst | 100 - docs/signals.rst | 167 -- docs/templating.rst | 229 -- docs/testing.rst | 319 --- docs/tutorial/blog.rst | 336 --- docs/tutorial/database.rst | 209 -- docs/tutorial/deploy.rst | 111 - docs/tutorial/factory.rst | 162 -- docs/tutorial/flaskr_edit.png | Bin 13259 -> 0 bytes docs/tutorial/flaskr_index.png | Bin 11675 -> 0 bytes docs/tutorial/flaskr_login.png | Bin 7455 -> 0 bytes docs/tutorial/index.rst | 64 - docs/tutorial/install.rst | 89 - docs/tutorial/layout.rst | 110 - docs/tutorial/next.rst | 38 - docs/tutorial/static.rst | 72 - docs/tutorial/templates.rst | 187 -- docs/tutorial/tests.rst | 559 ----- docs/tutorial/views.rst | 305 --- docs/views.rst | 324 --- docs/web-security.rst | 274 --- examples/celery/README.md | 27 - examples/celery/make_celery.py | 4 - examples/celery/pyproject.toml | 17 - examples/celery/requirements.txt | 58 - examples/celery/src/task_app/__init__.py | 39 - examples/celery/src/task_app/tasks.py | 23 - .../celery/src/task_app/templates/index.html | 108 - examples/celery/src/task_app/views.py | 38 - examples/javascript/.gitignore | 14 - examples/javascript/README.rst | 48 - examples/javascript/js_example/__init__.py | 5 - .../javascript/js_example/templates/base.html | 33 - .../js_example/templates/fetch.html | 33 - .../js_example/templates/jquery.html | 27 - .../javascript/js_example/templates/xhr.html | 29 - examples/javascript/js_example/views.py | 18 - examples/javascript/pyproject.toml | 32 - examples/javascript/tests/conftest.py | 15 - examples/javascript/tests/test_js_example.py | 27 - examples/tutorial/.gitignore | 14 - examples/tutorial/LICENSE.rst | 28 - examples/tutorial/pyproject.toml | 39 - examples/tutorial/tests/conftest.py | 62 - .../tutorial/flaskr => flaskr}/__init__.py | 0 {examples/tutorial/flaskr => flaskr}/auth.py | 0 {examples/tutorial/flaskr => flaskr}/blog.py | 0 {examples/tutorial/flaskr => flaskr}/db.py | 0 .../tutorial/flaskr => flaskr}/schema.sql | 0 .../flaskr => flaskr}/static/style.css | 0 .../templates/auth/login.html | 0 .../templates/auth/register.html | 0 .../flaskr => flaskr}/templates/base.html | 0 .../templates/blog/create.html | 0 .../templates/blog/index.html | 0 .../templates/blog/update.html | 0 pyproject.toml | 107 +- requirements-skip/README.md | 2 - requirements-skip/tests-dev.txt | 6 - requirements-skip/tests-min.in | 6 - requirements-skip/tests-min.txt | 21 - requirements/build.in | 1 - requirements/build.txt | 12 - requirements/dev.in | 5 - requirements/dev.txt | 192 -- requirements/docs.in | 4 - requirements/docs.txt | 64 - requirements/tests.in | 4 - requirements/tests.txt | 18 - requirements/typing.in | 8 - requirements/typing.txt | 38 - src/flask/__init__.py | 60 - src/flask/__main__.py | 3 - src/flask/app.py | 1515 ------------- src/flask/blueprints.py | 128 -- src/flask/cli.py | 1109 ---------- src/flask/config.py | 370 ---- src/flask/ctx.py | 449 ---- src/flask/debughelpers.py | 178 -- src/flask/globals.py | 51 - src/flask/helpers.py | 633 ------ src/flask/json/__init__.py | 170 -- src/flask/json/provider.py | 215 -- src/flask/json/tag.py | 327 --- src/flask/logging.py | 79 - src/flask/py.typed | 0 src/flask/sansio/README.md | 6 - src/flask/sansio/app.py | 964 --------- src/flask/sansio/blueprints.py | 632 ------ src/flask/sansio/scaffold.py | 801 ------- src/flask/sessions.py | 379 ---- src/flask/signals.py | 17 - src/flask/templating.py | 219 -- src/flask/testing.py | 298 --- src/flask/typing.py | 90 - src/flask/views.py | 191 -- src/flask/wrappers.py | 174 -- tests/conftest.py | 168 +- {examples/tutorial/tests => tests}/data.sql | 0 tests/static/config.json | 4 - tests/static/config.toml | 2 - tests/static/index.html | 1 - tests/templates/_macro.html | 1 - tests/templates/context_template.html | 1 - tests/templates/escaping_template.html | 6 - tests/templates/mail.txt | 1 - tests/templates/nested/nested.txt | 1 - tests/templates/non_escaping_template.txt | 8 - tests/templates/simple_template.html | 1 - tests/templates/template_filter.html | 1 - tests/templates/template_test.html | 3 - tests/test_appctx.py | 209 -- tests/test_apps/.env | 4 - tests/test_apps/.flaskenv | 3 - tests/test_apps/blueprintapp/__init__.py | 9 - tests/test_apps/blueprintapp/apps/__init__.py | 0 .../blueprintapp/apps/admin/__init__.py | 20 - .../apps/admin/static/css/test.css | 1 - .../blueprintapp/apps/admin/static/test.txt | 1 - .../apps/admin/templates/admin/index.html | 1 - .../blueprintapp/apps/frontend/__init__.py | 14 - .../frontend/templates/frontend/index.html | 1 - tests/test_apps/cliapp/__init__.py | 0 tests/test_apps/cliapp/app.py | 3 - tests/test_apps/cliapp/factory.py | 13 - tests/test_apps/cliapp/importerrorapp.py | 5 - tests/test_apps/cliapp/inner1/__init__.py | 3 - .../cliapp/inner1/inner2/__init__.py | 0 tests/test_apps/cliapp/inner1/inner2/flask.py | 3 - tests/test_apps/cliapp/message.txt | 1 - tests/test_apps/cliapp/multiapp.py | 4 - tests/test_apps/helloworld/hello.py | 8 - tests/test_apps/helloworld/wsgi.py | 1 - .../test_apps/subdomaintestmodule/__init__.py | 3 - .../subdomaintestmodule/static/hello.txt | 1 - tests/test_async.py | 145 -- .../tutorial/tests => tests}/test_auth.py | 0 tests/test_basic.py | 1890 ----------------- .../tutorial/tests => tests}/test_blog.py | 0 tests/test_blueprints.py | 1054 --------- tests/test_cli.py | 686 ------ tests/test_config.py | 250 --- tests/test_converters.py | 42 - {examples/tutorial/tests => tests}/test_db.py | 0 .../tutorial/tests => tests}/test_factory.py | 0 tests/test_helpers.py | 360 ---- tests/test_instance_config.py | 111 - tests/test_json.py | 346 --- tests/test_json_tag.py | 86 - tests/test_logging.py | 98 - tests/test_regression.py | 30 - tests/test_reqctx.py | 325 --- tests/test_session_interface.py | 28 - tests/test_signals.py | 181 -- tests/test_subclassing.py | 21 - tests/test_templating.py | 451 ---- tests/test_testing.py | 396 ---- tests/test_user_error_handler.py | 295 --- tests/test_views.py | 260 --- tests/typing/typing_app_decorators.py | 32 - tests/typing/typing_error_handler.py | 33 - tests/typing/typing_route.py | 112 - tox.ini | 58 - 235 files changed, 46 insertions(+), 33059 deletions(-) delete mode 100644 CHANGES.rst delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.rst rename examples/javascript/LICENSE.rst => LICENSE.rst (100%) delete mode 100644 LICENSE.txt delete mode 100644 README.md rename examples/tutorial/README.rst => README.rst (100%) delete mode 100644 docs/Makefile delete mode 100644 docs/_static/debugger.png delete mode 100644 docs/_static/flask-horizontal.png delete mode 100644 docs/_static/flask-vertical.png delete mode 100644 docs/_static/pycharm-run-config.png delete mode 100644 docs/_static/shortcut-icon.png delete mode 100644 docs/api.rst delete mode 100644 docs/appcontext.rst delete mode 100644 docs/async-await.rst delete mode 100644 docs/blueprints.rst delete mode 100644 docs/changes.rst delete mode 100644 docs/cli.rst delete mode 100644 docs/conf.py delete mode 100644 docs/config.rst delete mode 100644 docs/contributing.rst delete mode 100644 docs/debugging.rst delete mode 100644 docs/deploying/apache-httpd.rst delete mode 100644 docs/deploying/asgi.rst delete mode 100644 docs/deploying/eventlet.rst delete mode 100644 docs/deploying/gevent.rst delete mode 100644 docs/deploying/gunicorn.rst delete mode 100644 docs/deploying/index.rst delete mode 100644 docs/deploying/mod_wsgi.rst delete mode 100644 docs/deploying/nginx.rst delete mode 100644 docs/deploying/proxy_fix.rst delete mode 100644 docs/deploying/uwsgi.rst delete mode 100644 docs/deploying/waitress.rst delete mode 100644 docs/design.rst delete mode 100644 docs/errorhandling.rst delete mode 100644 docs/extensiondev.rst delete mode 100644 docs/extensions.rst delete mode 100644 docs/index.rst delete mode 100644 docs/installation.rst delete mode 100644 docs/license.rst delete mode 100644 docs/lifecycle.rst delete mode 100644 docs/logging.rst delete mode 100644 docs/make.bat delete mode 100644 docs/patterns/appdispatch.rst delete mode 100644 docs/patterns/appfactories.rst delete mode 100644 docs/patterns/caching.rst delete mode 100644 docs/patterns/celery.rst delete mode 100644 docs/patterns/deferredcallbacks.rst delete mode 100644 docs/patterns/favicon.rst delete mode 100644 docs/patterns/fileuploads.rst delete mode 100644 docs/patterns/flashing.rst delete mode 100644 docs/patterns/index.rst delete mode 100644 docs/patterns/javascript.rst delete mode 100644 docs/patterns/jquery.rst delete mode 100644 docs/patterns/lazyloading.rst delete mode 100644 docs/patterns/methodoverrides.rst delete mode 100644 docs/patterns/mongoengine.rst delete mode 100644 docs/patterns/packages.rst delete mode 100644 docs/patterns/requestchecksum.rst delete mode 100644 docs/patterns/singlepageapplications.rst delete mode 100644 docs/patterns/sqlalchemy.rst delete mode 100644 docs/patterns/sqlite3.rst delete mode 100644 docs/patterns/streaming.rst delete mode 100644 docs/patterns/subclassing.rst delete mode 100644 docs/patterns/templateinheritance.rst delete mode 100644 docs/patterns/urlprocessors.rst delete mode 100644 docs/patterns/viewdecorators.rst delete mode 100644 docs/patterns/wtforms.rst delete mode 100644 docs/quickstart.rst delete mode 100644 docs/reqcontext.rst delete mode 100644 docs/server.rst delete mode 100644 docs/shell.rst delete mode 100644 docs/signals.rst delete mode 100644 docs/templating.rst delete mode 100644 docs/testing.rst delete mode 100644 docs/tutorial/blog.rst delete mode 100644 docs/tutorial/database.rst delete mode 100644 docs/tutorial/deploy.rst delete mode 100644 docs/tutorial/factory.rst delete mode 100644 docs/tutorial/flaskr_edit.png delete mode 100644 docs/tutorial/flaskr_index.png delete mode 100644 docs/tutorial/flaskr_login.png delete mode 100644 docs/tutorial/index.rst delete mode 100644 docs/tutorial/install.rst delete mode 100644 docs/tutorial/layout.rst delete mode 100644 docs/tutorial/next.rst delete mode 100644 docs/tutorial/static.rst delete mode 100644 docs/tutorial/templates.rst delete mode 100644 docs/tutorial/tests.rst delete mode 100644 docs/tutorial/views.rst delete mode 100644 docs/views.rst delete mode 100644 docs/web-security.rst delete mode 100644 examples/celery/README.md delete mode 100644 examples/celery/make_celery.py delete mode 100644 examples/celery/pyproject.toml delete mode 100644 examples/celery/requirements.txt delete mode 100644 examples/celery/src/task_app/__init__.py delete mode 100644 examples/celery/src/task_app/tasks.py delete mode 100644 examples/celery/src/task_app/templates/index.html delete mode 100644 examples/celery/src/task_app/views.py delete mode 100644 examples/javascript/.gitignore delete mode 100644 examples/javascript/README.rst delete mode 100644 examples/javascript/js_example/__init__.py delete mode 100644 examples/javascript/js_example/templates/base.html delete mode 100644 examples/javascript/js_example/templates/fetch.html delete mode 100644 examples/javascript/js_example/templates/jquery.html delete mode 100644 examples/javascript/js_example/templates/xhr.html delete mode 100644 examples/javascript/js_example/views.py delete mode 100644 examples/javascript/pyproject.toml delete mode 100644 examples/javascript/tests/conftest.py delete mode 100644 examples/javascript/tests/test_js_example.py delete mode 100644 examples/tutorial/.gitignore delete mode 100644 examples/tutorial/LICENSE.rst delete mode 100644 examples/tutorial/pyproject.toml delete mode 100644 examples/tutorial/tests/conftest.py rename {examples/tutorial/flaskr => flaskr}/__init__.py (100%) rename {examples/tutorial/flaskr => flaskr}/auth.py (100%) rename {examples/tutorial/flaskr => flaskr}/blog.py (100%) rename {examples/tutorial/flaskr => flaskr}/db.py (100%) rename {examples/tutorial/flaskr => flaskr}/schema.sql (100%) rename {examples/tutorial/flaskr => flaskr}/static/style.css (100%) rename {examples/tutorial/flaskr => flaskr}/templates/auth/login.html (100%) rename {examples/tutorial/flaskr => flaskr}/templates/auth/register.html (100%) rename {examples/tutorial/flaskr => flaskr}/templates/base.html (100%) rename {examples/tutorial/flaskr => flaskr}/templates/blog/create.html (100%) rename {examples/tutorial/flaskr => flaskr}/templates/blog/index.html (100%) rename {examples/tutorial/flaskr => flaskr}/templates/blog/update.html (100%) delete mode 100644 requirements-skip/README.md delete mode 100644 requirements-skip/tests-dev.txt delete mode 100644 requirements-skip/tests-min.in delete mode 100644 requirements-skip/tests-min.txt delete mode 100644 requirements/build.in delete mode 100644 requirements/build.txt delete mode 100644 requirements/dev.in delete mode 100644 requirements/dev.txt delete mode 100644 requirements/docs.in delete mode 100644 requirements/docs.txt delete mode 100644 requirements/tests.in delete mode 100644 requirements/tests.txt delete mode 100644 requirements/typing.in delete mode 100644 requirements/typing.txt delete mode 100644 src/flask/__init__.py delete mode 100644 src/flask/__main__.py delete mode 100644 src/flask/app.py delete mode 100644 src/flask/blueprints.py delete mode 100644 src/flask/cli.py delete mode 100644 src/flask/config.py delete mode 100644 src/flask/ctx.py delete mode 100644 src/flask/debughelpers.py delete mode 100644 src/flask/globals.py delete mode 100644 src/flask/helpers.py delete mode 100644 src/flask/json/__init__.py delete mode 100644 src/flask/json/provider.py delete mode 100644 src/flask/json/tag.py delete mode 100644 src/flask/logging.py delete mode 100644 src/flask/py.typed delete mode 100644 src/flask/sansio/README.md delete mode 100644 src/flask/sansio/app.py delete mode 100644 src/flask/sansio/blueprints.py delete mode 100644 src/flask/sansio/scaffold.py delete mode 100644 src/flask/sessions.py delete mode 100644 src/flask/signals.py delete mode 100644 src/flask/templating.py delete mode 100644 src/flask/testing.py delete mode 100644 src/flask/typing.py delete mode 100644 src/flask/views.py delete mode 100644 src/flask/wrappers.py rename {examples/tutorial/tests => tests}/data.sql (100%) delete mode 100644 tests/static/config.json delete mode 100644 tests/static/config.toml delete mode 100644 tests/static/index.html delete mode 100644 tests/templates/_macro.html delete mode 100644 tests/templates/context_template.html delete mode 100644 tests/templates/escaping_template.html delete mode 100644 tests/templates/mail.txt delete mode 100644 tests/templates/nested/nested.txt delete mode 100644 tests/templates/non_escaping_template.txt delete mode 100644 tests/templates/simple_template.html delete mode 100644 tests/templates/template_filter.html delete mode 100644 tests/templates/template_test.html delete mode 100644 tests/test_appctx.py delete mode 100644 tests/test_apps/.env delete mode 100644 tests/test_apps/.flaskenv delete mode 100644 tests/test_apps/blueprintapp/__init__.py delete mode 100644 tests/test_apps/blueprintapp/apps/__init__.py delete mode 100644 tests/test_apps/blueprintapp/apps/admin/__init__.py delete mode 100644 tests/test_apps/blueprintapp/apps/admin/static/css/test.css delete mode 100644 tests/test_apps/blueprintapp/apps/admin/static/test.txt delete mode 100644 tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html delete mode 100644 tests/test_apps/blueprintapp/apps/frontend/__init__.py delete mode 100644 tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html delete mode 100644 tests/test_apps/cliapp/__init__.py delete mode 100644 tests/test_apps/cliapp/app.py delete mode 100644 tests/test_apps/cliapp/factory.py delete mode 100644 tests/test_apps/cliapp/importerrorapp.py delete mode 100644 tests/test_apps/cliapp/inner1/__init__.py delete mode 100644 tests/test_apps/cliapp/inner1/inner2/__init__.py delete mode 100644 tests/test_apps/cliapp/inner1/inner2/flask.py delete mode 100644 tests/test_apps/cliapp/message.txt delete mode 100644 tests/test_apps/cliapp/multiapp.py delete mode 100644 tests/test_apps/helloworld/hello.py delete mode 100644 tests/test_apps/helloworld/wsgi.py delete mode 100644 tests/test_apps/subdomaintestmodule/__init__.py delete mode 100644 tests/test_apps/subdomaintestmodule/static/hello.txt delete mode 100644 tests/test_async.py rename {examples/tutorial/tests => tests}/test_auth.py (100%) delete mode 100644 tests/test_basic.py rename {examples/tutorial/tests => tests}/test_blog.py (100%) delete mode 100644 tests/test_blueprints.py delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_config.py delete mode 100644 tests/test_converters.py rename {examples/tutorial/tests => tests}/test_db.py (100%) rename {examples/tutorial/tests => tests}/test_factory.py (100%) delete mode 100644 tests/test_helpers.py delete mode 100644 tests/test_instance_config.py delete mode 100644 tests/test_json.py delete mode 100644 tests/test_json_tag.py delete mode 100644 tests/test_logging.py delete mode 100644 tests/test_regression.py delete mode 100644 tests/test_reqctx.py delete mode 100644 tests/test_session_interface.py delete mode 100644 tests/test_signals.py delete mode 100644 tests/test_subclassing.py delete mode 100644 tests/test_templating.py delete mode 100644 tests/test_testing.py delete mode 100644 tests/test_user_error_handler.py delete mode 100644 tests/test_views.py delete mode 100644 tests/typing/typing_app_decorators.py delete mode 100644 tests/typing/typing_error_handler.py delete mode 100644 tests/typing/typing_route.py delete mode 100644 tox.ini 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 7d4181f6a41dfe2ab698c18834c8d64f5ccb1a8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207889 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

E};U;Vfh@)}ZW{g4y6nuks2h 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 TDA80s!Z-|Xm|QQ_v48PDCmEx^ diff --git a/docs/_static/flask-horizontal.png b/docs/_static/flask-horizontal.png deleted file mode 100644 index a0df2c61093237a7ca98fa875e9a3163a4075d7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24796 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 diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png deleted file mode 100644 index ad025545a7ff00c6366a6146d808d875c5eb6a0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99654 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 diff --git a/docs/_static/shortcut-icon.png b/docs/_static/shortcut-icon.png deleted file mode 100644 index 4d3e6c377414f47b8f935407a18d174c8199539c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4027 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 NFw`}HS8KaO{tNcto^SvF 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 6cd6e3980fbf1b5b619d2e013723368d137fafdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13259 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 diff --git a/docs/tutorial/flaskr_index.png b/docs/tutorial/flaskr_index.png deleted file mode 100644 index aa2b50f552ee094df735372fcf534a5c14d0c325..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11675 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 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}