Browse Source

Merge branch 'master' into json-object-hook

pull/2352/head
David Lord 8 years ago
parent
commit
ea2e9609bc
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 35
      .github/ISSUE_TEMPLATE.rst
  2. 16
      .github/PULL_REQUEST_TEMPLATE.rst
  3. 3
      .gitmodules
  4. 42
      .travis.yml
  5. 44
      CHANGES
  6. 185
      CONTRIBUTING.rst
  7. 36
      Makefile
  8. 1
      docs/_themes
  9. 56
      docs/appcontext.rst
  10. 21
      docs/conf.py
  11. 412
      docs/config.rst
  12. 1
      docs/contents.rst.inc
  13. 1
      docs/contributing.rst
  14. 71
      docs/errorhandling.rst
  15. 86
      docs/flaskext.py
  16. 102
      docs/security.rst
  17. 8
      examples/flaskr/README
  18. 1
      examples/flaskr/flaskr/__init__.py
  19. 0
      examples/flaskr/flaskr/blueprints/__init__.py
  20. 55
      examples/flaskr/flaskr/blueprints/flaskr.py
  21. 64
      examples/flaskr/flaskr/factory.py
  22. 4
      examples/flaskr/flaskr/templates/layout.html
  23. 2
      examples/flaskr/flaskr/templates/login.html
  24. 2
      examples/flaskr/flaskr/templates/show_entries.html
  25. 4
      examples/flaskr/setup.cfg
  26. 15
      examples/flaskr/setup.py
  27. 59
      examples/flaskr/tests/test_flaskr.py
  28. 2
      examples/minitwit/minitwit/minitwit.py
  29. 4
      examples/patterns/largerapp/yourapplication/__init__.py
  30. 129
      flask/app.py
  31. 93
      flask/cli.py
  32. 6
      flask/ctx.py
  33. 14
      flask/helpers.py
  34. 7
      flask/sessions.py
  35. 45
      flask/testing.py
  36. 14
      setup.py
  37. 15
      tests/test_apps/cliapp/factory.py
  38. 35
      tests/test_basic.py
  39. 38
      tests/test_cli.py
  40. 2
      tests/test_helpers.py
  41. 32
      tests/test_testing.py
  42. 17
      tox.ini

35
.github/ISSUE_TEMPLATE.rst

@ -1,2 +1,33 @@
The issue tracker is a tool to address bugs. **This issue tracker is a tool to address bugs in Flask itself.
Please use the #pocoo IRC channel on freenode or Stack Overflow for questions. Please use the #pocoo IRC channel on freenode or Stack Overflow for general
questions about using Jinja or issues not related to Jinja.**
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:

16
.github/PULL_REQUEST_TEMPLATE.rst

@ -0,0 +1,16 @@
Describe what this patch does to fix the issue.
Link to any relevant issues or pull requests.
<!--
Commit checklist:
* add tests that fail without the patch
* ensure all tests pass with ``pytest``
* add documentation to the relevant docstrings or pages
* add ``versionadded`` or ``versionchanged`` directives to relevant docstrings
* add a changelog entry if this patch changes code
Tests, coverage, and docs will be run automatically when you submit the pull
request, but running them yourself can save time.
-->

3
.gitmodules vendored

@ -1,3 +0,0 @@
[submodule "docs/_themes"]
path = docs/_themes
url = https://github.com/mitsuhiko/flask-sphinx-themes.git

42
.travis.yml

@ -4,49 +4,25 @@ language: python
matrix: matrix:
include: include:
- python: 3.6 - python: 3.6
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: 3.5 - python: 3.5
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: 3.4 - python: 3.4
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: 3.3 - python: 3.3
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: 2.7 - python: 2.7
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: 2.6 - python: 2.6
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: pypy - python: pypy
env: TOXENV=py-release,codecov env: TOXENV=py,codecov
- python: nightly - python: nightly
env: TOXENV=py-release env: TOXENV=py
- python: 3.6 - python: 3.6
env: TOXENV=docs-html env: TOXENV=docs-html
- python: 3.6 - python: 3.6
env: TOXENV=py-release-simplejson,codecov env: TOXENV=py-simplejson,codecov
- python: 2.7
env: TOXENV=py-release-simplejson,codecov
- python: pypy
env: TOXENV=py-release-simplejson,codecov
- python: 3.6
env: TOXENV=py-devel,codecov
- python: 3.3
env: TOXENV=py-devel,codecov
- python: 2.7
env: TOXENV=py-devel,codecov
- python: 2.6
env: TOXENV=py-devel,codecov
- python: pypy
env: TOXENV=py-devel,codecov
- python: 3.6
env: TOXENV=py-lowest,codecov
- python: 3.3
env: TOXENV=py-lowest,codecov
- python: 2.7
env: TOXENV=py-lowest,codecov
- python: 2.6
env: TOXENV=py-lowest,codecov
- python: pypy
env: TOXENV=py-lowest,codecov
install: install:
- pip install tox - pip install tox

44
CHANGES

