`::
+
+ from flask import Flask, render_template
+
+ def page_not_found(e):
+ return render_template('404.html'), 404
+
+ def create_app(config_filename):
+ app = Flask(__name__)
+ app.register_error_handler(404, page_not_found)
+ return app
+
An example template might be this:
.. sourcecode:: html+jinja
- {% extends "layout.html" %}
- {% block title %}Page Not Found{% endblock %}
- {% block body %}
- Page Not Found
- What you were looking for is just not there.
-
go somewhere nice
- {% endblock %}
-
+ {% extends "layout.html" %}
+ {% block title %}Page Not Found{% endblock %}
+ {% block body %}
+
Page Not Found
+ What you were looking for is just not there.
+
go somewhere nice
+ {% endblock %}
diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst
index dc3820be..1c4b0d36 100644
--- a/docs/patterns/fileuploads.rst
+++ b/docs/patterns/fileuploads.rst
@@ -21,7 +21,7 @@ specific upload folder and displays a file to the user. Let's look at the
bootstrapping code for our application::
import os
- from flask import Flask, request, redirect, url_for
+ from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = '/path/to/the/uploads'
@@ -58,7 +58,7 @@ the file and redirects the user to the URL for the uploaded file::
return redirect(request.url)
file = request.files['file']
# if user does not select file, browser also
- # submit a empty part without filename
+ # submit an empty part without filename
if file.filename == '':
flash('No selected file')
return redirect(request.url)
@@ -72,8 +72,8 @@ the file and redirects the user to the URL for the uploaded file::
Upload new File
Upload new File
'''
diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst
index 1bb84f8c..cc149839 100644
--- a/docs/patterns/packages.rst
+++ b/docs/patterns/packages.rst
@@ -65,7 +65,7 @@ that tells Flask where to find the application instance::
export FLASK_APP=yourapplication
If you are outside of the project directory make sure to provide the exact
-path to your application directory. Similiarly you can turn on "debug
+path to your application directory. Similarly you can turn on "debug
mode" with this environment variable::
export FLASK_DEBUG=true
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 7ce8a90f..d56fa8e2 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -50,7 +50,14 @@ to tell your terminal the application to work with by exporting the
$ flask run
* Running on http://127.0.0.1:5000/
-If you are on Windows you need to use ``set`` instead of ``export``.
+If you are on Windows, the environment variable syntax depends on command line
+interpreter. On Command Prompt::
+
+ C:\path\to\app>set FLASK_APP=hello.py
+
+And on PowerShell::
+
+ PS C:\path\to\app> $env:FLASK_APP = "hello.py"
Alternatively you can use :command:`python -m flask`::
@@ -153,20 +160,22 @@ Screenshot of the debugger in action:
:class: screenshot
:alt: screenshot of debugger in action
+More information on using the debugger can be found in the `Werkzeug
+documentation`_.
+
+.. _Werkzeug documentation: http://werkzeug.pocoo.org/docs/debug/#using-the-debugger
+
Have another debugger in mind? See :ref:`working-with-debuggers`.
Routing
-------
-Modern web applications have beautiful URLs. This helps people remember
-the URLs, which is especially handy for applications that are used from
-mobile devices with slower network connections. If the user can directly
-go to the desired page without having to hit the index page it is more
-likely they will like the page and come back next time.
+Modern web applications use meaningful URLs to help users. Users are more
+likely to like a page and come back if the page uses a meaningful URL they can
+remember and use to directly visit a page.
-As you have seen above, the :meth:`~flask.Flask.route` decorator is used to
-bind a function to a URL. Here are some basic examples::
+Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. ::
@app.route('/')
def index():
@@ -176,16 +185,16 @@ bind a function to a URL. Here are some basic examples::
def hello():
return 'Hello, World'
-But there is more to it! You can make certain parts of the URL dynamic and
-attach multiple rules to a function.
+You can do more! You can make parts of the URL dynamic and attach multiple
+rules to a function.
Variable Rules
``````````````
-To add variable parts to a URL you can mark these special sections as
-````. Such a part is then passed as a keyword argument to your
-function. Optionally a converter can be used by specifying a rule with
-````. Here are some nice examples::
+You can add variable sections to a URL by marking sections with
+````. Your function then receives the ````
+as a keyword argument. Optionally, you can use a converter to specify the type
+of the argument like ````. ::
@app.route('/user/')
def show_user_profile(username):
@@ -197,175 +206,122 @@ function. Optionally a converter can be used by specifying a rule with
# show the post with the given id, the id is an integer
return 'Post %d' % post_id
-The following converters exist:
-
-=========== ===============================================
-`string` accepts any text without a slash (the default)
-`int` accepts integers
-`float` like ``int`` but for floating point values
-`path` like the default but also accepts slashes
-`any` matches one of the items provided
-`uuid` accepts UUID strings
-=========== ===============================================
+ @app.route('/path/')
+ def show_subpath(subpath):
+ # show the subpath after /path/
+ return 'Subpath %s' % subpath
-.. admonition:: Unique URLs / Redirection Behavior
+Converter types:
- Flask's URL rules are based on Werkzeug's routing module. The idea
- behind that module is to ensure beautiful and unique URLs based on
- precedents laid down by Apache and earlier HTTP servers.
+========== ==========================================
+``string`` (default) accepts any text without a slash
+``int`` accepts positive integers
+``float`` accepts positive floating point values
+``path`` like ``string`` but also accepts slashes
+``uuid`` accepts UUID strings
+========== ==========================================
- Take these two rules::
+Unique URLs / Redirection Behavior
+``````````````````````````````````
- @app.route('/projects/')
- def projects():
- return 'The project page'
+Take these two rules::
- @app.route('/about')
- def about():
- return 'The about page'
+ @app.route('/projects/')
+ def projects():
+ return 'The project page'
- Though they look rather similar, they differ in their use of the trailing
- slash in the URL *definition*. In the first case, the canonical URL for the
- ``projects`` endpoint has a trailing slash. In that sense, it is similar to
- a folder on a filesystem. Accessing it without a trailing slash will cause
- Flask to redirect to the canonical URL with the trailing slash.
+ @app.route('/about')
+ def about():
+ return 'The about page'
- In the second case, however, the URL is defined without a trailing slash,
- rather like the pathname of a file on UNIX-like systems. Accessing the URL
- with a trailing slash will produce a 404 "Not Found" error.
+Though they look similar, they differ in their use of the trailing slash in
+the URL. In the first case, the canonical URL for the ``projects`` endpoint
+uses a trailing slash. It's similar to a folder in a file system; if you
+access the URL without a trailing slash, Flask redirects you to the
+canonical URL with the trailing slash.
- This behavior allows relative URLs to continue working even if the trailing
- slash is omitted, consistent with how Apache and other servers work. Also,
- the URLs will stay unique, which helps search engines avoid indexing the
- same page twice.
+In the second case, however, the URL definition lacks a trailing slash,
+like the pathname of a file on UNIX-like systems. Accessing the URL with a
+trailing slash produces a 404 “Not Found” error.
+This behavior allows relative URLs to continue working even if the trailing
+slash is omitted, consistent with how Apache and other servers work. Also,
+the URLs will stay unique, which helps search engines avoid indexing the
+same page twice.
.. _url-building:
URL Building
````````````
-If it can match URLs, can Flask also generate them? Of course it can. To
-build a URL to a specific function you can use the :func:`~flask.url_for`
-function. It accepts the name of the function as first argument and a number
-of keyword arguments, each corresponding to the variable part of the URL rule.
-Unknown variable parts are appended to the URL as query parameters. Here are
-some examples::
-
- >>> from flask import Flask, url_for
- >>> app = Flask(__name__)
- >>> @app.route('/')
- ... def index(): pass
- ...
- >>> @app.route('/login')
- ... def login(): pass
- ...
- >>> @app.route('/user/')
- ... def profile(username): pass
- ...
- >>> with app.test_request_context():
- ... print url_for('index')
- ... print url_for('login')
- ... print url_for('login', next='/')
- ... print url_for('profile', username='John Doe')
- ...
- /
- /login
- /login?next=/
- /user/John%20Doe
-
-(This also uses the :meth:`~flask.Flask.test_request_context` method, explained
-below. It tells Flask to behave as though it is handling a request, even
-though we are interacting with it through a Python shell. Have a look at the
-explanation below. :ref:`context-locals`).
+To build a URL to a specific function, use the :func:`~flask.url_for` function.
+It accepts the name of the function as its first argument and any number of
+keyword arguments, each corresponding to a variable part of the URL rule.
+Unknown variable parts are appended to the URL as query parameters.
Why would you want to build URLs using the URL reversing function
:func:`~flask.url_for` instead of hard-coding them into your templates?
-There are three good reasons for this:
-1. Reversing is often more descriptive than hard-coding the URLs. More
- importantly, it allows you to change URLs in one go, without having to
- remember to change URLs all over the place.
-2. URL building will handle escaping of special characters and Unicode
- data transparently for you, so you don't have to deal with them.
-3. If your application is placed outside the URL root - say, in
- ``/myapplication`` instead of ``/`` - :func:`~flask.url_for` will handle
- that properly for you.
+1. Reversing is often more descriptive than hard-coding the URLs.
+2. You can change your URLs in one go instead of needing to remember to
+ manually change hard-coded URLs.
+3. URL building handles escaping of special characters and Unicode data
+ transparently.
+4. If your application is placed outside the URL root, for example, in
+ ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly
+ handles that for you.
+
+For example, here we use the :meth:`~flask.Flask.test_request_context` method
+to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context`
+tells Flask to behave as though it's handling a request even while we use a
+Python shell. See :ref:`context-locals`. ::
+ from flask import Flask, url_for
+
+ app = Flask(__name__)
+
+ @app.route('/')
+ def index():
+ return 'index'
+
+ @app.route('/login')
+ def login():
+ return 'login'
+
+ @app.route('/user/')
+ def profile(username):
+ return '{}'s profile'.format(username)
+
+ with app.test_request_context():
+ print(url_for('index'))
+ print(url_for('login'))
+ print(url_for('login', next='/'))
+ print(url_for('profile', username='John Doe'))
+
+ /
+ /login
+ /login?next=/
+ /user/John%20Doe
HTTP Methods
````````````
-HTTP (the protocol web applications are speaking) knows different methods for
-accessing URLs. By default, a route only answers to ``GET`` requests, but that
-can be changed by providing the ``methods`` argument to the
-:meth:`~flask.Flask.route` decorator. Here are some examples::
-
- from flask import request
+Web applications use different HTTP methods when accessing URLs. You should
+familiarize yourself with the HTTP methods as you work with Flask. By default,
+a route only answers to ``GET`` requests. You can use the ``methods`` argument
+of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods.
+::
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
- return do_the_login()
+ do_the_login()
else:
- return show_the_login_form()
-
-If ``GET`` is present, ``HEAD`` will be added automatically for you. You
-don't have to deal with that. It will also make sure that ``HEAD`` requests
-are handled as the `HTTP RFC`_ (the document describing the HTTP
-protocol) demands, so you can completely ignore that part of the HTTP
-specification. Likewise, as of Flask 0.6, ``OPTIONS`` is implemented for you
-automatically as well.
-
-You have no idea what an HTTP method is? Worry not, here is a quick
-introduction to HTTP methods and why they matter:
-
-The HTTP method (also often called "the verb") tells the server what the
-client wants to *do* with the requested page. The following methods are
-very common:
-
-``GET``
- The browser tells the server to just *get* the information stored on
- that page and send it. This is probably the most common method.
-
-``HEAD``
- The browser tells the server to get the information, but it is only
- interested in the *headers*, not the content of the page. An
- application is supposed to handle that as if a ``GET`` request was
- received but to not deliver the actual content. In Flask you don't
- have to deal with that at all, the underlying Werkzeug library handles
- that for you.
-
-``POST``
- The browser tells the server that it wants to *post* some new
- information to that URL and that the server must ensure the data is
- stored and only stored once. This is how HTML forms usually
- transmit data to the server.
-
-``PUT``
- Similar to ``POST`` but the server might trigger the store procedure
- multiple times by overwriting the old values more than once. Now you
- might be asking why this is useful, but there are some good reasons
- to do it this way. Consider that the connection is lost during
- transmission: in this situation a system between the browser and the
- server might receive the request safely a second time without breaking
- things. With ``POST`` that would not be possible because it must only
- be triggered once.
-
-``DELETE``
- Remove the information at the given location.
-
-``OPTIONS``
- Provides a quick way for a client to figure out which methods are
- supported by this URL. Starting with Flask 0.6, this is implemented
- for you automatically.
-
-Now the interesting part is that in HTML4 and XHTML1, the only methods a
-form can submit to the server are ``GET`` and ``POST``. But with JavaScript
-and future HTML standards you can use the other methods as well. Furthermore
-HTTP has become quite popular lately and browsers are no longer the only
-clients that are using HTTP. For instance, many revision control systems
-use it.
+ show_the_login_form()
+
+If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method
+and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise,
+``OPTIONS`` is automatically implemented for you.
.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt
diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst
index 51cd66f6..c3d37297 100644
--- a/docs/reqcontext.rst
+++ b/docs/reqcontext.rst
@@ -119,9 +119,9 @@ understand what is actually happening. The new behavior is quite simple:
not executed yet or at all (for example in test environments sometimes
you might want to not execute before-request callbacks).
-Now what happens on errors? In production mode if an exception is not
-caught, the 500 internal server handler is called. In development mode
-however the exception is not further processed and bubbles up to the WSGI
+Now what happens on errors? If you are not in debug mode and an exception is not
+caught, the 500 internal server handler is called. In debug mode
+however the exception is not further processed and bubbles up to the WSGI
server. That way things like the interactive debugger can provide helpful
debug information.
@@ -214,10 +214,11 @@ provide you with important information.
Starting with Flask 0.7 you have finer control over that behavior by
setting the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. By
default it's linked to the setting of ``DEBUG``. If the application is in
-debug mode the context is preserved, in production mode it's not.
+debug mode the context is preserved. If debug mode is set to off, the context
+is not preserved.
-Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` in production mode
-as it will cause your application to leak memory on exceptions. However
+Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` if debug mode is set to off
+as it will cause your application to leak memory on exceptions. However,
it can be useful during development to get the same error preserving
-behavior as in development mode when attempting to debug an error that
+behavior as debug mode when attempting to debug an error that
only occurs under production settings.
diff --git a/docs/signals.rst b/docs/signals.rst
index 2426e920..40041491 100644
--- a/docs/signals.rst
+++ b/docs/signals.rst
@@ -27,7 +27,7 @@ executed in undefined order and do not modify any data.
The big advantage of signals over handlers is that you can safely
subscribe to them for just a split second. These temporary
-subscriptions are helpful for unittesting for example. Say you want to
+subscriptions are helpful for unit testing for example. Say you want to
know what templates were rendered as part of a request: signals allow you
to do exactly that.
@@ -45,7 +45,7 @@ signal. When you subscribe to a signal, be sure to also provide a sender
unless you really want to listen for signals from all applications. This is
especially true if you are developing an extension.
-For example, here is a helper context manager that can be used in a unittest
+For example, here is a helper context manager that can be used in a unit test
to determine which templates were rendered and what variables were passed
to the template::
diff --git a/docs/styleguide.rst b/docs/styleguide.rst
index e03e4ef5..390d5668 100644
--- a/docs/styleguide.rst
+++ b/docs/styleguide.rst
@@ -167,7 +167,7 @@ Docstring conventions:
"""
Module header:
- The module header consists of an utf-8 encoding declaration (if non
+ The module header consists of a utf-8 encoding declaration (if non
ASCII letters are used, but it is recommended all the time) and a
standard docstring::
diff --git a/docs/testing.rst b/docs/testing.rst
index 0737936e..fbd3fad5 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -5,23 +5,30 @@ Testing Flask Applications
**Something that is untested is broken.**
-The origin of this quote is unknown and while it is not entirely correct, it is also
-not far from the truth. Untested applications make it hard to
+The origin of this quote is unknown and while it is not entirely correct, it
+is also not far from the truth. Untested applications make it hard to
improve existing code and developers of untested applications tend to
become pretty paranoid. If an application has automated tests, you can
safely make changes and instantly know if anything breaks.
Flask provides a way to test your application by exposing the Werkzeug
test :class:`~werkzeug.test.Client` and handling the context locals for you.
-You can then use that with your favourite testing solution. In this documentation
-we will use the :mod:`unittest` package that comes pre-installed with Python.
+You can then use that with your favourite testing solution.
+
+In this documentation we will use the `pytest`_ package as the base
+framework for our tests. You can install it with ``pip``, like so::
+
+ pip install pytest
+
+.. _pytest:
+ https://pytest.org
The Application
---------------
First, we need an application to test; we will use the application from
the :ref:`tutorial`. If you don't have that application yet, get the
-sources from `the examples`_.
+source code from `the examples`_.
.. _the examples:
https://github.com/pallets/flask/tree/master/examples/flaskr/
@@ -29,90 +36,91 @@ sources from `the examples`_.
The Testing Skeleton
--------------------
-In order to test the application, we add a second module
-(:file:`flaskr_tests.py`) and create a unittest skeleton there::
+We begin by adding a tests directory under the application root. Then
+create a Python file to store our tests (:file:`test_flaskr.py`). When we
+format the filename like ``test_*.py``, it will be auto-discoverable by
+pytest.
+
+Next, we create a `pytest fixture`_ called
+:func:`client` that configures
+the application for testing and initializes a new database.::
import os
- import flaskr
- import unittest
import tempfile
- class FlaskrTestCase(unittest.TestCase):
+ import pytest
+
+ from flaskr import flaskr
+
+
+ @pytest.fixture
+ def client():
+ db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
+ flaskr.app.config['TESTING'] = True
+ client = flaskr.app.test_client()
- def setUp(self):
- self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
- flaskr.app.config['TESTING'] = True
- self.app = flaskr.app.test_client()
- with flaskr.app.app_context():
- flaskr.init_db()
+ with flaskr.app.app_context():
+ flaskr.init_db()
- def tearDown(self):
- os.close(self.db_fd)
- os.unlink(flaskr.app.config['DATABASE'])
+ yield client
- if __name__ == '__main__':
- unittest.main()
+ os.close(db_fd)
+ os.unlink(flaskr.app.config['DATABASE'])
-The code in the :meth:`~unittest.TestCase.setUp` method creates a new test
-client and initializes a new database. This function is called before
-each individual test function is run. To delete the database after the
-test, we close the file and remove it from the filesystem in the
-:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the
-``TESTING`` config flag is activated. What it does is disable the error
-catching during request handling so that you get better error reports when
-performing test requests against the application.
+This client fixture will be called by each individual test. It gives us a
+simple interface to the application, where we can trigger test requests to the
+application. The client will also keep track of cookies for us.
-This test client will give us a simple interface to the application. We can
-trigger test requests to the application, and the client will also keep track
-of cookies for us.
+During setup, the ``TESTING`` config flag is activated. What
+this does is disable error catching during request handling, so that
+you get better error reports when performing test requests against the
+application.
-Because SQLite3 is filesystem-based we can easily use the tempfile module
+Because SQLite3 is filesystem-based, we can easily use the :mod:`tempfile` module
to create a temporary database and initialize it. The
:func:`~tempfile.mkstemp` function does two things for us: it returns a
low-level file handle and a random file name, the latter we use as
database name. We just have to keep the `db_fd` around so that we can use
the :func:`os.close` function to close the file.
+To delete the database after the test, the fixture closes the file and removes
+it from the filesystem.
+
If we now run the test suite, we should see the following output::
- $ python flaskr_tests.py
+ $ pytest
- ----------------------------------------------------------------------
- Ran 0 tests in 0.000s
+ ================ test session starts ================
+ rootdir: ./flask/examples/flaskr, inifile: setup.cfg
+ collected 0 items
- OK
+ =========== no tests ran in 0.07 seconds ============
-Even though it did not run any actual tests, we already know that our flaskr
+Even though it did not run any actual tests, we already know that our ``flaskr``
application is syntactically valid, otherwise the import would have died
with an exception.
+.. _pytest fixture:
+ https://docs.pytest.org/en/latest/fixture.html
+
The First Test
--------------
Now it's time to start testing the functionality of the application.
Let's check that the application shows "No entries here so far" if we
-access the root of the application (``/``). To do this, we add a new
-test method to our class, like this::
+access the root of the application (``/``). To do this, we add a new
+test function to :file:`test_flaskr.py`, like this::
- class FlaskrTestCase(unittest.TestCase):
+ def test_empty_db(client):
+ """Start with a blank database."""
- def setUp(self):
- self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
- self.app = flaskr.app.test_client()
- flaskr.init_db()
-
- def tearDown(self):
- os.close(self.db_fd)
- os.unlink(flaskr.app.config['DATABASE'])
-
- def test_empty_db(self):
- rv = self.app.get('/')
- assert b'No entries here so far' in rv.data
+ rv = client.get('/')
+ assert b'No entries here so far' in rv.data
Notice that our test functions begin with the word `test`; this allows
-:mod:`unittest` to automatically identify the method as a test to run.
+`pytest`_ to automatically identify the function as a test to run.
-By using `self.app.get` we can send an HTTP ``GET`` request to the application with
+By using ``client.get`` we can send an HTTP ``GET`` request to the application with
the given path. The return value will be a :class:`~flask.Flask.response_class` object.
We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect
the return value (as string) from the application. In this case, we ensure that
@@ -120,12 +128,15 @@ the return value (as string) from the application. In this case, we ensure that
Run it again and you should see one passing test::
- $ python flaskr_tests.py
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.034s
+ $ pytest -v
+
+ ================ test session starts ================
+ rootdir: ./flask/examples/flaskr, inifile: setup.cfg
+ collected 1 items
- OK
+ tests/test_flaskr.py::test_empty_db PASSED
+
+ ============= 1 passed in 0.10 seconds ==============
Logging In and Out
------------------
@@ -136,39 +147,47 @@ of the application. To do this, we fire some requests to the login and logout
pages with the required form data (username and password). And because the
login and logout pages redirect, we tell the client to `follow_redirects`.
-Add the following two methods to your `FlaskrTestCase` class::
+Add the following two functions to your :file:`test_flaskr.py` file::
+
+ def login(client, username, password):
+ return client.post('/login', data=dict(
+ username=username,
+ password=password
+ ), follow_redirects=True)
- def login(self, username, password):
- return self.app.post('/login', data=dict(
- username=username,
- password=password
- ), follow_redirects=True)
- def logout(self):
- return self.app.get('/logout', follow_redirects=True)
+ def logout(client):
+ return client.get('/logout', follow_redirects=True)
Now we can easily test that logging in and out works and that it fails with
-invalid credentials. Add this new test to the class::
-
- def test_login_logout(self):
- rv = self.login('admin', 'default')
- assert b'You were logged in' in rv.data
- rv = self.logout()
- assert b'You were logged out' in rv.data
- rv = self.login('adminx', 'default')
- assert b'Invalid username' in rv.data
- rv = self.login('admin', 'defaultx')
- assert b'Invalid password' in rv.data
+invalid credentials. Add this new test function::
+
+ def test_login_logout(client):
+ """Make sure login and logout works."""
+
+ rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
+ assert b'You were logged in' in rv.data
+
+ rv = logout(client)
+ assert b'You were logged out' in rv.data
+
+ rv = login(client, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD'])
+ assert b'Invalid username' in rv.data
+
+ rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x')
+ assert b'Invalid password' in rv.data
Test Adding Messages
--------------------
-We should also test that adding messages works. Add a new test method
+We should also test that adding messages works. Add a new test function
like this::
- def test_messages(self):
- self.login('admin', 'default')
- rv = self.app.post('/add', data=dict(
+ def test_messages(client):
+ """Test that messages work."""
+
+ login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
+ rv = client.post('/add', data=dict(
title='',
text='HTML allowed here'
), follow_redirects=True)
@@ -181,22 +200,25 @@ which is the intended behavior.
Running that should now give us three passing tests::
- $ python flaskr_tests.py
- ...
- ----------------------------------------------------------------------
- Ran 3 tests in 0.332s
+ $ pytest -v
+
+ ================ test session starts ================
+ rootdir: ./flask/examples/flaskr, inifile: setup.cfg
+ collected 3 items
+
+ tests/test_flaskr.py::test_empty_db PASSED
+ tests/test_flaskr.py::test_login_logout PASSED
+ tests/test_flaskr.py::test_messages PASSED
- OK
+ ============= 3 passed in 0.23 seconds ==============
For more complex tests with headers and status codes, check out the
`MiniTwit Example`_ from the sources which contains a larger test
suite.
-
.. _MiniTwit Example:
https://github.com/pallets/flask/tree/master/examples/minitwit/
-
Other Testing Tricks
--------------------
@@ -208,7 +230,7 @@ temporarily. With this you can access the :class:`~flask.request`,
functions. Here is a full example that demonstrates this approach::
import flask
-
+
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst
index 2dd3d7be..179c962b 100644
--- a/docs/tutorial/dbcon.rst
+++ b/docs/tutorial/dbcon.rst
@@ -3,6 +3,9 @@
Step 4: Database Connections
----------------------------
+Let's continue building our code in the ``flaskr.py`` file.
+(Scroll to the end of the page for more about project layout.)
+
You currently have a function for establishing a database connection with
`connect_db`, but by itself, it is not particularly useful. Creating and
closing database connections all the time is very inefficient, so you will
diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst
index fbbcde00..484354ba 100644
--- a/docs/tutorial/dbinit.rst
+++ b/docs/tutorial/dbinit.rst
@@ -9,31 +9,37 @@ systems need a schema that tells them how to store that information.
Before starting the server for the first time, it's important to create
that schema.
-Such a schema can be created by piping the ``schema.sql`` file into the
-`sqlite3` command as follows::
+Such a schema could be created by piping the ``schema.sql`` file into the
+``sqlite3`` command as follows::
sqlite3 /tmp/flaskr.db < schema.sql
-The downside of this is that it requires the ``sqlite3`` command to be
-installed, which is not necessarily the case on every system. This also
-requires that you provide the path to the database, which can introduce
-errors. It's a good idea to add a function that initializes the database
-for you, to the application.
+However, the downside of this is that it requires the ``sqlite3`` command
+to be installed, which is not necessarily the case on every system. This
+also requires that you provide the path to the database, which can introduce
+errors.
-To do this, you can create a function and hook it into a :command:`flask`
-command that initializes the database. For now just take a look at the
-code segment below. A good place to add this function, and command, is
-just below the `connect_db` function in :file:`flaskr.py`::
+Instead of the ``sqlite3`` command above, it's a good idea to add a function
+to our application that initializes the database for you. To do this, you
+can create a function and hook it into a :command:`flask` command that
+initializes the database.
+
+Take a look at the code segment below. A good place to add this function,
+and command, is just below the ``connect_db`` function in :file:`flaskr.py`::
def init_db():
db = get_db()
+
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
+
db.commit()
+
@app.cli.command('initdb')
def initdb_command():
"""Initializes the database."""
+
init_db()
print('Initialized the database.')
@@ -59,7 +65,8 @@ On that cursor, there is a method to execute a complete script. Finally, you
only have to commit the changes. SQLite3 and other transactional
databases will not commit unless you explicitly tell it to.
-Now, it is possible to create a database with the :command:`flask` script::
+Now, in a terminal, from the application root directory :file:`flaskr/` it is
+possible to create a database with the :command:`flask` script::
flask initdb
Initialized the database.
diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst
index ba62b3b7..23fefaec 100644
--- a/docs/tutorial/folders.rst
+++ b/docs/tutorial/folders.rst
@@ -3,8 +3,11 @@
Step 0: Creating The Folders
============================
-Before getting started, you will need to create the folders needed for this
-application::
+It is recommended to install your Flask application within a virtualenv. Please
+read the :ref:`installation` section to set up your environment.
+
+Now that you have installed Flask, you will need to create the folders required
+for this tutorial. Your directory structure will look like this::
/flaskr
/flaskr
@@ -13,9 +16,10 @@ application::
The application will be installed and run as Python package. This is the
recommended way to install and run Flask applications. You will see exactly
-how to run ``flaskr`` later on in this tutorial. For now go ahead and create
-the applications directory structure. In the next few steps you will be
-creating the database schema as well as the main module.
+how to run ``flaskr`` later on in this tutorial.
+
+For now go ahead and create the applications directory structure. In the next
+few steps you will be creating the database schema as well as the main module.
As a quick side note, the files inside of the :file:`static` folder are
available to users of the application via HTTP. This is the place where CSS and
diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst
index f0a583e0..7eee5fa0 100644
--- a/docs/tutorial/index.rst
+++ b/docs/tutorial/index.rst
@@ -3,19 +3,19 @@
Tutorial
========
-You want to develop an application with Python and Flask? Here you have
-the chance to learn by example. In this tutorial, we will create a simple
-microblogging application. It only supports one user that can create
-text-only entries and there are no feeds or comments, but it still
-features everything you need to get started. We will use Flask and SQLite
-as a database (which comes out of the box with Python) so there is nothing
-else you need.
+Learn by example to develop an application with Python and Flask.
+
+In this tutorial, we will create a simple blogging application. It only
+supports one user, only allows text entries, and has no feeds or comments.
+
+While very simple, this example still features everything you need to get
+started. In addition to Flask, we will use SQLite for the database, which is
+built-in to Python, so there is nothing else you need.
If you want the full source code in advance or for comparison, check out
the `example source`_.
-.. _example source:
- https://github.com/pallets/flask/tree/master/examples/flaskr/
+.. _example source: https://github.com/pallets/flask/tree/master/examples/flaskr/
.. toctree::
:maxdepth: 2
diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst
index 1abe597f..67008435 100644
--- a/docs/tutorial/introduction.rst
+++ b/docs/tutorial/introduction.rst
@@ -22,7 +22,7 @@ connections in a more intelligent way, allowing you to target different
relational databases at once and more. You might also want to consider
one of the popular NoSQL databases if your data is more suited for those.
-Here a screenshot of the final application:
+Here is a screenshot of the final application:
.. image:: ../_static/flaskr.png
:align: center
diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst
index 18f5c9b3..5db921aa 100644
--- a/docs/tutorial/packaging.rst
+++ b/docs/tutorial/packaging.rst
@@ -9,10 +9,10 @@ tutorial you will see exactly how to extend the ``flask`` command line
interface (CLI).
A useful pattern to manage a Flask application is to install your app
-following the `Python Packaging Guide`_. Presently this involves
-creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the
-projects root directory. You also need to add an :file:`__init__.py`
-file to make the :file:`flaskr/flaskr` directory a package. After these
+following the `Python Packaging Guide`_. Presently this involves
+creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the
+projects root directory. You also need to add an :file:`__init__.py`
+file to make the :file:`flaskr/flaskr` directory a package. After these
changes, your code structure should be::
/flaskr
@@ -25,9 +25,7 @@ changes, your code structure should be::
setup.py
MANIFEST.in
-The content of the ``setup.py`` file for ``flaskr`` is:
-
-.. sourcecode:: python
+Create the ``setup.py`` file for ``flaskr`` with the following content::
from setuptools import setup
@@ -43,53 +41,53 @@ The content of the ``setup.py`` file for ``flaskr`` is:
When using setuptools, it is also necessary to specify any special files
that should be included in your package (in the :file:`MANIFEST.in`).
In this case, the static and templates directories need to be included,
-as well as the schema. Create the :file:`MANIFEST.in` and add the
-following lines::
+as well as the schema.
+
+Create the :file:`MANIFEST.in` and add the following lines::
graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql
-To simplify locating the application, add the following import statement
-into this file, :file:`flaskr/__init__.py`:
-
-.. sourcecode:: python
+Next, to simplify locating the application, create the file,
+:file:`flaskr/__init__.py` containing only the following import statement::
from .flaskr import app
-This import statement brings the application instance into the top-level
-of the application package. When it is time to run the application, the
-Flask development server needs the location of the app instance. This
-import statement simplifies the location process. Without it the export
-statement a few steps below would need to be
+This import statement brings the application instance into the top-level
+of the application package. When it is time to run the application, the
+Flask development server needs the location of the app instance. This
+import statement simplifies the location process. Without the above
+import statement, the export statement a few steps below would need to be
``export FLASK_APP=flaskr.flaskr``.
At this point you should be able to install the application. As usual, it
is recommended to install your Flask application within a `virtualenv`_.
-With that said, go ahead and install the application with::
+With that said, from the ``flaskr/`` directory, go ahead and install the
+application with::
pip install --editable .
-The above installation command assumes that it is run within the projects
-root directory, `flaskr/`. The `editable` flag allows editing
-source code without having to reinstall the Flask app each time you make
-changes. The flaskr app is now installed in your virtualenv (see output
+The above installation command assumes that it is run within the projects
+root directory, ``flaskr/``. The ``editable`` flag allows editing
+source code without having to reinstall the Flask app each time you make
+changes. The flaskr app is now installed in your virtualenv (see output
of ``pip freeze``).
With that out of the way, you should be able to start up the application.
-Do this with the following commands::
+Do this on Mac or Linux with the following commands in ``flaskr/``::
export FLASK_APP=flaskr
export FLASK_DEBUG=true
flask run
-(In case you are on Windows you need to use `set` instead of `export`).
+(In case you are on Windows you need to use ``set`` instead of ``export``).
The :envvar:`FLASK_DEBUG` flag enables or disables the interactive debugger.
*Never leave debug mode activated in a production system*, because it will
allow users to execute code on the server!
You will see a message telling you that server has started along with
-the address at which you can access it.
+the address at which you can access it in a browser.
When you head over to the server in your browser, you will get a 404 error
because we don't have any views yet. That will be addressed a little later,
diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst
index 4bedb54c..81309e65 100644
--- a/docs/tutorial/setup.rst
+++ b/docs/tutorial/setup.rst
@@ -3,27 +3,31 @@
Step 2: Application Setup Code
==============================
-Now that the schema is in place, you can create the application module,
-:file:`flaskr.py`. This file should be placed inside of the
-:file:`flaskr/flaskr` folder. The first several lines of code in the
-application module are the needed import statements. After that there will be a
-few lines of configuration code. For small applications like ``flaskr``, it is
-possible to drop the configuration directly into the module. However, a cleaner
-solution is to create a separate ``.ini`` or ``.py`` file, load that, and
-import the values from there.
+Next, we will create the application module, :file:`flaskr.py`. Just like the
+:file:`schema.sql` file you created in the previous step, this file should be
+placed inside of the :file:`flaskr/flaskr` folder.
+
+For this tutorial, all the Python code we use will be put into this file
+(except for one line in ``__init__.py``, and any testing or optional files you
+decide to create).
+
+The first several lines of code in the application module are the needed import
+statements. After that there will be a few lines of configuration code.
+
+For small applications like ``flaskr``, it is possible to drop the configuration
+directly into the module. However, a cleaner solution is to create a separate
+``.py`` file, load that, and import the values from there.
Here are the import statements (in :file:`flaskr.py`)::
- # all the imports
import os
import sqlite3
- from flask import Flask, request, session, g, redirect, url_for, abort, \
- render_template, flash
-The next couple lines will create the actual application instance and
-initialize it with the config from the same file in :file:`flaskr.py`:
+ from flask import (Flask, request, session, g, redirect, url_for, abort,
+ render_template, flash)
-.. sourcecode:: python
+The next couple lines will create the actual application instance and
+initialize it with the config from the same file in :file:`flaskr.py`::
app = Flask(__name__) # create the application instance :)
app.config.from_object(__name__) # load config from this file , flaskr.py
@@ -37,8 +41,8 @@ initialize it with the config from the same file in :file:`flaskr.py`:
))
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
-The :class:`~flask.Config` object works similarly to a dictionary, so it can be
-updated with new values.
+In the above code, the :class:`~flask.Config` object works similarly to a
+dictionary, so it can be updated with new values.
.. admonition:: Database Path
@@ -58,15 +62,15 @@ updated with new values.
Usually, it is a good idea to load a separate, environment-specific
configuration file. Flask allows you to import multiple configurations and it
will use the setting defined in the last import. This enables robust
-configuration setups. :meth:`~flask.Config.from_envvar` can help achieve this.
-
-.. sourcecode:: python
+configuration setups. :meth:`~flask.Config.from_envvar` can help achieve
+this. ::
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
-Simply define the environment variable :envvar:`FLASKR_SETTINGS` that points to
-a config file to be loaded. The silent switch just tells Flask to not complain
-if no such environment key is set.
+If you want to do this (not required for this tutorial) simply define the
+environment variable :envvar:`FLASKR_SETTINGS` that points to a config file
+to be loaded. The silent switch just tells Flask to not complain if no such
+environment key is set.
In addition to that, you can use the :meth:`~flask.Config.from_object`
method on the config object and provide it with an import name of a
@@ -76,22 +80,22 @@ that in all cases, only variable names that are uppercase are considered.
The ``SECRET_KEY`` is needed to keep the client-side sessions secure.
Choose that key wisely and as hard to guess and complex as possible.
-Lastly, you will add a method that allows for easy connections to the
-specified database. This can be used to open a connection on request and
-also from the interactive Python shell or a script. This will come in
-handy later. You can create a simple database connection through SQLite and
-then tell it to use the :class:`sqlite3.Row` object to represent rows.
-This allows the rows to be treated as if they were dictionaries instead of
-tuples.
-
-.. sourcecode:: python
+Lastly, add a method that allows for easy connections to the specified
+database. ::
def connect_db():
"""Connects to the specific database."""
+
rv = sqlite3.connect(app.config['DATABASE'])
rv.row_factory = sqlite3.Row
return rv
+This can be used to open a connection on request and also from the
+interactive Python shell or a script. This will come in handy later.
+You can create a simple database connection through SQLite and then tell
+it to use the :class:`sqlite3.Row` object to represent rows. This allows
+the rows to be treated as if they were dictionaries instead of tuples.
+
In the next section you will see how to run the application.
Continue with :ref:`tutorial-packaging`.
diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst
index 4cb7db7f..12a555e7 100644
--- a/docs/tutorial/templates.rst
+++ b/docs/tutorial/templates.rst
@@ -15,7 +15,8 @@ escaped with their XML equivalents.
We are also using template inheritance which makes it possible to reuse
the layout of the website in all pages.
-Put the following templates into the :file:`templates` folder:
+Create the follwing three HTML files and place them in the
+:file:`templates` folder:
.. _Jinja2: http://jinja.pocoo.org/docs/templates
@@ -79,9 +80,9 @@ HTTP method:
{% endif %}
{% for entry in entries %}
- {{ entry.title }}
{{ entry.text|safe }}
+ {{ entry.title }}
{{ entry.text|safe }}
{% else %}
- - Unbelievable. No entries here so far
+
- Unbelievable. No entries here so far
{% endfor %}
{% endblock %}
diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst
index 4364d973..1b09fcb8 100644
--- a/docs/tutorial/views.rst
+++ b/docs/tutorial/views.rst
@@ -4,7 +4,8 @@ Step 6: The View Functions
==========================
Now that the database connections are working, you can start writing the
-view functions. You will need four of them:
+view functions. You will need four of them; Show Entries, Add New Entry,
+Login and Logout. Add the following code snipets to :file:`flaskr.py`.
Show Entries
------------
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index 436b0430..af2383c0 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -49,7 +49,7 @@ Any of the following is functionally equivalent::
response = send_file(open(fname), attachment_filename=fname)
response.set_etag(...)
-The reason for this is that some file-like objects have a invalid or even
+The reason for this is that some file-like objects have an invalid or even
misleading ``name`` attribute. Silently swallowing errors in such cases was not
a satisfying solution.
@@ -198,7 +198,7 @@ applications with Flask. Because we want to make upgrading as easy as
possible we tried to counter the problems arising from these changes by
providing a script that can ease the transition.
-The script scans your whole application and generates an unified diff with
+The script scans your whole application and generates a unified diff with
changes it assumes are safe to apply. However as this is an automated
tool it won't be able to find all use cases and it might miss some. We
internally spread a lot of deprecation warnings all over the place to make
diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py
index 663e92e0..493067ee 100644
--- a/examples/flaskr/tests/test_flaskr.py
+++ b/examples/flaskr/tests/test_flaskr.py
@@ -16,19 +16,15 @@ from flaskr import flaskr
@pytest.fixture
-def client(request):
+def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
client = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
-
- def teardown():
- os.close(db_fd)
- os.unlink(flaskr.app.config['DATABASE'])
- request.addfinalizer(teardown)
-
- return client
+ yield client
+ os.close(db_fd)
+ os.unlink(flaskr.app.config['DATABASE'])
def login(client, username, password):
diff --git a/examples/minitwit/tests/test_minitwit.py b/examples/minitwit/tests/test_minitwit.py
index 50ca26d9..c8992e57 100644
--- a/examples/minitwit/tests/test_minitwit.py
+++ b/examples/minitwit/tests/test_minitwit.py
@@ -15,18 +15,16 @@ from minitwit import minitwit
@pytest.fixture
-def client(request):
+def client():
db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp()
client = minitwit.app.test_client()
with minitwit.app.app_context():
minitwit.init_db()
- def teardown():
- """Get rid of the database again after each test."""
- os.close(db_fd)
- os.unlink(minitwit.app.config['DATABASE'])
- request.addfinalizer(teardown)
- return client
+ yield client
+
+ os.close(db_fd)
+ os.unlink(minitwit.app.config['DATABASE'])
def register(client, username, password, password2=None, email=None):
diff --git a/flask/_compat.py b/flask/_compat.py
index 071628fc..173b3689 100644
--- a/flask/_compat.py
+++ b/flask/_compat.py
@@ -25,6 +25,7 @@ if not PY2:
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
+ from inspect import getfullargspec as getargspec
from io import StringIO
def reraise(tp, value, tb=None):
@@ -43,6 +44,7 @@ else:
itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems()
+ from inspect import getargspec
from cStringIO import StringIO
exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')
diff --git a/flask/app.py b/flask/app.py
index 27918d01..51b50d34 100644
--- a/flask/app.py
+++ b/flask/app.py
@@ -10,30 +10,30 @@
"""
import os
import sys
-from threading import Lock
from datetime import timedelta
-from itertools import chain
from functools import update_wrapper
+from itertools import chain
+from threading import Lock
-from werkzeug.datastructures import ImmutableDict
-from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
-from werkzeug.exceptions import HTTPException, InternalServerError, \
- MethodNotAllowed, BadRequest, default_exceptions
-
-from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
- locked_cached_property, _endpoint_from_view_func, find_package, \
- get_debug_flag
-from . import json, cli
-from .wrappers import Request, Response
-from .config import ConfigAttribute, Config
-from .ctx import RequestContext, AppContext, _AppCtxGlobals
-from .globals import _request_ctx_stack, request, session, g
+from werkzeug.datastructures import ImmutableDict, Headers
+from werkzeug.exceptions import BadRequest, HTTPException, \
+ InternalServerError, MethodNotAllowed, default_exceptions
+from werkzeug.routing import BuildError, Map, RequestRedirect, Rule
+
+from . import cli, json
+from ._compat import integer_types, reraise, string_types, text_type
+from .config import Config, ConfigAttribute
+from .ctx import AppContext, RequestContext, _AppCtxGlobals
+from .globals import _request_ctx_stack, g, request, session
+from .helpers import _PackageBoundObject, \
+ _endpoint_from_view_func, find_package, get_debug_flag, \
+ get_flashed_messages, locked_cached_property, url_for
from .sessions import SecureCookieSessionInterface
+from .signals import appcontext_tearing_down, got_request_exception, \
+ request_finished, request_started, request_tearing_down
from .templating import DispatchingJinjaLoader, Environment, \
- _default_template_ctx_processor
-from .signals import request_started, request_finished, got_request_exception, \
- request_tearing_down, appcontext_tearing_down
-from ._compat import reraise, string_types, text_type, integer_types
+ _default_template_ctx_processor
+from .wrappers import Request, Response
# a lock used for logger initialization
_logger_lock = Lock()
@@ -123,6 +123,9 @@ class Flask(_PackageBoundObject):
.. versionadded:: 0.11
The `root_path` parameter was added.
+ .. versionadded:: 0.13
+ The `host_matching` and `static_host` parameters were added.
+
:param import_name: the name of the application package
:param static_url_path: can be used to specify a different path for the
static files on the web. Defaults to the name
@@ -130,6 +133,11 @@ class Flask(_PackageBoundObject):
:param static_folder: the folder with static files that should be served
at `static_url_path`. Defaults to the ``'static'``
folder in the root path of the application.
+ :param host_matching: sets the app's ``url_map.host_matching`` to the given
+ given value. Defaults to False.
+ :param static_host: the host to use when adding the static route. Defaults
+ to None. Required when using ``host_matching=True``
+ with a ``static_folder`` configured.
:param template_folder: the folder that contains the templates that should
be used by the application. Defaults to
``'templates'`` folder in the root path of the
@@ -212,7 +220,7 @@ class Flask(_PackageBoundObject):
#: The testing flag. Set this to ``True`` to enable the test mode of
#: Flask extensions (and in the future probably also Flask itself).
- #: For example this might activate unittest helpers that have an
+ #: For example this might activate test helpers that have an
#: additional runtime cost which should not be enabled by default.
#:
#: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the
@@ -314,7 +322,7 @@ class Flask(_PackageBoundObject):
'PREFERRED_URL_SCHEME': 'http',
'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True,
- 'JSONIFY_PRETTYPRINT_REGULAR': True,
+ 'JSONIFY_PRETTYPRINT_REGULAR': False,
'JSONIFY_MIMETYPE': 'application/json',
'TEMPLATES_AUTO_RELOAD': None,
})
@@ -337,7 +345,8 @@ class Flask(_PackageBoundObject):
session_interface = SecureCookieSessionInterface()
def __init__(self, import_name, static_path=None, static_url_path=None,
- static_folder='static', template_folder='templates',
+ static_folder='static', static_host=None,
+ host_matching=False, template_folder='templates',
instance_path=None, instance_relative_config=False,
root_path=None):
_PackageBoundObject.__init__(self, import_name,
@@ -391,7 +400,7 @@ class Flask(_PackageBoundObject):
#: is the class for the instance check and the second the error handler
#: function.
#:
- #: To register a error handler, use the :meth:`errorhandler`
+ #: To register an error handler, use the :meth:`errorhandler`
#: decorator.
self.error_handler_spec = {None: self._error_handlers}
@@ -404,17 +413,16 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.9
self.url_build_error_handlers = []
- #: A dictionary with lists of functions that should be called at the
- #: beginning of the request. The key of the dictionary is the name of
- #: the blueprint this function is active for, ``None`` for all requests.
- #: This can for example be used to open database connections or
- #: getting hold of the currently logged in user. To register a
- #: function here, use the :meth:`before_request` decorator.
+ #: A dictionary with lists of functions that will be called at the
+ #: beginning of each request. The key of the dictionary is the name of
+ #: the blueprint this function is active for, or ``None`` for all
+ #: requests. To register a function, use the :meth:`before_request`
+ #: decorator.
self.before_request_funcs = {}
- #: A lists of functions that should be called at the beginning of the
- #: first request to this instance. To register a function here, use
- #: the :meth:`before_first_request` decorator.
+ #: A list of functions that will be called at the beginning of the
+ #: first request to this instance. To register a function, use the
+ #: :meth:`before_first_request` decorator.
#:
#: .. versionadded:: 0.8
self.before_first_request_funcs = []
@@ -446,12 +454,11 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.9
self.teardown_appcontext_funcs = []
- #: A dictionary with lists of functions that can be used as URL
- #: value processor functions. Whenever a URL is built these functions
- #: are called to modify the dictionary of values in place. The key
- #: ``None`` here is used for application wide
- #: callbacks, otherwise the key is the name of the blueprint.
- #: Each of these functions has the chance to modify the dictionary
+ #: A dictionary with lists of functions that are called before the
+ #: :attr:`before_request_funcs` functions. The key of the dictionary is
+ #: the name of the blueprint this function is active for, or ``None``
+ #: for all requests. To register a function, use
+ #: :meth:`url_value_preprocessor`.
#:
#: .. versionadded:: 0.7
self.url_value_preprocessors = {}
@@ -525,19 +532,22 @@ class Flask(_PackageBoundObject):
#: app.url_map.converters['list'] = ListConverter
self.url_map = Map()
+ self.url_map.host_matching = host_matching
+
# tracks internally if the application already handled at least one
# request.
self._got_first_request = False
self._before_request_lock = Lock()
- # register the static folder for the application. Do that even
- # if the folder does not exist. First of all it might be created
- # while the server is running (usually happens during development)
- # but also because google appengine stores static files somewhere
- # else when mapped with the .yml file.
+ # Add a static route using the provided static_url_path, static_host,
+ # and static_folder if there is a configured static_folder.
+ # Note we do this without checking if static_folder exists.
+ # For one, it might be created while the server is running (e.g. during
+ # development). Also, Google App Engine stores static files somewhere
if self.has_static_folder:
+ assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination'
self.add_url_rule(self.static_url_path + '/',
- endpoint='static',
+ endpoint='static', host=static_host,
view_func=self.send_static_file)
#: The click command line context for this application. Commands
@@ -813,7 +823,8 @@ class Flask(_PackageBoundObject):
:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
have the server available externally as well. Defaults to
- ``'127.0.0.1'``.
+ ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config
+ variable if present.
:param port: the port of the webserver. Defaults to ``5000`` or the
port defined in the ``SERVER_NAME`` config variable if
present.
@@ -965,7 +976,7 @@ class Flask(_PackageBoundObject):
return iter(self._blueprint_order)
@setupmethod
- def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
+ def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options):
"""Connects a URL rule. Works exactly like the :meth:`route`
decorator. If a view_func is provided it will be registered with the
endpoint.
@@ -1005,6 +1016,10 @@ class Flask(_PackageBoundObject):
endpoint
:param view_func: the function to call when serving a request to the
provided endpoint
+ :param provide_automatic_options: controls whether the ``OPTIONS``
+ method should be added automatically. This can also be controlled
+ by setting the ``view_func.provide_automatic_options = False``
+ before adding the rule.
:param options: the options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods
@@ -1034,8 +1049,9 @@ class Flask(_PackageBoundObject):
# starting with Flask 0.8 the view_func object can disable and
# force-enable the automatic options handling.
- provide_automatic_options = getattr(view_func,
- 'provide_automatic_options', None)
+ if provide_automatic_options is None:
+ provide_automatic_options = getattr(view_func,
+ 'provide_automatic_options', None)
if provide_automatic_options is None:
if 'OPTIONS' not in methods:
@@ -1294,11 +1310,13 @@ class Flask(_PackageBoundObject):
@setupmethod
def before_request(self, f):
"""Registers a function to run before each request.
+
+ For example, this can be used to open a database connection, or to load
+ the logged in user from the session.
- The function will be called without any arguments.
- If the function returns a non-None value, it's handled as
- if it was the return value from the view and further
- request handling is stopped.
+ The function will be called without any arguments. If it returns a
+ non-None value, the value is handled as if it was the return value from
+ the view, and further request handling is stopped.
"""
self.before_request_funcs.setdefault(None, []).append(f)
return f
@@ -1354,7 +1372,7 @@ class Flask(_PackageBoundObject):
will have to surround the execution of these code by try/except
statements and log occurring errors.
- When a teardown function was called because of a exception it will
+ When a teardown function was called because of an exception it will
be passed an error object.
The return values of teardown functions are ignored.
@@ -1417,9 +1435,17 @@ class Flask(_PackageBoundObject):
@setupmethod
def url_value_preprocessor(self, f):
- """Registers a function as URL value preprocessor for all view
- functions of the application. It's called before the view functions
- are called and can modify the url values provided.
+ """Register a URL value preprocessor function for all view
+ functions in the application. These functions will be called before the
+ :meth:`before_request` functions.
+
+ The function can modify the values captured from the matched url before
+ they are passed to the view. For example, this can be used to pop a
+ common language code value and place it in ``g`` rather than pass it to
+ every view.
+
+ The function is passed the endpoint name and values dict. The return
+ value is ignored.
"""
self.url_value_preprocessors.setdefault(None, []).append(f)
return f
@@ -1434,15 +1460,17 @@ class Flask(_PackageBoundObject):
return f
def _find_error_handler(self, e):
- """Finds a registered error handler for the request’s blueprint.
- Otherwise falls back to the app, returns None if not a suitable
- handler is found.
+ """Find a registered error handler for a request in this order:
+ blueprint handler for a specific code, app handler for a specific code,
+ blueprint generic HTTPException handler, app generic HTTPException handler,
+ and returns None if a suitable handler is not found.
"""
exc_class, code = self._get_exc_class_and_code(type(e))
def find_handler(handler_map):
if not handler_map:
return
+
for cls in exc_class.__mro__:
handler = handler_map.get(cls)
if handler is not None:
@@ -1450,15 +1478,13 @@ class Flask(_PackageBoundObject):
handler_map[exc_class] = handler
return handler
- # try blueprint handlers
- handler = find_handler(self.error_handler_spec
- .get(request.blueprint, {})
- .get(code))
- if handler is not None:
- return handler
+ # check for any in blueprint or app
+ for name, c in ((request.blueprint, code), (None, code),
+ (request.blueprint, None), (None, None)):
+ handler = find_handler(self.error_handler_spec.get(name, {}).get(c))
- # fall back to app handlers
- return find_handler(self.error_handler_spec[None].get(code))
+ if handler:
+ return handler
def handle_http_exception(self, e):
"""Handles an HTTP exception. By default this will invoke the
@@ -1695,62 +1721,106 @@ class Flask(_PackageBoundObject):
return False
def make_response(self, rv):
- """Converts the return value from a view function to a real
- response object that is an instance of :attr:`response_class`.
-
- The following types are allowed for `rv`:
-
- .. tabularcolumns:: |p{3.5cm}|p{9.5cm}|
-
- ======================= ===========================================
- :attr:`response_class` the object is returned unchanged
- :class:`str` a response object is created with the
- string as body
- :class:`unicode` a response object is created with the
- string encoded to utf-8 as body
- a WSGI function the function is called as WSGI application
- and buffered as response object
- :class:`tuple` A tuple in the form ``(response, status,
- headers)`` or ``(response, headers)``
- where `response` is any of the
- types defined here, `status` is a string
- or an integer and `headers` is a list or
- a dictionary with header values.
- ======================= ===========================================
-
- :param rv: the return value from the view function
+ """Convert the return value from a view function to an instance of
+ :attr:`response_class`.
+
+ :param rv: the return value from the view function. The view function
+ must return a response. Returning ``None``, or the view ending
+ without returning, is not allowed. The following types are allowed
+ for ``view_rv``:
+
+ ``str`` (``unicode`` in Python 2)
+ A response object is created with the string encoded to UTF-8
+ as the body.
+
+ ``bytes`` (``str`` in Python 2)
+ A response object is created with the bytes as the body.
+
+ ``tuple``
+ Either ``(body, status, headers)``, ``(body, status)``, or
+ ``(body, headers)``, where ``body`` is any of the other types
+ allowed here, ``status`` is a string or an integer, and
+ ``headers`` is a dictionary or a list of ``(key, value)``
+ tuples. If ``body`` is a :attr:`response_class` instance,
+ ``status`` overwrites the exiting value and ``headers`` are
+ extended.
+
+ :attr:`response_class`
+ The object is returned unchanged.
+
+ other :class:`~werkzeug.wrappers.Response` class
+ The object is coerced to :attr:`response_class`.
+
+ :func:`callable`
+ The function is called as a WSGI application. The result is
+ used to create a response object.
.. versionchanged:: 0.9
Previously a tuple was interpreted as the arguments for the
response object.
"""
- status_or_headers = headers = None
- if isinstance(rv, tuple):
- rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
- if rv is None:
- raise ValueError('View function did not return a response')
+ status = headers = None
- if isinstance(status_or_headers, (dict, list)):
- headers, status_or_headers = status_or_headers, None
+ # unpack tuple returns
+ if isinstance(rv, (tuple, list)):
+ len_rv = len(rv)
+ # a 3-tuple is unpacked directly
+ if len_rv == 3:
+ rv, status, headers = rv
+ # decide if a 2-tuple has status or headers
+ elif len_rv == 2:
+ if isinstance(rv[1], (Headers, dict, tuple, list)):
+ rv, headers = rv
+ else:
+ rv, status = rv
+ # other sized tuples are not allowed
+ else:
+ raise TypeError(
+ 'The view function did not return a valid response tuple.'
+ ' The tuple must have the form (body, status, headers),'
+ ' (body, status), or (body, headers).'
+ )
+
+ # the body must not be None
+ if rv is None:
+ raise TypeError(
+ 'The view function did not return a valid response. The'
+ ' function either returned None or ended without a return'
+ ' statement.'
+ )
+
+ # make sure the body is an instance of the response class
if not isinstance(rv, self.response_class):
- # When we create a response object directly, we let the constructor
- # set the headers and status. We do this because there can be
- # some extra logic involved when creating these objects with
- # specific values (like default content type selection).
if isinstance(rv, (text_type, bytes, bytearray)):
- rv = self.response_class(rv, headers=headers,
- status=status_or_headers)
- headers = status_or_headers = None
+ # let the response class set the status and headers instead of
+ # waiting to do it manually, so that the class can handle any
+ # special logic
+ rv = self.response_class(rv, status=status, headers=headers)
+ status = headers = None
else:
- rv = self.response_class.force_type(rv, request.environ)
-
- if status_or_headers is not None:
- if isinstance(status_or_headers, string_types):
- rv.status = status_or_headers
+ # evaluate a WSGI callable, or coerce a different response
+ # class to the correct type
+ try:
+ rv = self.response_class.force_type(rv, request.environ)
+ except TypeError as e:
+ new_error = TypeError(
+ '{e}\nThe view function did not return a valid'
+ ' response. The return type must be a string, tuple,'
+ ' Response instance, or WSGI callable, but it was a'
+ ' {rv.__class__.__name__}.'.format(e=e, rv=rv)
+ )
+ reraise(TypeError, new_error, sys.exc_info()[2])
+
+ # prefer the status if it was provided
+ if status is not None:
+ if isinstance(status, (text_type, bytes, bytearray)):
+ rv.status = status
else:
- rv.status_code = status_or_headers
+ rv.status_code = status
+
+ # extend existing headers with provided headers
if headers:
rv.headers.extend(headers)
@@ -1813,16 +1883,16 @@ class Flask(_PackageBoundObject):
raise error
def preprocess_request(self):
- """Called before the actual request dispatching and will
- call each :meth:`before_request` decorated function, passing no
- arguments.
- If any of these functions returns a value, it's handled as
- if it was the return value from the view and further
- request handling is stopped.
-
- This also triggers the :meth:`url_value_preprocessor` functions before
- the actual :meth:`before_request` functions are called.
+ """Called before the request is dispatched. Calls
+ :attr:`url_value_preprocessors` registered with the app and the
+ current blueprint (if any). Then calls :attr:`before_request_funcs`
+ registered with the app and the blueprint.
+
+ If any :meth:`before_request` handler returns a non-None value, the
+ value is handled as if it was the return value from the view, and
+ further request handling is stopped.
"""
+
bp = _request_ctx_stack.top.request.blueprint
funcs = self.url_value_preprocessors.get(None, ())
@@ -1982,14 +2052,17 @@ class Flask(_PackageBoundObject):
exception context to start the response
"""
ctx = self.request_context(environ)
- ctx.push()
error = None
try:
try:
+ ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
+ except:
+ error = sys.exc_info()[1]
+ raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
diff --git a/flask/blueprints.py b/flask/blueprints.py
index 586a1b0b..57d77512 100644
--- a/flask/blueprints.py
+++ b/flask/blueprints.py
@@ -89,6 +89,13 @@ class Blueprint(_PackageBoundObject):
warn_on_modifications = False
_got_registered_once = False
+ #: Blueprint local JSON decoder class to use.
+ #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`.
+ json_encoder = None
+ #: Blueprint local JSON decoder class to use.
+ #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`.
+ json_decoder = None
+
def __init__(self, name, import_name, static_folder=None,
static_url_path=None, template_folder=None,
url_prefix=None, subdomain=None, url_defaults=None,
diff --git a/flask/cli.py b/flask/cli.py
index bde5a13b..e6e8f65f 100644
--- a/flask/cli.py
+++ b/flask/cli.py
@@ -11,41 +11,86 @@
import os
import sys
-from threading import Lock, Thread
+import traceback
from functools import update_wrapper
+from operator import attrgetter
+from threading import Lock, Thread
import click
+from . import __version__
from ._compat import iteritems, reraise
+from .globals import current_app
from .helpers import get_debug_flag
-from . import __version__
+from ._compat import getargspec
+
class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded."""
-def find_best_app(module):
+def find_best_app(script_info, module):
"""Given a module instance this tries to find the best possible
application in the module or raises an exception.
"""
from . import Flask
# Search for the most common names first.
- for attr_name in 'app', 'application':
+ for attr_name in ('app', 'application'):
app = getattr(module, attr_name, None)
- if app is not None and isinstance(app, Flask):
+ if isinstance(app, Flask):
return app
# Otherwise find the only object that is a Flask instance.
- matches = [v for k, v in iteritems(module.__dict__)
- if isinstance(v, Flask)]
+ matches = [
+ v for k, v in iteritems(module.__dict__) if isinstance(v, Flask)
+ ]
if len(matches) == 1:
return matches[0]
- raise NoAppException('Failed to find application in module "%s". Are '
- 'you sure it contains a Flask application? Maybe '
- 'you wrapped it in a WSGI middleware or you are '
- 'using a factory function.' % module.__name__)
+ elif len(matches) > 1:
+ raise NoAppException(
+ 'Auto-detected multiple Flask applications in module "{module}".'
+ ' Use "FLASK_APP={module}:name" to specify the correct'
+ ' one.'.format(module=module.__name__)
+ )
+
+ # Search for app factory callables.
+ for attr_name in ('create_app', 'make_app'):
+ app_factory = getattr(module, attr_name, None)
+
+ if callable(app_factory):
+ try:
+ app = call_factory(app_factory, script_info)
+ if isinstance(app, Flask):
+ return app
+ except TypeError:
+ raise NoAppException(
+ 'Auto-detected "{callable}()" in module "{module}", but '
+ 'could not call it without specifying arguments.'.format(
+ callable=attr_name, module=module.__name__
+ )
+ )
+
+ raise NoAppException(
+ 'Failed to find application in module "{module}". Are you sure '
+ 'it contains a Flask application? Maybe you wrapped it in a WSGI '
+ 'middleware.'.format(module=module.__name__)
+ )
+
+
+def call_factory(func, script_info):
+ """Checks if the given app factory function has an argument named
+ ``script_info`` or just a single argument and calls the function passing
+ ``script_info`` if so. Otherwise, calls the function without any arguments
+ and returns the result.
+ """
+ arguments = getargspec(func).args
+ if 'script_info' in arguments:
+ return func(script_info=script_info)
+ elif len(arguments) == 1:
+ return func(script_info)
+ return func()
def prepare_exec_for_file(filename):
@@ -77,7 +122,7 @@ def prepare_exec_for_file(filename):
return '.'.join(module[::-1])
-def locate_app(app_id):
+def locate_app(script_info, app_id):
"""Attempts to locate the application."""
__traceback_hide__ = True
if ':' in app_id:
@@ -92,7 +137,9 @@ def locate_app(app_id):
# Reraise the ImportError if it occurred within the imported module.
# Determine this by checking whether the trace has a depth > 1.
if sys.exc_info()[-1].tb_next:
- raise
+ stack_trace = traceback.format_exc()
+ raise NoAppException('There was an error trying to import'
+ ' the app (%s):\n%s' % (module, stack_trace))
else:
raise NoAppException('The file/path provided (%s) does not appear'
' to exist. Please verify the path is '
@@ -101,7 +148,7 @@ def locate_app(app_id):
mod = sys.modules[module]
if app_obj is None:
- app = find_best_app(mod)
+ app = find_best_app(script_info, mod)
else:
app = getattr(mod, app_obj, None)
if app is None:
@@ -226,7 +273,7 @@ class ScriptInfo(object):
if self._loaded_app is not None:
return self._loaded_app
if self.create_app is not None:
- rv = self.create_app(self)
+ rv = call_factory(self.create_app, self)
else:
if not self.app_import_path:
raise NoAppException(
@@ -234,7 +281,7 @@ class ScriptInfo(object):
'the FLASK_APP environment variable.\n\nFor more '
'information see '
'http://flask.pocoo.org/docs/latest/quickstart/')
- rv = locate_app(self.app_import_path)
+ rv = locate_app(self, self.app_import_path)
debug = get_debug_flag()
if debug is not None:
rv.debug = debug
@@ -316,6 +363,7 @@ class FlaskGroup(AppGroup):
if add_default_commands:
self.add_command(run_command)
self.add_command(shell_command)
+ self.add_command(routes_command)
self._loaded_plugin_commands = False
@@ -368,7 +416,9 @@ class FlaskGroup(AppGroup):
# want the help page to break if the app does not exist.
# If someone attempts to use the command we try to create
# the app again and this will give us the error.
- pass
+ # However, we will not do so silently because that would confuse
+ # users.
+ traceback.print_exc()
return sorted(rv)
def main(self, *args, **kwargs):
@@ -479,6 +529,53 @@ def shell_command():
code.interact(banner=banner, local=ctx)
+@click.command('routes', short_help='Show the routes for the app.')
+@click.option(
+ '--sort', '-s',
+ type=click.Choice(('endpoint', 'methods', 'rule', 'match')),
+ default='endpoint',
+ help=(
+ 'Method to sort routes by. "match" is the order that Flask will match '
+ 'routes when dispatching a request.'
+ )
+)
+@click.option(
+ '--all-methods',
+ is_flag=True,
+ help="Show HEAD and OPTIONS methods."
+)
+@with_appcontext
+def routes_command(sort, all_methods):
+ """Show all registered routes with endpoints and methods."""
+
+ rules = list(current_app.url_map.iter_rules())
+ ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
+
+ if sort in ('endpoint', 'rule'):
+ rules = sorted(rules, key=attrgetter(sort))
+ elif sort == 'methods':
+ rules = sorted(rules, key=lambda rule: sorted(rule.methods))
+
+ rule_methods = [
+ ', '.join(sorted(rule.methods - ignored_methods)) for rule in rules
+ ]
+
+ headers = ('Endpoint', 'Methods', 'Rule')
+ widths = (
+ max(len(rule.endpoint) for rule in rules),
+ max(len(methods) for methods in rule_methods),
+ max(len(rule.rule) for rule in rules),
+ )
+ widths = [max(len(h), w) for h, w in zip(headers, widths)]
+ row = '{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}'.format(*widths)
+
+ click.echo(row.format(*headers).strip())
+ click.echo(row.format(*('-' * width for width in widths)))
+
+ for rule, methods in zip(rules, rule_methods):
+ click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
+
+
cli = FlaskGroup(help="""\
This shell command acts as general utility script for Flask applications.
diff --git a/flask/helpers.py b/flask/helpers.py
index 2f446327..2bcdc10b 100644
--- a/flask/helpers.py
+++ b/flask/helpers.py
@@ -10,6 +10,7 @@
"""
import os
+import socket
import sys
import pkgutil
import posixpath
@@ -17,6 +18,7 @@ import mimetypes
from time import time
from zlib import adler32
from threading import RLock
+import unicodedata
from werkzeug.routing import BuildError
from functools import update_wrapper
@@ -330,6 +332,7 @@ def url_for(endpoint, **values):
values['_external'] = external
values['_anchor'] = anchor
values['_method'] = method
+ values['_scheme'] = scheme
return appctx.app.handle_url_build_error(error, endpoint, values)
if anchor is not None:
@@ -477,8 +480,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
.. versionchanged:: 0.12
The `attachment_filename` is preferred over `filename` for MIME-type
detection.
+
+ .. versionchanged:: 0.13
+ UTF-8 filenames, as specified in `RFC 2231`_, are supported.
+
+ .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4
- :param filename_or_fp: the filename of the file to send in `latin-1`.
+ :param filename_or_fp: the filename of the file to send.
This is relative to the :attr:`~Flask.root_path`
if a relative path is specified.
Alternatively a file object might be provided in
@@ -534,8 +542,19 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
if attachment_filename is None:
raise TypeError('filename unavailable, required for '
'sending as attachment')
- headers.add('Content-Disposition', 'attachment',
- filename=attachment_filename)
+
+ try:
+ attachment_filename = attachment_filename.encode('latin-1')
+ except UnicodeEncodeError:
+ filenames = {
+ 'filename': unicodedata.normalize(
+ 'NFKD', attachment_filename).encode('latin-1', 'ignore'),
+ 'filename*': "UTF-8''%s" % url_quote(attachment_filename),
+ }
+ else:
+ filenames = {'filename': attachment_filename}
+
+ headers.add('Content-Disposition', 'attachment', **filenames)
if current_app.use_x_sendfile and filename:
if file is not None:
@@ -619,18 +638,24 @@ def safe_join(directory, *pathnames):
:raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed
paths fall out of its boundaries.
"""
+
+ parts = [directory]
+
for filename in pathnames:
if filename != '':
filename = posixpath.normpath(filename)
- for sep in _os_alt_seps:
- if sep in filename:
- raise NotFound()
- if os.path.isabs(filename) or \
- filename == '..' or \
- filename.startswith('../'):
+
+ if (
+ any(sep in filename for sep in _os_alt_seps)
+ or os.path.isabs(filename)
+ or filename == '..'
+ or filename.startswith('../')
+ ):
raise NotFound()
- directory = os.path.join(directory, filename)
- return directory
+
+ parts.append(filename)
+
+ return posixpath.join(*parts)
def send_from_directory(directory, filename, **options):
@@ -958,3 +983,38 @@ def total_seconds(td):
:rtype: int
"""
return td.days * 60 * 60 * 24 + td.seconds
+
+
+def is_ip(value):
+ """Determine if the given string is an IP address.
+
+ :param value: value to check
+ :type value: str
+
+ :return: True if string is an IP address
+ :rtype: bool
+ """
+
+ for family in (socket.AF_INET, socket.AF_INET6):
+ try:
+ socket.inet_pton(family, value)
+ except socket.error:
+ pass
+ else:
+ return True
+
+ 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
diff --git a/flask/json.py b/flask/json.py
index 19b337c3..a029e73a 100644
--- a/flask/json.py
+++ b/flask/json.py
@@ -91,9 +91,16 @@ class JSONDecoder(_json.JSONDecoder):
def _dump_arg_defaults(kwargs):
"""Inject default arguments for dump functions."""
if current_app:
- kwargs.setdefault('cls', current_app.json_encoder)
+ bp = current_app.blueprints.get(request.blueprint) if request else None
+ kwargs.setdefault(
+ 'cls',
+ bp.json_encoder if bp and bp.json_encoder
+ else current_app.json_encoder
+ )
+
if not current_app.config['JSON_AS_ASCII']:
kwargs.setdefault('ensure_ascii', False)
+
kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS'])
else:
kwargs.setdefault('sort_keys', True)
@@ -103,7 +110,12 @@ def _dump_arg_defaults(kwargs):
def _load_arg_defaults(kwargs):
"""Inject default arguments for load functions."""
if current_app:
- kwargs.setdefault('cls', current_app.json_decoder)
+ bp = current_app.blueprints.get(request.blueprint) if request else None
+ kwargs.setdefault(
+ 'cls',
+ bp.json_decoder if bp and bp.json_decoder
+ else current_app.json_decoder
+ )
else:
kwargs.setdefault('cls', JSONDecoder)
@@ -236,11 +248,10 @@ def jsonify(*args, **kwargs):
Added support for serializing top-level arrays. This introduces a
security risk in ancient browsers. See :ref:`json-security` for details.
- This function's response will be pretty printed if it was not requested
- with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless
- the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false.
- Compressed (not pretty) formatting currently means no indents and no
- spaces after separators.
+ This function's response will be pretty printed if the
+ ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to True or the
+ Flask app is running in debug mode. Compressed (not pretty) formatting
+ currently means no indents and no spaces after separators.
.. versionadded:: 0.2
"""
@@ -248,7 +259,7 @@ def jsonify(*args, **kwargs):
indent = None
separators = (',', ':')
- if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] and not request.is_xhr:
+ if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] or current_app.debug:
indent = 2
separators = (', ', ': ')
diff --git a/flask/sessions.py b/flask/sessions.py
index 4335eeaa..4f2c84f5 100644
--- a/flask/sessions.py
+++ b/flask/sessions.py
@@ -8,17 +8,20 @@
:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-import uuid
import hashlib
-from base64 import b64encode, b64decode
+import uuid
+import warnings
+from base64 import b64decode, b64encode
from datetime import datetime
-from werkzeug.http import http_date, parse_date
+
+from itsdangerous import BadSignature, URLSafeTimedSerializer
from werkzeug.datastructures import CallbackDict
+from werkzeug.http import http_date, parse_date
+
+from flask.helpers import patch_vary_header
from . import Markup, json
from ._compat import iteritems, text_type
-from .helpers import total_seconds
-
-from itsdangerous import URLSafeTimedSerializer, BadSignature
+from .helpers import is_ip, total_seconds
class SessionMixin(object):
@@ -47,6 +50,13 @@ class SessionMixin(object):
#: The default mixin implementation just hardcodes ``True`` in.
modified = True
+ #: the accessed variable indicates whether or not the session object has
+ #: been accessed in that request. This allows flask to append a `Vary:
+ #: Cookie` header to the response if the session is being accessed. This
+ #: allows caching proxy servers, like Varnish, to use both the URL and the
+ #: session cookie as keys when caching pages, preventing multiple users
+ #: from being served the same cache.
+ accessed = True
class TaggedJSONSerializer(object):
"""A customized JSON serializer that supports a few extra types that
@@ -175,8 +185,23 @@ class SecureCookieSession(CallbackDict, SessionMixin):
def __init__(self, initial=None):
def on_update(self):
self.modified = True
- CallbackDict.__init__(self, initial, on_update)
+ self.accessed = True
+
+ super(SecureCookieSession, self).__init__(initial, on_update)
self.modified = False
+ self.accessed = False
+
+ def __getitem__(self, key):
+ self.accessed = True
+ return super(SecureCookieSession, self).__getitem__(key)
+
+ def get(self, key, default=None):
+ self.accessed = True
+ return super(SecureCookieSession, self).get(key, default)
+
+ def setdefault(self, key, default=None):
+ self.accessed = True
+ return super(SecureCookieSession, self).setdefault(key, default)
class NullSession(SecureCookieSession):
@@ -259,30 +284,62 @@ class SessionInterface(object):
return isinstance(obj, self.null_session_class)
def get_cookie_domain(self, app):
- """Helpful helper method that returns the cookie domain that should
- be used for the session cookie if session cookies are used.
+ """Returns the domain that should be set for the session cookie.
+
+ Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
+ falls back to detecting the domain based on ``SERVER_NAME``.
+
+ Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
+ updated to avoid re-running the logic.
"""
- if app.config['SESSION_COOKIE_DOMAIN'] is not None:
- return app.config['SESSION_COOKIE_DOMAIN']
- if app.config['SERVER_NAME'] is not None:
- # chop off the port which is usually not supported by browsers
- rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
-
- # Google chrome does not like cookies set to .localhost, so
- # we just go with no domain then. Flask documents anyways that
- # cross domain cookies need a fully qualified domain name
- if rv == '.localhost':
- rv = None
-
- # If we infer the cookie domain from the server name we need
- # to check if we are in a subpath. In that case we can't
- # set a cross domain cookie.
- if rv is not None:
- path = self.get_cookie_path(app)
- if path != '/':
- rv = rv.lstrip('.')
-
- return rv
+
+ rv = app.config['SESSION_COOKIE_DOMAIN']
+
+ # set explicitly, or cached from SERVER_NAME detection
+ # if False, return None
+ if rv is not None:
+ return rv if rv else None
+
+ rv = app.config['SERVER_NAME']
+
+ # server name not set, cache False to return none next time
+ if not rv:
+ app.config['SESSION_COOKIE_DOMAIN'] = False
+ return None
+
+ # chop off the port which is usually not supported by browsers
+ # remove any leading '.' since we'll add that later
+ rv = rv.rsplit(':', 1)[0].lstrip('.')
+
+ if '.' not in rv:
+ # Chrome doesn't allow names without a '.'
+ # this should only come up with localhost
+ # hack around this by not setting the name, and show a warning
+ warnings.warn(
+ '"{rv}" is not a valid cookie domain, it must contain a ".".'
+ ' Add an entry to your hosts file, for example'
+ ' "{rv}.localdomain", and use that instead.'.format(rv=rv)
+ )
+ app.config['SESSION_COOKIE_DOMAIN'] = False
+ return None
+
+ ip = is_ip(rv)
+
+ if ip:
+ warnings.warn(
+ 'The session cookie domain is an IP address. This may not work'
+ ' as intended in some browsers. Add an entry to your hosts'
+ ' file, for example "localhost.localdomain", and use that'
+ ' instead.'
+ )
+
+ # if this is not an ip and app is mounted at the root, allow subdomain
+ # matching by adding a '.' prefix
+ if self.get_cookie_path(app) == '/' and not ip:
+ rv = '.' + rv
+
+ app.config['SESSION_COOKIE_DOMAIN'] = rv
+ return rv
def get_cookie_path(self, app):
"""Returns the path for which the cookie should be valid. The
@@ -316,22 +373,20 @@ class SessionInterface(object):
return datetime.utcnow() + app.permanent_session_lifetime
def should_set_cookie(self, app, session):
- """Indicates whether a cookie should be set now or not. This is
- used by session backends to figure out if they should emit a
- set-cookie header or not. The default behavior is controlled by
- the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If
- it's set to ``False`` then a cookie is only set if the session is
- modified, if set to ``True`` it's always set if the session is
- permanent.
-
- This check is usually skipped if sessions get deleted.
+ """Used by session backends to determine if a ``Set-Cookie`` header
+ should be set for this session cookie for this response. If the session
+ has been modified, the cookie is set. If the session is permanent and
+ the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
+ always set.
+
+ This check is usually skipped if the session was deleted.
.. versionadded:: 0.11
"""
- if session.modified:
- return True
- save_each = app.config['SESSION_REFRESH_EACH_REQUEST']
- return save_each and session.permanent
+
+ return session.modified or (
+ session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
+ )
def open_session(self, app, request):
"""This method has to be implemented and must either return ``None``
@@ -397,22 +452,22 @@ class SecureCookieSessionInterface(SessionInterface):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
- # Delete case. If there is no session we bail early.
- # If the session was modified to be empty we remove the
- # whole cookie.
+ # If the session is modified to be empty, remove the cookie.
+ # If the session is empty, return without setting the cookie.
if not session:
if session.modified:
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
+ response.delete_cookie(
+ app.session_cookie_name,
+ domain=domain,
+ path=path
+ )
+
return
- # Modification case. There are upsides and downsides to
- # emitting a set-cookie header each request. The behavior
- # is controlled by the :meth:`should_set_cookie` method
- # which performs a quick check to figure out if the cookie
- # should be set or not. This is controlled by the
- # SESSION_REFRESH_EACH_REQUEST config flag as well as
- # the permanent flag on the session itself.
+ # Add a "Vary: Cookie" header if the session was accessed at all.
+ if session.accessed:
+ patch_vary_header(response, 'Cookie')
+
if not self.should_set_cookie(app, session):
return
@@ -420,6 +475,12 @@ class SecureCookieSessionInterface(SessionInterface):
secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session))
- response.set_cookie(app.session_cookie_name, val,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure)
+ response.set_cookie(
+ app.session_cookie_name,
+ val,
+ expires=expires,
+ httponly=httponly,
+ domain=domain,
+ path=path,
+ secure=secure
+ )
diff --git a/flask/views.py b/flask/views.py
index 83394c1f..b3027970 100644
--- a/flask/views.py
+++ b/flask/views.py
@@ -51,6 +51,9 @@ class View(object):
#: A list of methods this view can handle.
methods = None
+ #: Setting this disables or force-enables the automatic options handling.
+ provide_automatic_options = None
+
#: The canonical way to decorate class-based views is to decorate the
#: return value of as_view(). However since this moves parts of the
#: logic from the class declaration to the place where it's hooked
@@ -99,37 +102,39 @@ class View(object):
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.methods = cls.methods
+ view.provide_automatic_options = cls.provide_automatic_options
return view
class MethodViewType(type):
+ """Metaclass for :class:`MethodView` that determines what methods the view
+ defines.
+ """
+
+ def __init__(cls, name, bases, d):
+ super(MethodViewType, cls).__init__(name, bases, d)
- def __new__(cls, name, bases, d):
- rv = type.__new__(cls, name, bases, d)
if 'methods' not in d:
- methods = set(rv.methods or [])
- for key in d:
- if key in http_method_funcs:
+ methods = set()
+
+ for key in http_method_funcs:
+ if hasattr(cls, key):
methods.add(key.upper())
- # If we have no method at all in there we don't want to
- # add a method list. (This is for instance the case for
- # the base class or another subclass of a base method view
- # that does not introduce new methods).
+
+ # If we have no method at all in there we don't want to add a
+ # method list. This is for instance the case for the base class
+ # or another subclass of a base method view that does not introduce
+ # new methods.
if methods:
- rv.methods = sorted(methods)
- return rv
+ cls.methods = methods
class MethodView(with_metaclass(MethodViewType, View)):
- """Like a regular class-based view but that dispatches requests to
- particular methods. For instance if you implement a method called
- :meth:`get` it means it will respond to ``'GET'`` requests and
- the :meth:`dispatch_request` implementation will automatically
- forward your request to that. Also :attr:`options` is set for you
- automatically::
+ """A class-based view that dispatches request methods to the corresponding
+ class methods. For example, if you implement a ``get`` method, it will be
+ used to handle ``GET`` requests. ::
class CounterAPI(MethodView):
-
def get(self):
return session.get('counter', 0)
@@ -139,11 +144,14 @@ class MethodView(with_metaclass(MethodViewType, View)):
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
"""
+
def dispatch_request(self, *args, **kwargs):
meth = getattr(self, request.method.lower(), None)
+
# If the request method is HEAD and we don't have a handler for it
# retry with GET.
if meth is None and request.method == 'HEAD':
meth = getattr(self, 'get', None)
+
assert meth is not None, 'Unimplemented method %r' % request.method
return meth(*args, **kwargs)
diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py
index 7fbdd49c..18e1a14b 100644
--- a/scripts/flask-07-upgrade.py
+++ b/scripts/flask-07-upgrade.py
@@ -5,7 +5,7 @@
~~~~~~~~~~~~~~~~
This command line script scans a whole application tree and attempts to
- output an unified diff with all the changes that are necessary to easily
+ output a unified diff with all the changes that are necessary to easily
upgrade the application to 0.7 and to not yield deprecation warnings.
This will also attempt to find `after_request` functions that don't modify
diff --git a/setup.cfg b/setup.cfg
index 781de592..527dd3e6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,4 +8,5 @@ universal = 1
license_file = LICENSE
[tool:pytest]
-norecursedirs = .* *.egg *.egg-info env* artwork docs
+minversion = 3.0
+testpaths = tests
diff --git a/setup.py b/setup.py
index 08995073..d8cd874a 100644
--- a/setup.py
+++ b/setup.py
@@ -71,10 +71,10 @@ setup(
zip_safe=False,
platforms='any',
install_requires=[
- 'Werkzeug>=0.7',
+ 'Werkzeug>=0.9',
'Jinja2>=2.4',
'itsdangerous>=0.21',
- 'click>=2.0',
+ 'click>=4.0',
],
classifiers=[
'Development Status :: 4 - Beta',
diff --git a/test-requirements.txt b/test-requirements.txt
index 053148f8..edf1abb9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1 +1,3 @@
tox
+pytest
+pytest-cov
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 8c9541de..40b1e88f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,40 @@ import sys
import pkgutil
import pytest
import textwrap
+from flask import Flask as _Flask
+
+
+class Flask(_Flask):
+ testing = True
+ secret_key = __name__
+
+ def make_response(self, rv):
+ if rv is None:
+ rv = ''
+ return super(Flask, self).make_response(rv)
+
+
+@pytest.fixture
+def app():
+ app = Flask(__name__)
+ return app
+
+
+@pytest.fixture
+def app_ctx(app):
+ with app.app_context() as ctx:
+ yield ctx
+
+
+@pytest.fixture
+def req_ctx(app):
+ with app.test_request_context() as ctx:
+ yield ctx
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
@pytest.fixture
@@ -22,16 +56,17 @@ def test_apps(monkeypatch):
os.path.dirname(__file__), 'test_apps'))
)
+
@pytest.fixture(autouse=True)
-def leak_detector(request):
- def ensure_clean_request_context():
- # make sure we're not leaking a request context since we are
- # testing flask internally in debug mode in a few cases
- leaks = []
- while flask._request_ctx_stack.top is not None:
- leaks.append(flask._request_ctx_stack.pop())
- assert leaks == []
- request.addfinalizer(ensure_clean_request_context)
+def leak_detector():
+ yield
+
+ # make sure we're not leaking a request context since we are
+ # testing flask internally in debug mode in a few cases
+ leaks = []
+ while flask._request_ctx_stack.top is not None:
+ leaks.append(flask._request_ctx_stack.pop())
+ assert leaks == []
@pytest.fixture(params=(True, False))
@@ -62,12 +97,13 @@ def limit_loader(request, monkeypatch):
def get_loader(*args, **kwargs):
return LimitedLoader(old_get_loader(*args, **kwargs))
+
monkeypatch.setattr(pkgutil, 'get_loader', get_loader)
@pytest.fixture
def modules_tmpdir(tmpdir, monkeypatch):
- '''A tmpdir added to sys.path'''
+ """A tmpdir added to sys.path."""
rv = tmpdir.mkdir('modules_tmpdir')
monkeypatch.syspath_prepend(str(rv))
return rv
@@ -81,10 +117,10 @@ def modules_tmpdir_prefix(modules_tmpdir, monkeypatch):
@pytest.fixture
def site_packages(modules_tmpdir, monkeypatch):
- '''Create a fake site-packages'''
+ """Create a fake site-packages."""
rv = modules_tmpdir \
- .mkdir('lib')\
- .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info))\
+ .mkdir('lib') \
+ .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info)) \
.mkdir('site-packages')
monkeypatch.syspath_prepend(str(rv))
return rv
@@ -92,8 +128,9 @@ def site_packages(modules_tmpdir, monkeypatch):
@pytest.fixture
def install_egg(modules_tmpdir, monkeypatch):
- '''Generate egg from package name inside base and put the egg into
- sys.path'''
+ """Generate egg from package name inside base and put the egg into
+ sys.path."""
+
def inner(name, base=modules_tmpdir):
if not isinstance(name, str):
raise ValueError(name)
@@ -117,6 +154,7 @@ def install_egg(modules_tmpdir, monkeypatch):
egg_path, = modules_tmpdir.join('dist/').listdir()
monkeypatch.syspath_prepend(str(egg_path))
return egg_path
+
return inner
@@ -124,6 +162,7 @@ def install_egg(modules_tmpdir, monkeypatch):
def purge_module(request):
def inner(name):
request.addfinalizer(lambda: sys.modules.pop(name, None))
+
return inner
@@ -131,4 +170,4 @@ def purge_module(request):
def catch_deprecation_warnings(recwarn):
yield
gc.collect()
- assert not recwarn.list
+ assert not recwarn.list, '\n'.join(str(w.message) for w in recwarn.list)
diff --git a/tests/test_appctx.py b/tests/test_appctx.py
index 13b61eee..7ef7b479 100644
--- a/tests/test_appctx.py
+++ b/tests/test_appctx.py
@@ -14,8 +14,7 @@ import pytest
import flask
-def test_basic_url_generation():
- app = flask.Flask(__name__)
+def test_basic_url_generation(app):
app.config['SERVER_NAME'] = 'localhost'
app.config['PREFERRED_URL_SCHEME'] = 'https'
@@ -27,31 +26,33 @@ def test_basic_url_generation():
rv = flask.url_for('index')
assert rv == 'https://localhost/'
-def test_url_generation_requires_server_name():
- app = flask.Flask(__name__)
+
+def test_url_generation_requires_server_name(app):
with app.app_context():
with pytest.raises(RuntimeError):
flask.url_for('index')
+
def test_url_generation_without_context_fails():
with pytest.raises(RuntimeError):
flask.url_for('index')
-def test_request_context_means_app_context():
- app = flask.Flask(__name__)
+
+def test_request_context_means_app_context(app):
with app.test_request_context():
assert flask.current_app._get_current_object() == app
assert flask._app_ctx_stack.top is None
-def test_app_context_provides_current_app():
- app = flask.Flask(__name__)
+
+def test_app_context_provides_current_app(app):
with app.app_context():
assert flask.current_app._get_current_object() == app
assert flask._app_ctx_stack.top is None
-def test_app_tearing_down():
+
+def test_app_tearing_down(app):
cleanup_stuff = []
- app = flask.Flask(__name__)
+
@app.teardown_appcontext
def cleanup(exception):
cleanup_stuff.append(exception)
@@ -61,9 +62,10 @@ def test_app_tearing_down():
assert cleanup_stuff == [None]
-def test_app_tearing_down_with_previous_exception():
+
+def test_app_tearing_down_with_previous_exception(app):
cleanup_stuff = []
- app = flask.Flask(__name__)
+
@app.teardown_appcontext
def cleanup(exception):
cleanup_stuff.append(exception)
@@ -78,9 +80,10 @@ def test_app_tearing_down_with_previous_exception():
assert cleanup_stuff == [None]
-def test_app_tearing_down_with_handled_exception():
+
+def test_app_tearing_down_with_handled_exception(app):
cleanup_stuff = []
- app = flask.Flask(__name__)
+
@app.teardown_appcontext
def cleanup(exception):
cleanup_stuff.append(exception)
@@ -93,46 +96,49 @@ def test_app_tearing_down_with_handled_exception():
assert cleanup_stuff == [None]
-def test_app_ctx_globals_methods():
- app = flask.Flask(__name__)
- with app.app_context():
- # get
- assert flask.g.get('foo') is None
- assert flask.g.get('foo', 'bar') == 'bar'
- # __contains__
- assert 'foo' not in flask.g
- flask.g.foo = 'bar'
- assert 'foo' in flask.g
- # setdefault
- flask.g.setdefault('bar', 'the cake is a lie')
- flask.g.setdefault('bar', 'hello world')
- assert flask.g.bar == 'the cake is a lie'
- # pop
- assert flask.g.pop('bar') == 'the cake is a lie'
- with pytest.raises(KeyError):
- flask.g.pop('bar')
- assert flask.g.pop('bar', 'more cake') == 'more cake'
- # __iter__
- assert list(flask.g) == ['foo']
-
-def test_custom_app_ctx_globals_class():
+
+def test_app_ctx_globals_methods(app, app_ctx):
+ # get
+ assert flask.g.get('foo') is None
+ assert flask.g.get('foo', 'bar') == 'bar'
+ # __contains__
+ assert 'foo' not in flask.g
+ flask.g.foo = 'bar'
+ assert 'foo' in flask.g
+ # setdefault
+ flask.g.setdefault('bar', 'the cake is a lie')
+ flask.g.setdefault('bar', 'hello world')
+ assert flask.g.bar == 'the cake is a lie'
+ # pop
+ assert flask.g.pop('bar') == 'the cake is a lie'
+ with pytest.raises(KeyError):
+ flask.g.pop('bar')
+ assert flask.g.pop('bar', 'more cake') == 'more cake'
+ # __iter__
+ assert list(flask.g) == ['foo']
+
+
+def test_custom_app_ctx_globals_class(app):
class CustomRequestGlobals(object):
def __init__(self):
self.spam = 'eggs'
- app = flask.Flask(__name__)
+
app.app_ctx_globals_class = CustomRequestGlobals
with app.app_context():
assert flask.render_template_string('{{ g.spam }}') == 'eggs'
-def test_context_refcounts():
+
+def test_context_refcounts(app, client):
called = []
- app = flask.Flask(__name__)
+
@app.teardown_request
def teardown_req(error=None):
called.append('request')
+
@app.teardown_appcontext
def teardown_app(error=None):
called.append('app')
+
@app.route('/')
def index():
with flask._app_ctx_stack.top:
@@ -141,16 +147,16 @@ def test_context_refcounts():
env = flask._request_ctx_stack.top.request.environ
assert env['werkzeug.request'] is not None
return u''
- c = app.test_client()
- res = c.get('/')
+
+ res = client.get('/')
assert res.status_code == 200
assert res.data == b''
assert called == ['request', 'app']
-def test_clean_pop():
+def test_clean_pop(app):
+ app.testing = False
called = []
- app = flask.Flask(__name__)
@app.teardown_request
def teardown_req(error=None):
@@ -166,5 +172,5 @@ def test_clean_pop():
except ZeroDivisionError:
pass
- assert called == ['test_appctx', 'TEARDOWN']
+ assert called == ['conftest', 'TEARDOWN']
assert not flask.current_app
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 62a90cf6..8bb079d3 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -25,20 +25,17 @@ from werkzeug.routing import BuildError
import werkzeug.serving
-def test_options_work():
- app = flask.Flask(__name__)
-
+def test_options_work(app, client):
@app.route('/', methods=['GET', 'POST'])
def index():
return 'Hello World'
- rv = app.test_client().open('/', method='OPTIONS')
+
+ rv = client.open('/', method='OPTIONS')
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST']
assert rv.data == b''
-def test_options_on_multiple_rules():
- app = flask.Flask(__name__)
-
+def test_options_on_multiple_rules(app, client):
@app.route('/', methods=['GET', 'POST'])
def index():
return 'Hello World'
@@ -46,15 +43,17 @@ def test_options_on_multiple_rules():
@app.route('/', methods=['PUT'])
def index_put():
return 'Aha!'
- rv = app.test_client().open('/', method='OPTIONS')
+
+ rv = client.open('/', method='OPTIONS')
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
-def test_options_handling_disabled():
+def test_provide_automatic_options_attr():
app = flask.Flask(__name__)
def index():
return 'Hello World!'
+
index.provide_automatic_options = False
app.route('/')(index)
rv = app.test_client().open('/', method='OPTIONS')
@@ -64,15 +63,58 @@ def test_options_handling_disabled():
def index2():
return 'Hello World!'
+
index2.provide_automatic_options = True
app.route('/', methods=['OPTIONS'])(index2)
rv = app.test_client().open('/', method='OPTIONS')
assert sorted(rv.allow) == ['OPTIONS']
-def test_request_dispatching():
- app = flask.Flask(__name__)
+def test_provide_automatic_options_kwarg(app, client):
+ def index():
+ return flask.request.method
+
+ def more():
+ return flask.request.method
+
+ app.add_url_rule('/', view_func=index, provide_automatic_options=False)
+ app.add_url_rule(
+ '/more', view_func=more, methods=['GET', 'POST'],
+ provide_automatic_options=False
+ )
+ assert client.get('/').data == b'GET'
+
+ rv = client.post('/')
+ assert rv.status_code == 405
+ assert sorted(rv.allow) == ['GET', 'HEAD']
+ # Older versions of Werkzeug.test.Client don't have an options method
+ if hasattr(client, 'options'):
+ rv = client.options('/')
+ else:
+ rv = client.open('/', method='OPTIONS')
+
+ assert rv.status_code == 405
+
+ rv = client.head('/')
+ assert rv.status_code == 200
+ assert not rv.data # head truncates
+ assert client.post('/more').data == b'POST'
+ assert client.get('/more').data == b'GET'
+
+ rv = client.delete('/more')
+ assert rv.status_code == 405
+ assert sorted(rv.allow) == ['GET', 'HEAD', 'POST']
+
+ if hasattr(client, 'options'):
+ rv = client.options('/more')
+ else:
+ rv = client.open('/more', method='OPTIONS')
+
+ assert rv.status_code == 405
+
+
+def test_request_dispatching(app, client):
@app.route('/')
def index():
return flask.request.method
@@ -81,32 +123,28 @@ def test_request_dispatching():
def more():
return flask.request.method
- c = app.test_client()
- assert c.get('/').data == b'GET'
- rv = c.post('/')
+ assert client.get('/').data == b'GET'
+ rv = client.post('/')
assert rv.status_code == 405
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS']
- rv = c.head('/')
+ rv = client.head('/')
assert rv.status_code == 200
assert not rv.data # head truncates
- assert c.post('/more').data == b'POST'
- assert c.get('/more').data == b'GET'
- rv = c.delete('/more')
+ assert client.post('/more').data == b'POST'
+ assert client.get('/more').data == b'GET'
+ rv = client.delete('/more')
assert rv.status_code == 405
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST']
-def test_disallow_string_for_allowed_methods():
- app = flask.Flask(__name__)
+def test_disallow_string_for_allowed_methods(app):
with pytest.raises(TypeError):
@app.route('/', methods='GET POST')
def index():
return "Hey"
-def test_url_mapping():
- app = flask.Flask(__name__)
-
+def test_url_mapping(app, client):
random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383"
def index():
@@ -118,34 +156,31 @@ def test_url_mapping():
def options():
return random_uuid4
-
app.add_url_rule('/', 'index', index)
app.add_url_rule('/more', 'more', more, methods=['GET', 'POST'])
# Issue 1288: Test that automatic options are not added when non-uppercase 'options' in methods
app.add_url_rule('/options', 'options', options, methods=['options'])
- c = app.test_client()
- assert c.get('/').data == b'GET'
- rv = c.post('/')
+ assert client.get('/').data == b'GET'
+ rv = client.post('/')
assert rv.status_code == 405
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS']
- rv = c.head('/')
+ rv = client.head('/')
assert rv.status_code == 200
assert not rv.data # head truncates
- assert c.post('/more').data == b'POST'
- assert c.get('/more').data == b'GET'
- rv = c.delete('/more')
+ assert client.post('/more').data == b'POST'
+ assert client.get('/more').data == b'GET'
+ rv = client.delete('/more')
assert rv.status_code == 405
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST']
- rv = c.open('/options', method='OPTIONS')
+ rv = client.open('/options', method='OPTIONS')
assert rv.status_code == 200
assert random_uuid4 in rv.data.decode("utf-8")
-def test_werkzeug_routing():
+def test_werkzeug_routing(app, client):
from werkzeug.routing import Submount, Rule
- app = flask.Flask(__name__)
app.url_map.add(Submount('/foo', [
Rule('/bar', endpoint='bar'),
Rule('/', endpoint='index')
@@ -156,17 +191,16 @@ def test_werkzeug_routing():
def index():
return 'index'
+
app.view_functions['bar'] = bar
app.view_functions['index'] = index
- c = app.test_client()
- assert c.get('/foo/').data == b'index'
- assert c.get('/foo/bar').data == b'bar'
+ assert client.get('/foo/').data == b'index'
+ assert client.get('/foo/bar').data == b'bar'
-def test_endpoint_decorator():
+def test_endpoint_decorator(app, client):
from werkzeug.routing import Submount, Rule
- app = flask.Flask(__name__)
app.url_map.add(Submount('/foo', [
Rule('/bar', endpoint='bar'),
Rule('/', endpoint='index')
@@ -180,13 +214,11 @@ def test_endpoint_decorator():
def index():
return 'index'
- c = app.test_client()
- assert c.get('/foo/').data == b'index'
- assert c.get('/foo/bar').data == b'bar'
+ assert client.get('/foo/').data == b'index'
+ assert client.get('/foo/bar').data == b'bar'
-def test_session():
- app = flask.Flask(__name__)
+def test_session(app, client):
app.secret_key = 'testkey'
@app.route('/set', methods=['POST'])
@@ -198,13 +230,11 @@ def test_session():
def get():
return flask.session['value']
- c = app.test_client()
- assert c.post('/set', data={'value': '42'}).data == b'value set'
- assert c.get('/get').data == b'42'
+ assert client.post('/set', data={'value': '42'}).data == b'value set'
+ assert client.get('/get').data == b'42'
-def test_session_using_server_name():
- app = flask.Flask(__name__)
+def test_session_using_server_name(app, client):
app.config.update(
SECRET_KEY='foo',
SERVER_NAME='example.com'
@@ -214,13 +244,13 @@ def test_session_using_server_name():
def index():
flask.session['testing'] = 42
return 'Hello World'
- rv = app.test_client().get('/', 'http://example.com/')
+
+ rv = client.get('/', 'http://example.com/')
assert 'domain=.example.com' in rv.headers['set-cookie'].lower()
assert 'httponly' in rv.headers['set-cookie'].lower()
-def test_session_using_server_name_and_port():
- app = flask.Flask(__name__)
+def test_session_using_server_name_and_port(app, client):
app.config.update(
SECRET_KEY='foo',
SERVER_NAME='example.com:8080'
@@ -230,13 +260,13 @@ def test_session_using_server_name_and_port():
def index():
flask.session['testing'] = 42
return 'Hello World'
- rv = app.test_client().get('/', 'http://example.com:8080/')
+
+ rv = client.get('/', 'http://example.com:8080/')
assert 'domain=.example.com' in rv.headers['set-cookie'].lower()
assert 'httponly' in rv.headers['set-cookie'].lower()
-def test_session_using_server_name_port_and_path():
- app = flask.Flask(__name__)
+def test_session_using_server_name_port_and_path(app, client):
app.config.update(
SECRET_KEY='foo',
SERVER_NAME='example.com:8080',
@@ -247,15 +277,15 @@ def test_session_using_server_name_port_and_path():
def index():
flask.session['testing'] = 42
return 'Hello World'
- rv = app.test_client().get('/', 'http://example.com:8080/foo')
+
+ rv = client.get('/', 'http://example.com:8080/foo')
assert 'domain=example.com' in rv.headers['set-cookie'].lower()
assert 'path=/foo' in rv.headers['set-cookie'].lower()
assert 'httponly' in rv.headers['set-cookie'].lower()
-def test_session_using_application_root():
+def test_session_using_application_root(app, client):
class PrefixPathMiddleware(object):
-
def __init__(self, app, prefix):
self.app = app
self.prefix = prefix
@@ -264,7 +294,6 @@ def test_session_using_application_root():
environ['SCRIPT_NAME'] = self.prefix
return self.app(environ, start_response)
- app = flask.Flask(__name__)
app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar')
app.config.update(
SECRET_KEY='foo',
@@ -275,12 +304,12 @@ def test_session_using_application_root():
def index():
flask.session['testing'] = 42
return 'Hello World'
- rv = app.test_client().get('/', 'http://example.com:8080/')
+
+ rv = client.get('/', 'http://example.com:8080/')
assert 'path=/bar' in rv.headers['set-cookie'].lower()
-def test_session_using_session_settings():
- app = flask.Flask(__name__)
+def test_session_using_session_settings(app, client):
app.config.update(
SECRET_KEY='foo',
SERVER_NAME='www.example.com:8080',
@@ -295,7 +324,8 @@ def test_session_using_session_settings():
def index():
flask.session['testing'] = 42
return 'Hello World'
- rv = app.test_client().get('/', 'http://www.example.com:8080/test/')
+
+ rv = client.get('/', 'http://www.example.com:8080/test/')
cookie = rv.headers['set-cookie'].lower()
assert 'domain=.example.com' in cookie
assert 'path=/' in cookie
@@ -303,21 +333,55 @@ def test_session_using_session_settings():
assert 'httponly' not in cookie
+def test_session_localhost_warning(recwarn, app, client):
+ app.config.update(
+ SECRET_KEY='testing',
+ SERVER_NAME='localhost:5000',
+ )
+
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'testing'
+
+ rv = client.get('/', 'http://localhost:5000/')
+ assert 'domain' not in rv.headers['set-cookie'].lower()
+ w = recwarn.pop(UserWarning)
+ assert '"localhost" is not a valid cookie domain' in str(w.message)
+
+
+def test_session_ip_warning(recwarn, app, client):
+ app.config.update(
+ SECRET_KEY='testing',
+ SERVER_NAME='127.0.0.1:5000',
+ )
+
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'testing'
+
+ rv = client.get('/', 'http://127.0.0.1:5000/')
+ assert 'domain=127.0.0.1' in rv.headers['set-cookie'].lower()
+ w = recwarn.pop(UserWarning)
+ assert 'cookie domain is an IP' in str(w.message)
+
+
def test_missing_session():
app = flask.Flask(__name__)
def expect_exception(f, *args, **kwargs):
e = pytest.raises(RuntimeError, f, *args, **kwargs)
assert e.value.args and 'session is unavailable' in e.value.args[0]
+
with app.test_request_context():
assert flask.session.get('missing_key') is None
expect_exception(flask.session.__setitem__, 'foo', 42)
expect_exception(flask.session.pop, 'foo')
-def test_session_expiration():
+def test_session_expiration(app, client):
permanent = True
- app = flask.Flask(__name__)
app.secret_key = 'testkey'
@app.route('/')
@@ -330,7 +394,6 @@ def test_session_expiration():
def test():
return text_type(flask.session.permanent)
- client = app.test_client()
rv = client.get('/')
assert 'set-cookie' in rv.headers
match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie'])
@@ -344,16 +407,14 @@ def test_session_expiration():
assert rv.data == b'True'
permanent = False
- rv = app.test_client().get('/')
+ rv = client.get('/')
assert 'set-cookie' in rv.headers
match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie'])
assert match is None
-def test_session_stored_last():
- app = flask.Flask(__name__)
+def test_session_stored_last(app, client):
app.secret_key = 'development-key'
- app.testing = True
@app.after_request
def modify_session(response):
@@ -364,15 +425,12 @@ def test_session_stored_last():
def dump_session_contents():
return repr(flask.session.get('foo'))
- c = app.test_client()
- assert c.get('/').data == b'None'
- assert c.get('/').data == b'42'
+ assert client.get('/').data == b'None'
+ assert client.get('/').data == b'42'
-def test_session_special_types():
- app = flask.Flask(__name__)
+def test_session_special_types(app, client):
app.secret_key = 'development-key'
- app.testing = True
now = datetime.utcnow().replace(microsecond=0)
the_uuid = uuid.uuid4()
@@ -392,9 +450,8 @@ def test_session_special_types():
def dump_session_contents():
return pickle.dumps(dict(flask.session))
- c = app.test_client()
- c.get('/')
- rv = pickle.loads(c.get('/').data)
+ client.get('/')
+ rv = pickle.loads(client.get('/').data)
assert rv['m'] == flask.Markup('Hello!')
assert type(rv['m']) == flask.Markup
assert rv['dt'] == now
@@ -407,9 +464,7 @@ def test_session_special_types():
assert rv['notadict'] == {' di': 'not-a-dict'}
-def test_session_cookie_setting():
- app = flask.Flask(__name__)
- app.testing = True
+def test_session_cookie_setting(app):
app.secret_key = 'dev key'
is_permanent = True
@@ -451,29 +506,82 @@ def test_session_cookie_setting():
run_test(expect_header=False)
-def test_flashes():
- app = flask.Flask(__name__)
+def test_session_vary_cookie(app, client):
app.secret_key = 'testkey'
- with app.test_request_context():
- assert not flask.session.modified
- flask.flash('Zap')
- flask.session.modified = False
- flask.flash('Zip')
- assert flask.session.modified
- assert list(flask.get_flashed_messages()) == ['Zap', 'Zip']
+ @app.route('/set')
+ def set_session():
+ flask.session['test'] = 'test'
+ return ''
+
+ @app.route('/get')
+ def get():
+ return flask.session.get('test')
+
+ @app.route('/getitem')
+ def getitem():
+ return flask.session['test']
+
+ @app.route('/setdefault')
+ def setdefault():
+ return flask.session.setdefault('test', 'default')
+
+ @app.route('/vary-cookie-header-set')
+ def vary_cookie_header_set():
+ response = flask.Response()
+ response.headers['Vary'] = 'Cookie'
+ flask.session['test'] = 'test'
+ return response
+
+ @app.route('/vary-header-set')
+ def vary_header_set():
+ response = flask.Response()
+ response.headers['Vary'] = 'Accept-Encoding, Accept-Language'
+ flask.session['test'] = 'test'
+ return response
+
+ @app.route('/no-vary-header')
+ def no_vary_header():
+ return ''
+
+ def expect(path, header_value='Cookie'):
+ rv = client.get(path)
+
+ if header_value:
+ # The 'Vary' key should exist in the headers only once.
+ assert len(rv.headers.get_all('Vary')) == 1
+ assert rv.headers['Vary'] == header_value
+ else:
+ assert 'Vary' not in rv.headers
+
+ expect('/set')
+ expect('/get')
+ expect('/getitem')
+ expect('/setdefault')
+ expect('/vary-cookie-header-set')
+ expect('/vary-header-set', 'Accept-Encoding, Accept-Language, Cookie')
+ expect('/no-vary-header', None)
+
+
+def test_flashes(app, req_ctx):
+ app.secret_key = 'testkey'
+
+ assert not flask.session.modified
+ flask.flash('Zap')
+ flask.session.modified = False
+ flask.flash('Zip')
+ assert flask.session.modified
+ assert list(flask.get_flashed_messages()) == ['Zap', 'Zip']
-def test_extended_flashing():
+def test_extended_flashing(app):
# Be sure app.testing=True below, else tests can fail silently.
#
# Specifically, if app.testing is not set to True, the AssertionErrors
# in the view functions will cause a 500 response to the test client
# instead of propagating exceptions.
- app = flask.Flask(__name__)
app.secret_key = 'testkey'
- app.testing = True
@app.route('/')
def index():
@@ -531,29 +639,24 @@ def test_extended_flashing():
# Create new test client on each test to clean flashed messages.
- c = app.test_client()
- c.get('/')
- c.get('/test/')
-
- c = app.test_client()
- c.get('/')
- c.get('/test_with_categories/')
+ client = app.test_client()
+ client.get('/')
+ client.get('/test_with_categories/')
- c = app.test_client()
- c.get('/')
- c.get('/test_filter/')
+ client = app.test_client()
+ client.get('/')
+ client.get('/test_filter/')
- c = app.test_client()
- c.get('/')
- c.get('/test_filters/')
+ client = app.test_client()
+ client.get('/')
+ client.get('/test_filters/')
- c = app.test_client()
- c.get('/')
- c.get('/test_filters_without_returning_categories/')
+ client = app.test_client()
+ client.get('/')
+ client.get('/test_filters_without_returning_categories/')
-def test_request_processing():
- app = flask.Flask(__name__)
+def test_request_processing(app, client):
evts = []
@app.before_request
@@ -571,14 +674,14 @@ def test_request_processing():
assert 'before' in evts
assert 'after' not in evts
return 'request'
+
assert 'after' not in evts
- rv = app.test_client().get('/').data
+ rv = client.get('/').data
assert 'after' in evts
assert rv == b'request|after'
-def test_request_preprocessing_early_return():
- app = flask.Flask(__name__)
+def test_request_preprocessing_early_return(app, client):
evts = []
@app.before_request
@@ -600,31 +703,28 @@ def test_request_preprocessing_early_return():
evts.append('index')
return "damnit"
- rv = app.test_client().get('/').data.strip()
+ rv = client.get('/').data.strip()
assert rv == b'hello'
assert evts == [1, 2]
-def test_after_request_processing():
- app = flask.Flask(__name__)
- app.testing = True
-
+def test_after_request_processing(app, client):
@app.route('/')
def index():
@flask.after_this_request
def foo(response):
response.headers['X-Foo'] = 'a header'
return response
+
return 'Test'
- c = app.test_client()
- resp = c.get('/')
+
+ resp = client.get('/')
assert resp.status_code == 200
assert resp.headers['X-Foo'] == 'a header'
-def test_teardown_request_handler():
+def test_teardown_request_handler(app, client):
called = []
- app = flask.Flask(__name__)
@app.teardown_request
def teardown_request(exc):
@@ -634,16 +734,15 @@ def test_teardown_request_handler():
@app.route('/')
def root():
return "Response"
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.status_code == 200
assert b'Response' in rv.data
assert len(called) == 1
-def test_teardown_request_handler_debug_mode():
+def test_teardown_request_handler_debug_mode(app, client):
called = []
- app = flask.Flask(__name__)
- app.testing = True
@app.teardown_request
def teardown_request(exc):
@@ -653,16 +752,17 @@ def test_teardown_request_handler_debug_mode():
@app.route('/')
def root():
return "Response"
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.status_code == 200
assert b'Response' in rv.data
assert len(called) == 1
-def test_teardown_request_handler_error():
+def test_teardown_request_handler_error(app, client):
called = []
- app = flask.Flask(__name__)
app.config['LOGGER_HANDLER_POLICY'] = 'never'
+ app.testing = False
@app.teardown_request
def teardown_request1(exc):
@@ -691,15 +791,15 @@ def test_teardown_request_handler_error():
@app.route('/')
def fails():
1 // 0
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.status_code == 500
assert b'Internal Server Error' in rv.data
assert len(called) == 2
-def test_before_after_request_order():
+def test_before_after_request_order(app, client):
called = []
- app = flask.Flask(__name__)
@app.before_request
def before1():
@@ -730,14 +830,15 @@ def test_before_after_request_order():
@app.route('/')
def index():
return '42'
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.data == b'42'
assert called == [1, 2, 3, 4, 5, 6]
-def test_error_handling():
- app = flask.Flask(__name__)
+def test_error_handling(app, client):
app.config['LOGGER_HANDLER_POLICY'] = 'never'
+ app.testing = False
@app.errorhandler(404)
def not_found(e):
@@ -762,21 +863,21 @@ def test_error_handling():
@app.route('/forbidden')
def error2():
flask.abort(403)
- c = app.test_client()
- rv = c.get('/')
+
+ rv = client.get('/')
assert rv.status_code == 404
assert rv.data == b'not found'
- rv = c.get('/error')
+ rv = client.get('/error')
assert rv.status_code == 500
assert b'internal server error' == rv.data
- rv = c.get('/forbidden')
+ rv = client.get('/forbidden')
assert rv.status_code == 403
assert b'forbidden' == rv.data
-def test_error_handling_processing():
- app = flask.Flask(__name__)
+def test_error_handling_processing(app, client):
app.config['LOGGER_HANDLER_POLICY'] = 'never'
+ app.testing = False
@app.errorhandler(500)
def internal_server_error(e):
@@ -791,15 +892,28 @@ def test_error_handling_processing():
resp.mimetype = 'text/x-special'
return resp
- with app.test_client() as c:
- resp = c.get('/')
- assert resp.mimetype == 'text/x-special'
- assert resp.data == b'internal server error'
+ resp = client.get('/')
+ assert resp.mimetype == 'text/x-special'
+ assert resp.data == b'internal server error'
-def test_before_request_and_routing_errors():
- app = flask.Flask(__name__)
+def test_baseexception_error_handling(app, client):
+ app.config['LOGGER_HANDLER_POLICY'] = 'never'
+ app.testing = False
+
+ @app.route('/')
+ def broken_func():
+ raise KeyboardInterrupt()
+
+ with pytest.raises(KeyboardInterrupt):
+ client.get('/')
+ ctx = flask._request_ctx_stack.top
+ assert ctx.preserved
+ assert type(ctx._preserved_exc) is KeyboardInterrupt
+
+
+def test_before_request_and_routing_errors(app, client):
@app.before_request
def attach_something():
flask.g.something = 'value'
@@ -807,17 +921,16 @@ def test_before_request_and_routing_errors():
@app.errorhandler(404)
def return_something(error):
return flask.g.something, 404
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.status_code == 404
assert rv.data == b'value'
-def test_user_error_handling():
+def test_user_error_handling(app, client):
class MyException(Exception):
pass
- app = flask.Flask(__name__)
-
@app.errorhandler(MyException)
def handle_my_exception(e):
assert isinstance(e, MyException)
@@ -827,16 +940,13 @@ def test_user_error_handling():
def index():
raise MyException()
- c = app.test_client()
- assert c.get('/').data == b'42'
+ assert client.get('/').data == b'42'
-def test_http_error_subclass_handling():
+def test_http_error_subclass_handling(app, client):
class ForbiddenSubclass(Forbidden):
pass
- app = flask.Flask(__name__)
-
@app.errorhandler(ForbiddenSubclass)
def handle_forbidden_subclass(e):
assert isinstance(e, ForbiddenSubclass)
@@ -860,46 +970,37 @@ def test_http_error_subclass_handling():
def index3():
raise Forbidden()
- c = app.test_client()
- assert c.get('/1').data == b'banana'
- assert c.get('/2').data == b'apple'
- assert c.get('/3').data == b'apple'
-
+ assert client.get('/1').data == b'banana'
+ assert client.get('/2').data == b'apple'
+ assert client.get('/3').data == b'apple'
-def test_trapping_of_bad_request_key_errors():
- app = flask.Flask(__name__)
- app.testing = True
+def test_trapping_of_bad_request_key_errors(app, client):
@app.route('/fail')
def fail():
flask.request.form['missing_key']
- c = app.test_client()
- assert c.get('/fail').status_code == 400
+
+ assert client.get('/fail').status_code == 400
app.config['TRAP_BAD_REQUEST_ERRORS'] = True
- c = app.test_client()
with pytest.raises(KeyError) as e:
- c.get("/fail")
+ client.get("/fail")
assert e.errisinstance(BadRequest)
-def test_trapping_of_all_http_exceptions():
- app = flask.Flask(__name__)
- app.testing = True
+def test_trapping_of_all_http_exceptions(app, client):
app.config['TRAP_HTTP_EXCEPTIONS'] = True
@app.route('/fail')
def fail():
flask.abort(404)
- c = app.test_client()
with pytest.raises(NotFound):
- c.get('/fail')
+ client.get('/fail')
-def test_enctype_debug_helper():
+def test_enctype_debug_helper(app, client):
from flask.debughelpers import DebugFilesKeyError
- app = flask.Flask(__name__)
app.debug = True
@app.route('/fail', methods=['POST'])
@@ -909,180 +1010,224 @@ def test_enctype_debug_helper():
# with statement is important because we leave an exception on the
# stack otherwise and we want to ensure that this is not the case
# to not negatively affect other tests.
- with app.test_client() as c:
+ with client:
with pytest.raises(DebugFilesKeyError) as e:
- c.post('/fail', data={'foo': 'index.txt'})
+ client.post('/fail', data={'foo': 'index.txt'})
assert 'no file contents were transmitted' in str(e.value)
assert 'This was submitted: "index.txt"' in str(e.value)
-def test_response_creation():
- app = flask.Flask(__name__)
-
- @app.route('/unicode')
- def from_unicode():
+def test_response_types(app, client):
+ @app.route('/text')
+ def from_text():
return u'Hällo Wörld'
- @app.route('/string')
- def from_string():
+ @app.route('/bytes')
+ def from_bytes():
return u'Hällo Wörld'.encode('utf-8')
- @app.route('/args')
- def from_tuple():
+ @app.route('/full_tuple')
+ def from_full_tuple():
return 'Meh', 400, {
'X-Foo': 'Testing',
'Content-Type': 'text/plain; charset=utf-8'
}
- @app.route('/two_args')
- def from_two_args_tuple():
+ @app.route('/text_headers')
+ def from_text_headers():
return 'Hello', {
'X-Foo': 'Test',
'Content-Type': 'text/plain; charset=utf-8'
}
- @app.route('/args_status')
- def from_status_tuple():
+ @app.route('/text_status')
+ def from_text_status():
return 'Hi, status!', 400
- @app.route('/args_header')
- def from_response_instance_status_tuple():
- return flask.Response('Hello world', 404), {
+ @app.route('/response_headers')
+ def from_response_headers():
+ return flask.Response('Hello world', 404, {'X-Foo': 'Baz'}), {
"X-Foo": "Bar",
"X-Bar": "Foo"
}
- c = app.test_client()
- assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8')
- assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8')
- rv = c.get('/args')
+ @app.route('/response_status')
+ def from_response_status():
+ return app.response_class('Hello world', 400), 500
+
+ @app.route('/wsgi')
+ def from_wsgi():
+ return NotFound()
+
+ assert client.get('/text').data == u'Hällo Wörld'.encode('utf-8')
+ assert client.get('/bytes').data == u'Hällo Wörld'.encode('utf-8')
+
+ rv = client.get('/full_tuple')
assert rv.data == b'Meh'
assert rv.headers['X-Foo'] == 'Testing'
assert rv.status_code == 400
assert rv.mimetype == 'text/plain'
- rv2 = c.get('/two_args')
- assert rv2.data == b'Hello'
- assert rv2.headers['X-Foo'] == 'Test'
- assert rv2.status_code == 200
- assert rv2.mimetype == 'text/plain'
- rv3 = c.get('/args_status')
- assert rv3.data == b'Hi, status!'
- assert rv3.status_code == 400
- assert rv3.mimetype == 'text/html'
- rv4 = c.get('/args_header')
- assert rv4.data == b'Hello world'
- assert rv4.headers['X-Foo'] == 'Bar'
- assert rv4.headers['X-Bar'] == 'Foo'
- assert rv4.status_code == 404
-
-
-def test_make_response():
- app = flask.Flask(__name__)
- with app.test_request_context():
- rv = flask.make_response()
- assert rv.status_code == 200
- assert rv.data == b''
- assert rv.mimetype == 'text/html'
- rv = flask.make_response('Awesome')
- assert rv.status_code == 200
- assert rv.data == b'Awesome'
- assert rv.mimetype == 'text/html'
+ rv = client.get('/text_headers')
+ assert rv.data == b'Hello'
+ assert rv.headers['X-Foo'] == 'Test'
+ assert rv.status_code == 200
+ assert rv.mimetype == 'text/plain'
- rv = flask.make_response('W00t', 404)
- assert rv.status_code == 404
- assert rv.data == b'W00t'
- assert rv.mimetype == 'text/html'
+ rv = client.get('/text_status')
+ assert rv.data == b'Hi, status!'
+ assert rv.status_code == 400
+ assert rv.mimetype == 'text/html'
+ rv = client.get('/response_headers')
+ assert rv.data == b'Hello world'
+ assert rv.headers.getlist('X-Foo') == ['Baz', 'Bar']
+ assert rv.headers['X-Bar'] == 'Foo'
+ assert rv.status_code == 404
-def test_make_response_with_response_instance():
- app = flask.Flask(__name__)
- with app.test_request_context():
- rv = flask.make_response(
- flask.jsonify({'msg': 'W00t'}), 400)
- assert rv.status_code == 400
- assert rv.data == b'{\n "msg": "W00t"\n}\n'
- assert rv.mimetype == 'application/json'
-
- rv = flask.make_response(
- flask.Response(''), 400)
- assert rv.status_code == 400
- assert rv.data == b''
- assert rv.mimetype == 'text/html'
-
- rv = flask.make_response(
- flask.Response('', headers={'Content-Type': 'text/html'}),
- 400, [('X-Foo', 'bar')])
- assert rv.status_code == 400
- assert rv.headers['Content-Type'] == 'text/html'
- assert rv.headers['X-Foo'] == 'bar'
-
-
-def test_jsonify_no_prettyprint():
+ rv = client.get('/response_status')
+ assert rv.data == b'Hello world'
+ assert rv.status_code == 500
+
+ rv = client.get('/wsgi')
+ assert b'Not Found' in rv.data
+ assert rv.status_code == 404
+
+
+def test_response_type_errors():
app = flask.Flask(__name__)
+ app.testing = True
+
+ @app.route('/none')
+ def from_none():
+ pass
+
+ @app.route('/small_tuple')
+ def from_small_tuple():
+ return 'Hello',
+
+ @app.route('/large_tuple')
+ def from_large_tuple():
+ return 'Hello', 234, {'X-Foo': 'Bar'}, '???'
+
+ @app.route('/bad_type')
+ def from_bad_type():
+ return True
+
+ @app.route('/bad_wsgi')
+ def from_bad_wsgi():
+ return lambda: None
+
+ c = app.test_client()
+
+ with pytest.raises(TypeError) as e:
+ c.get('/none')
+ assert 'returned None' in str(e)
+
+ with pytest.raises(TypeError) as e:
+ c.get('/small_tuple')
+ assert 'tuple must have the form' in str(e)
+
+ pytest.raises(TypeError, c.get, '/large_tuple')
+
+ with pytest.raises(TypeError) as e:
+ c.get('/bad_type')
+ assert 'it was a bool' in str(e)
+
+ pytest.raises(TypeError, c.get, '/bad_wsgi')
+
+
+def test_make_response(app, req_ctx):
+ rv = flask.make_response()
+ assert rv.status_code == 200
+ assert rv.data == b''
+ assert rv.mimetype == 'text/html'
+
+ rv = flask.make_response('Awesome')
+ assert rv.status_code == 200
+ assert rv.data == b'Awesome'
+ assert rv.mimetype == 'text/html'
+
+ rv = flask.make_response('W00t', 404)
+ assert rv.status_code == 404
+ assert rv.data == b'W00t'
+ assert rv.mimetype == 'text/html'
+
+
+def test_make_response_with_response_instance(app, req_ctx):
+ rv = flask.make_response(
+ flask.jsonify({'msg': 'W00t'}), 400)
+ assert rv.status_code == 400
+ assert rv.data == b'{"msg":"W00t"}\n'
+ assert rv.mimetype == 'application/json'
+
+ rv = flask.make_response(
+ flask.Response(''), 400)
+ assert rv.status_code == 400
+ assert rv.data == b''
+ assert rv.mimetype == 'text/html'
+
+ rv = flask.make_response(
+ flask.Response('', headers={'Content-Type': 'text/html'}),
+ 400, [('X-Foo', 'bar')])
+ assert rv.status_code == 400
+ assert rv.headers['Content-Type'] == 'text/html'
+ assert rv.headers['X-Foo'] == 'bar'
+
+
+def test_jsonify_no_prettyprint(app, req_ctx):
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": False})
- with app.test_request_context():
- compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n'
- uncompressed_msg = {
- "msg": {
- "submsg": "W00t"
- },
- "msg2": "foobar"
- }
+ compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n'
+ uncompressed_msg = {
+ "msg": {
+ "submsg": "W00t"
+ },
+ "msg2": "foobar"
+ }
- rv = flask.make_response(
- flask.jsonify(uncompressed_msg), 200)
- assert rv.data == compressed_msg
+ rv = flask.make_response(
+ flask.jsonify(uncompressed_msg), 200)
+ assert rv.data == compressed_msg
-def test_jsonify_prettyprint():
- app = flask.Flask(__name__)
+def test_jsonify_prettyprint(app, req_ctx):
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": True})
- with app.test_request_context():
- compressed_msg = {"msg":{"submsg":"W00t"},"msg2":"foobar"}
- pretty_response =\
- b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n'
+ compressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"}
+ pretty_response = \
+ b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n'
- rv = flask.make_response(
- flask.jsonify(compressed_msg), 200)
- assert rv.data == pretty_response
+ rv = flask.make_response(
+ flask.jsonify(compressed_msg), 200)
+ assert rv.data == pretty_response
-def test_jsonify_mimetype():
- app = flask.Flask(__name__)
+def test_jsonify_mimetype(app, req_ctx):
app.config.update({"JSONIFY_MIMETYPE": 'application/vnd.api+json'})
- with app.test_request_context():
- msg = {
- "msg": {"submsg": "W00t"},
- }
- rv = flask.make_response(
- flask.jsonify(msg), 200)
- assert rv.mimetype == 'application/vnd.api+json'
+ msg = {
+ "msg": {"submsg": "W00t"},
+ }
+ rv = flask.make_response(
+ flask.jsonify(msg), 200)
+ assert rv.mimetype == 'application/vnd.api+json'
-def test_jsonify_args_and_kwargs_check():
- app = flask.Flask(__name__)
- with app.test_request_context():
- with pytest.raises(TypeError) as e:
- flask.jsonify('fake args', kwargs='fake')
- assert 'behavior undefined' in str(e.value)
-
+def test_jsonify_args_and_kwargs_check(app, req_ctx):
+ with pytest.raises(TypeError) as e:
+ flask.jsonify('fake args', kwargs='fake')
+ assert 'behavior undefined' in str(e.value)
-def test_url_generation():
- app = flask.Flask(__name__)
+def test_url_generation(app, req_ctx):
@app.route('/hello/', methods=['POST'])
def hello():
pass
- with app.test_request_context():
- assert flask.url_for('hello', name='test x') == '/hello/test%20x'
- assert flask.url_for('hello', name='test x', _external=True) == \
- 'http://localhost/hello/test%20x'
+ assert flask.url_for('hello', name='test x') == '/hello/test%20x'
+ assert flask.url_for('hello', name='test x', _external=True) == \
+ 'http://localhost/hello/test%20x'
-def test_build_error_handler():
- app = flask.Flask(__name__)
+def test_build_error_handler(app):
# Test base case, a URL which results in a BuildError.
with app.test_request_context():
pytest.raises(BuildError, flask.url_for, 'spam')
@@ -1103,53 +1248,65 @@ def test_build_error_handler():
def handler(error, endpoint, values):
# Just a test.
return '/test_handler/'
+
app.url_build_error_handlers.append(handler)
with app.test_request_context():
assert flask.url_for('spam') == '/test_handler/'
-def test_build_error_handler_reraise():
- app = flask.Flask(__name__)
-
+def test_build_error_handler_reraise(app):
# Test a custom handler which reraises the BuildError
def handler_raises_build_error(error, endpoint, values):
raise error
+
app.url_build_error_handlers.append(handler_raises_build_error)
with app.test_request_context():
pytest.raises(BuildError, flask.url_for, 'not.existing')
-def test_custom_converters():
+def test_url_for_passes_special_values_to_build_error_handler(app):
+ @app.url_build_error_handlers.append
+ def handler(error, endpoint, values):
+ assert values == {
+ '_external': False,
+ '_anchor': None,
+ '_method': None,
+ '_scheme': None,
+ }
+ return 'handled'
+
+ with app.test_request_context():
+ flask.url_for('/')
+
+
+def test_custom_converters(app, client):
from werkzeug.routing import BaseConverter
class ListConverter(BaseConverter):
-
def to_python(self, value):
return value.split(',')
def to_url(self, value):
base_to_url = super(ListConverter, self).to_url
return ','.join(base_to_url(x) for x in value)
- app = flask.Flask(__name__)
+
app.url_map.converters['list'] = ListConverter
@app.route('/')
def index(args):
return '|'.join(args)
- c = app.test_client()
- assert c.get('/1,2,3').data == b'1|2|3'
+ assert client.get('/1,2,3').data == b'1|2|3'
-def test_static_files():
- app = flask.Flask(__name__)
- app.testing = True
- rv = app.test_client().get('/static/index.html')
+
+def test_static_files(app, client):
+ rv = client.get('/static/index.html')
assert rv.status_code == 200
assert rv.data.strip() == b'Hello World!
'
with app.test_request_context():
assert flask.url_for('static', filename='index.html') == \
- '/static/index.html'
+ '/static/index.html'
rv.close()
@@ -1177,20 +1334,23 @@ def test_static_url_path():
assert flask.url_for('static', filename='index.html') == '/foo/index.html'
-def test_none_response():
- app = flask.Flask(__name__)
- app.testing = True
-
- @app.route('/')
- def test():
- return None
- try:
- app.test_client().get('/')
- except ValueError as e:
- assert str(e) == 'View function did not return a response'
- pass
- else:
- assert "Expected ValueError"
+def test_static_route_with_host_matching():
+ app = flask.Flask(__name__, host_matching=True, static_host='example.com')
+ c = app.test_client()
+ rv = c.get('http://example.com/static/index.html')
+ assert rv.status_code == 200
+ rv.close()
+ with app.test_request_context():
+ rv = flask.url_for('static', filename='index.html', _external=True)
+ assert rv == 'http://example.com/static/index.html'
+ # Providing static_host without host_matching=True should error.
+ with pytest.raises(Exception):
+ flask.Flask(__name__, static_host='example.com')
+ # Providing host_matching=True with static_folder but without static_host should error.
+ with pytest.raises(Exception):
+ flask.Flask(__name__, host_matching=True)
+ # Providing host_matching=True without static_host but with static_folder=None should not error.
+ flask.Flask(__name__, host_matching=True, static_folder=None)
def test_request_locals():
@@ -1198,8 +1358,7 @@ def test_request_locals():
assert not flask.g
-def test_test_app_proper_environ():
- app = flask.Flask(__name__)
+def test_test_app_proper_environ(app, client):
app.config.update(
SERVER_NAME='localhost.localdomain:5000'
)
@@ -1212,22 +1371,22 @@ def test_test_app_proper_environ():
def subdomain():
return 'Foo SubDomain'
- rv = app.test_client().get('/')
+ rv = client.get('/')
assert rv.data == b'Foo'
- rv = app.test_client().get('/', 'http://localhost.localdomain:5000')
+ rv = client.get('/', 'http://localhost.localdomain:5000')
assert rv.data == b'Foo'
- rv = app.test_client().get('/', 'https://localhost.localdomain:5000')
+ rv = client.get('/', 'https://localhost.localdomain:5000')
assert rv.data == b'Foo'
app.config.update(SERVER_NAME='localhost.localdomain')
- rv = app.test_client().get('/', 'https://localhost.localdomain')
+ rv = client.get('/', 'https://localhost.localdomain')
assert rv.data == b'Foo'
try:
app.config.update(SERVER_NAME='localhost.localdomain:443')
- rv = app.test_client().get('/', 'https://localhost.localdomain')
+ rv = client.get('/', 'https://localhost.localdomain')
# Werkzeug 0.8
assert rv.status_code == 404
except ValueError as e:
@@ -1240,7 +1399,7 @@ def test_test_app_proper_environ():
try:
app.config.update(SERVER_NAME='localhost.localdomain')
- rv = app.test_client().get('/', 'http://foo.localhost')
+ rv = client.get('/', 'http://foo.localhost')
# Werkzeug 0.8
assert rv.status_code == 404
except ValueError as e:
@@ -1251,25 +1410,24 @@ def test_test_app_proper_environ():
"server name from the WSGI environment ('foo.localhost')"
)
- rv = app.test_client().get('/', 'http://foo.localhost.localdomain')
+ rv = client.get('/', 'http://foo.localhost.localdomain')
assert rv.data == b'Foo SubDomain'
-def test_exception_propagation():
+def test_exception_propagation(app, client):
def apprunner(config_key):
- app = flask.Flask(__name__)
app.config['LOGGER_HANDLER_POLICY'] = 'never'
@app.route('/')
def index():
1 // 0
- c = app.test_client()
+
if config_key is not None:
app.config[config_key] = True
with pytest.raises(Exception):
- c.get('/')
+ client.get('/')
else:
- assert c.get('/').status_code == 500
+ assert client.get('/').status_code == 500
# we have to run this test in an isolated thread because if the
# debug flag is set to true and an exception happens the context is
@@ -1286,21 +1444,19 @@ def test_exception_propagation():
@pytest.mark.parametrize('use_reloader', [True, False])
@pytest.mark.parametrize('propagate_exceptions', [None, True, False])
def test_werkzeug_passthrough_errors(monkeypatch, debug, use_debugger,
- use_reloader, propagate_exceptions):
+ use_reloader, propagate_exceptions, app):
rv = {}
# Mocks werkzeug.serving.run_simple method
def run_simple_mock(*args, **kwargs):
rv['passthrough_errors'] = kwargs.get('passthrough_errors')
- app = flask.Flask(__name__)
monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock)
app.config['PROPAGATE_EXCEPTIONS'] = propagate_exceptions
app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader)
-def test_max_content_length():
- app = flask.Flask(__name__)
+def test_max_content_length(app, client):
app.config['MAX_CONTENT_LENGTH'] = 64
@app.before_request
@@ -1317,18 +1473,16 @@ def test_max_content_length():
def catcher(error):
return '42'
- c = app.test_client()
- rv = c.post('/accept', data={'myfile': 'foo' * 100})
+ rv = client.post('/accept', data={'myfile': 'foo' * 100})
assert rv.data == b'42'
-def test_url_processors():
- app = flask.Flask(__name__)
+def test_url_processors(app, client):
@app.url_defaults
def add_language_code(endpoint, values):
if flask.g.lang_code is not None and \
- app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
+ app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
values.setdefault('lang_code', flask.g.lang_code)
@app.url_value_preprocessor
@@ -1347,15 +1501,12 @@ def test_url_processors():
def something_else():
return flask.url_for('about', lang_code='en')
- c = app.test_client()
-
- assert c.get('/de/').data == b'/de/about'
- assert c.get('/de/about').data == b'/foo'
- assert c.get('/foo').data == b'/en/about'
+ assert client.get('/de/').data == b'/de/about'
+ assert client.get('/de/about').data == b'/foo'
+ assert client.get('/foo').data == b'/en/about'
-def test_inject_blueprint_url_defaults():
- app = flask.Flask(__name__)
+def test_inject_blueprint_url_defaults(app):
bp = flask.Blueprint('foo.bar.baz', __name__,
template_folder='template')
@@ -1380,28 +1531,24 @@ def test_inject_blueprint_url_defaults():
assert url == expected
-def test_nonascii_pathinfo():
- app = flask.Flask(__name__)
- app.testing = True
-
+def test_nonascii_pathinfo(app, client):
@app.route(u'/киртест')
def index():
return 'Hello World!'
- c = app.test_client()
- rv = c.get(u'/киртест')
+ rv = client.get(u'/киртест')
assert rv.data == b'Hello World!'
-def test_debug_mode_complains_after_first_request():
- app = flask.Flask(__name__)
+def test_debug_mode_complains_after_first_request(app, client):
app.debug = True
@app.route('/')
def index():
return 'Awesome'
+
assert not app.got_first_request
- assert app.test_client().get('/').data == b'Awesome'
+ assert client.get('/').data == b'Awesome'
with pytest.raises(AssertionError) as e:
@app.route('/foo')
def broken():
@@ -1413,38 +1560,35 @@ def test_debug_mode_complains_after_first_request():
@app.route('/foo')
def working():
return 'Meh'
- assert app.test_client().get('/foo').data == b'Meh'
+
+ assert client.get('/foo').data == b'Meh'
assert app.got_first_request
-def test_before_first_request_functions():
+def test_before_first_request_functions(app, client):
got = []
- app = flask.Flask(__name__)
@app.before_first_request
def foo():
got.append(42)
- c = app.test_client()
- c.get('/')
+
+ client.get('/')
assert got == [42]
- c.get('/')
+ client.get('/')
assert got == [42]
assert app.got_first_request
-def test_before_first_request_functions_concurrent():
+def test_before_first_request_functions_concurrent(app, client):
got = []
- app = flask.Flask(__name__)
@app.before_first_request
def foo():
time.sleep(0.2)
got.append(42)
- c = app.test_client()
-
def get_and_assert():
- c.get("/")
+ client.get("/")
assert got == [42]
t = Thread(target=get_and_assert)
@@ -1454,31 +1598,30 @@ def test_before_first_request_functions_concurrent():
assert app.got_first_request
-def test_routing_redirect_debugging():
- app = flask.Flask(__name__)
+def test_routing_redirect_debugging(app, client):
app.debug = True
@app.route('/foo/', methods=['GET', 'POST'])
def foo():
return 'success'
- with app.test_client() as c:
+
+ with client:
with pytest.raises(AssertionError) as e:
- c.post('/foo', data={})
+ client.post('/foo', data={})
assert 'http://localhost/foo/' in str(e)
assert ('Make sure to directly send '
'your POST-request to this URL') in str(e)
- rv = c.get('/foo', data={}, follow_redirects=True)
+ rv = client.get('/foo', data={}, follow_redirects=True)
assert rv.data == b'success'
app.debug = False
- with app.test_client() as c:
- rv = c.post('/foo', data={}, follow_redirects=True)
+ with client:
+ rv = client.post('/foo', data={}, follow_redirects=True)
assert rv.data == b'success'
-def test_route_decorator_custom_endpoint():
- app = flask.Flask(__name__)
+def test_route_decorator_custom_endpoint(app, client):
app.debug = True
@app.route('/foo/')
@@ -1498,24 +1641,21 @@ def test_route_decorator_custom_endpoint():
assert flask.url_for('bar') == '/bar/'
assert flask.url_for('123') == '/bar/123'
- c = app.test_client()
- assert c.get('/foo/').data == b'foo'
- assert c.get('/bar/').data == b'bar'
- assert c.get('/bar/123').data == b'123'
+ assert client.get('/foo/').data == b'foo'
+ assert client.get('/bar/').data == b'bar'
+ assert client.get('/bar/123').data == b'123'
-def test_preserve_only_once():
- app = flask.Flask(__name__)
+def test_preserve_only_once(app, client):
app.debug = True
@app.route('/fail')
def fail_func():
1 // 0
- c = app.test_client()
for x in range(3):
with pytest.raises(ZeroDivisionError):
- c.get('/fail')
+ client.get('/fail')
assert flask._request_ctx_stack.top is not None
assert flask._app_ctx_stack.top is not None
@@ -1525,8 +1665,7 @@ def test_preserve_only_once():
assert flask._app_ctx_stack.top is None
-def test_preserve_remembers_exception():
- app = flask.Flask(__name__)
+def test_preserve_remembers_exception(app, client):
app.debug = True
errors = []
@@ -1542,51 +1681,40 @@ def test_preserve_remembers_exception():
def teardown_handler(exc):
errors.append(exc)
- c = app.test_client()
-
# After this failure we did not yet call the teardown handler
with pytest.raises(ZeroDivisionError):
- c.get('/fail')
+ client.get('/fail')
assert errors == []
# But this request triggers it, and it's an error
- c.get('/success')
+ client.get('/success')
assert len(errors) == 2
assert isinstance(errors[0], ZeroDivisionError)
# At this point another request does nothing.
- c.get('/success')
+ client.get('/success')
assert len(errors) == 3
assert errors[1] is None
-def test_get_method_on_g():
- app = flask.Flask(__name__)
- app.testing = True
-
- with app.app_context():
- assert flask.g.get('x') is None
- assert flask.g.get('x', 11) == 11
- flask.g.x = 42
- assert flask.g.get('x') == 42
- assert flask.g.x == 42
-
+def test_get_method_on_g(app_ctx):
+ assert flask.g.get('x') is None
+ assert flask.g.get('x', 11) == 11
+ flask.g.x = 42
+ assert flask.g.get('x') == 42
+ assert flask.g.x == 42
-def test_g_iteration_protocol():
- app = flask.Flask(__name__)
- app.testing = True
- with app.app_context():
- flask.g.foo = 23
- flask.g.bar = 42
- assert 'foo' in flask.g
- assert 'foos' not in flask.g
- assert sorted(flask.g) == ['bar', 'foo']
+def test_g_iteration_protocol(app_ctx):
+ flask.g.foo = 23
+ flask.g.bar = 42
+ assert 'foo' in flask.g
+ assert 'foos' not in flask.g
+ assert sorted(flask.g) == ['bar', 'foo']
-def test_subdomain_basic_support():
- app = flask.Flask(__name__)
- app.config['SERVER_NAME'] = 'localhost'
+def test_subdomain_basic_support(app, client):
+ app.config['SERVER_NAME'] = 'localhost.localdomain'
@app.route('/')
def normal_index():
@@ -1596,57 +1724,49 @@ def test_subdomain_basic_support():
def test_index():
return 'test index'
- c = app.test_client()
- rv = c.get('/', 'http://localhost/')
+ rv = client.get('/', 'http://localhost.localdomain/')
assert rv.data == b'normal index'
- rv = c.get('/', 'http://test.localhost/')
+ rv = client.get('/', 'http://test.localhost.localdomain/')
assert rv.data == b'test index'
-def test_subdomain_matching():
- app = flask.Flask(__name__)
- app.config['SERVER_NAME'] = 'localhost'
+def test_subdomain_matching(app, client):
+ app.config['SERVER_NAME'] = 'localhost.localdomain'
@app.route('/', subdomain='')
def index(user):
return 'index for %s' % user
- c = app.test_client()
- rv = c.get('/', 'http://mitsuhiko.localhost/')
+ rv = client.get('/', 'http://mitsuhiko.localhost.localdomain/')
assert rv.data == b'index for mitsuhiko'
-def test_subdomain_matching_with_ports():
- app = flask.Flask(__name__)
- app.config['SERVER_NAME'] = 'localhost:3000'
+def test_subdomain_matching_with_ports(app, client):
+ app.config['SERVER_NAME'] = 'localhost.localdomain:3000'
@app.route('/', subdomain='')
def index(user):
return 'index for %s' % user
- c = app.test_client()
- rv = c.get('/', 'http://mitsuhiko.localhost:3000/')
+ rv = client.get('/', 'http://mitsuhiko.localhost.localdomain:3000/')
assert rv.data == b'index for mitsuhiko'
-def test_multi_route_rules():
- app = flask.Flask(__name__)
-
+def test_multi_route_rules(app, client):
@app.route('/')
@app.route('//')
def index(test='a'):
return test
- rv = app.test_client().open('/')
+ rv = client.open('/')
assert rv.data == b'a'
- rv = app.test_client().open('/b/')
+ rv = client.open('/b/')
assert rv.data == b'b'
-def test_multi_route_class_views():
+def test_multi_route_class_views(app, client):
class View(object):
-
def __init__(self, app):
app.add_url_rule('/', 'index', self.index)
app.add_url_rule('//', 'index', self.index)
@@ -1654,35 +1774,32 @@ def test_multi_route_class_views():
def index(self, test='a'):
return test
- app = flask.Flask(__name__)
_ = View(app)
- rv = app.test_client().open('/')
+ rv = client.open('/')
assert rv.data == b'a'
- rv = app.test_client().open('/b/')
+ rv = client.open('/b/')
assert rv.data == b'b'
-def test_run_defaults(monkeypatch):
+def test_run_defaults(monkeypatch, app):
rv = {}
# Mocks werkzeug.serving.run_simple method
def run_simple_mock(*args, **kwargs):
rv['result'] = 'running...'
- app = flask.Flask(__name__)
monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock)
app.run()
assert rv['result'] == 'running...'
-def test_run_server_port(monkeypatch):
+def test_run_server_port(monkeypatch, app):
rv = {}
# Mocks werkzeug.serving.run_simple method
def run_simple_mock(hostname, port, application, *args, **kwargs):
rv['result'] = 'running on %s:%s ...' % (hostname, port)
- app = flask.Flask(__name__)
monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock)
hostname, port = 'localhost', 8000
app.run(hostname, port, debug=True)
@@ -1690,17 +1807,16 @@ def test_run_server_port(monkeypatch):
@pytest.mark.parametrize('host,port,expect_host,expect_port', (
- (None, None, 'pocoo.org', 8080),
- ('localhost', None, 'localhost', 8080),
- (None, 80, 'pocoo.org', 80),
- ('localhost', 80, 'localhost', 80),
+ (None, None, 'pocoo.org', 8080),
+ ('localhost', None, 'localhost', 8080),
+ (None, 80, 'pocoo.org', 80),
+ ('localhost', 80, 'localhost', 80),
))
-def test_run_from_config(monkeypatch, host, port, expect_host, expect_port):
+def test_run_from_config(monkeypatch, host, port, expect_host, expect_port, app):
def run_simple_mock(hostname, port, *args, **kwargs):
assert hostname == expect_host
assert port == expect_port
monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock)
- app = flask.Flask(__name__)
app.config['SERVER_NAME'] = 'pocoo.org:8080'
app.run(host, port)
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
index a3309037..d57b3034 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -18,7 +18,7 @@ from werkzeug.http import parse_cache_control_header
from jinja2 import TemplateNotFound
-def test_blueprint_specific_error_handling():
+def test_blueprint_specific_error_handling(app, client):
frontend = flask.Blueprint('frontend', __name__)
backend = flask.Blueprint('backend', __name__)
sideend = flask.Blueprint('sideend', __name__)
@@ -43,7 +43,6 @@ def test_blueprint_specific_error_handling():
def sideend_no():
flask.abort(403)
- app = flask.Flask(__name__)
app.register_blueprint(frontend)
app.register_blueprint(backend)
app.register_blueprint(sideend)
@@ -52,15 +51,15 @@ def test_blueprint_specific_error_handling():
def app_forbidden(e):
return 'application itself says no', 403
- c = app.test_client()
+ assert client.get('/frontend-no').data == b'frontend says no'
+ assert client.get('/backend-no').data == b'backend says no'
+ assert client.get('/what-is-a-sideend').data == b'application itself says no'
- assert c.get('/frontend-no').data == b'frontend says no'
- assert c.get('/backend-no').data == b'backend says no'
- assert c.get('/what-is-a-sideend').data == b'application itself says no'
-def test_blueprint_specific_user_error_handling():
+def test_blueprint_specific_user_error_handling(app, client):
class MyDecoratorException(Exception):
pass
+
class MyFunctionException(Exception):
pass
@@ -74,24 +73,48 @@ def test_blueprint_specific_user_error_handling():
def my_function_exception_handler(e):
assert isinstance(e, MyFunctionException)
return 'bam'
+
blue.register_error_handler(MyFunctionException, my_function_exception_handler)
@blue.route('/decorator')
def blue_deco_test():
raise MyDecoratorException()
+
@blue.route('/function')
def blue_func_test():
raise MyFunctionException()
- app = flask.Flask(__name__)
app.register_blueprint(blue)
- c = app.test_client()
+ assert client.get('/decorator').data == b'boom'
+ assert client.get('/function').data == b'bam'
+
+
+def test_blueprint_app_error_handling(app, client):
+ errors = flask.Blueprint('errors', __name__)
+
+ @errors.app_errorhandler(403)
+ def forbidden_handler(e):
+ return 'you shall not pass', 403
+
+ @app.route('/forbidden')
+ def app_forbidden():
+ flask.abort(403)
+
+ forbidden_bp = flask.Blueprint('forbidden_bp', __name__)
+
+ @forbidden_bp.route('/nope')
+ def bp_forbidden():
+ flask.abort(403)
- assert c.get('/decorator').data == b'boom'
- assert c.get('/function').data == b'bam'
+ app.register_blueprint(errors)
+ app.register_blueprint(forbidden_bp)
-def test_blueprint_url_definitions():
+ assert client.get('/forbidden').data == b'you shall not pass'
+ assert client.get('/nope').data == b'you shall not pass'
+
+
+def test_blueprint_url_definitions(app, client):
bp = flask.Blueprint('test', __name__)
@bp.route('/foo', defaults={'baz': 42})
@@ -102,17 +125,16 @@ def test_blueprint_url_definitions():
def bar(bar):
return text_type(bar)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23})
app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19})
- c = app.test_client()
- assert c.get('/1/foo').data == b'23/42'
- assert c.get('/2/foo').data == b'19/42'
- assert c.get('/1/bar').data == b'23'
- assert c.get('/2/bar').data == b'19'
+ assert client.get('/1/foo').data == b'23/42'
+ assert client.get('/2/foo').data == b'19/42'
+ assert client.get('/1/bar').data == b'23'
+ assert client.get('/2/bar').data == b'19'
+
-def test_blueprint_url_processors():
+def test_blueprint_url_processors(app, client):
bp = flask.Blueprint('frontend', __name__, url_prefix='/')
@bp.url_defaults
@@ -131,28 +153,26 @@ def test_blueprint_url_processors():
def about():
return flask.url_for('.index')
- app = flask.Flask(__name__)
app.register_blueprint(bp)
- c = app.test_client()
+ assert client.get('/de/').data == b'/de/about'
+ assert client.get('/de/about').data == b'/de/'
- assert c.get('/de/').data == b'/de/about'
- assert c.get('/de/about').data == b'/de/'
def test_templates_and_static(test_apps):
from blueprintapp import app
- c = app.test_client()
+ client = app.test_client()
- rv = c.get('/')
+ rv = client.get('/')
assert rv.data == b'Hello from the Frontend'
- rv = c.get('/admin/')
+ rv = client.get('/admin/')
assert rv.data == b'Hello from the Admin'
- rv = c.get('/admin/index2')
+ rv = client.get('/admin/index2')
assert rv.data == b'Hello from the Admin'
- rv = c.get('/admin/static/test.txt')
+ rv = client.get('/admin/static/test.txt')
assert rv.data.strip() == b'Admin File'
rv.close()
- rv = c.get('/admin/static/css/test.css')
+ rv = client.get('/admin/static/css/test.css')
assert rv.data.strip() == b'/* nested file */'
rv.close()
@@ -163,7 +183,7 @@ def test_templates_and_static(test_apps):
if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age:
expected_max_age = 7200
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age
- rv = c.get('/admin/static/css/test.css')
+ rv = client.get('/admin/static/css/test.css')
cc = parse_cache_control_header(rv.headers['Cache-Control'])
assert cc.max_age == expected_max_age
rv.close()
@@ -181,8 +201,8 @@ def test_templates_and_static(test_apps):
with flask.Flask(__name__).test_request_context():
assert flask.render_template('nested/nested.txt') == 'I\'m nested'
-def test_default_static_cache_timeout():
- app = flask.Flask(__name__)
+
+def test_default_static_cache_timeout(app):
class MyBlueprint(flask.Blueprint):
def get_send_file_max_age(self, filename):
return 100
@@ -205,12 +225,14 @@ def test_default_static_cache_timeout():
finally:
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default
+
def test_templates_list(test_apps):
from blueprintapp import app
templates = sorted(app.jinja_env.list_templates())
assert templates == ['admin/index.html', 'frontend/index.html']
-def test_dotted_names():
+
+def test_dotted_names(app, client):
frontend = flask.Blueprint('myapp.frontend', __name__)
backend = flask.Blueprint('myapp.backend', __name__)
@@ -226,18 +248,15 @@ def test_dotted_names():
def backend_index():
return flask.url_for('myapp.frontend.frontend_index')
- app = flask.Flask(__name__)
app.register_blueprint(frontend)
app.register_blueprint(backend)
- c = app.test_client()
- assert c.get('/fe').data.strip() == b'/be'
- assert c.get('/fe2').data.strip() == b'/fe'
- assert c.get('/be').data.strip() == b'/fe'
+ assert client.get('/fe').data.strip() == b'/be'
+ assert client.get('/fe2').data.strip() == b'/fe'
+ assert client.get('/be').data.strip() == b'/fe'
+
-def test_dotted_names_from_app():
- app = flask.Flask(__name__)
- app.testing = True
+def test_dotted_names_from_app(app, client):
test = flask.Blueprint('test', __name__)
@app.route('/')
@@ -250,11 +269,11 @@ def test_dotted_names_from_app():
app.register_blueprint(test)
- with app.test_client() as c:
- rv = c.get('/')
- assert rv.data == b'/test/'
+ rv = client.get('/')
+ assert rv.data == b'/test/'
+
-def test_empty_url_defaults():
+def test_empty_url_defaults(app, client):
bp = flask.Blueprint('bp', __name__)
@bp.route('/', defaults={'page': 1})
@@ -262,15 +281,13 @@ def test_empty_url_defaults():
def something(page):
return str(page)
- app = flask.Flask(__name__)
app.register_blueprint(bp)
- c = app.test_client()
- assert c.get('/').data == b'1'
- assert c.get('/page/2').data == b'2'
+ assert client.get('/').data == b'1'
+ assert client.get('/page/2').data == b'2'
-def test_route_decorator_custom_endpoint():
+def test_route_decorator_custom_endpoint(app, client):
bp = flask.Blueprint('bp', __name__)
@bp.route('/foo')
@@ -289,21 +306,20 @@ def test_route_decorator_custom_endpoint():
def bar_foo():
return flask.request.endpoint
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
@app.route('/')
def index():
return flask.request.endpoint
- c = app.test_client()
- assert c.get('/').data == b'index'
- assert c.get('/py/foo').data == b'bp.foo'
- assert c.get('/py/bar').data == b'bp.bar'
- assert c.get('/py/bar/123').data == b'bp.123'
- assert c.get('/py/bar/foo').data == b'bp.bar_foo'
+ assert client.get('/').data == b'index'
+ assert client.get('/py/foo').data == b'bp.foo'
+ assert client.get('/py/bar').data == b'bp.bar'
+ assert client.get('/py/bar/123').data == b'bp.123'
+ assert client.get('/py/bar/foo').data == b'bp.bar_foo'
-def test_route_decorator_custom_endpoint_with_dots():
+
+def test_route_decorator_custom_endpoint_with_dots(app, client):
bp = flask.Blueprint('bp', __name__)
@bp.route('/foo')
@@ -344,231 +360,461 @@ def test_route_decorator_custom_endpoint_with_dots():
lambda: None
)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
- c = app.test_client()
- assert c.get('/py/foo').data == b'bp.foo'
+ assert client.get('/py/foo').data == b'bp.foo'
# The rule's didn't actually made it through
- rv = c.get('/py/bar')
+ rv = client.get('/py/bar')
assert rv.status_code == 404
- rv = c.get('/py/bar/123')
+ rv = client.get('/py/bar/123')
assert rv.status_code == 404
-def test_template_filter():
+
+def test_endpoint_decorator(app, client):
+ from werkzeug.routing import Rule
+ app.url_map.add(Rule('/foo', endpoint='bar'))
+
+ bp = flask.Blueprint('bp', __name__)
+
+ @bp.endpoint('bar')
+ def foobar():
+ return flask.request.endpoint
+
+ app.register_blueprint(bp, url_prefix='/bp_prefix')
+
+ assert client.get('/foo').data == b'bar'
+ assert client.get('/bp_prefix/bar').status_code == 404
+
+
+def test_template_filter(app):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_filter()
def my_reverse(s):
return s[::-1]
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
assert 'my_reverse' in app.jinja_env.filters.keys()
assert app.jinja_env.filters['my_reverse'] == my_reverse
assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba'
-def test_add_template_filter():
+
+def test_add_template_filter(app):
bp = flask.Blueprint('bp', __name__)
+
def my_reverse(s):
return s[::-1]
+
bp.add_app_template_filter(my_reverse)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
assert 'my_reverse' in app.jinja_env.filters.keys()
assert app.jinja_env.filters['my_reverse'] == my_reverse
assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba'
-def test_template_filter_with_name():
+
+def test_template_filter_with_name(app):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_filter('strrev')
def my_reverse(s):
return s[::-1]
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
assert 'strrev' in app.jinja_env.filters.keys()
assert app.jinja_env.filters['strrev'] == my_reverse
assert app.jinja_env.filters['strrev']('abcd') == 'dcba'
-def test_add_template_filter_with_name():
+
+def test_add_template_filter_with_name(app):
bp = flask.Blueprint('bp', __name__)
+
def my_reverse(s):
return s[::-1]
+
bp.add_app_template_filter(my_reverse, 'strrev')
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
assert 'strrev' in app.jinja_env.filters.keys()
assert app.jinja_env.filters['strrev'] == my_reverse
assert app.jinja_env.filters['strrev']('abcd') == 'dcba'
-def test_template_filter_with_template():
+
+def test_template_filter_with_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_filter()
def super_reverse(s):
return s[::-1]
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_filter.html', value='abcd')
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.data == b'dcba'
-def test_template_filter_after_route_with_template():
- app = flask.Flask(__name__)
+
+def test_template_filter_after_route_with_template(app, client):
@app.route('/')
def index():
return flask.render_template('template_filter.html', value='abcd')
+
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_filter()
def super_reverse(s):
return s[::-1]
+
app.register_blueprint(bp, url_prefix='/py')
- rv = app.test_client().get('/')
+ rv = client.get('/')
assert rv.data == b'dcba'
-def test_add_template_filter_with_template():
+
+def test_add_template_filter_with_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
def super_reverse(s):
return s[::-1]
+
bp.add_app_template_filter(super_reverse)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_filter.html', value='abcd')
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.data == b'dcba'
-def test_template_filter_with_name_and_template():
+
+def test_template_filter_with_name_and_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_filter('super_reverse')
def my_reverse(s):
return s[::-1]
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_filter.html', value='abcd')
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.data == b'dcba'
-def test_add_template_filter_with_name_and_template():
+
+def test_add_template_filter_with_name_and_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
def my_reverse(s):
return s[::-1]
+
bp.add_app_template_filter(my_reverse, 'super_reverse')
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_filter.html', value='abcd')
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert rv.data == b'dcba'
-def test_template_test():
+
+def test_template_test(app):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_test()
def is_boolean(value):
return isinstance(value, bool)
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
assert 'is_boolean' in app.jinja_env.tests.keys()
assert app.jinja_env.tests['is_boolean'] == is_boolean
assert app.jinja_env.tests['is_boolean'](False)
-def test_add_template_test():
+
+def test_add_template_test(app):
bp = flask.Blueprint('bp', __name__)
+
def is_boolean(value):
return isinstance(value, bool)
+
bp.add_app_template_test(is_boolean)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
assert 'is_boolean' in app.jinja_env.tests.keys()
assert app.jinja_env.tests['is_boolean'] == is_boolean
assert app.jinja_env.tests['is_boolean'](False)
-def test_template_test_with_name():
+
+def test_template_test_with_name(app):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_test('boolean')
def is_boolean(value):
return isinstance(value, bool)
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
assert 'boolean' in app.jinja_env.tests.keys()
assert app.jinja_env.tests['boolean'] == is_boolean
assert app.jinja_env.tests['boolean'](False)
-def test_add_template_test_with_name():
+
+def test_add_template_test_with_name(app):
bp = flask.Blueprint('bp', __name__)
+
def is_boolean(value):
return isinstance(value, bool)
+
bp.add_app_template_test(is_boolean, 'boolean')
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
assert 'boolean' in app.jinja_env.tests.keys()
assert app.jinja_env.tests['boolean'] == is_boolean
assert app.jinja_env.tests['boolean'](False)
-def test_template_test_with_template():
+
+def test_template_test_with_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_test()
def boolean(value):
return isinstance(value, bool)
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_test.html', value=False)
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert b'Success!' in rv.data
-def test_template_test_after_route_with_template():
- app = flask.Flask(__name__)
+
+def test_template_test_after_route_with_template(app, client):
@app.route('/')
def index():
return flask.render_template('template_test.html', value=False)
+
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_test()
def boolean(value):
return isinstance(value, bool)
+
app.register_blueprint(bp, url_prefix='/py')
- rv = app.test_client().get('/')
+ rv = client.get('/')
assert b'Success!' in rv.data
-def test_add_template_test_with_template():
+
+def test_add_template_test_with_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
def boolean(value):
return isinstance(value, bool)
+
bp.add_app_template_test(boolean)
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_test.html', value=False)
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert b'Success!' in rv.data
-def test_template_test_with_name_and_template():
+
+def test_template_test_with_name_and_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
@bp.app_template_test('boolean')
def is_boolean(value):
return isinstance(value, bool)
- app = flask.Flask(__name__)
+
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_test.html', value=False)
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert b'Success!' in rv.data
-def test_add_template_test_with_name_and_template():
+
+def test_add_template_test_with_name_and_template(app, client):
bp = flask.Blueprint('bp', __name__)
+
def is_boolean(value):
return isinstance(value, bool)
+
bp.add_app_template_test(is_boolean, 'boolean')
- app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
+
@app.route('/')
def index():
return flask.render_template('template_test.html', value=False)
- rv = app.test_client().get('/')
+
+ rv = client.get('/')
assert b'Success!' in rv.data
+
+
+def test_context_processing(app, client):
+ answer_bp = flask.Blueprint('answer_bp', __name__)
+
+ template_string = lambda: flask.render_template_string(
+ '{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}'
+ '{% if answer %}{{ answer }} is the answer.{% endif %}'
+ )
+
+ # App global context processor
+ @answer_bp.app_context_processor
+ def not_answer_context_processor():
+ return {'notanswer': 43}
+
+ # Blueprint local context processor
+ @answer_bp.context_processor
+ def answer_context_processor():
+ return {'answer': 42}
+
+ # Setup endpoints for testing
+ @answer_bp.route('/bp')
+ def bp_page():
+ return template_string()
+
+ @app.route('/')
+ def app_page():
+ return template_string()
+
+ # Register the blueprint
+ app.register_blueprint(answer_bp)
+
+ app_page_bytes = client.get('/').data
+ answer_page_bytes = client.get('/bp').data
+
+ assert b'43' in app_page_bytes
+ assert b'42' not in app_page_bytes
+
+ assert b'42' in answer_page_bytes
+ assert b'43' in answer_page_bytes
+
+
+def test_template_global(app):
+ bp = flask.Blueprint('bp', __name__)
+
+ @bp.app_template_global()
+ def get_answer():
+ return 42
+
+ # Make sure the function is not in the jinja_env already
+ assert 'get_answer' not in app.jinja_env.globals.keys()
+ app.register_blueprint(bp)
+
+ # Tests
+ assert 'get_answer' in app.jinja_env.globals.keys()
+ assert app.jinja_env.globals['get_answer'] is get_answer
+ assert app.jinja_env.globals['get_answer']() == 42
+
+ with app.app_context():
+ rv = flask.render_template_string('{{ get_answer() }}')
+ assert rv == '42'
+
+
+def test_request_processing(app, client):
+ bp = flask.Blueprint('bp', __name__)
+ evts = []
+
+ @bp.before_request
+ def before_bp():
+ evts.append('before')
+
+ @bp.after_request
+ def after_bp(response):
+ response.data += b'|after'
+ evts.append('after')
+ return response
+
+ @bp.teardown_request
+ def teardown_bp(exc):
+ evts.append('teardown')
+
+ # Setup routes for testing
+ @bp.route('/bp')
+ def bp_endpoint():
+ return 'request'
+
+ app.register_blueprint(bp)
+
+ assert evts == []
+ rv = client.get('/bp')
+ assert rv.data == b'request|after'
+ assert evts == ['before', 'after', 'teardown']
+
+
+def test_app_request_processing(app, client):
+ bp = flask.Blueprint('bp', __name__)
+ evts = []
+
+ @bp.before_app_first_request
+ def before_first_request():
+ evts.append('first')
+
+ @bp.before_app_request
+ def before_app():
+ evts.append('before')
+
+ @bp.after_app_request
+ def after_app(response):
+ response.data += b'|after'
+ evts.append('after')
+ return response
+
+ @bp.teardown_app_request
+ def teardown_app(exc):
+ evts.append('teardown')
+
+ app.register_blueprint(bp)
+
+ # Setup routes for testing
+ @app.route('/')
+ def bp_endpoint():
+ return 'request'
+
+ # before first request
+ assert evts == []
+
+ # first request
+ resp = client.get('/').data
+ assert resp == b'request|after'
+ assert evts == ['first', 'before', 'after', 'teardown']
+
+ # second request
+ resp = client.get('/').data
+ assert resp == b'request|after'
+ assert evts == ['first'] + ['before', 'after', 'teardown'] * 2
+
+
+def test_app_url_processors(app, client):
+ bp = flask.Blueprint('bp', __name__)
+
+ # Register app-wide url defaults and preprocessor on blueprint
+ @bp.app_url_defaults
+ def add_language_code(endpoint, values):
+ values.setdefault('lang_code', flask.g.lang_code)
+
+ @bp.app_url_value_preprocessor
+ def pull_lang_code(endpoint, values):
+ flask.g.lang_code = values.pop('lang_code')
+
+ # Register route rules at the app level
+ @app.route('//')
+ def index():
+ return flask.url_for('about')
+
+ @app.route('//about')
+ def about():
+ return flask.url_for('index')
+
+ app.register_blueprint(bp)
+
+ assert client.get('/de/').data == b'/de/about'
+ assert client.get('/de/about').data == b'/de/'
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 313a34d2..459d6ef9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -14,17 +14,23 @@
from __future__ import absolute_import, print_function
import os
import sys
+from functools import partial
import click
import pytest
from click.testing import CliRunner
from flask import Flask, current_app
-from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
+from flask.cli import cli, AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \
find_default_import_path, get_version
+@pytest.fixture
+def runner():
+ return CliRunner()
+
+
def test_cli_name(test_apps):
"""Make sure the CLI object's name is the app's name and not the app itself"""
from cliapp.app import testapp
@@ -33,26 +39,90 @@ def test_cli_name(test_apps):
def test_find_best_app(test_apps):
"""Test if `find_best_app` behaves as expected with different combinations of input."""
+ script_info = ScriptInfo()
+
class Module:
app = Flask('appname')
- assert find_best_app(Module) == Module.app
+
+ assert find_best_app(script_info, Module) == Module.app
class Module:
application = Flask('appname')
- assert find_best_app(Module) == Module.application
+
+ assert find_best_app(script_info, Module) == Module.application
class Module:
myapp = Flask('appname')
- assert find_best_app(Module) == Module.myapp
+
+ assert find_best_app(script_info, Module) == Module.myapp
+
+ class Module:
+ @staticmethod
+ def create_app():
+ return Flask('appname')
+
+ assert isinstance(find_best_app(script_info, Module), Flask)
+ assert find_best_app(script_info, Module).name == 'appname'
+
+ class Module:
+ @staticmethod
+ def create_app(foo):
+ return Flask('appname')
+
+ assert isinstance(find_best_app(script_info, Module), Flask)
+ assert find_best_app(script_info, Module).name == 'appname'
+
+ class Module:
+ @staticmethod
+ def create_app(foo=None, script_info=None):
+ return Flask('appname')
+
+ assert isinstance(find_best_app(script_info, Module), Flask)
+ assert find_best_app(script_info, Module).name == 'appname'
+
+ class Module:
+ @staticmethod
+ def make_app():
+ return Flask('appname')
+
+ assert isinstance(find_best_app(script_info, Module), Flask)
+ assert find_best_app(script_info, Module).name == 'appname'
+
+ class Module:
+ myapp = Flask('appname1')
+
+ @staticmethod
+ def create_app():
+ return Flask('appname2')
+
+ assert find_best_app(script_info, Module) == Module.myapp
+
+ class Module:
+ myapp = Flask('appname1')
+
+ @staticmethod
+ def create_app():
+ return Flask('appname2')
+
+ assert find_best_app(script_info, Module) == Module.myapp
class Module:
pass
- pytest.raises(NoAppException, find_best_app, Module)
+
+ pytest.raises(NoAppException, find_best_app, script_info, Module)
class Module:
myapp1 = Flask('appname1')
myapp2 = Flask('appname2')
- pytest.raises(NoAppException, find_best_app, Module)
+
+ pytest.raises(NoAppException, find_best_app, script_info, Module)
+
+ class Module:
+ @staticmethod
+ def create_app(foo, bar):
+ return Flask('appname2')
+
+ pytest.raises(NoAppException, find_best_app, script_info, Module)
def test_prepare_exec_for_file(test_apps):
@@ -77,13 +147,18 @@ def test_prepare_exec_for_file(test_apps):
def test_locate_app(test_apps):
"""Test of locate_app."""
- assert locate_app("cliapp.app").name == "testapp"
- assert locate_app("cliapp.app:testapp").name == "testapp"
- assert locate_app("cliapp.multiapp:app1").name == "app1"
- pytest.raises(NoAppException, locate_app, "notanpp.py")
- pytest.raises(NoAppException, locate_app, "cliapp/app")
- pytest.raises(RuntimeError, locate_app, "cliapp.app:notanapp")
- pytest.raises(ImportError, locate_app, "cliapp.importerrorapp")
+ script_info = ScriptInfo()
+ 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.multiapp:app1").name == "app1"
+ pytest.raises(NoAppException, locate_app,
+ script_info, "notanpp.py")
+ pytest.raises(NoAppException, locate_app,
+ script_info, "cliapp/app")
+ pytest.raises(RuntimeError, locate_app,
+ script_info, "cliapp.app:notanapp")
+ pytest.raises(NoAppException, locate_app,
+ script_info, "cliapp.importerrorapp")
def test_find_default_import_path(test_apps, monkeypatch, tmpdir):
@@ -103,10 +178,13 @@ def test_get_version(test_apps, capsys):
"""Test of get_version."""
from flask import __version__ as flask_ver
from sys import version as py_ver
+
class MockCtx(object):
resilient_parsing = False
color = None
+
def exit(self): return
+
ctx = MockCtx()
get_version(ctx, None, "test")
out, err = capsys.readouterr()
@@ -129,8 +207,9 @@ def test_scriptinfo(test_apps):
assert obj.load_app() == app
-def test_with_appcontext():
+def test_with_appcontext(runner):
"""Test of with_appcontext."""
+
@click.command()
@with_appcontext
def testcmd():
@@ -138,14 +217,14 @@ def test_with_appcontext():
obj = ScriptInfo(create_app=lambda info: Flask("testapp"))
- runner = CliRunner()
result = runner.invoke(testcmd, obj=obj)
assert result.exit_code == 0
assert result.output == 'testapp\n'
-def test_appgroup():
+def test_appgroup(runner):
"""Test of with_appcontext."""
+
@click.group(cls=AppGroup)
def cli():
pass
@@ -164,7 +243,6 @@ def test_appgroup():
obj = ScriptInfo(create_app=lambda info: Flask("testappgroup"))
- runner = CliRunner()
result = runner.invoke(cli, ['test'], obj=obj)
assert result.exit_code == 0
assert result.output == 'testappgroup\n'
@@ -174,8 +252,9 @@ def test_appgroup():
assert result.output == 'testappgroup\n'
-def test_flaskgroup():
+def test_flaskgroup(runner):
"""Test FlaskGroup."""
+
def create_app(info):
return Flask("flaskgroup")
@@ -187,7 +266,81 @@ def test_flaskgroup():
def test():
click.echo(current_app.name)
- runner = CliRunner()
result = runner.invoke(cli, ['test'])
assert result.exit_code == 0
assert result.output == 'flaskgroup\n'
+
+
+def test_print_exceptions(runner):
+ """Print the stacktrace if the CLI."""
+
+ def create_app(info):
+ raise Exception("oh no")
+ return Flask("flaskgroup")
+
+ @click.group(cls=FlaskGroup, create_app=create_app)
+ def cli(**params):
+ pass
+
+ result = runner.invoke(cli, ['--help'])
+ assert result.exit_code == 0
+ assert 'Exception: oh no' in result.output
+ assert 'Traceback' in result.output
+
+
+class TestRoutes:
+ @pytest.fixture
+ def invoke(self, runner):
+ def create_app(info):
+ app = Flask(__name__)
+ app.testing = True
+
+ @app.route('/get_post//', methods=['GET', 'POST'])
+ def yyy_get_post(x, y):
+ pass
+
+ @app.route('/zzz_post', methods=['POST'])
+ def aaa_post():
+ pass
+
+ return app
+
+ cli = FlaskGroup(create_app=create_app)
+ return partial(runner.invoke, cli)
+
+ def expect_order(self, order, output):
+ # skip the header and match the start of each row
+ for expect, line in zip(order, output.splitlines()[2:]):
+ # do this instead of startswith for nicer pytest output
+ assert line[:len(expect)] == expect
+
+ def test_simple(self, invoke):
+ result = invoke(['routes'])
+ assert result.exit_code == 0
+ self.expect_order(
+ ['aaa_post', 'static', 'yyy_get_post'],
+ result.output
+ )
+
+ def test_sort(self, invoke):
+ default_output = invoke(['routes']).output
+ endpoint_output = invoke(['routes', '-s', 'endpoint']).output
+ assert default_output == endpoint_output
+ self.expect_order(
+ ['static', 'yyy_get_post', 'aaa_post'],
+ invoke(['routes', '-s', 'methods']).output
+ )
+ self.expect_order(
+ ['yyy_get_post', 'static', 'aaa_post'],
+ invoke(['routes', '-s', 'rule']).output
+ )
+ self.expect_order(
+ ['aaa_post', 'yyy_get_post', 'static'],
+ invoke(['routes', '-s', 'match']).output
+ )
+
+ def test_all_methods(self, invoke):
+ output = invoke(['routes']).output
+ assert 'GET, HEAD, OPTIONS, POST' not in output
+ output = invoke(['routes', '--all-methods']).output
+ assert 'GET, HEAD, OPTIONS, POST' in output
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
index 666f7d56..6ab63dd4 100644
--- a/tests/test_deprecations.py
+++ b/tests/test_deprecations.py
@@ -15,11 +15,8 @@ import flask
class TestRequestDeprecation(object):
-
- def test_request_json(self, recwarn):
+ def test_request_json(self, recwarn, app, client):
"""Request.json is deprecated"""
- app = flask.Flask(__name__)
- app.testing = True
@app.route('/', methods=['POST'])
def index():
@@ -27,20 +24,16 @@ class TestRequestDeprecation(object):
print(flask.request.json)
return 'OK'
- c = app.test_client()
- c.post('/', data='{"spam": 42}', content_type='application/json')
+ client.post('/', data='{"spam": 42}', content_type='application/json')
recwarn.pop(DeprecationWarning)
- def test_request_module(self, recwarn):
+ def test_request_module(self, recwarn, app, client):
"""Request.module is deprecated"""
- app = flask.Flask(__name__)
- app.testing = True
@app.route('/')
def index():
assert flask.request.module is None
return 'OK'
- c = app.test_client()
- c.get('/')
+ client.get('/')
recwarn.pop(DeprecationWarning)
diff --git a/tests/test_ext.py b/tests/test_ext.py
index ebb5f02d..48214905 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -21,19 +21,18 @@ from flask._compat import PY2
@pytest.fixture(autouse=True)
-def disable_extwarnings(request, recwarn):
+def disable_extwarnings(recwarn):
from flask.exthook import ExtDeprecationWarning
- def inner():
- assert set(w.category for w in recwarn.list) \
- <= set([ExtDeprecationWarning])
- recwarn.clear()
+ yield
- request.addfinalizer(inner)
+ assert set(w.category for w in recwarn.list) \
+ <= set([ExtDeprecationWarning])
+ recwarn.clear()
@pytest.fixture(autouse=True)
-def importhook_setup(monkeypatch, request):
+def importhook_setup(monkeypatch):
# we clear this out for various reasons. The most important one is
# that a real flaskext could be in there which would disable our
# fake package. Secondly we want to make sure that the flaskext
@@ -58,12 +57,11 @@ def importhook_setup(monkeypatch, request):
import_hooks += 1
assert import_hooks == 1
- def teardown():
- from flask import ext
- for key in ext.__dict__:
- assert '.' not in key
+ yield
- request.addfinalizer(teardown)
+ from flask import ext
+ for key in ext.__dict__:
+ assert '.' not in key
@pytest.fixture
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 3e2ea8cd..a67fed0b 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -22,6 +22,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from werkzeug.http import parse_cache_control_header, parse_options_header
from werkzeug.http import http_date
from flask._compat import StringIO, text_type
+from flask.helpers import get_debug_flag, make_response
def has_encoding(name):
@@ -34,244 +35,265 @@ def has_encoding(name):
class TestJSON(object):
-
- def test_ignore_cached_json(self):
- app = flask.Flask(__name__)
+ def test_ignore_cached_json(self, app):
with app.test_request_context('/', method='POST', data='malformed',
content_type='application/json'):
assert flask.request.get_json(silent=True, cache=True) is None
with pytest.raises(BadRequest):
flask.request.get_json(silent=False, cache=False)
- def test_post_empty_json_adds_exception_to_response_content_in_debug(self):
- app = flask.Flask(__name__)
+ def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client):
app.config['DEBUG'] = True
+
@app.route('/json', methods=['POST'])
def post_json():
flask.request.get_json()
return None
- c = app.test_client()
- rv = c.post('/json', data=None, content_type='application/json')
+
+ rv = client.post('/json', data=None, content_type='application/json')
assert rv.status_code == 400
assert b'Failed to decode JSON object' in rv.data
- def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self):
- app = flask.Flask(__name__)
+ def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client):
app.config['DEBUG'] = False
+
@app.route('/json', methods=['POST'])
def post_json():
flask.request.get_json()
return None
- c = app.test_client()
- rv = c.post('/json', data=None, content_type='application/json')
+
+ rv = client.post('/json', data=None, content_type='application/json')
assert rv.status_code == 400
assert b'Failed to decode JSON object' not in rv.data
- def test_json_bad_requests(self):
- app = flask.Flask(__name__)
+ def test_json_bad_requests(self, app, client):
+
@app.route('/json', methods=['POST'])
def return_json():
return flask.jsonify(foo=text_type(flask.request.get_json()))
- c = app.test_client()
- rv = c.post('/json', data='malformed', content_type='application/json')
+
+ rv = client.post('/json', data='malformed', content_type='application/json')
assert rv.status_code == 400
- def test_json_custom_mimetypes(self):
- app = flask.Flask(__name__)
+ def test_json_custom_mimetypes(self, app, client):
+
@app.route('/json', methods=['POST'])
def return_json():
return flask.request.get_json()
- c = app.test_client()
- rv = c.post('/json', data='"foo"', content_type='application/x+json')
+
+ rv = client.post('/json', data='"foo"', content_type='application/x+json')
assert rv.data == b'foo'
- def test_json_body_encoding(self):
- app = flask.Flask(__name__)
- app.testing = True
+ def test_json_body_encoding(self, app, client):
+
@app.route('/')
def index():
return flask.request.get_json()
- c = app.test_client()
- resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'),
- content_type='application/json; charset=iso-8859-15')
+ resp = client.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'),
+ content_type='application/json; charset=iso-8859-15')
assert resp.data == u'Hällo Wörld'.encode('utf-8')
- def test_json_as_unicode(self):
- app = flask.Flask(__name__)
-
- app.config['JSON_AS_ASCII'] = True
- with app.app_context():
- rv = flask.json.dumps(u'\N{SNOWMAN}')
- assert rv == '"\\u2603"'
+ @pytest.mark.parametrize('test_value,expected', [(True, '"\\u2603"'), (False, u'"\u2603"')])
+ def test_json_as_unicode(self, test_value, expected, app, app_ctx):
- app.config['JSON_AS_ASCII'] = False
- with app.app_context():
- rv = flask.json.dumps(u'\N{SNOWMAN}')
- assert rv == u'"\u2603"'
+ app.config['JSON_AS_ASCII'] = test_value
+ rv = flask.json.dumps(u'\N{SNOWMAN}')
+ assert rv == expected
- def test_json_dump_to_file(self):
- app = flask.Flask(__name__)
+ def test_json_dump_to_file(self, app, app_ctx):
test_data = {'name': 'Flask'}
out = StringIO()
- with app.app_context():
- flask.json.dump(test_data, out)
- out.seek(0)
- rv = flask.json.load(out)
- assert rv == test_data
+ flask.json.dump(test_data, out)
+ out.seek(0)
+ rv = flask.json.load(out)
+ assert rv == test_data
@pytest.mark.parametrize('test_value', [0, -1, 1, 23, 3.14, 's', "longer string", True, False, None])
- def test_jsonify_basic_types(self, test_value):
+ def test_jsonify_basic_types(self, test_value, app, client):
"""Test jsonify with basic types."""
- app = flask.Flask(__name__)
- c = app.test_client()
url = '/jsonify_basic_types'
app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x))
- rv = c.get(url)
+ rv = client.get(url)
assert rv.mimetype == 'application/json'
assert flask.json.loads(rv.data) == test_value
- def test_jsonify_dicts(self):
+ def test_jsonify_dicts(self, app, client):
"""Test jsonify with dicts and kwargs unpacking."""
- d = dict(
- a=0, b=23, c=3.14, d='t', e='Hi', f=True, g=False,
- h=['test list', 10, False],
- i={'test':'dict'}
- )
- app = flask.Flask(__name__)
+ d = {'a': 0, 'b': 23, 'c': 3.14, 'd': 't',
+ 'e': 'Hi', 'f': True, 'g': False,
+ 'h': ['test list', 10, False],
+ 'i': {'test': 'dict'}}
+
@app.route('/kw')
def return_kwargs():
return flask.jsonify(**d)
+
@app.route('/dict')
def return_dict():
return flask.jsonify(d)
- c = app.test_client()
+
for url in '/kw', '/dict':
- rv = c.get(url)
+ rv = client.get(url)
assert rv.mimetype == 'application/json'
assert flask.json.loads(rv.data) == d
- def test_jsonify_arrays(self):
+ def test_jsonify_arrays(self, app, client):
"""Test jsonify of lists and args unpacking."""
l = [
0, 42, 3.14, 't', 'hello', True, False,
['test list', 2, False],
- {'test':'dict'}
+ {'test': 'dict'}
]
- app = flask.Flask(__name__)
+
@app.route('/args_unpack')
def return_args_unpack():
return flask.jsonify(*l)
+
@app.route('/array')
def return_array():
return flask.jsonify(l)
- c = app.test_client()
+
for url in '/args_unpack', '/array':
- rv = c.get(url)
+ rv = client.get(url)
assert rv.mimetype == 'application/json'
assert flask.json.loads(rv.data) == l
- def test_jsonify_date_types(self):
+ def test_jsonify_date_types(self, app, client):
"""Test jsonify with datetime.date and datetime.datetime types."""
test_dates = (
datetime.datetime(1973, 3, 11, 6, 30, 45),
datetime.date(1975, 1, 5)
)
- app = flask.Flask(__name__)
- c = app.test_client()
for i, d in enumerate(test_dates):
url = '/datetest{0}'.format(i)
app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val))
- rv = c.get(url)
+ rv = client.get(url)
assert rv.mimetype == 'application/json'
assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple())
- def test_jsonify_uuid_types(self):
+ def test_jsonify_uuid_types(self, app, client):
"""Test jsonify with uuid.UUID types"""
test_uuid = uuid.UUID(bytes=b'\xDE\xAD\xBE\xEF' * 4)
- app = flask.Flask(__name__)
url = '/uuid_test'
app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid))
- c = app.test_client()
- rv = c.get(url)
+ rv = client.get(url)
rv_x = flask.json.loads(rv.data)['x']
assert rv_x == str(test_uuid)
rv_uuid = uuid.UUID(rv_x)
assert rv_uuid == test_uuid
- def test_json_attr(self):
- app = flask.Flask(__name__)
+ def test_json_attr(self, app, client):
+
@app.route('/add', methods=['POST'])
def add():
json = flask.request.get_json()
return text_type(json['a'] + json['b'])
- c = app.test_client()
- rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}),
- content_type='application/json')
+
+ rv = client.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}),
+ content_type='application/json')
assert rv.data == b'3'
- def test_template_escaping(self):
- app = flask.Flask(__name__)
+ def test_template_escaping(self, app, req_ctx):
render = flask.render_template_string
- with app.test_request_context():
- rv = flask.json.htmlsafe_dumps('')
- assert rv == u'"\\u003c/script\\u003e"'
- assert type(rv) == text_type
- rv = render('{{ ""|tojson }}')
- assert rv == '"\\u003c/script\\u003e"'
- rv = render('{{ "<\0/script>"|tojson }}')
- assert rv == '"\\u003c\\u0000/script\\u003e"'
- rv = render('{{ "