diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..3a7d2f63 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,23 @@ +environment: + global: + TOXENV: py + + matrix: + - PYTHON: C:\Python36 + - PYTHON: C:\Python27 + +init: + - SET PATH=%PYTHON%;%PATH% + +install: + - python -m pip install -U pip setuptools wheel tox + +build: false + +test_script: + - python -m tox + +branches: + only: + - master + - /^.*-maintenance$/ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..b2cf1785 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,11 @@ +[run] +branch = True +source = + flask + tests + +[paths] +source = + flask + .tox/*/lib/python*/site-packages/flask + .tox/pypy/site-packages/flask diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..8b6910fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,33 @@ +**This issue tracker is a tool to address bugs in Flask itself. +Please use the #pocoo IRC channel on freenode or Stack Overflow for general +questions about using Flask or issues not related to Flask.** + +If you'd like to report a bug in Flask, fill out the template below. Provide +any any extra information that may be useful / related to your problem. +Ideally, create an [MCVE](http://stackoverflow.com/help/mcve), which helps us +understand the problem and helps check that it is not caused by something in +your code. + +--- + +### Expected Behavior + +Tell us what should happen. + +```python +Paste a minimal example that causes the problem. +``` + +### Actual Behavior + +Tell us what happens instead. + +```pytb +Paste the full traceback if there was an exception. +``` + +### Environment + +* Python version: +* Flask version: +* Werkzeug version: diff --git a/.github/ISSUE_TEMPLATE.rst b/.github/ISSUE_TEMPLATE.rst deleted file mode 100644 index 8854961a..00000000 --- a/.github/ISSUE_TEMPLATE.rst +++ /dev/null @@ -1,2 +0,0 @@ -The issue tracker is a tool to address bugs. -Please use the #pocoo IRC channel on freenode or Stack Overflow for questions. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..9dda856c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +Describe what this patch does to fix the issue. + +Link to any relevant issues or pull requests. + + diff --git a/.gitignore b/.gitignore index 9bf4f063..231c0873 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +.env +.flaskenv *.pyc *.pyo env @@ -11,3 +13,9 @@ _mailinglist .tox .cache/ .idea/ + +# Coverage reports +htmlcov +.coverage +.coverage.* +*,cover diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3d7df149..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/_themes"] - path = docs/_themes - url = https://github.com/mitsuhiko/flask-sphinx-themes.git diff --git a/.travis.yml b/.travis.yml index 0f99a7e8..e156d1b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,55 +1,37 @@ sudo: false language: python -python: - - "2.6" - - "2.7" - - "pypy" - - "3.3" - - "3.4" - - "3.5" - -env: - - REQUIREMENTS=lowest - - REQUIREMENTS=lowest-simplejson - - REQUIREMENTS=release - - REQUIREMENTS=release-simplejson - - REQUIREMENTS=devel - - REQUIREMENTS=devel-simplejson - matrix: - exclude: - # Python 3 support currently does not work with lowest requirements - - python: "3.3" - env: REQUIREMENTS=lowest - - python: "3.3" - env: REQUIREMENTS=lowest-simplejson - - python: "3.4" - env: REQUIREMENTS=lowest - - python: "3.4" - env: REQUIREMENTS=lowest-simplejson - - python: "3.5" - env: REQUIREMENTS=lowest - - python: "3.5" - env: REQUIREMENTS=lowest-simplejson - + include: + - python: 3.6 + env: TOXENV=py,simplejson,devel,lowest,codecov,docs-html + - python: 3.5 + env: TOXENV=py,codecov + - python: 3.4 + env: TOXENV=py,codecov + - python: 2.7 + env: TOXENV=py,simplejson,devel,lowest,codecov + - python: pypy + env: TOXENV=py,codecov + - python: nightly + env: TOXENV=py + allow_failures: + - python: nightly + env: TOXENV=py install: - - pip install tox + - pip install tox script: - - tox -e py-$REQUIREMENTS + - tox + +cache: + - pip branches: - except: - - website + only: + - master + - /^.*-maintenance$/ notifications: email: false - irc: - channels: - - "chat.freenode.net#pocoo" - on_success: change - on_failure: always - use_notice: true - skip_join: true diff --git a/AUTHORS b/AUTHORS index cc157dc4..3237ea65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Suggestions ``````````````````````` +- Adam Byrtek - Adam Zapletal - Ali Afshar - Chris Edgemon @@ -20,7 +21,9 @@ Patches and Suggestions - Edmond Burnett - Florent Xicluna - Georg Brandl +- Hsiaoming Yang @lepture - Jeff Widman @jeffwidman +- Joshua Bronson @jab - Justin Quick - Kenneth Reitz - Keyan Pishdadian diff --git a/CHANGES b/CHANGES.rst similarity index 74% rename from CHANGES rename to CHANGES.rst index 13ce156c..b1b65a45 100644 --- a/CHANGES +++ b/CHANGES.rst @@ -1,11 +1,197 @@ Flask Changelog =============== -Here you can see the full list of changes between each Flask release. + +Version 1.0 +----------- + +unreleased + +- **Python 2.6 and 3.3 are no longer supported.** (`pallets/meta#24`_) +- Bump minimum dependency versions to the latest stable versions: + Werkzeug >= 0.14, Jinja >= 2.10, itsdangerous >= 0.24, Click >= 5.1. + (`#2586`_) +- Make ``app.run()`` into a noop if a Flask application is run from the + development server on the command line. This avoids some behavior that + was confusing to debug for newcomers. +- Change default configuration ``JSONIFY_PRETTYPRINT_REGULAR=False``. + ``jsonify()`` method returns compressed response by default, and pretty + response in debug mode. (`#2193`_) +- Change ``Flask.__init__`` to accept two new keyword arguments, + ``host_matching`` and ``static_host``. This enables ``host_matching`` to be + set properly by the time the constructor adds the static route, and enables + the static route to be properly associated with the required host. + (``#1559``) +- ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) +- Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``. + (`#2017`_) +- Add support for ``provide_automatic_options`` in ``add_url_rule`` to disable + adding OPTIONS method when the ``view_func`` argument is not a class. + (`#1489`_). +- ``MethodView`` can inherit method handlers from base classes. (`#1936`_) +- Errors caused while opening the session at the beginning of the request are + handled by the app's error handlers. (`#2254`_) +- Blueprints gained ``json_encoder`` and ``json_decoder`` attributes to + override the app's encoder and decoder. (`#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. (`#2256`_) +- Add ``routes`` CLI command to output routes registered on the application. + (`#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. + (`#2282`_) +- Allow IP address as exact session cookie domain. (`#2282`_) +- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``. + (`#2282`_) +- Auto-detect zero-argument app factory called ``create_app`` or ``make_app`` + from ``FLASK_APP``. (`#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. + (`#2319`_) +- FLASK_APP=myproject.app:create_app('dev') support. +- ``FLASK_APP`` can be set to an app factory, with arguments if needed, for + example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_) +- ``FLASK_APP`` can point to local packages that are not installed in dev mode, + although `pip install -e` should still be preferred. (`#2414`_) +- ``View.provide_automatic_options = True`` is set on the view function from + ``View.as_view``, to be detected in ``app.add_url_rule``. (`#2316`_) +- Error handling will try handlers registered for ``blueprint, code``, + ``app, code``, ``blueprint, exception``, ``app, exception``. (`#2314`_) +- ``Cookie`` is added to the response's ``Vary`` header if the session is + accessed at all during the request (and it wasn't deleted). (`#2288`_) +- ``app.test_request_context()`` take ``subdomain`` and ``url_scheme`` + parameters for use when building base URL. (`#1621`_) +- Set ``APPLICATION_ROOT = '/'`` 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. (`#2348`_) +- Allow registering new tags with ``TaggedJSONSerializer`` to support + storing other types in the session cookie. (`#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. (`#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. + (`#2358`_) +- Extract JSON handling to a mixin applied to both the request and response + classes used by Flask. This adds the ``is_json`` and ``get_json`` methods to + the response to make testing JSON response much easier. (`#2358`_) +- Removed error handler caching because it caused unexpected results for some + exception inheritance hierarchies. Register handlers explicitly for each + exception if you don't want to traverse the MRO. (`#2362`_) +- Fix incorrect JSON encoding of aware, non-UTC datetimes. (`#2374`_) +- Template auto reloading will honor the ``run`` command's ``debug`` flag even + if ``app.jinja_env`` was already accessed. (`#2373`_) +- The following old deprecated code was removed. (`#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 ``@app.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. (`#1421`_) +- Support passing an existing ``EnvironBuilder`` or ``dict`` to + ``test_client.open``. (`#2412`_) +- The ``flask`` command and ``app.run`` will load environment variables using + from ``.env`` and ``.flaskenv`` files if python-dotenv is installed. + (`#2416`_) +- When passing a full URL to the test client, use the scheme in the URL instead + of the ``PREFERRED_URL_SCHEME``. (`#2430`_) +- ``app.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 + ``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. (`#2436`_) +- Blueprint view function name may not contain dots. (`#2450`_) +- Fix a ``ValueError`` caused by invalid Range requests in some cases. + (`#2526`_) +- The dev server now uses threads by default. (`#2529`_) +- Loading config files with ``silent=True`` will ignore ``ENOTDIR`` + errors. (`#2581`_) +- Pass ``--cert`` and ``--key`` options to ``flask run`` to run the + development server over HTTPS. (`#2606`_) +- Added :data:`SESSION_COOKIE_SAMESITE` to control the ``SameSite`` + attribute on the session cookie. (`#2607`_) + +.. _pallets/meta#24: https://github.com/pallets/meta/issues/24 +.. _#1421: https://github.com/pallets/flask/issues/1421 +.. _#1489: https://github.com/pallets/flask/pull/1489 +.. _#1621: https://github.com/pallets/flask/pull/1621 +.. _#1898: https://github.com/pallets/flask/pull/1898 +.. _#1936: https://github.com/pallets/flask/pull/1936 +.. _#2017: https://github.com/pallets/flask/pull/2017 +.. _#2193: https://github.com/pallets/flask/pull/2193 +.. _#2223: https://github.com/pallets/flask/pull/2223 +.. _#2254: https://github.com/pallets/flask/pull/2254 +.. _#2256: https://github.com/pallets/flask/pull/2256 +.. _#2259: https://github.com/pallets/flask/pull/2259 +.. _#2282: https://github.com/pallets/flask/pull/2282 +.. _#2288: https://github.com/pallets/flask/pull/2288 +.. _#2297: https://github.com/pallets/flask/pull/2297 +.. _#2314: https://github.com/pallets/flask/pull/2314 +.. _#2316: https://github.com/pallets/flask/pull/2316 +.. _#2319: https://github.com/pallets/flask/pull/2319 +.. _#2326: https://github.com/pallets/flask/pull/2326 +.. _#2348: https://github.com/pallets/flask/pull/2348 +.. _#2352: https://github.com/pallets/flask/pull/2352 +.. _#2354: https://github.com/pallets/flask/pull/2354 +.. _#2358: https://github.com/pallets/flask/pull/2358 +.. _#2362: https://github.com/pallets/flask/pull/2362 +.. _#2374: https://github.com/pallets/flask/pull/2374 +.. _#2373: https://github.com/pallets/flask/pull/2373 +.. _#2385: https://github.com/pallets/flask/issues/2385 +.. _#2412: https://github.com/pallets/flask/pull/2412 +.. _#2414: https://github.com/pallets/flask/pull/2414 +.. _#2416: https://github.com/pallets/flask/pull/2416 +.. _#2430: https://github.com/pallets/flask/pull/2430 +.. _#2436: https://github.com/pallets/flask/pull/2436 +.. _#2450: https://github.com/pallets/flask/pull/2450 +.. _#2526: https://github.com/pallets/flask/issues/2526 +.. _#2529: https://github.com/pallets/flask/pull/2529 +.. _#2586: https://github.com/pallets/flask/issues/2586 +.. _#2581: https://github.com/pallets/flask/pull/2581 +.. _#2606: https://github.com/pallets/flask/pull/2606 +.. _#2607: https://github.com/pallets/flask/pull/2607 + + +Version 0.12.2 +-------------- + +Released on May 16 2017 + +- Fix a bug in `safe_join` on Windows. + +Version 0.12.1 +-------------- + +Bugfix release, released on March 31st 2017 + +- 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. Fix + ``#2118``. +- Use the ``SERVER_NAME`` config if it is present as default values for + ``app.run``. ``#2109``, ``#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 on December 21st 2016, codename Punsch. + - the cli command now responds to `--version`. - Mimetype guessing and ETag generation for file-like objects in ``send_file`` has been removed, as per issue ``#104``. See pull request ``#1849``. @@ -104,6 +290,8 @@ Released on May 29th 2016, codename Absinthe. - Don't leak exception info of already catched exceptions to context teardown handlers (pull request ``#1393``). - Allow custom Jinja environment subclasses (pull request ``#1422``). +- Updated extension dev guidelines. + - ``flask.g`` now has ``pop()`` and ``setdefault`` methods. - Turn on autoescape for ``flask.templating.render_template_string`` by default (pull request ``#1515``). diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d9cd2214..ef02b732 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,114 +1,166 @@ -========================== How to contribute to Flask ========================== -Thanks for considering contributing to Flask. +Thank you for considering contributing to Flask! Support questions -================= +----------------- + +Please, don't use the issue tracker for this. Use one of the following +resources for questions about your own code: + +* The IRC channel ``#pocoo`` on FreeNode. +* The IRC channel ``#python`` on FreeNode for more general questions. +* The mailing list flask@python.org for long term discussion or larger issues. +* Ask on `Stack Overflow`_. Search with Google first using: + ``site:stackoverflow.com flask {search term, exception message, etc.}`` -Please, don't use the issue tracker for this. Check whether the ``#pocoo`` IRC -channel on Freenode can help with your issue. If your problem is not strictly -Werkzeug or Flask specific, ``#python`` is generally more active. -`Stack Overflow `_ is also worth considering. +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?sort=linked Reporting issues -================ +---------------- -- Under which versions of Python does this happen? This is even more important - if your issue is encoding related. +- Describe what you expected to happen. +- If possible, include a `minimal, complete, and verifiable 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, Flask, and Werkzeug versions. If possible, check if this + issue is already fixed in the repository. -- Under which versions of Werkzeug does this happen? Check if this issue is - fixed in the repository. +.. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve Submitting patches -================== +------------------ - Include tests if your patch is supposed to solve a bug, and explain clearly under which circumstances the bug happens. Make sure the test fails without your patch. +- Try to follow `PEP8`_, but you may ignore the line length limit if following + it would make the code uglier. + +First time setup +~~~~~~~~~~~~~~~~ + +- Download and install the `latest version of git`_. +- Configure git with your `username`_ and `email`_:: + + git config --global user.name 'your name' + git config --global user.email 'your email' -- Try to follow `PEP8 `_, but you - may ignore the line-length-limit if following it would make the code uglier. +- Make sure you have a `GitHub account`_. +- Fork Flask to your GitHub account by clicking the `Fork`_ button. +- `Clone`_ your GitHub fork locally:: + git clone https://github.com/{username}/flask + cd flask -Running the testsuite ---------------------- +- Add the main repository as a remote to update later:: -You probably want to set up a `virtualenv -`_. + git remote add pallets https://github.com/pallets/flask + git fetch pallets -The minimal requirement for running the testsuite is ``py.test``. You can -install it with:: +- Create a virtualenv:: - pip install pytest + python3 -m venv env + . env/bin/activate + # or "env\Scripts\activate" on Windows -Clone this repository:: +- Install Flask in editable mode with development dependencies:: - git clone https://github.com/pallets/flask.git + pip install -e ".[dev]" -Install Flask as an editable package using the current source:: +.. _GitHub account: https://github.com/join +.. _latest version of git: https://git-scm.com/downloads +.. _username: https://help.github.com/articles/setting-your-username-in-git/ +.. _email: https://help.github.com/articles/setting-your-email-in-git/ +.. _Fork: https://github.com/pallets/flask/pull/2305#fork-destination-box +.. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork - cd flask - pip install --editable . +Start coding +~~~~~~~~~~~~ -Then you can run the testsuite with:: +- Create a branch to identify the issue you would like to work on (e.g. + ``2287-dry-test-suite``) +- Using your favorite editor, make your changes, `committing as you go`_. +- Try to follow `PEP8`_, but you may ignore the line length limit if following + it would make the code uglier. +- Include tests that cover any code changes you make. Make sure the test fails + without your patch. `Run the tests. `_. +- Push your commits to GitHub and `create a pull request`_. +- Celebrate 🎉 - py.test +.. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _PEP8: https://pep8.org/ +.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ -With only py.test installed, a large part of the testsuite will get skipped -though. Whether this is relevant depends on which part of Flask you're working -on. Travis is set up to run the full testsuite when you submit your pull -request anyways. +.. _contributing-testsuite: -If you really want to test everything, you will have to install ``tox`` instead -of ``pytest``. You can install it with:: +Running the tests +~~~~~~~~~~~~~~~~~ - pip install tox +Run the basic test suite with:: -The ``tox`` command will then run all tests against multiple combinations -Python versions and dependency versions. + pytest + +This only runs the tests for the current environment. Whether this is relevant +depends on which part of Flask you're working on. Travis-CI will run the full +suite when you submit your pull request. + +The full test suite takes a long time to run because it tests multiple +combinations of Python and dependencies. You need to have Python 2.7, 3.4, +3.5 3.6, and PyPy 2.7 installed to run all of the environments. Then run:: + + tox Running test coverage ---------------------- -Generating a report of lines that do not have unit test coverage can indicate where -to start contributing. ``pytest`` integrates with ``coverage.py``, using the ``pytest-cov`` -plugin. This assumes you have already run the testsuite (see previous section):: +~~~~~~~~~~~~~~~~~~~~~ - pip install pytest-cov +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 on the terminal and as an interactive HTML document:: -After this has been installed, you can output a report to the command line using this command:: + coverage run -m pytest + coverage report + coverage html + # then open htmlcov/index.html - py.test --cov=flask tests/ +Read more about `coverage `_. -Generate a HTML report can be done using this command:: +Running the full test suite with ``tox`` will combine the coverage reports +from all runs. - py.test --cov-report html --cov=flask tests/ +``make`` targets +~~~~~~~~~~~~~~~~ -Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io +Flask provides a ``Makefile`` with various shortcuts. They will ensure that +all dependencies are installed. -Caution -======= -pushing -------- -This repository contains several zero-padded file modes that may cause issues when pushing this repository to git hosts other than github. Fixing this is destructive to the commit history, so we suggest ignoring these warnings. If it fails to push and you're using a self-hosted git service like Gitlab, you can turn off repository checks in the admin panel. +- ``make test`` runs the basic test suite with ``pytest`` +- ``make cov`` runs the basic test suite with ``coverage`` +- ``make test-all`` runs the full test suite with ``tox`` +- ``make docs`` builds the HTML documentation +Caution: zero-padded file modes +------------------------------- -cloning -------- -The zero-padded file modes files above can cause issues while cloning, too. If you have +This repository contains several zero-padded file modes that may cause issues +when pushing this repository to git hosts other than GitHub. Fixing this is +destructive to the commit history, so we suggest ignoring these warnings. If it +fails to push and you're using a self-hosted git service like GitLab, you can +turn off repository checks in the admin panel. -:: +These files can also cause issues while cloning. If you have :: [fetch] fsckobjects = true -or - -:: +or :: [receive] fsckObjects = true - -set in your git configuration file, cloning this repository will fail. The only solution is to set both of the above settings to false while cloning, and then setting them back to true after the cloning is finished. +set in your git configuration file, cloning this repository will fail. The only +solution is to set both of the above settings to false while cloning, and then +setting them back to true after the cloning is finished. diff --git a/MANIFEST.in b/MANIFEST.in index f8d9c2ae..3616212e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include Makefile CHANGES LICENSE AUTHORS +include Makefile CHANGES LICENSE AUTHORS tox.ini graft artwork graft tests diff --git a/Makefile b/Makefile index 9bcdebc2..aef8a782 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,35 @@ -.PHONY: clean-pyc ext-test test tox-test test-with-mem upload-docs docs audit +.PHONY: all install-dev test coverage cov test-all tox docs audit release clean-pyc upload-docs ebook -all: clean-pyc test +all: test -test: - pip install -r test-requirements.txt -q - FLASK_DEBUG= py.test tests examples +install-dev: + pip install -q -e .[dev] -tox-test: +test: clean-pyc install-dev + pytest + +coverage: clean-pyc install-dev + pip install -q -e .[test] + coverage run -m pytest + coverage report + coverage html + +cov: coverage + +test-all: install-dev tox +tox: test-all + +docs: clean-pyc install-dev + $(MAKE) -C docs html + audit: python setup.py audit release: python scripts/make-release.py -ext-test: - python tests/flaskext_test.py --browse - clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + @@ -39,6 +51,3 @@ ebook: @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' @echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' ssh -X pocoo.org ebook-convert /var/www/flask.pocoo.org/docs/flask-docs.epub /var/www/flask.pocoo.org/docs/flask-docs.mobi --cover http://flask.pocoo.org/docs/_images/logo-full.png --authors 'Armin Ronacher' - -docs: - $(MAKE) -C docs html diff --git a/README b/README index baea6b24..75c5e7b1 100644 --- a/README +++ b/README @@ -33,9 +33,9 @@ Good that you're asking. The tests are in the tests/ folder. To run the tests use the - `py.test` testing tool: + `pytest` testing tool: - $ py.test + $ pytest Details on contributing can be found in CONTRIBUTING.rst diff --git a/docs/_static/debugger.png b/docs/_static/debugger.png index 4f47229d..7d4181f6 100644 Binary files a/docs/_static/debugger.png and b/docs/_static/debugger.png differ diff --git a/docs/_static/flask.png b/docs/_static/flask.png index 5c603cc2..55cb8478 100644 Binary files a/docs/_static/flask.png and b/docs/_static/flask.png differ diff --git a/docs/_static/flaskr.png b/docs/_static/flaskr.png index 07d027dd..838f7604 100644 Binary files a/docs/_static/flaskr.png and b/docs/_static/flaskr.png differ diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png index 5deaf1b8..ce236061 100644 Binary files a/docs/_static/logo-full.png and b/docs/_static/logo-full.png differ diff --git a/docs/_static/no.png b/docs/_static/no.png index 4ac1083d..644c3f70 100644 Binary files a/docs/_static/no.png and b/docs/_static/no.png differ diff --git a/docs/_static/pycharm-runconfig.png b/docs/_static/pycharm-runconfig.png new file mode 100644 index 00000000..dff21fa0 Binary files /dev/null and b/docs/_static/pycharm-runconfig.png differ diff --git a/docs/_static/touch-icon.png b/docs/_static/touch-icon.png index cd1e91e1..ef151f15 100644 Binary files a/docs/_static/touch-icon.png and b/docs/_static/touch-icon.png differ diff --git a/docs/_static/yes.png b/docs/_static/yes.png index ac27c4e1..56917ab2 100644 Binary files a/docs/_static/yes.png and b/docs/_static/yes.png differ diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index ec1608fd..c3cb2881 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,6 +1,6 @@

About Flask

- Flask is a micro webdevelopment framework for Python. You are currently + Flask is a micro web development framework for Python. You are currently looking at the documentation of the development version.

Other Formats

@@ -16,7 +16,7 @@

Useful Links

diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 3d964b66..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst index 82b3dc58..bd56f53c 100644 --- a/docs/advanced_foreword.rst +++ b/docs/advanced_foreword.rst @@ -45,11 +45,3 @@ spam, links to malicious software, and the like. Flask is no different from any other framework in that you the developer must build with caution, watching for exploits when building to your requirements. - -Python 3 Support in Flask -------------------------- - -Flask, its dependencies, and most Flask extensions all support Python 3. -If you want to use Flask with Python 3 have a look at the :ref:`python3-support` page. - -Continue to :ref:`installation` or the :ref:`quickstart`. diff --git a/docs/api.rst b/docs/api.rst index d77da3de..e24160c4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -30,61 +30,12 @@ Incoming Request Data .. autoclass:: Request :members: - - .. attribute:: form - - A :class:`~werkzeug.datastructures.MultiDict` with the parsed form data from ``POST`` - or ``PUT`` requests. Please keep in mind that file uploads will not - end up here, but instead in the :attr:`files` attribute. - - .. attribute:: args - - A :class:`~werkzeug.datastructures.MultiDict` with the parsed contents of the query - string. (The part in the URL after the question mark). - - .. attribute:: values - - A :class:`~werkzeug.datastructures.CombinedMultiDict` with the contents of both - :attr:`form` and :attr:`args`. - - .. attribute:: cookies - - A :class:`dict` with the contents of all cookies transmitted with - the request. - - .. attribute:: stream - - If the incoming form data was not encoded with a known mimetype - the data is stored unmodified in this stream for consumption. Most - of the time it is a better idea to use :attr:`data` which will give - you that data as a string. The stream only returns the data once. - - .. attribute:: headers - - The incoming request headers as a dictionary like object. - - .. attribute:: data - - Contains the incoming request data as string in case it came with - a mimetype Flask does not handle. - - .. attribute:: files - - A :class:`~werkzeug.datastructures.MultiDict` with files uploaded as part of a - ``POST`` or ``PUT`` request. Each file is stored as - :class:`~werkzeug.datastructures.FileStorage` object. It basically behaves like a - standard file object you know from Python, with the difference that - it also has a :meth:`~werkzeug.datastructures.FileStorage.save` function that can - store the file on the filesystem. + :inherited-members: .. attribute:: environ The underlying WSGI environment. - .. attribute:: method - - The current request method (``POST``, ``GET`` etc.) - .. attribute:: path .. attribute:: full_path .. attribute:: script_root @@ -114,15 +65,8 @@ Incoming Request Data `url_root` ``u'http://www.example.com/myapplication/'`` ============= ====================================================== - .. attribute:: is_xhr - ``True`` if the request was triggered via a JavaScript - `XMLHttpRequest`. This only works with libraries that support the - ``X-Requested-With`` header and set it to `XMLHttpRequest`. - Libraries that do that are prototype, jQuery and Mochikit and - probably some more. - -.. class:: request +.. attribute:: request To access incoming request data, you can use the global `request` object. Flask parses incoming request data for you and gives you @@ -141,7 +85,7 @@ Response Objects ---------------- .. autoclass:: flask.Response - :members: set_cookie, data, mimetype + :members: set_cookie, data, mimetype, is_json, get_json .. attribute:: headers @@ -159,12 +103,12 @@ Response Objects Sessions -------- -If you have the :attr:`Flask.secret_key` set you can use sessions in Flask -applications. A session basically makes it possible to remember -information from one request to another. The way Flask does this is by -using a signed cookie. So the user can look at the session contents, but -not modify it unless they know the secret key, so make sure to set that -to something complex and unguessable. +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: @@ -227,18 +171,6 @@ implementation that Flask is using. .. autoclass:: SessionMixin :members: -.. autodata:: session_json_serializer - - This object provides dumping and loading methods similar to simplejson - but it also tags certain builtin Python objects that commonly appear in - sessions. Currently the following extended values are supported in - the JSON it dumps: - - - :class:`~markupsafe.Markup` objects - - :class:`~uuid.UUID` objects - - :class:`~datetime.datetime` objects - - :class:`tuple`\s - .. admonition:: Notice The ``PERMANENT_SESSION_LIFETIME`` config key can also be an integer @@ -410,6 +342,8 @@ you are using Flask 0.10 which implies that: .. autoclass:: JSONDecoder :members: +.. automodule:: flask.json.tag + Template Rendering ------------------ @@ -705,6 +639,7 @@ The following signals exist in Flask: .. _blinker: https://pypi.python.org/pypi/blinker +.. _class-based-views: Class-Based Views ----------------- @@ -879,6 +814,8 @@ Command Line Interface .. autoclass:: ScriptInfo :members: +.. autofunction:: load_dotenv + .. autofunction:: with_appcontext .. autofunction:: pass_script_info diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 166c5aa3..976609b6 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -5,31 +5,37 @@ The Application Context .. versionadded:: 0.9 -One of the design ideas behind Flask is that there are two different -“states” in which code is executed. The application setup state in which -the application implicitly is on the module level. It starts when the -:class:`Flask` object is instantiated, and it implicitly ends when the -first request comes in. While the application is in this state a few -assumptions are true: - -- the programmer can modify the application object safely. -- no request handling happened so far -- you have to have a reference to the application object in order to - modify it, there is no magic proxy that can give you a reference to - the application object you're currently creating or modifying. - -In contrast, during request handling, a couple of other rules exist: - -- while a request is active, the context local objects - (:data:`flask.request` and others) point to the current request. -- any code can get hold of these objects at any time. - -There is a third state which is sitting in between a little bit. -Sometimes you are dealing with an application in a way that is similar to -how you interact with applications during request handling; just that there -is no request active. Consider, for instance, that you're sitting in an -interactive Python shell and interacting with the application, or a -command line application. +One of the design ideas behind Flask is that there are at least two +different “states” in which code is executed: + +1. The application setup state, in which the application implicitly is +on the module level. + + This state starts when the :class:`Flask` object is instantiated, and + it implicitly ends when the first request comes in. While the + application is in this state, a few assumptions are true: + + - the programmer can modify the application object safely. + - no request handling happened so far + - you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +2. In contrast, in the request handling state, a couple of other rules +exist: + + - while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. + - any code can get hold of these objects at any time. + +3. There is also a third state somewhere in between 'module-level' and +'request-handling': + + Sometimes you are dealing with an application in a way that is similar to + how you interact with applications during request handling, but without + there being an active request. Consider, for instance, that you're + sitting in an interactive Python shell and interacting with the + application, or a command line application. The application context is what powers the :data:`~flask.current_app` context local. diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 89d3701e..98a3d630 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -177,11 +177,11 @@ 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. +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. +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 @@ -194,7 +194,7 @@ want to render the template ``'admin/index.html'`` and you have provided 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. +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 @@ -245,4 +245,22 @@ Here is an example for a "404 Page Not Found" exception:: 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 errorhandler 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(ex) + else: + return ex + More information on error handling see :ref:`errorpages`. diff --git a/docs/changelog.rst b/docs/changelog.rst index d6c5f48c..d9e113ec 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1 @@ -.. include:: ../CHANGES +.. include:: ../CHANGES.rst diff --git a/docs/cli.rst b/docs/cli.rst index 7ddf50f3..456fdc03 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,89 +1,216 @@ +.. currentmodule:: flask + .. _cli: Command Line Interface ====================== -.. versionadded:: 0.11 +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. -.. currentmodule:: flask +.. _Click: http://click.pocoo.org/ -One of the nice new features in Flask 0.11 is the built-in integration of -the `click `_ command line interface. This -enables a wide range of new features for the Flask ecosystem and your own -applications. -Basic Usage ------------ +Application Discovery +--------------------- -After installation of Flask you will now find a :command:`flask` script -installed into your virtualenv. If you don't want to install Flask or you -have a special use-case you can also use ``python -m flask`` to accomplish -exactly the same. +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 ``FLASK_APP`` +environment variable is used to specify how to load the application. -The way this script works is by providing access to all the commands on -your Flask application's :attr:`Flask.cli` instance as well as some -built-in commands that are always there. Flask extensions can also -register more commands there if they desire so. +Unix Bash (Linux, Mac, etc.):: -For the :command:`flask` script to work, an application needs to be -discovered. This is achieved by exporting the ``FLASK_APP`` environment -variable. It can be either set to an import path or to a filename of a -Python module that contains a Flask application. + $ export FLASK_APP=hello + $ flask run -In that imported file the name of the app needs to be called ``app`` or -optionally be specified after a colon. For instance -``mymodule:application`` would tell it to use the `application` object in -the :file:`mymodule.py` file. +Windows CMD:: -Given a :file:`hello.py` file with the application in it named ``app`` -this is how it can be run. + > set FLASK_APP=hello + > flask run -Environment variables (On Windows use ``set`` instead of ``export``):: +Windows PowerShell:: - export FLASK_APP=hello - flask run + > $env:FLASK_APP = "hello" + > flask run + +While ``FLASK_APP`` supports a variety of options for specifying your +application, most use cases should be simple. Here are the typical values: + +(nothing) + The file :file:`wsgi.py` is imported, automatically detecting an app + (``app``). This provides an easy way to create an app from a factory with + extra arguments. + +``FLASK_APP=hello`` + The name is imported, automatically detecting an app (``app``) or factory + (``create_app``). + +---- + +``FLASK_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: + +``FLASK_APP=src/hello`` + Sets the current working directory to ``src`` then imports ``hello``. + +``FLASK_APP=hello.web`` + Imports the path ``hello.web``. + +``FLASK_APP=hello:app2`` + Uses the ``app2`` Flask instance in ``hello``. + +``FLASK_APP="hello:create_app('dev')"`` + The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` + as the argument. + +If ``FLASK_APP`` is not set, the command will look for a file called +:file:`wsgi.py` or :file:`app.py` 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. + +When calling an application factory, if the factory takes an argument named +``info``, then the :class:`~cli.ScriptInfo` instance is passed as a keyword +argument. If parentheses follow the factory name, their contents are parsed +as Python literals and passes as 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 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 :ref:`deployment` for how to run in production. + + +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.6.2 (default, Jul 20 2017, 03:52:27) + [GCC 7.1.1 20170630] on linux + App: example + Instance: /home/user/Projects/hello/instance + >>> + +Use :meth:`~Flask.shell_context_processor` to add other automatic imports. -Or with a filename:: - export FLASK_APP=/path/to/hello.py - flask run +Environments +------------ -Virtualenv Integration ----------------------- +.. versionadded:: 1.0 -If you are constantly working with a virtualenv you can also put the -``export FLASK_APP`` into your ``activate`` script by adding it to the -bottom of the file. That way every time you activate your virtualenv you -automatically also activate the correct application name. +The environment in which the Flask app runs is set by the +:envvar:`FLASK_ENV` environment variable. If not set it defaults to +``production``. The other recognized environment is ``development``. +Flask and extensions may choose to enable behaviors based on the +environment. -Debug Flag +If the env is set to ``development``, the ``flask`` command will enable +debug mode and ``flask run`` will enable the interactive debugger and +reloader. + +:: + + $ FLASK_ENV=development flask run + * Serving Flask app "hello" + * Environment: development + * 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 + + +Debug Mode ---------- -The :command:`flask` script can also be instructed to enable the debug -mode of the application automatically by exporting ``FLASK_DEBUG``. If -set to ``1`` debug is enabled or ``0`` disables it:: +Debug mode will be enabled when :envvar:`FLASK_ENV` is ``development``, +as described above. If you want to control debug mode separately, use +:envvar:`FLASK_DEBUG`. The value ``1`` enables it, ``0`` disables it. - export FLASK_DEBUG=1 -Running a Shell ---------------- +.. _dotenv: + +Environment Variables From dotenv +--------------------------------- + +Rather than setting ``FLASK_APP`` each 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 :file:`.env` and :file:`.flaskenv`. +This can be used to avoid having to set ``FLASK_APP`` manually every time you +open a new terminal, 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 current working directory will be set to the +location of the file, with the assumption that that is the top level project +directory. + +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 + -To run an interactive Python shell you can use the ``shell`` command:: +Environment Variables From virtualenv +------------------------------------- - flask shell +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. + +Unix Bash, :file:`venv/bin/activate`:: + + export FLASK_APP=hello + +Windows CMD, :file:`venv\\Scripts\\activate.bat`:: + + set 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. -This will start up an interactive Python shell, setup the correct -application context and setup the local variables in the shell. This is -done by invoking the :meth:`Flask.make_shell_context` method of the -application. By default you have access to your ``app`` and :data:`g`. Custom Commands --------------- -If you want to add more commands to the shell script you can do this -easily. Flask uses `click`_ for the command interface which makes -creating custom commands very easy. For instance if you want a shell -command to initialize the database you can do this:: +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 @@ -91,158 +218,207 @@ command to initialize the database you can do this:: app = Flask(__name__) @app.cli.command() - def initdb(): - """Initialize the database.""" - click.echo('Init the db') + @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) + +:: -The command will then show up on the command line:: + flask user create demo + +See :ref:`testing-cli` for an overview of how to test your custom +commands. - $ flask initdb - Init the db Application Context -------------------- +~~~~~~~~~~~~~~~~~~~ + +Commands added using the Flask app's :attr:`~Flask.cli` +:meth:`~cli.AppGroup.command` decorator will be executed with an application +context pushed, so your command and extensions have access to the app and its +configuration. If you create a command using the Click :func:`~click.command` +decorator instead of the Flask decorator, you can use +:func:`~cli.with_appcontext` to get the same behavior. :: + + import click + from flask.cli import with_appcontext + + @click.command + @with_appcontext + def do_work(): + ... -Most commands operate on the application so it makes a lot of sense if -they have the application context setup. Because of this, if you register -a callback on ``app.cli`` with the :meth:`~flask.cli.AppGroup.command` the -callback will automatically be wrapped through :func:`cli.with_appcontext` -which informs the cli system to ensure that an application context is set -up. This behavior is not available if a command is added later with -:func:`~click.Group.add_command` or through other means. + app.cli.add_command(do_work) -It can also be disabled by passing ``with_appcontext=False`` to the -decorator:: +If you're sure a command doesn't need the context, you can disable it:: @app.cli.command(with_appcontext=False) - def example(): - pass + def do_work(): + ... -Factory Functions ------------------ -In case you are using factory functions to create your application (see -:ref:`app-factories`) you will discover that the :command:`flask` command -cannot work with them directly. Flask won't be able to figure out how to -instantiate your application properly by itself. Because of this reason -the recommendation is to create a separate file that instantiates -applications. This is not the only way to make this work. Another is the -:ref:`custom-scripts` support. +Plugins +------- -For instance if you have a factory function that creates an application -from a filename you could make a separate file that creates such an -application from an environment variable. +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:`setup.py` :: + + from setuptools import setup -This could be a file named :file:`autoapp.py` with these contents:: + setup( + name='flask-my-extension', + ..., + entry_points={ + 'flask.commands': [ + 'my-command=flask_my_extension.commands:cli' + ], + }, + ) - import os - from yourapplication import create_app - app = create_app(os.environ['YOURAPPLICATION_CONFIG']) -Once this has happened you can make the flask command automatically pick -it up:: +.. _entry point: https://packaging.python.org/tutorials/distributing-packages/#entry-points + +Inside :file:`flask_my_extension/commands.py` you can then export a Click +object:: + + import click + + @click.command() + def cli(): + ... - export YOURAPPLICATION_CONFIG=/path/to/config.cfg - export FLASK_APP=/path/to/autoapp.py +Once that package is installed in the same virtualenv as your Flask project, +you can run ``flask my-command`` to invoke the command. -From this point onwards :command:`flask` will find your application. .. _custom-scripts: Custom Scripts -------------- -While the most common way is to use the :command:`flask` command, you can -also make your own "driver scripts". Since Flask uses click for the -scripts there is no reason you cannot hook these scripts into any click -application. There is one big caveat and that is, that commands -registered to :attr:`Flask.cli` will expect to be (indirectly at least) -launched from a :class:`flask.cli.FlaskGroup` click group. This is -necessary so that the commands know which Flask application they have to -work with. - -To understand why you might want custom scripts you need to understand how -click finds and executes the Flask application. If you use the -:command:`flask` script you specify the application to work with on the -command line or environment variable as an import name. This is simple -but it has some limitations. Primarily it does not work with application -factory functions (see :ref:`app-factories`). - -With a custom script you don't have this problem as you can fully -customize how the application will be created. This is very useful if you -write reusable applications that you want to ship to users and they should -be presented with a custom management script. - -To explain all of this, here is an example :file:`manage.py` script that -manages a hypothetical wiki application. We will go through the details -afterwards:: - - import os +When you are using the app factory pattern, it may be more convenient to define +your own Click script. Instead of using ``FLASK_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_wiki_app(info): - from yourwiki import create_app - return create_app( - config=os.environ.get('WIKI_CONFIG', 'wikiconfig.py')) + def create_app(): + app = Flask('wiki') + # other setup + return app - @click.group(cls=FlaskGroup, create_app=create_wiki_app) + @click.group(cls=FlaskGroup, create_app=create_app) def cli(): - """This is a management script for the wiki application.""" - - if __name__ == '__main__': - cli() - -That's a lot of code for not much, so let's go through all parts step by -step. - -1. First we import the ``click`` library as well as the click extensions - from the ``flask.cli`` package. Primarily we are here interested - in the :class:`~flask.cli.FlaskGroup` click group. -2. The next thing we do is defining a function that is invoked with the - script info object (:class:`~flask.cli.ScriptInfo`) from Flask and its - purpose is to fully import and create the application. This can - either directly import an application object or create it (see - :ref:`app-factories`). In this case we load the config from an - environment variable. -3. Next step is to create a :class:`FlaskGroup`. In this case we just - make an empty function with a help doc string that just does nothing - and then pass the ``create_wiki_app`` function as a factory function. - - Whenever click now needs to operate on a Flask application it will - call that function with the script info and ask for it to be created. -4. All is rounded up by invoking the script. - -CLI Plugins ------------ - -Flask extensions can always patch the :attr:`Flask.cli` instance with more -commands if they want. However there is a second way to add CLI plugins -to Flask which is through ``setuptools``. If you make a Python package that -should export a Flask command line plugin you can ship a :file:`setup.py` file -that declares an entrypoint that points to a click command: - -Example :file:`setup.py`:: + """Management script for the Wiki application.""" + +Define the entry point in :file:`setup.py`:: from setuptools import setup setup( name='flask-my-extension', - ... - entry_points=''' - [flask.commands] - my-command=mypackage.commands:cli - ''', + ..., + entry_points={ + 'console_scripts': [ + 'wiki=wiki:cli' + ], + }, ) -Inside :file:`mypackage/commands.py` you can then export a Click object:: +Install the application in the virtualenv in editable mode and the custom +script is available. Note that you don't need to set ``FLASK_APP``. :: - import click + $ pip install -e . + $ wiki run - @click.command() - def cli(): - """This is an example command.""" +.. 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/distributing-packages/#console-scripts + + +PyCharm Integration +------------------- + +Prior to PyCharm 2018.1, the Flask CLI features weren't yet fully +integrated into PyCharm. We have to do a few tweaks to get them working +smoothly. These instructions should be similar for any other IDE you +might want to use. + +In PyCharm, with your project open, click on *Run* from the menu bar and +go to *Edit Configurations*. You'll be greeted by a screen similar to +this: + +.. image:: _static/pycharm-runconfig.png + :align: center + :class: screenshot + :alt: screenshot of pycharm's run configuration settings + +There's quite a few options to change, but once we've done it for one +command, we can easily copy the entire configuration and make a single +tweak to give us access to other commands, including any custom ones you +may implement yourself. + +Click the + (*Add New Configuration*) button and select *Python*. Give +the configuration a good descriptive name such as "Run Flask Server". +For the ``flask run`` command, check "Single instance only" since you +can't run the server more than once at the same time. + +Select *Module name* from the dropdown (**A**) then input ``flask``. + +The *Parameters* field (**B**) is set to the CLI command to execute +(with any arguments). In this example we use ``run``, which will run +the development server. + +You can skip this next step if you're using :ref:`dotenv`. We need to +add an environment variable (**C**) to identify our application. Click +on the browse button and add an entry with ``FLASK_APP`` on the left and +the Python import or file on the right (``hello`` for example). + +Next we need to set the working directory (**D**) to be the folder where +our application resides. + +If you have installed your project as a package in your virtualenv, you +may untick the *PYTHONPATH* options (**E**). This will more accurately +match how you deploy the app later. + +Click *Apply* to save the configuration, or *OK* to save and close the +window. Select the configuration in the main PyCharm window and click +the play button next to it to run the server. -Once that package is installed in the same virtualenv as Flask itself you -can run ``flask my-command`` to invoke your command. This is useful to -provide extra functionality that Flask itself cannot ship. +Now that we have a configuration which runs ``flask run`` from within +PyCharm, we can copy that configuration and alter the *Script* argument +to run a different CLI command, e.g. ``flask shell``. diff --git a/docs/conf.py b/docs/conf.py index b37427a8..94cae16d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,15 +11,17 @@ # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import print_function -from datetime import datetime import os import sys import pkg_resources +import time +import datetime + +BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.join(os.path.dirname(__file__), '_themes')) sys.path.append(os.path.dirname(__file__)) # -- General configuration ----------------------------------------------------- @@ -35,6 +37,14 @@ extensions = [ 'flaskdocext' ] +try: + __import__('sphinxcontrib.log_cabinet') +except ImportError: + print('sphinxcontrib-log-cabinet is not installed.') + print('Changelog directives will not be re-organized.') +else: + extensions.append('sphinxcontrib.log_cabinet') + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -49,7 +59,7 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2010 - {0}, Armin Ronacher'.format(datetime.utcnow().year) +copyright = u'2010 - {0}, Armin Ronacher'.format(BUILD_DATE.year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -110,7 +120,7 @@ exclude_patterns = ['_build'] # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +# html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -231,7 +241,7 @@ latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' -# The unique identifier of the text. This can be a ISBN number +# The unique identifier of the text. This can be an ISBN number # or the project homepage. #epub_identifier = '' @@ -257,26 +267,15 @@ intersphinx_mapping = { 'werkzeug': ('http://werkzeug.pocoo.org/docs/', None), 'click': ('http://click.pocoo.org/', None), 'jinja': ('http://jinja.pocoo.org/docs/', None), - 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None), + 'itsdangerous': ('https://pythonhosted.org/itsdangerous', None), + 'sqlalchemy': ('https://docs.sqlalchemy.org/en/latest/', None), 'wtforms': ('https://wtforms.readthedocs.io/en/latest/', None), - 'blinker': ('https://pythonhosted.org/blinker/', None) + 'blinker': ('https://pythonhosted.org/blinker/', None), } -try: - __import__('flask_theme_support') - pygments_style = 'flask_theme_support.FlaskyStyle' - html_theme = 'flask' - html_theme_options = { - 'touch_icon': 'touch-icon.png' - } -except ImportError: - print('-' * 74) - print('Warning: Flask themes unavailable. Building with default theme') - print('If you want the Flask themes, run this command and build again:') - print() - print(' git submodule update --init') - print('-' * 74) - +html_theme_options = { + 'touch_icon': 'touch-icon.png' +} # unwrap decorators def unwrap_decorators(): diff --git a/docs/config.rst b/docs/config.rst index 89fa0924..2e2833f9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -3,8 +3,6 @@ Configuration Handling ====================== -.. versionadded:: 0.3 - 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 @@ -22,6 +20,7 @@ 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 -------------------- @@ -29,193 +28,319 @@ 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['DEBUG'] = True + 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.debug = True + app.testing = True To update multiple keys at once you can use the :meth:`dict.update` method:: app.config.update( - DEBUG=True, - SECRET_KEY='...' + TESTING=True, + SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/' ) + +Environment and Debug Features +------------------------------ + +The :data:`ENV` and :data:`DEBUG` config values are special because they +may behave inconsistently if changed after the app has begun setting up. +In order to set the environment and debug mode reliably, Flask uses +environment variables. + +The environment is used to indicate to Flask, extensions, and other +programs, like Sentry, what context Flask is running in. It is +controlled with the :envvar:`FLASK_ENV` environment variable and +defaults to ``production``. + +Setting :envvar:`FLASK_ENV` to ``development`` will enable debug mode. +``flask run`` will use the interactive debugger and reloader by default +in debug mode. To control this separately from the environment, use the +:envvar:`FLASK_DEBUG` flag. + +.. versionchanged:: 1.0 + Added :envvar:`FLASK_ENV` to control the environment separately + from debug mode. The development environment enables debug mode. + +To switch Flask to the development environment and enable debug mode, +set :envvar:`FLASK_ENV`:: + + $ export FLASK_ENV=development + $ flask run + +(On Windows, use ``set`` instead of ``export``.) + +Using the environment variables as described above is recommended. While +it is possible to set :data:`ENV` and :data:`DEBUG` in your config or +code, this is strongly discouraged. They can't be read early by the +``flask`` 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: -.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| - -================================= ========================================= -``DEBUG`` enable/disable debug mode -``TESTING`` enable/disable testing mode -``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the - propagation of exceptions. If not set or - explicitly set to ``None`` this is - implicitly true if either ``TESTING`` or - ``DEBUG`` is true. -``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in - debug mode the request context is not - popped on exceptions to enable debuggers - to introspect the data. This can be - disabled by this key. You can also use - this setting to force-enable it for non - debug execution which might be useful to - debug production applications (but also - very risky). -``SECRET_KEY`` the secret key -``SESSION_COOKIE_NAME`` the name of the session cookie -``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If - this is not set, the cookie will be - valid for all subdomains of - ``SERVER_NAME``. -``SESSION_COOKIE_PATH`` the path for the session cookie. If - this is not set the cookie will be valid - for all of ``APPLICATION_ROOT`` or if - that is not set for ``'/'``. -``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set - with the httponly flag. Defaults to - ``True``. -``SESSION_COOKIE_SECURE`` controls if the cookie should be set - with the secure flag. Defaults to - ``False``. -``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as - :class:`datetime.timedelta` object. - Starting with Flask 0.8 this can also be - an integer representing seconds. -``SESSION_REFRESH_EACH_REQUEST`` this flag controls how permanent - sessions are refreshed. If set to ``True`` - (which is the default) then the cookie - is refreshed each request which - automatically bumps the lifetime. If - set to ``False`` a `set-cookie` header is - only sent if the session is modified. - Non permanent sessions are not affected - by this. -``USE_X_SENDFILE`` enable/disable x-sendfile -``LOGGER_NAME`` the name of the logger -``LOGGER_HANDLER_POLICY`` the policy of the default logging - handler. The default is ``'always'`` - which means that the default logging - handler is always active. ``'debug'`` - will only activate logging in debug - mode, ``'production'`` will only log in - production and ``'never'`` disables it - entirely. -``SERVER_NAME`` the name and port number of the server. - Required for subdomain support (e.g.: - ``'myapp.dev:5000'``) Note that - localhost does not support subdomains so - setting this to “localhost” does not - help. Setting a ``SERVER_NAME`` also - by default enables URL generation - without a request context but with an - application context. -``APPLICATION_ROOT`` If the application does not occupy - a whole domain or subdomain this can - be set to the path where the application - is configured to live. This is for - session cookie as path value. If - domains are used, this should be - ``None``. -``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will - reject incoming requests with a - content length greater than this by - returning a 413 status code. -``SEND_FILE_MAX_AGE_DEFAULT`` Default cache control max age to use with - :meth:`~flask.Flask.send_static_file` (the - default static file handler) and - :func:`~flask.send_file`, as - :class:`datetime.timedelta` or as seconds. - Override this value on a per-file - basis using the - :meth:`~flask.Flask.get_send_file_max_age` - hook on :class:`~flask.Flask` or - :class:`~flask.Blueprint`, - respectively. Defaults to 43200 (12 hours). -``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will - not execute the error handlers of HTTP - exceptions but instead treat the - exception like any other and bubble it - through the exception stack. This is - helpful for hairy debugging situations - where you have to find out where an HTTP - exception is coming from. -``TRAP_BAD_REQUEST_ERRORS`` Werkzeug's internal data structures that - deal with request specific data will - raise special key errors that are also - bad request exceptions. Likewise many - operations can implicitly fail with a - BadRequest exception for consistency. - Since it's nice for debugging to know - why exactly it failed this flag can be - used to debug those situations. If this - config is set to ``True`` you will get - a regular traceback instead. -``PREFERRED_URL_SCHEME`` The URL scheme that should be used for - URL generation if no URL scheme is - available. This defaults to ``http``. -``JSON_AS_ASCII`` By default Flask serialize object to - ascii-encoded JSON. If this is set to - ``False`` Flask will not encode to ASCII - and output strings as-is and return - unicode strings. ``jsonify`` will - automatically encode it in ``utf-8`` - then for transport for instance. -``JSON_SORT_KEYS`` By default Flask will serialize JSON - objects in a way that the keys are - ordered. This is done in order to - ensure that independent of the hash seed - of the dictionary the return value will - be consistent to not trash external HTTP - caches. You can override the default - behavior by changing this variable. - This is not recommended but might give - you a performance improvement on the - cost of cacheability. -``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` (the default) - jsonify responses will be pretty printed - if they are not requested by an - XMLHttpRequest object (controlled by - the ``X-Requested-With`` header) -``JSONIFY_MIMETYPE`` MIME type used for jsonify responses. -``TEMPLATES_AUTO_RELOAD`` Whether to check for modifications of - the template source and reload it - automatically. By default the value is - ``None`` which means that Flask checks - original file only in debug mode. -``EXPLAIN_TEMPLATE_LOADING`` If this is enabled then every attempt to - load a template will write an info - message to the logger explaining the - attempts to locate the template. This - can be useful to figure out why - templates cannot be found or wrong - templates appear to be loaded. -================================= ========================================= - -.. admonition:: More on ``SERVER_NAME`` - - The ``SERVER_NAME`` key is used for the subdomain support. Because - Flask cannot guess the subdomain part without the knowledge of the - actual server name, this is required if you want to work with - subdomains. This is also used for the session cookie. - - Please keep in mind that not only Flask has the problem of not knowing - what subdomains are, your web browser does as well. Most modern web - browsers will not allow cross-subdomain cookies to be set on a - server name without dots in it. So if your server name is - ``'localhost'`` you will not be able to set a cookie for - ``'localhost'`` and every subdomain of it. Please choose a different - server name in that case, like ``'myapplication.local'`` and add - this name + the subdomains you want to use into your host config - or setup a local `bind`_. - -.. _bind: https://www.isc.org/downloads/bind/ +.. py:data:: ENV + + What environment the app is running in. Flask and extensions may + enable behaviors based on the environment, such as enabling debug + mode. The :attr:`~flask.Flask.env` attribute maps to this config + key. This is set by the :envvar:`FLASK_ENV` environment variable and + may not behave as expected if set in code. + + **Do not enable development when deploying in production.** + + Default: ``'production'`` + + .. versionadded:: 1.0 + +.. 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 enabled when :data:`ENV` is ``'development'`` + and is overridden by 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: ``True`` if :data:`ENV` is ``'production'``, or ``False`` + otherwise. + +.. 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:: PRESERVE_CONTEXT_ON_EXCEPTION + + Don't pop the request context when an exception occurs. If not set, this + is true if ``DEBUG`` is true. This allows debuggers to introspect the + request data on errors, and should normally not need to be set directly. + + 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 string of bytes, although unicode + is accepted too. For example, copy the output of this to your config:: + + python -c 'import os; print(os.urandom(16))' + b'_5#y2L"F4Q8z\n\xec]/' + + **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 domain match rule that the session cookie will be valid for. If not + set, the cookie will be valid for all subdomains of ``SERVER_NAME``. If + ``False``, the cookie's domain will not be set. + + Default: ``None`` + +.. 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 either 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. + + Default: ``timedelta(hours=12)`` (``43200`` seconds) + +.. py:data:: SERVER_NAME + + Inform the application what host and port it is bound to. Required for + subdomain route matching support. + + If set, will be used for the session cookie domain if + ``SESSION_COOKIE_DOMAIN`` is not set. Modern web browsers will not allow + setting cookies for domains without a dot. To use a domain locally, + add any names that should route to the app to your ``hosts`` file. :: + + 127.0.0.1 localhost.dev + + If set, ``url_for`` can generate external URLs with only an application + context instead of a request context. + + Default: ``None`` + +.. py:data:: APPLICATION_ROOT + + Inform the application what path it is mounted under by the application / + web server. + + 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:: JSON_AS_ASCII + + Serialize objects to ASCII-encoded JSON. If this is disabled, the JSON + will be returned as a Unicode string, or encoded as ``UTF-8`` by + ``jsonify``. This has security implications when rendering the JSON in + to JavaScript in templates, and should typically remain enabled. + + Default: ``True`` + +.. py:data:: JSON_SORT_KEYS + + Sort the keys of JSON objects alphabetically. This is useful for caching + because it ensures the data is serialized the same way no matter what + Python's hash seed is. While not recommended, you can disable this for a + possible performance improvement at the cost of caching. + + Default: ``True`` + +.. py:data:: JSONIFY_PRETTYPRINT_REGULAR + + ``jsonify`` responses will be output with newlines, spaces, and indentation + for easier reading by humans. Always enabled in debug mode. + + Default: ``False`` + +.. py:data:: JSONIFY_MIMETYPE + + The mimetype of ``jsonify`` responses. + + Default: ``'application/json'`` + +.. 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`` .. versionadded:: 0.4 ``LOGGER_NAME`` @@ -245,6 +370,17 @@ The following configuration values are used internally by Flask: ``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 + :ref:`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. + + Configuring from Files ---------------------- @@ -262,7 +398,7 @@ So a common pattern is this:: 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`` +with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` environment variable points to. This environment variable can be set on Linux or OS X with the export command in the shell before starting the server:: @@ -284,7 +420,7 @@ Here is an example of a configuration file:: # Example configuration DEBUG = False - SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' + SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' 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 @@ -292,6 +428,54 @@ 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 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. + +Environment variables can be set on Linux or OS X with the export command in +the shell before starting the server:: + + $ export SECRET_KEY='5f352379324c22463451387a0aec5d2f' + $ export DEBUG=False + $ python run-app.py + * Running on http://127.0.0.1:5000/ + * Restarting with reloader... + +On Windows systems use the `set` builtin instead:: + + >set SECRET_KEY='5f352379324c22463451387a0aec5d2f' + >set DEBUG=False + +While this approach is straightforward to use, it is important to remember that +environment variables are strings -- they are not automatically deserialized +into Python types. + +Here is an example of a configuration file that uses environment variables:: + + # Example configuration + import os + + ENVIRONMENT_DEBUG = os.environ.get("DEBUG", default=False) + if ENVIRONMENT_DEBUG.lower() in ("f", "false"): + ENVIRONMENT_DEBUG = False + + DEBUG = ENVIRONMENT_DEBUG + SECRET_KEY = os.environ.get("SECRET_KEY", default=None) + if not SECRET_KEY: + raise ValueError("No secret key set for Flask application") + + +Notice that any value besides an empty string will be interpreted as a boolean +``True`` value in Python, which requires care if an environment explicitly sets +values intended to be ``False``. + +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` class documentation. Configuration Best Practices ---------------------------- @@ -344,7 +528,7 @@ configuration:: class Config(object): DEBUG = False TESTING = False - DATABASE_URI = 'sqlite://:memory:' + DATABASE_URI = 'sqlite:///:memory:' class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 8b25e61d..f76b1591 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -16,6 +16,7 @@ instructions for web development with Flask. templating testing errorhandling + logging config signals views @@ -55,7 +56,7 @@ Design notes, legal information and changelog are here for the interested. unicode extensiondev styleguide - python3 upgrading changelog license + contributing diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 00000000..e582053e --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index c0beae0c..46706033 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -111,7 +111,7 @@ Set yourapplication.fcgi:: #!/usr/bin/python #: optional path to your local python site-packages folder import sys - sys.path.insert(0, '/lib/python2.6/site-packages') + sys.path.insert(0, '/lib/python/site-packages') from flup.server.fcgi import WSGIServer from yourapplication import app @@ -144,7 +144,7 @@ A basic FastCGI configuration for lighttpd looks like that:: ) alias.url = ( - "/static/" => "/path/to/your/static" + "/static/" => "/path/to/your/static/" ) url.rewrite-once = ( @@ -159,7 +159,7 @@ work in the URL root you have to work around a lighttpd bug with the Make sure to apply it only if you are mounting the application the URL root. Also, see the Lighty docs for more information on `FastCGI and Python -`_ (note that +`_ (note that explicitly passing a socket to run() is no longer necessary). Configuring nginx @@ -234,7 +234,7 @@ python path. Common problems are: web server. - Different python interpreters being used. -.. _nginx: http://nginx.org/ -.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: https://nginx.org/ +.. _lighttpd: https://www.lighttpd.net/ .. _cherokee: http://cherokee-project.com/ .. _flup: https://pypi.python.org/pypi/flup diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 5d88cf72..edf5a256 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -4,9 +4,8 @@ Deployment Options ================== While lightweight and easy to use, **Flask's built-in server is not suitable -for production** as it doesn't scale well and by default serves only one -request at a time. Some of the options available for properly running Flask in -production are documented here. +for production** as it doesn't scale well. Some of the options available for +properly running Flask in production are documented here. If you want to deploy your Flask application to a WSGI server not listed here, look up the server documentation about how to use a WSGI app with it. Just @@ -20,9 +19,11 @@ Hosted options - `Deploying Flask on Heroku `_ - `Deploying Flask on OpenShift `_ - `Deploying Flask on Webfaction `_ -- `Deploying Flask on Google App Engine `_ +- `Deploying Flask on Google App Engine `_ +- `Deploying Flask on AWS Elastic Beanstalk `_ - `Sharing your Localhost Server with Localtunnel `_ - `Deploying on Azure (IIS) `_ +- `Deploying on PythonAnywhere `_ Self-hosted options ------------------- @@ -30,8 +31,8 @@ Self-hosted options .. toctree:: :maxdepth: 2 - mod_wsgi wsgi-standalone uwsgi + mod_wsgi fastcgi cgi diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 0f4af6c3..ca694b7d 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -13,7 +13,7 @@ If you are using the `Apache`_ webserver, consider using `mod_wsgi`_. not called because this will always start a local WSGI server which we do not want if we deploy that application to mod_wsgi. -.. _Apache: http://httpd.apache.org/ +.. _Apache: https://httpd.apache.org/ Installing `mod_wsgi` --------------------- @@ -114,7 +114,7 @@ refuse to run with the above configuration. On a Windows system, eliminate those Note: There have been some changes in access control configuration for `Apache 2.4`_. -.. _Apache 2.4: http://httpd.apache.org/docs/trunk/upgrading.html +.. _Apache 2.4: https://httpd.apache.org/docs/trunk/upgrading.html Most notably, the syntax for directory permissions has changed from httpd 2.2 @@ -133,9 +133,9 @@ to httpd 2.4 syntax For more information consult the `mod_wsgi documentation`_. .. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi -.. _installation instructions: http://modwsgi.readthedocs.io/en/develop/installation.html +.. _installation instructions: https://modwsgi.readthedocs.io/en/develop/installation.html .. _virtual python: https://pypi.python.org/pypi/virtualenv -.. _mod_wsgi documentation: http://modwsgi.readthedocs.io/en/develop/index.html +.. _mod_wsgi documentation: https://modwsgi.readthedocs.io/en/develop/index.html Troubleshooting --------------- diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index fc991e72..50c85fb2 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -66,7 +66,7 @@ to have it in the URL root its a bit simpler:: uwsgi_pass unix:/tmp/yourapplication.sock; } -.. _nginx: http://nginx.org/ -.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: https://nginx.org/ +.. _lighttpd: https://www.lighttpd.net/ .. _cherokee: http://cherokee-project.com/ .. _uwsgi: http://projects.unbit.it/uwsgi/ diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index ad43c144..5b0740a6 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -27,6 +27,22 @@ For example, to run a Flask application with 4 worker processes (``-w .. _eventlet: http://eventlet.net/ .. _greenlet: https://greenlet.readthedocs.io/en/latest/ +uWSGI +-------- + +`uWSGI`_ is a fast application server written in C. It is very configurable +which makes it more complicated to setup than gunicorn. + +Running `uWSGI HTTP Router`_:: + + uwsgi --http 127.0.0.1:5000 --module myproject:app + +For a more optimized setup, see `configuring uWSGI and NGINX`_. + +.. _uWSGI: http://uwsgi-docs.readthedocs.io/en/latest/ +.. _uWSGI HTTP Router: http://uwsgi-docs.readthedocs.io/en/latest/HTTP.html#the-uwsgi-http-https-router +.. _configuring uWSGI and NGINX: uwsgi.html#starting-your-app-with-uwsgi + Gevent ------- @@ -62,7 +78,7 @@ as well; see ``twistd -h`` and ``twistd web -h`` for more information. For example, to run a Twisted Web server in the foreground, on port 8080, with an application from ``myproject``:: - twistd -n web --port 8080 --wsgi myproject.app + twistd -n web --port tcp:8080 --wsgi myproject.app .. _Twisted: https://twistedmatrix.com/ .. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 3bda5f15..4c260112 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -34,7 +34,7 @@ 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 +`_ for dealing with application errors. It's available as an Open Source project `on GitHub `__ and is also available as a `hosted version `_ which you can try for free. Sentry @@ -42,9 +42,9 @@ 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 `raven` client:: +To use Sentry you need to install the `raven` client with extra `flask` dependencies:: - $ pip install raven + $ pip install raven[flask] And then add this to your Flask app:: @@ -76,256 +76,78 @@ Error handlers You might want to show custom error pages to the user when an error occurs. This can be done by registering error handlers. -Error handlers are normal :ref:`views` but instead of being registered for -routes, they are registered for exceptions that are raised while trying to -do something else. +An error handler is a normal view function that return a response, but instead +of being registered for a route, it is registered for an exception or HTTP +status code that would is raised while trying to handle a request. Registering ``````````` -Register error handlers using :meth:`~flask.Flask.errorhandler` or -:meth:`~flask.Flask.register_error_handler`:: +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. :: @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): - return 'bad request!' - - app.register_error_handler(400, lambda e: 'bad request!') + return 'bad request!', 400 + + # or, without the decorator + app.register_error_handler(400, handle_bad_request) -Those two ways are equivalent, but the first one is more clear and leaves -you with a function to call on your whim (and in tests). Note that :exc:`werkzeug.exceptions.HTTPException` subclasses like -:exc:`~werkzeug.exceptions.BadRequest` from the example and their HTTP codes -are interchangeable when handed to the registration methods or decorator -(``BadRequest.code == 400``). +:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable +when registering handlers. (``BadRequest.code == 400``) -You are however not limited to :exc:`~werkzeug.exceptions.HTTPException` -or HTTP status codes but can register a handler for every exception class you -like. +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. :: -.. versionchanged:: 0.11 + class InsufficientStorage(werkzeug.exceptions.HTTPException): + code = 507 + description = 'Not enough storage space.' + + app.register_error_handler(InsuffcientStorage, handle_507) - Errorhandlers are now prioritized by specificity of the exception classes - they are registered for instead of the order they are registered in. + 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 ```````` -Once an exception instance is raised, its class hierarchy is traversed, -and searched for in the exception classes for which handlers are registered. -The most specific handler is selected. +When an exception is caught by Flask while handling a request, it is first +looked up by code. If no handler is registered for the code, it is looked up +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. -E.g. if an instance of :exc:`ConnectionRefusedError` is raised, and a handler +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 on the -exception instance, and its response is shown to the user. - -Error Mails ------------ - -If the application runs in production mode (which it will do on your -server) you might not see any log messages. The reason for that is that -Flask by default will just report to the WSGI error stream or stderr -(depending on what's available). Where this ends up is sometimes hard to -find. Often it's in your webserver's log files. - -I can pretty much promise you however that if you only use a logfile for -the application errors you will never look at it except for debugging an -issue when a user reported it for you. What you probably want instead is -a mail the second the exception happened. Then you get an alert and you -can do something about it. - -Flask uses the Python builtin logging system, and it can actually send -you mails for errors which is probably what you want. Here is how you can -configure the Flask logger to send you mails for exceptions:: - - ADMINS = ['yourname@example.com'] - if not app.debug: - import logging - from logging.handlers import SMTPHandler - mail_handler = SMTPHandler('127.0.0.1', - 'server-error@example.com', - ADMINS, 'YourApplication Failed') - mail_handler.setLevel(logging.ERROR) - app.logger.addHandler(mail_handler) - -So what just happened? We created a new -:class:`~logging.handlers.SMTPHandler` that will send mails with the mail -server listening on ``127.0.0.1`` to all the `ADMINS` from the address -*server-error@example.com* with the subject "YourApplication Failed". If -your mail server requires credentials, these can also be provided. For -that check out the documentation for the -:class:`~logging.handlers.SMTPHandler`. - -We also tell the handler to only send errors and more critical messages. -Because we certainly don't want to get a mail for warnings or other -useless logs that might happen during request handling. - -Before you run that in production, please also look at :ref:`logformat` to -put more information into that error mail. That will save you from a lot -of frustration. - - -Logging to a File ------------------ - -Even if you get mails, you probably also want to log warnings. It's a -good idea to keep as much information around that might be required to -debug a problem. By default as of Flask 0.11, errors are logged to your -webserver's log automatically. Warnings however are not. Please note -that Flask itself will not issue any warnings in the core system, so it's -your responsibility to warn in the code if something seems odd. - -There are a couple of handlers provided by the logging system out of the -box but not all of them are useful for basic error logging. The most -interesting are probably the following: - -- :class:`~logging.FileHandler` - logs messages to a file on the - filesystem. -- :class:`~logging.handlers.RotatingFileHandler` - logs messages to a file - on the filesystem and will rotate after a certain number of messages. -- :class:`~logging.handlers.NTEventLogHandler` - will log to the system - event log of a Windows system. If you are deploying on a Windows box, - this is what you want to use. -- :class:`~logging.handlers.SysLogHandler` - sends logs to a UNIX - syslog. - -Once you picked your log handler, do like you did with the SMTP handler -above, just make sure to use a lower setting (I would recommend -`WARNING`):: - - if not app.debug: - import logging - from themodule import TheHandlerYouWant - file_handler = TheHandlerYouWant(...) - file_handler.setLevel(logging.WARNING) - app.logger.addHandler(file_handler) - -.. _logformat: - -Controlling the Log Format --------------------------- - -By default a handler will only write the message string into a file or -send you that message as mail. A log record stores more information, -and it makes a lot of sense to configure your logger to also contain that -information so that you have a better idea of why that error happened, and -more importantly, where it did. - -A formatter can be instantiated with a format string. Note that -tracebacks are appended to the log entry automatically. You don't have to -do that in the log formatter format string. - -Here some example setups: - -Email -````` - -:: - - from logging import Formatter - mail_handler.setFormatter(Formatter(''' - Message type: %(levelname)s - Location: %(pathname)s:%(lineno)d - Module: %(module)s - Function: %(funcName)s - Time: %(asctime)s - - Message: - - %(message)s - ''')) - -File logging -```````````` - -:: - - from logging import Formatter - file_handler.setFormatter(Formatter( - '%(asctime)s %(levelname)s: %(message)s ' - '[in %(pathname)s:%(lineno)d]' - )) - - -Complex Log Formatting -`````````````````````` - -Here is a list of useful formatting variables for the format string. Note -that this list is not complete, consult the official documentation of the -:mod:`logging` package for a full list. - -.. tabularcolumns:: |p{3cm}|p{12cm}| - -+------------------+----------------------------------------------------+ -| Format | Description | -+==================+====================================================+ -| ``%(levelname)s``| Text logging level for the message | -| | (``'DEBUG'``, ``'INFO'``, ``'WARNING'``, | -| | ``'ERROR'``, ``'CRITICAL'``). | -+------------------+----------------------------------------------------+ -| ``%(pathname)s`` | Full pathname of the source file where the | -| | logging call was issued (if available). | -+------------------+----------------------------------------------------+ -| ``%(filename)s`` | Filename portion of pathname. | -+------------------+----------------------------------------------------+ -| ``%(module)s`` | Module (name portion of filename). | -+------------------+----------------------------------------------------+ -| ``%(funcName)s`` | Name of function containing the logging call. | -+------------------+----------------------------------------------------+ -| ``%(lineno)d`` | Source line number where the logging call was | -| | issued (if available). | -+------------------+----------------------------------------------------+ -| ``%(asctime)s`` | Human-readable time when the LogRecord` was | -| | created. By default this is of the form | -| | ``"2003-07-08 16:49:45,896"`` (the numbers after | -| | the comma are millisecond portion of the time). | -| | This can be changed by subclassing the formatter | -| | and overriding the | -| | :meth:`~logging.Formatter.formatTime` method. | -+------------------+----------------------------------------------------+ -| ``%(message)s`` | The logged message, computed as ``msg % args`` | -+------------------+----------------------------------------------------+ - -If you want to further customize the formatting, you can subclass the -formatter. The formatter has three interesting methods: - -:meth:`~logging.Formatter.format`: - handles the actual formatting. It is passed a - :class:`~logging.LogRecord` object and has to return the formatted - string. -:meth:`~logging.Formatter.formatTime`: - called for `asctime` formatting. If you want a different time format - you can override this method. -:meth:`~logging.Formatter.formatException` - called for exception formatting. It is passed an :attr:`~sys.exc_info` - tuple and has to return a string. The default is usually fine, you - don't have to override it. - -For more information, head over to the official documentation. - - -Other Libraries ---------------- - -So far we only configured the logger your application created itself. -Other libraries might log themselves as well. For example, SQLAlchemy uses -logging heavily in its core. While there is a method to configure all -loggers at once in the :mod:`logging` package, I would not recommend using -it. There might be a situation in which you want to have multiple -separate applications running side by side in the same Python interpreter -and then it becomes impossible to have different logging setups for those. - -Instead, I would recommend figuring out which loggers you are interested -in, getting the loggers with the :func:`~logging.getLogger` function and -iterating over them to attach handlers:: - - from logging import getLogger - loggers = [app.logger, getLogger('sqlalchemy'), - getLogger('otherlibrary')] - for logger in loggers: - logger.addHandler(mail_handler) - logger.addHandler(file_handler) +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. + +.. versionchanged:: 0.11 + + Handlers are prioritized by specificity of the exception classes they are + registered for instead of the order they are registered in. + +Logging +------- + +See :ref:`logging` for information on how to log exceptions, such as by +emailing them to admins. Debugging Application Errors diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index d73d6019..29c33f3c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -29,12 +29,6 @@ be something like "Flask-SimpleXML". Make sure to include the name This is how users can then register dependencies to your extension in their :file:`setup.py` files. -Flask sets up a redirect package called :data:`flask.ext` where users -should import the extensions from. If you for instance have a package -called ``flask_something`` users would import it as -``flask.ext.something``. This is done to transition from the old -namespace packages. See :ref:`ext-import-transition` for more details. - But what do extensions look like themselves? An extension has to ensure that it works with multiple Flask application instances at once. This is a requirement because many people will use patterns like the @@ -48,7 +42,7 @@ that people can easily install the development version into their virtualenv without having to download the library by hand. Flask extensions must be licensed under a BSD, MIT or more liberal license -to be able to be enlisted in the Flask Extension Registry. Keep in mind +in order to be listed in the Flask Extension Registry. Keep in mind that the Flask Extension Registry is a moderated place and libraries will be reviewed upfront if they behave as required. @@ -154,10 +148,10 @@ What to use depends on what you have in mind. For the SQLite 3 extension we will use the class-based approach because it will provide users with an object that handles opening and closing database connections. -What's important about classes is that they encourage to be shared around -on module level. In that case, the object itself must not under any +When designing your classes, it's important to make them easily reusable +at the module level. This means the object itself must not under any circumstances store any application specific state and must be shareable -between different application. +between different applications. The Extension Code ------------------ @@ -334,10 +328,10 @@ development. If you want to learn more, it's a very good idea to check out existing extensions on the `Flask Extension Registry`_. If you feel lost there is still the `mailinglist`_ and the `IRC channel`_ to get some ideas for nice looking APIs. Especially if you do something nobody before -you did, it might be a very good idea to get some more input. This not -only to get an idea about what people might want to have from an -extension, but also to avoid having multiple developers working on pretty -much the same side by side. +you did, it might be a very good idea to get some more input. This not only +generates useful feedback on what people might want from an extension, but +also avoids having multiple developers working in isolation on pretty much the +same problem. Remember: good API design is hard, so introduce your project on the mailinglist, and let other developers give you a helping hand with @@ -370,10 +364,10 @@ extension to be approved you have to follow these guidelines: 3. APIs of approved extensions will be checked for the following characteristics: - - an approved extension has to support multiple applications - running in the same Python process. - - it must be possible to use the factory pattern for creating - applications. + - an approved extension has to support multiple applications + running in the same Python process. + - it must be possible to use the factory pattern for creating + applications. 4. The license must be BSD/MIT/WTFPL licensed. 5. The naming scheme for official extensions is *Flask-ExtensionName* or @@ -387,13 +381,11 @@ extension to be approved you have to follow these guidelines: link to the documentation, website (if there is one) and there must be a link to automatically install the development version (``PackageName==dev``). -9. The ``zip_safe`` flag in the setup script must be set to ``False``, - even if the extension would be safe for zipping. -10. An extension currently has to support Python 2.6 as well as - Python 2.7 +9. The ``zip_safe`` flag in the setup script must be set to ``False``, + even if the extension would be safe for zipping. +10. An extension currently has to support Python 3.4 and newer and 2.7. -.. _ext-import-transition: Extension Import Transition --------------------------- @@ -413,6 +405,6 @@ schema. The ``flask.ext.foo`` compatibility alias is still in Flask 0.11 but is now deprecated -- you should use ``flask_foo``. -.. _OAuth extension: http://pythonhosted.org/Flask-OAuth/ +.. _OAuth extension: https://pythonhosted.org/Flask-OAuth/ .. _mailinglist: http://flask.pocoo.org/mailinglist/ .. _IRC channel: http://flask.pocoo.org/community/irc/ diff --git a/docs/flaskext.py b/docs/flaskext.py deleted file mode 100644 index 33f47449..00000000 --- a/docs/flaskext.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/installation.rst b/docs/installation.rst index 6f833eac..88b9af09 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,163 +3,179 @@ Installation ============ -Flask depends on some external libraries, like `Werkzeug -`_ and `Jinja2 `_. -Werkzeug is a toolkit for WSGI, the standard Python interface between web -applications and a variety of servers for both development and deployment. -Jinja2 renders templates. +Python Version +-------------- -So how do you get all that on your computer quickly? There are many ways you -could do that, but the most kick-ass method is virtualenv, so let's have a look -at that first. +We recommend using the latest version of Python 3. Flask supports Python 3.4 +and newer, Python 2.7, and PyPy. -You will need Python 2.6 or newer to get started, so be sure to have an -up-to-date Python 2.x installation. For using Flask with Python 3 have a -look at :ref:`python3-support`. +Dependencies +------------ -.. _virtualenv: +These distributions will be installed automatically when installing Flask. -virtualenv ----------- +* `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. -Virtualenv is probably what you want to use during development, and if you have -shell access to your production machines, you'll probably want to use it there, -too. +.. _Werkzeug: http://werkzeug.pocoo.org/ +.. _Jinja: http://jinja.pocoo.org/ +.. _MarkupSafe: https://pypi.python.org/pypi/MarkupSafe +.. _ItsDangerous: https://pythonhosted.org/itsdangerous/ +.. _Click: http://click.pocoo.org/ -What problem does virtualenv solve? If you like Python as much as I do, -chances are you want to use it for other projects besides Flask-based web -applications. But the more projects you have, the more likely it is that you -will be working with different versions of Python itself, or at least different -versions of Python libraries. Let's face it: quite often libraries break -backwards compatibility, and it's unlikely that any serious application will -have zero dependencies. So what do you do if two or more of your projects have -conflicting dependencies? +Optional dependencies +~~~~~~~~~~~~~~~~~~~~~ -Virtualenv to the rescue! Virtualenv enables multiple side-by-side -installations of Python, one for each project. It doesn't actually install -separate copies of Python, but it does provide a clever way to keep different -project environments isolated. Let's see how virtualenv works. +These distributions will not be installed automatically. Flask will detect and +use them if you install them. -If you are on Mac OS X or Linux, chances are that the following -command will work for you:: +* `Blinker`_ provides support for :ref:`signals`. +* `SimpleJSON`_ is a fast JSON implementation that is compatible with + Python's ``json`` module. It is preferred for JSON operations if it is + installed. +* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` + commands. +* `Watchdog`_ provides a faster, more efficient reloader for the development + server. - $ sudo pip install virtualenv +.. _Blinker: https://pythonhosted.org/blinker/ +.. _SimpleJSON: https://simplejson.readthedocs.io/ +.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme +.. _watchdog: https://pythonhosted.org/watchdog/ -It will probably install virtualenv on your system. Maybe it's even -in your package manager. If you use Ubuntu, try:: +Virtual environments +-------------------- - $ sudo apt-get install python-virtualenv +Use a virtual environment to manage the dependencies for your project, both in +development and in production. -If you are on Windows and don't have the ``easy_install`` command, you must -install it first. Check the :ref:`windows-easy-install` section for more -information about how to do that. Once you have it installed, run the same -commands as above, but without the ``sudo`` prefix. +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. -Once you have virtualenv installed, just fire up a shell and create -your own environment. I usually create a project folder and a :file:`venv` -folder within:: +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. - $ mkdir myproject - $ cd myproject - $ virtualenv venv - New python executable in venv/bin/python - Installing setuptools, pip............done. +Python 3 comes bundled with the :mod:`venv` module to create virtual +environments. If you're using a modern version of Python, you can continue on +to the next section. -Now, whenever you want to work on a project, you only have to activate the -corresponding environment. On OS X and Linux, do the following:: +If you're using Python 2, see :ref:`install-install-virtualenv` first. - $ . venv/bin/activate +.. _install-create-env: -If you are a Windows user, the following command is for you:: +Create an environment +~~~~~~~~~~~~~~~~~~~~~ - $ venv\Scripts\activate +Create a project folder and a :file:`venv` folder within: -Either way, you should now be using your virtualenv (notice how the prompt of -your shell has changed to show the active environment). +.. code-block:: sh -And if you want to go back to the real world, use the following command:: + mkdir myproject + cd myproject + python3 -m venv venv - $ deactivate +On Windows: -After doing this, the prompt of your shell should be as familiar as before. +.. code-block:: bat -Now, let's move on. Enter the following command to get Flask activated in your -virtualenv:: + py -3 -m venv venv - $ pip install Flask +If you needed to install virtualenv because you are on an older version of +Python, use the following command instead: -A few seconds later and you are good to go. +.. code-block:: sh + virtualenv venv -System-Wide Installation ------------------------- +On Windows: -This is possible as well, though I do not recommend it. Just run -``pip`` with root privileges:: +.. code-block:: bat - $ sudo pip install Flask + \Python27\Scripts\virtualenv.exe venv -(On Windows systems, run it in a command-prompt window with administrator -privileges, and leave out ``sudo``.) +Activate the environment +~~~~~~~~~~~~~~~~~~~~~~~~ +Before you work on your project, activate the corresponding environment: -Living on the Edge ------------------- +.. code-block:: sh + + . venv/bin/activate + +On Windows: + +.. code-block:: bat + + venv\Scripts\activate + +Your shell prompt will change to show the name of the activated environment. + +Install Flask +------------- -If you want to work with the latest version of Flask, there are two ways: you -can either let ``pip`` pull in the development version, or you can tell -it to operate on a git checkout. Either way, virtualenv is recommended. +Within the activated environment, use the following command to install Flask: -Get the git checkout in a new virtualenv and run in development mode:: +.. code-block:: sh - $ git clone http://github.com/pallets/flask.git - Initialized empty Git repository in ~/dev/flask/.git/ - $ cd flask - $ virtualenv venv - New python executable in venv/bin/python - Installing setuptools, pip............done. - $ . venv/bin/activate - $ python setup.py develop - ... - Finished processing dependencies for Flask + pip install Flask + +Living on the edge +~~~~~~~~~~~~~~~~~~ + +If you want to work with the latest Flask code before it's released, install or +update the code from the master branch: + +.. code-block:: sh + + pip install -U https://github.com/pallets/flask/archive/master.tar.gz + +.. _install-install-virtualenv: + +Install virtualenv +------------------ -This will pull in the dependencies and activate the git head as the current -version inside the virtualenv. Then all you have to do is run ``git pull -origin`` to update to the latest version. +If you are using Python 2, the venv module is not available. Instead, +install `virtualenv`_. -.. _windows-easy-install: +On Linux, virtualenv is provided by your package manager: -`pip` and `setuptools` on Windows ---------------------------------- +.. code-block:: sh -Sometimes getting the standard "Python packaging tools" like ``pip``, ``setuptools`` -and ``virtualenv`` can be a little trickier, but nothing very hard. The crucial -package you will need is pip - this will let you install -anything else (like virtualenv). Fortunately there is a "bootstrap script" -you can run to install. + # Debian, Ubuntu + sudo apt-get install python-virtualenv -If you don't currently have ``pip``, then `get-pip.py` will install it for you. + # CentOS, Fedora + sudo yum install python-virtualenv -`get-pip.py`_ + # Arch + sudo pacman -S python-virtualenv -It should be double-clickable once you download it. If you already have ``pip``, -you can upgrade them by running:: +If you are on Mac OS X or Windows, download `get-pip.py`_, then: - > pip install --upgrade pip setuptools +.. code-block:: sh -Most often, once you pull up a command prompt you want to be able to type ``pip`` -and ``python`` which will run those things, but this might not automatically happen -on Windows, because it doesn't know where those executables are (give either a try!). + sudo python2 Downloads/get-pip.py + sudo python2 -m pip install virtualenv -To fix this, you should be able to navigate to your Python install directory -(e.g :file:`C:\Python27`), then go to :file:`Tools`, then :file:`Scripts`, then find the -:file:`win_add2path.py` file and run that. Open a **new** Command Prompt and -check that you can now just type ``python`` to bring up the interpreter. +On Windows, as an administrator: -Finally, to install `virtualenv`_, you can simply run:: +.. code-block:: bat - > pip install virtualenv + \Python27\python.exe Downloads\get-pip.py + \Python27\python.exe -m pip install virtualenv -Then you can be off on your way following the installation instructions above. +Now you can continue to :ref:`install-create-env`. +.. _virtualenv: https://virtualenv.pypa.io/ .. _get-pip.py: https://bootstrap.pypa.io/get-pip.py diff --git a/docs/logging.rst b/docs/logging.rst new file mode 100644 index 00000000..36ed7c85 --- /dev/null +++ b/docs/logging.rst @@ -0,0 +1,175 @@ +.. _logging: + +Logging +======= + +Flask uses standard Python :mod:`logging`. All Flask-related messages are +logged under the ``'flask'`` logger namespace. +:meth:`Flask.logger ` returns the logger named +``'flask.app'``, and can be used to log messages for your application. :: + + @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) + + +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 request + from flask.logging import default_handler + + class RequestFormatter(logging.Formatter): + def format(self, record): + record.url = request.url + record.remote_addr = request.remote_addr + 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 ( + app.logger, + 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/patterns/appfactories.rst b/docs/patterns/appfactories.rst index dc9660ae..3e880205 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -6,7 +6,7 @@ Application Factories If you are already using packages and blueprints for your application (:ref:`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, +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? @@ -60,7 +60,7 @@ 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 `_, +Using `Flask-SQLAlchemy `_, as an example, you should not do something along those lines:: def create_app(config_filename): @@ -89,28 +89,29 @@ For more information about the design of extensions refer to :doc:`/extensiondev Using Applications ------------------ -So to use such an application you then have to create the application -first in a separate file otherwise the :command:`flask` command won't be able -to find it. Here an example :file:`exampleapp.py` file that creates such -an application:: +To run such an application, you can use the :command:`flask` command:: - from yourapplication import create_app - app = create_app('/path/to/config.cfg') - -It can then be used with the :command:`flask` command:: + export FLASK_APP=myapp + flask run + +Flask will automatically detect the factory (``create_app`` or ``make_app``) +in ``myapp``. You can also pass arguments to the factory like this:: - export FLASK_APP=exampleapp + export FLASK_APP="myapp:create_app('dev')" flask run + +Then the ``create_app`` factory in ``myapp`` is called with the string +``'dev'`` as the argument. See :doc:`/cli` for more detail. Factory Improvements -------------------- -The factory function from above is not very clever so far, you can improve -it. The following changes are straightforward and possible: +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 unittests 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 +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 creating if necessary. + 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/celery.rst b/docs/patterns/celery.rst index 673d953b..c3098a9e 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -1,24 +1,27 @@ -Celery Based Background Tasks -============================= +Celery Background Tasks +======================= -Celery is a task queue for Python with batteries included. It used to -have a Flask integration but it became unnecessary after some -restructuring of the internals of Celery with Version 3. This guide fills -in the blanks in how to properly use Celery with Flask but assumes that -you generally already read the `First Steps with Celery -`_ -guide in the official Celery documentation. +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. -Installing Celery ------------------ +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, but assumes you've already read the +`First Steps with Celery `_ +guide in the Celery documentation. -Celery is on the Python Package Index (PyPI), so it can be installed with -standard Python tools like :command:`pip` or :command:`easy_install`:: +Install +------- + +Celery is a separate Python package. Install it from PyPI using pip:: $ pip install celery -Configuring Celery ------------------- +Configure +--------- The first thing you need is a Celery instance, this is called the celery application. It serves the same purpose as the :class:`~flask.Flask` @@ -36,15 +39,18 @@ This is all that is necessary to properly integrate Celery with Flask:: from celery import Celery def make_celery(app): - celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], - broker=app.config['CELERY_BROKER_URL']) + celery = Celery( + app.import_name, + backend=app.config['CELERY_RESULT_BACKEND'], + broker=app.config['CELERY_BROKER_URL'] + ) celery.conf.update(app.config) - TaskBase = celery.Task - class ContextTask(TaskBase): - abstract = True + + class ContextTask(celery.Task): def __call__(self, *args, **kwargs): with app.app_context(): - return TaskBase.__call__(self, *args, **kwargs) + return self.run(*args, **kwargs) + celery.Task = ContextTask return celery @@ -53,11 +59,12 @@ from the application config, updates the rest of the Celery config from the Flask config and then creates a subclass of the task that wraps the task execution in an application context. -Minimal Example +An example task --------------- -With what we have above this is the minimal example of using Celery with -Flask:: +Let's write a task that adds two numbers together and returns the result. We +configure Celery's broker and backend to use Redis, create a ``celery`` +application using the factor from above, and then use it to define the task. :: from flask import Flask @@ -68,26 +75,27 @@ Flask:: ) celery = make_celery(flask_app) - @celery.task() def add_together(a, b): return a + b -This task can now be called in the background: +This task can now be called in the background:: ->>> result = add_together.delay(23, 42) ->>> result.wait() -65 + result = add_together.delay(23, 42) + result.wait() # 65 -Running the Celery Worker -------------------------- +Run a worker +------------ -Now if you jumped in and already executed the above code you will be -disappointed to learn that your ``.wait()`` will never actually return. -That's because you also need to run celery. You can do that by running -celery as a worker:: +If you jumped in and already executed the above code you will be +disappointed to learn that ``.wait()`` will never actually return. +That's because you also need to run a Celery worker to receive and execute the +task. :: $ celery -A your_application.celery worker The ``your_application`` string has to point to your application's package -or module that creates the `celery` object. +or module that creates the ``celery`` object. + +Now that the worker is running, ``wait`` will return the result once the task +is finished. diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index 886ae40a..bc20cdd6 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -3,71 +3,43 @@ 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. - -But 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 -before-request function 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 an after-request callback -instead. Sometimes however moving that code there is just not a very -pleasant experience or makes code look very awkward. - -As an alternative possibility you can attach a bunch of callback functions -to the :data:`~flask.g` object and call them at the end of the request. -This way you can defer code execution from anywhere in the application. - - -The Decorator -------------- - -The following decorator is the key. It registers a function on a list on -the :data:`~flask.g` object:: - - from flask import g - - def after_this_request(f): - if not hasattr(g, 'after_request_callbacks'): - g.after_request_callbacks = [] - g.after_request_callbacks.append(f) - return f - - -Calling the Deferred --------------------- - -Now you can use the `after_this_request` decorator to mark a function to -be called at the end of the request. But we still need to call them. For -this the following function needs to be registered as -:meth:`~flask.Flask.after_request` callback:: - - @app.after_request - def call_after_request_callbacks(response): - for callback in getattr(g, 'after_request_callbacks', ()): - callback(response) - return response - - -A Practical Example -------------------- +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 +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 the before-request function:: +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 + 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) + g.language = language diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index 72cc25d6..0c7c8658 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -88,15 +88,15 @@ support them and they make debugging a lot harder. Tagging Builds -------------- -It is useful to distinguish between release and development builds. Add a -:file:`setup.cfg` file to configure these options. +It is useful to distinguish between release and development builds. Add a +:file:`setup.cfg` file to configure these options. :: [egg_info] tag_build = .dev tag_date = 1 [aliases] - release = egg_info -RDb '' + release = egg_info -Db '' Running ``python setup.py sdist`` will create a development package with ".dev" and the current date appended: ``flaskr-1.0.dev20160314.tar.gz``. @@ -174,4 +174,4 @@ the code without having to run ``install`` again after each change. .. _pip: https://pypi.python.org/pypi/pip -.. _Setuptools: https://pythonhosted.org/setuptools +.. _Setuptools: https://pypi.python.org/pypi/setuptools diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index fccd4a6f..1df9c061 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -47,37 +47,53 @@ even if the application behaves correctly: Error Handlers -------------- -An error handler is a function, just like a view function, but it is -called when an error happens and is passed that error. The error is most -likely a :exc:`~werkzeug.exceptions.HTTPException`, but in one case it -can be a different error: a handler for internal server errors will be -passed other exception instances as well if they are uncaught. +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`. An error +handler for "500 Internal Server Error" will be passed uncaught exceptions in +addition to explicit 500 errors. An error handler is registered with the :meth:`~flask.Flask.errorhandler` -decorator and the error code of the exception. Keep in mind that Flask -will *not* set the error code for you, so make sure to also provide the -HTTP status code when returning a response. +decorator or the :meth:`~flask.Flask.register_error_handler` method. A handler +can be registered for a status code, like 404, or for an exception class. -Please note that if you add an error handler for "500 Internal Server -Error", Flask will not trigger it if it's running in Debug mode. +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. -Here an example implementation for a "404 Page Not Found" exception:: +A handler for "500 Internal Server Error" will not be used when running in +debug mode. Instead, the interactive debugger will be shown. + +Here is an example implementation for a "404 Page Not Found" exception:: 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 the :ref:`application factory pattern `:: + + 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: .. sourcecode:: 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 %} - + {% 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 %} diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst index acdee24b..21ea767f 100644 --- a/docs/patterns/favicon.rst +++ b/docs/patterns/favicon.rst @@ -49,5 +49,5 @@ web server's documentation. See also -------- -* The `Favicon `_ article on +* The `Favicon `_ article on Wikipedia diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 8ab8c033..c7ae14f9 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -21,7 +21,7 @@ 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, request, redirect, url_for + from flask import Flask, flash, request, redirect, url_for from werkzeug.utils import secure_filename UPLOAD_FOLDER = '/path/to/the/uploads' @@ -58,22 +58,22 @@ the file and redirects the user to the URL for the uploaded file:: return redirect(request.url) file = request.files['file'] # if user does not select file, browser also - # submit a empty part without filename + # submit an empty part without 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('uploaded_file', + return redirect(url_for('upload_file', filename=filename)) return ''' Upload new File

Upload new File

-

- + +

''' @@ -149,8 +149,8 @@ config key:: app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 -The code above will limited the maximum allowed payload to 16 megabytes. -If a larger file is transmitted, Flask will raise an +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. This feature was added in Flask 0.6 but can be achieved in older versions @@ -181,4 +181,4 @@ applications dealing with uploads, there is also a Flask extension called blacklisting of extensions and more. .. _jQuery: https://jquery.com/ -.. _Flask-Uploads: http://pythonhosted.org/Flask-Uploads/ +.. _Flask-Uploads: https://pythonhosted.org/Flask-Uploads/ diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 7efd1446..a61c719f 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -22,7 +22,7 @@ So here is a full example:: request, url_for app = Flask(__name__) - app.secret_key = 'some_secret' + app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' @app.route('/') def index(): diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 1cd77974..6b0ee7ad 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -17,6 +17,10 @@ this:: login.html ... +If you find yourself stuck on something, feel free +to take a look at the source code for this example. +You'll find `the full src for this example here`_. + Simple Packages --------------- @@ -61,10 +65,10 @@ that tells Flask where to find the application instance:: export FLASK_APP=yourapplication If you are outside of the project directory make sure to provide the exact -path to your application directory. Similiarly you can turn on "debug -mode" with this environment variable:: +path to your application directory. Similarly you can turn on the +development features like this:: - export FLASK_DEBUG=true + export FLASK_ENV=development In order to install and run the application you need to issue the following commands:: @@ -130,6 +134,7 @@ You should then end up with something like that:: .. _working-with-modules: +.. _the full src for this example here: https://github.com/pallets/flask/tree/master/examples/patterns/largerapp Working with Blueprints ----------------------- diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 40e048e0..8785a6e2 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -22,7 +22,7 @@ if you want to get started quickly. You can download `Flask-SQLAlchemy`_ from `PyPI `_. -.. _Flask-SQLAlchemy: http://pythonhosted.org/Flask-SQLAlchemy/ +.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/ Declarative @@ -108,9 +108,9 @@ Querying is simple as well: >>> User.query.filter(User.name == 'admin').first() -.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org/ .. _declarative: - http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ + https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ Manual Object Relational Mapping -------------------------------- @@ -135,7 +135,7 @@ Here is an example :file:`database.py` module for your application:: def init_db(): metadata.create_all(bind=engine) -As for the declarative approach you need to close the session after +As in the declarative approach, you need to close the session after each request or application context shutdown. Put this into your application module:: @@ -215,4 +215,4 @@ You can also pass strings of SQL statements to the (1, u'admin', u'admin@localhost') For more information about SQLAlchemy, head over to the -`website `_. +`website `_. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 66a7c4c4..eecaaae8 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,8 +3,8 @@ 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 +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:: @@ -67,11 +67,11 @@ the application context by hand:: Easy Querying ------------- -Now in each request handling function you can access `g.db` to get the +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`` +dictionaries instead of tuples, this could be inserted into the ``get_db`` function we created above:: def make_dicts(cursor, row): @@ -102,15 +102,15 @@ This would use Row objects rather than dicts to return the results of queries. T 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 +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:: @@ -131,7 +131,7 @@ 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 --------------- diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 2649cad6..0e53de17 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -19,7 +19,7 @@ forms. fun. You can get it from `PyPI `_. -.. _Flask-WTF: http://pythonhosted.org/Flask-WTF/ +.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ The Forms --------- diff --git a/docs/python3.rst b/docs/python3.rst deleted file mode 100644 index a7a4f165..00000000 --- a/docs/python3.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. _python3-support: - -Python 3 Support -================ - -Flask, its dependencies, and most Flask extensions support Python 3. -You should start using Python 3 for your next project, -but there are a few things to be aware of. - -You need to use Python 3.3 or higher. 3.2 and older are *not* supported. - -You should use the latest versions of all Flask-related packages. -Flask 0.10 and Werkzeug 0.9 were the first versions to introduce Python 3 support. - -Python 3 changed how unicode and bytes are handled, which complicates how low -level code handles HTTP data. This mainly affects WSGI middleware interacting -with the WSGI ``environ`` data. Werkzeug wraps that information in high-level -helpers, so encoding issues should not affect you. - -The majority of the upgrade work is in the lower-level libraries like -Flask and Werkzeug, not the high-level application code. -For example, all of the examples in the Flask repository work on both Python 2 and 3 -and did not require a single line of code changed. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b444e080..334d7dc4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -50,7 +50,14 @@ to tell your terminal the application to work with by exporting the $ flask run * Running on http://127.0.0.1:5000/ -If you are on Windows you need to use ``set`` instead of ``export``. +If you are on Windows, the environment variable syntax depends on command line +interpreter. On Command Prompt:: + + C:\path\to\app>set FLASK_APP=hello.py + +And on PowerShell:: + + PS C:\path\to\app> $env:FLASK_APP = "hello.py" Alternatively you can use :command:`python -m flask`:: @@ -102,9 +109,9 @@ docs to see the alternative method for running a server. Invalid Import Name ``````````````````` -The ``FLASK_APP`` environment variable is the name of the module to import at -:command:`flask run`. In case that module is incorrectly named you will get an -import error upon start (or if debug is enabled when you navigate to the +The ``FLASK_APP`` environment variable is the name of the module to import at +:command:`flask run`. In case that module is incorrectly named you will get an +import error upon start (or if debug is enabled when you navigate to the application). It will tell you what it tried to import and why it failed. The most common reason is a typo or because you did not actually create an @@ -123,13 +130,14 @@ That is not very nice and Flask can do better. If you enable debug support the server will reload itself on code changes, and it will also provide you with a helpful debugger if things go wrong. -To enable debug mode you can export the ``FLASK_DEBUG`` environment variable +To enable all development features (including debug mode) you can export +the ``FLASK_ENV`` environment variable and set it to ``development`` before running the server:: - $ export FLASK_DEBUG=1 + $ export FLASK_ENV=development $ flask run -(On Windows you need to use ``set`` instead of ``export``). +(On Windows you need to use ``set`` instead of ``export``.) This does the following things: @@ -137,6 +145,9 @@ This does the following things: 2. it activates the automatic reloader 3. it enables the debug mode on the Flask application. +You can also control debug mode separately from the environment by +exporting ``FLASK_DEBUG=1``. + There are more parameters that are explained in the :ref:`server` docs. .. admonition:: Attention @@ -153,20 +164,22 @@ Screenshot of the debugger in action: :class: screenshot :alt: screenshot of debugger in action +More information on using the debugger can be found in the `Werkzeug +documentation`_. + +.. _Werkzeug documentation: http://werkzeug.pocoo.org/docs/debug/#using-the-debugger + Have another debugger in mind? See :ref:`working-with-debuggers`. Routing ------- -Modern web applications have beautiful URLs. This helps people remember -the URLs, which is especially handy for applications that are used from -mobile devices with slower network connections. If the user can directly -go to the desired page without having to hit the index page it is more -likely they will like the page and come back next time. +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. -As you have seen above, the :meth:`~flask.Flask.route` decorator is used to -bind a function to a URL. Here are some basic examples:: +Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: @app.route('/') def index(): @@ -176,16 +189,16 @@ bind a function to a URL. Here are some basic examples:: def hello(): return 'Hello, World' -But there is more to it! You can make certain parts of the URL dynamic and -attach multiple rules to a function. +You can do more! You can make parts of the URL dynamic and attach multiple +rules to a function. Variable Rules `````````````` -To add variable parts to a URL you can mark these special sections as -````. Such a part is then passed as a keyword argument to your -function. Optionally a converter can be used by specifying a rule with -````. Here are some nice examples:: +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 ````. :: @app.route('/user/') def show_user_profile(username): @@ -197,177 +210,124 @@ function. Optionally a converter can be used by specifying a rule with # show the post with the given id, the id is an integer return 'Post %d' % post_id -The following converters exist: - -=========== =============================================== -`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 -=========== =============================================== + @app.route('/path/') + def show_subpath(subpath): + # show the subpath after /path/ + return 'Subpath %s' % subpath -.. admonition:: Unique URLs / Redirection Behavior +Converter types: - Flask's URL rules are based on Werkzeug's routing module. The idea - behind that module is to ensure beautiful and unique URLs based on - precedents laid down by Apache and earlier HTTP servers. +========== ========================================== +``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 +========== ========================================== - Take these two rules:: +Unique URLs / Redirection Behavior +`````````````````````````````````` - @app.route('/projects/') - def projects(): - return 'The project page' +Take these two rules:: - @app.route('/about') - def about(): - return 'The about page' + @app.route('/projects/') + def projects(): + return 'The project page' - Though they look rather similar, they differ in their use of the trailing - slash in the URL *definition*. In the first case, the canonical URL for the - ``projects`` endpoint has a trailing slash. In that sense, it is similar to - a folder on a filesystem. Accessing it without a trailing slash will cause - Flask to redirect to the canonical URL with the trailing slash. + @app.route('/about') + def about(): + return 'The about page' - In the second case, however, the URL is defined without a trailing slash, - rather like the pathname of a file on UNIX-like systems. Accessing the URL - with a trailing slash will produce a 404 "Not Found" error. +Though they look similar, they differ in their use of the trailing slash in +the URL. In the first case, the canonical URL for the ``projects`` endpoint +uses a trailing slash. It's similar to a folder in a file system; if you +access the URL without a trailing slash, Flask redirects you to the +canonical URL with the trailing slash. - This behavior allows relative URLs to continue working even if the trailing - slash is omitted, consistent with how Apache and other servers work. Also, - the URLs will stay unique, which helps search engines avoid indexing the - same page twice. +In the second case, however, the URL definition lacks a trailing slash, +like the pathname of a file on UNIX-like systems. Accessing the URL with a +trailing slash produces a 404 “Not Found” error. +This behavior allows relative URLs to continue working even if the trailing +slash is omitted, consistent with how Apache and other servers work. Also, +the URLs will stay unique, which helps search engines avoid indexing the +same page twice. .. _url-building: URL Building ```````````` -If it can match URLs, can Flask also generate them? Of course it can. To -build a URL to a specific function you can use the :func:`~flask.url_for` -function. It accepts the name of the function as first argument and a number -of keyword arguments, each corresponding to the variable part of the URL rule. -Unknown variable parts are appended to the URL as query parameters. Here are -some examples:: - - >>> from flask import Flask, url_for - >>> app = Flask(__name__) - >>> @app.route('/') - ... def index(): pass - ... - >>> @app.route('/login') - ... def login(): pass - ... - >>> @app.route('/user/') - ... def profile(username): pass - ... - >>> 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') - ... - / - /login - /login?next=/ - /user/John%20Doe - -(This also uses the :meth:`~flask.Flask.test_request_context` method, explained -below. It tells Flask to behave as though it is handling a request, even -though we are interacting with it through a Python shell. Have a look at the -explanation below. :ref:`context-locals`). +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? -There are three good reasons for this: -1. Reversing is often more descriptive than hard-coding the URLs. More - importantly, it allows you to change URLs in one go, without having to - remember to change URLs all over the place. -2. URL building will handle escaping of special characters and Unicode - data transparently for you, so you don't have to deal with them. -3. If your application is placed outside the URL root - say, in - ``/myapplication`` instead of ``/`` - :func:`~flask.url_for` will handle - that properly for you. +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 and Unicode data + transparently. +4. 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`. :: + + from flask import Flask, url_for + + app = Flask(__name__) + + @app.route('/') + def index(): + return 'index' + + @app.route('/login') + def login(): + return 'login' + @app.route('/user/') + def profile(username): + return '{}'s profile'.format(username) + + 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')) + + / + /login + /login?next=/ + /user/John%20Doe HTTP Methods ```````````` -HTTP (the protocol web applications are speaking) knows different methods for -accessing URLs. By default, a route only answers to ``GET`` requests, but that -can be changed by providing the ``methods`` argument to the -:meth:`~flask.Flask.route` decorator. Here are some examples:: - - from flask import request +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. +:: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': - do_the_login() + return do_the_login() else: - show_the_login_form() - -If ``GET`` is present, ``HEAD`` will be added automatically for you. You -don't have to deal with that. It will also make sure that ``HEAD`` requests -are handled as the `HTTP RFC`_ (the document describing the HTTP -protocol) demands, so you can completely ignore that part of the HTTP -specification. Likewise, as of Flask 0.6, ``OPTIONS`` is implemented for you -automatically as well. - -You have no idea what an HTTP method is? Worry not, here is a quick -introduction to HTTP methods and why they matter: - -The HTTP method (also often called "the verb") tells the server what the -client wants to *do* with the requested page. The following methods are -very common: - -``GET`` - The browser tells the server to just *get* the information stored on - that page and send it. This is probably the most common method. - -``HEAD`` - The browser tells the server to get the information, but it is only - interested in the *headers*, not the content of the page. An - application is supposed to handle that as if a ``GET`` request was - received but to not deliver the actual content. In Flask you don't - have to deal with that at all, the underlying Werkzeug library handles - that for you. - -``POST`` - The browser tells the server that it wants to *post* some new - information to that URL and that the server must ensure the data is - stored and only stored once. This is how HTML forms usually - transmit data to the server. - -``PUT`` - Similar to ``POST`` but the server might trigger the store procedure - multiple times by overwriting the old values more than once. Now you - might be asking why this is useful, but there are some good reasons - to do it this way. Consider that the connection is lost during - transmission: in this situation a system between the browser and the - server might receive the request safely a second time without breaking - things. With ``POST`` that would not be possible because it must only - be triggered once. - -``DELETE`` - Remove the information at the given location. - -``OPTIONS`` - Provides a quick way for a client to figure out which methods are - supported by this URL. Starting with Flask 0.6, this is implemented - for you automatically. - -Now the interesting part is that in HTML4 and XHTML1, the only methods a -form can submit to the server are ``GET`` and ``POST``. But with JavaScript -and future HTML standards you can use the other methods as well. Furthermore -HTTP has become quite popular lately and browsers are no longer the only -clients that are using HTTP. For instance, many revision control systems -use it. - -.. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt + return show_the_login_form() + +If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method +and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise, +``OPTIONS`` is automatically implemented for you. + +.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt Static Files ------------ @@ -538,16 +498,16 @@ 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 +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 +: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 +:attr:`~flask.Request.form` attribute. Here is a full example of the two attributes mentioned above:: @app.route('/login', methods=['POST', 'GET']) @@ -570,7 +530,7 @@ 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:: +:attr:`~flask.Request.args` attribute:: searchword = request.args.get('key', '') @@ -579,7 +539,7 @@ We recommend accessing URL parameters with `get` or by catching the 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. +to the :class:`~flask.Request` documentation. File Uploads @@ -768,6 +728,9 @@ sessions work:: app = Flask(__name__) + # 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: @@ -792,24 +755,18 @@ sessions work:: session.pop('username', None) return redirect(url_for('index')) - # set the secret key. keep this really secret: - app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' - The :func:`~flask.escape` mentioned here does escaping for you if you are not using the template engine (as in this example). .. admonition:: How to generate good secret keys - The problem with random is that it's hard to judge what is truly random. And - a secret key should be as random as possible. Your operating system - has ways to generate pretty random stuff based on a cryptographic - random generator which can be used to get such a key:: - - >>> import os - >>> os.urandom(24) - '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O`_. +`_. Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused @@ -38,7 +38,7 @@ either double or single quotes when using Jinja expressions in them: .. sourcecode:: html+jinja - the text + Why is this necessary? Because if you would not be doing that, an attacker could easily inject custom JavaScript handlers. For example an @@ -46,15 +46,26 @@ attacker could inject this piece of HTML+JavaScript: .. sourcecode:: html - onmouseover=alert(document.cookie) + onmouseover=alert(document.cookie) -When the user would then move with the mouse over the link, the 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) --------------------------------- @@ -104,3 +115,146 @@ 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 + +X-XSS-Protection +~~~~~~~~~~~~~~~~ + +The browser will try to prevent reflected XSS attacks by not loading the page +if the request contains something that looks like JavaScript and the response +contains the same data. :: + + response.headers['X-XSS-Protection'] = '1; mode=block' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + + +.. _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 diff --git a/docs/server.rst b/docs/server.rst index f8332ebf..db431a6c 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -12,23 +12,33 @@ but you can also continue using the :meth:`Flask.run` method. Command Line ------------ -The :command:`flask` command line script (:ref:`cli`) is strongly recommended for -development because it provides a superior reload experience due to how it -loads the application. The basic usage is like this:: +The :command:`flask` command line script (:ref:`cli`) is strongly +recommended for development because it provides a superior reload +experience due to how it loads the application. The basic usage is like +this:: $ export FLASK_APP=my_application - $ export FLASK_DEBUG=1 + $ export FLASK_ENV=development $ flask run -This will enable the debugger, the reloader and then start the server on +This enables the development environment, including the interactive +debugger and reloader, and then starts the server on *http://localhost:5000/*. The individual features of the server can be controlled by passing more -arguments to the ``run`` option. For instance the reloader can be +arguments to the ``run`` option. For instance the reloader can be disabled:: $ flask run --no-reload +.. note:: + + Prior to Flask 1.0 the :envvar:`FLASK_ENV` environment variable was + not supported and you needed to enable debug mode by exporting + ``FLASK_DEBUG=1``. This can still be used to control debug mode, but + you should prefer setting the development environment as shown + above. + In Code ------- diff --git a/docs/signals.rst b/docs/signals.rst index 2426e920..40041491 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -27,7 +27,7 @@ executed in undefined order and do not modify any data. The big advantage of signals over handlers is that you can safely subscribe to them for just a split second. These temporary -subscriptions are helpful for unittesting for example. Say you want to +subscriptions are helpful for unit testing for example. Say you want to know what templates were rendered as part of a request: signals allow you to do exactly that. @@ -45,7 +45,7 @@ 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 unittest +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:: diff --git a/docs/styleguide.rst b/docs/styleguide.rst index e03e4ef5..390d5668 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -167,7 +167,7 @@ Docstring conventions: """ Module header: - The module header consists of an utf-8 encoding declaration (if non + The module header consists of a utf-8 encoding declaration (if non ASCII letters are used, but it is recommended all the time) and a standard docstring:: diff --git a/docs/templating.rst b/docs/templating.rst index 9ba2223a..c0af6639 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -1,3 +1,5 @@ +.. _templates: + Templates ========= diff --git a/docs/testing.rst b/docs/testing.rst index 0737936e..79856341 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -5,23 +5,30 @@ Testing Flask Applications **Something that is untested is broken.** -The origin of this quote is unknown and while it is not entirely correct, it is also -not far from the truth. Untested applications make it hard to +The origin of this quote is unknown and while it is not entirely correct, it +is also not far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to become pretty paranoid. If an application has automated tests, you can safely make changes and instantly know if anything breaks. Flask provides a way to test your application by exposing the Werkzeug test :class:`~werkzeug.test.Client` and handling the context locals for you. -You can then use that with your favourite testing solution. In this documentation -we will use the :mod:`unittest` package that comes pre-installed with Python. +You can then use that with your favourite testing solution. + +In this documentation we will use the `pytest`_ package as the base +framework for our tests. You can install it with ``pip``, like so:: + + pip install pytest + +.. _pytest: + https://pytest.org The Application --------------- First, we need an application to test; we will use the application from the :ref:`tutorial`. If you don't have that application yet, get the -sources from `the examples`_. +source code from `the examples`_. .. _the examples: https://github.com/pallets/flask/tree/master/examples/flaskr/ @@ -29,90 +36,91 @@ sources from `the examples`_. The Testing Skeleton -------------------- -In order to test the application, we add a second module -(:file:`flaskr_tests.py`) and create a unittest skeleton there:: +We begin by adding a tests directory under the application root. Then +create a Python file to store our tests (:file:`test_flaskr.py`). When we +format the filename like ``test_*.py``, it will be auto-discoverable by +pytest. + +Next, we create a `pytest fixture`_ called +:func:`client` that configures +the application for testing and initializes a new database.:: import os - import flaskr - import unittest import tempfile - class FlaskrTestCase(unittest.TestCase): + import pytest - def setUp(self): - self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True - self.app = flaskr.app.test_client() - with flaskr.app.app_context(): - flaskr.init_db() + from flaskr import flaskr - def tearDown(self): - os.close(self.db_fd) - os.unlink(flaskr.app.config['DATABASE']) - if __name__ == '__main__': - unittest.main() + @pytest.fixture + def client(): + db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True + client = flaskr.app.test_client() + + with flaskr.app.app_context(): + flaskr.init_db() + + yield client -The code in the :meth:`~unittest.TestCase.setUp` method creates a new test -client and initializes a new database. This function is called before -each individual test function is run. To delete the database after the -test, we close the file and remove it from the filesystem in the -:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the -``TESTING`` config flag is activated. What it does is disable the error -catching during request handling so that you get better error reports when -performing test requests against the application. + os.close(db_fd) + os.unlink(flaskr.app.config['DATABASE']) -This test client will give us a simple interface to the application. We can -trigger test requests to the application, and the client will also keep track -of cookies for us. +This client fixture will be called by each individual test. It gives us a +simple interface to the application, where we can trigger test requests to the +application. The client will also keep track of cookies for us. -Because SQLite3 is filesystem-based we can easily use the tempfile module +During setup, the ``TESTING`` config flag is activated. What +this does is disable error catching during request handling, so that +you get better error reports when performing test requests against the +application. + +Because SQLite3 is filesystem-based, we can easily use the :mod:`tempfile` module to create a temporary database and initialize it. The :func:`~tempfile.mkstemp` function does two things for us: it returns a low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. +To delete the database after the test, the fixture closes the file and removes +it from the filesystem. + If we now run the test suite, we should see the following output:: - $ python flaskr_tests.py + $ pytest - ---------------------------------------------------------------------- - Ran 0 tests in 0.000s + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 0 items - OK + =========== no tests ran in 0.07 seconds ============ -Even though it did not run any actual tests, we already know that our flaskr +Even though it did not run any actual tests, we already know that our ``flaskr`` application is syntactically valid, otherwise the import would have died with an exception. +.. _pytest fixture: + https://docs.pytest.org/en/latest/fixture.html + The First Test -------------- Now it's time to start testing the functionality of the application. Let's check that the application shows "No entries here so far" if we -access the root of the application (``/``). To do this, we add a new -test method to our class, like this:: - - class FlaskrTestCase(unittest.TestCase): - - def setUp(self): - self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - self.app = flaskr.app.test_client() - flaskr.init_db() +access the root of the application (``/``). To do this, we add a new +test function to :file:`test_flaskr.py`, like this:: - def tearDown(self): - os.close(self.db_fd) - os.unlink(flaskr.app.config['DATABASE']) + def test_empty_db(client): + """Start with a blank database.""" - def test_empty_db(self): - rv = self.app.get('/') - assert b'No entries here so far' in rv.data + rv = client.get('/') + assert b'No entries here so far' in rv.data Notice that our test functions begin with the word `test`; this allows -:mod:`unittest` to automatically identify the method as a test to run. +`pytest`_ to automatically identify the function as a test to run. -By using `self.app.get` we can send an HTTP ``GET`` request to the application with +By using ``client.get`` we can send an HTTP ``GET`` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure that @@ -120,12 +128,15 @@ the return value (as string) from the application. In this case, we ensure that Run it again and you should see one passing test:: - $ python flaskr_tests.py - . - ---------------------------------------------------------------------- - Ran 1 test in 0.034s + $ pytest -v - OK + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 1 items + + tests/test_flaskr.py::test_empty_db PASSED + + ============= 1 passed in 0.10 seconds ============== Logging In and Out ------------------ @@ -136,39 +147,47 @@ of the application. To do this, we fire some requests to the login and logout pages with the required form data (username and password). And because the login and logout pages redirect, we tell the client to `follow_redirects`. -Add the following two methods to your `FlaskrTestCase` class:: +Add the following two functions to your :file:`test_flaskr.py` file:: + + def login(client, username, password): + return client.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) - def login(self, username, password): - return self.app.post('/login', data=dict( - username=username, - password=password - ), follow_redirects=True) - def logout(self): - return self.app.get('/logout', follow_redirects=True) + def logout(client): + return client.get('/logout', follow_redirects=True) Now we can easily test that logging in and out works and that it fails with -invalid credentials. Add this new test to the class:: - - def test_login_logout(self): - rv = self.login('admin', 'default') - assert b'You were logged in' in rv.data - rv = self.logout() - assert b'You were logged out' in rv.data - rv = self.login('adminx', 'default') - assert b'Invalid username' in rv.data - rv = self.login('admin', 'defaultx') - assert b'Invalid password' in rv.data +invalid credentials. Add this new test function:: + + def test_login_logout(client): + """Make sure login and logout works.""" + + rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) + assert b'You were logged in' in rv.data + + rv = logout(client) + assert b'You were logged out' in rv.data + + rv = login(client, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD']) + assert b'Invalid username' in rv.data + + rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x') + assert b'Invalid password' in rv.data Test Adding Messages -------------------- -We should also test that adding messages works. Add a new test method +We should also test that adding messages works. Add a new test function like this:: - def test_messages(self): - self.login('admin', 'default') - rv = self.app.post('/add', data=dict( + def test_messages(client): + """Test that messages work.""" + + login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) + rv = client.post('/add', data=dict( title='', text='HTML allowed here' ), follow_redirects=True) @@ -181,22 +200,25 @@ which is the intended behavior. Running that should now give us three passing tests:: - $ python flaskr_tests.py - ... - ---------------------------------------------------------------------- - Ran 3 tests in 0.332s + $ pytest -v - OK + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 3 items + + tests/test_flaskr.py::test_empty_db PASSED + tests/test_flaskr.py::test_login_logout PASSED + tests/test_flaskr.py::test_messages PASSED + + ============= 3 passed in 0.23 seconds ============== For more complex tests with headers and status codes, check out the `MiniTwit Example`_ from the sources which contains a larger test suite. - .. _MiniTwit Example: https://github.com/pallets/flask/tree/master/examples/minitwit/ - Other Testing Tricks -------------------- @@ -208,7 +230,7 @@ temporarily. With this you can access the :class:`~flask.request`, functions. Here is a full example that demonstrates this approach:: import flask - + app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): @@ -353,3 +375,81 @@ independently of the session backend used:: Note that in this case you have to use the ``sess`` object instead of the :data:`flask.session` proxy. The object however itself will provide the same interface. + + +Testing JSON APIs +----------------- + +.. versionadded:: 1.0 + +Flask has great support for JSON, and is a popular choice for building JSON +APIs. Making requests with JSON data and examining JSON data in responses is +very convenient:: + + from flask import request, jsonify + + @app.route('/api/auth') + def auth(): + json_data = request.get_json() + email = json_data['email'] + password = json_data['password'] + return jsonify(token=generate_token(email, password)) + + with app.test_client() as c: + rv = c.post('/api/auth', json={ + 'username': 'flask', 'password': 'secret' + }) + json_data = rv.get_json() + assert verify_token(email, json_data['token']) + +Passing the ``json`` argument in the test client methods sets the request data +to the JSON-serialized object and sets the content type to +``application/json``. You can get the JSON data from the request or response +with ``get_json``. + + +.. _testing-cli: + +Testing CLI Commands +-------------------- + +Click comes with `utilities for testing`_ your CLI commands. + +Use :meth:`CliRunner.invoke ` to call +commands in the same way they would be called from the command line. The +:class:`~click.testing.CliRunner` runs the command in isolation and +captures the output in a :class:`~click.testing.Result` object. :: + + import click + from click.testing import CliRunner + + @app.cli.command('hello') + @click.option('--name', default='World') + def hello_command(name) + click.echo(f'Hello, {name}!') + + def test_hello(): + runner = CliRunner() + result = runner.invoke(hello_command, ['--name', 'Flask']) + assert 'Hello, Flask' in result.output + +If you want to test how your command parses parameters, without running +the command, use the command's :meth:`~click.BaseCommand.make_context` +method. This is useful for testing complex validation rules and custom +types. :: + + def upper(ctx, param, value): + if value is not None: + return value.upper() + + @app.cli.command('hello') + @click.option('--name', default='World', callback=upper) + def hello_command(name) + click.echo(f'Hello, {name}!') + + def test_hello_params(): + context = hello_command.make_context('hello', ['--name', 'flask']) + assert context.params['name'] == 'FLASK' + +.. _click: http://click.pocoo.org/ +.. _utilities for testing: http://click.pocoo.org/testing diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 2dd3d7be..179c962b 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -3,6 +3,9 @@ Step 4: Database Connections ---------------------------- +Let's continue building our code in the ``flaskr.py`` file. +(Scroll to the end of the page for more about project layout.) + You currently have a function for establishing a database connection with `connect_db`, but by itself, it is not particularly useful. Creating and closing database connections all the time is very inefficient, so you will diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index fbbcde00..484354ba 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -9,31 +9,37 @@ systems need a schema that tells them how to store that information. Before starting the server for the first time, it's important to create that schema. -Such a schema can be created by piping the ``schema.sql`` file into the -`sqlite3` command as follows:: +Such a schema could be created by piping the ``schema.sql`` file into the +``sqlite3`` command as follows:: sqlite3 /tmp/flaskr.db < schema.sql -The downside of this is that it requires the ``sqlite3`` command to be -installed, which is not necessarily the case on every system. This also -requires that you provide the path to the database, which can introduce -errors. It's a good idea to add a function that initializes the database -for you, to the application. +However, the downside of this is that it requires the ``sqlite3`` command +to be installed, which is not necessarily the case on every system. This +also requires that you provide the path to the database, which can introduce +errors. -To do this, you can create a function and hook it into a :command:`flask` -command that initializes the database. For now just take a look at the -code segment below. A good place to add this function, and command, is -just below the `connect_db` function in :file:`flaskr.py`:: +Instead of the ``sqlite3`` command above, it's a good idea to add a function +to our application that initializes the database for you. To do this, you +can create a function and hook it into a :command:`flask` command that +initializes the database. + +Take a look at the code segment below. A good place to add this function, +and command, is just below the ``connect_db`` function in :file:`flaskr.py`:: def init_db(): db = get_db() + with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) + db.commit() + @app.cli.command('initdb') def initdb_command(): """Initializes the database.""" + init_db() print('Initialized the database.') @@ -59,7 +65,8 @@ On that cursor, there is a method to execute a complete script. Finally, you only have to commit the changes. SQLite3 and other transactional databases will not commit unless you explicitly tell it to. -Now, it is possible to create a database with the :command:`flask` script:: +Now, in a terminal, from the application root directory :file:`flaskr/` it is +possible to create a database with the :command:`flask` script:: flask initdb Initialized the database. diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst index ba62b3b7..23fefaec 100644 --- a/docs/tutorial/folders.rst +++ b/docs/tutorial/folders.rst @@ -3,8 +3,11 @@ Step 0: Creating The Folders ============================ -Before getting started, you will need to create the folders needed for this -application:: +It is recommended to install your Flask application within a virtualenv. Please +read the :ref:`installation` section to set up your environment. + +Now that you have installed Flask, you will need to create the folders required +for this tutorial. Your directory structure will look like this:: /flaskr /flaskr @@ -13,9 +16,10 @@ application:: The application will be installed and run as Python package. This is the recommended way to install and run Flask applications. You will see exactly -how to run ``flaskr`` later on in this tutorial. For now go ahead and create -the applications directory structure. In the next few steps you will be -creating the database schema as well as the main module. +how to run ``flaskr`` later on in this tutorial. + +For now go ahead and create the applications directory structure. In the next +few steps you will be creating the database schema as well as the main module. As a quick side note, the files inside of the :file:`static` folder are available to users of the application via HTTP. This is the place where CSS and diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index f0a583e0..7eee5fa0 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -3,19 +3,19 @@ Tutorial ======== -You want to develop an application with Python and Flask? Here you have -the chance to learn by example. In this tutorial, we will create a simple -microblogging application. It only supports one user that can create -text-only entries and there are no feeds or comments, but it still -features everything you need to get started. We will use Flask and SQLite -as a database (which comes out of the box with Python) so there is nothing -else you need. +Learn by example to develop an application with Python and Flask. + +In this tutorial, we will create a simple blogging application. It only +supports one user, only allows text entries, and has no feeds or comments. + +While very simple, this example still features everything you need to get +started. In addition to Flask, we will use SQLite for the database, which is +built-in to Python, so there is nothing else you need. If you want the full source code in advance or for comparison, check out the `example source`_. -.. _example source: - https://github.com/pallets/flask/tree/master/examples/flaskr/ +.. _example source: https://github.com/pallets/flask/tree/master/examples/flaskr/ .. toctree:: :maxdepth: 2 diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index dd46628b..ed984715 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -22,7 +22,12 @@ connections in a more intelligent way, allowing you to target different relational databases at once and more. You might also want to consider one of the popular NoSQL databases if your data is more suited for those. -Here a screenshot of the final application: +.. warning:: + If you're following the tutorial from a specific version of the docs, be + sure to check out the same tag in the repository, otherwise the tutorial + may be different than the example. + +Here is a screenshot of the final application: .. image:: ../_static/flaskr.png :align: center @@ -31,4 +36,4 @@ Here a screenshot of the final application: Continue with :ref:`tutorial-folders`. -.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org/ diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst index 8db6531e..e08f26fa 100644 --- a/docs/tutorial/packaging.rst +++ b/docs/tutorial/packaging.rst @@ -9,10 +9,10 @@ tutorial you will see exactly how to extend the ``flask`` command line interface (CLI). A useful pattern to manage a Flask application is to install your app -following the `Python Packaging Guide`_. Presently this involves -creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the -projects root directory. You also need to add an :file:`__init__.py` -file to make the :file:`flaskr/flaskr` directory a package. After these +following the `Python Packaging Guide`_. Presently this involves +creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the +projects root directory. You also need to add an :file:`__init__.py` +file to make the :file:`flaskr/flaskr` directory a package. After these changes, your code structure should be:: /flaskr @@ -25,9 +25,7 @@ changes, your code structure should be:: setup.py MANIFEST.in -The content of the ``setup.py`` file for ``flaskr`` is: - -.. sourcecode:: python +Create the ``setup.py`` file for ``flaskr`` with the following content:: from setuptools import setup @@ -43,53 +41,55 @@ The content of the ``setup.py`` file for ``flaskr`` is: When using setuptools, it is also necessary to specify any special files that should be included in your package (in the :file:`MANIFEST.in`). In this case, the static and templates directories need to be included, -as well as the schema. Create the :file:`MANIFEST.in` and add the -following lines:: +as well as the schema. + +Create the :file:`MANIFEST.in` and add the following lines:: graft flaskr/templates graft flaskr/static include flaskr/schema.sql -To simplify locating the application, add the following import statement -into this file, :file:`flaskr/__init__.py`: +Next, to simplify locating the application, create the file, +:file:`flaskr/__init__.py` containing only the following import statement:: -.. sourcecode:: python + from .flaskr import app - from flaskr import app - -This import statement brings the application instance into the top-level -of the application package. When it is time to run the application, the -Flask development server needs the location of the app instance. This -import statement simplifies the location process. Without it the export -statement a few steps below would need to be +This import statement brings the application instance into the top-level +of the application package. When it is time to run the application, the +Flask development server needs the location of the app instance. This +import statement simplifies the location process. Without the above +import statement, the export statement a few steps below would need to be ``export FLASK_APP=flaskr.flaskr``. At this point you should be able to install the application. As usual, it is recommended to install your Flask application within a `virtualenv`_. -With that said, go ahead and install the application with:: +With that said, from the ``flaskr/`` directory, go ahead and install the +application with:: pip install --editable . -The above installation command assumes that it is run within the projects -root directory, `flaskr/`. The `editable` flag allows editing -source code without having to reinstall the Flask app each time you make -changes. The flaskr app is now installed in your virtualenv (see output +The above installation command assumes that it is run within the projects +root directory, ``flaskr/``. The ``editable`` flag allows editing +source code without having to reinstall the Flask app each time you make +changes. The flaskr app is now installed in your virtualenv (see output of ``pip freeze``). With that out of the way, you should be able to start up the application. -Do this with the following commands:: +Do this on Mac or Linux with the following commands in ``flaskr/``:: export FLASK_APP=flaskr - export FLASK_DEBUG=true + export FLASK_ENV=development flask run -(In case you are on Windows you need to use `set` instead of `export`). -The :envvar:`FLASK_DEBUG` flag enables or disables the interactive debugger. +(In case you are on Windows you need to use ``set`` instead of ``export``). +Exporting ``FLASK_ENV=development`` turns on all development features +such as enabling the interactive debugger. + *Never leave debug mode activated in a production system*, because it will allow users to execute code on the server! You will see a message telling you that server has started along with -the address at which you can access it. +the address at which you can access it in a browser. When you head over to the server in your browser, you will get a 404 error because we don't have any views yet. That will be addressed a little later, diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 4bedb54c..5c69ecca 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -3,42 +3,46 @@ Step 2: Application Setup Code ============================== -Now that the schema is in place, you can create the application module, -:file:`flaskr.py`. This file should be placed inside of the -:file:`flaskr/flaskr` folder. The first several lines of code in the -application module are the needed import statements. After that there will be a -few lines of configuration code. For small applications like ``flaskr``, it is -possible to drop the configuration directly into the module. However, a cleaner -solution is to create a separate ``.ini`` or ``.py`` file, load that, and -import the values from there. +Next, we will create the application module, :file:`flaskr.py`. Just like the +:file:`schema.sql` file you created in the previous step, this file should be +placed inside of the :file:`flaskr/flaskr` folder. + +For this tutorial, all the Python code we use will be put into this file +(except for one line in ``__init__.py``, and any testing or optional files you +decide to create). + +The first several lines of code in the application module are the needed import +statements. After that there will be a few lines of configuration code. + +For small applications like ``flaskr``, it is possible to drop the configuration +directly into the module. However, a cleaner solution is to create a separate +``.py`` file, load that, and import the values from there. Here are the import statements (in :file:`flaskr.py`):: - # all the imports import os import sqlite3 - from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash -The next couple lines will create the actual application instance and -initialize it with the config from the same file in :file:`flaskr.py`: + from flask import (Flask, request, session, g, redirect, url_for, abort, + render_template, flash) -.. sourcecode:: python +The next couple lines will create the actual application instance and +initialize it with the config from the same file in :file:`flaskr.py`:: app = Flask(__name__) # create the application instance :) app.config.from_object(__name__) # load config from this file , flaskr.py # Load default config and override config from an environment variable - app.config.update(dict( + app.config.update( DATABASE=os.path.join(app.root_path, 'flaskr.db'), - SECRET_KEY='development key', + SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', USERNAME='admin', PASSWORD='default' - )) + ) app.config.from_envvar('FLASKR_SETTINGS', silent=True) -The :class:`~flask.Config` object works similarly to a dictionary, so it can be -updated with new values. +In the above code, the :class:`~flask.Config` object works similarly to a +dictionary, so it can be updated with new values. .. admonition:: Database Path @@ -58,40 +62,40 @@ updated with new values. Usually, it is a good idea to load a separate, environment-specific configuration file. Flask allows you to import multiple configurations and it will use the setting defined in the last import. This enables robust -configuration setups. :meth:`~flask.Config.from_envvar` can help achieve this. - -.. sourcecode:: python +configuration setups. :meth:`~flask.Config.from_envvar` can help achieve +this. :: app.config.from_envvar('FLASKR_SETTINGS', silent=True) -Simply define the environment variable :envvar:`FLASKR_SETTINGS` that points to -a config file to be loaded. The silent switch just tells Flask to not complain -if no such environment key is set. +If you want to do this (not required for this tutorial) simply define the +environment variable :envvar:`FLASKR_SETTINGS` that points to a config file +to be loaded. The silent switch just tells Flask to not complain if no such +environment key is set. In addition to that, you can use the :meth:`~flask.Config.from_object` method on the config object and provide it with an import name of a module. Flask will then initialize the variable from that module. Note that in all cases, only variable names that are uppercase are considered. -The ``SECRET_KEY`` is needed to keep the client-side sessions secure. +The :data:`SECRET_KEY` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. -Lastly, you will add a method that allows for easy connections to the -specified database. This can be used to open a connection on request and -also from the interactive Python shell or a script. This will come in -handy later. You can create a simple database connection through SQLite and -then tell it to use the :class:`sqlite3.Row` object to represent rows. -This allows the rows to be treated as if they were dictionaries instead of -tuples. - -.. sourcecode:: python +Lastly, add a method that allows for easy connections to the specified +database. :: def connect_db(): """Connects to the specific database.""" + rv = sqlite3.connect(app.config['DATABASE']) rv.row_factory = sqlite3.Row return rv +This can be used to open a connection on request and also from the +interactive Python shell or a script. This will come in handy later. +You can create a simple database connection through SQLite and then tell +it to use the :class:`sqlite3.Row` object to represent rows. This allows +the rows to be treated as if they were dictionaries instead of tuples. + In the next section you will see how to run the application. Continue with :ref:`tutorial-packaging`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index 269e8df1..12a555e7 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -15,7 +15,8 @@ escaped with their XML equivalents. We are also using template inheritance which makes it possible to reuse the layout of the website in all pages. -Put the following templates into the :file:`templates` folder: +Create the follwing three HTML files and place them in the +:file:`templates` folder: .. _Jinja2: http://jinja.pocoo.org/docs/templates @@ -59,7 +60,7 @@ show_entries.html This template extends the :file:`layout.html` template from above to display the messages. Note that the ``for`` loop iterates over the messages we passed in with the :func:`~flask.render_template` function. Notice that the form is -configured to to submit to the `add_entry` view function and use ``POST`` as +configured to submit to the `add_entry` view function and use ``POST`` as HTTP method: .. sourcecode:: html+jinja @@ -79,9 +80,9 @@ HTTP method: {% endif %}
    {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }} +
  • {{ entry.title }}

    {{ entry.text|safe }}
  • {% else %} -
  • Unbelievable. No entries here so far +
  • Unbelievable. No entries here so far
  • {% endfor %}
{% endblock %} diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index dcf36594..26099375 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -46,7 +46,7 @@ At this point you can run the tests. Here ``pytest`` will be used. Run and watch the tests pass, within the top-level :file:`flaskr/` directory as:: - py.test + pytest Testing + setuptools -------------------- diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 4364d973..1b09fcb8 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -4,7 +4,8 @@ Step 6: The View Functions ========================== Now that the database connections are working, you can start writing the -view functions. You will need four of them: +view functions. You will need four of them; Show Entries, Add New Entry, +Login and Logout. Add the following code snipets to :file:`flaskr.py`. Show Entries ------------ diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 41b70f03..af2383c0 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -49,7 +49,7 @@ Any of the following is functionally equivalent:: response = send_file(open(fname), attachment_filename=fname) response.set_etag(...) -The reason for this is that some file-like objects have a invalid or even +The reason for this is that some file-like objects have an invalid or even misleading ``name`` attribute. Silently swallowing errors in such cases was not a satisfying solution. @@ -143,7 +143,7 @@ when there is no request context yet but an application context. The old ``flask.Flask.request_globals_class`` attribute was renamed to :attr:`flask.Flask.app_ctx_globals_class`. -.. _Flask-OldSessions: http://pythonhosted.org/Flask-OldSessions/ +.. _Flask-OldSessions: https://pythonhosted.org/Flask-OldSessions/ Version 0.9 ----------- @@ -198,7 +198,7 @@ applications with Flask. Because we want to make upgrading as easy as possible we tried to counter the problems arising from these changes by providing a script that can ease the transition. -The script scans your whole application and generates an unified diff with +The script scans your whole application and generates a unified diff with changes it assumes are safe to apply. However as this is an automated tool it won't be able to find all use cases and it might miss some. We internally spread a lot of deprecation warnings all over the place to make diff --git a/examples/flaskr/README b/examples/flaskr/README index 90860ff2..ab668d67 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -9,17 +9,19 @@ ~ How do I use it? - 1. edit the configuration in the flaskr.py file or - export an FLASKR_SETTINGS environment variable - pointing to a configuration file. + 1. edit the configuration in the factory.py file or + export a FLASKR_SETTINGS environment variable + pointing to a configuration file or pass in a + dictionary with config values using the create_app + function. 2. install the app from the root of the project directory pip install --editable . - 3. Instruct flask to use the right application + 3. instruct flask to use the right application - export FLASK_APP=flaskr + export FLASK_APP="flaskr.factory:create_app()" 4. initialize the database with this command: diff --git a/examples/flaskr/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py index 37a7b5cc..e69de29b 100644 --- a/examples/flaskr/flaskr/__init__.py +++ b/examples/flaskr/flaskr/__init__.py @@ -1 +0,0 @@ -from flaskr.flaskr import app \ No newline at end of file diff --git a/examples/flaskr/flaskr/blueprints/__init__.py b/examples/flaskr/flaskr/blueprints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/flaskr/flaskr/flaskr.py b/examples/flaskr/flaskr/blueprints/flaskr.py similarity index 55% rename from examples/flaskr/flaskr/flaskr.py rename to examples/flaskr/flaskr/blueprints/flaskr.py index b4c1d6bd..7b64dd9e 100644 --- a/examples/flaskr/flaskr/flaskr.py +++ b/examples/flaskr/flaskr/blueprints/flaskr.py @@ -10,29 +10,18 @@ :license: BSD, see LICENSE for more details. """ -import os from sqlite3 import dbapi2 as sqlite3 -from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash +from flask import Blueprint, request, session, g, redirect, url_for, abort, \ + render_template, flash, current_app -# create our little application :) -app = Flask(__name__) - -# Load default config and override config from an environment variable -app.config.update(dict( - DATABASE=os.path.join(app.root_path, 'flaskr.db'), - DEBUG=True, - SECRET_KEY='development key', - USERNAME='admin', - PASSWORD='default' -)) -app.config.from_envvar('FLASKR_SETTINGS', silent=True) +# create our blueprint :) +bp = Blueprint('flaskr', __name__) def connect_db(): """Connects to the specific database.""" - rv = sqlite3.connect(app.config['DATABASE']) + rv = sqlite3.connect(current_app.config['DATABASE']) rv.row_factory = sqlite3.Row return rv @@ -40,18 +29,11 @@ def connect_db(): def init_db(): """Initializes the database.""" db = get_db() - with app.open_resource('schema.sql', mode='r') as f: + with current_app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() -@app.cli.command('initdb') -def initdb_command(): - """Creates the database tables.""" - init_db() - print('Initialized the database.') - - def get_db(): """Opens a new database connection if there is none yet for the current application context. @@ -61,14 +43,7 @@ def get_db(): return g.sqlite_db -@app.teardown_appcontext -def close_db(error): - """Closes the database again at the end of the request.""" - if hasattr(g, 'sqlite_db'): - g.sqlite_db.close() - - -@app.route('/') +@bp.route('/') def show_entries(): db = get_db() cur = db.execute('select title, text from entries order by id desc') @@ -76,7 +51,7 @@ def show_entries(): return render_template('show_entries.html', entries=entries) -@app.route('/add', methods=['POST']) +@bp.route('/add', methods=['POST']) def add_entry(): if not session.get('logged_in'): abort(401) @@ -85,26 +60,26 @@ def add_entry(): [request.form['title'], request.form['text']]) db.commit() flash('New entry was successfully posted') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) -@app.route('/login', methods=['GET', 'POST']) +@bp.route('/login', methods=['GET', 'POST']) def login(): error = None if request.method == 'POST': - if request.form['username'] != app.config['USERNAME']: + if request.form['username'] != current_app.config['USERNAME']: error = 'Invalid username' - elif request.form['password'] != app.config['PASSWORD']: + elif request.form['password'] != current_app.config['PASSWORD']: error = 'Invalid password' else: session['logged_in'] = True flash('You were logged in') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) return render_template('login.html', error=error) -@app.route('/logout') +@bp.route('/logout') def logout(): session.pop('logged_in', None) flash('You were logged out') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) diff --git a/examples/flaskr/flaskr/factory.py b/examples/flaskr/flaskr/factory.py new file mode 100644 index 00000000..7541ec3c --- /dev/null +++ b/examples/flaskr/flaskr/factory.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" + Flaskr + ~~~~~~ + + A microblog example application written as Flask tutorial with + Flask and sqlite3. + + :copyright: (c) 2015 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +import os +from flask import Flask, g +from werkzeug.utils import find_modules, import_string +from flaskr.blueprints.flaskr import init_db + + +def create_app(config=None): + app = Flask('flaskr') + + app.config.update(dict( + DATABASE=os.path.join(app.root_path, 'flaskr.db'), + DEBUG=True, + SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', + USERNAME='admin', + PASSWORD='default' + )) + app.config.update(config or {}) + app.config.from_envvar('FLASKR_SETTINGS', silent=True) + + register_blueprints(app) + register_cli(app) + register_teardowns(app) + + return app + + +def register_blueprints(app): + """Register all blueprint modules + + Reference: Armin Ronacher, "Flask for Fun and for Profit" PyBay 2016. + """ + for name in find_modules('flaskr.blueprints'): + mod = import_string(name) + if hasattr(mod, 'bp'): + app.register_blueprint(mod.bp) + return None + + +def register_cli(app): + @app.cli.command('initdb') + def initdb_command(): + """Creates the database tables.""" + init_db() + print('Initialized the database.') + + +def register_teardowns(app): + @app.teardown_appcontext + def close_db(error): + """Closes the database again at the end of the request.""" + if hasattr(g, 'sqlite_db'): + g.sqlite_db.close() diff --git a/examples/flaskr/flaskr/templates/layout.html b/examples/flaskr/flaskr/templates/layout.html index 737b51b2..862a9f4a 100644 --- a/examples/flaskr/flaskr/templates/layout.html +++ b/examples/flaskr/flaskr/templates/layout.html @@ -5,9 +5,9 @@

Flaskr

{% if not session.logged_in %} - log in + log in {% else %} - log out + log out {% endif %}
{% for message in get_flashed_messages() %} diff --git a/examples/flaskr/flaskr/templates/login.html b/examples/flaskr/flaskr/templates/login.html index ed09aeba..505d2f66 100644 --- a/examples/flaskr/flaskr/templates/login.html +++ b/examples/flaskr/flaskr/templates/login.html @@ -2,7 +2,7 @@ {% block body %}

Login

{% if error %}

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

+
Username:
diff --git a/examples/flaskr/flaskr/templates/show_entries.html b/examples/flaskr/flaskr/templates/show_entries.html index 2f68b9d3..cf8fbb86 100644 --- a/examples/flaskr/flaskr/templates/show_entries.html +++ b/examples/flaskr/flaskr/templates/show_entries.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %} {% if session.logged_in %} - +
Title:
diff --git a/examples/flaskr/setup.cfg b/examples/flaskr/setup.cfg index b7e47898..9af7e6f1 100644 --- a/examples/flaskr/setup.cfg +++ b/examples/flaskr/setup.cfg @@ -1,2 +1,2 @@ [aliases] -test=pytest +test=pytest \ No newline at end of file diff --git a/examples/flaskr/setup.py b/examples/flaskr/setup.py index 910f23ac..7f1dae53 100644 --- a/examples/flaskr/setup.py +++ b/examples/flaskr/setup.py @@ -1,8 +1,19 @@ -from setuptools import setup +# -*- coding: utf-8 -*- +""" + Flaskr Tests + ~~~~~~~~~~~~ + + Tests the Flaskr application. + + :copyright: (c) 2015 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from setuptools import setup, find_packages setup( name='flaskr', - packages=['flaskr'], + packages=find_packages(), include_package_data=True, install_requires=[ 'flask', diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py index 663e92e0..e1e9a4e7 100644 --- a/examples/flaskr/tests/test_flaskr.py +++ b/examples/flaskr/tests/test_flaskr.py @@ -12,23 +12,30 @@ import os import tempfile import pytest -from flaskr import flaskr +from flaskr.factory import create_app +from flaskr.blueprints.flaskr import init_db @pytest.fixture -def client(request): - db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True - client = flaskr.app.test_client() - with flaskr.app.app_context(): - flaskr.init_db() +def app(): + db_fd, db_path = tempfile.mkstemp() + config = { + 'DATABASE': db_path, + 'TESTING': True, + } + app = create_app(config=config) - def teardown(): - os.close(db_fd) - os.unlink(flaskr.app.config['DATABASE']) - request.addfinalizer(teardown) + with app.app_context(): + init_db() + yield app - return client + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture +def client(app): + return app.test_client() def login(client, username, password): @@ -48,25 +55,25 @@ def test_empty_db(client): assert b'No entries here so far' in rv.data -def test_login_logout(client): +def test_login_logout(client, app): """Make sure login and logout works""" - rv = login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + rv = login(client, app.config['USERNAME'], + app.config['PASSWORD']) assert b'You were logged in' in rv.data rv = logout(client) assert b'You were logged out' in rv.data - rv = login(client, flaskr.app.config['USERNAME'] + 'x', - flaskr.app.config['PASSWORD']) + rv = login(client,app.config['USERNAME'] + 'x', + app.config['PASSWORD']) assert b'Invalid username' in rv.data - rv = login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD'] + 'x') + rv = login(client, app.config['USERNAME'], + app.config['PASSWORD'] + 'x') assert b'Invalid password' in rv.data -def test_messages(client): +def test_messages(client, app): """Test that messages work""" - login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + login(client, app.config['USERNAME'], + app.config['PASSWORD']) rv = client.post('/add', data=dict( title='', text='HTML allowed here' diff --git a/examples/minitwit/README b/examples/minitwit/README index 4561d836..b9bc5ea2 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -14,15 +14,19 @@ export an MINITWIT_SETTINGS environment variable pointing to a configuration file. - 2. tell flask about the right application: + 2. install the app from the root of the project directory + + pip install --editable . + + 3. tell flask about the right application: export FLASK_APP=minitwit - 2. fire up a shell and run this: + 4. fire up a shell and run this: flask initdb - 3. now you can run minitwit: + 5. now you can run minitwit: flask run diff --git a/examples/minitwit/minitwit/__init__.py b/examples/minitwit/minitwit/__init__.py index 0b8bd697..96c81aec 100644 --- a/examples/minitwit/minitwit/__init__.py +++ b/examples/minitwit/minitwit/__init__.py @@ -1 +1 @@ -from minitwit import app \ No newline at end of file +from .minitwit import app diff --git a/examples/minitwit/minitwit/minitwit.py b/examples/minitwit/minitwit/minitwit.py index bbc3b483..50693dd8 100644 --- a/examples/minitwit/minitwit/minitwit.py +++ b/examples/minitwit/minitwit/minitwit.py @@ -22,10 +22,10 @@ from werkzeug import check_password_hash, generate_password_hash DATABASE = '/tmp/minitwit.db' PER_PAGE = 30 DEBUG = True -SECRET_KEY = 'development key' +SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' # create our little application :) -app = Flask(__name__) +app = Flask('minitwit') app.config.from_object(__name__) app.config.from_envvar('MINITWIT_SETTINGS', silent=True) @@ -85,7 +85,7 @@ def format_datetime(timestamp): def gravatar_url(email, size=80): """Return the gravatar image for the given email address.""" - return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ + return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) diff --git a/examples/minitwit/tests/test_minitwit.py b/examples/minitwit/tests/test_minitwit.py index 50ca26d9..c8992e57 100644 --- a/examples/minitwit/tests/test_minitwit.py +++ b/examples/minitwit/tests/test_minitwit.py @@ -15,18 +15,16 @@ from minitwit import minitwit @pytest.fixture -def client(request): +def client(): db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() client = minitwit.app.test_client() with minitwit.app.app_context(): minitwit.init_db() - def teardown(): - """Get rid of the database again after each test.""" - os.close(db_fd) - os.unlink(minitwit.app.config['DATABASE']) - request.addfinalizer(teardown) - return client + yield client + + os.close(db_fd) + os.unlink(minitwit.app.config['DATABASE']) def register(client, username, password, password2=None, email=None): diff --git a/examples/patterns/largerapp/setup.py b/examples/patterns/largerapp/setup.py new file mode 100644 index 00000000..eaf00f07 --- /dev/null +++ b/examples/patterns/largerapp/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup( + name='yourapplication', + packages=['yourapplication'], + include_package_data=True, + install_requires=[ + 'flask', + ], +) diff --git a/examples/patterns/largerapp/tests/test_largerapp.py b/examples/patterns/largerapp/tests/test_largerapp.py new file mode 100644 index 00000000..6bc0531e --- /dev/null +++ b/examples/patterns/largerapp/tests/test_largerapp.py @@ -0,0 +1,12 @@ +from yourapplication import app +import pytest + +@pytest.fixture +def client(): + app.config['TESTING'] = True + client = app.test_client() + return client + +def test_index(client): + rv = client.get('/') + assert b"Hello World!" in rv.data \ No newline at end of file diff --git a/examples/patterns/largerapp/yourapplication/__init__.py b/examples/patterns/largerapp/yourapplication/__init__.py new file mode 100644 index 00000000..09407711 --- /dev/null +++ b/examples/patterns/largerapp/yourapplication/__init__.py @@ -0,0 +1,4 @@ +from flask import Flask +app = Flask('yourapplication') + +import yourapplication.views diff --git a/examples/patterns/largerapp/yourapplication/static/style.css b/examples/patterns/largerapp/yourapplication/static/style.css new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/index.html b/examples/patterns/largerapp/yourapplication/templates/index.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/layout.html b/examples/patterns/largerapp/yourapplication/templates/layout.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/login.html b/examples/patterns/largerapp/yourapplication/templates/login.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/views.py b/examples/patterns/largerapp/yourapplication/views.py new file mode 100644 index 00000000..b112328e --- /dev/null +++ b/examples/patterns/largerapp/yourapplication/views.py @@ -0,0 +1,5 @@ +from yourapplication import app + +@app.route('/') +def index(): + return 'Hello World!' \ No newline at end of file diff --git a/flask/__init__.py b/flask/__init__.py index 509b944f..bb6c4c18 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.11.2-dev' +__version__ = '0.13-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. @@ -40,7 +40,7 @@ from .signals import signals_available, template_rendered, request_started, \ # it. from . import json -# This was the only thing that flask used to export at one point and it had +# This was the only thing that Flask used to export at one point and it had # a more generic name. jsonify = json.jsonify diff --git a/flask/_compat.py b/flask/_compat.py index 071628fc..173b3689 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -25,6 +25,7 @@ if not PY2: itervalues = lambda d: iter(d.values()) iteritems = lambda d: iter(d.items()) + from inspect import getfullargspec as getargspec from io import StringIO def reraise(tp, value, tb=None): @@ -43,6 +44,7 @@ else: itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() + from inspect import getargspec from cStringIO import StringIO exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') diff --git a/flask/app.py b/flask/app.py index 59c77a15..200b5c20 100644 --- a/flask/app.py +++ b/flask/app.py @@ -10,34 +10,32 @@ """ import os import sys -from threading import Lock +import warnings from datetime import timedelta -from itertools import chain from functools import update_wrapper -from collections import deque - -from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule, RequestRedirect, BuildError -from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed, BadRequest, default_exceptions +from itertools import chain +from threading import Lock -from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - locked_cached_property, _endpoint_from_view_func, find_package, \ - get_debug_flag -from . import json, cli -from .wrappers import Request, Response -from .config import ConfigAttribute, Config -from .ctx import RequestContext, AppContext, _AppCtxGlobals -from .globals import _request_ctx_stack, request, session, g +from werkzeug.datastructures import Headers, ImmutableDict +from werkzeug.exceptions import BadRequest, BadRequestKeyError, HTTPException, \ + InternalServerError, MethodNotAllowed, default_exceptions +from werkzeug.routing import BuildError, Map, RequestRedirect, Rule + +from . import cli, json +from ._compat import integer_types, reraise, string_types, text_type +from .config import Config, ConfigAttribute +from .ctx import AppContext, RequestContext, _AppCtxGlobals +from .globals import _request_ctx_stack, g, request, session +from .helpers import _PackageBoundObject, \ + _endpoint_from_view_func, find_package, get_env, get_debug_flag, \ + get_flashed_messages, locked_cached_property, url_for +from .logging import create_logger from .sessions import SecureCookieSessionInterface +from .signals import appcontext_tearing_down, got_request_exception, \ + request_finished, request_started, request_tearing_down from .templating import DispatchingJinjaLoader, Environment, \ - _default_template_ctx_processor -from .signals import request_started, request_finished, got_request_exception, \ - request_tearing_down, appcontext_tearing_down -from ._compat import reraise, string_types, text_type, integer_types - -# a lock used for logger initialization -_logger_lock = Lock() + _default_template_ctx_processor +from .wrappers import Request, Response # a singleton sentinel value for parameter defaults _sentinel = object() @@ -124,6 +122,9 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.11 The `root_path` parameter was added. + .. versionadded:: 0.13 + The `host_matching` and `static_host` parameters were added. + :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 @@ -131,6 +132,11 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. + :param host_matching: sets the app's ``url_map.host_matching`` to the given + value. Defaults to False. + :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 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 @@ -179,18 +185,6 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.10 app_ctx_globals_class = _AppCtxGlobals - # Backwards compatibility support - def _get_request_globals_class(self): - return self.app_ctx_globals_class - def _set_request_globals_class(self, value): - from warnings import warn - warn(DeprecationWarning('request_globals_class attribute is now ' - 'called app_ctx_globals_class')) - self.app_ctx_globals_class = value - request_globals_class = property(_get_request_globals_class, - _set_request_globals_class) - del _get_request_globals_class, _set_request_globals_class - #: The class that is used for the ``config`` attribute of this app. #: Defaults to :class:`~flask.Config`. #: @@ -202,18 +196,9 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.11 config_class = Config - #: The debug flag. Set this to ``True`` to enable debugging of the - #: application. In debug mode the debugger will kick in when an unhandled - #: exception occurs and the integrated server will automatically reload - #: the application if changes in the code are detected. - #: - #: This attribute can also be configured from the config with the ``DEBUG`` - #: configuration key. Defaults to ``False``. - debug = ConfigAttribute('DEBUG') - #: 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 unittest helpers that have an + #: 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 @@ -224,11 +209,11 @@ class Flask(_PackageBoundObject): testing = ConfigAttribute('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 + #: 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 - #: ``SECRET_KEY`` configuration key. Defaults to ``None``. + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. secret_key = ConfigAttribute('SECRET_KEY') #: The secure cookie uses this for the name of the session cookie. @@ -267,12 +252,6 @@ class Flask(_PackageBoundObject): #: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``. use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') - #: The name of the logger to use. By default the logger name is the - #: package name passed to the constructor. - #: - #: .. versionadded:: 0.4 - logger_name = ConfigAttribute('LOGGER_NAME') - #: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`. #: #: .. versionadded:: 0.10 @@ -290,32 +269,32 @@ class Flask(_PackageBoundObject): #: Default configuration parameters. default_config = ImmutableDict({ - 'DEBUG': get_debug_flag(default=False), + 'ENV': None, + 'DEBUG': None, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, - 'LOGGER_NAME': None, - 'LOGGER_HANDLER_POLICY': 'always', 'SERVER_NAME': None, - 'APPLICATION_ROOT': 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': timedelta(hours=12), - 'TRAP_BAD_REQUEST_ERRORS': False, + 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, - 'JSONIFY_PRETTYPRINT_REGULAR': True, + 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, }) @@ -337,28 +316,53 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.8 session_interface = SecureCookieSessionInterface() - def __init__(self, import_name, static_path=None, static_url_path=None, - static_folder='static', template_folder='templates', - instance_path=None, instance_relative_config=False, - root_path=None): - _PackageBoundObject.__init__(self, import_name, - template_folder=template_folder, - root_path=root_path) - if static_path is not None: - from warnings import warn - warn(DeprecationWarning('static_path is now called ' - 'static_url_path'), stacklevel=2) - static_url_path = static_path + # TODO remove the next three attrs when Sphinx :inherited-members: works + # https://github.com/sphinx-doc/sphinx/issues/741 + + #: The name of the package or module that this app belongs to. Do not + #: change this once it is set by the constructor. + import_name = None + + #: Location of the template files to be added to the template lookup. + #: ``None`` if templates should not be added. + template_folder = None + + #: Absolute path to the package on the filesystem. Used to look up + #: resources contained in the package. + root_path = None + + def __init__( + self, + import_name, + static_url_path=None, + static_folder='static', + static_host=None, + host_matching=False, + template_folder='templates', + instance_path=None, + instance_relative_config=False, + root_path=None + ): + _PackageBoundObject.__init__( + self, + import_name, + template_folder=template_folder, + root_path=root_path + ) if static_url_path is not None: self.static_url_path = static_url_path + if static_folder is not None: self.static_folder = static_folder + 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.') + 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. #: @@ -370,20 +374,12 @@ class Flask(_PackageBoundObject): #: to load a config from files. self.config = self.make_config(instance_relative_config) - # Prepare the deferred setup of the logger. - self._logger = None - self.logger_name = self.import_name - #: A dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. #: To register a view function, use the :meth:`route` decorator. self.view_functions = {} - # support for the now deprecated `error_handlers` attribute. The - # :attr:`error_handler_spec` shall be used now. - self._error_handlers = {} - #: A dictionary of all registered error handlers. The key is ``None`` #: for error handlers active on the application, otherwise the key is #: the name of the blueprint. Each key points to another dictionary @@ -392,9 +388,9 @@ class Flask(_PackageBoundObject): #: is the class for the instance check and the second the error handler #: function. #: - #: To register a error handler, use the :meth:`errorhandler` + #: To register an error handler, use the :meth:`errorhandler` #: decorator. - self.error_handler_spec = {None: self._error_handlers} + self.error_handler_spec = {} #: A list of functions that are called when :meth:`url_for` raises a #: :exc:`~werkzeug.routing.BuildError`. Each function registered here @@ -405,17 +401,16 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.9 self.url_build_error_handlers = [] - #: A dictionary with lists of functions that should be called at the - #: beginning of the request. The key of the dictionary is the name of - #: the blueprint this function is active for, ``None`` for all requests. - #: This can for example be used to open database connections or - #: getting hold of the currently logged in user. To register a - #: function here, use the :meth:`before_request` decorator. + #: A dictionary with lists of functions that will be called at the + #: beginning of each request. The key of the dictionary is the name of + #: the blueprint this function is active for, or ``None`` for all + #: requests. To register a function, use the :meth:`before_request` + #: decorator. self.before_request_funcs = {} - #: A lists of functions that should be called at the beginning of the - #: first request to this instance. To register a function here, use - #: the :meth:`before_first_request` decorator. + #: A list of functions that will be called at the beginning of the + #: first request to this instance. To register a function, use the + #: :meth:`before_first_request` decorator. #: #: .. versionadded:: 0.8 self.before_first_request_funcs = [] @@ -447,12 +442,11 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.9 self.teardown_appcontext_funcs = [] - #: A dictionary with lists of functions that can be used as URL - #: value processor functions. Whenever a URL is built these functions - #: are called to modify the dictionary of values in place. The key - #: ``None`` here is used for application wide - #: callbacks, otherwise the key is the name of the blueprint. - #: Each of these functions has the chance to modify the dictionary + #: A dictionary with lists of functions that are called before the + #: :attr:`before_request_funcs` functions. The key of the dictionary is + #: the name of the blueprint this function is active for, or ``None`` + #: for all requests. To register a function, use + #: :meth:`url_value_preprocessor`. #: #: .. versionadded:: 0.7 self.url_value_preprocessors = {} @@ -526,20 +520,26 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() + self.url_map.host_matching = host_matching + # tracks internally if the application already handled at least one # request. self._got_first_request = False self._before_request_lock = Lock() - # register the static folder for the application. Do that even - # if the folder does not exist. First of all it might be created - # while the server is running (usually happens during development) - # but also because google appengine stores static files somewhere - # else when mapped with the .yml file. + # 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: - self.add_url_rule(self.static_url_path + '/', - endpoint='static', - view_func=self.send_static_file) + assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination' + self.add_url_rule( + self.static_url_path + '/', + endpoint='static', + host=static_host, + view_func=self.send_static_file + ) #: The click command line context for this application. Commands #: registered here show up in the :command:`flask` command once the @@ -549,17 +549,6 @@ class Flask(_PackageBoundObject): #: This is an instance of a :class:`click.Group` object. self.cli = cli.AppGroup(self.name) - def _get_error_handlers(self): - from warnings import warn - warn(DeprecationWarning('error_handlers is deprecated, use the ' - 'new error_handler_spec attribute instead.'), stacklevel=1) - return self._error_handlers - def _set_error_handlers(self, value): - self._error_handlers = value - self.error_handler_spec[None] = value - error_handlers = property(_get_error_handlers, _set_error_handlers) - del _get_error_handlers, _set_error_handlers - @locked_cached_property def name(self): """The name of the application. This is usually the import name @@ -602,27 +591,28 @@ class Flask(_PackageBoundObject): return rv return self.debug - @property + @locked_cached_property def logger(self): - """A :class:`logging.Logger` object for this application. The - default configuration is to log to stderr if the application is - in debug mode. This logger can be used to (surprise) log messages. - Here some examples:: + """The ``'flask.app'`` logger, a standard Python + :class:`~logging.Logger`. + + 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 :ref:`logging` for more information. - app.logger.debug('A value for debugging') - app.logger.warning('A warning occurred (%d apples)', 42) - app.logger.error('An error occurred') + .. versionchanged:: 1.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 """ - if self._logger and self._logger.name == self.logger_name: - return self._logger - with _logger_lock: - if self._logger and self._logger.name == self.logger_name: - return self._logger - from flask.logging import create_logger - self._logger = rv = create_logger(self) - return rv + return create_logger(self) @locked_cached_property def jinja_env(self): @@ -650,7 +640,10 @@ class Flask(_PackageBoundObject): root_path = self.root_path if instance_relative: root_path = self.instance_path - return self.config_class(root_path, self.default_config) + defaults = dict(self.default_config) + defaults['ENV'] = get_env() + defaults['DEBUG'] = get_debug_flag() + return self.config_class(root_path, defaults) def auto_find_instance_path(self): """Tries to locate the instance path if it was not provided to the @@ -677,6 +670,28 @@ class Flask(_PackageBoundObject): """ return open(os.path.join(self.instance_path, resource), mode) + def _get_templates_auto_reload(self): + """Reload templates when they are changed. Used by + :meth:`create_jinja_environment`. + + This attribute can be configured with :data:`TEMPLATES_AUTO_RELOAD`. If + not set, it will be enabled in debug mode. + + .. versionadded:: 1.0 + This property was added but the underlying config and behavior + already existed. + """ + rv = self.config['TEMPLATES_AUTO_RELOAD'] + return rv if rv is not None else self.debug + + def _set_templates_auto_reload(self, value): + self.config['TEMPLATES_AUTO_RELOAD'] = value + + templates_auto_reload = property( + _get_templates_auto_reload, _set_templates_auto_reload + ) + del _get_templates_auto_reload, _set_templates_auto_reload + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` and :meth:`select_jinja_autoescape`. Since 0.7 this also adds @@ -689,13 +704,13 @@ class Flask(_PackageBoundObject): ``TEMPLATES_AUTO_RELOAD`` configuration option. """ options = dict(self.jinja_options) + if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape + if 'auto_reload' not in options: - if self.config['TEMPLATES_AUTO_RELOAD'] is not None: - options['auto_reload'] = self.config['TEMPLATES_AUTO_RELOAD'] - else: - options['auto_reload'] = self.debug + options['auto_reload'] = self.templates_auto_reload + rv = self.jinja_environment(self, **options) rv.globals.update( url_for=url_for, @@ -724,15 +739,6 @@ class Flask(_PackageBoundObject): """ return DispatchingJinjaLoader(self) - def init_jinja_globals(self): - """Deprecated. Used to initialize the Jinja2 globals. - - .. versionadded:: 0.5 - .. versionchanged:: 0.7 - This method is deprecated with 0.7. Override - :meth:`create_jinja_environment` instead. - """ - def select_jinja_autoescape(self, filename): """Returns ``True`` if autoescaping should be active for the given template name. If no template name is given, returns `True`. @@ -780,7 +786,41 @@ class Flask(_PackageBoundObject): rv.update(processor()) return rv - def run(self, host=None, port=None, debug=None, **options): + #: What environment the app is running in. Flask and extensions may + #: enable behaviors based on the environment, such as enabling debug + #: mode. This maps to the :data:`ENV` config key. This is set by the + #: :envvar:`FLASK_ENV` environment variable and may not behave as + #: expected if set in code. + #: + #: **Do not enable development when deploying in production.** + #: + #: Default: ``'production'`` + env = ConfigAttribute('ENV') + + def _get_debug(self): + return self.config['DEBUG'] + + def _set_debug(self, value): + self.config['DEBUG'] = value + self.jinja_env.auto_reload = self.templates_auto_reload + + #: 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. This is + #: enabled when :attr:`env` is ``'development'`` and is overridden + #: by 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: ``True`` if :attr:`env` is ``'development'``, or + #: ``False`` otherwise. + debug = property(_get_debug, _set_debug) + del _get_debug, _set_debug + + def run(self, host=None, port=None, debug=None, + load_dotenv=True, **options): """Runs the application on a local development server. Do not use ``run()`` in a production setting. It is not intended to @@ -809,35 +849,75 @@ class Flask(_PackageBoundObject): won't catch any exceptions because there won't be any to catch. - .. versionchanged:: 0.10 - The default port is now picked from the ``SERVER_NAME`` variable. - :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'``. + 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 options: the options to be forwarded to the underlying - Werkzeug server. See - :func:`werkzeug.serving.run_simple` for more - information. - """ - from werkzeug.serving import run_simple - if host is None: - host = '127.0.0.1' - if port is None: - server_name = self.config['SERVER_NAME'] - if server_name and ':' in server_name: - port = int(server_name.rsplit(':', 1)[1]) - else: - port = 5000 + 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. + + If set, the :envvar:`FLASK_ENV` and :envvar:`FLASK_DEBUG` + environment variables will override :attr:`env` and + :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Change this into a no-op if the server is invoked from the + # command line. Have a look at cli.py for more information. + if os.environ.get('FLASK_RUN_FROM_CLI') == 'true': + from .debughelpers import explain_ignored_app_run + explain_ignored_app_run() + return + + if load_dotenv: + cli.load_dotenv() + + # if set, let env vars override previous values + if 'FLASK_ENV' in os.environ: + self.env = get_env() + self.debug = get_debug_flag() + elif '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) + + _host = '127.0.0.1' + _port = 5000 + server_name = self.config.get('SERVER_NAME') + sn_host, sn_port = None, None + + if server_name: + sn_host, _, sn_port = server_name.partition(':') + + host = host or sn_host or _host + port = int(port or sn_port or _port) + options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) + options.setdefault('threaded', True) + + cli.show_server_banner(self.env, self.debug, self.name) + + from werkzeug.serving import run_simple + try: run_simple(host, port, self, **options) finally: @@ -908,8 +988,17 @@ class Flask(_PackageBoundObject): :attr:`secret_key` is set. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.open_session`` + instead. + :param request: an instance of :attr:`request_class`. """ + + warnings.warn(DeprecationWarning( + '"open_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.open_session" instead.' + )) return self.session_interface.open_session(self, request) def save_session(self, session, response): @@ -917,38 +1006,74 @@ class Flask(_PackageBoundObject): implementation, check :meth:`open_session`. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.save_session`` + instead. + :param session: the session to be saved (a :class:`~werkzeug.contrib.securecookie.SecureCookie` object) :param response: an instance of :attr:`response_class` """ + + warnings.warn(DeprecationWarning( + '"save_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.save_session" instead.' + )) return self.session_interface.save_session(self, session, response) def make_null_session(self): """Creates a new instance of a missing session. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.make_null_session`` + instead. + .. versionadded:: 0.7 """ + + warnings.warn(DeprecationWarning( + '"make_null_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.make_null_session" instead.' + )) return self.session_interface.make_null_session(self) @setupmethod def register_blueprint(self, blueprint, **options): - """Registers a blueprint on the application. + """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. .. versionadded:: 0.7 """ first_registration = False + if blueprint.name in self.blueprints: - assert self.blueprints[blueprint.name] is blueprint, \ - 'A blueprint\'s name collision occurred between %r and ' \ - '%r. Both share the same name "%s". Blueprints that ' \ - 'are created on the fly need unique names.' % \ - (blueprint, self.blueprints[blueprint.name], blueprint.name) + assert self.blueprints[blueprint.name] is blueprint, ( + 'A name collision occurred between blueprints %r and %r. Both' + ' share the same name "%s". Blueprints that are created on the' + ' fly need unique names.' % ( + blueprint, self.blueprints[blueprint.name], blueprint.name + ) + ) else: self.blueprints[blueprint.name] = blueprint self._blueprint_order.append(blueprint) first_registration = True + blueprint.register(self, options, first_registration) def iter_blueprints(self): @@ -959,7 +1084,8 @@ class Flask(_PackageBoundObject): return iter(self._blueprint_order) @setupmethod - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, + provide_automatic_options=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. @@ -999,6 +1125,10 @@ class Flask(_PackageBoundObject): endpoint :param view_func: the function to call when serving a request to the provided endpoint + :param provide_automatic_options: controls whether the ``OPTIONS`` + method should be added automatically. This can also be controlled + by setting the ``view_func.provide_automatic_options = False`` + before adding the rule. :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. A change to Werkzeug is handling of method options. methods @@ -1028,8 +1158,9 @@ class Flask(_PackageBoundObject): # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. - provide_automatic_options = getattr(view_func, - 'provide_automatic_options', None) + 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: @@ -1115,7 +1246,9 @@ class Flask(_PackageBoundObject): @setupmethod def errorhandler(self, code_or_exception): - """A decorator that is used to register a function given an + """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) @@ -1128,21 +1261,6 @@ class Flask(_PackageBoundObject): def special_exception_handler(error): return 'Database connection failed', 500 - You can also register a function as error handler without using - the :meth:`errorhandler` decorator. The following example is - equivalent to the one above:: - - def page_not_found(error): - return 'This page does not exist', 404 - app.error_handler_spec[None][404] = page_not_found - - Setting error handlers via assignments to :attr:`error_handler_spec` - however is discouraged as it requires fiddling with nested dictionaries - and the special case for arbitrary exception types. - - The first ``None`` refers to the active blueprint. If the error - handler should be application wide ``None`` shall be used. - .. versionadded:: 0.7 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error @@ -1153,13 +1271,15 @@ class Flask(_PackageBoundObject): that do not necessarily have to be a subclass of the :class:`~werkzeug.exceptions.HTTPException` class. - :param code: the code as integer for the handler + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception """ def decorator(f): self._register_error_handler(None, code_or_exception, f) return f return decorator + @setupmethod def register_error_handler(self, code_or_exception, f): """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator @@ -1178,11 +1298,18 @@ class Flask(_PackageBoundObject): """ if isinstance(code_or_exception, HTTPException): # old broken behavior raise ValueError( - 'Tried to register a handler for an exception instance {0!r}. ' - 'Handlers can only be registered for exception classes or HTTP error codes.' - .format(code_or_exception)) + 'Tried to register a handler for an exception instance {0!r}.' + ' Handlers can only be registered for exception classes or' + ' HTTP error codes.'.format(code_or_exception) + ) - exc_class, code = self._get_exc_class_and_code(code_or_exception) + try: + exc_class, code = self._get_exc_class_and_code(code_or_exception) + except KeyError: + raise KeyError( + "'{0}' is not a recognized HTTP error code. Use a subclass of" + " HTTPException with that code instead.".format(code_or_exception) + ) handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) handlers[exc_class] = f @@ -1288,10 +1415,12 @@ class Flask(_PackageBoundObject): def before_request(self, f): """Registers a function to run before each request. - The function will be called without any arguments. - If the function returns a non-None value, it's handled as - if it was the return value from the view and further - request handling is stopped. + For example, this can be used to open a database connection, or to load + the logged in user from the session. + + The function will be called without any arguments. If it returns a + non-None value, the value is handled as if it was the return value from + the view, and further request handling is stopped. """ self.before_request_funcs.setdefault(None, []).append(f) return f @@ -1347,7 +1476,7 @@ class Flask(_PackageBoundObject): will have to surround the execution of these code by try/except statements and log occurring errors. - When a teardown function was called because of a exception it will + When a teardown function was called because of an exception it will be passed an error object. The return values of teardown functions are ignored. @@ -1383,8 +1512,10 @@ class Flask(_PackageBoundObject): 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 exception it will - be passed an error object. + 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. The return values of teardown functions are ignored. @@ -1410,9 +1541,17 @@ class Flask(_PackageBoundObject): @setupmethod def url_value_preprocessor(self, f): - """Registers a function as URL value preprocessor for all view - functions of the application. It's called before the view functions - are called and can modify the url values provided. + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. """ self.url_value_preprocessors.setdefault(None, []).append(f) return f @@ -1427,43 +1566,28 @@ class Flask(_PackageBoundObject): return f def _find_error_handler(self, e): - """Finds a registered error handler for the request’s blueprint. - Otherwise falls back to the app, returns None if not a suitable - handler is found. + """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)) - def find_handler(handler_map): + for name, c in ( + (request.blueprint, code), (None, code), + (request.blueprint, None), (None, None) + ): + handler_map = self.error_handler_spec.setdefault(name, {}).get(c) + if not handler_map: - return - queue = deque(exc_class.__mro__) - # Protect from geniuses who might create circular references in - # __mro__ - done = set() - - while queue: - cls = queue.popleft() - if cls in done: - continue - done.add(cls) + continue + + for cls in exc_class.__mro__: handler = handler_map.get(cls) + if handler is not None: - # cache for next time exc_class is raised - handler_map[exc_class] = handler return handler - queue.extend(cls.__mro__) - - # try blueprint handlers - handler = find_handler(self.error_handler_spec - .get(request.blueprint, {}) - .get(code)) - if handler is not None: - return handler - - # fall back to app handlers - return find_handler(self.error_handler_spec[None].get(code)) - def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the @@ -1493,12 +1617,20 @@ class Flask(_PackageBoundObject): 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 - if self.config['TRAP_BAD_REQUEST_ERRORS']: + + trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS'] + + # if unset, trap based on debug mode + if (trap_bad_request is None and self.debug) or trap_bad_request: return isinstance(e, BadRequest) + return False def handle_user_exception(self, e): @@ -1509,16 +1641,30 @@ class Flask(_PackageBoundObject): 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 the bad + key in debug mode rather than a generic bad request message. + .. versionadded:: 0.7 """ exc_type, exc_value, tb = sys.exc_info() assert exc_value is e - # ensure not to trash sys.exc_info() at that point in case someone # wants the traceback preserved in handle_http_exception. Of course # we cannot prevent users from trashing it themselves in a custom # trap_http_exception method so that's their fault then. + # MultiDict passes the key to the exception, but that's ignored + # when generating the response message. Set an informative + # description for key errors in debug mode or when trapping errors. + if ( + (self.debug or self.config['TRAP_BAD_REQUEST_ERRORS']) + and isinstance(e, BadRequestKeyError) + # only set it if it's still the default description + and e.description is BadRequestKeyError.description + ): + e.description = "KeyError: '{0}'".format(*e.args) + if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) @@ -1699,62 +1845,106 @@ class Flask(_PackageBoundObject): return False def make_response(self, rv): - """Converts the return value from a view function to a real - response object that is an instance of :attr:`response_class`. - - The following types are allowed for `rv`: - - .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| - - ======================= =========================================== - :attr:`response_class` the object is returned unchanged - :class:`str` a response object is created with the - string as body - :class:`unicode` a response object is created with the - string encoded to utf-8 as body - a WSGI function the function is called as WSGI application - and buffered as response object - :class:`tuple` A tuple in the form ``(response, status, - headers)`` or ``(response, headers)`` - where `response` is any of the - types defined here, `status` is a string - or an integer and `headers` is a list or - a dictionary with header values. - ======================= =========================================== - - :param rv: the return value from the view function + """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`` (``unicode`` in Python 2) + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` (``str`` in Python 2) + A response object is created with the bytes as the body. + + ``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:: 0.9 Previously a tuple was interpreted as the arguments for the response object. """ - status_or_headers = headers = None - if isinstance(rv, tuple): - rv, status_or_headers, headers = rv + (None,) * (3 - len(rv)) - if rv is None: - raise ValueError('View function did not return a response') + status = headers = None + + # unpack tuple returns + if isinstance(rv, (tuple, list)): + len_rv = len(rv) - if isinstance(status_or_headers, (dict, list)): - headers, status_or_headers = status_or_headers, None + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv + # 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 + # 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( + 'The view function 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): - # When we create a response object directly, we let the constructor - # set the headers and status. We do this because there can be - # some extra logic involved when creating these objects with - # specific values (like default content type selection). if isinstance(rv, (text_type, bytes, bytearray)): - rv = self.response_class(rv, headers=headers, - status=status_or_headers) - headers = status_or_headers = None + # 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) + status = headers = None else: - rv = self.response_class.force_type(rv, request.environ) - - if status_or_headers is not None: - if isinstance(status_or_headers, string_types): - rv.status = status_or_headers + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type(rv, request.environ) + except TypeError as e: + new_error = TypeError( + '{e}\nThe view function did not return a valid' + ' response. The return type must be a string, tuple,' + ' Response instance, or WSGI callable, but it was a' + ' {rv.__class__.__name__}.'.format(e=e, rv=rv) + ) + reraise(TypeError, new_error, sys.exc_info()[2]) + + # prefer the status if it was provided + if status is not None: + if isinstance(status, (text_type, bytes, bytearray)): + rv.status = status else: - rv.status_code = status_or_headers + rv.status_code = status + + # extend existing headers with provided headers if headers: rv.headers.extend(headers) @@ -1779,7 +1969,7 @@ class Flask(_PackageBoundObject): if self.config['SERVER_NAME'] is not None: return self.url_map.bind( self.config['SERVER_NAME'], - script_name=self.config['APPLICATION_ROOT'] or '/', + script_name=self.config['APPLICATION_ROOT'], url_scheme=self.config['PREFERRED_URL_SCHEME']) def inject_url_defaults(self, endpoint, values): @@ -1817,16 +2007,16 @@ class Flask(_PackageBoundObject): raise error def preprocess_request(self): - """Called before the actual request dispatching and will - call each :meth:`before_request` decorated function, passing no - arguments. - If any of these functions returns a value, it's handled as - if it was the return value from the view and further - request handling is stopped. + """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. - This also triggers the :meth:`url_value_preprocessor` functions before - the actual :meth:`before_request` functions are called. + 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. """ + bp = _request_ctx_stack.top.request.blueprint funcs = self.url_value_preprocessors.get(None, ()) @@ -1866,7 +2056,7 @@ class Flask(_PackageBoundObject): for handler in funcs: response = handler(response) if not self.session_interface.is_null_session(ctx.session): - self.save_session(ctx.session, response) + self.session_interface.save_session(self, ctx.session, response) return response def do_teardown_request(self, exc=_sentinel): @@ -1951,10 +2141,19 @@ class Flask(_PackageBoundObject): def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see :class:`werkzeug.test.EnvironBuilder` for more information, this - function accepts the same arguments). + function accepts the same arguments plus two additional). + + Additional arguments (only if ``base_url`` is not specified): + + :param subdomain: subdomain to use for route matching + :param url_scheme: scheme for the request, default + ``PREFERRED_URL_SCHEME`` or ``http``. """ + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: return self.request_context(builder.get_environ()) finally: @@ -1986,14 +2185,17 @@ class Flask(_PackageBoundObject): exception context to start the response """ ctx = self.request_context(environ) - ctx.push() error = None try: try: + ctx.push() response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) + except: + error = sys.exc_info()[1] + raise return response(environ, start_response) finally: if self.should_ignore_error(error): diff --git a/flask/blueprints.py b/flask/blueprints.py index 586a1b0b..4c9938e2 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -89,6 +89,28 @@ class Blueprint(_PackageBoundObject): warn_on_modifications = False _got_registered_once = False + #: Blueprint local JSON decoder class to use. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`. + json_encoder = None + #: Blueprint local JSON decoder class to use. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`. + json_decoder = None + + # TODO remove the next three attrs when Sphinx :inherited-members: works + # https://github.com/sphinx-doc/sphinx/issues/741 + + #: The name of the package or module that this app belongs to. Do not + #: change this once it is set by the constructor. + import_name = None + + #: Location of the template files to be added to the template lookup. + #: ``None`` if templates should not be added. + template_folder = None + + #: Absolute path to the package on the filesystem. Used to look up + #: resources contained in the package. + root_path = None + def __init__(self, name, import_name, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, @@ -137,18 +159,25 @@ class Blueprint(_PackageBoundObject): return BlueprintSetupState(self, app, options, first_registration) def register(self, app, options, first_registration=False): - """Called by :meth:`Flask.register_blueprint` to register a blueprint - on the application. This can be overridden to customize the register - behavior. Keyword arguments from - :func:`~flask.Flask.register_blueprint` are directly forwarded to this - method in the `options` dictionary. + """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`. + :param first_registration: Whether this is the first time this + blueprint has been registered on the application. """ self._got_registered_once = True state = self.make_setup_state(app, options, first_registration) + if self.has_static_folder: - state.add_url_rule(self.static_url_path + '/', - view_func=self.send_static_file, - endpoint='static') + state.add_url_rule( + self.static_url_path + '/', + view_func=self.send_static_file, endpoint='static' + ) for deferred in self.deferred_functions: deferred(state) @@ -169,6 +198,8 @@ class Blueprint(_PackageBoundObject): """ if endpoint: assert '.' not in endpoint, "Blueprint endpoints should not contain dots" + if view_func: + assert '.' not in view_func.__name__, "Blueprint view function name should not contain dots" self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) diff --git a/flask/cli.py b/flask/cli.py index 6c8cf32d..a812ae2e 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -8,110 +8,251 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import print_function +import ast +import inspect import os +import re +import ssl import sys -from threading import Lock, Thread +import traceback from functools import update_wrapper +from operator import attrgetter +from threading import Lock, Thread import click +from werkzeug.utils import import_string -from ._compat import iteritems, reraise -from .helpers import get_debug_flag from . import __version__ +from ._compat import getargspec, iteritems, reraise, text_type +from .globals import current_app +from .helpers import get_debug_flag, get_env + +try: + import dotenv +except ImportError: + dotenv = None + class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" -def find_best_app(module): +def find_best_app(script_info, module): """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': + for attr_name in ('app', 'application'): app = getattr(module, attr_name, None) - if app is not None and isinstance(app, Flask): + + if isinstance(app, Flask): return app # Otherwise find the only object that is a Flask instance. - matches = [v for k, v in iteritems(module.__dict__) - if isinstance(v, Flask)] + matches = [ + v for k, v in iteritems(module.__dict__) if isinstance(v, Flask) + ] if len(matches) == 1: return matches[0] - raise NoAppException('Failed to find application in module "%s". Are ' - 'you sure it contains a Flask application? Maybe ' - 'you wrapped it in a WSGI middleware or you are ' - 'using a factory function.' % module.__name__) + elif len(matches) > 1: + raise NoAppException( + 'Detected multiple Flask applications in module "{module}". Use ' + '"FLASK_APP={module}:name" to specify the correct ' + 'one.'.format(module=module.__name__) + ) + + # 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 = call_factory(script_info, app_factory) + + if isinstance(app, Flask): + return app + except TypeError: + if not _called_with_wrong_args(app_factory): + raise + raise NoAppException( + 'Detected factory "{factory}" in module "{module}", but ' + 'could not call it without arguments. Use ' + '"FLASK_APP=\'{module}:{factory}(args)\'" to specify ' + 'arguments.'.format( + factory=attr_name, module=module.__name__ + ) + ) + + raise NoAppException( + 'Failed to find Flask application or factory in module "{module}". ' + 'Use "FLASK_APP={module}:name to specify one.'.format( + module=module.__name__ + ) + ) + + +def call_factory(script_info, app_factory, arguments=()): + """Takes an app factory, a ``script_info` object and optionally a tuple + of arguments. Checks for the existence of a script_info argument and calls + the app_factory depending on that and the arguments provided. + """ + args_spec = getargspec(app_factory) + arg_names = args_spec.args + arg_defaults = args_spec.defaults + + if 'script_info' in arg_names: + return app_factory(*arguments, script_info=script_info) + elif arguments: + return app_factory(*arguments) + elif not arguments and len(arg_names) == 1 and arg_defaults is None: + return app_factory(script_info) + + return app_factory() + + +def _called_with_wrong_args(factory): + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param factory: the factory 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 factory.__code__: + # in the factory, it was called successfully + return False + tb = tb.tb_next -def prepare_exec_for_file(filename): + # didn't reach the factory + return True + finally: + del tb + + +def find_app_by_string(script_info, module, app_name): + """Checks if the given string is a variable name or a function. If it is a + function, it checks for specified arguments and whether it takes a + ``script_info`` argument and calls the function with the appropriate + arguments. + """ + from flask import Flask + match = re.match(r'^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$', app_name) + + if not match: + raise NoAppException( + '"{name}" is not a valid variable name or function ' + 'expression.'.format(name=app_name) + ) + + name, args = match.groups() + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException(e.args[0]) + + if inspect.isfunction(attr): + if args: + try: + args = ast.literal_eval('({args},)'.format(args=args)) + except (ValueError, SyntaxError)as e: + raise NoAppException( + 'Could not parse the arguments in ' + '"{app_name}".'.format(e=e, app_name=app_name) + ) + else: + args = () + + try: + app = call_factory(script_info, attr, args) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + '{e}\nThe factory "{app_name}" in module "{module}" could not ' + 'be called with the specified arguments.'.format( + e=e, app_name=app_name, module=module.__name__ + ) + ) + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + 'A valid Flask application was not obtained from ' + '"{module}:{app_name}".'.format( + module=module.__name__, app_name=app_name + ) + ) + + +def prepare_import(path): """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. """ - module = [] + path = os.path.realpath(path) - # Chop off file extensions or package markers - if os.path.split(filename)[1] == '__init__.py': - filename = os.path.dirname(filename) - elif filename.endswith('.py'): - filename = filename[:-3] - else: - raise NoAppException('The file provided (%s) does exist but is not a ' - 'valid Python file. This means that it cannot ' - 'be used as application. Please change the ' - 'extension to .py' % filename) - filename = os.path.realpath(filename) - - dirpath = filename - while 1: - dirpath, extra = os.path.split(dirpath) - module.append(extra) - if not os.path.isfile(os.path.join(dirpath, '__init__.py')): + if os.path.splitext(path)[1] == '.py': + path = os.path.splitext(path)[0] + + 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 - sys.path.insert(0, dirpath) - return '.'.join(module[::-1]) + if sys.path[0] != path: + sys.path.insert(0, path) + + return '.'.join(module_name[::-1]) -def locate_app(app_id): - """Attempts to locate the application.""" +def locate_app(script_info, module_name, app_name, raise_if_not_found=True): __traceback_hide__ = True - if ':' in app_id: - module, app_obj = app_id.split(':', 1) - else: - module = app_id - app_obj = None try: - __import__(module) + __import__(module_name) except ImportError: - raise NoAppException('The file/path provided (%s) does not appear to ' - 'exist. Please verify the path is correct. If ' - 'app is not on PYTHONPATH, ensure the extension ' - 'is .py' % module) - mod = sys.modules[module] - if app_obj is None: - app = find_best_app(mod) - else: - app = getattr(mod, app_obj, None) - if app is None: - raise RuntimeError('Failed to find application in module "%s"' - % module) - - return app + # 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()[-1].tb_next: + raise NoAppException( + 'While importing "{name}", an ImportError was raised:' + '\n\n{tb}'.format(name=module_name, tb=traceback.format_exc()) + ) + elif raise_if_not_found: + raise NoAppException( + 'Could not import "{name}"."'.format(name=module_name) + ) + else: + return + module = sys.modules[module_name] -def find_default_import_path(): - app = os.environ.get('FLASK_APP') - if app is None: - return - if os.path.isfile(app): - return prepare_exec_for_file(app) - return app + if app_name is None: + return find_best_app(script_info, module) + else: + return find_app_by_string(script_info, module, app_name) def get_version(ctx, param, value): @@ -124,16 +265,21 @@ def get_version(ctx, param, value): }, 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) + +version_option = click.Option( + ['--version'], + help='Show the flask version', + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True +) + class DispatchingApp(object): - """Special application that dispatches to a flask application which + """Special application that dispatches to a Flask application which is imported by name in a background thread. If an error happens - it is is recorded and shows as part of the WSGI handling which in case + it is recorded and shown as part of the WSGI handling which in case of the Werkzeug debugger means that it shows up in the browser. """ @@ -194,15 +340,8 @@ class ScriptInfo(object): """ def __init__(self, app_import_path=None, create_app=None): - if create_app is None: - if app_import_path is None: - app_import_path = find_default_import_path() - self.app_import_path = app_import_path - else: - app_import_path = None - #: Optionally the import path for the Flask application. - self.app_import_path = app_import_path + self.app_import_path = app_import_path or os.environ.get('FLASK_APP') #: Optionally a function that is passed the script info to create #: the instance of the application. self.create_app = create_app @@ -217,23 +356,44 @@ class ScriptInfo(object): be returned. """ __traceback_hide__ = True + if self._loaded_app is not None: return self._loaded_app + + app = None + if self.create_app is not None: - rv = self.create_app(self) + app = call_factory(self, self.create_app) else: - if not self.app_import_path: - raise NoAppException( - 'Could not locate Flask application. You did not provide ' - 'the FLASK_APP environment variable.\n\nFor more ' - 'information see ' - 'http://flask.pocoo.org/docs/latest/quickstart/') - rv = locate_app(self.app_import_path) + if self.app_import_path: + path, name = (self.app_import_path.split(':', 1) + [None])[:2] + import_name = prepare_import(path) + app = locate_app(self, import_name, name) + else: + for path in ('wsgi.py', 'app.py'): + import_name = prepare_import(path) + app = locate_app(self, import_name, None, + raise_if_not_found=False) + + if app: + break + + if not app: + raise NoAppException( + 'Could not locate a Flask application. You did not provide ' + 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' + '"app.py" module was not found in the current directory.' + ) + debug = get_debug_flag() + + # Update the app's debug flag through the descriptor so that other + # values repopulate as well. if debug is not None: - rv.debug = debug - self._loaded_app = rv - return rv + app.debug = debug + + self._loaded_app = app + return app pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) @@ -291,14 +451,21 @@ class FlaskGroup(AppGroup): For information as of why this is useful see :ref:`custom-scripts`. :param add_default_commands: if this is True then the default run and - shell commands wil be added. + shell commands wil 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 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. + + .. 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=True, create_app=None, - add_version_option=True, **extra): + add_version_option=True, load_dotenv=True, **extra): params = list(extra.pop('params', None) or ()) if add_version_option: @@ -306,10 +473,12 @@ class FlaskGroup(AppGroup): AppGroup.__init__(self, params=params, **extra) self.create_app = create_app + self.load_dotenv = load_dotenv if add_default_commands: self.add_command(run_command) self.add_command(shell_command) + self.add_command(routes_command) self._loaded_plugin_commands = False @@ -362,16 +531,182 @@ class FlaskGroup(AppGroup): # want the help page to break if the app does not exist. # If someone attempts to use the command we try to create # the app again and this will give us the error. - pass + # However, we will not do so silently because that would confuse + # users. + traceback.print_exc() return sorted(rv) def main(self, *args, **kwargs): + # Set a global flag that indicates that we were invoked from the + # command line interface. This is detected by Flask.run to make the + # call into a no-op. This is necessary to avoid ugly errors when the + # script that is loaded here also attempts to start a server. + os.environ['FLASK_RUN_FROM_CLI'] = 'true' + + if self.load_dotenv: + load_dotenv() + obj = kwargs.get('obj') + if obj is None: obj = ScriptInfo(create_app=self.create_app) + kwargs['obj'] = obj kwargs.setdefault('auto_envvar_prefix', 'FLASK') - return AppGroup.main(self, *args, **kwargs) + return super(FlaskGroup, self).main(*args, **kwargs) + + +def _path_is_ancestor(path, other): + """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=None): + """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. + + Changes the current working directory to the location of the first file + found, with the assumption that it is in the top level project directory + and will be where the Python path should import local packages from. + + 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. + + .. versionadded:: 1.0 + """ + + if dotenv is None: + return + + if path is not None: + return dotenv.load_dotenv(path) + + new_dir = None + + for name in ('.env', '.flaskenv'): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + if new_dir is None: + new_dir = os.path.dirname(path) + + dotenv.load_dotenv(path) + + if new_dir and os.getcwd() != new_dir: + os.chdir(new_dir) + + return new_dir is not None # at least one file was located and loaded + + +def show_server_banner(env, debug, app_import_path): + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + return + + if app_import_path is not None: + print(' * Serving Flask app "{0}"'.format(app_import_path)) + + print(' * Environment: {0}'.format(env)) + + if env == 'production': + click.secho( + ' WARNING: Do not use the development server in a production' + ' environment.', fg='red') + click.secho(' Use a production WSGI server instead.', dim=True) + + if debug is not None: + print(' * Debug mode: {0}'.format('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): + self.path_type = click.Path( + exists=True, dir_okay=False, resolve_path=True) + + def convert(self, value, param, ctx): + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == 'adhoc': + try: + import OpenSSL + except ImportError: + raise click.BadParameter( + 'Using ad-hoc certificates requires pyOpenSSL.', + ctx, param) + + return value + + obj = import_string(value, silent=True) + + if sys.version_info < (2, 7): + if obj: + return obj + else: + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx, param, value): + """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' + + if sys.version_info < (2, 7): + is_context = cert and not isinstance(cert, (text_type, bytes)) + 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 @click.command('run', short_help='Runs a development server.') @@ -379,57 +714,51 @@ class FlaskGroup(AppGroup): 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.') +@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 ' + 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 ' + help='Enable or disable the debugger. By default the debugger ' 'is active if debug is enabled.') @click.option('--eager-loading/--lazy-loader', default=None, - help='Enable or disable eager loading. By default eager ' + help='Enable or disable eager loading. By default eager ' 'loading is enabled if the reloader is disabled.') -@click.option('--with-threads/--without-threads', default=False, +@click.option('--with-threads/--without-threads', default=True, help='Enable or disable multithreading.') @pass_script_info def run_command(info, host, port, reload, debugger, eager_loading, - with_threads): - """Runs a local development server for the Flask application. + with_threads, cert): + """Run a local development server. - This local server is recommended for development purposes only but it - can also be used for simple intranet deployments. By default it will - not support any sort of concurrency at all to simplify debugging. This - can be changed with the --with-threads option which will enable basic - multithreading. + 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 by default enabled if the debug flag of - Flask is enabled and disabled otherwise. + The reloader and debugger are enabled by default if + FLASK_ENV=development or FLASK_DEBUG=1. """ - from werkzeug.serving import run_simple - debug = get_debug_flag() + if reload is None: - reload = bool(debug) + reload = debug + if debugger is None: - debugger = bool(debug) + debugger = debug + if eager_loading is None: eager_loading = not reload + show_server_banner(get_env(), debug, info.app_import_path) app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) - # Extra startup messages. This depends a bit on Werkzeug internals to - # not double execute when the reloader kicks in. - if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': - # If we have an import path we can print it out now which can help - # people understand what's being served. If we do not have an - # import path because the app was loaded through a callback then - # we won't print anything. - if info.app_import_path is not None: - print(' * Serving Flask app "%s"' % info.app_import_path) - if debug is not None: - print(' * Forcing debug mode %s' % (debug and 'on' or 'off')) - - run_simple(host, port, app, use_reloader=reload, - use_debugger=debugger, threaded=with_threads) + from werkzeug.serving import run_simple + run_simple(host, port, app, use_reloader=reload, use_debugger=debugger, + threaded=with_threads, ssl_context=cert) @click.command('shell', short_help='Runs a shell in the app context.') @@ -440,16 +769,16 @@ def shell_command(): namespace of this shell according to it's configuration. This is useful for executing small snippets of management code - without having to manually configuring the application. + without having to manually configure the application. """ import code from flask.globals import _app_ctx_stack app = _app_ctx_stack.top.app - banner = 'Python %s on %s\nApp: %s%s\nInstance: %s' % ( + banner = 'Python %s on %s\nApp: %s [%s]\nInstance: %s' % ( sys.version, sys.platform, app.import_name, - app.debug and ' [debug]' or '', + app.env, app.instance_path, ) ctx = {} @@ -466,41 +795,85 @@ def shell_command(): code.interact(banner=banner, local=ctx) -cli = FlaskGroup(help="""\ -This shell command acts as general utility script for Flask applications. +@click.command('routes', short_help='Show the routes for the app.') +@click.option( + '--sort', '-s', + type=click.Choice(('endpoint', 'methods', '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, all_methods): + """Show all registered routes with endpoints and methods.""" + + rules = list(current_app.url_map.iter_rules()) + ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS')) + + if sort in ('endpoint', 'rule'): + rules = sorted(rules, key=attrgetter(sort)) + elif sort == 'methods': + rules = sorted(rules, key=lambda rule: sorted(rule.methods)) + + rule_methods = [ + ', '.join(sorted(rule.methods - ignored_methods)) for rule in rules + ] + + headers = ('Endpoint', 'Methods', 'Rule') + widths = ( + max(len(rule.endpoint) for rule in rules), + max(len(methods) for methods in rule_methods), + max(len(rule.rule) for rule in rules), + ) + widths = [max(len(h), w) for h, w in zip(headers, widths)] + row = '{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}'.format(*widths) + + click.echo(row.format(*headers).strip()) + click.echo(row.format(*('-' * width for width in widths))) + + for rule, methods in zip(rules, rule_methods): + click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) -It loads the application configured (through the FLASK_APP environment -variable) and then provides commands either provided by the application or -Flask itself. -The most useful commands are the "run" and "shell" command. +cli = FlaskGroup(help="""\ +A general utility script for Flask applications. -Example usage: +Provides commands from Flask, extensions, and the application. Loads the +application defined in the FLASK_APP environment variable, or from a wsgi.py +file. Setting the FLASK_ENV environment variable to 'development' will enable +debug mode. \b - %(prefix)s%(cmd)s FLASK_APP=hello.py - %(prefix)s%(cmd)s FLASK_DEBUG=1 - %(prefix)sflask run -""" % { - 'cmd': os.name == 'posix' and 'export' or 'set', - 'prefix': os.name == 'posix' and '$ ' or '', -}) + {prefix}{cmd} FLASK_APP=hello.py + {prefix}{cmd} FLASK_ENV=development + {prefix}flask run +""".format( + cmd='export' if os.name == 'posix' else 'set', + prefix='$ ' if os.name == 'posix' else '> ' +)) def main(as_module=False): - this_module = __package__ + '.cli' args = sys.argv[1:] if as_module: - if sys.version_info >= (2, 7): - name = 'python -m ' + this_module.rsplit('.', 1)[0] - else: - name = 'python -m ' + this_module + this_module = 'flask' + + if sys.version_info < (2, 7): + this_module += '.cli' + + name = 'python -m ' + this_module - # This module is always executed as "python -m flask.run" and as such - # we need to ensure that we restore the actual command line so that - # the reloader can properly operate. - sys.argv = ['-m', this_module] + sys.argv[1:] + # Python rewrites "python -m flask" to the path to the file in argv. + # Restore the original command so that the reloader works. + sys.argv = ['-m', this_module] + args else: name = None diff --git a/flask/config.py b/flask/config.py index 36e8a123..f73a4232 100644 --- a/flask/config.py +++ b/flask/config.py @@ -126,10 +126,12 @@ class Config(dict): d = types.ModuleType('config') d.__file__ = filename try: - with open(filename) as config_file: + with open(filename, mode='rb') as config_file: exec(compile(config_file.read(), filename, 'exec'), d.__dict__) except IOError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR): + if silent and e.errno in ( + errno.ENOENT, errno.EISDIR, errno.ENOTDIR + ): return False e.strerror = 'Unable to load configuration file (%s)' % e.strerror raise diff --git a/flask/ctx.py b/flask/ctx.py index 480d9c5c..9e184c18 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -325,13 +325,18 @@ class RequestContext(object): _request_ctx_stack.push(self) - # Open the session at the moment that the request context is - # available. This allows a custom open_session method to use the - # request context (e.g. code that access database information - # stored on `g` instead of the appcontext). - self.session = self.app.open_session(self.request) + # 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: - self.session = self.app.make_null_session() + 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) def pop(self, exc=_sentinel): """Pops the request context and unbinds it by doing that. This will diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 90710dd3..9e44fe69 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -8,6 +8,9 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import os +from warnings import warn + from ._compat import implements_to_string, text_type from .app import Flask from .blueprints import Blueprint @@ -153,3 +156,12 @@ def explain_template_loading_attempts(app, template, attempts): info.append(' See http://flask.pocoo.org/docs/blueprints/#templates') app.logger.info('\n'.join(info)) + + +def explain_ignored_app_run(): + if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + warn(Warning('Silently ignoring app.run() because the ' + 'application is run from the flask command line ' + 'executable. Consider putting app.run() behind an ' + 'if __name__ == "__main__" guard to silence this ' + 'warning.'), stacklevel=3) diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py deleted file mode 100644 index 051f44ac..00000000 --- a/flask/ext/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext - ~~~~~~~~~ - - Redirect imports for extensions. This module basically makes it possible - for us to transition from flaskext.foo to flask_foo without having to - force all extensions to upgrade at the same time. - - When a user does ``from flask.ext.foo import bar`` it will attempt to - import ``from flask_foo import bar`` first and when that fails it will - try to import ``from flaskext.foo import bar``. - - We're switching from namespace packages because it was just too painful for - everybody involved. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - - -def setup(): - from ..exthook import ExtensionImporter - importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) - importer.install() - - -setup() -del setup diff --git a/flask/exthook.py b/flask/exthook.py deleted file mode 100644 index d8842802..00000000 --- a/flask/exthook.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.exthook - ~~~~~~~~~~~~~ - - Redirect imports for extensions. This module basically makes it possible - for us to transition from flaskext.foo to flask_foo without having to - force all extensions to upgrade at the same time. - - When a user does ``from flask.ext.foo import bar`` it will attempt to - import ``from flask_foo import bar`` first and when that fails it will - try to import ``from flaskext.foo import bar``. - - We're switching from namespace packages because it was just too painful for - everybody involved. - - This is used by `flask.ext`. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -import sys -import os -import warnings -from ._compat import reraise - - -class ExtDeprecationWarning(DeprecationWarning): - pass - -warnings.simplefilter('always', ExtDeprecationWarning) - - -class ExtensionImporter(object): - """This importer redirects imports from this submodule to other locations. - This makes it possible to transition from the old flaskext.name to the - newer flask_name without people having a hard time. - """ - - def __init__(self, module_choices, wrapper_module): - self.module_choices = module_choices - self.wrapper_module = wrapper_module - self.prefix = wrapper_module + '.' - self.prefix_cutoff = wrapper_module.count('.') + 1 - - def __eq__(self, other): - return self.__class__.__module__ == other.__class__.__module__ and \ - self.__class__.__name__ == other.__class__.__name__ and \ - self.wrapper_module == other.wrapper_module and \ - self.module_choices == other.module_choices - - def __ne__(self, other): - return not self.__eq__(other) - - def install(self): - sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] - - def find_module(self, fullname, path=None): - if fullname.startswith(self.prefix) and \ - fullname != 'flask.ext.ExtDeprecationWarning': - return self - - def load_module(self, fullname): - if fullname in sys.modules: - return sys.modules[fullname] - - modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] - - warnings.warn( - "Importing flask.ext.{x} is deprecated, use flask_{x} instead." - .format(x=modname), ExtDeprecationWarning, stacklevel=2 - ) - - for path in self.module_choices: - realname = path % modname - try: - __import__(realname) - except ImportError: - exc_type, exc_value, tb = sys.exc_info() - # since we only establish the entry in sys.modules at the - # very this seems to be redundant, but if recursive imports - # happen we will call into the move import a second time. - # On the second invocation we still don't have an entry for - # fullname in sys.modules, but we will end up with the same - # fake module name and that import will succeed since this - # one already has a temporary entry in the modules dict. - # Since this one "succeeded" temporarily that second - # invocation now will have created a fullname entry in - # sys.modules which we have to kill. - sys.modules.pop(fullname, None) - - # If it's an important traceback we reraise it, otherwise - # we swallow it and try the next choice. The skipped frame - # is the one from __import__ above which we don't care about - if self.is_important_traceback(realname, tb): - reraise(exc_type, exc_value, tb.tb_next) - continue - module = sys.modules[fullname] = sys.modules[realname] - if '.' not in modname: - setattr(sys.modules[self.wrapper_module], modname, module) - - if realname.startswith('flaskext.'): - warnings.warn( - "Detected extension named flaskext.{x}, please rename it " - "to flask_{x}. The old form is deprecated." - .format(x=modname), ExtDeprecationWarning - ) - - return module - raise ImportError('No module named %s' % fullname) - - def is_important_traceback(self, important_module, tb): - """Walks a traceback's frames and checks if any of the frames - originated in the given important module. If that is the case then we - were able to import the module itself but apparently something went - wrong when the module was imported. (Eg: import of an import failed). - """ - while tb is not None: - if self.is_important_frame(important_module, tb): - return True - tb = tb.tb_next - return False - - def is_important_frame(self, important_module, tb): - """Checks a single frame if it's important.""" - g = tb.tb_frame.f_globals - if '__name__' not in g: - return False - - module_name = g['__name__'] - - # Python 2.7 Behavior. Modules are cleaned up late so the - # name shows up properly here. Success! - if module_name == important_module: - return True - - # Some python versions will clean up modules so early that the - # module name at that point is no longer set. Try guessing from - # the filename then. - filename = os.path.abspath(tb.tb_frame.f_code.co_filename) - test_string = os.path.sep + important_module.replace('.', os.path.sep) - return test_string + '.py' in filename or \ - test_string + os.path.sep + '__init__.py' in filename diff --git a/flask/helpers.py b/flask/helpers.py index c6c2cddc..49968bfc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -10,6 +10,7 @@ """ import os +import socket import sys import pkgutil import posixpath @@ -17,31 +18,22 @@ import mimetypes from time import time from zlib import adler32 from threading import RLock +import unicodedata from werkzeug.routing import BuildError from functools import update_wrapper -try: - from werkzeug.urls import url_quote -except ImportError: - from urlparse import quote as url_quote - +from werkzeug.urls import url_quote from werkzeug.datastructures import Headers, Range from werkzeug.exceptions import BadRequest, NotFound, \ RequestedRangeNotSatisfiable -# this was moved in 0.7 -try: - from werkzeug.wsgi import wrap_file -except ImportError: - from werkzeug.utils import wrap_file - +from werkzeug.wsgi import wrap_file from jinja2 import FileSystemLoader from .signals import message_flashed from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request -from ._compat import string_types, text_type - +from ._compat import string_types, text_type, PY2 # sentinel _missing = object() @@ -54,11 +46,26 @@ _os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, '/')) -def get_debug_flag(default=None): +def get_env(): + """Get the environment the app is running in, indicated by the + :envvar:`FLASK_ENV` environment variable. The default is + ``'production'``. + """ + return os.environ.get('FLASK_ENV') or 'production' + + +def get_debug_flag(): + """Get whether debug mode should be enabled for the app, indicated + by the :envvar:`FLASK_DEBUG` environment variable. The default is + ``True`` if :func:`.get_env` returns ``'development'``, or ``False`` + otherwise. + """ val = os.environ.get('FLASK_DEBUG') + if not val: - return default - return val not in ('0', 'false', 'no') + return get_env() == 'development' + + return val.lower() not in ('0', 'false', 'no') def _endpoint_from_view_func(view_func): @@ -266,40 +273,40 @@ def url_for(endpoint, **values): """ appctx = _app_ctx_stack.top reqctx = _request_ctx_stack.top + if appctx is None: - raise RuntimeError('Attempted to generate a URL without the ' - 'application context being pushed. This has to be ' - 'executed when application context is available.') + raise RuntimeError( + 'Attempted to generate a URL without the application context being' + ' pushed. This has to be executed when application context is' + ' available.' + ) # If request specific information is available we have some extra # features that support "relative" URLs. if reqctx is not None: url_adapter = reqctx.url_adapter blueprint_name = request.blueprint - if not reqctx.request._is_old_module: - if endpoint[:1] == '.': - if blueprint_name is not None: - endpoint = blueprint_name + endpoint - else: - endpoint = endpoint[1:] - else: - # TODO: get rid of this deprecated functionality in 1.0 - if '.' not in endpoint: - if blueprint_name is not None: - endpoint = blueprint_name + '.' + endpoint - elif endpoint.startswith('.'): + + if endpoint[:1] == '.': + if blueprint_name is not None: + endpoint = blueprint_name + endpoint + else: endpoint = endpoint[1:] + external = values.pop('_external', False) # Otherwise go with the url adapter from the appctx and make # the URLs external by default. else: url_adapter = appctx.url_adapter + if url_adapter is None: - raise RuntimeError('Application was not able to create a URL ' - 'adapter for request independent URL generation. ' - 'You might be able to fix this by setting ' - 'the SERVER_NAME config variable.') + raise RuntimeError( + 'Application was not able to create a URL adapter for request' + ' independent URL generation. You might be able to fix this by' + ' setting the SERVER_NAME config variable.' + ) + external = values.pop('_external', True) anchor = values.pop('_anchor', None) @@ -330,6 +337,7 @@ def url_for(endpoint, **values): values['_external'] = external values['_anchor'] = anchor values['_method'] = method + values['_scheme'] = scheme return appctx.app.handle_url_build_error(error, endpoint, values) if anchor is not None: @@ -380,7 +388,7 @@ def flash(message, category='message'): # session.setdefault('_flashes', []).append((category, message)) # # This assumed that changes made to mutable structures in the session are - # are always in sync with the session object, which is not true for session + # 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)) @@ -478,7 +486,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, The `attachment_filename` is preferred over `filename` for MIME-type detection. - :param filename_or_fp: the filename of the file to send in `latin-1`. + .. versionchanged:: 0.13 + UTF-8 filenames, as specified in `RFC 2231`_, are supported. + + .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4 + + :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. Alternatively a file object might be provided in @@ -534,8 +547,19 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if attachment_filename is None: raise TypeError('filename unavailable, required for ' 'sending as attachment') - headers.add('Content-Disposition', 'attachment', - filename=attachment_filename) + + try: + attachment_filename = attachment_filename.encode('latin-1') + except UnicodeEncodeError: + filenames = { + 'filename': unicodedata.normalize( + 'NFKD', attachment_filename).encode('latin-1', 'ignore'), + 'filename*': "UTF-8''%s" % url_quote(attachment_filename), + } + else: + filenames = {'filename': attachment_filename} + + headers.add('Content-Disposition', 'attachment', **filenames) if current_app.use_x_sendfile and filename: if file is not None: @@ -584,17 +608,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, 'headers' % filename, stacklevel=2) if conditional: - if callable(getattr(Range, 'to_content_range_header', None)): - # Werkzeug supports Range Requests - # Remove this test when support for Werkzeug <0.12 is dropped - try: - rv = rv.make_conditional(request, accept_ranges=True, - complete_length=fsize) - except RequestedRangeNotSatisfiable: + try: + rv = rv.make_conditional(request, accept_ranges=True, + complete_length=fsize) + except RequestedRangeNotSatisfiable: + if file is not None: file.close() - raise - else: - rv = rv.make_conditional(request) + raise # make sure we don't send x-sendfile for servers that # ignore the 304 status code for x-sendfile. if rv.status_code == 304: @@ -619,18 +639,24 @@ def safe_join(directory, *pathnames): :raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed paths fall out of its boundaries. """ + + parts = [directory] + for filename in pathnames: if filename != '': filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - raise NotFound() - if os.path.isabs(filename) or \ - filename == '..' or \ - filename.startswith('../'): + + if ( + any(sep in filename for sep in _os_alt_seps) + or os.path.isabs(filename) + or filename == '..' + or filename.startswith('../') + ): raise NotFound() - directory = os.path.join(directory, filename) - return directory + + parts.append(filename) + + return posixpath.join(*parts) def send_from_directory(directory, filename, **options): @@ -823,43 +849,56 @@ class locked_cached_property(object): class _PackageBoundObject(object): + #: The name of the package or module that this app belongs to. Do not + #: change this once it is set by the constructor. + import_name = None + + #: Location of the template files to be added to the template lookup. + #: ``None`` if templates should not be added. + template_folder = None + + #: Absolute path to the package on the filesystem. Used to look up + #: resources contained in the package. + root_path = None def __init__(self, import_name, template_folder=None, root_path=None): - #: The name of the package or module. Do not change this once - #: it was set by the constructor. self.import_name = import_name - - #: location of the templates. ``None`` if templates should not be - #: exposed. self.template_folder = template_folder if root_path is None: root_path = get_root_path(self.import_name) - #: Where is the app root located? self.root_path = root_path - self._static_folder = None self._static_url_path = None def _get_static_folder(self): if self._static_folder is not None: return os.path.join(self.root_path, self._static_folder) + def _set_static_folder(self, value): self._static_folder = value - static_folder = property(_get_static_folder, _set_static_folder, doc=''' - The absolute path to the configured static folder. - ''') + + static_folder = property( + _get_static_folder, _set_static_folder, + doc='The absolute path to the configured static folder.' + ) del _get_static_folder, _set_static_folder def _get_static_url_path(self): if self._static_url_path is not None: return self._static_url_path + if self.static_folder is not None: return '/' + os.path.basename(self.static_folder) + def _set_static_url_path(self, value): self._static_url_path = value - static_url_path = property(_get_static_url_path, _set_static_url_path) + + static_url_path = property( + _get_static_url_path, _set_static_url_path, + doc='The URL prefix that the static route will be registered for.' + ) del _get_static_url_path, _set_static_url_path @property @@ -958,3 +997,33 @@ def total_seconds(td): :rtype: int """ return td.days * 60 * 60 * 24 + td.seconds + + +def is_ip(value): + """Determine if the given string is an IP address. + + Python 2 on Windows doesn't provide ``inet_pton``, so this only + checks IPv4 addresses in that environment. + + :param value: value to check + :type value: str + + :return: True if string is an IP address + :rtype: bool + """ + if PY2 and os.name == 'nt': + try: + socket.inet_aton(value) + return True + except socket.error: + return False + + for family in (socket.AF_INET, socket.AF_INET6): + try: + socket.inet_pton(family, value) + except socket.error: + pass + else: + return True + + return False diff --git a/flask/json.py b/flask/json/__init__.py similarity index 88% rename from flask/json.py rename to flask/json/__init__.py index 16e0c295..6a10b737 100644 --- a/flask/json.py +++ b/flask/json/__init__.py @@ -1,18 +1,9 @@ # -*- coding: utf-8 -*- -""" - flask.jsonimpl - ~~~~~~~~~~~~~~ - - Implementation helpers for the JSON support in Flask. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" import io import uuid -from datetime import date -from .globals import current_app, request -from ._compat import text_type, PY2 +from datetime import date, datetime +from flask.globals import current_app, request +from flask._compat import text_type, PY2 from werkzeug.http import http_date from jinja2 import Markup @@ -71,6 +62,8 @@ class JSONEncoder(_json.JSONEncoder): return list(iterable) return JSONEncoder.default(self, o) """ + if isinstance(o, datetime): + return http_date(o.utctimetuple()) if isinstance(o, date): return http_date(o.timetuple()) if isinstance(o, uuid.UUID): @@ -91,9 +84,16 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" if current_app: - kwargs.setdefault('cls', current_app.json_encoder) + bp = current_app.blueprints.get(request.blueprint) if request else None + kwargs.setdefault( + 'cls', + bp.json_encoder if bp and bp.json_encoder + else current_app.json_encoder + ) + if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) + kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) else: kwargs.setdefault('sort_keys', True) @@ -103,7 +103,12 @@ def _dump_arg_defaults(kwargs): def _load_arg_defaults(kwargs): """Inject default arguments for load functions.""" if current_app: - kwargs.setdefault('cls', current_app.json_decoder) + bp = current_app.blueprints.get(request.blueprint) if request else None + kwargs.setdefault( + 'cls', + bp.json_decoder if bp and bp.json_decoder + else current_app.json_decoder + ) else: kwargs.setdefault('cls', JSONDecoder) @@ -236,11 +241,10 @@ def jsonify(*args, **kwargs): Added support for serializing top-level arrays. This introduces a security risk in ancient browsers. See :ref:`json-security` for details. - This function's response will be pretty printed if it was not requested - with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless - the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false. - Compressed (not pretty) formatting currently means no indents and no - spaces after separators. + This function's response will be pretty printed if the + ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to True or the + Flask app is running in debug mode. Compressed (not pretty) formatting + currently means no indents and no spaces after separators. .. versionadded:: 0.2 """ @@ -248,7 +252,7 @@ def jsonify(*args, **kwargs): indent = None separators = (',', ':') - if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] and not request.is_xhr: + if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] or current_app.debug: indent = 2 separators = (', ', ': ') @@ -260,7 +264,7 @@ def jsonify(*args, **kwargs): data = args or kwargs return current_app.response_class( - (dumps(data, indent=indent, separators=separators), '\n'), + dumps(data, indent=indent, separators=separators) + '\n', mimetype=current_app.config['JSONIFY_MIMETYPE'] ) diff --git a/flask/json/tag.py b/flask/json/tag.py new file mode 100644 index 00000000..3c57884e --- /dev/null +++ b/flask/json/tag.py @@ -0,0 +1,297 @@ +""" +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 seen an example that adds support for :class:`~collections.OrderedDict`. +Dicts don't have an order in Python or 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``. :: + + 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, 0) + +""" + +from base64 import b64decode, b64encode +from datetime import datetime +from uuid import UUID + +from jinja2 import Markup +from werkzeug.http import http_date, parse_date + +from flask._compat import iteritems, text_type +from flask.json import dumps, loads + + +class JSONTag(object): + """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" + + __slots__ = ('serializer',) + + #: The tag to mark the serialized object with. If ``None``, this tag is + #: only used as an intermediate step during tagging. + key = None + + def __init__(self, serializer): + """Create a tagger for the given serializer.""" + self.serializer = serializer + + def check(self, value): + """Check if the given value should be tagged by this tag.""" + raise NotImplementedError + + def to_json(self, value): + """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): + """Convert the JSON representation back to the correct type. The tag + will already be removed.""" + raise NotImplementedError + + def tag(self, value): + """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): + return ( + isinstance(value, dict) + and len(value) == 1 + and next(iter(value)) in self.serializer.tags + ) + + def to_json(self, value): + key = next(iter(value)) + return {key + '__': self.serializer.tag(value[key])} + + def to_python(self, value): + key = next(iter(value)) + return {key[:-2]: value[key]} + + +class PassDict(JSONTag): + __slots__ = () + + def check(self, value): + return isinstance(value, dict) + + def to_json(self, value): + # JSON objects may only have string keys, so don't bother tagging the + # key here. + return dict((k, self.serializer.tag(v)) for k, v in iteritems(value)) + + tag = to_json + + +class TagTuple(JSONTag): + __slots__ = () + key = ' t' + + def check(self, value): + return isinstance(value, tuple) + + def to_json(self, value): + return [self.serializer.tag(item) for item in value] + + def to_python(self, value): + return tuple(value) + + +class PassList(JSONTag): + __slots__ = () + + def check(self, value): + return isinstance(value, list) + + def to_json(self, value): + return [self.serializer.tag(item) for item in value] + + tag = to_json + + +class TagBytes(JSONTag): + __slots__ = () + key = ' b' + + def check(self, value): + return isinstance(value, bytes) + + def to_json(self, value): + return b64encode(value).decode('ascii') + + def to_python(self, value): + return b64decode(value) + + +class TagMarkup(JSONTag): + """Serialize anything matching the :class:`~flask.Markup` API by + having a ``__html__`` method to the result of that method. Always + deserializes to an instance of :class:`~flask.Markup`.""" + + __slots__ = () + key = ' m' + + def check(self, value): + return callable(getattr(value, '__html__', None)) + + def to_json(self, value): + return text_type(value.__html__()) + + def to_python(self, value): + return Markup(value) + + +class TagUUID(JSONTag): + __slots__ = () + key = ' u' + + def check(self, value): + return isinstance(value, UUID) + + def to_json(self, value): + return value.hex + + def to_python(self, value): + return UUID(value) + + +class TagDateTime(JSONTag): + __slots__ = () + key = ' d' + + def check(self, value): + return isinstance(value, datetime) + + def to_json(self, value): + return http_date(value) + + def to_python(self, value): + return parse_date(value) + + +class TaggedJSONSerializer(object): + """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:`~flask.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): + self.tags = {} + self.order = [] + + for cls in self.default_tags: + self.register(cls) + + def register(self, tag_class, force=False, index=-1): + """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 -1 (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 is not None: + if not force and key in self.tags: + raise KeyError("Tag '{0}' is already registered.".format(key)) + + self.tags[key] = tag + + if index == -1: + self.order.append(tag) + else: + self.order.insert(index, tag) + + def tag(self, value): + """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): + """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 dumps(self, value): + """Tag the value and dump it to a compact JSON string.""" + return dumps(self.tag(value), separators=(',', ':')) + + def loads(self, value): + """Load data from a JSON string and deserialized any tagged objects.""" + return loads(value, object_hook=self.untag) diff --git a/flask/logging.py b/flask/logging.py index 3f888a75..86a3fa33 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -1,94 +1,69 @@ -# -*- coding: utf-8 -*- -""" - flask.logging - ~~~~~~~~~~~~~ - - Implements the logging support for Flask. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - from __future__ import absolute_import +import logging import sys from werkzeug.local import LocalProxy -from logging import getLogger, StreamHandler, Formatter, getLoggerClass, \ - DEBUG, ERROR -from .globals import _request_ctx_stack - -PROD_LOG_FORMAT = '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' -DEBUG_LOG_FORMAT = ( - '-' * 80 + '\n' + - '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + - '%(message)s\n' + - '-' * 80 -) +from .globals import request @LocalProxy -def _proxy_stream(): - """Finds the most appropriate error stream for the application. If a - WSGI request is in flight we log to wsgi.errors, otherwise this resolves - to sys.stderr. +def wsgi_errors_stream(): + """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``. """ - ctx = _request_ctx_stack.top - if ctx is not None: - return ctx.request.environ['wsgi.errors'] - return sys.stderr + return request.environ['wsgi.errors'] if request else sys.stderr + + +def has_level_handler(logger): + """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 -def _should_log_for(app, mode): - policy = app.config['LOGGER_HANDLER_POLICY'] - if policy == mode or policy == 'always': - return True 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) +default_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' +)) + + def create_logger(app): - """Creates a logger for the given application. This logger works - similar to a regular Python logger but changes the effective logging - level based on the application's debug flag. Furthermore this - function also removes all attached handlers in case there was a - logger with the log name before. + """Get the ``'flask.app'`` logger and configure it if needed. + + 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 = getLoggerClass() - - class DebugLogger(Logger): - def getEffectiveLevel(self): - if self.level == 0 and app.debug: - return DEBUG - return Logger.getEffectiveLevel(self) - - class DebugHandler(StreamHandler): - def emit(self, record): - if app.debug and _should_log_for(app, 'debug'): - StreamHandler.emit(self, record) - - class ProductionHandler(StreamHandler): - def emit(self, record): - if not app.debug and _should_log_for(app, 'production'): - StreamHandler.emit(self, record) - - debug_handler = DebugHandler() - debug_handler.setLevel(DEBUG) - debug_handler.setFormatter(Formatter(DEBUG_LOG_FORMAT)) - - prod_handler = ProductionHandler(_proxy_stream) - prod_handler.setLevel(ERROR) - prod_handler.setFormatter(Formatter(PROD_LOG_FORMAT)) - - logger = getLogger(app.logger_name) - # just in case that was not a new logger, get rid of all the handlers - # already attached to it. - del logger.handlers[:] - logger.__class__ = DebugLogger - logger.addHandler(debug_handler) - logger.addHandler(prod_handler) - - # Disable propagation by default - logger.propagate = False + logger = logging.getLogger('flask.app') + + if app.debug and logger.level == logging.NOTSET: + logger.setLevel(logging.DEBUG) + + if not has_level_handler(logger): + logger.addHandler(default_handler) return logger diff --git a/flask/sessions.py b/flask/sessions.py index b9120712..621f3f5e 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -8,112 +8,86 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ - -import uuid import hashlib -from base64 import b64encode, b64decode +import warnings +from collections import MutableMapping from datetime import datetime -from werkzeug.http import http_date, parse_date + +from itsdangerous import BadSignature, URLSafeTimedSerializer from werkzeug.datastructures import CallbackDict -from . import Markup, json -from ._compat import iteritems, text_type -from .helpers import total_seconds -from itsdangerous import URLSafeTimedSerializer, BadSignature +from flask.helpers import is_ip, total_seconds +from flask.json.tag import TaggedJSONSerializer -class SessionMixin(object): - """Expands a basic dictionary with an accessors that are expected - by Flask extensions and users for the session. - """ +class SessionMixin(MutableMapping): + """Expands a basic dictionary with session attributes.""" - def _get_permanent(self): + @property + def permanent(self): + """This reflects the ``'_permanent'`` key in the dict.""" return self.get('_permanent', False) - def _set_permanent(self, value): + @permanent.setter + def permanent(self, value): self['_permanent'] = bool(value) - #: this reflects the ``'_permanent'`` key in the dict. - permanent = property(_get_permanent, _set_permanent) - del _get_permanent, _set_permanent - - #: some session backends can tell you if a session is new, but that is - #: not necessarily guaranteed. Use with caution. The default mixin - #: implementation just hardcodes ``False`` in. + #: 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 - #: for some backends this will always be ``True``, but some backends will - #: default this to false and detect changes in the dictionary for as - #: long as changes do not happen on mutable structures in the session. - #: The default mixin implementation just hardcodes ``True`` in. + #: 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 -def _tag(value): - if isinstance(value, tuple): - return {' t': [_tag(x) for x in value]} - elif isinstance(value, uuid.UUID): - return {' u': value.hex} - elif isinstance(value, bytes): - return {' b': b64encode(value).decode('ascii')} - elif callable(getattr(value, '__html__', None)): - return {' m': text_type(value.__html__())} - elif isinstance(value, list): - return [_tag(x) for x in value] - elif isinstance(value, datetime): - return {' d': http_date(value)} - elif isinstance(value, dict): - return dict((k, _tag(v)) for k, v in iteritems(value)) - elif isinstance(value, str): - try: - return text_type(value) - except UnicodeError: - from flask.debughelpers import UnexpectedUnicodeError - raise UnexpectedUnicodeError(u'A byte string with ' - u'non-ASCII data was passed to the session system ' - u'which can only store unicode strings. Consider ' - u'base64 encoding your string (String was %r)' % value) - return value - - -class TaggedJSONSerializer(object): - """A customized JSON serializer that supports a few extra types that - we take for granted when serializing (tuples, markup objects, datetime). - """ - def dumps(self, value): - return json.dumps(_tag(value), separators=(',', ':')) - - def loads(self, value): - def object_hook(obj): - if len(obj) != 1: - return obj - the_key, the_value = next(iteritems(obj)) - if the_key == ' t': - return tuple(the_value) - elif the_key == ' u': - return uuid.UUID(the_value) - elif the_key == ' b': - return b64decode(the_value) - elif the_key == ' m': - return Markup(the_value) - elif the_key == ' d': - return parse_date(the_value) - return obj - return json.loads(value, object_hook=object_hook) +class SecureCookieSession(CallbackDict, SessionMixin): + """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``. + """ -session_json_serializer = TaggedJSONSerializer() + #: 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 - -class SecureCookieSession(CallbackDict, SessionMixin): - """Base class for sessions based on signed cookies.""" + #: 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=None): def on_update(self): self.modified = True - CallbackDict.__init__(self, initial, on_update) - self.modified = False + self.accessed = True + + super(SecureCookieSession, self).__init__(initial, on_update) + + def __getitem__(self, key): + self.accessed = True + return super(SecureCookieSession, self).__getitem__(key) + + def get(self, key, default=None): + self.accessed = True + return super(SecureCookieSession, self).get(key, default) + + def setdefault(self, key, default=None): + self.accessed = True + return super(SecureCookieSession, self).setdefault(key, default) class NullSession(SecureCookieSession): @@ -168,7 +142,7 @@ class SessionInterface(object): 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 + #: This can be used by Flask extensions to make a decision in regards #: to how to deal with the session object. #: #: .. versionadded:: 0.10 @@ -196,30 +170,62 @@ class SessionInterface(object): return isinstance(obj, self.null_session_class) def get_cookie_domain(self, app): - """Helpful helper method that returns the cookie domain that should - be used for the session cookie if session cookies are used. + """Returns the domain that should be set for the session cookie. + + Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise + falls back to detecting the domain based on ``SERVER_NAME``. + + Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is + updated to avoid re-running the logic. """ - if app.config['SESSION_COOKIE_DOMAIN'] is not None: - return app.config['SESSION_COOKIE_DOMAIN'] - if app.config['SERVER_NAME'] is not None: - # chop off the port which is usually not supported by browsers - rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] - - # Google chrome does not like cookies set to .localhost, so - # we just go with no domain then. Flask documents anyways that - # cross domain cookies need a fully qualified domain name - if rv == '.localhost': - rv = None - - # If we infer the cookie domain from the server name we need - # to check if we are in a subpath. In that case we can't - # set a cross domain cookie. - if rv is not None: - path = self.get_cookie_path(app) - if path != '/': - rv = rv.lstrip('.') - - return rv + + rv = app.config['SESSION_COOKIE_DOMAIN'] + + # set explicitly, or cached from SERVER_NAME detection + # if False, return None + if rv is not None: + return rv if rv else None + + rv = app.config['SERVER_NAME'] + + # server name not set, cache False to return none next time + if not rv: + app.config['SESSION_COOKIE_DOMAIN'] = False + return None + + # chop off the port which is usually not supported by browsers + # remove any leading '.' since we'll add that later + rv = rv.rsplit(':', 1)[0].lstrip('.') + + if '.' not in rv: + # Chrome doesn't allow names without a '.' + # this should only come up with localhost + # hack around this by not setting the name, and show a warning + warnings.warn( + '"{rv}" is not a valid cookie domain, it must contain a ".".' + ' Add an entry to your hosts file, for example' + ' "{rv}.localdomain", and use that instead.'.format(rv=rv) + ) + app.config['SESSION_COOKIE_DOMAIN'] = False + return None + + ip = is_ip(rv) + + if ip: + warnings.warn( + 'The session cookie domain is an IP address. This may not work' + ' as intended in some browsers. Add an entry to your hosts' + ' file, for example "localhost.localdomain", and use that' + ' instead.' + ) + + # if this is not an ip and app is mounted at the root, allow subdomain + # matching by adding a '.' prefix + if self.get_cookie_path(app) == '/' and not ip: + rv = '.' + rv + + app.config['SESSION_COOKIE_DOMAIN'] = rv + return rv def get_cookie_path(self, app): """Returns the path for which the cookie should be valid. The @@ -227,8 +233,8 @@ class SessionInterface(object): 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'] or '/' + return app.config['SESSION_COOKIE_PATH'] \ + or app.config['APPLICATION_ROOT'] def get_cookie_httponly(self, app): """Returns True if the session cookie should be httponly. This @@ -243,6 +249,13 @@ class SessionInterface(object): """ return app.config['SESSION_COOKIE_SECURE'] + def get_cookie_samesite(self, app): + """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'] + def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session or ``None`` if the session is linked to the browser session. The @@ -253,22 +266,20 @@ class SessionInterface(object): return datetime.utcnow() + app.permanent_session_lifetime def should_set_cookie(self, app, session): - """Indicates whether a cookie should be set now or not. This is - used by session backends to figure out if they should emit a - set-cookie header or not. The default behavior is controlled by - the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If - it's set to ``False`` then a cookie is only set if the session is - modified, if set to ``True`` it's always set if the session is - permanent. + """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 sessions get deleted. + This check is usually skipped if the session was deleted. .. versionadded:: 0.11 """ - if session.modified: - return True - save_each = app.config['SESSION_REFRESH_EACH_REQUEST'] - return save_each and session.permanent + + return session.modified or ( + session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST'] + ) def open_session(self, app, request): """This method has to be implemented and must either return ``None`` @@ -287,6 +298,9 @@ class SessionInterface(object): raise NotImplementedError() +session_json_serializer = TaggedJSONSerializer() + + class SecureCookieSessionInterface(SessionInterface): """The default session interface that stores sessions in signed cookies through the :mod:`itsdangerous` module. @@ -334,29 +348,37 @@ class SecureCookieSessionInterface(SessionInterface): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) - # Delete case. If there is no session we bail early. - # If the session was modified to be empty we remove the - # whole 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(app.session_cookie_name, - domain=domain, path=path) + response.delete_cookie( + app.session_cookie_name, + domain=domain, + path=path + ) + return - # Modification case. There are upsides and downsides to - # emitting a set-cookie header each request. The behavior - # is controlled by the :meth:`should_set_cookie` method - # which performs a quick check to figure out if the cookie - # should be set or not. This is controlled by the - # SESSION_REFRESH_EACH_REQUEST config flag as well as - # the permanent flag on the session itself. + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: + response.vary.add('Cookie') + if not self.should_set_cookie(app, session): return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) + samesite = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) - response.set_cookie(app.session_cookie_name, val, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure) + response.set_cookie( + app.session_cookie_name, + val, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + samesite=samesite + ) diff --git a/flask/signals.py b/flask/signals.py index c9b8a210..dd52cdb5 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -37,7 +37,7 @@ except ImportError: temporarily_connected_to = connected_to = _fail del _fail -# The namespace for code signals. If you are not flask code, do +# The namespace for code signals. If you are not Flask code, do # not put signals in here. Create your own namespace instead. _signals = Namespace() diff --git a/flask/testing.py b/flask/testing.py index 31600245..730ad61d 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -14,26 +14,55 @@ import werkzeug from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +from flask.json import dumps as json_dumps +from werkzeug.urls import url_parse -try: - from werkzeug.urls import url_parse -except ImportError: - from urlparse import urlsplit as url_parse - -def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): +def make_test_environ_builder( + app, path='/', base_url=None, subdomain=None, url_scheme=None, + *args, **kwargs +): """Creates a new test builder with some application defaults thrown in.""" - http_host = app.config.get('SERVER_NAME') - app_root = app.config.get('APPLICATION_ROOT') + + 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 = '{0}.{1}'.format(subdomain, http_host) + + if url_scheme is None: + url_scheme = app.config['PREFERRED_URL_SCHEME'] + url = url_parse(path) - base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') - if app_root: - base_url += app_root.lstrip('/') - if url.netloc: - path = url.path - if url.query: - path += '?' + url.query + base_url = '{scheme}://{netloc}/{path}'.format( + scheme=url.scheme or url_scheme, + netloc=url.netloc or http_host, + path=app_root.lstrip('/') + ) + path = url.path + + if url.query: + sep = b'?' if isinstance(url.query, bytes) else '?' + path += sep + url.query + + if 'json' in kwargs: + assert 'data' not in kwargs, ( + "Client cannot provide both 'json' and 'data'." + ) + + # push a context so flask.json can use app's json attributes + with app.app_context(): + kwargs['data'] = json_dumps(kwargs.pop('json')) + + if 'content_type' not in kwargs: + kwargs['content_type'] = 'application/json' + return EnvironBuilder(path, base_url, *args, **kwargs) @@ -87,7 +116,8 @@ class FlaskClient(Client): self.cookie_jar.inject_wsgi(environ_overrides) outer_reqctx = _request_ctx_stack.top with app.test_request_context(*args, **kwargs) as c: - sess = app.open_session(c.request) + session_interface = app.session_interface + sess = session_interface.open_session(app, c.request) if sess is None: raise RuntimeError('Session backend did not open a session. ' 'Check the configuration') @@ -106,25 +136,47 @@ class FlaskClient(Client): _request_ctx_stack.pop() resp = app.response_class() - if not app.session_interface.is_null_session(sess): - app.save_session(sess, resp) + if not session_interface.is_null_session(sess): + session_interface.save_session(app, sess, resp) headers = resp.get_wsgi_headers(c.request.environ) self.cookie_jar.extract_wsgi(c.request.environ, headers) def open(self, *args, **kwargs): - kwargs.setdefault('environ_overrides', {}) \ - ['flask._preserve_context'] = self.preserve_context - kwargs.setdefault('environ_base', self.environ_base) - as_tuple = kwargs.pop('as_tuple', False) buffered = kwargs.pop('buffered', False) follow_redirects = kwargs.pop('follow_redirects', False) - builder = make_test_environ_builder(self.application, *args, **kwargs) - return Client.open(self, builder, - as_tuple=as_tuple, - buffered=buffered, - follow_redirects=follow_redirects) + if ( + not kwargs and len(args) == 1 + and isinstance(args[0], (EnvironBuilder, dict)) + ): + environ = self.environ_base.copy() + + if isinstance(args[0], EnvironBuilder): + environ.update(args[0].get_environ()) + else: + environ.update(args[0]) + + environ['flask._preserve_context'] = self.preserve_context + else: + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + kwargs.setdefault('environ_base', self.environ_base) + builder = make_test_environ_builder( + self.application, *args, **kwargs + ) + + try: + environ = builder.get_environ() + finally: + builder.close() + + return Client.open( + self, environ, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects + ) def __enter__(self): if self.preserve_context: diff --git a/flask/views.py b/flask/views.py index 83394c1f..b3027970 100644 --- a/flask/views.py +++ b/flask/views.py @@ -51,6 +51,9 @@ class View(object): #: A list of methods this view can handle. methods = None + #: Setting this disables or force-enables the automatic options handling. + provide_automatic_options = None + #: The canonical way to decorate class-based views is to decorate the #: return value of as_view(). However since this moves parts of the #: logic from the class declaration to the place where it's hooked @@ -99,37 +102,39 @@ class View(object): view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ view.methods = cls.methods + view.provide_automatic_options = cls.provide_automatic_options return view class MethodViewType(type): + """Metaclass for :class:`MethodView` that determines what methods the view + defines. + """ + + def __init__(cls, name, bases, d): + super(MethodViewType, cls).__init__(name, bases, d) - def __new__(cls, name, bases, d): - rv = type.__new__(cls, name, bases, d) if 'methods' not in d: - methods = set(rv.methods or []) - for key in d: - if key in http_method_funcs: + methods = set() + + for key in http_method_funcs: + if hasattr(cls, key): methods.add(key.upper()) - # If we have no method at all in there we don't want to - # add a method list. (This is for instance the case for - # the base class or another subclass of a base method view - # that does not introduce new methods). + + # If we have no method at all in there we don't want to add a + # method list. This is for instance the case for the base class + # or another subclass of a base method view that does not introduce + # new methods. if methods: - rv.methods = sorted(methods) - return rv + cls.methods = methods class MethodView(with_metaclass(MethodViewType, View)): - """Like a regular class-based view but that dispatches requests to - particular methods. For instance if you implement a method called - :meth:`get` it means it will respond to ``'GET'`` requests and - the :meth:`dispatch_request` implementation will automatically - forward your request to that. Also :attr:`options` is set for you - automatically:: + """A class-based view that dispatches request methods to the corresponding + class methods. For example, if you implement a ``get`` method, it will be + used to handle ``GET`` requests. :: class CounterAPI(MethodView): - def get(self): return session.get('counter', 0) @@ -139,11 +144,14 @@ class MethodView(with_metaclass(MethodViewType, View)): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ + def dispatch_request(self, *args, **kwargs): 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, 'Unimplemented method %r' % request.method return meth(*args, **kwargs) diff --git a/flask/wrappers.py b/flask/wrappers.py index d1d7ba7d..807059d0 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -8,24 +8,102 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ - -from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.exceptions import BadRequest +from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase + +from flask import json +from flask.globals import current_app + + +class JSONMixin(object): + """Common mixin for both request and response objects to provide JSON + parsing capabilities. + + .. versionadded:: 1.0 + """ + + _cached_json = Ellipsis + + @property + def is_json(self): + """Check if the mimetype indicates JSON data, either + :mimetype:`application/json` or :mimetype:`application/*+json`. + + .. versionadded:: 0.11 + """ + mt = self.mimetype + return ( + mt == 'application/json' + or (mt.startswith('application/')) and mt.endswith('+json') + ) + + @property + def json(self): + """This will contain the parsed JSON data if the mimetype indicates + JSON (:mimetype:`application/json`, see :meth:`is_json`), otherwise it + will be ``None``. + """ + return self.get_json() + + def _get_data_for_json(self, cache): + return self.get_data(cache=cache) + + def get_json(self, force=False, silent=False, cache=True): + """Parse and return the data as JSON. If the mimetype does not indicate + JSON (:mimetype:`application/json`, see :meth:`is_json`), this returns + ``None`` unless ``force`` is true. If parsing fails, + :meth:`on_json_loading_failed` is called and its return value is used + as the return value. + + :param force: Ignore the mimetype and always try to parse JSON. + :param silent: Silence parsing errors and return ``None`` instead. + :param cache: Store the parsed JSON to return for subsequent calls. + """ + if cache and self._cached_json is not Ellipsis: + return self._cached_json + + if not (force or self.is_json): + return None + + # We accept MIME charset against the specification as certain clients + # have used this in the past. For responses, we assume that if the + # charset is set then the data has been encoded correctly as well. + charset = self.mimetype_params.get('charset') + + try: + data = self._get_data_for_json(cache=cache) + rv = json.loads(data, encoding=charset) + except ValueError as e: + if silent: + rv = None + else: + rv = self.on_json_loading_failed(e) + + if cache: + self._cached_json = rv + + return rv -from . import json -from .globals import _request_ctx_stack + def on_json_loading_failed(self, e): + """Called if :meth:`get_json` parsing fails and isn't silenced. If + this method returns a value, it is used as the return value for + :meth:`get_json`. The default implementation raises a + :class:`BadRequest` exception. -_missing = object() + .. versionchanged:: 0.10 + Raise a :exc:`BadRequest` error instead of returning an error + message as JSON. If you want that behavior you can add it by + subclassing. + .. versionadded:: 0.8 + """ + if current_app is not None and current_app.debug: + raise BadRequest('Failed to decode JSON object: {0}'.format(e)) -def _get_data(req, cache): - getter = getattr(req, 'get_data', None) - if getter is not None: - return getter(cache=cache) - return req.data + raise BadRequest() -class Request(RequestBase): +class Request(RequestBase, JSONMixin): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. @@ -41,6 +119,10 @@ class Request(RequestBase): #: 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 = None @@ -55,16 +137,11 @@ class Request(RequestBase): #: something similar. routing_exception = None - # Switched by the request context until 1.0 to opt in deprecated - # module functionality. - _is_old_module = False - @property def max_content_length(self): """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" - ctx = _request_ctx_stack.top - if ctx is not None: - return ctx.app.config['MAX_CONTENT_LENGTH'] + if current_app: + return current_app.config['MAX_CONTENT_LENGTH'] @property def endpoint(self): @@ -76,123 +153,28 @@ class Request(RequestBase): if self.url_rule is not None: return self.url_rule.endpoint - @property - def module(self): - """The name of the current module if the request was dispatched - to an actual module. This is deprecated functionality, use blueprints - instead. - """ - from warnings import warn - warn(DeprecationWarning('modules were deprecated in favor of ' - 'blueprints. Use request.blueprint ' - 'instead.'), stacklevel=2) - if self._is_old_module: - return self.blueprint - @property def blueprint(self): """The name of the current blueprint""" if self.url_rule and '.' in self.url_rule.endpoint: return self.url_rule.endpoint.rsplit('.', 1)[0] - @property - def json(self): - """If the mimetype is :mimetype:`application/json` this will contain the - parsed JSON data. Otherwise this will be ``None``. - - The :meth:`get_json` method should be used instead. - """ - from warnings import warn - warn(DeprecationWarning('json is deprecated. ' - 'Use get_json() instead.'), stacklevel=2) - return self.get_json() - - @property - def is_json(self): - """Indicates if this request is JSON or not. By default a request - is considered to include JSON data if the mimetype is - :mimetype:`application/json` or :mimetype:`application/*+json`. - - .. versionadded:: 0.11 - """ - mt = self.mimetype - if mt == 'application/json': - return True - if mt.startswith('application/') and mt.endswith('+json'): - return True - return False - - def get_json(self, force=False, silent=False, cache=True): - """Parses the incoming JSON request data and returns it. By default - this function will return ``None`` if the mimetype is not - :mimetype:`application/json` but this can be overridden by the - ``force`` parameter. If parsing fails the - :meth:`on_json_loading_failed` method on the request object will be - invoked. - - :param force: if set to ``True`` the mimetype is ignored. - :param silent: if set to ``True`` this method will fail silently - and return ``None``. - :param cache: if set to ``True`` the parsed JSON data is remembered - on the request. - """ - rv = getattr(self, '_cached_json', _missing) - if rv is not _missing: - return rv - - if not (force or self.is_json): - return None - - # We accept a request charset against the specification as - # certain clients have been using this in the past. This - # fits our general approach of being nice in what we accept - # and strict in what we send out. - request_charset = self.mimetype_params.get('charset') - try: - data = _get_data(self, cache) - if request_charset is not None: - rv = json.loads(data, encoding=request_charset) - else: - rv = json.loads(data) - except ValueError as e: - if silent: - rv = None - else: - rv = self.on_json_loading_failed(e) - if cache: - self._cached_json = rv - return rv - - def on_json_loading_failed(self, e): - """Called if decoding of the JSON data failed. The return value of - this method is used by :meth:`get_json` when an error occurred. The - default implementation just raises a :class:`BadRequest` exception. - - .. versionchanged:: 0.10 - Removed buggy previous behavior of generating a random JSON - response. If you want that behavior back you can trivially - add it by subclassing. - - .. versionadded:: 0.8 - """ - ctx = _request_ctx_stack.top - if ctx is not None and ctx.app.config.get('DEBUG', False): - raise BadRequest('Failed to decode JSON object: {0}'.format(e)) - raise BadRequest() - def _load_form_data(self): RequestBase._load_form_data(self) # In debug mode we're replacing the files multidict with an ad-hoc # subclass that raises a different error for key errors. - ctx = _request_ctx_stack.top - if ctx is not None and ctx.app.debug and \ - self.mimetype != 'multipart/form-data' and not self.files: + 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) -class Response(ResponseBase): +class Response(ResponseBase, JSONMixin): """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 @@ -200,5 +182,13 @@ class Response(ResponseBase): 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. """ + default_mimetype = 'text/html' + + def _get_data_for_json(self, cache): + return self.get_data() diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py deleted file mode 100644 index 7fbdd49c..00000000 --- a/scripts/flask-07-upgrade.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - flask-07-upgrade - ~~~~~~~~~~~~~~~~ - - This command line script scans a whole application tree and attempts to - output an unified diff with all the changes that are necessary to easily - upgrade the application to 0.7 and to not yield deprecation warnings. - - This will also attempt to find `after_request` functions that don't modify - the response and appear to be better suited for `teardown_request`. - - This application is indeed an incredible hack, but because what it - attempts to accomplish is impossible to do statically it tries to support - the most common patterns at least. The diff it generates should be - hand reviewed and not applied blindly without making backups. - - :copyright: (c) Copyright 2015 by Armin Ronacher. - :license: see LICENSE for more details. -""" -from __future__ import print_function -import re -import os -import inspect -import difflib -import posixpath -from optparse import OptionParser - -try: - import ast -except ImportError: - ast = None - - -TEMPLATE_LOOKAHEAD = 4096 - -_app_re_part = r'((?:[a-zA-Z_][a-zA-Z0-9_]*app)|app|application)' -_string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ - r'|"([^"\\]*(?:\\.[^"\\]*)*)")' - -_from_import_re = re.compile(r'^\s*from flask import\s+') -_url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) -_render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part) -_after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') -_module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' - r'\(__name__\s*(?:,\s*(?:name\s*=\s*)?(%s))?' % - _string_re_part) -_error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part) -_mod_route_re = re.compile(r'@([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') -_blueprint_related = [ - (re.compile(r'request\.module'), 'request.blueprint'), - (re.compile(r'register_module'), 'register_blueprint'), - (re.compile(r'%s\.modules' % _app_re_part), '\\1.blueprints') -] - - -def make_diff(filename, old, new): - for line in difflib.unified_diff(old.splitlines(), new.splitlines(), - posixpath.normpath(posixpath.join('a', filename)), - posixpath.normpath(posixpath.join('b', filename)), - lineterm=''): - print(line) - - -def looks_like_teardown_function(node): - returns = [x for x in ast.walk(node) if isinstance(x, ast.Return)] - if len(returns) != 1: - return - return_def = returns[0] - resp_name = node.args.args[0] - if not isinstance(return_def.value, ast.Name) or \ - return_def.value.id != resp_name.id: - return - - for body_node in node.body: - for child in ast.walk(body_node): - if isinstance(child, ast.Name) and \ - child.id == resp_name.id: - if child is not return_def.value: - return - - return resp_name.id - - -def fix_url_for(contents, module_declarations=None): - if module_declarations is None: - skip_module_test = True - else: - skip_module_test = False - mapping = dict(module_declarations) - annotated_lines = [] - - def make_line_annotations(): - if not annotated_lines: - last_index = 0 - for line in contents.splitlines(True): - last_index += len(line) - annotated_lines.append((last_index, line)) - - def backtrack_module_name(call_start): - make_line_annotations() - for idx, (line_end, line) in enumerate(annotated_lines): - if line_end > call_start: - for _, line in reversed(annotated_lines[:idx]): - match = _mod_route_re.search(line) - if match is not None: - shortname = match.group(1) - return mapping.get(shortname) - - def handle_match(match): - if not skip_module_test: - modname = backtrack_module_name(match.start()) - if modname is None: - return match.group(0) - prefix = match.group(1) - endpoint = ast.literal_eval(match.group(2)) - if endpoint.startswith('.'): - endpoint = endpoint[1:] - elif '.' not in endpoint: - endpoint = '.' + endpoint - else: - return match.group(0) - return prefix + repr(endpoint) - return _url_for_re.sub(handle_match, contents) - - -def fix_teardown_funcs(contents): - - def is_return_line(line): - args = line.strip().split() - return args and args[0] == 'return' - - def fix_single(match, lines, lineno): - if not lines[lineno + 1].startswith('def'): - return - block_lines = inspect.getblock(lines[lineno + 1:]) - func_code = ''.join(block_lines) - if func_code[0].isspace(): - node = ast.parse('if 1:\n' + func_code).body[0].body - else: - node = ast.parse(func_code).body[0] - response_param_name = looks_like_teardown_function(node) - if response_param_name is None: - return - before = lines[:lineno] - decorator = [match.group(1) + - match.group(2).replace('after_', 'teardown_') + - match.group(3)] - body = [line.replace(response_param_name, 'exception') - for line in block_lines if - not is_return_line(line)] - after = lines[lineno + len(block_lines) + 1:] - return before + decorator + body + after - - content_lines = contents.splitlines(True) - while 1: - found_one = False - for idx, line in enumerate(content_lines): - match = _after_request_re.match(line) - if match is None: - continue - new_content_lines = fix_single(match, content_lines, idx) - if new_content_lines is not None: - content_lines = new_content_lines - break - else: - break - - return ''.join(content_lines) - - -def get_module_autoname(filename): - directory, filename = os.path.split(filename) - if filename != '__init__.py': - return os.path.splitext(filename)[0] - return os.path.basename(directory) - - -def rewrite_from_imports(prefix, fromlist, lineiter): - import_block = [prefix, fromlist] - if fromlist[0] == '(' and fromlist[-1] != ')': - for line in lineiter: - import_block.append(line) - if line.rstrip().endswith(')'): - break - elif fromlist[-1] == '\\': - for line in lineiter: - import_block.append(line) - if line.rstrip().endswith('\\'): - break - - return ''.join(import_block).replace('Module', 'Blueprint') - - -def rewrite_blueprint_imports(contents): - new_file = [] - lineiter = iter(contents.splitlines(True)) - for line in lineiter: - match = _from_import_re.search(line) - if match is not None: - new_file.extend(rewrite_from_imports(match.group(), - line[match.end():], - lineiter)) - else: - new_file.append(line) - return ''.join(new_file) - - -def rewrite_for_blueprints(contents, filename): - modules_declared = [] - def handle_match(match): - target = match.group(1) - name_param = match.group(2) - if name_param is None: - modname = get_module_autoname(filename) - else: - modname = ast.literal_eval(name_param) - modules_declared.append((target, modname)) - return '%s = %s' % (target, 'Blueprint(%r, __name__' % modname) - new_contents = _module_constructor_re.sub(handle_match, contents) - - if modules_declared: - new_contents = rewrite_blueprint_imports(new_contents) - - for pattern, replacement in _blueprint_related: - new_contents = pattern.sub(replacement, new_contents) - return new_contents, dict(modules_declared) - - -def upgrade_python_file(filename, contents, teardown): - new_contents = contents - if teardown: - new_contents = fix_teardown_funcs(new_contents) - new_contents, modules = rewrite_for_blueprints(new_contents, filename) - new_contents = fix_url_for(new_contents, modules) - new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]', - new_contents) - make_diff(filename, contents, new_contents) - - -def upgrade_template_file(filename, contents): - new_contents = fix_url_for(contents, None) - make_diff(filename, contents, new_contents) - - -def walk_path(path): - this_file = os.path.realpath(__file__).rstrip('c') - for dirpath, dirnames, filenames in os.walk(path): - dirnames[:] = [x for x in dirnames if not x.startswith('.')] - for filename in filenames: - filename = os.path.join(dirpath, filename) - if os.path.realpath(filename) == this_file: - continue - if filename.endswith('.py'): - yield filename, 'python' - # skip files that are diffs. These might be false positives - # when run multiple times. - elif not filename.endswith(('.diff', '.patch', '.udiff')): - with open(filename) as f: - contents = f.read(TEMPLATE_LOOKAHEAD) - if '{% for' or '{% if' or '{{ url_for' in contents: - yield filename, 'template' - - -def scan_path(path=None, teardown=True): - for filename, type in walk_path(path): - with open(filename) as f: - contents = f.read() - if type == 'python': - upgrade_python_file(filename, contents, teardown) - elif type == 'template': - upgrade_template_file(filename, contents) - - -def main(): - """Entrypoint""" - parser = OptionParser(usage='%prog [options] [paths]') - parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', - action='store_true', help='Do not attempt to ' - 'detect teardown function rewrites.') - parser.add_option('-b', '--bundled-templates', dest='bundled_tmpl', - action='store_true', help='Indicate to the system ' - 'that templates are bundled with modules. Default ' - 'is auto detect.') - options, args = parser.parse_args() - if not args: - args = ['.'] - - if ast is None: - parser.error('Python 2.6 or later is required to run the upgrade script.') - - for path in args: - scan_path(path, teardown=not options.no_teardown) - - -if __name__ == '__main__': - main() diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py deleted file mode 100644 index 77d38c20..00000000 --- a/scripts/flaskext_compat.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flaskext_compat - ~~~~~~~~~~~~~~~ - - Implements the ``flask.ext`` virtual package for versions of Flask - older than 0.7. This module is a noop if Flask 0.8 was detected. - - Usage:: - - import flaskext_compat - flaskext_compat.activate() - from flask.ext import foo - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -import types -import sys -import os - - -class ExtensionImporter(object): - """This importer redirects imports from this submodule to other locations. - This makes it possible to transition from the old flaskext.name to the - newer flask_name without people having a hard time. - """ - - def __init__(self, module_choices, wrapper_module): - self.module_choices = module_choices - self.wrapper_module = wrapper_module - self.prefix = wrapper_module + '.' - self.prefix_cutoff = wrapper_module.count('.') + 1 - - def __eq__(self, other): - return self.__class__.__module__ == other.__class__.__module__ and \ - self.__class__.__name__ == other.__class__.__name__ and \ - self.wrapper_module == other.wrapper_module and \ - self.module_choices == other.module_choices - - def __ne__(self, other): - return not self.__eq__(other) - - def install(self): - sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] - - def find_module(self, fullname, path=None): - if fullname.startswith(self.prefix): - return self - - def load_module(self, fullname): - if fullname in sys.modules: - return sys.modules[fullname] - modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] - for path in self.module_choices: - realname = path % modname - try: - __import__(realname) - except ImportError: - exc_type, exc_value, tb = sys.exc_info() - # since we only establish the entry in sys.modules at the - # end this seems to be redundant, but if recursive imports - # happen we will call into the move import a second time. - # On the second invocation we still don't have an entry for - # fullname in sys.modules, but we will end up with the same - # fake module name and that import will succeed since this - # one already has a temporary entry in the modules dict. - # Since this one "succeeded" temporarily that second - # invocation now will have created a fullname entry in - # sys.modules which we have to kill. - sys.modules.pop(fullname, None) - - # If it's an important traceback we reraise it, otherwise - # we swallow it and try the next choice. The skipped frame - # is the one from __import__ above which we don't care about. - if self.is_important_traceback(realname, tb): - raise exc_type, exc_value, tb.tb_next - continue - module = sys.modules[fullname] = sys.modules[realname] - if '.' not in modname: - setattr(sys.modules[self.wrapper_module], modname, module) - return module - raise ImportError('No module named %s' % fullname) - - def is_important_traceback(self, important_module, tb): - """Walks a traceback's frames and checks if any of the frames - originated in the given important module. If that is the case then we - were able to import the module itself but apparently something went - wrong when the module was imported. (Eg: import of an import failed). - """ - while tb is not None: - if self.is_important_frame(important_module, tb): - return True - tb = tb.tb_next - return False - - def is_important_frame(self, important_module, tb): - """Checks a single frame if it's important.""" - g = tb.tb_frame.f_globals - if '__name__' not in g: - return False - - module_name = g['__name__'] - - # Python 2.7 Behavior. Modules are cleaned up late so the - # name shows up properly here. Success! - if module_name == important_module: - return True - - # Some python versions will clean up modules so early that the - # module name at that point is no longer set. Try guessing from - # the filename then. - filename = os.path.abspath(tb.tb_frame.f_code.co_filename) - test_string = os.path.sep + important_module.replace('.', os.path.sep) - return test_string + '.py' in filename or \ - test_string + os.path.sep + '__init__.py' in filename - - -def activate(): - import flask - ext_module = types.ModuleType('flask.ext') - ext_module.__path__ = [] - flask.ext = sys.modules['flask.ext'] = ext_module - importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], 'flask.ext') - importer.install() diff --git a/scripts/make-release.py b/scripts/make-release.py index fc6421ab..7fb42787 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -21,7 +21,7 @@ _date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)') def parse_changelog(): - with open('CHANGES') as f: + with open('CHANGES.rst') as f: lineiter = iter(f) for line in lineiter: match = re.search('^Version\s+(.*)', line.strip()) diff --git a/setup.cfg b/setup.cfg index 34414b3e..527dd3e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,12 @@ [aliases] release = egg_info -RDb '' -[wheel] +[bdist_wheel] universal = 1 +[metadata] +license_file = LICENSE + [tool:pytest] -norecursedirs = .* *.egg *.egg-info env* artwork docs examples +minversion = 3.0 +testpaths = tests diff --git a/setup.py b/setup.py index 983f7611..2ece939a 100644 --- a/setup.py +++ b/setup.py @@ -31,9 +31,9 @@ And run it: $ pip install Flask $ python hello.py - * Running on http://localhost:5000/ + * Running on http://localhost:5000/ - Ready for production? `Read this first `. +Ready for production? `Read this first `. Links ````` @@ -41,60 +41,71 @@ Links * `website `_ * `documentation `_ * `development version - `_ + `_ """ import re import ast from setuptools import setup - _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('flask/__init__.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) - setup( name='Flask', version=version, - url='http://github.com/pallets/flask/', + url='https://github.com/pallets/flask/', license='BSD', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - packages=['flask', 'flask.ext'], + packages=['flask', 'flask.json'], include_package_data=True, zip_safe=False, platforms='any', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', install_requires=[ - 'Werkzeug>=0.7', - 'Jinja2>=2.4', - 'itsdangerous>=0.21', - 'click>=2.0', + 'Werkzeug>=0.14', + 'Jinja2>=2.10', + 'itsdangerous>=0.24', + 'click>=5.1', ], + extras_require={ + 'dotenv': ['python-dotenv'], + 'dev': [ + 'pytest>=3', + 'coverage', + 'tox', + 'sphinx', + ], + }, classifiers=[ - 'Development Status :: 4 - Beta', + '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', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Software Development :: Libraries :: Python Modules', ], - entry_points=''' - [console_scripts] - flask=flask.cli:main - ''' + entry_points={ + 'console_scripts': [ + 'flask = flask.cli:main', + ], + }, ) diff --git a/test-requirements.txt b/test-requirements.txt index e079f8a6..edf1abb9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,3 @@ +tox pytest +pytest-cov \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8c9541de..486d4b0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,14 +6,79 @@ :copyright: (c) 2015 by the Flask Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -import flask import gc import os -import sys import pkgutil -import pytest +import sys import textwrap +import pytest +from _pytest import monkeypatch + +import flask +from flask import Flask as _Flask + + +@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_APP', monkeypatch.notset), + (os.environ, 'FLASK_ENV', 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) + + +class Flask(_Flask): + testing = True + secret_key = 'test key' + + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +@pytest.fixture +def app_ctx(app): + with app.app_context() as ctx: + yield ctx + + +@pytest.fixture +def req_ctx(app): + with app.test_request_context() as ctx: + yield ctx + + +@pytest.fixture +def client(app): + return app.test_client() + @pytest.fixture def test_apps(monkeypatch): @@ -22,16 +87,17 @@ def test_apps(monkeypatch): os.path.dirname(__file__), 'test_apps')) ) + @pytest.fixture(autouse=True) -def leak_detector(request): - def ensure_clean_request_context(): - # make sure we're not leaking a request context since we are - # testing flask internally in debug mode in a few cases - leaks = [] - while flask._request_ctx_stack.top is not None: - leaks.append(flask._request_ctx_stack.pop()) - assert leaks == [] - request.addfinalizer(ensure_clean_request_context) +def leak_detector(): + yield + + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + leaks = [] + while flask._request_ctx_stack.top is not None: + leaks.append(flask._request_ctx_stack.pop()) + assert leaks == [] @pytest.fixture(params=(True, False)) @@ -62,12 +128,13 @@ def limit_loader(request, monkeypatch): def get_loader(*args, **kwargs): return LimitedLoader(old_get_loader(*args, **kwargs)) + monkeypatch.setattr(pkgutil, 'get_loader', get_loader) @pytest.fixture def modules_tmpdir(tmpdir, monkeypatch): - '''A tmpdir added to sys.path''' + """A tmpdir added to sys.path.""" rv = tmpdir.mkdir('modules_tmpdir') monkeypatch.syspath_prepend(str(rv)) return rv @@ -81,10 +148,10 @@ def modules_tmpdir_prefix(modules_tmpdir, monkeypatch): @pytest.fixture def site_packages(modules_tmpdir, monkeypatch): - '''Create a fake site-packages''' + """Create a fake site-packages.""" rv = modules_tmpdir \ - .mkdir('lib')\ - .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info))\ + .mkdir('lib') \ + .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info)) \ .mkdir('site-packages') monkeypatch.syspath_prepend(str(rv)) return rv @@ -92,8 +159,9 @@ def site_packages(modules_tmpdir, monkeypatch): @pytest.fixture def install_egg(modules_tmpdir, monkeypatch): - '''Generate egg from package name inside base and put the egg into - sys.path''' + """Generate egg from package name inside base and put the egg into + sys.path.""" + def inner(name, base=modules_tmpdir): if not isinstance(name, str): raise ValueError(name) @@ -117,6 +185,7 @@ def install_egg(modules_tmpdir, monkeypatch): egg_path, = modules_tmpdir.join('dist/').listdir() monkeypatch.syspath_prepend(str(egg_path)) return egg_path + return inner @@ -124,11 +193,12 @@ def install_egg(modules_tmpdir, monkeypatch): def purge_module(request): def inner(name): request.addfinalizer(lambda: sys.modules.pop(name, None)) + return inner -@pytest.yield_fixture(autouse=True) +@pytest.fixture(autouse=True) def catch_deprecation_warnings(recwarn): yield gc.collect() - assert not recwarn.list + assert not recwarn.list, '\n'.join(str(w.message) for w in recwarn.list) diff --git a/tests/static/config.json b/tests/static/config.json index 4a9722ec..4eedab12 100644 --- a/tests/static/config.json +++ b/tests/static/config.json @@ -1,4 +1,4 @@ { "TEST_KEY": "foo", - "SECRET_KEY": "devkey" + "SECRET_KEY": "config" } diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 13b61eee..fc2f6b13 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -14,8 +14,7 @@ import pytest import flask -def test_basic_url_generation(): - app = flask.Flask(__name__) +def test_basic_url_generation(app): app.config['SERVER_NAME'] = 'localhost' app.config['PREFERRED_URL_SCHEME'] = 'https' @@ -27,31 +26,33 @@ def test_basic_url_generation(): rv = flask.url_for('index') assert rv == 'https://localhost/' -def test_url_generation_requires_server_name(): - app = flask.Flask(__name__) + +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 = flask.Flask(__name__) + +def test_request_context_means_app_context(app): with app.test_request_context(): assert flask.current_app._get_current_object() == app assert flask._app_ctx_stack.top is None -def test_app_context_provides_current_app(): - app = flask.Flask(__name__) + +def test_app_context_provides_current_app(app): with app.app_context(): assert flask.current_app._get_current_object() == app assert flask._app_ctx_stack.top is None -def test_app_tearing_down(): + +def test_app_tearing_down(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -61,9 +62,10 @@ def test_app_tearing_down(): assert cleanup_stuff == [None] -def test_app_tearing_down_with_previous_exception(): + +def test_app_tearing_down_with_previous_exception(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -78,9 +80,10 @@ def test_app_tearing_down_with_previous_exception(): assert cleanup_stuff == [None] -def test_app_tearing_down_with_handled_exception(): + +def test_app_tearing_down_with_handled_exception_by_except_block(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -93,46 +96,92 @@ def test_app_tearing_down_with_handled_exception(): assert cleanup_stuff == [None] -def test_app_ctx_globals_methods(): - app = flask.Flask(__name__) + +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(): - # 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'] - -def test_custom_app_ctx_globals_class(): + 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 Exception('dummy') + + with pytest.raises(Exception): + with app.app_context(): + client.get('/') + + assert len(cleanup_stuff) == 1 + assert isinstance(cleanup_stuff[0], Exception) + 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'] + + +def test_custom_app_ctx_globals_class(app): class CustomRequestGlobals(object): def __init__(self): self.spam = 'eggs' - app = flask.Flask(__name__) + app.app_ctx_globals_class = CustomRequestGlobals with app.app_context(): assert flask.render_template_string('{{ g.spam }}') == 'eggs' -def test_context_refcounts(): + +def test_context_refcounts(app, client): called = [] - app = flask.Flask(__name__) + @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 flask._app_ctx_stack.top: @@ -141,16 +190,16 @@ def test_context_refcounts(): env = flask._request_ctx_stack.top.request.environ assert env['werkzeug.request'] is not None return u'' - c = app.test_client() - res = c.get('/') + + res = client.get('/') assert res.status_code == 200 assert res.data == b'' assert called == ['request', 'app'] -def test_clean_pop(): +def test_clean_pop(app): + app.testing = False called = [] - app = flask.Flask(__name__) @app.teardown_request def teardown_req(error=None): @@ -166,5 +215,5 @@ def test_clean_pop(): except ZeroDivisionError: pass - assert called == ['test_appctx', 'TEARDOWN'] + assert called == ['conftest', 'TEARDOWN'] assert not flask.current_app diff --git a/tests/test_apps/.env b/tests/test_apps/.env new file mode 100644 index 00000000..13ac3483 --- /dev/null +++ b/tests/test_apps/.env @@ -0,0 +1,3 @@ +FOO=env +SPAM=1 +EGGS=2 diff --git a/tests/test_apps/.flaskenv b/tests/test_apps/.flaskenv new file mode 100644 index 00000000..59f96af7 --- /dev/null +++ b/tests/test_apps/.flaskenv @@ -0,0 +1,3 @@ +FOO=flaskenv +BAR=bar +EGGS=0 diff --git a/tests/test_apps/cliapp/factory.py b/tests/test_apps/cliapp/factory.py new file mode 100644 index 00000000..95d60396 --- /dev/null +++ b/tests/test_apps/cliapp/factory.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + + +def create_app(): + return Flask('app') + + +def create_app2(foo, bar): + return Flask('_'.join(['app2', foo, bar])) + + +def create_app3(foo, script_info): + return Flask('_'.join(['app3', foo, script_info.data['test']])) + + +def no_app(): + pass diff --git a/tests/test_apps/cliapp/importerrorapp.py b/tests/test_apps/cliapp/importerrorapp.py new file mode 100644 index 00000000..fb87c9b1 --- /dev/null +++ b/tests/test_apps/cliapp/importerrorapp.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, print_function + +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 new file mode 100644 index 00000000..8330f6e0 --- /dev/null +++ b/tests/test_apps/cliapp/inner1/__init__.py @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_apps/cliapp/inner1/inner2/flask.py b/tests/test_apps/cliapp/inner1/inner2/flask.py new file mode 100644 index 00000000..d7562aac --- /dev/null +++ b/tests/test_apps/cliapp/inner1/inner2/flask.py @@ -0,0 +1,3 @@ +from flask import Flask + +app = Flask(__name__) diff --git a/tests/test_apps/cliapp/message.txt b/tests/test_apps/cliapp/message.txt new file mode 100644 index 00000000..fc2b2cf0 --- /dev/null +++ b/tests/test_apps/cliapp/message.txt @@ -0,0 +1 @@ +So long, and thanks for all the fish. diff --git a/tests/test_apps/helloworld/hello.py b/tests/test_apps/helloworld/hello.py new file mode 100644 index 00000000..bbf7e467 --- /dev/null +++ b/tests/test_apps/helloworld/hello.py @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..fab4048b --- /dev/null +++ b/tests/test_apps/helloworld/wsgi.py @@ -0,0 +1 @@ +from hello import app diff --git a/tests/test_basic.py b/tests/test_basic.py index be3d5edd..0e55b52e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -9,36 +9,33 @@ :license: BSD, see LICENSE for more details. """ -import pytest - import re -import uuid import time -import flask -import pickle +import uuid from datetime import datetime from threading import Thread -from flask._compat import text_type -from werkzeug.exceptions import BadRequest, NotFound, Forbidden + +import pytest +import werkzeug.serving +from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError -import werkzeug.serving +import flask +from flask._compat import text_type -def test_options_work(): - app = flask.Flask(__name__) +def test_options_work(app, client): @app.route('/', methods=['GET', 'POST']) def index(): return 'Hello World' - rv = app.test_client().open('/', method='OPTIONS') + + rv = client.open('/', method='OPTIONS') assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] assert rv.data == b'' -def test_options_on_multiple_rules(): - app = flask.Flask(__name__) - +def test_options_on_multiple_rules(app, client): @app.route('/', methods=['GET', 'POST']) def index(): return 'Hello World' @@ -46,15 +43,17 @@ def test_options_on_multiple_rules(): @app.route('/', methods=['PUT']) def index_put(): return 'Aha!' - rv = app.test_client().open('/', method='OPTIONS') + + rv = client.open('/', method='OPTIONS') assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] -def test_options_handling_disabled(): +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') @@ -64,15 +63,58 @@ def test_options_handling_disabled(): 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_request_dispatching(): - app = flask.Flask(__name__) +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'] + + # Older versions of Werkzeug.test.Client don't have an options method + if hasattr(client, 'options'): + rv = client.options('/') + else: + 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'] + + if hasattr(client, 'options'): + rv = client.options('/more') + else: + 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 @@ -81,32 +123,28 @@ def test_request_dispatching(): def more(): return flask.request.method - c = app.test_client() - assert c.get('/').data == b'GET' - rv = c.post('/') + assert client.get('/').data == b'GET' + rv = client.post('/') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') + rv = client.head('/') assert rv.status_code == 200 assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' - rv = c.delete('/more') + 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 = flask.Flask(__name__) +def test_disallow_string_for_allowed_methods(app): with pytest.raises(TypeError): @app.route('/', methods='GET POST') def index(): return "Hey" -def test_url_mapping(): - app = flask.Flask(__name__) - +def test_url_mapping(app, client): random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" def index(): @@ -118,34 +156,31 @@ def test_url_mapping(): 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']) - c = app.test_client() - assert c.get('/').data == b'GET' - rv = c.post('/') + assert client.get('/').data == b'GET' + rv = client.post('/') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') + rv = client.head('/') assert rv.status_code == 200 assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' - rv = c.delete('/more') + 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 = c.open('/options', method='OPTIONS') + rv = client.open('/options', method='OPTIONS') assert rv.status_code == 200 assert random_uuid4 in rv.data.decode("utf-8") -def test_werkzeug_routing(): +def test_werkzeug_routing(app, client): from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index') @@ -156,17 +191,16 @@ def test_werkzeug_routing(): def index(): return 'index' + app.view_functions['bar'] = bar app.view_functions['index'] = index - c = app.test_client() - assert c.get('/foo/').data == b'index' - assert c.get('/foo/bar').data == b'bar' + assert client.get('/foo/').data == b'index' + assert client.get('/foo/bar').data == b'bar' -def test_endpoint_decorator(): +def test_endpoint_decorator(app, client): from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index') @@ -180,33 +214,35 @@ def test_endpoint_decorator(): def index(): return 'index' - c = app.test_client() - assert c.get('/foo/').data == b'index' - assert c.get('/foo/bar').data == b'bar' + assert client.get('/foo/').data == b'index' + assert client.get('/foo/bar').data == b'bar' -def test_session(): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - +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(): - return flask.session['value'] + 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 - c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == b'value set' - assert c.get('/get').data == b'42' + assert client.post('/set', data={'value': '42'}).data == b'value set' + assert client.get('/get').data == b'42' -def test_session_using_server_name(): - app = flask.Flask(__name__) +def test_session_using_server_name(app, client): app.config.update( - SECRET_KEY='foo', SERVER_NAME='example.com' ) @@ -214,15 +250,14 @@ def test_session_using_server_name(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com/') + + rv = client.get('/', 'http://example.com/') assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_server_name_and_port(): - app = flask.Flask(__name__) +def test_session_using_server_name_and_port(app, client): app.config.update( - SECRET_KEY='foo', SERVER_NAME='example.com:8080' ) @@ -230,15 +265,14 @@ def test_session_using_server_name_and_port(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') + + rv = client.get('/', 'http://example.com:8080/') assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_server_name_port_and_path(): - app = flask.Flask(__name__) +def test_session_using_server_name_port_and_path(app, client): app.config.update( - SECRET_KEY='foo', SERVER_NAME='example.com:8080', APPLICATION_ROOT='/foo' ) @@ -247,15 +281,15 @@ def test_session_using_server_name_port_and_path(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/foo') + + rv = client.get('/', 'http://example.com:8080/foo') assert 'domain=example.com' in rv.headers['set-cookie'].lower() assert 'path=/foo' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_application_root(): +def test_session_using_application_root(app, client): class PrefixPathMiddleware(object): - def __init__(self, app, prefix): self.app = app self.prefix = prefix @@ -264,10 +298,8 @@ def test_session_using_application_root(): environ['SCRIPT_NAME'] = self.prefix return self.app(environ, start_response) - app = flask.Flask(__name__) app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') app.config.update( - SECRET_KEY='foo', APPLICATION_ROOT='/bar' ) @@ -275,19 +307,19 @@ def test_session_using_application_root(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') + + rv = client.get('/', 'http://example.com:8080/') assert 'path=/bar' in rv.headers['set-cookie'].lower() -def test_session_using_session_settings(): - app = flask.Flask(__name__) +def test_session_using_session_settings(app, client): app.config.update( - SECRET_KEY='foo', 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='/' ) @@ -295,30 +327,90 @@ def test_session_using_session_settings(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + + rv = client.get('/', 'http://www.example.com:8080/test/') cookie = rv.headers['set-cookie'].lower() assert 'domain=.example.com' in cookie assert 'path=/' in cookie assert 'secure' in cookie assert 'httponly' not in cookie + assert 'samesite' in cookie -def test_missing_session(): - app = flask.Flask(__name__) +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_session_localhost_warning(recwarn, app, client): + app.config.update( + SERVER_NAME='localhost:5000', + ) + + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'testing' + + rv = client.get('/', 'http://localhost:5000/') + assert 'domain' not in rv.headers['set-cookie'].lower() + w = recwarn.pop(UserWarning) + assert '"localhost" is not a valid cookie domain' in str(w.message) + + +def test_session_ip_warning(recwarn, app, client): + app.config.update( + SERVER_NAME='127.0.0.1:5000', + ) + + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'testing' + + rv = client.get('/', 'http://127.0.0.1:5000/') + assert 'domain=127.0.0.1' in rv.headers['set-cookie'].lower() + w = recwarn.pop(UserWarning) + assert 'cookie domain is an IP' in str(w.message) + + +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(): +def test_session_expiration(app, client): permanent = True - app = flask.Flask(__name__) - app.secret_key = 'testkey' @app.route('/') def index(): @@ -330,10 +422,9 @@ def test_session_expiration(): def test(): return text_type(flask.session.permanent) - client = app.test_client() rv = client.get('/') assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)(?i)', rv.headers['set-cookie']) + match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime assert expires.year == expected.year @@ -344,17 +435,13 @@ def test_session_expiration(): assert rv.data == b'True' permanent = False - rv = app.test_client().get('/') + 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 = flask.Flask(__name__) - app.secret_key = 'development-key' - app.testing = True - +def test_session_stored_last(app, client): @app.after_request def modify_session(response): flask.session['foo'] = 42 @@ -364,47 +451,42 @@ def test_session_stored_last(): def dump_session_contents(): return repr(flask.session.get('foo')) - c = app.test_client() - assert c.get('/').data == b'None' - assert c.get('/').data == b'42' + assert client.get('/').data == b'None' + assert client.get('/').data == b'42' -def test_session_special_types(): - app = flask.Flask(__name__) - app.secret_key = 'development-key' - app.testing = True +def test_session_special_types(app, client): now = datetime.utcnow().replace(microsecond=0) the_uuid = uuid.uuid4() - @app.after_request - def modify_session(response): - flask.session['m'] = flask.Markup('Hello!') - flask.session['u'] = the_uuid - flask.session['dt'] = now - flask.session['b'] = b'\xff' - flask.session['t'] = (1, 2, 3) - return response - @app.route('/') def dump_session_contents(): - return pickle.dumps(dict(flask.session)) - - c = app.test_client() - c.get('/') - rv = pickle.loads(c.get('/').data) - assert rv['m'] == flask.Markup('Hello!') - assert type(rv['m']) == flask.Markup - assert rv['dt'] == now - assert rv['u'] == the_uuid - assert rv['b'] == b'\xff' - assert type(rv['b']) == bytes - assert rv['t'] == (1, 2, 3) - - -def test_session_cookie_setting(): - app = flask.Flask(__name__) - app.testing = True - app.secret_key = 'dev key' + flask.session['t'] = (1, 2, 3) + flask.session['b'] = b'\xff' + flask.session['m'] = flask.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']) == bytes + assert s['b'] == b'\xff' + assert type(s['m']) == flask.Markup + assert s['m'] == flask.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') @@ -445,30 +527,77 @@ def test_session_cookie_setting(): run_test(expect_header=False) -def test_flashes(): - app = flask.Flask(__name__) - app.secret_key = 'testkey' +def test_session_vary_cookie(app, client): + @app.route('/set') + def set_session(): + flask.session['test'] = 'test' + return '' - with app.test_request_context(): - 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'] + @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('/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('/vary-cookie-header-set') + expect('/vary-header-set', 'Accept-Encoding, Accept-Language, Cookie') + expect('/no-vary-header', None) + + +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(): +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 = flask.Flask(__name__) - app.secret_key = 'testkey' - app.testing = True - @app.route('/') def index(): flask.flash(u'Hello World') @@ -525,29 +654,24 @@ def test_extended_flashing(): # Create new test client on each test to clean flashed messages. - c = app.test_client() - c.get('/') - c.get('/test/') - - c = app.test_client() - c.get('/') - c.get('/test_with_categories/') + client = app.test_client() + client.get('/') + client.get('/test_with_categories/') - c = app.test_client() - c.get('/') - c.get('/test_filter/') + client = app.test_client() + client.get('/') + client.get('/test_filter/') - c = app.test_client() - c.get('/') - c.get('/test_filters/') + client = app.test_client() + client.get('/') + client.get('/test_filters/') - c = app.test_client() - c.get('/') - c.get('/test_filters_without_returning_categories/') + client = app.test_client() + client.get('/') + client.get('/test_filters_without_returning_categories/') -def test_request_processing(): - app = flask.Flask(__name__) +def test_request_processing(app, client): evts = [] @app.before_request @@ -565,14 +689,14 @@ def test_request_processing(): assert 'before' in evts assert 'after' not in evts return 'request' + assert 'after' not in evts - rv = app.test_client().get('/').data + rv = client.get('/').data assert 'after' in evts assert rv == b'request|after' -def test_request_preprocessing_early_return(): - app = flask.Flask(__name__) +def test_request_preprocessing_early_return(app, client): evts = [] @app.before_request @@ -594,31 +718,28 @@ def test_request_preprocessing_early_return(): evts.append('index') return "damnit" - rv = app.test_client().get('/').data.strip() + rv = client.get('/').data.strip() assert rv == b'hello' assert evts == [1, 2] -def test_after_request_processing(): - app = flask.Flask(__name__) - app.testing = True - +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' - c = app.test_client() - resp = c.get('/') + + resp = client.get('/') assert resp.status_code == 200 assert resp.headers['X-Foo'] == 'a header' -def test_teardown_request_handler(): +def test_teardown_request_handler(app, client): called = [] - app = flask.Flask(__name__) @app.teardown_request def teardown_request(exc): @@ -628,16 +749,15 @@ def test_teardown_request_handler(): @app.route('/') def root(): return "Response" - rv = app.test_client().get('/') + + 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(): +def test_teardown_request_handler_debug_mode(app, client): called = [] - app = flask.Flask(__name__) - app.testing = True @app.teardown_request def teardown_request(exc): @@ -647,16 +767,16 @@ def test_teardown_request_handler_debug_mode(): @app.route('/') def root(): return "Response" - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 200 assert b'Response' in rv.data assert len(called) == 1 -def test_teardown_request_handler_error(): +def test_teardown_request_handler_error(app, client): called = [] - app = flask.Flask(__name__) - app.config['LOGGER_HANDLER_POLICY'] = 'never' + app.testing = False @app.teardown_request def teardown_request1(exc): @@ -685,15 +805,15 @@ def test_teardown_request_handler_error(): @app.route('/') def fails(): 1 // 0 - rv = app.test_client().get('/') + + 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(): +def test_before_after_request_order(app, client): called = [] - app = flask.Flask(__name__) @app.before_request def before1(): @@ -724,14 +844,14 @@ def test_before_after_request_order(): @app.route('/') def index(): return '42' - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'42' assert called == [1, 2, 3, 4, 5, 6] -def test_error_handling(): - app = flask.Flask(__name__) - app.config['LOGGER_HANDLER_POLICY'] = 'never' +def test_error_handling(app, client): + app.testing = False @app.errorhandler(404) def not_found(e): @@ -756,21 +876,27 @@ def test_error_handling(): @app.route('/forbidden') def error2(): flask.abort(403) - c = app.test_client() - rv = c.get('/') + + rv = client.get('/') assert rv.status_code == 404 assert rv.data == b'not found' - rv = c.get('/error') + rv = client.get('/error') assert rv.status_code == 500 assert b'internal server error' == rv.data - rv = c.get('/forbidden') + rv = client.get('/forbidden') assert rv.status_code == 403 assert b'forbidden' == rv.data -def test_error_handling_processing(): - app = flask.Flask(__name__) - app.config['LOGGER_HANDLER_POLICY'] = 'never' +def test_error_handler_unknown_code(app): + with pytest.raises(KeyError) as exc_info: + app.register_error_handler(999, lambda e: ('999', 999)) + + assert 'Use a subclass' in exc_info.value.args[0] + + +def test_error_handling_processing(app, client): + app.testing = False @app.errorhandler(500) def internal_server_error(e): @@ -785,15 +911,27 @@ def test_error_handling_processing(): resp.mimetype = 'text/x-special' return resp - with app.test_client() as c: - resp = c.get('/') - assert resp.mimetype == 'text/x-special' - assert resp.data == b'internal server error' + resp = client.get('/') + assert resp.mimetype == 'text/x-special' + assert resp.data == b'internal server error' -def test_before_request_and_routing_errors(): - app = flask.Flask(__name__) +def test_baseexception_error_handling(app, client): + app.testing = False + + @app.route('/') + def broken_func(): + raise KeyboardInterrupt() + + with pytest.raises(KeyboardInterrupt): + client.get('/') + ctx = flask._request_ctx_stack.top + assert ctx.preserved + assert type(ctx._preserved_exc) is KeyboardInterrupt + + +def test_before_request_and_routing_errors(app, client): @app.before_request def attach_something(): flask.g.something = 'value' @@ -801,17 +939,16 @@ def test_before_request_and_routing_errors(): @app.errorhandler(404) def return_something(error): return flask.g.something, 404 - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 404 assert rv.data == b'value' -def test_user_error_handling(): +def test_user_error_handling(app, client): class MyException(Exception): pass - app = flask.Flask(__name__) - @app.errorhandler(MyException) def handle_my_exception(e): assert isinstance(e, MyException) @@ -821,16 +958,13 @@ def test_user_error_handling(): def index(): raise MyException() - c = app.test_client() - assert c.get('/').data == b'42' + assert client.get('/').data == b'42' -def test_http_error_subclass_handling(): +def test_http_error_subclass_handling(app, client): class ForbiddenSubclass(Forbidden): pass - app = flask.Flask(__name__) - @app.errorhandler(ForbiddenSubclass) def handle_forbidden_subclass(e): assert isinstance(e, ForbiddenSubclass) @@ -854,46 +988,103 @@ def test_http_error_subclass_handling(): def index3(): raise Forbidden() - c = app.test_client() - assert c.get('/1').data == b'banana' - assert c.get('/2').data == b'apple' - assert c.get('/3').data == b'apple' + assert client.get('/1').data == b'banana' + assert client.get('/2').data == b'apple' + assert client.get('/3').data == b'apple' -def test_trapping_of_bad_request_key_errors(): - app = flask.Flask(__name__) - app.testing = True +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' + +def test_trapping_of_bad_request_key_errors(app, client): @app.route('/fail') def fail(): flask.request.form['missing_key'] - c = app.test_client() - assert c.get('/fail').status_code == 400 + + rv = client.get('/fail') + assert rv.status_code == 400 + assert b'missing_key' not in rv.data app.config['TRAP_BAD_REQUEST_ERRORS'] = True - c = app.test_client() + with pytest.raises(KeyError) as e: - c.get("/fail") + client.get("/fail") + assert e.errisinstance(BadRequest) + assert 'missing_key' in e.value.description -def test_trapping_of_all_http_exceptions(): - app = flask.Flask(__name__) - app.testing = True +def test_trapping_of_all_http_exceptions(app, client): app.config['TRAP_HTTP_EXCEPTIONS'] = True @app.route('/fail') def fail(): flask.abort(404) - c = app.test_client() with pytest.raises(NotFound): - c.get('/fail') + client.get('/fail') + + +def test_error_handler_after_processor_error(app, client): + app.testing = False + + @app.before_request + def before_request(): + if trigger == 'before': + 1 // 0 + + @app.after_request + def after_request(response): + if trigger == 'after': + 1 // 0 + return response + @app.route('/') + def index(): + return 'Foo' -def test_enctype_debug_helper(): + @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 = flask.Flask(__name__) app.debug = True @app.route('/fail', methods=['POST']) @@ -903,180 +1094,224 @@ def test_enctype_debug_helper(): # with statement is important because we leave an exception on the # stack otherwise and we want to ensure that this is not the case # to not negatively affect other tests. - with app.test_client() as c: + with client: with pytest.raises(DebugFilesKeyError) as e: - c.post('/fail', data={'foo': 'index.txt'}) + 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_creation(): - app = flask.Flask(__name__) - - @app.route('/unicode') - def from_unicode(): +def test_response_types(app, client): + @app.route('/text') + def from_text(): return u'Hällo Wörld' - @app.route('/string') - def from_string(): + @app.route('/bytes') + def from_bytes(): return u'Hällo Wörld'.encode('utf-8') - @app.route('/args') - def from_tuple(): + @app.route('/full_tuple') + def from_full_tuple(): return 'Meh', 400, { 'X-Foo': 'Testing', 'Content-Type': 'text/plain; charset=utf-8' } - @app.route('/two_args') - def from_two_args_tuple(): + @app.route('/text_headers') + def from_text_headers(): return 'Hello', { 'X-Foo': 'Test', 'Content-Type': 'text/plain; charset=utf-8' } - @app.route('/args_status') - def from_status_tuple(): + @app.route('/text_status') + def from_text_status(): return 'Hi, status!', 400 - @app.route('/args_header') - def from_response_instance_status_tuple(): - return flask.Response('Hello world', 404), { + @app.route('/response_headers') + def from_response_headers(): + return flask.Response('Hello world', 404, {'X-Foo': 'Baz'}), { "X-Foo": "Bar", "X-Bar": "Foo" } - c = app.test_client() - assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') - assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') - rv = c.get('/args') + @app.route('/response_status') + def from_response_status(): + return app.response_class('Hello world', 400), 500 + + @app.route('/wsgi') + def from_wsgi(): + return NotFound() + + assert client.get('/text').data == u'Hällo Wörld'.encode('utf-8') + assert client.get('/bytes').data == u'Hällo Wörld'.encode('utf-8') + + 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' - rv2 = c.get('/two_args') - assert rv2.data == b'Hello' - assert rv2.headers['X-Foo'] == 'Test' - assert rv2.status_code == 200 - assert rv2.mimetype == 'text/plain' - rv3 = c.get('/args_status') - assert rv3.data == b'Hi, status!' - assert rv3.status_code == 400 - assert rv3.mimetype == 'text/html' - rv4 = c.get('/args_header') - assert rv4.data == b'Hello world' - assert rv4.headers['X-Foo'] == 'Bar' - assert rv4.headers['X-Bar'] == 'Foo' - assert rv4.status_code == 404 - - -def test_make_response(): - app = flask.Flask(__name__) - with app.test_request_context(): - 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 = 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 = flask.make_response('W00t', 404) - assert rv.status_code == 404 - assert rv.data == b'W00t' - assert rv.mimetype == 'text/html' + 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.headers.getlist('X-Foo') == ['Baz', 'Bar'] + assert rv.headers['X-Bar'] == 'Foo' + assert rv.status_code == 404 -def test_make_response_with_response_instance(): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.make_response( - flask.jsonify({'msg': 'W00t'}), 400) - assert rv.status_code == 400 - assert rv.data == b'{\n "msg": "W00t"\n}\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' - - -def test_jsonify_no_prettyprint(): + 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 + + +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) + + with pytest.raises(TypeError) as e: + c.get('/small_tuple') + assert 'tuple must have the form' in str(e) + + 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) + + 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' + + +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' + + +def test_jsonify_no_prettyprint(app, req_ctx): app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": False}) - with app.test_request_context(): - compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n' - uncompressed_msg = { - "msg": { - "submsg": "W00t" - }, - "msg2": "foobar" - } + compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n' + uncompressed_msg = { + "msg": { + "submsg": "W00t" + }, + "msg2": "foobar" + } - rv = flask.make_response( - flask.jsonify(uncompressed_msg), 200) - assert rv.data == compressed_msg + rv = flask.make_response( + flask.jsonify(uncompressed_msg), 200) + assert rv.data == compressed_msg -def test_jsonify_prettyprint(): - app = flask.Flask(__name__) +def test_jsonify_prettyprint(app, req_ctx): app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": True}) - with app.test_request_context(): - compressed_msg = {"msg":{"submsg":"W00t"},"msg2":"foobar"} - pretty_response =\ - b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n' + compressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"} + pretty_response = \ + b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n' - rv = flask.make_response( - flask.jsonify(compressed_msg), 200) - assert rv.data == pretty_response + rv = flask.make_response( + flask.jsonify(compressed_msg), 200) + assert rv.data == pretty_response -def test_jsonify_mimetype(): - app = flask.Flask(__name__) +def test_jsonify_mimetype(app, req_ctx): app.config.update({"JSONIFY_MIMETYPE": 'application/vnd.api+json'}) - with app.test_request_context(): - msg = { - "msg": {"submsg": "W00t"}, - } - rv = flask.make_response( - flask.jsonify(msg), 200) - assert rv.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_jsonify_args_and_kwargs_check(): - app = flask.Flask(__name__) - with app.test_request_context(): - with pytest.raises(TypeError) as e: - flask.jsonify('fake args', kwargs='fake') - assert 'behavior undefined' in str(e.value) +def test_jsonify_args_and_kwargs_check(app, req_ctx): + with pytest.raises(TypeError) as e: + flask.jsonify('fake args', kwargs='fake') + assert 'behavior undefined' in str(e.value) -def test_url_generation(): - app = flask.Flask(__name__) - +def test_url_generation(app, req_ctx): @app.route('/hello/', methods=['POST']) def hello(): pass - with app.test_request_context(): - 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' + 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 = flask.Flask(__name__) +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') @@ -1097,60 +1332,70 @@ def test_build_error_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 = flask.Flask(__name__) - +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_custom_converters(): +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_custom_converters(app, client): from werkzeug.routing import BaseConverter class ListConverter(BaseConverter): - def to_python(self, value): return value.split(',') def to_url(self, value): base_to_url = super(ListConverter, self).to_url return ','.join(base_to_url(x) for x in value) - app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter @app.route('/') def index(args): return '|'.join(args) - c = app.test_client() - assert c.get('/1,2,3').data == b'1|2|3' + assert client.get('/1,2,3').data == b'1|2|3' -def test_static_files(): - app = flask.Flask(__name__) - app.testing = True - rv = app.test_client().get('/static/index.html') + +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' + '/static/index.html' rv.close() -def test_static_path_deprecated(recwarn): - app = flask.Flask(__name__, static_path='/foo') - recwarn.pop(DeprecationWarning) - +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 @@ -1160,31 +1405,23 @@ def test_static_path_deprecated(recwarn): assert flask.url_for('static', filename='index.html') == '/foo/index.html' -def test_static_url_path(): - app = flask.Flask(__name__, static_url_path='/foo') - app.testing = True - rv = app.test_client().get('/foo/index.html') +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(): - assert flask.url_for('static', filename='index.html') == '/foo/index.html' - - -def test_none_response(): - app = flask.Flask(__name__) - app.testing = True - - @app.route('/') - def test(): - return None - try: - app.test_client().get('/') - except ValueError as e: - assert str(e) == 'View function did not return a response' - pass - else: - assert "Expected ValueError" + 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(Exception): + flask.Flask(__name__, static_host='example.com') + # Providing host_matching=True with static_folder but without static_host should error. + with pytest.raises(Exception): + 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(): @@ -1192,8 +1429,7 @@ def test_request_locals(): assert not flask.g -def test_test_app_proper_environ(): - app = flask.Flask(__name__) +def test_test_app_proper_environ(app, client): app.config.update( SERVER_NAME='localhost.localdomain:5000' ) @@ -1206,22 +1442,22 @@ def test_test_app_proper_environ(): def subdomain(): return 'Foo SubDomain' - rv = app.test_client().get('/') + rv = client.get('/') assert rv.data == b'Foo' - rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + rv = client.get('/', 'http://localhost.localdomain:5000') assert rv.data == b'Foo' - rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + rv = client.get('/', 'https://localhost.localdomain:5000') assert rv.data == b'Foo' app.config.update(SERVER_NAME='localhost.localdomain') - rv = app.test_client().get('/', 'https://localhost.localdomain') + rv = client.get('/', 'https://localhost.localdomain') assert rv.data == b'Foo' try: app.config.update(SERVER_NAME='localhost.localdomain:443') - rv = app.test_client().get('/', 'https://localhost.localdomain') + rv = client.get('/', 'https://localhost.localdomain') # Werkzeug 0.8 assert rv.status_code == 404 except ValueError as e: @@ -1234,7 +1470,7 @@ def test_test_app_proper_environ(): try: app.config.update(SERVER_NAME='localhost.localdomain') - rv = app.test_client().get('/', 'http://foo.localhost') + rv = client.get('/', 'http://foo.localhost') # Werkzeug 0.8 assert rv.status_code == 404 except ValueError as e: @@ -1245,25 +1481,23 @@ def test_test_app_proper_environ(): "server name from the WSGI environment ('foo.localhost')" ) - rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + rv = client.get('/', 'http://foo.localhost.localdomain') assert rv.data == b'Foo SubDomain' -def test_exception_propagation(): +def test_exception_propagation(app, client): def apprunner(config_key): - app = flask.Flask(__name__) - app.config['LOGGER_HANDLER_POLICY'] = 'never' @app.route('/') def index(): 1 // 0 - c = app.test_client() + if config_key is not None: app.config[config_key] = True with pytest.raises(Exception): - c.get('/') + client.get('/') else: - assert c.get('/').status_code == 500 + assert client.get('/').status_code == 500 # we have to run this test in an isolated thread because if the # debug flag is set to true and an exception happens the context is @@ -1280,21 +1514,19 @@ def test_exception_propagation(): @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): + 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') - app = flask.Flask(__name__) 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 = flask.Flask(__name__) +def test_max_content_length(app, client): app.config['MAX_CONTENT_LENGTH'] = 64 @app.before_request @@ -1311,18 +1543,16 @@ def test_max_content_length(): def catcher(error): return '42' - c = app.test_client() - rv = c.post('/accept', data={'myfile': 'foo' * 100}) + rv = client.post('/accept', data={'myfile': 'foo' * 100}) assert rv.data == b'42' -def test_url_processors(): - app = flask.Flask(__name__) +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'): + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): values.setdefault('lang_code', flask.g.lang_code) @app.url_value_preprocessor @@ -1341,15 +1571,12 @@ def test_url_processors(): def something_else(): return flask.url_for('about', lang_code='en') - c = app.test_client() - - assert c.get('/de/').data == b'/de/about' - assert c.get('/de/about').data == b'/foo' - assert c.get('/foo').data == b'/en/about' + 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 = flask.Flask(__name__) +def test_inject_blueprint_url_defaults(app): bp = flask.Blueprint('foo.bar.baz', __name__, template_folder='template') @@ -1374,28 +1601,24 @@ def test_inject_blueprint_url_defaults(): assert url == expected -def test_nonascii_pathinfo(): - app = flask.Flask(__name__) - app.testing = True - +def test_nonascii_pathinfo(app, client): @app.route(u'/киртест') def index(): return 'Hello World!' - c = app.test_client() - rv = c.get(u'/киртест') + rv = client.get(u'/киртест') assert rv.data == b'Hello World!' -def test_debug_mode_complains_after_first_request(): - app = flask.Flask(__name__) +def test_debug_mode_complains_after_first_request(app, client): app.debug = True @app.route('/') def index(): return 'Awesome' + assert not app.got_first_request - assert app.test_client().get('/').data == b'Awesome' + assert client.get('/').data == b'Awesome' with pytest.raises(AssertionError) as e: @app.route('/foo') def broken(): @@ -1407,38 +1630,35 @@ def test_debug_mode_complains_after_first_request(): @app.route('/foo') def working(): return 'Meh' - assert app.test_client().get('/foo').data == b'Meh' + + assert client.get('/foo').data == b'Meh' assert app.got_first_request -def test_before_first_request_functions(): +def test_before_first_request_functions(app, client): got = [] - app = flask.Flask(__name__) @app.before_first_request def foo(): got.append(42) - c = app.test_client() - c.get('/') + + client.get('/') assert got == [42] - c.get('/') + client.get('/') assert got == [42] assert app.got_first_request -def test_before_first_request_functions_concurrent(): +def test_before_first_request_functions_concurrent(app, client): got = [] - app = flask.Flask(__name__) @app.before_first_request def foo(): time.sleep(0.2) got.append(42) - c = app.test_client() - def get_and_assert(): - c.get("/") + client.get("/") assert got == [42] t = Thread(target=get_and_assert) @@ -1448,31 +1668,30 @@ def test_before_first_request_functions_concurrent(): assert app.got_first_request -def test_routing_redirect_debugging(): - app = flask.Flask(__name__) +def test_routing_redirect_debugging(app, client): app.debug = True @app.route('/foo/', methods=['GET', 'POST']) def foo(): return 'success' - with app.test_client() as c: + + with client: with pytest.raises(AssertionError) as e: - c.post('/foo', data={}) + client.post('/foo', data={}) assert 'http://localhost/foo/' in str(e) assert ('Make sure to directly send ' 'your POST-request to this URL') in str(e) - rv = c.get('/foo', data={}, follow_redirects=True) + rv = client.get('/foo', data={}, follow_redirects=True) assert rv.data == b'success' app.debug = False - with app.test_client() as c: - rv = c.post('/foo', data={}, follow_redirects=True) + with client: + rv = client.post('/foo', data={}, follow_redirects=True) assert rv.data == b'success' -def test_route_decorator_custom_endpoint(): - app = flask.Flask(__name__) +def test_route_decorator_custom_endpoint(app, client): app.debug = True @app.route('/foo/') @@ -1492,24 +1711,21 @@ def test_route_decorator_custom_endpoint(): assert flask.url_for('bar') == '/bar/' assert flask.url_for('123') == '/bar/123' - c = app.test_client() - assert c.get('/foo/').data == b'foo' - assert c.get('/bar/').data == b'bar' - assert c.get('/bar/123').data == b'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_preserve_only_once(): - app = flask.Flask(__name__) +def test_preserve_only_once(app, client): app.debug = True @app.route('/fail') def fail_func(): 1 // 0 - c = app.test_client() for x in range(3): with pytest.raises(ZeroDivisionError): - c.get('/fail') + client.get('/fail') assert flask._request_ctx_stack.top is not None assert flask._app_ctx_stack.top is not None @@ -1519,8 +1735,7 @@ def test_preserve_only_once(): assert flask._app_ctx_stack.top is None -def test_preserve_remembers_exception(): - app = flask.Flask(__name__) +def test_preserve_remembers_exception(app, client): app.debug = True errors = [] @@ -1536,51 +1751,40 @@ def test_preserve_remembers_exception(): def teardown_handler(exc): errors.append(exc) - c = app.test_client() - # After this failure we did not yet call the teardown handler with pytest.raises(ZeroDivisionError): - c.get('/fail') + client.get('/fail') assert errors == [] # But this request triggers it, and it's an error - c.get('/success') + client.get('/success') assert len(errors) == 2 assert isinstance(errors[0], ZeroDivisionError) # At this point another request does nothing. - c.get('/success') + client.get('/success') assert len(errors) == 3 assert errors[1] is None -def test_get_method_on_g(): - app = flask.Flask(__name__) - app.testing = True +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 - with app.app_context(): - 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_g_iteration_protocol(): - app = flask.Flask(__name__) - app.testing = True - with app.app_context(): - 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__) - app.config['SERVER_NAME'] = 'localhost' +def test_subdomain_basic_support(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain' @app.route('/') def normal_index(): @@ -1590,57 +1794,49 @@ def test_subdomain_basic_support(): def test_index(): return 'test index' - c = app.test_client() - rv = c.get('/', 'http://localhost/') + rv = client.get('/', 'http://localhost.localdomain/') assert rv.data == b'normal index' - rv = c.get('/', 'http://test.localhost/') + rv = client.get('/', 'http://test.localhost.localdomain/') assert rv.data == b'test index' -def test_subdomain_matching(): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' +def test_subdomain_matching(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain' @app.route('/', subdomain='') def index(user): return 'index for %s' % user - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost/') + rv = client.get('/', 'http://mitsuhiko.localhost.localdomain/') assert rv.data == b'index for mitsuhiko' -def test_subdomain_matching_with_ports(): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost:3000' +def test_subdomain_matching_with_ports(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain:3000' @app.route('/', subdomain='') def index(user): return 'index for %s' % user - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + rv = client.get('/', 'http://mitsuhiko.localhost.localdomain:3000/') assert rv.data == b'index for mitsuhiko' -def test_multi_route_rules(): - app = flask.Flask(__name__) - +def test_multi_route_rules(app, client): @app.route('/') @app.route('//') def index(test='a'): return test - rv = app.test_client().open('/') + rv = client.open('/') assert rv.data == b'a' - rv = app.test_client().open('/b/') + rv = client.open('/b/') assert rv.data == b'b' -def test_multi_route_class_views(): +def test_multi_route_class_views(app, client): class View(object): - def __init__(self, app): app.add_url_rule('/', 'index', self.index) app.add_url_rule('//', 'index', self.index) @@ -1648,36 +1844,49 @@ def test_multi_route_class_views(): def index(self, test='a'): return test - app = flask.Flask(__name__) _ = View(app) - rv = app.test_client().open('/') + rv = client.open('/') assert rv.data == b'a' - rv = app.test_client().open('/b/') + rv = client.open('/b/') assert rv.data == b'b' -def test_run_defaults(monkeypatch): +def test_run_defaults(monkeypatch, app): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(*args, **kwargs): rv['result'] = 'running...' - app = flask.Flask(__name__) monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) app.run() assert rv['result'] == 'running...' -def test_run_server_port(monkeypatch): +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'] = 'running on %s:%s ...' % (hostname, port) - app = flask.Flask(__name__) monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) hostname, port = 'localhost', 8000 app.run(hostname, port, debug=True) assert rv['result'] == 'running on %s:%s ...' % (hostname, port) + + +@pytest.mark.parametrize('host,port,expect_host,expect_port', ( + (None, None, 'pocoo.org', 8080), + ('localhost', None, 'localhost', 8080), + (None, 80, 'pocoo.org', 80), + ('localhost', 80, 'localhost', 80), +)) +def test_run_from_config(monkeypatch, host, port, 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'] = 'pocoo.org:8080' + app.run(host, port) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a3309037..c58a0d3b 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -18,7 +18,7 @@ from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound -def test_blueprint_specific_error_handling(): +def test_blueprint_specific_error_handling(app, client): frontend = flask.Blueprint('frontend', __name__) backend = flask.Blueprint('backend', __name__) sideend = flask.Blueprint('sideend', __name__) @@ -43,7 +43,6 @@ def test_blueprint_specific_error_handling(): def sideend_no(): flask.abort(403) - app = flask.Flask(__name__) app.register_blueprint(frontend) app.register_blueprint(backend) app.register_blueprint(sideend) @@ -52,15 +51,15 @@ def test_blueprint_specific_error_handling(): def app_forbidden(e): return 'application itself says no', 403 - c = app.test_client() + 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' - assert c.get('/frontend-no').data == b'frontend says no' - assert c.get('/backend-no').data == b'backend says no' - assert c.get('/what-is-a-sideend').data == b'application itself says no' -def test_blueprint_specific_user_error_handling(): +def test_blueprint_specific_user_error_handling(app, client): class MyDecoratorException(Exception): pass + class MyFunctionException(Exception): pass @@ -74,24 +73,48 @@ def test_blueprint_specific_user_error_handling(): 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 = flask.Flask(__name__) app.register_blueprint(blue) - c = app.test_client() + 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) - assert c.get('/decorator').data == b'boom' - assert c.get('/function').data == b'bam' + app.register_blueprint(errors) + app.register_blueprint(forbidden_bp) -def test_blueprint_url_definitions(): + assert client.get('/forbidden').data == b'you shall not pass' + assert client.get('/nope').data == b'you shall not pass' + + +def test_blueprint_url_definitions(app, client): bp = flask.Blueprint('test', __name__) @bp.route('/foo', defaults={'baz': 42}) @@ -102,17 +125,16 @@ def test_blueprint_url_definitions(): def bar(bar): return text_type(bar) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) - c = app.test_client() - assert c.get('/1/foo').data == b'23/42' - assert c.get('/2/foo').data == b'19/42' - assert c.get('/1/bar').data == b'23' - assert c.get('/2/bar').data == b'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(): +def test_blueprint_url_processors(app, client): bp = flask.Blueprint('frontend', __name__, url_prefix='/') @bp.url_defaults @@ -131,28 +153,26 @@ def test_blueprint_url_processors(): def about(): return flask.url_for('.index') - app = flask.Flask(__name__) app.register_blueprint(bp) - c = app.test_client() + assert client.get('/de/').data == b'/de/about' + assert client.get('/de/about').data == b'/de/' - assert c.get('/de/').data == b'/de/about' - assert c.get('/de/about').data == b'/de/' def test_templates_and_static(test_apps): from blueprintapp import app - c = app.test_client() + client = app.test_client() - rv = c.get('/') + rv = client.get('/') assert rv.data == b'Hello from the Frontend' - rv = c.get('/admin/') + rv = client.get('/admin/') assert rv.data == b'Hello from the Admin' - rv = c.get('/admin/index2') + rv = client.get('/admin/index2') assert rv.data == b'Hello from the Admin' - rv = c.get('/admin/static/test.txt') + rv = client.get('/admin/static/test.txt') assert rv.data.strip() == b'Admin File' rv.close() - rv = c.get('/admin/static/css/test.css') + rv = client.get('/admin/static/css/test.css') assert rv.data.strip() == b'/* nested file */' rv.close() @@ -163,7 +183,7 @@ def test_templates_and_static(test_apps): 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 = c.get('/admin/static/css/test.css') + 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() @@ -181,8 +201,8 @@ def test_templates_and_static(test_apps): with flask.Flask(__name__).test_request_context(): assert flask.render_template('nested/nested.txt') == 'I\'m nested' -def test_default_static_cache_timeout(): - app = flask.Flask(__name__) + +def test_default_static_cache_timeout(app): class MyBlueprint(flask.Blueprint): def get_send_file_max_age(self, filename): return 100 @@ -205,12 +225,14 @@ def test_default_static_cache_timeout(): 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_names(): + +def test_dotted_names(app, client): frontend = flask.Blueprint('myapp.frontend', __name__) backend = flask.Blueprint('myapp.backend', __name__) @@ -226,18 +248,15 @@ def test_dotted_names(): def backend_index(): return flask.url_for('myapp.frontend.frontend_index') - app = flask.Flask(__name__) app.register_blueprint(frontend) app.register_blueprint(backend) - c = app.test_client() - assert c.get('/fe').data.strip() == b'/be' - assert c.get('/fe2').data.strip() == b'/fe' - assert c.get('/be').data.strip() == b'/fe' + assert client.get('/fe').data.strip() == b'/be' + assert client.get('/fe2').data.strip() == b'/fe' + assert client.get('/be').data.strip() == b'/fe' + -def test_dotted_names_from_app(): - app = flask.Flask(__name__) - app.testing = True +def test_dotted_names_from_app(app, client): test = flask.Blueprint('test', __name__) @app.route('/') @@ -250,11 +269,11 @@ def test_dotted_names_from_app(): app.register_blueprint(test) - with app.test_client() as c: - rv = c.get('/') - assert rv.data == b'/test/' + rv = client.get('/') + assert rv.data == b'/test/' + -def test_empty_url_defaults(): +def test_empty_url_defaults(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/', defaults={'page': 1}) @@ -262,15 +281,13 @@ def test_empty_url_defaults(): def something(page): return str(page) - app = flask.Flask(__name__) app.register_blueprint(bp) - c = app.test_client() - assert c.get('/').data == b'1' - assert c.get('/page/2').data == b'2' + assert client.get('/').data == b'1' + assert client.get('/page/2').data == b'2' -def test_route_decorator_custom_endpoint(): +def test_route_decorator_custom_endpoint(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/foo') @@ -289,21 +306,20 @@ def test_route_decorator_custom_endpoint(): def bar_foo(): return flask.request.endpoint - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') @app.route('/') def index(): return flask.request.endpoint - c = app.test_client() - assert c.get('/').data == b'index' - assert c.get('/py/foo').data == b'bp.foo' - assert c.get('/py/bar').data == b'bp.bar' - assert c.get('/py/bar/123').data == b'bp.123' - assert c.get('/py/bar/foo').data == b'bp.bar_foo' + 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(): + +def test_route_decorator_custom_endpoint_with_dots(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/foo') @@ -344,231 +360,470 @@ def test_route_decorator_custom_endpoint_with_dots(): lambda: None ) - app = flask.Flask(__name__) + foo_foo_foo.__name__ = 'bar.123' + + pytest.raises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', view_func=foo_foo_foo + ) + ) + app.register_blueprint(bp, url_prefix='/py') - c = app.test_client() - assert c.get('/py/foo').data == b'bp.foo' + assert client.get('/py/foo').data == b'bp.foo' # The rule's didn't actually made it through - rv = c.get('/py/bar') + rv = client.get('/py/bar') assert rv.status_code == 404 - rv = c.get('/py/bar/123') + rv = client.get('/py/bar/123') assert rv.status_code == 404 -def test_template_filter(): + +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 = flask.Flask(__name__) + 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(): + +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 = flask.Flask(__name__) 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(): + +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 = flask.Flask(__name__) + 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(): + +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 = flask.Flask(__name__) 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(): + +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 = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_filter_after_route_with_template(): - app = flask.Flask(__name__) + +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 = app.test_client().get('/') + rv = client.get('/') assert rv.data == b'dcba' -def test_add_template_filter_with_template(): + +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 = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_filter_with_name_and_template(): + +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 = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_add_template_filter_with_name_and_template(): + +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 = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_test(): + +def test_template_test(app): bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() def is_boolean(value): return isinstance(value, bool) - app = flask.Flask(__name__) + 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(): + +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 = flask.Flask(__name__) 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(): + +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 = flask.Flask(__name__) + 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(): + +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 = flask.Flask(__name__) 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(): + +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 = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_template_test_after_route_with_template(): - app = flask.Flask(__name__) + +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 = app.test_client().get('/') + rv = client.get('/') assert b'Success!' in rv.data -def test_add_template_test_with_template(): + +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 = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_template_test_with_name_and_template(): + +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 = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_add_template_test_with_name_and_template(): + +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 = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data + + +def test_context_processing(app, client): + answer_bp = flask.Blueprint('answer_bp', __name__) + + template_string = lambda: 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_first_request + def before_first_request(): + evts.append('first') + + @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 == ['first', 'before', 'after', 'teardown'] + + # second request + resp = client.get('/').data + assert resp == b'request|after' + assert evts == ['first'] + ['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/' diff --git a/tests/test_cli.py b/tests/test_cli.py index 18026a75..3ffb3034 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,18 +11,35 @@ # the Revised BSD License. # Copyright (C) 2015 CERN. # -from __future__ import absolute_import, print_function +from __future__ import absolute_import + import os +import ssl import sys +import types +from functools import partial import click import pytest +from _pytest.monkeypatch import notset from click.testing import CliRunner + from flask import Flask, current_app +from flask.cli import ( + AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv, find_best_app, + get_version, load_dotenv, locate_app, prepare_import, run_command, + with_appcontext +) + +cwd = os.getcwd() +test_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps' +)) + -from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ - find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ - find_default_import_path, get_version +@pytest.fixture +def runner(): + return CliRunner() def test_cli_name(test_apps): @@ -33,79 +50,208 @@ def test_cli_name(test_apps): def test_find_best_app(test_apps): """Test if `find_best_app` behaves as expected with different combinations of input.""" + script_info = ScriptInfo() + class Module: app = Flask('appname') - assert find_best_app(Module) == Module.app + + assert find_best_app(script_info, Module) == Module.app class Module: application = Flask('appname') - assert find_best_app(Module) == Module.application + + assert find_best_app(script_info, Module) == Module.application class Module: myapp = Flask('appname') - assert find_best_app(Module) == Module.myapp + + assert find_best_app(script_info, Module) == Module.myapp + + class Module: + @staticmethod + def create_app(): + return Flask('appname') + + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + @staticmethod + def create_app(foo): + return Flask('appname') + + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + @staticmethod + def create_app(foo=None, script_info=None): + return Flask('appname') + + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + @staticmethod + def make_app(): + return Flask('appname') + + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + myapp = Flask('appname1') + + @staticmethod + def create_app(): + return Flask('appname2') + + assert find_best_app(script_info, Module) == Module.myapp + + class Module: + myapp = Flask('appname1') + + @staticmethod + def create_app(): + return Flask('appname2') + + assert find_best_app(script_info, Module) == Module.myapp class Module: pass - pytest.raises(NoAppException, find_best_app, Module) + + pytest.raises(NoAppException, find_best_app, script_info, Module) class Module: myapp1 = Flask('appname1') myapp2 = Flask('appname2') - pytest.raises(NoAppException, find_best_app, Module) + pytest.raises(NoAppException, find_best_app, script_info, Module) -def test_prepare_exec_for_file(test_apps): - """Expect the correct path to be set and the correct module name to be returned. + class Module: + @staticmethod + def create_app(foo, bar): + return Flask('appname2') - :func:`prepare_exec_for_file` has a side effect, where - the parent directory of given file is added to `sys.path`. - """ - realpath = os.path.realpath('/tmp/share/test.py') - dirname = os.path.dirname(realpath) - assert prepare_exec_for_file('/tmp/share/test.py') == 'test' - assert dirname in sys.path + pytest.raises(NoAppException, find_best_app, script_info, Module) - realpath = os.path.realpath('/tmp/share/__init__.py') - dirname = os.path.dirname(os.path.dirname(realpath)) - assert prepare_exec_for_file('/tmp/share/__init__.py') == 'share' - assert dirname in sys.path + class Module: + @staticmethod + def create_app(): + raise TypeError('bad bad factory!') + + pytest.raises(TypeError, find_best_app, script_info, Module) + + +@pytest.mark.parametrize('value,path,result', ( + ('test', cwd, 'test'), + ('test.py', cwd, 'test'), + ('a/test', os.path.join(cwd, 'a'), 'test'), + ('test/__init__.py', cwd, 'test'), + ('test/__init__', cwd, 'test'), + # nested package + ( + os.path.join(test_path, 'cliapp', 'inner1', '__init__'), + test_path, 'cliapp.inner1' + ), + ( + os.path.join(test_path, 'cliapp', 'inner1', 'inner2'), + test_path, 'cliapp.inner1.inner2' + ), + # dotted name + ('test.a.b', cwd, 'test.a.b'), + (os.path.join(test_path, 'cliapp.app'), test_path, 'cliapp.app'), + # not a Python file, will be caught during import + ( + os.path.join(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] == 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'), + # no script_info + ('cliapp.factory', 'create_app2("foo", "bar")', 'app2_foo_bar'), + # trailing comma space + ('cliapp.factory', 'create_app2("foo", "bar", )', 'app2_foo_bar'), + # takes script_info + ('cliapp.factory', 'create_app3("foo")', 'app3_foo_spam'), + # strip whitespace + ('cliapp.factory', ' create_app () ', 'app'), +)) +def test_locate_app(test_apps, iname, aname, result): + info = ScriptInfo() + info.data['test'] = 'spam' + assert locate_app(info, 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): + info = ScriptInfo() with pytest.raises(NoAppException): - prepare_exec_for_file('/tmp/share/test.txt') - + locate_app(info, iname, aname) -def test_locate_app(test_apps): - """Test of locate_app.""" - assert locate_app("cliapp.app").name == "testapp" - assert locate_app("cliapp.app:testapp").name == "testapp" - assert locate_app("cliapp.multiapp:app1").name == "app1" - pytest.raises(NoAppException, locate_app, "notanpp.py") - pytest.raises(NoAppException, locate_app, "cliapp/app") - pytest.raises(RuntimeError, locate_app, "cliapp.app:notanapp") +def test_locate_app_suppress_raise(): + info = ScriptInfo() + app = locate_app(info, 'notanapp.py', None, raise_if_not_found=False) + assert app is None -def test_find_default_import_path(test_apps, monkeypatch, tmpdir): - """Test of find_default_import_path.""" - monkeypatch.delitem(os.environ, 'FLASK_APP', raising=False) - assert find_default_import_path() == None - monkeypatch.setitem(os.environ, 'FLASK_APP', 'notanapp') - assert find_default_import_path() == 'notanapp' - tmpfile = tmpdir.join('testapp.py') - tmpfile.write('') - monkeypatch.setitem(os.environ, 'FLASK_APP', str(tmpfile)) - expect_rv = prepare_exec_for_file(str(tmpfile)) - assert find_default_import_path() == expect_rv + # only direct import error is suppressed + with pytest.raises(NoAppException): + locate_app( + info, 'cliapp.importerrorapp', None, raise_if_not_found=False + ) def test_get_version(test_apps, capsys): """Test of get_version.""" from flask import __version__ as flask_ver from sys import version as py_ver + class MockCtx(object): resilient_parsing = False color = None + def exit(self): return + ctx = MockCtx() get_version(ctx, None, "test") out, err = capsys.readouterr() @@ -113,7 +259,7 @@ def test_get_version(test_apps, capsys): assert py_ver in out -def test_scriptinfo(test_apps): +def test_scriptinfo(test_apps, monkeypatch): """Test of ScriptInfo.""" obj = ScriptInfo(app_import_path="cliapp.app:testapp") assert obj.load_app().name == "testapp" @@ -127,9 +273,29 @@ def test_scriptinfo(test_apps): assert app.name == "createapp" assert obj.load_app() == app + obj = ScriptInfo() + pytest.raises(NoAppException, obj.load_app) -def test_with_appcontext(): + # import app from wsgi.py in current directory + monkeypatch.chdir(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps', 'helloworld' + ))) + obj = ScriptInfo() + app = obj.load_app() + assert app.name == 'hello' + + # import app from app.py in current directory + monkeypatch.chdir(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps', 'cliapp' + ))) + obj = ScriptInfo() + app = obj.load_app() + assert app.name == 'testapp' + + +def test_with_appcontext(runner): """Test of with_appcontext.""" + @click.command() @with_appcontext def testcmd(): @@ -137,14 +303,14 @@ def test_with_appcontext(): obj = ScriptInfo(create_app=lambda info: Flask("testapp")) - runner = CliRunner() result = runner.invoke(testcmd, obj=obj) assert result.exit_code == 0 assert result.output == 'testapp\n' -def test_appgroup(): +def test_appgroup(runner): """Test of with_appcontext.""" + @click.group(cls=AppGroup) def cli(): pass @@ -163,7 +329,6 @@ def test_appgroup(): obj = ScriptInfo(create_app=lambda info: Flask("testappgroup")) - runner = CliRunner() result = runner.invoke(cli, ['test'], obj=obj) assert result.exit_code == 0 assert result.output == 'testappgroup\n' @@ -173,8 +338,9 @@ def test_appgroup(): assert result.output == 'testappgroup\n' -def test_flaskgroup(): +def test_flaskgroup(runner): """Test FlaskGroup.""" + def create_app(info): return Flask("flaskgroup") @@ -186,7 +352,183 @@ def test_flaskgroup(): def test(): click.echo(current_app.name) - runner = CliRunner() result = runner.invoke(cli, ['test']) assert result.exit_code == 0 assert result.output == 'flaskgroup\n' + + +def test_print_exceptions(runner): + """Print the stacktrace if the CLI.""" + + def create_app(info): + raise Exception("oh no") + return Flask("flaskgroup") + + @click.group(cls=FlaskGroup, create_app=create_app) + def cli(**params): + pass + + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert 'Exception: oh no' in result.output + assert 'Traceback' in result.output + + +class TestRoutes: + @pytest.fixture + def invoke(self, runner): + def create_app(info): + app = Flask(__name__) + app.testing = True + + @app.route('/get_post//', methods=['GET', 'POST']) + def yyy_get_post(x, y): + pass + + @app.route('/zzz_post', methods=['POST']) + def aaa_post(): + pass + + return app + + cli = FlaskGroup(create_app=create_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, 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 + ) + self.expect_order( + ['aaa_post', 'yyy_get_post', 'static'], + 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 + + +need_dotenv = pytest.mark.skipif( + dotenv is None, 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'): + monkeypatch._setitem.append((os.environ, item, notset)) + + monkeypatch.setenv('EGGS', '3') + monkeypatch.chdir(os.path.join(test_path, 'cliapp', 'inner1')) + load_dotenv() + assert os.getcwd() == 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' + + +@need_dotenv +def test_dotenv_path(monkeypatch): + for item in ('FOO', 'BAR', 'EGGS'): + monkeypatch._setitem.append((os.environ, item, notset)) + + cwd = os.getcwd() + load_dotenv(os.path.join(test_path, '.flaskenv')) + assert os.getcwd() == cwd + assert 'FOO' in os.environ + + +def test_dotenv_optional(monkeypatch): + monkeypatch.setattr('flask.cli.dotenv', None) + monkeypatch.chdir(test_path) + load_dotenv() + 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__]) + + ctx = run_command.make_context( + 'run', ['--cert', __file__, '--key', __file__]) + assert ctx.params['cert'] == (__file__, __file__) + + +def test_run_cert_adhoc(monkeypatch): + monkeypatch.setitem(sys.modules, 'OpenSSL', None) + + # pyOpenSSL not installed + with pytest.raises(click.BadParameter): + run_command.make_context('run', ['--cert', 'adhoc']) + + # pyOpenSSL installed + monkeypatch.setitem(sys.modules, 'OpenSSL', types.ModuleType('OpenSSL')) + 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']) + + # not an SSLContext + if sys.version_info >= (2, 7): + with pytest.raises(click.BadParameter): + run_command.make_context('run', ['--cert', 'flask']) + + # SSLContext + if sys.version_info < (2, 7): + ssl_context = object() + else: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + + 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__]) diff --git a/tests/test_config.py b/tests/test_config.py index 333a5cff..1f817c3e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,20 +7,23 @@ :license: BSD, see LICENSE for more details. """ -import pytest -import os from datetime import timedelta +import os +import textwrap + import flask +from flask._compat import PY2 +import pytest # config keys used for the TestConfig TEST_KEY = 'foo' -SECRET_KEY = 'devkey' +SECRET_KEY = 'config' def common_object_test(app): - assert app.secret_key == 'devkey' + assert app.secret_key == 'config' assert app.config['TEST_KEY'] == 'foo' assert 'TestConfig' not in app.config @@ -47,21 +50,21 @@ def test_config_from_json(): def test_config_from_mapping(): app = flask.Flask(__name__) app.config.from_mapping({ - 'SECRET_KEY': 'devkey', + 'SECRET_KEY': 'config', 'TEST_KEY': 'foo' }) common_object_test(app) app = flask.Flask(__name__) app.config.from_mapping([ - ('SECRET_KEY', 'devkey'), + ('SECRET_KEY', 'config'), ('TEST_KEY', 'foo') ]) common_object_test(app) app = flask.Flask(__name__) app.config.from_mapping( - SECRET_KEY='devkey', + SECRET_KEY='config', TEST_KEY='foo' ) common_object_test(app) @@ -78,7 +81,8 @@ def test_config_from_class(): TEST_KEY = 'foo' class Test(Base): - SECRET_KEY = 'devkey' + SECRET_KEY = 'config' + app = flask.Flask(__name__) app.config.from_object(Test) common_object_test(app) @@ -187,3 +191,18 @@ def test_get_namespace(): 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(tmpdir, encoding): + f = tmpdir.join('my_config.py') + f.write_binary(textwrap.dedent(u''' + # -*- coding: {0} -*- + TEST_VALUE = "föö" + '''.format(encoding)).encode(encoding)) + app = flask.Flask(__name__) + app.config.from_pyfile(str(f)) + value = app.config['TEST_VALUE'] + if PY2: + value = value.decode(encoding) + assert value == u'föö' diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py deleted file mode 100644 index 666f7d56..00000000 --- a/tests/test_deprecations.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" - tests.deprecations - ~~~~~~~~~~~~~~~~~~ - - Tests deprecation support. Not used currently. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - -import pytest - -import flask - - -class TestRequestDeprecation(object): - - def test_request_json(self, recwarn): - """Request.json is deprecated""" - app = flask.Flask(__name__) - app.testing = True - - @app.route('/', methods=['POST']) - def index(): - assert flask.request.json == {'spam': 42} - print(flask.request.json) - return 'OK' - - c = app.test_client() - c.post('/', data='{"spam": 42}', content_type='application/json') - recwarn.pop(DeprecationWarning) - - def test_request_module(self, recwarn): - """Request.module is deprecated""" - app = flask.Flask(__name__) - app.testing = True - - @app.route('/') - def index(): - assert flask.request.module is None - return 'OK' - - c = app.test_client() - c.get('/') - recwarn.pop(DeprecationWarning) diff --git a/tests/test_ext.py b/tests/test_ext.py deleted file mode 100644 index d336e404..00000000 --- a/tests/test_ext.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -""" - tests.ext - ~~~~~~~~~~~~~~~~~~~ - - Tests the extension import thing. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - -import sys -import pytest - -try: - from imp import reload as reload_module -except ImportError: - reload_module = reload - -from flask._compat import PY2 - - -@pytest.fixture(autouse=True) -def disable_extwarnings(request, recwarn): - from flask.exthook import ExtDeprecationWarning - - def inner(): - assert set(w.category for w in recwarn.list) \ - <= set([ExtDeprecationWarning]) - recwarn.clear() - - request.addfinalizer(inner) - - -@pytest.fixture(autouse=True) -def importhook_setup(monkeypatch, request): - # we clear this out for various reasons. The most important one is - # that a real flaskext could be in there which would disable our - # fake package. Secondly we want to make sure that the flaskext - # import hook does not break on reloading. - for entry, value in list(sys.modules.items()): - if ( - entry.startswith('flask.ext.') or - entry.startswith('flask_') or - entry.startswith('flaskext.') or - entry == 'flaskext' - ) and value is not None: - monkeypatch.delitem(sys.modules, entry) - from flask import ext - reload_module(ext) - - # reloading must not add more hooks - import_hooks = 0 - for item in sys.meta_path: - cls = type(item) - if cls.__module__ == 'flask.exthook' and \ - cls.__name__ == 'ExtensionImporter': - import_hooks += 1 - assert import_hooks == 1 - - def teardown(): - from flask import ext - for key in ext.__dict__: - assert '.' not in key - - request.addfinalizer(teardown) - - -@pytest.fixture -def newext_simple(modules_tmpdir): - x = modules_tmpdir.join('flask_newext_simple.py') - x.write('ext_id = "newext_simple"') - - -@pytest.fixture -def oldext_simple(modules_tmpdir): - flaskext = modules_tmpdir.mkdir('flaskext') - flaskext.join('__init__.py').write('\n') - flaskext.join('oldext_simple.py').write('ext_id = "oldext_simple"') - - -@pytest.fixture -def newext_package(modules_tmpdir): - pkg = modules_tmpdir.mkdir('flask_newext_package') - pkg.join('__init__.py').write('ext_id = "newext_package"') - pkg.join('submodule.py').write('def test_function():\n return 42\n') - - -@pytest.fixture -def oldext_package(modules_tmpdir): - flaskext = modules_tmpdir.mkdir('flaskext') - flaskext.join('__init__.py').write('\n') - oldext = flaskext.mkdir('oldext_package') - oldext.join('__init__.py').write('ext_id = "oldext_package"') - oldext.join('submodule.py').write('def test_function():\n' - ' return 42') - - -@pytest.fixture -def flaskext_broken(modules_tmpdir): - ext = modules_tmpdir.mkdir('flask_broken') - ext.join('b.py').write('\n') - ext.join('__init__.py').write('import flask.ext.broken.b\n' - 'import missing_module') - - -def test_flaskext_new_simple_import_normal(newext_simple): - from flask.ext.newext_simple import ext_id - assert ext_id == 'newext_simple' - - -def test_flaskext_new_simple_import_module(newext_simple): - from flask.ext import newext_simple - assert newext_simple.ext_id == 'newext_simple' - assert newext_simple.__name__ == 'flask_newext_simple' - - -def test_flaskext_new_package_import_normal(newext_package): - from flask.ext.newext_package import ext_id - assert ext_id == 'newext_package' - - -def test_flaskext_new_package_import_module(newext_package): - from flask.ext import newext_package - assert newext_package.ext_id == 'newext_package' - assert newext_package.__name__ == 'flask_newext_package' - - -def test_flaskext_new_package_import_submodule_function(newext_package): - from flask.ext.newext_package.submodule import test_function - assert test_function() == 42 - - -def test_flaskext_new_package_import_submodule(newext_package): - from flask.ext.newext_package import submodule - assert submodule.__name__ == 'flask_newext_package.submodule' - assert submodule.test_function() == 42 - - -def test_flaskext_old_simple_import_normal(oldext_simple): - from flask.ext.oldext_simple import ext_id - assert ext_id == 'oldext_simple' - - -def test_flaskext_old_simple_import_module(oldext_simple): - from flask.ext import oldext_simple - assert oldext_simple.ext_id == 'oldext_simple' - assert oldext_simple.__name__ == 'flaskext.oldext_simple' - - -def test_flaskext_old_package_import_normal(oldext_package): - from flask.ext.oldext_package import ext_id - assert ext_id == 'oldext_package' - - -def test_flaskext_old_package_import_module(oldext_package): - from flask.ext import oldext_package - assert oldext_package.ext_id == 'oldext_package' - assert oldext_package.__name__ == 'flaskext.oldext_package' - - -def test_flaskext_old_package_import_submodule(oldext_package): - from flask.ext.oldext_package import submodule - assert submodule.__name__ == 'flaskext.oldext_package.submodule' - assert submodule.test_function() == 42 - - -def test_flaskext_old_package_import_submodule_function(oldext_package): - from flask.ext.oldext_package.submodule import test_function - assert test_function() == 42 - - -def test_flaskext_broken_package_no_module_caching(flaskext_broken): - for x in range(2): - with pytest.raises(ImportError): - import flask.ext.broken - - -def test_no_error_swallowing(flaskext_broken): - with pytest.raises(ImportError) as excinfo: - import flask.ext.broken - - assert excinfo.type is ImportError - if PY2: - message = 'No module named missing_module' - else: - message = 'No module named \'missing_module\'' - assert str(excinfo.value) == message - assert excinfo.tb.tb_frame.f_globals is globals() - - # reraise() adds a second frame so we need to skip that one too. - # On PY3 we even have another one :( - next = excinfo.tb.tb_next.tb_next - if not PY2: - next = next.tb_next - - import os.path - assert os.path.join('flask_broken', '__init__.py') in \ - next.tb_frame.f_code.co_filename diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8348331b..1ddde116 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -9,19 +9,19 @@ :license: BSD, see LICENSE for more details. """ -import pytest - +import datetime import os import uuid -import datetime -import flask -from logging import StreamHandler +import pytest from werkzeug.datastructures import Range from werkzeug.exceptions import BadRequest, NotFound -from werkzeug.http import parse_cache_control_header, parse_options_header -from werkzeug.http import http_date +from werkzeug.http import http_date, parse_cache_control_header, \ + parse_options_header + +import flask from flask._compat import StringIO, text_type +from flask.helpers import get_debug_flag, get_env def has_encoding(name): @@ -33,243 +33,298 @@ def has_encoding(name): return False +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() + + class TestJSON(object): + def test_ignore_cached_json(self, app): + with app.test_request_context('/', method='POST', data='malformed', + content_type='application/json'): + assert flask.request.get_json(silent=True, cache=True) is None + with pytest.raises(BadRequest): + flask.request.get_json(silent=False, cache=False) - def test_post_empty_json_adds_exception_to_response_content_in_debug(self): - app = flask.Flask(__name__) + def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client): app.config['DEBUG'] = True + app.config['TRAP_BAD_REQUEST_ERRORS'] = False + @app.route('/json', methods=['POST']) def post_json(): flask.request.get_json() return None - c = app.test_client() - rv = c.post('/json', data=None, content_type='application/json') + + rv = client.post('/json', data=None, content_type='application/json') assert rv.status_code == 400 assert b'Failed to decode JSON object' in rv.data - def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self): - app = flask.Flask(__name__) + def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client): app.config['DEBUG'] = False + app.config['TRAP_BAD_REQUEST_ERRORS'] = False + @app.route('/json', methods=['POST']) def post_json(): flask.request.get_json() return None - c = app.test_client() - rv = c.post('/json', data=None, content_type='application/json') + + rv = client.post('/json', data=None, content_type='application/json') assert rv.status_code == 400 assert b'Failed to decode JSON object' not in rv.data - def test_json_bad_requests(self): - app = flask.Flask(__name__) + def test_json_bad_requests(self, app, client): + @app.route('/json', methods=['POST']) def return_json(): return flask.jsonify(foo=text_type(flask.request.get_json())) - c = app.test_client() - rv = c.post('/json', data='malformed', content_type='application/json') + + rv = client.post('/json', data='malformed', content_type='application/json') assert rv.status_code == 400 - def test_json_custom_mimetypes(self): - app = flask.Flask(__name__) + def test_json_custom_mimetypes(self, app, client): + @app.route('/json', methods=['POST']) def return_json(): return flask.request.get_json() - c = app.test_client() - rv = c.post('/json', data='"foo"', content_type='application/x+json') + + rv = client.post('/json', data='"foo"', content_type='application/x+json') assert rv.data == b'foo' - def test_json_body_encoding(self): - app = flask.Flask(__name__) - app.testing = True + def test_json_body_encoding(self, app, client): + @app.route('/') def index(): return flask.request.get_json() - c = app.test_client() - resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), - content_type='application/json; charset=iso-8859-15') + resp = client.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') assert resp.data == u'Hällo Wörld'.encode('utf-8') - def test_json_as_unicode(self): - app = flask.Flask(__name__) + @pytest.mark.parametrize('test_value,expected', [(True, '"\\u2603"'), (False, u'"\u2603"')]) + def test_json_as_unicode(self, test_value, expected, app, app_ctx): - app.config['JSON_AS_ASCII'] = True - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == '"\\u2603"' + app.config['JSON_AS_ASCII'] = test_value + rv = flask.json.dumps(u'\N{SNOWMAN}') + assert rv == expected - app.config['JSON_AS_ASCII'] = False - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == u'"\u2603"' - - def test_json_dump_to_file(self): - app = flask.Flask(__name__) + def test_json_dump_to_file(self, app, app_ctx): test_data = {'name': 'Flask'} out = StringIO() - with app.app_context(): - flask.json.dump(test_data, out) - out.seek(0) - rv = flask.json.load(out) - assert rv == test_data + flask.json.dump(test_data, out) + out.seek(0) + rv = flask.json.load(out) + assert rv == test_data - def test_jsonify_basic_types(self): + @pytest.mark.parametrize('test_value', [0, -1, 1, 23, 3.14, 's', "longer string", True, False, None]) + def test_jsonify_basic_types(self, test_value, app, client): """Test jsonify with basic types.""" - # Should be able to use pytest parametrize on this, but I couldn't - # figure out the correct syntax - # https://pytest.org/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions - test_data = (0, 1, 23, 3.14, 's', "longer string", True, False,) - app = flask.Flask(__name__) - c = app.test_client() - for i, d in enumerate(test_data): - url = '/jsonify_basic_types{0}'.format(i) - app.add_url_rule(url, str(i), lambda x=d: flask.jsonify(x)) - rv = c.get(url) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d - def test_jsonify_dicts(self): + 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(self, app, client): """Test jsonify with dicts and kwargs unpacking.""" - d = dict( - a=0, b=23, c=3.14, d='t', e='Hi', f=True, g=False, - h=['test list', 10, False], - i={'test':'dict'} - ) - app = flask.Flask(__name__) + 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) - c = app.test_client() + for url in '/kw', '/dict': - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == d - def test_jsonify_arrays(self): + def test_jsonify_arrays(self, app, client): """Test jsonify of lists and args unpacking.""" l = [ 0, 42, 3.14, 't', 'hello', True, False, ['test list', 2, False], - {'test':'dict'} + {'test': 'dict'} ] - app = flask.Flask(__name__) + @app.route('/args_unpack') def return_args_unpack(): return flask.jsonify(*l) + @app.route('/array') def return_array(): return flask.jsonify(l) - c = app.test_client() + for url in '/args_unpack', '/array': - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == l - def test_jsonify_date_types(self): + def test_jsonify_date_types(self, app, client): """Test jsonify with datetime.date and datetime.datetime types.""" - test_dates = ( datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5) ) - app = flask.Flask(__name__) - c = app.test_client() - for i, d in enumerate(test_dates): url = '/datetest{0}'.format(i) app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val)) - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) - def test_jsonify_uuid_types(self): - """Test jsonify with uuid.UUID types""" + @pytest.mark.parametrize('tz', (('UTC', 0), ('PST', -8), ('KST', 9))) + def test_jsonify_aware_datetimes(self, 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.JSONEncoder().encode(dt) == expected - test_uuid = uuid.UUID(bytes=b'\xDE\xAD\xBE\xEF'*4) + def test_jsonify_uuid_types(self, app, client): + """Test jsonify with uuid.UUID types""" - app = flask.Flask(__name__) + 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)) - c = app.test_client() - rv = c.get(url) + 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_attr(self): - app = flask.Flask(__name__) + def test_json_attr(self, app, client): + @app.route('/add', methods=['POST']) def add(): json = flask.request.get_json() return text_type(json['a'] + json['b']) - c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), - content_type='application/json') + + rv = client.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') assert rv.data == b'3' - def test_template_escaping(self): - app = flask.Flask(__name__) + def test_template_escaping(self, app, req_ctx): render = flask.render_template_string - with app.test_request_context(): - rv = flask.json.htmlsafe_dumps('') - assert rv == u'"\\u003c/script\\u003e"' - assert type(rv) == text_type - rv = render('{{ ""|tojson }}') - assert rv == '"\\u003c/script\\u003e"' - rv = render('{{ "<\0/script>"|tojson }}') - assert rv == '"\\u003c\\u0000/script\\u003e"' - rv = render('{{ "