@ -8,16 +8,19 @@ Version 0.13
Major release, unreleased Major release, unreleased
- Make `app.run()` into a noop if a Flask application is run from the - Minimum Werkzeug version bumped to 0.9, but please use the latest version.
development server on the command line. This avoids some behavior that - Minimum Click version bumped to 4, but please use the latest version.
- 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. was confusing to debug for newcomers.
- Change default configuration `JSONIFY_PRETTYPRINT_REGULAR=False`. jsonify() - Change default configuration ``JSONIFY_PRETTYPRINT_REGULAR=False``.
method returns compressed response by default, and pretty response in ``jsonify()`` method returns compressed response by default, and pretty
debug mode. response in debug mode. (`#2193`_)
- Change Flask.__init__ to accept two new keyword arguments, ``host_matching`` - Change ``Flask.__init__`` to accept two new keyword arguments,
and ``static_host``. This enables ``host_matching`` to be set properly by the ``host_matching`` and ``static_host``. This enables ``host_matching`` to be
time the constructor adds the static route, and enables the static route to set properly by the time the constructor adds the static route, and enables
be properly associated with the required host. (``#1559``) the static route to be properly associated with the required host.
(``#1559``)
- ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) - ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_)
- Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``. - Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``.
(`#2017`_) (`#2017`_)
@ -46,18 +49,41 @@ Major release, unreleased
work with the ``flask`` command. If they take a single parameter or a work with the ``flask`` command. If they take a single parameter or a
parameter named ``script_info``, the ``ScriptInfo`` object will be passed. parameter named ``script_info``, the ``ScriptInfo`` object will be passed.
(`#2319`_) (`#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`_)
- ``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`_)
.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1621: https://github.com/pallets/flask/pull/1621
.. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1898: https://github.com/pallets/flask/pull/1898
.. _#1936: https://github.com/pallets/flask/pull/1936 .. _#1936: https://github.com/pallets/flask/pull/1936
.. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2017: https://github.com/pallets/flask/pull/2017
.. _#2193: https://github.com/pallets/flask/pull/2193
.. _#2223: https://github.com/pallets/flask/pull/2223 .. _#2223: https://github.com/pallets/flask/pull/2223
.. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2254: https://github.com/pallets/flask/pull/2254
.. _#2256: https://github.com/pallets/flask/pull/2256 .. _#2256: https://github.com/pallets/flask/pull/2256
.. _#2259: https://github.com/pallets/flask/pull/2259 .. _#2259: https://github.com/pallets/flask/pull/2259
.. _#2282: https://github.com/pallets/flask/pull/2282 .. _#2282: https://github.com/pallets/flask/pull/2282
.. _#2288: https://github.com/pallets/flask/pull/2288
.. _#2297: https://github.com/pallets/flask/pull/2297 .. _#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 .. _#2319: https://github.com/pallets/flask/pull/2319
.. _#2326: https://github.com/pallets/flask/pull/2326
.. _#2348: https://github.com/pallets/flask/pull/2348
Version 0.12.2 Version 0.12.2
-------------- --------------

185
CONTRIBUTING.rst

@ -1,46 +1,75 @@
==========================
How to contribute to Flask How to contribute to Flask
========================== ==========================
Thanks for considering contributing to Flask. Thank you for considering contributing to Flask!
Support questions 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 .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?sort=linked
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 <https://stackoverflow.com/>`_ is also worth considering.
Reporting issues Reporting issues
================ ----------------
- Under which versions of Python does this happen? This is even more important - Describe what you expected to happen.
if your issue is encoding related. - 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 .. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve
fixed in the repository.
Submitting patches Submitting patches
================== ------------------
- Include tests if your patch is supposed to solve a bug, and explain - 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 clearly under which circumstances the bug happens. Make sure the test fails
without your patch. without your patch.
- Try to follow `PEP8`_, but you may ignore the line length limit if following
- Try to follow `PEP8 <https://www.python.org/dev/peps/pep-0008/>`_, but you it would make the code uglier.
may ignore the line-length-limit if following it would make the code uglier.
First time setup First time setup
---------------- ~~~~~~~~~~~~~~~~
- Download and install the `latest version of git`_. - Download and install the `latest version of git`_.
- Configure git with your `username`_ and `email`_. - Configure git with your `username`_ and `email`_::
git config --global user.name 'your name'
git config --global user.email 'your email'
- Make sure you have a `GitHub account`_. - Make sure you have a `GitHub account`_.
- Fork Flask to your GitHub account by clicking the `Fork`_ button. - Fork Flask to your GitHub account by clicking the `Fork`_ button.
- `Clone`_ your GitHub fork locally. - `Clone`_ your GitHub fork locally::
- Add the main repository as a remote to update later.
``git remote add pallets https://github.com/pallets/flask`` git clone https://github.com/{username}/flask
cd flask
- Add the main repository as a remote to update later::
git remote add pallets https://github.com/pallets/flask
git fetch pallets
- Create a virtualenv::
python3 -m venv env
. env/bin/activate
# or "env\Scripts\activate" on Windows
- Install Flask in editable mode with development dependencies::
pip install -e ".[dev]"
.. _GitHub account: https://github.com/join .. _GitHub account: https://github.com/join
.. _latest version of git: https://git-scm.com/downloads .. _latest version of git: https://git-scm.com/downloads
@ -50,7 +79,7 @@ First time setup
.. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork
Start coding Start coding
------------ ~~~~~~~~~~~~
- Create a branch to identify the issue you would like to work on (e.g. - Create a branch to identify the issue you would like to work on (e.g.
``2287-dry-test-suite``) ``2287-dry-test-suite``)
@ -68,98 +97,70 @@ Start coding
.. _contributing-testsuite: .. _contributing-testsuite:
Running the testsuite Running the tests
--------------------- ~~~~~~~~~~~~~~~~~
You probably want to set up a `virtualenv Run the basic test suite with::
<https://virtualenv.readthedocs.io/en/latest/index.html>`_.
The minimal requirement for running the testsuite is ``pytest``. You can pytest
install it with::
pip install 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.
Clone this repository:: 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.6, 2.7, 3.3,
3.4, 3.5 3.6, and PyPy 2.7 installed to run all of the environments. Then run::
git clone https://github.com/pallets/flask.git tox
Install Flask as an editable package using the current source::
cd flask
pip install --editable .
Running the testsuite
---------------------
The minimal requirement for running the testsuite is ``pytest``. You can
install it with::
pip install pytest
Then you can run the testsuite with::
pytest tests/
**Shortcut**: ``make test`` will ensure ``pytest`` is installed, and run it.
With only pytest 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.
If you really want to test everything, you will have to install ``tox`` instead
of ``pytest``. You can install it with::
pip install tox
The ``tox`` command will then run all tests against multiple combinations
Python versions and dependency versions.
**Shortcut**: ``make tox-test`` will ensure ``tox`` is installed, and run it.
Running test coverage 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
pytest --cov=flask tests/ Read more about `coverage <https://coverage.readthedocs.io>`_.
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.
pytest --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.
**Shortcut**: ``make cov`` will ensure ``pytest-cov`` is installed, run it, display the results, *and* save the HTML report. - ``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
-------------------------------
Caution 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
pushing 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
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. turn off repository checks in the admin panel.
These files can also cause issues while cloning. If you have ::
cloning
-------
The zero-padded file modes files above can cause issues while cloning, too. If you have
::
[fetch] [fetch]
fsckobjects = true fsckobjects = true
or or ::
::
[receive] [receive]
fsckObjects = true fsckObjects = true
set in your git configuration file, cloning this repository will fail. The only
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. 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.

36
Makefile

@ -1,14 +1,28 @@
.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: install-dev:
pip install -r test-requirements.txt pip install -q -e .[dev]
tox -e py-release
cov: test: clean-pyc install-dev
pip install -r test-requirements.txt -q pytest
FLASK_DEBUG= py.test --cov-report term --cov-report html --cov=flask --cov=examples tests examples
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: audit:
python setup.py audit python setup.py audit
@ -16,9 +30,6 @@ audit:
release: release:
python scripts/make-release.py python scripts/make-release.py
ext-test:
python tests/flaskext_test.py --browse
clean-pyc: clean-pyc:
find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} +
@ -40,6 +51,3 @@ ebook:
@echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).'
@echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' @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' 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

1
docs/_themes

@ -1 +0,0 @@
Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9

56
docs/appcontext.rst

@ -5,31 +5,37 @@ The Application Context
.. versionadded:: 0.9 .. versionadded:: 0.9
One of the design ideas behind Flask is that there are two different One of the design ideas behind Flask is that there are at least two
“states” in which code is executed. The application setup state in which different “states” in which code is executed:
the application implicitly is on the module level. It starts when the
:class:`Flask` object is instantiated, and it implicitly ends when the 1. The application setup state, in which the application implicitly is
first request comes in. While the application is in this state a few on the module level.
assumptions are true:
This state starts when the :class:`Flask` object is instantiated, and
- the programmer can modify the application object safely. it implicitly ends when the first request comes in. While the
- no request handling happened so far application is in this state, a few assumptions are true:
- 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 programmer can modify the application object safely.
the application object you're currently creating or modifying. - no request handling happened so far
- you have to have a reference to the application object in order to
In contrast, during request handling, a couple of other rules exist: modify it, there is no magic proxy that can give you a reference to
the application object you're currently creating or modifying.
- while a request is active, the context local objects
(:data:`flask.request` and others) point to the current request. 2. In contrast, in the request handling state, a couple of other rules
- any code can get hold of these objects at any time. exist:
There is a third state which is sitting in between a little bit. - while a request is active, the context local objects
Sometimes you are dealing with an application in a way that is similar to (:data:`flask.request` and others) point to the current request.
how you interact with applications during request handling; just that there - any code can get hold of these objects at any time.
is no request active. Consider, for instance, that you're sitting in an
interactive Python shell and interacting with the application, or a 3. There is also a third state somewhere in between 'module-level' and
command line application. '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` The application context is what powers the :data:`~flask.current_app`
context local. context local.

21
docs/conf.py

@ -22,7 +22,6 @@ BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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. # 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__)) sys.path.append(os.path.dirname(__file__))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -121,7 +120,7 @@ exclude_patterns = ['_build']
# html_theme_options = {} # html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
@ -273,21 +272,9 @@ intersphinx_mapping = {
'blinker': ('https://pythonhosted.org/blinker/', None) 'blinker': ('https://pythonhosted.org/blinker/', None)
} }
try: html_theme_options = {
__import__('flask_theme_support') 'touch_icon': 'touch-icon.png'
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)
# unwrap decorators # unwrap decorators
def unwrap_decorators(): def unwrap_decorators():

412
docs/config.rst

@ -3,8 +3,6 @@
Configuration Handling Configuration Handling
====================== ======================
.. versionadded:: 0.3
Applications need some kind of configuration. There are different settings Applications need some kind of configuration. There are different settings
you might want to change depending on the application environment like you might want to change depending on the application environment like
toggling the debug mode, setting the secret key, and other such toggling the debug mode, setting the secret key, and other such
@ -54,182 +52,253 @@ method::
$ flask run $ flask run
(On Windows you need to use ``set`` instead of ``export``). (On Windows you need to use ``set`` instead of ``export``).
``app.debug`` and ``app.config['DEBUG']`` are not compatible with ``app.debug`` and ``app.config['DEBUG']`` are not compatible with
  the :command:`flask` script. They only worked when using ``Flask.run()``   the :command:`flask` script. They only worked when using ``Flask.run()``
method. method.
Builtin Configuration Values Builtin Configuration Values
---------------------------- ----------------------------
The following configuration values are used internally by Flask: The following configuration values are used internally by Flask:
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| .. py:data:: DEBUG
================================= ========================================= Enable debug mode. When using the development server with ``flask run`` or
``DEBUG`` enable/disable debug mode when using ``app.run``, an interactive debugger will be shown for unhanlded
``Flask.run()`` method to start server exceptions, and the server will be reloaded when code changes.
``TESTING`` enable/disable testing mode
``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the **Do not enable debug mode in production.**
propagation of exceptions. If not set or
explicitly set to ``None`` this is Default: ``False``
implicitly true if either ``TESTING`` or
``DEBUG`` is true. .. py:data:: TESTING
``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in
debug mode the request context is not Enable testing mode. Exceptions are propagated rather than handled by the
popped on exceptions to enable debuggers the app's error handlers. Extensions may also change their behavior to
to introspect the data. This can be facilitate easier testing. You should enable this in your own tests.
disabled by this key. You can also use
this setting to force-enable it for non Default: ``False``
debug execution which might be useful to
debug production applications (but also .. py:data:: PROPAGATE_EXCEPTIONS
very risky).
``SECRET_KEY`` the secret key Exceptions are re-raised rather than being handled by the app's error
``SESSION_COOKIE_NAME`` the name of the session cookie handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG``
``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If is enabled.
this is not set, the cookie will be
valid for all subdomains of Default: ``None``
``SERVER_NAME``.
``SESSION_COOKIE_PATH`` the path for the session cookie. If .. py:data:: PRESERVE_CONTEXT_ON_EXCEPTION
this is not set the cookie will be valid
for all of ``APPLICATION_ROOT`` or if Don't pop the request context when an exception occurs. If not set, this
that is not set for ``'/'``. is true if ``DEBUG`` is true. This allows debuggers to introspect the
``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set request data on errors, and should normally not need to be set directly.
with the httponly flag. Defaults to
``True``. Default: ``None``
``SESSION_COOKIE_SECURE`` controls if the cookie should be set
with the secure flag. Defaults to .. py:data:: TRAP_HTTP_EXCEPTIONS
``False``.
``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as If there is no handler for an ``HTTPException``-type exception, re-raise it
:class:`datetime.timedelta` object. to be handled by the interactive debugger instead of returning it as a
Starting with Flask 0.8 this can also be simple error response.
an integer representing seconds.
``SESSION_REFRESH_EACH_REQUEST`` this flag controls how permanent Default: ``False``
sessions are refreshed. If set to ``True``
(which is the default) then the cookie .. py:data:: TRAP_BAD_REQUEST_ERRORS``
is refreshed each request which
automatically bumps the lifetime. If Trying to access a key that doesn't exist from request dicts like ``args``
set to ``False`` a `set-cookie` header is and ``form`` will return a 400 Bad Request error page. Enable this to treat
only sent if the session is modified. the error as an unhandled exception instead so that you get the interactive
Non permanent sessions are not affected debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If
by this. unset, it is enabled in debug mode.
``USE_X_SENDFILE`` enable/disable x-sendfile
``LOGGER_NAME`` the name of the logger Default: ``None``
``LOGGER_HANDLER_POLICY`` the policy of the default logging
handler. The default is ``'always'`` .. py:data:: SECRET_KEY
which means that the default logging
handler is always active. ``'debug'`` A secret key that will be used for securely signing the session cookie
will only activate logging in debug and can be used for any other security related needs by extensions or your
mode, ``'production'`` will only log in application. It should be a long random string of bytes, although unicode
production and ``'never'`` disables it is accepted too. For example, copy the output of this to your config::
entirely.
``SERVER_NAME`` the name and port number of the server. python -c 'import os; print(os.urandom(32))'
Required for subdomain support (e.g.:
``'myapp.dev:5000'``) Note that **Do not reveal the secret key when posting questions or committing code.**
localhost does not support subdomains so
setting this to “localhost” does not Default: ``None``
help. Setting a ``SERVER_NAME`` also
by default enables URL generation .. py:data:: SESSION_COOKIE_NAME
without a request context but with an
application context. The name of the session cookie. Can be changed in case you already have a
``APPLICATION_ROOT`` The path value used for the session cookie with the same name.
cookie if ``SESSION_COOKIE_PATH`` isn't
set. If it's also ``None`` ``'/'`` is used. Default: ``'session'``
Note that to actually serve your Flask
app under a subpath you need to tell .. py:data:: SESSION_COOKIE_DOMAIN
your WSGI container the ``SCRIPT_NAME``
WSGI environment variable. The domain match rule that the session cookie will be valid for. If not
``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will set, the cookie will be valid for all subdomains of ``SERVER_NAME``. If
reject incoming requests with a ``False``, the cookie's domain will not be set.
content length greater than this by
returning a 413 status code. Default: ``None``
``SEND_FILE_MAX_AGE_DEFAULT`` Default cache control max age to use with
:meth:`~flask.Flask.send_static_file` (the .. py:data:: SESSION_COOKIE_PATH
default static file handler) and
:func:`~flask.send_file`, as The path that the session cookie will be valid for. If not set, the cookie
:class:`datetime.timedelta` or as seconds. will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set.
Override this value on a per-file
basis using the Default: ``None``
:meth:`~flask.Flask.get_send_file_max_age`
hook on :class:`~flask.Flask` or .. py:data:: SESSION_COOKIE_HTTPONLY
:class:`~flask.Blueprint`,
respectively. Defaults to 43200 (12 hours). Browsers will not allow JavaScript access to cookies marked as "HTTP only"
``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will for security.
not execute the error handlers of HTTP
exceptions but instead treat the Default: ``True``
exception like any other and bubble it
through the exception stack. This is .. py:data:: SESSION_COOKIE_SECURE
helpful for hairy debugging situations
where you have to find out where an HTTP Browsers will only send cookies with requests over HTTPS if the cookie is
exception is coming from. marked "secure". The application must be served over HTTPS for this to make
``TRAP_BAD_REQUEST_ERRORS`` Werkzeug's internal data structures that sense.
deal with request specific data will
raise special key errors that are also Default: ``False``
bad request exceptions. Likewise many
operations can implicitly fail with a .. py:data:: PERMANENT_SESSION_LIFETIME
BadRequest exception for consistency.
Since it's nice for debugging to know If ``session.permanent`` is true, the cookie's max age will be set to this
why exactly it failed this flag can be number of seconds. Can either be a :class:`datetime.timedelta` or an
used to debug those situations. If this ``int``.
config is set to ``True`` you will get
a regular traceback instead. Default: ``timedelta(days=31)`` (``2678400`` seconds)
``PREFERRED_URL_SCHEME`` The URL scheme that should be used for
URL generation if no URL scheme is .. py:data:: SESSION_REFRESH_EACH_REQUEST
available. This defaults to ``http``.
``JSON_AS_ASCII`` By default Flask serialize object to Control whether the cookie is sent with every response when
ascii-encoded JSON. If this is set to ``session.permanent`` is true. Sending the cookie every time (the default)
``False`` Flask will not encode to ASCII can more reliably keep the session from expiring, but uses more bandwidth.
and output strings as-is and return Non-permanent sessions are not affected.
unicode strings. ``jsonify`` will
automatically encode it in ``utf-8`` Default: ``True``
then for transport for instance.
``JSON_SORT_KEYS`` By default Flask will serialize JSON .. py:data:: USE_X_SENDFILE
objects in a way that the keys are
ordered. This is done in order to When serving files, set the ``X-Sendfile`` header instead of serving the
ensure that independent of the hash seed data with Flask. Some web servers, such as Apache, recognize this and serve
of the dictionary the return value will the data more efficiently. This only makes sense when using such a server.
be consistent to not trash external HTTP
caches. You can override the default Default: ``False``
behavior by changing this variable.
This is not recommended but might give .. py:data:: SEND_FILE_MAX_AGE_DEFAULT
you a performance improvement on the
cost of cacheability. When serving files, set the cache control max age to this number of
``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` or the Flask app seconds. Can either be a :class:`datetime.timedelta` or an ``int``.
is running in debug mode, jsonify responses Override this value on a per-file basis using
will be pretty printed. :meth:`~flask.Flask.get_send_file_max_age` on the application or blueprint.
``JSONIFY_MIMETYPE`` MIME type used for jsonify responses.
``TEMPLATES_AUTO_RELOAD`` Whether to check for modifications of Default: ``timedelta(hours=12)`` (``43200`` seconds)
the template source and reload it
automatically. By default the value is .. py:data:: LOGGER_NAME
``None`` which means that Flask checks
original file only in debug mode. The name of the logger that the Flask application sets up. If not set,
``EXPLAIN_TEMPLATE_LOADING`` If this is enabled then every attempt to it will take the import name passed to ``Flask.__init__``.
load a template will write an info
message to the logger explaining the Default: ``None``
attempts to locate the template. This
can be useful to figure out why .. py:data:: LOGGER_HANDLER_POLICY
templates cannot be found or wrong
templates appear to be loaded. When to activate the application's logger handler. ``'always'`` always
================================= ========================================= enables it, ``'debug'`` only activates it in debug mode, ``'production'``
only activates it when not in debug mode, and ``'never'`` never enables it.
.. admonition:: More on ``SERVER_NAME``
Default: ``'always'``
The ``SERVER_NAME`` key is used for the subdomain support. Because
Flask cannot guess the subdomain part without the knowledge of the .. py:data:: SERVER_NAME
actual server name, this is required if you want to work with
subdomains. This is also used for the session cookie. Inform the application what host and port it is bound to. Required for
subdomain route matching support.
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 If set, will be used for the session cookie domain if
browsers will not allow cross-subdomain cookies to be set on a ``SESSION_COOKIE_DOMAIN`` is not set. Modern web browsers will not allow
server name without dots in it. So if your server name is setting cookies for domains without a dot. To use a domain locally,
``'localhost'`` you will not be able to set a cookie for add any names that should route to the app to your ``hosts`` file. ::
``'localhost'`` and every subdomain of it. Please choose a different
server name in that case, like ``'myapplication.local'`` and add 127.0.0.1 localhost.dev
this name + the subdomains you want to use into your host config
or setup a local `bind`_. If set, ``url_for`` can generate external URLs with only an application
context instead of a request context.
.. _bind: https://www.isc.org/downloads/bind/
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 .. versionadded:: 0.4
``LOGGER_NAME`` ``LOGGER_NAME``
@ -477,3 +546,4 @@ Example usage for both::
# or via open_instance_resource: # or via open_instance_resource:
with app.open_instance_resource('application.cfg') as f: with app.open_instance_resource('application.cfg') as f:
config = f.read() config = f.read()

1
docs/contents.rst.inc

@ -59,3 +59,4 @@ Design notes, legal information and changelog are here for the interested.
upgrading upgrading
changelog changelog
license license
contributing

1
docs/contributing.rst

@ -0,0 +1 @@
.. include:: ../CONTRIBUTING.rst

71
docs/errorhandling.rst

@ -76,49 +76,72 @@ Error handlers
You might want to show custom error pages to the user when an error occurs. You might want to show custom error pages to the user when an error occurs.
This can be done by registering error handlers. This can be done by registering error handlers.
Error handlers are normal :ref:`views` but instead of being registered for An error handler is a normal view function that return a response, but instead
routes, they are registered for exceptions that are raised while trying to of being registered for a route, it is registered for an exception or HTTP
do something else. status code that would is raised while trying to handle a request.
Registering Registering
``````````` ```````````
Register error handlers using :meth:`~flask.Flask.errorhandler` or Register handlers by decorating a function with
:meth:`~flask.Flask.register_error_handler`:: :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) @app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e): def handle_bad_request(e):
return 'bad request!' return 'bad request!', 400
app.register_error_handler(400, lambda e: 'bad request!') # 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.HTTPException` subclasses like
:exc:`~werkzeug.exceptions.BadRequest` from the example and their HTTP codes :exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable
are interchangeable when handed to the registration methods or decorator when registering handlers. (``BadRequest.code == 400``)
(``BadRequest.code == 400``).
You are however not limited to :exc:`~werkzeug.exceptions.HTTPException` Non-standard HTTP codes cannot be registered by code because they are not known
or HTTP status codes but can register a handler for every exception class you by Werkzeug. Instead, define a subclass of
like. :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 raise InsufficientStorage()
they are registered for instead of the order they are registered in.
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 Handling
```````` ````````
Once an exception instance is raised, its class hierarchy is traversed, When an exception is caught by Flask while handling a request, it is first
and searched for in the exception classes for which handlers are registered. looked up by code. If no handler is registered for the code, it is looked up
The most specific handler is selected. 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`, is registered for :exc:`ConnectionError` and :exc:`ConnectionRefusedError`,
the more specific :exc:`ConnectionRefusedError` handler is called on the the more specific :exc:`ConnectionRefusedError` handler is called with the
exception instance, and its response is shown to the user. 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.
Error Mails Error Mails
----------- -----------

86
docs/flaskext.py

@ -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'
}

102
docs/security.rst

@ -104,3 +104,105 @@ vulnerabilities
<https://github.com/pallets/flask/issues/248#issuecomment-59934857>`_, so <https://github.com/pallets/flask/issues/248#issuecomment-59934857>`_, so
this behavior was changed and :func:`~flask.jsonify` now supports serializing this behavior was changed and :func:`~flask.jsonify` now supports serializing
arrays. 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.haders['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
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
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`` ensures that cookies can only be requested from the same
domain that created them. It is not supported by Flask yet.
::
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
)
response.set_cookie('username', 'flask', secure=True, httponly=True)
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies
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

8
examples/flaskr/README

@ -9,9 +9,11 @@
~ How do I use it? ~ How do I use it?
1. edit the configuration in the flaskr.py file or 1. edit the configuration in the factory.py file or
export an FLASKR_SETTINGS environment variable export an FLASKR_SETTINGS environment variable
pointing to a configuration file. 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 2. install the app from the root of the project directory
@ -19,7 +21,7 @@
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: 4. initialize the database with this command:

1
examples/flaskr/flaskr/__init__.py

@ -1 +0,0 @@
from .flaskr import app

0
examples/flaskr/flaskr/blueprints/__init__.py

55
examples/flaskr/flaskr/flaskr.py → examples/flaskr/flaskr/blueprints/flaskr.py

@ -10,29 +10,18 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import os
from sqlite3 import dbapi2 as sqlite3 from sqlite3 import dbapi2 as sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, \ from flask import Blueprint, request, session, g, redirect, url_for, abort, \
render_template, flash render_template, flash, current_app
# create our little application :) # create our blueprint :)
app = Flask(__name__) bp = Blueprint('flaskr', __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)
def connect_db(): def connect_db():
"""Connects to the specific database.""" """Connects to the specific database."""
rv = sqlite3.connect(app.config['DATABASE']) rv = sqlite3.connect(current_app.config['DATABASE'])
rv.row_factory = sqlite3.Row rv.row_factory = sqlite3.Row
return rv return rv
@ -40,18 +29,11 @@ def connect_db():
def init_db(): def init_db():
"""Initializes the database.""" """Initializes the database."""
db = get_db() 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.cursor().executescript(f.read())
db.commit() db.commit()
@app.cli.command('initdb')
def initdb_command():
"""Creates the database tables."""
init_db()
print('Initialized the database.')
def get_db(): def get_db():
"""Opens a new database connection if there is none yet for the """Opens a new database connection if there is none yet for the
current application context. current application context.
@ -61,14 +43,7 @@ def get_db():
return g.sqlite_db return g.sqlite_db
@app.teardown_appcontext @bp.route('/')
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('/')
def show_entries(): def show_entries():
db = get_db() db = get_db()
cur = db.execute('select title, text from entries order by id desc') 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) return render_template('show_entries.html', entries=entries)
@app.route('/add', methods=['POST']) @bp.route('/add', methods=['POST'])
def add_entry(): def add_entry():
if not session.get('logged_in'): if not session.get('logged_in'):
abort(401) abort(401)
@ -85,26 +60,26 @@ def add_entry():
[request.form['title'], request.form['text']]) [request.form['title'], request.form['text']])
db.commit() db.commit()
flash('New entry was successfully posted') 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(): def login():
error = None error = None
if request.method == 'POST': if request.method == 'POST':
if request.form['username'] != app.config['USERNAME']: if request.form['username'] != current_app.config['USERNAME']:
error = 'Invalid username' error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']: elif request.form['password'] != current_app.config['PASSWORD']:
error = 'Invalid password' error = 'Invalid password'
else: else:
session['logged_in'] = True session['logged_in'] = True
flash('You were logged in') 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) return render_template('login.html', error=error)
@app.route('/logout') @bp.route('/logout')
def logout(): def logout():
session.pop('logged_in', None) session.pop('logged_in', None)
flash('You were logged out') flash('You were logged out')
return redirect(url_for('show_entries')) return redirect(url_for('flaskr.show_entries'))

64
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='development key',
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()

4
examples/flaskr/flaskr/templates/layout.html

@ -5,9 +5,9 @@
<h1>Flaskr</h1> <h1>Flaskr</h1>
<div class="metanav"> <div class="metanav">
{% if not session.logged_in %} {% if not session.logged_in %}
<a href="{{ url_for('login') }}">log in</a> <a href="{{ url_for('flaskr.login') }}">log in</a>
{% else %} {% else %}
<a href="{{ url_for('logout') }}">log out</a> <a href="{{ url_for('flaskr.logout') }}">log out</a>
{% endif %} {% endif %}
</div> </div>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}

2
examples/flaskr/flaskr/templates/login.html

@ -2,7 +2,7 @@
{% block body %} {% block body %}
<h2>Login</h2> <h2>Login</h2>
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %} {% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('login') }}" method="post"> <form action="{{ url_for('flaskr.login') }}" method="post">
<dl> <dl>
<dt>Username: <dt>Username:
<dd><input type="text" name="username"> <dd><input type="text" name="username">

2
examples/flaskr/flaskr/templates/show_entries.html

@ -1,7 +1,7 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
{% if session.logged_in %} {% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method="post" class="add-entry"> <form action="{{ url_for('flaskr.add_entry') }}" method="post" class="add-entry">
<dl> <dl>
<dt>Title: <dt>Title:
<dd><input type="text" size="30" name="title"> <dd><input type="text" size="30" name="title">

4
examples/flaskr/setup.cfg

@ -1,2 +1,2 @@
[tool:pytest] [aliases]
test=pytest test=pytest

15
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( setup(
name='flaskr', name='flaskr',
packages=['flaskr'], packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'flask', 'flask',

59
examples/flaskr/tests/test_flaskr.py

@ -12,19 +12,38 @@
import os import os
import tempfile import tempfile
import pytest import pytest
from flaskr import flaskr from flaskr.factory import create_app
from flaskr.blueprints.flaskr import init_db
@pytest.fixture @pytest.fixture
def client(): def app(request):
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True db_fd, temp_db_location = tempfile.mkstemp()
client = flaskr.app.test_client() config = {
with flaskr.app.app_context(): 'DATABASE': temp_db_location,
flaskr.init_db() 'TESTING': True,
yield client 'DB_FD': db_fd
os.close(db_fd) }
os.unlink(flaskr.app.config['DATABASE'])
app = create_app(config=config)
with app.app_context():
init_db()
yield app
@pytest.fixture
def client(request, app):
client = app.test_client()
def teardown():
os.close(app.config['DB_FD'])
os.unlink(app.config['DATABASE'])
request.addfinalizer(teardown)
return client
def login(client, username, password): def login(client, username, password):
@ -44,25 +63,25 @@ def test_empty_db(client):
assert b'No entries here so far' in rv.data 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""" """Make sure login and logout works"""
rv = login(client, flaskr.app.config['USERNAME'], rv = login(client, app.config['USERNAME'],
flaskr.app.config['PASSWORD']) app.config['PASSWORD'])
assert b'You were logged in' in rv.data assert b'You were logged in' in rv.data
rv = logout(client) rv = logout(client)
assert b'You were logged out' in rv.data assert b'You were logged out' in rv.data
rv = login(client, flaskr.app.config['USERNAME'] + 'x', rv = login(client,app.config['USERNAME'] + 'x',
flaskr.app.config['PASSWORD']) app.config['PASSWORD'])
assert b'Invalid username' in rv.data assert b'Invalid username' in rv.data
rv = login(client, flaskr.app.config['USERNAME'], rv = login(client, app.config['USERNAME'],
flaskr.app.config['PASSWORD'] + 'x') app.config['PASSWORD'] + 'x')
assert b'Invalid password' in rv.data assert b'Invalid password' in rv.data
def test_messages(client): def test_messages(client, app):
"""Test that messages work""" """Test that messages work"""
login(client, flaskr.app.config['USERNAME'], login(client, app.config['USERNAME'],
flaskr.app.config['PASSWORD']) app.config['PASSWORD'])
rv = client.post('/add', data=dict( rv = client.post('/add', data=dict(
title='<Hello>', title='<Hello>',
text='<strong>HTML</strong> allowed here' text='<strong>HTML</strong> allowed here'

2
examples/minitwit/minitwit/minitwit.py

@ -25,7 +25,7 @@ DEBUG = True
SECRET_KEY = 'development key' SECRET_KEY = 'development key'
# create our little application :) # create our little application :)
app = Flask(__name__) app = Flask('minitwit')
app.config.from_object(__name__) app.config.from_object(__name__)
app.config.from_envvar('MINITWIT_SETTINGS', silent=True) app.config.from_envvar('MINITWIT_SETTINGS', silent=True)

4
examples/patterns/largerapp/yourapplication/__init__.py

@ -1,4 +1,4 @@
from flask import Flask from flask import Flask
app = Flask(__name__) app = Flask('yourapplication')
import yourapplication.views import yourapplication.views

129
flask/app.py

@ -10,6 +10,7 @@
""" """
import os import os
import sys import sys
import warnings
from datetime import timedelta from datetime import timedelta
from functools import update_wrapper from functools import update_wrapper
from itertools import chain from itertools import chain
@ -17,7 +18,8 @@ from threading import Lock
from werkzeug.datastructures import ImmutableDict, Headers from werkzeug.datastructures import ImmutableDict, Headers
from werkzeug.exceptions import BadRequest, HTTPException, \ from werkzeug.exceptions import BadRequest, HTTPException, \
InternalServerError, MethodNotAllowed, default_exceptions InternalServerError, MethodNotAllowed, default_exceptions, \
BadRequestKeyError
from werkzeug.routing import BuildError, Map, RequestRedirect, Rule from werkzeug.routing import BuildError, Map, RequestRedirect, Rule
from . import cli, json from . import cli, json
@ -307,7 +309,7 @@ class Flask(_PackageBoundObject):
'LOGGER_NAME': None, 'LOGGER_NAME': None,
'LOGGER_HANDLER_POLICY': 'always', 'LOGGER_HANDLER_POLICY': 'always',
'SERVER_NAME': None, 'SERVER_NAME': None,
'APPLICATION_ROOT': None, 'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_NAME': 'session',
'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_PATH': None,
@ -316,7 +318,7 @@ class Flask(_PackageBoundObject):
'SESSION_REFRESH_EACH_REQUEST': True, 'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12), 'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12),
'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'PREFERRED_URL_SCHEME': 'http',
@ -925,8 +927,17 @@ class Flask(_PackageBoundObject):
:attr:`secret_key` is set. Instead of overriding this method :attr:`secret_key` is set. Instead of overriding this method
we recommend replacing the :class:`session_interface`. 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`. :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) return self.session_interface.open_session(self, request)
def save_session(self, session, response): def save_session(self, session, response):
@ -934,19 +945,37 @@ class Flask(_PackageBoundObject):
implementation, check :meth:`open_session`. Instead of overriding this implementation, check :meth:`open_session`. Instead of overriding this
method we recommend replacing the :class:`session_interface`. 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 :param session: the session to be saved (a
:class:`~werkzeug.contrib.securecookie.SecureCookie` :class:`~werkzeug.contrib.securecookie.SecureCookie`
object) object)
:param response: an instance of :attr:`response_class` :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) return self.session_interface.save_session(self, session, response)
def make_null_session(self): def make_null_session(self):
"""Creates a new instance of a missing session. Instead of overriding """Creates a new instance of a missing session. Instead of overriding
this method we recommend replacing the :class:`session_interface`. 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 .. 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) return self.session_interface.make_null_session(self)
@setupmethod @setupmethod
@ -1137,7 +1166,9 @@ class Flask(_PackageBoundObject):
@setupmethod @setupmethod
def errorhandler(self, code_or_exception): 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:: error code. Example::
@app.errorhandler(404) @app.errorhandler(404)
@ -1150,21 +1181,6 @@ class Flask(_PackageBoundObject):
def special_exception_handler(error): def special_exception_handler(error):
return 'Database connection failed', 500 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 .. versionadded:: 0.7
Use :meth:`register_error_handler` instead of modifying Use :meth:`register_error_handler` instead of modifying
:attr:`error_handler_spec` directly, for application wide error :attr:`error_handler_spec` directly, for application wide error
@ -1183,6 +1199,7 @@ class Flask(_PackageBoundObject):
return f return f
return decorator return decorator
@setupmethod
def register_error_handler(self, code_or_exception, f): def register_error_handler(self, code_or_exception, f):
"""Alternative error attach function to the :meth:`errorhandler` """Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator decorator that is more straightforward to use for non decorator
@ -1201,11 +1218,18 @@ class Flask(_PackageBoundObject):
""" """
if isinstance(code_or_exception, HTTPException): # old broken behavior if isinstance(code_or_exception, HTTPException): # old broken behavior
raise ValueError( raise ValueError(
'Tried to register a handler for an exception instance {0!r}. ' 'Tried to register a handler for an exception instance {0!r}.'
'Handlers can only be registered for exception classes or HTTP error codes.' ' Handlers can only be registered for exception classes or'
.format(code_or_exception)) ' 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 = self.error_handler_spec.setdefault(key, {}).setdefault(code, {})
handlers[exc_class] = f handlers[exc_class] = f
@ -1310,7 +1334,7 @@ class Flask(_PackageBoundObject):
@setupmethod @setupmethod
def before_request(self, f): def before_request(self, f):
"""Registers a function to run before each request. """Registers a function to run before each request.
For example, this can be used to open a database connection, or to load For example, this can be used to open a database connection, or to load
the logged in user from the session. the logged in user from the session.
@ -1438,12 +1462,12 @@ class Flask(_PackageBoundObject):
"""Register a URL value preprocessor function for all view """Register a URL value preprocessor function for all view
functions in the application. These functions will be called before the functions in the application. These functions will be called before the
:meth:`before_request` functions. :meth:`before_request` functions.
The function can modify the values captured from the matched url before 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 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 common language code value and place it in ``g`` rather than pass it to
every view. every view.
The function is passed the endpoint name and values dict. The return The function is passed the endpoint name and values dict. The return
value is ignored. value is ignored.
""" """
@ -1515,12 +1539,20 @@ class Flask(_PackageBoundObject):
traceback. This is helpful for debugging implicitly raised HTTP traceback. This is helpful for debugging implicitly raised HTTP
exceptions. exceptions.
.. versionchanged:: 1.0
Bad request errors are not trapped by default in debug mode.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
if self.config['TRAP_HTTP_EXCEPTIONS']: if self.config['TRAP_HTTP_EXCEPTIONS']:
return True 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 isinstance(e, BadRequest)
return False return False
def handle_user_exception(self, e): def handle_user_exception(self, e):
@ -1531,16 +1563,30 @@ class Flask(_PackageBoundObject):
function will either return a response value or reraise the function will either return a response value or reraise the
exception with the same traceback. 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 .. versionadded:: 0.7
""" """
exc_type, exc_value, tb = sys.exc_info() exc_type, exc_value, tb = sys.exc_info()
assert exc_value is e assert exc_value is e
# ensure not to trash sys.exc_info() at that point in case someone # ensure not to trash sys.exc_info() at that point in case someone
# wants the traceback preserved in handle_http_exception. Of course # wants the traceback preserved in handle_http_exception. Of course
# we cannot prevent users from trashing it themselves in a custom # we cannot prevent users from trashing it themselves in a custom
# trap_http_exception method so that's their fault then. # 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): if isinstance(e, HTTPException) and not self.trap_http_exception(e):
return self.handle_http_exception(e) return self.handle_http_exception(e)
@ -1732,10 +1778,10 @@ class Flask(_PackageBoundObject):
``str`` (``unicode`` in Python 2) ``str`` (``unicode`` in Python 2)
A response object is created with the string encoded to UTF-8 A response object is created with the string encoded to UTF-8
as the body. as the body.
``bytes`` (``str`` in Python 2) ``bytes`` (``str`` in Python 2)
A response object is created with the bytes as the body. A response object is created with the bytes as the body.
``tuple`` ``tuple``
Either ``(body, status, headers)``, ``(body, status)``, or Either ``(body, status, headers)``, ``(body, status)``, or
``(body, headers)``, where ``body`` is any of the other types ``(body, headers)``, where ``body`` is any of the other types
@ -1744,13 +1790,13 @@ class Flask(_PackageBoundObject):
tuples. If ``body`` is a :attr:`response_class` instance, tuples. If ``body`` is a :attr:`response_class` instance,
``status`` overwrites the exiting value and ``headers`` are ``status`` overwrites the exiting value and ``headers`` are
extended. extended.
:attr:`response_class` :attr:`response_class`
The object is returned unchanged. The object is returned unchanged.
other :class:`~werkzeug.wrappers.Response` class other :class:`~werkzeug.wrappers.Response` class
The object is coerced to :attr:`response_class`. The object is coerced to :attr:`response_class`.
:func:`callable` :func:`callable`
The function is called as a WSGI application. The result is The function is called as a WSGI application. The result is
used to create a response object. used to create a response object.
@ -1845,7 +1891,7 @@ class Flask(_PackageBoundObject):
if self.config['SERVER_NAME'] is not None: if self.config['SERVER_NAME'] is not None:
return self.url_map.bind( return self.url_map.bind(
self.config['SERVER_NAME'], self.config['SERVER_NAME'],
script_name=self.config['APPLICATION_ROOT'] or '/', script_name=self.config['APPLICATION_ROOT'],
url_scheme=self.config['PREFERRED_URL_SCHEME']) url_scheme=self.config['PREFERRED_URL_SCHEME'])
def inject_url_defaults(self, endpoint, values): def inject_url_defaults(self, endpoint, values):
@ -1887,7 +1933,7 @@ class Flask(_PackageBoundObject):
:attr:`url_value_preprocessors` registered with the app and the :attr:`url_value_preprocessors` registered with the app and the
current blueprint (if any). Then calls :attr:`before_request_funcs` current blueprint (if any). Then calls :attr:`before_request_funcs`
registered with the app and the blueprint. registered with the app and the blueprint.
If any :meth:`before_request` handler returns a non-None value, the 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 value is handled as if it was the return value from the view, and
further request handling is stopped. further request handling is stopped.
@ -1932,7 +1978,7 @@ class Flask(_PackageBoundObject):
for handler in funcs: for handler in funcs:
response = handler(response) response = handler(response)
if not self.session_interface.is_null_session(ctx.session): 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 return response
def do_teardown_request(self, exc=_sentinel): def do_teardown_request(self, exc=_sentinel):
@ -2017,10 +2063,19 @@ class Flask(_PackageBoundObject):
def test_request_context(self, *args, **kwargs): def test_request_context(self, *args, **kwargs):
"""Creates a WSGI environment from the given values (see """Creates a WSGI environment from the given values (see
:class:`werkzeug.test.EnvironBuilder` for more information, this :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 from flask.testing import make_test_environ_builder
builder = make_test_environ_builder(self, *args, **kwargs) builder = make_test_environ_builder(self, *args, **kwargs)
try: try:
return self.request_context(builder.get_environ()) return self.request_context(builder.get_environ())
finally: finally:

93
flask/cli.py

@ -9,7 +9,10 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import ast
import inspect
import os import os
import re
import sys import sys
import traceback import traceback
from functools import update_wrapper from functools import update_wrapper
@ -55,20 +58,20 @@ def find_best_app(script_info, module):
' one.'.format(module=module.__name__) ' one.'.format(module=module.__name__)
) )
# Search for app factory callables. # Search for app factory functions.
for attr_name in ('create_app', 'make_app'): for attr_name in ('create_app', 'make_app'):
app_factory = getattr(module, attr_name, None) app_factory = getattr(module, attr_name, None)
if callable(app_factory): if inspect.isfunction(app_factory):
try: try:
app = call_factory(app_factory, script_info) app = call_factory(app_factory, script_info)
if isinstance(app, Flask): if isinstance(app, Flask):
return app return app
except TypeError: except TypeError:
raise NoAppException( raise NoAppException(
'Auto-detected "{callable}()" in module "{module}", but ' 'Auto-detected "{function}()" in module "{module}", but '
'could not call it without specifying arguments.'.format( 'could not call it without specifying arguments.'.format(
callable=attr_name, module=module.__name__ function=attr_name, module=module.__name__
) )
) )
@ -79,18 +82,69 @@ def find_best_app(script_info, module):
) )
def call_factory(func, script_info): def call_factory(app_factory, script_info, arguments=()):
"""Checks if the given app factory function has an argument named """Takes an app factory, a ``script_info` object and optionally a tuple
``script_info`` or just a single argument and calls the function passing of arguments. Checks for the existence of a script_info argument and calls
``script_info`` if so. Otherwise, calls the function without any arguments the app_factory depending on that and the arguments provided.
and returns the result.
""" """
arguments = getargspec(func).args args_spec = getargspec(app_factory)
if 'script_info' in arguments: arg_names = args_spec.args
return func(script_info=script_info) arg_defaults = args_spec.defaults
elif len(arguments) == 1:
return func(script_info) if 'script_info' in arg_names:
return func() 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 find_app_by_string(string, script_info, module):
"""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 . import Flask
function_regex = r'^(?P<name>\w+)(?:\((?P<args>.*)\))?$'
match = re.match(function_regex, string)
if match:
name, args = match.groups()
try:
if args is not None:
args = args.rstrip(' ,')
if args:
args = ast.literal_eval(
"({args}, )".format(args=args))
else:
args = ()
app_factory = getattr(module, name, None)
app = call_factory(app_factory, script_info, args)
else:
attr = getattr(module, name, None)
if inspect.isfunction(attr):
app = call_factory(attr, script_info)
else:
app = attr
if isinstance(app, Flask):
return app
else:
raise RuntimeError('Failed to find application in module '
'"{name}"'.format(name=module))
except TypeError as e:
new_error = NoAppException(
'{e}\nThe app factory "{factory}" in module "{module}" could'
' not be called with the specified arguments (and a'
' script_info argument automatically added if applicable).'
' Did you make sure to use the right number of arguments as'
' well as not using keyword arguments or'
' non-literals?'.format(e=e, factory=string, module=module))
reraise(NoAppException, new_error, sys.exc_info()[2])
else:
raise NoAppException(
'The provided string "{string}" is not a valid variable name'
'or function expression.'.format(string=string))
def prepare_exec_for_file(filename): def prepare_exec_for_file(filename):
@ -148,14 +202,9 @@ def locate_app(script_info, app_id):
mod = sys.modules[module] mod = sys.modules[module]
if app_obj is None: if app_obj is None:
app = find_best_app(script_info, mod) return find_best_app(script_info, mod)
else: else:
app = getattr(mod, app_obj, None) return find_app_by_string(app_obj, script_info, mod)
if app is None:
raise RuntimeError('Failed to find application in module "%s"'
% module)
return app
def find_default_import_path(): def find_default_import_path():

6
flask/ctx.py

@ -329,9 +329,11 @@ class RequestContext(object):
# available. This allows a custom open_session method to use the # available. This allows a custom open_session method to use the
# request context (e.g. code that access database information # request context (e.g. code that access database information
# stored on `g` instead of the appcontext). # stored on `g` instead of the appcontext).
self.session = self.app.open_session(self.request) session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None: if self.session is None:
self.session = self.app.make_null_session() self.session = session_interface.make_null_session(self.app)
def pop(self, exc=_sentinel): def pop(self, exc=_sentinel):
"""Pops the request context and unbinds it by doing that. This will """Pops the request context and unbinds it by doing that. This will

14
flask/helpers.py

@ -1004,17 +1004,3 @@ def is_ip(value):
return True return True
return False return False
def patch_vary_header(response, value):
"""Add a value to the ``Vary`` header if it is not already present."""
header = response.headers.get('Vary', '')
headers = [h for h in (h.strip() for h in header.split(',')) if h]
lower_value = value.lower()
if not any(h.lower() == lower_value for h in headers):
headers.append(value)
updated_header = ', '.join(headers)
response.headers['Vary'] = updated_header

7
flask/sessions.py

@ -18,7 +18,6 @@ from itsdangerous import BadSignature, URLSafeTimedSerializer
from werkzeug.datastructures import CallbackDict from werkzeug.datastructures import CallbackDict
from werkzeug.http import http_date, parse_date from werkzeug.http import http_date, parse_date
from flask.helpers import patch_vary_header
from . import Markup, json from . import Markup, json
from ._compat import iteritems, text_type from ._compat import iteritems, text_type
from .helpers import is_ip, total_seconds from .helpers import is_ip, total_seconds
@ -347,8 +346,8 @@ class SessionInterface(object):
config var if it's set, and falls back to ``APPLICATION_ROOT`` or config var if it's set, and falls back to ``APPLICATION_ROOT`` or
uses ``/`` if it's ``None``. uses ``/`` if it's ``None``.
""" """
return app.config['SESSION_COOKIE_PATH'] or \ return app.config['SESSION_COOKIE_PATH'] \
app.config['APPLICATION_ROOT'] or '/' or app.config['APPLICATION_ROOT']
def get_cookie_httponly(self, app): def get_cookie_httponly(self, app):
"""Returns True if the session cookie should be httponly. This """Returns True if the session cookie should be httponly. This
@ -466,7 +465,7 @@ class SecureCookieSessionInterface(SessionInterface):
# Add a "Vary: Cookie" header if the session was accessed at all. # Add a "Vary: Cookie" header if the session was accessed at all.
if session.accessed: if session.accessed:
patch_vary_header(response, 'Cookie') response.vary.add('Cookie')
if not self.should_set_cookie(app, session): if not self.should_set_cookie(app, session):
return return

45
flask/testing.py

@ -21,19 +21,37 @@ except ImportError:
from urlparse import urlsplit as url_parse 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.""" """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: 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) url = url_parse(path)
base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') base_url = '{0}://{1}/{2}'.format(
if app_root: url_scheme, url.netloc or http_host, app_root.lstrip('/')
base_url += app_root.lstrip('/') )
if url.netloc: path = url.path
path = url.path
if url.query: if url.query:
path += '?' + url.query sep = b'?' if isinstance(url.query, bytes) else '?'
path += sep + url.query
return EnvironBuilder(path, base_url, *args, **kwargs) return EnvironBuilder(path, base_url, *args, **kwargs)
@ -87,7 +105,8 @@ class FlaskClient(Client):
self.cookie_jar.inject_wsgi(environ_overrides) self.cookie_jar.inject_wsgi(environ_overrides)
outer_reqctx = _request_ctx_stack.top outer_reqctx = _request_ctx_stack.top
with app.test_request_context(*args, **kwargs) as c: 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: if sess is None:
raise RuntimeError('Session backend did not open a session. ' raise RuntimeError('Session backend did not open a session. '
'Check the configuration') 'Check the configuration')
@ -106,8 +125,8 @@ class FlaskClient(Client):
_request_ctx_stack.pop() _request_ctx_stack.pop()
resp = app.response_class() resp = app.response_class()
if not app.session_interface.is_null_session(sess): if not session_interface.is_null_session(sess):
app.save_session(sess, resp) session_interface.save_session(app, sess, resp)
headers = resp.get_wsgi_headers(c.request.environ) headers = resp.get_wsgi_headers(c.request.environ)
self.cookie_jar.extract_wsgi(c.request.environ, headers) self.cookie_jar.extract_wsgi(c.request.environ, headers)

14
setup.py

@ -48,14 +48,12 @@ import re
import ast import ast
from setuptools import setup from setuptools import setup
_version_re = re.compile(r'__version__\s+=\s+(.*)') _version_re = re.compile(r'__version__\s+=\s+(.*)')
with open('flask/__init__.py', 'rb') as f: with open('flask/__init__.py', 'rb') as f:
version = str(ast.literal_eval(_version_re.search( version = str(ast.literal_eval(_version_re.search(
f.read().decode('utf-8')).group(1))) f.read().decode('utf-8')).group(1)))
setup( setup(
name='Flask', name='Flask',
version=version, version=version,
@ -76,6 +74,17 @@ setup(
'itsdangerous>=0.21', 'itsdangerous>=0.21',
'click>=4.0', 'click>=4.0',
], ],
extras_require={
'dev': [
'blinker',
'greenlet',
'pytest>=3',
'coverage',
'tox',
'sphinx',
'sphinxcontrib-log-cabinet'
],
},
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Environment :: Web Environment', 'Environment :: Web Environment',
@ -90,6 +99,7 @@ setup(
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries :: Python Modules' 'Topic :: Software Development :: Libraries :: Python Modules'
], ],

15
tests/test_apps/cliapp/factory.py

@ -0,0 +1,15 @@
from __future__ import absolute_import, print_function
from flask import Flask
def create_app():
return Flask('create_app')
def create_app2(foo, bar):
return Flask("_".join(['create_app2', foo, bar]))
def create_app3(foo, bar, script_info):
return Flask("_".join(['create_app3', foo, bar]))

35
tests/test_basic.py

@ -9,20 +9,21 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import pytest import pickle
import re import re
import uuid
import time import time
import flask import uuid
import pickle
from datetime import datetime from datetime import datetime
from threading import Thread 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.http import parse_date
from werkzeug.routing import BuildError from werkzeug.routing import BuildError
import werkzeug.serving
import flask
from flask._compat import text_type
def test_options_work(app, client): def test_options_work(app, client):
@ -529,14 +530,14 @@ def test_session_vary_cookie(app, client):
@app.route('/vary-cookie-header-set') @app.route('/vary-cookie-header-set')
def vary_cookie_header_set(): def vary_cookie_header_set():
response = flask.Response() response = flask.Response()
response.headers['Vary'] = 'Cookie' response.vary.add('Cookie')
flask.session['test'] = 'test' flask.session['test'] = 'test'
return response return response
@app.route('/vary-header-set') @app.route('/vary-header-set')
def vary_header_set(): def vary_header_set():
response = flask.Response() response = flask.Response()
response.headers['Vary'] = 'Accept-Encoding, Accept-Language' response.vary.update(('Accept-Encoding', 'Accept-Language'))
flask.session['test'] = 'test' flask.session['test'] = 'test'
return response return response
@ -875,6 +876,13 @@ def test_error_handling(app, client):
assert b'forbidden' == rv.data assert b'forbidden' == rv.data
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): def test_error_handling_processing(app, client):
app.config['LOGGER_HANDLER_POLICY'] = 'never' app.config['LOGGER_HANDLER_POLICY'] = 'never'
app.testing = False app.testing = False
@ -980,12 +988,17 @@ def test_trapping_of_bad_request_key_errors(app, client):
def fail(): def fail():
flask.request.form['missing_key'] flask.request.form['missing_key']
assert client.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 app.config['TRAP_BAD_REQUEST_ERRORS'] = True
with pytest.raises(KeyError) as e: with pytest.raises(KeyError) as e:
client.get("/fail") client.get("/fail")
assert e.errisinstance(BadRequest) assert e.errisinstance(BadRequest)
assert 'missing_key' in e.value.description
def test_trapping_of_all_http_exceptions(app, client): def test_trapping_of_all_http_exceptions(app, client):

38
tests/test_cli.py

@ -150,15 +150,37 @@ def test_locate_app(test_apps):
script_info = ScriptInfo() script_info = ScriptInfo()
assert locate_app(script_info, "cliapp.app").name == "testapp" assert locate_app(script_info, "cliapp.app").name == "testapp"
assert locate_app(script_info, "cliapp.app:testapp").name == "testapp" assert locate_app(script_info, "cliapp.app:testapp").name == "testapp"
assert locate_app(script_info, "cliapp.factory").name == "create_app"
assert locate_app(
script_info, "cliapp.factory").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app()").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app2('foo', 'bar')"
).name == "create_app2_foo_bar"
assert locate_app(
script_info, "cliapp.factory:create_app2('foo', 'bar', )"
).name == "create_app2_foo_bar"
assert locate_app(
script_info, "cliapp.factory:create_app3('baz', 'qux')"
).name == "create_app3_baz_qux"
assert locate_app(script_info, "cliapp.multiapp:app1").name == "app1" assert locate_app(script_info, "cliapp.multiapp:app1").name == "app1"
pytest.raises(NoAppException, locate_app, pytest.raises(
script_info, "notanpp.py") NoAppException, locate_app, script_info, "notanpp.py")
pytest.raises(NoAppException, locate_app, pytest.raises(
script_info, "cliapp/app") NoAppException, locate_app, script_info, "cliapp/app")
pytest.raises(RuntimeError, locate_app, pytest.raises(
script_info, "cliapp.app:notanapp") RuntimeError, locate_app, script_info, "cliapp.app:notanapp")
pytest.raises(NoAppException, locate_app, pytest.raises(
script_info, "cliapp.importerrorapp") NoAppException, locate_app,
script_info, "cliapp.factory:create_app2('foo')")
pytest.raises(
NoAppException, locate_app,
script_info, "cliapp.factory:create_app ()")
pytest.raises(
NoAppException, locate_app, script_info, "cliapp.importerrorapp")
def test_find_default_import_path(test_apps, monkeypatch, tmpdir): def test_find_default_import_path(test_apps, monkeypatch, tmpdir):

2
tests/test_helpers.py

@ -44,6 +44,7 @@ class TestJSON(object):
def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client): def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client):
app.config['DEBUG'] = True app.config['DEBUG'] = True
app.config['TRAP_BAD_REQUEST_ERRORS'] = False
@app.route('/json', methods=['POST']) @app.route('/json', methods=['POST'])
def post_json(): def post_json():
@ -56,6 +57,7 @@ class TestJSON(object):
def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client): def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client):
app.config['DEBUG'] = False app.config['DEBUG'] = False
app.config['TRAP_BAD_REQUEST_ERRORS'] = False
@app.route('/json', methods=['POST']) @app.route('/json', methods=['POST'])
def post_json(): def post_json():

32
tests/test_testing.py

@ -73,6 +73,38 @@ def test_environ_base_modified(app, client, app_ctx):
assert flask.g.user_agent == 'Bar' assert flask.g.user_agent == 'Bar'
def test_specify_url_scheme(app, client):
@app.route('/')
def index():
return flask.request.url
ctx = app.test_request_context(url_scheme='https')
assert ctx.request.url == 'https://localhost/'
rv = client.get('/', url_scheme='https')
assert rv.data == b'https://localhost/'
def test_blueprint_with_subdomain(app, client):
app.config['SERVER_NAME'] = 'example.com:1234'
app.config['APPLICATION_ROOT'] = '/foo'
bp = flask.Blueprint('company', __name__, subdomain='xxx')
@bp.route('/')
def index():
return flask.request.url
app.register_blueprint(bp)
ctx = app.test_request_context('/', subdomain='xxx')
assert ctx.request.url == 'http://xxx.example.com:1234/foo/'
assert ctx.request.blueprint == bp.name
rv = client.get('/', subdomain='xxx')
assert rv.data == b'http://xxx.example.com:1234/foo/'
def test_redirect_keep_session(app, client, app_ctx): def test_redirect_keep_session(app, client, app_ctx):
app.secret_key = 'testing' app.secret_key = 'testing'

17
tox.ini

@ -1,7 +1,7 @@
[tox] [tox]
envlist = envlist =
py{36,35,34,33,27,26,py}-release py{36,35,34,33,27,26,py}
py{36,27,py}-release-simplejson py{36,27,py}-simplejson
py{36,33,27,26,py}-devel py{36,33,27,26,py}-devel
py{36,33,27,26,py}-lowest py{36,33,27,26,py}-lowest
docs-html docs-html
@ -35,12 +35,10 @@ commands =
pip install -e examples/patterns/largerapp -q pip install -e examples/patterns/largerapp -q
# pytest-cov doesn't seem to play nice with -p # pytest-cov doesn't seem to play nice with -p
coverage run -p -m pytest coverage run -p -m pytest tests examples
[testenv:docs-html] [testenv:docs-html]
deps = deps = sphinx
sphinx
flask-sphinx-themes
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
[testenv:docs-linkcheck] [testenv:docs-linkcheck]
@ -63,3 +61,10 @@ commands =
coverage combine coverage combine
coverage report coverage report
codecov codecov
[testenv:detox]
skip_install = true
deps = detox
commands =
detox -e py{36,35,34,33,27,26,py},py{36,27,py}-simplejson,py{36,33,27,26,py}-devel,py{36,33,27,26,py}-lowest,docs-html
tox -e coverage-report

Loading…
Cancel
Save