`::
+
+ from flask import Flask, render_template
+
+ def page_not_found(e):
+ return render_template('404.html'), 404
+
+ def create_app(config_filename):
+ app = Flask(__name__)
+ app.register_error_handler(404, page_not_found)
+ return app
+
An example template might be this:
.. sourcecode:: html+jinja
- {% extends "layout.html" %}
- {% block title %}Page Not Found{% endblock %}
- {% block body %}
- Page Not Found
- What you were looking for is just not there.
-
go somewhere nice
- {% endblock %}
-
+ {% extends "layout.html" %}
+ {% block title %}Page Not Found{% endblock %}
+ {% block body %}
+
Page Not Found
+ What you were looking for is just not there.
+
go somewhere nice
+ {% endblock %}
diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst
index acdee24b..21ea767f 100644
--- a/docs/patterns/favicon.rst
+++ b/docs/patterns/favicon.rst
@@ -49,5 +49,5 @@ web server's documentation.
See also
--------
-* The `Favicon `_ article on
+* The `Favicon `_ article on
Wikipedia
diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst
index 8ab8c033..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
'''
@@ -181,4 +181,4 @@ applications dealing with uploads, there is also a Flask extension called
blacklisting of extensions and more.
.. _jQuery: https://jquery.com/
-.. _Flask-Uploads: http://pythonhosted.org/Flask-Uploads/
+.. _Flask-Uploads: https://pythonhosted.org/Flask-Uploads/
diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst
index 1cd77974..cc149839 100644
--- a/docs/patterns/packages.rst
+++ b/docs/patterns/packages.rst
@@ -17,6 +17,10 @@ this::
login.html
...
+If you find yourself stuck on something, feel free
+to take a look at the source code for this example.
+You'll find `the full src for this example here`_.
+
Simple Packages
---------------
@@ -61,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
@@ -130,6 +134,7 @@ You should then end up with something like that::
.. _working-with-modules:
+.. _the full src for this example here: https://github.com/pallets/flask/tree/master/examples/patterns/largerapp
Working with Blueprints
-----------------------
diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst
index 40e048e0..8785a6e2 100644
--- a/docs/patterns/sqlalchemy.rst
+++ b/docs/patterns/sqlalchemy.rst
@@ -22,7 +22,7 @@ if you want to get started quickly.
You can download `Flask-SQLAlchemy`_ from `PyPI
`_.
-.. _Flask-SQLAlchemy: http://pythonhosted.org/Flask-SQLAlchemy/
+.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/
Declarative
@@ -108,9 +108,9 @@ Querying is simple as well:
>>> User.query.filter(User.name == 'admin').first()
-.. _SQLAlchemy: http://www.sqlalchemy.org/
+.. _SQLAlchemy: https://www.sqlalchemy.org/
.. _declarative:
- http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/
+ https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/
Manual Object Relational Mapping
--------------------------------
@@ -135,7 +135,7 @@ Here is an example :file:`database.py` module for your application::
def init_db():
metadata.create_all(bind=engine)
-As for the declarative approach you need to close the session after
+As in the declarative approach, you need to close the session after
each request or application context shutdown. Put this into your
application module::
@@ -215,4 +215,4 @@ You can also pass strings of SQL statements to the
(1, u'admin', u'admin@localhost')
For more information about SQLAlchemy, head over to the
-`website `_.
+`website `_.
diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst
index 66a7c4c4..15f38ea7 100644
--- a/docs/patterns/sqlite3.rst
+++ b/docs/patterns/sqlite3.rst
@@ -3,8 +3,8 @@
Using SQLite 3 with Flask
=========================
-In Flask you can easily implement the opening of database connections on
-demand and closing them when the context dies (usually at the end of the
+In Flask you can easily implement the opening of database connections on
+demand and closing them when the context dies (usually at the end of the
request).
Here is a simple example of how you can use SQLite 3 with Flask::
@@ -71,7 +71,7 @@ Now in each request handling function you can access `g.db` to get the
current open database connection. To simplify working with SQLite, a
row factory function is useful. It is executed for every result returned
from the database to convert the result. For instance, in order to get
-dictionaries instead of tuples, this could be inserted into the ``get_db``
+dictionaries instead of tuples, this could be inserted into the ``get_db``
function we created above::
def make_dicts(cursor, row):
@@ -102,15 +102,15 @@ This would use Row objects rather than dicts to return the results of queries. T
Additionally, it is a good idea to provide a query function that combines
getting the cursor, executing and fetching the results::
-
+
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
-This handy little function, in combination with a row factory, makes
-working with the database much more pleasant than it is by just using the
+This handy little function, in combination with a row factory, makes
+working with the database much more pleasant than it is by just using the
raw cursor and connection objects.
Here is how you can use it::
@@ -131,7 +131,7 @@ To pass variable parts to the SQL statement, use a question mark in the
statement and pass in the arguments as a list. Never directly add them to
the SQL statement with string formatting because this makes it possible
to attack the application using `SQL Injections
-`_.
+`_.
Initial Schemas
---------------
diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst
index 2649cad6..0e53de17 100644
--- a/docs/patterns/wtforms.rst
+++ b/docs/patterns/wtforms.rst
@@ -19,7 +19,7 @@ forms.
fun. You can get it from `PyPI
`_.
-.. _Flask-WTF: http://pythonhosted.org/Flask-WTF/
+.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/
The Forms
---------
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index b444e080..7bdb67e4 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -102,9 +102,9 @@ docs to see the alternative method for running a server.
Invalid Import Name
```````````````````
-The ``FLASK_APP`` environment variable is the name of the module to import at
-:command:`flask run`. In case that module is incorrectly named you will get an
-import error upon start (or if debug is enabled when you navigate to the
+The ``FLASK_APP`` environment variable is the name of the module to import at
+:command:`flask run`. In case that module is incorrectly named you will get an
+import error upon start (or if debug is enabled when you navigate to the
application). It will tell you what it tried to import and why it failed.
The most common reason is a typo or because you did not actually create an
@@ -159,14 +159,11 @@ 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 +173,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,111 +194,111 @@ 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:
+ @app.route('/path/')
+ def show_subpath(subpath):
+ # show the subpath after /path/
+ return 'Subpath %s' % subpath
-=========== ===============================================
-`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
-=========== ===============================================
+Converter types:
-.. admonition:: Unique URLs / Redirection Behavior
+========== ==========================================
+``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
+========== ==========================================
- 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.
+Unique URLs / Redirection Behavior
+``````````````````````````````````
- Take these two rules::
+Take these two rules::
- @app.route('/projects/')
- def projects():
- return 'The project page'
+ @app.route('/projects/')
+ def projects():
+ return 'The project page'
- @app.route('/about')
- def about():
- return 'The about page'
+ @app.route('/about')
+ def about():
+ return 'The about 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.
+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.
- 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.
-
- 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():
@@ -310,64 +307,11 @@ can be changed by providing the ``methods`` argument to the
else:
show_the_login_form()
-If ``GET`` is present, ``HEAD`` will be added automatically for you. You
-don't have to deal with that. It will also make sure that ``HEAD`` requests
-are handled as the `HTTP RFC`_ (the document describing the HTTP
-protocol) demands, so you can completely ignore that part of the HTTP
-specification. Likewise, as of Flask 0.6, ``OPTIONS`` is implemented for you
-automatically as well.
-
-You have no idea what an HTTP method is? Worry not, here is a quick
-introduction to HTTP methods and why they matter:
-
-The HTTP method (also often called "the verb") tells the server what the
-client wants to *do* with the requested page. The following methods are
-very common:
-
-``GET``
- The browser tells the server to just *get* the information stored on
- that page and send it. This is probably the most common method.
-
-``HEAD``
- The browser tells the server to get the information, but it is only
- interested in the *headers*, not the content of the page. An
- application is supposed to handle that as if a ``GET`` request was
- received but to not deliver the actual content. In Flask you don't
- have to deal with that at all, the underlying Werkzeug library handles
- that for you.
-
-``POST``
- The browser tells the server that it wants to *post* some new
- information to that URL and that the server must ensure the data is
- stored and only stored once. This is how HTML forms usually
- transmit data to the server.
-
-``PUT``
- Similar to ``POST`` but the server might trigger the store procedure
- multiple times by overwriting the old values more than once. Now you
- might be asking why this is useful, but there are some good reasons
- to do it this way. Consider that the connection is lost during
- transmission: in this situation a system between the browser and the
- server might receive the request safely a second time without breaking
- things. With ``POST`` that would not be possible because it must only
- be triggered once.
-
-``DELETE``
- Remove the information at the given location.
-
-``OPTIONS``
- Provides a quick way for a client to figure out which methods are
- supported by this URL. Starting with Flask 0.6, this is implemented
- for you automatically.
-
-Now the interesting part is that in HTML4 and XHTML1, the only methods a
-form can submit to the server are ``GET`` and ``POST``. But with JavaScript
-and future HTML standards you can use the other methods as well. Furthermore
-HTTP has become quite popular lately and browsers are no longer the only
-clients that are using HTTP. For instance, many revision control systems
-use it.
-
-.. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt
+If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method
+and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise,
+``OPTIONS`` is automatically implemented for you.
+
+.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt
Static Files
------------
@@ -538,16 +482,16 @@ The Request Object
``````````````````
The request object is documented in the API section and we will not cover
-it here in detail (see :class:`~flask.request`). Here is a broad overview of
+it here in detail (see :class:`~flask.Request`). Here is a broad overview of
some of the most common operations. First of all you have to import it from
the ``flask`` module::
from flask import request
The current request method is available by using the
-:attr:`~flask.request.method` attribute. To access form data (data
+:attr:`~flask.Request.method` attribute. To access form data (data
transmitted in a ``POST`` or ``PUT`` request) you can use the
-:attr:`~flask.request.form` attribute. Here is a full example of the two
+:attr:`~flask.Request.form` attribute. Here is a full example of the two
attributes mentioned above::
@app.route('/login', methods=['POST', 'GET'])
@@ -570,7 +514,7 @@ error page is shown instead. So for many situations you don't have to
deal with that problem.
To access parameters submitted in the URL (``?key=value``) you can use the
-:attr:`~flask.request.args` attribute::
+:attr:`~flask.Request.args` attribute::
searchword = request.args.get('key', '')
@@ -579,7 +523,7 @@ We recommend accessing URL parameters with `get` or by catching the
bad request page in that case is not user friendly.
For a full list of methods and attributes of the request object, head over
-to the :class:`~flask.request` documentation.
+to the :class:`~flask.Request` documentation.
File Uploads
diff --git a/docs/security.rst b/docs/security.rst
index 587bd4ef..ad0d1244 100644
--- a/docs/security.rst
+++ b/docs/security.rst
@@ -15,7 +15,7 @@ it JavaScript) into the context of a website. To remedy this, developers
have to properly escape text so that it cannot include arbitrary HTML
tags. For more information on that have a look at the Wikipedia article
on `Cross-Site Scripting
-`_.
+`_.
Flask configures Jinja2 to automatically escape all values unless
explicitly told otherwise. This should rule out all XSS problems caused
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..bd1c8467 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -33,7 +33,7 @@ In order to test the application, we add a second module
(:file:`flaskr_tests.py`) and create a unittest skeleton there::
import os
- import flaskr
+ from flaskr import flaskr
import unittest
import tempfile
@@ -41,7 +41,7 @@ In order to test the application, we add a second module
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
- flaskr.app.config['TESTING'] = True
+ flaskr.app.testing = True
self.app = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
@@ -98,8 +98,10 @@ test method to our class, like this::
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
+ flaskr.app.testing = True
self.app = flaskr.app.test_client()
- flaskr.init_db()
+ with flaskr.app.app_context():
+ flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
@@ -208,7 +210,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/introduction.rst b/docs/tutorial/introduction.rst
index dd46628b..1abe597f 100644
--- a/docs/tutorial/introduction.rst
+++ b/docs/tutorial/introduction.rst
@@ -31,4 +31,4 @@ Here a screenshot of the final application:
Continue with :ref:`tutorial-folders`.
-.. _SQLAlchemy: http://www.sqlalchemy.org/
+.. _SQLAlchemy: https://www.sqlalchemy.org/
diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst
index 8db6531e..18f5c9b3 100644
--- a/docs/tutorial/packaging.rst
+++ b/docs/tutorial/packaging.rst
@@ -55,7 +55,7 @@ into this file, :file:`flaskr/__init__.py`:
.. sourcecode:: python
- from flaskr import app
+ from .flaskr import app
This import statement brings the application instance into the top-level
of the application package. When it is time to run the application, the
diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst
index 269e8df1..4f7977e8 100644
--- a/docs/tutorial/templates.rst
+++ b/docs/tutorial/templates.rst
@@ -59,7 +59,7 @@ show_entries.html
This template extends the :file:`layout.html` template from above to display the
messages. Note that the ``for`` loop iterates over the messages we passed
in with the :func:`~flask.render_template` function. Notice that the form is
-configured to to submit to the `add_entry` view function and use ``POST`` as
+configured to submit to the `add_entry` view function and use ``POST`` as
HTTP method:
.. sourcecode:: html+jinja
@@ -79,9 +79,9 @@ HTTP method:
{% endif %}
{% for entry in entries %}
- {{ entry.title }}
{{ entry.text|safe }}
+ {{ entry.title }}
{{ entry.text|safe }}
{% else %}
- - Unbelievable. No entries here so far
+
- Unbelievable. No entries here so far
{% endfor %}
{% endblock %}
diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst
index dcf36594..26099375 100644
--- a/docs/tutorial/testing.rst
+++ b/docs/tutorial/testing.rst
@@ -46,7 +46,7 @@ At this point you can run the tests. Here ``pytest`` will be used.
Run and watch the tests pass, within the top-level :file:`flaskr/`
directory as::
- py.test
+ pytest
Testing + setuptools
--------------------
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index 41b70f03..af2383c0 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -49,7 +49,7 @@ Any of the following is functionally equivalent::
response = send_file(open(fname), attachment_filename=fname)
response.set_etag(...)
-The reason for this is that some file-like objects have a invalid or even
+The reason for this is that some file-like objects have an invalid or even
misleading ``name`` attribute. Silently swallowing errors in such cases was not
a satisfying solution.
@@ -143,7 +143,7 @@ when there is no request context yet but an application context. The old
``flask.Flask.request_globals_class`` attribute was renamed to
:attr:`flask.Flask.app_ctx_globals_class`.
-.. _Flask-OldSessions: http://pythonhosted.org/Flask-OldSessions/
+.. _Flask-OldSessions: https://pythonhosted.org/Flask-OldSessions/
Version 0.9
-----------
@@ -198,7 +198,7 @@ applications with Flask. Because we want to make upgrading as easy as
possible we tried to counter the problems arising from these changes by
providing a script that can ease the transition.
-The script scans your whole application and generates an unified diff with
+The script scans your whole application and generates a unified diff with
changes it assumes are safe to apply. However as this is an automated
tool it won't be able to find all use cases and it might miss some. We
internally spread a lot of deprecation warnings all over the place to make
diff --git a/examples/flaskr/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py
index 37a7b5cc..d096b1e7 100644
--- a/examples/flaskr/flaskr/__init__.py
+++ b/examples/flaskr/flaskr/__init__.py
@@ -1 +1 @@
-from flaskr.flaskr import app
\ No newline at end of file
+from .flaskr import app
diff --git a/examples/flaskr/setup.cfg b/examples/flaskr/setup.cfg
index b7e47898..db50667a 100644
--- a/examples/flaskr/setup.cfg
+++ b/examples/flaskr/setup.cfg
@@ -1,2 +1,2 @@
-[aliases]
+[tool:pytest]
test=pytest
diff --git a/examples/minitwit/README b/examples/minitwit/README
index 4561d836..b9bc5ea2 100644
--- a/examples/minitwit/README
+++ b/examples/minitwit/README
@@ -14,15 +14,19 @@
export an MINITWIT_SETTINGS environment variable
pointing to a configuration file.
- 2. tell flask about the right application:
+ 2. install the app from the root of the project directory
+
+ pip install --editable .
+
+ 3. tell flask about the right application:
export FLASK_APP=minitwit
- 2. fire up a shell and run this:
+ 4. fire up a shell and run this:
flask initdb
- 3. now you can run minitwit:
+ 5. now you can run minitwit:
flask run
diff --git a/examples/minitwit/minitwit/__init__.py b/examples/minitwit/minitwit/__init__.py
index 0b8bd697..96c81aec 100644
--- a/examples/minitwit/minitwit/__init__.py
+++ b/examples/minitwit/minitwit/__init__.py
@@ -1 +1 @@
-from minitwit import app
\ No newline at end of file
+from .minitwit import app
diff --git a/examples/minitwit/minitwit/minitwit.py b/examples/minitwit/minitwit/minitwit.py
index bbc3b483..69840267 100644
--- a/examples/minitwit/minitwit/minitwit.py
+++ b/examples/minitwit/minitwit/minitwit.py
@@ -85,7 +85,7 @@ def format_datetime(timestamp):
def gravatar_url(email, size=80):
"""Return the gravatar image for the given email address."""
- return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
+ return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
diff --git a/examples/patterns/largerapp/setup.py b/examples/patterns/largerapp/setup.py
new file mode 100644
index 00000000..eaf00f07
--- /dev/null
+++ b/examples/patterns/largerapp/setup.py
@@ -0,0 +1,10 @@
+from setuptools import setup
+
+setup(
+ name='yourapplication',
+ packages=['yourapplication'],
+ include_package_data=True,
+ install_requires=[
+ 'flask',
+ ],
+)
diff --git a/examples/patterns/largerapp/tests/test_largerapp.py b/examples/patterns/largerapp/tests/test_largerapp.py
new file mode 100644
index 00000000..6bc0531e
--- /dev/null
+++ b/examples/patterns/largerapp/tests/test_largerapp.py
@@ -0,0 +1,12 @@
+from yourapplication import app
+import pytest
+
+@pytest.fixture
+def client():
+ app.config['TESTING'] = True
+ client = app.test_client()
+ return client
+
+def test_index(client):
+ rv = client.get('/')
+ assert b"Hello World!" in rv.data
\ No newline at end of file
diff --git a/examples/patterns/largerapp/yourapplication/__init__.py b/examples/patterns/largerapp/yourapplication/__init__.py
new file mode 100644
index 00000000..089d2937
--- /dev/null
+++ b/examples/patterns/largerapp/yourapplication/__init__.py
@@ -0,0 +1,4 @@
+from flask import Flask
+app = Flask(__name__)
+
+import yourapplication.views
\ No newline at end of file
diff --git a/examples/patterns/largerapp/yourapplication/static/style.css b/examples/patterns/largerapp/yourapplication/static/style.css
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/patterns/largerapp/yourapplication/templates/index.html b/examples/patterns/largerapp/yourapplication/templates/index.html
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/patterns/largerapp/yourapplication/templates/layout.html b/examples/patterns/largerapp/yourapplication/templates/layout.html
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/patterns/largerapp/yourapplication/templates/login.html b/examples/patterns/largerapp/yourapplication/templates/login.html
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/patterns/largerapp/yourapplication/views.py b/examples/patterns/largerapp/yourapplication/views.py
new file mode 100644
index 00000000..b112328e
--- /dev/null
+++ b/examples/patterns/largerapp/yourapplication/views.py
@@ -0,0 +1,5 @@
+from yourapplication import app
+
+@app.route('/')
+def index():
+ return 'Hello World!'
\ No newline at end of file
diff --git a/flask/__init__.py b/flask/__init__.py
index 509b944f..bb6c4c18 100644
--- a/flask/__init__.py
+++ b/flask/__init__.py
@@ -10,7 +10,7 @@
:license: BSD, see LICENSE for more details.
"""
-__version__ = '0.11.2-dev'
+__version__ = '0.13-dev'
# utilities we import from Werkzeug and Jinja2 that are unused
# in the module but are exported as public interface.
@@ -40,7 +40,7 @@ from .signals import signals_available, template_rendered, request_started, \
# it.
from . import json
-# This was the only thing that flask used to export at one point and it had
+# This was the only thing that Flask used to export at one point and it had
# a more generic name.
jsonify = json.jsonify
diff --git a/flask/app.py b/flask/app.py
index 59c77a15..703cfef2 100644
--- a/flask/app.py
+++ b/flask/app.py
@@ -10,31 +10,30 @@
"""
import os
import sys
-from threading import Lock
from datetime import timedelta
-from itertools import chain
from functools import update_wrapper
-from collections import deque
-
-from werkzeug.datastructures import ImmutableDict
-from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
-from werkzeug.exceptions import HTTPException, InternalServerError, \
- MethodNotAllowed, BadRequest, default_exceptions
+from itertools import chain
+from threading import Lock
-from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
- locked_cached_property, _endpoint_from_view_func, find_package, \
- get_debug_flag
-from . import json, cli
-from .wrappers import Request, Response
-from .config import ConfigAttribute, Config
-from .ctx import RequestContext, AppContext, _AppCtxGlobals
-from .globals import _request_ctx_stack, request, session, g
+from werkzeug.datastructures import 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()
@@ -124,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
@@ -131,6 +133,13 @@ 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.
+ folder in the root path of the application. Defaults
+ to None.
+ :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
@@ -315,7 +324,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,
})
@@ -338,7 +347,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,
@@ -392,7 +402,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}
@@ -405,17 +415,16 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.9
self.url_build_error_handlers = []
- #: A dictionary with lists of functions that should be called at the
- #: beginning of the request. The key of the dictionary is the name of
- #: the blueprint this function is active for, ``None`` for all requests.
- #: This can for example be used to open database connections or
- #: getting hold of the currently logged in user. To register a
- #: function here, use the :meth:`before_request` decorator.
+ #: A dictionary with lists of functions that will be called at the
+ #: beginning of each request. The key of the dictionary is the name of
+ #: the blueprint this function is active for, or ``None`` for all
+ #: requests. To register a function, use the :meth:`before_request`
+ #: decorator.
self.before_request_funcs = {}
- #: A lists of functions that should be called at the beginning of the
- #: first request to this instance. To register a function here, use
- #: the :meth:`before_first_request` decorator.
+ #: A list of functions that will be called at the beginning of the
+ #: first request to this instance. To register a function, use the
+ #: :meth:`before_first_request` decorator.
#:
#: .. versionadded:: 0.8
self.before_first_request_funcs = []
@@ -447,12 +456,11 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.9
self.teardown_appcontext_funcs = []
- #: A dictionary with lists of functions that can be used as URL
- #: value processor functions. Whenever a URL is built these functions
- #: are called to modify the dictionary of values in place. The key
- #: ``None`` here is used for application wide
- #: callbacks, otherwise the key is the name of the blueprint.
- #: Each of these functions has the chance to modify the dictionary
+ #: A dictionary with lists of functions that are called before the
+ #: :attr:`before_request_funcs` functions. The key of the dictionary is
+ #: the name of the blueprint this function is active for, or ``None``
+ #: for all requests. To register a function, use
+ #: :meth:`url_value_preprocessor`.
#:
#: .. versionadded:: 0.7
self.url_value_preprocessors = {}
@@ -526,19 +534,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
@@ -814,7 +825,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.
@@ -825,15 +837,22 @@ class Flask(_PackageBoundObject):
:func:`werkzeug.serving.run_simple` for more
information.
"""
+ # Change this into a no-op if the server is invoked from the
+ # command line. Have a look at cli.py for more information.
+ if os.environ.get('FLASK_RUN_FROM_CLI_SERVER') == '1':
+ from .debughelpers import explain_ignored_app_run
+ explain_ignored_app_run()
+ return
+
from werkzeug.serving import run_simple
- if host is None:
- host = '127.0.0.1'
- if port is None:
- server_name = self.config['SERVER_NAME']
- if server_name and ':' in server_name:
- port = int(server_name.rsplit(':', 1)[1])
- else:
- port = 5000
+ _host = '127.0.0.1'
+ _port = 5000
+ server_name = self.config.get("SERVER_NAME")
+ sn_host, sn_port = None, None
+ if server_name:
+ sn_host, _, sn_port = server_name.partition(':')
+ host = host or sn_host or _host
+ port = int(port or sn_port or _port)
if debug is not None:
self.debug = bool(debug)
options.setdefault('use_reloader', self.debug)
@@ -959,7 +978,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.
@@ -999,6 +1018,10 @@ class Flask(_PackageBoundObject):
endpoint
:param view_func: the function to call when serving a request to the
provided endpoint
+ :param provide_automatic_options: controls whether the ``OPTIONS``
+ method should be added automatically. This can also be controlled
+ by setting the ``view_func.provide_automatic_options = False``
+ before adding the rule.
:param options: the options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods
@@ -1028,8 +1051,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:
@@ -1153,7 +1177,8 @@ class Flask(_PackageBoundObject):
that do not necessarily have to be a subclass of the
:class:`~werkzeug.exceptions.HTTPException` class.
- :param code: the code as integer for the handler
+ :param code_or_exception: the code as integer for the handler, or
+ an arbitrary exception
"""
def decorator(f):
self._register_error_handler(None, code_or_exception, f)
@@ -1287,11 +1312,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
@@ -1347,7 +1374,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.
@@ -1410,9 +1437,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
@@ -1436,24 +1471,13 @@ class Flask(_PackageBoundObject):
def find_handler(handler_map):
if not handler_map:
return
- queue = deque(exc_class.__mro__)
- # Protect from geniuses who might create circular references in
- # __mro__
- done = set()
-
- while queue:
- cls = queue.popleft()
- if cls in done:
- continue
- done.add(cls)
+ for cls in exc_class.__mro__:
handler = handler_map.get(cls)
if handler is not None:
# cache for next time exc_class is raised
handler_map[exc_class] = handler
return handler
- queue.extend(cls.__mro__)
-
# try blueprint handlers
handler = find_handler(self.error_handler_spec
.get(request.blueprint, {})
@@ -1699,62 +1723,106 @@ class Flask(_PackageBoundObject):
return False
def make_response(self, rv):
- """Converts the return value from a view function to a real
- response object that is an instance of :attr:`response_class`.
-
- The following types are allowed for `rv`:
-
- .. tabularcolumns:: |p{3.5cm}|p{9.5cm}|
-
- ======================= ===========================================
- :attr:`response_class` the object is returned unchanged
- :class:`str` a response object is created with the
- string as body
- :class:`unicode` a response object is created with the
- string encoded to utf-8 as body
- a WSGI function the function is called as WSGI application
- and buffered as response object
- :class:`tuple` A tuple in the form ``(response, status,
- headers)`` or ``(response, headers)``
- where `response` is any of the
- types defined here, `status` is a string
- or an integer and `headers` is a list or
- a dictionary with header values.
- ======================= ===========================================
-
- :param rv: the return value from the view function
+ """Convert the return value from a view function to an instance of
+ :attr:`response_class`.
+
+ :param rv: the return value from the view function. The view function
+ must return a response. Returning ``None``, or the view ending
+ without returning, is not allowed. The following types are allowed
+ for ``view_rv``:
+
+ ``str`` (``unicode`` in Python 2)
+ A response object is created with the string encoded to UTF-8
+ as the body.
+
+ ``bytes`` (``str`` in Python 2)
+ A response object is created with the bytes as the body.
+
+ ``tuple``
+ Either ``(body, status, headers)``, ``(body, status)``, or
+ ``(body, headers)``, where ``body`` is any of the other types
+ allowed here, ``status`` is a string or an integer, and
+ ``headers`` is a dictionary or a list of ``(key, value)``
+ tuples. If ``body`` is a :attr:`response_class` instance,
+ ``status`` overwrites the exiting value and ``headers`` are
+ extended.
+
+ :attr:`response_class`
+ The object is returned unchanged.
+
+ other :class:`~werkzeug.wrappers.Response` class
+ The object is coerced to :attr:`response_class`.
+
+ :func:`callable`
+ The function is called as a WSGI application. The result is
+ used to create a response object.
.. versionchanged:: 0.9
Previously a tuple was interpreted as the arguments for the
response object.
"""
- status_or_headers = headers = None
- if isinstance(rv, tuple):
- rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
- if rv is None:
- raise ValueError('View function did not return a response')
+ status = headers = None
+
+ # unpack tuple returns
+ if isinstance(rv, (tuple, list)):
+ len_rv = len(rv)
+
+ # 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).'
+ )
- if isinstance(status_or_headers, (dict, list)):
- headers, status_or_headers = status_or_headers, None
+ # 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)
@@ -1817,16 +1885,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, ())
@@ -1986,14 +2054,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 6c8cf32d..3d361be8 100644
--- a/flask/cli.py
+++ b/flask/cli.py
@@ -11,14 +11,18 @@
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__
+
class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded."""
@@ -89,10 +93,18 @@ def locate_app(app_id):
try:
__import__(module)
except ImportError:
- raise NoAppException('The file/path provided (%s) does not appear to '
- 'exist. Please verify the path is correct. If '
- 'app is not on PYTHONPATH, ensure the extension '
- 'is .py' % module)
+ # 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:
+ 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 '
+ 'correct. If app is not on PYTHONPATH, '
+ 'ensure the extension is .py' % module)
+
mod = sys.modules[module]
if app_obj is None:
app = find_best_app(mod)
@@ -131,9 +143,9 @@ version_option = click.Option(['--version'],
is_flag=True, is_eager=True)
class DispatchingApp(object):
- """Special application that dispatches to a flask application which
+ """Special application that dispatches to a Flask application which
is imported by name in a background thread. If an error happens
- it is is recorded and shows as part of the WSGI handling which in case
+ it is recorded and shown as part of the WSGI handling which in case
of the Werkzeug debugger means that it shows up in the browser.
"""
@@ -310,6 +322,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
@@ -362,7 +375,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):
@@ -406,6 +421,13 @@ def run_command(info, host, port, reload, debugger, eager_loading,
"""
from werkzeug.serving import run_simple
+ # Set a global flag that indicates that we were invoked from the
+ # command line interface provided server command. This is detected
+ # by Flask.run to make the call into a no-op. This is necessary to
+ # avoid ugly errors when the script that is loaded here also attempts
+ # to start a server.
+ os.environ['FLASK_RUN_FROM_CLI_SERVER'] = '1'
+
debug = get_debug_flag()
if reload is None:
reload = bool(debug)
@@ -466,6 +488,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/config.py b/flask/config.py
index 36e8a123..697add71 100644
--- a/flask/config.py
+++ b/flask/config.py
@@ -126,7 +126,7 @@ class Config(dict):
d = types.ModuleType('config')
d.__file__ = filename
try:
- with open(filename) as config_file:
+ with open(filename, mode='rb') as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
diff --git a/flask/debughelpers.py b/flask/debughelpers.py
index 90710dd3..9e44fe69 100644
--- a/flask/debughelpers.py
+++ b/flask/debughelpers.py
@@ -8,6 +8,9 @@
:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
+import os
+from warnings import warn
+
from ._compat import implements_to_string, text_type
from .app import Flask
from .blueprints import Blueprint
@@ -153,3 +156,12 @@ def explain_template_loading_attempts(app, template, attempts):
info.append(' See http://flask.pocoo.org/docs/blueprints/#templates')
app.logger.info('\n'.join(info))
+
+
+def explain_ignored_app_run():
+ if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
+ warn(Warning('Silently ignoring app.run() because the '
+ 'application is run from the flask command line '
+ 'executable. Consider putting app.run() behind an '
+ 'if __name__ == "__main__" guard to silence this '
+ 'warning.'), stacklevel=3)
diff --git a/flask/helpers.py b/flask/helpers.py
index c6c2cddc..51c3b064 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
@@ -58,7 +60,7 @@ def get_debug_flag(default=None):
val = os.environ.get('FLASK_DEBUG')
if not val:
return default
- return val not in ('0', 'false', 'no')
+ return val.lower() not in ('0', 'false', 'no')
def _endpoint_from_view_func(view_func):
@@ -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:
@@ -958,3 +977,24 @@ 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
diff --git a/flask/json.py b/flask/json.py
index 16e0c295..a029e73a 100644
--- a/flask/json.py
+++ b/flask/json.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
- flask.jsonimpl
- ~~~~~~~~~~~~~~
+ flask.json
+ ~~~~~~~~~~
Implementation helpers for the JSON support in Flask.
@@ -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 b9120712..9fef6a9d 100644
--- a/flask/sessions.py
+++ b/flask/sessions.py
@@ -11,13 +11,14 @@
import uuid
import hashlib
+import warnings
from base64 import b64encode, b64decode
from datetime import datetime
from werkzeug.http import http_date, parse_date
from werkzeug.datastructures import CallbackDict
from . import Markup, json
from ._compat import iteritems, text_type
-from .helpers import total_seconds
+from .helpers import total_seconds, is_ip
from itsdangerous import URLSafeTimedSerializer, BadSignature
@@ -84,21 +85,25 @@ class TaggedJSONSerializer(object):
def dumps(self, value):
return json.dumps(_tag(value), separators=(',', ':'))
+ LOADS_MAP = {
+ ' t': tuple,
+ ' u': uuid.UUID,
+ ' b': b64decode,
+ ' m': Markup,
+ ' d': parse_date,
+ }
+
def loads(self, value):
def object_hook(obj):
if len(obj) != 1:
return obj
the_key, the_value = next(iteritems(obj))
- if the_key == ' t':
- return tuple(the_value)
- elif the_key == ' u':
- return uuid.UUID(the_value)
- elif the_key == ' b':
- return b64decode(the_value)
- elif the_key == ' m':
- return Markup(the_value)
- elif the_key == ' d':
- return parse_date(the_value)
+ # Check the key for a corresponding function
+ return_function = self.LOADS_MAP.get(the_key)
+ if return_function:
+ # Pass the value to the function
+ return return_function(the_value)
+ # Didn't find a function for this object
return obj
return json.loads(value, object_hook=object_hook)
@@ -168,7 +173,7 @@ class SessionInterface(object):
null_session_class = NullSession
#: A flag that indicates if the session interface is pickle based.
- #: This can be used by flask extensions to make a decision in regards
+ #: This can be used by Flask extensions to make a decision in regards
#: to how to deal with the session object.
#:
#: .. versionadded:: 0.10
@@ -196,30 +201,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
diff --git a/flask/signals.py b/flask/signals.py
index c9b8a210..dd52cdb5 100644
--- a/flask/signals.py
+++ b/flask/signals.py
@@ -37,7 +37,7 @@ except ImportError:
temporarily_connected_to = connected_to = _fail
del _fail
-# The namespace for code signals. If you are not flask code, do
+# The namespace for code signals. If you are not Flask code, do
# not put signals in here. Create your own namespace instead.
_signals = Namespace()
diff --git a/flask/testing.py b/flask/testing.py
index 8eacf58b..31600245 100644
--- a/flask/testing.py
+++ b/flask/testing.py
@@ -10,6 +10,7 @@
:license: BSD, see LICENSE for more details.
"""
+import werkzeug
from contextlib import contextmanager
from werkzeug.test import Client, EnvironBuilder
from flask import _request_ctx_stack
@@ -43,11 +44,23 @@ class FlaskClient(Client):
information about how to use this class refer to
:class:`werkzeug.test.Client`.
+ .. versionchanged:: 0.12
+ `app.test_client()` includes preset default environment, which can be
+ set after instantiation of the `app.test_client()` object in
+ `client.environ_base`.
+
Basic usage is outlined in the :ref:`testing` chapter.
"""
preserve_context = False
+ def __init__(self, *args, **kwargs):
+ super(FlaskClient, self).__init__(*args, **kwargs)
+ self.environ_base = {
+ "REMOTE_ADDR": "127.0.0.1",
+ "HTTP_USER_AGENT": "werkzeug/" + werkzeug.__version__
+ }
+
@contextmanager
def session_transaction(self, *args, **kwargs):
"""When used in combination with a ``with`` statement this opens a
@@ -101,6 +114,7 @@ class FlaskClient(Client):
def open(self, *args, **kwargs):
kwargs.setdefault('environ_overrides', {}) \
['flask._preserve_context'] = self.preserve_context
+ kwargs.setdefault('environ_base', self.environ_base)
as_tuple = kwargs.pop('as_tuple', False)
buffered = kwargs.pop('buffered', False)
diff --git a/flask/views.py b/flask/views.py
index 83394c1f..848ccb0b 100644
--- a/flask/views.py
+++ b/flask/views.py
@@ -103,33 +103,34 @@ class View(object):
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 +140,14 @@ class MethodView(with_metaclass(MethodViewType, View)):
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
"""
+
def dispatch_request(self, *args, **kwargs):
meth = getattr(self, request.method.lower(), None)
+
# If the request method is HEAD and we don't have a handler for it
# retry with GET.
if meth is None and request.method == 'HEAD':
meth = getattr(self, 'get', None)
+
assert meth is not None, 'Unimplemented method %r' % request.method
return meth(*args, **kwargs)
diff --git a/flask/wrappers.py b/flask/wrappers.py
index d1d7ba7d..04bdcb5d 100644
--- a/flask/wrappers.py
+++ b/flask/wrappers.py
@@ -137,7 +137,8 @@ class Request(RequestBase):
on the request.
"""
rv = getattr(self, '_cached_json', _missing)
- if rv is not _missing:
+ # We return cached JSON only when the cache is enabled.
+ if cache and rv is not _missing:
return rv
if not (force or self.is_json):
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 34414b3e..781de592 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,11 @@
[aliases]
release = egg_info -RDb ''
-[wheel]
+[bdist_wheel]
universal = 1
+[metadata]
+license_file = LICENSE
+
[tool:pytest]
-norecursedirs = .* *.egg *.egg-info env* artwork docs examples
+norecursedirs = .* *.egg *.egg-info env* artwork docs
diff --git a/setup.py b/setup.py
index 983f7611..08995073 100644
--- a/setup.py
+++ b/setup.py
@@ -41,7 +41,7 @@ Links
* `website `_
* `documentation `_
* `development version
- `_
+ `_
"""
import re
@@ -59,7 +59,7 @@ with open('flask/__init__.py', 'rb') as f:
setup(
name='Flask',
version=version,
- url='http://github.com/pallets/flask/',
+ url='https://github.com/pallets/flask/',
license='BSD',
author='Armin Ronacher',
author_email='armin.ronacher@active-4.com',
diff --git a/test-requirements.txt b/test-requirements.txt
index e079f8a6..053148f8 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1 +1 @@
-pytest
+tox
diff --git a/tests/test_apps/cliapp/importerrorapp.py b/tests/test_apps/cliapp/importerrorapp.py
new file mode 100644
index 00000000..fb87c9b1
--- /dev/null
+++ b/tests/test_apps/cliapp/importerrorapp.py
@@ -0,0 +1,7 @@
+from __future__ import absolute_import, print_function
+
+from flask import Flask
+
+raise ImportError()
+
+testapp = Flask('testapp')
diff --git a/tests/test_basic.py b/tests/test_basic.py
index be3d5edd..80091bd6 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -50,7 +50,7 @@ def test_options_on_multiple_rules():
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():
@@ -70,6 +70,54 @@ def test_options_handling_disabled():
assert sorted(rv.allow) == ['OPTIONS']
+def test_provide_automatic_options_kwarg():
+ app = flask.Flask(__name__)
+
+ 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
+ )
+
+ c = app.test_client()
+ assert c.get('/').data == b'GET'
+
+ rv = c.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(c, 'options'):
+ rv = c.options('/')
+ else:
+ rv = c.open('/', method='OPTIONS')
+
+ assert rv.status_code == 405
+
+ rv = c.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 rv.status_code == 405
+ assert sorted(rv.allow) == ['GET', 'HEAD', 'POST']
+
+ if hasattr(c, 'options'):
+ rv = c.options('/more')
+ else:
+ rv = c.open('/more', method='OPTIONS')
+
+ assert rv.status_code == 405
+
+
def test_request_dispatching():
app = flask.Flask(__name__)
@@ -303,6 +351,42 @@ def test_session_using_session_settings():
assert 'httponly' not in cookie
+def test_session_localhost_warning(recwarn):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SECRET_KEY='testing',
+ SERVER_NAME='localhost:5000',
+ )
+
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'testing'
+
+ rv = app.test_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 = flask.Flask(__name__)
+ app.config.update(
+ SECRET_KEY='testing',
+ SERVER_NAME='127.0.0.1:5000',
+ )
+
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'testing'
+
+ rv = app.test_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__)
@@ -333,7 +417,7 @@ def test_session_expiration():
client = app.test_client()
rv = client.get('/')
assert 'set-cookie' in rv.headers
- match = re.search(r'\bexpires=([^;]+)(?i)', rv.headers['set-cookie'])
+ match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie'])
expires = parse_date(match.group())
expected = datetime.utcnow() + app.permanent_session_lifetime
assert expires.year == expected.year
@@ -791,6 +875,23 @@ def test_error_handling_processing():
assert resp.data == b'internal server error'
+def test_baseexception_error_handling():
+ app = flask.Flask(__name__)
+ app.config['LOGGER_HANDLER_POLICY'] = 'never'
+
+ @app.route('/')
+ def broken_func():
+ raise KeyboardInterrupt()
+
+ with app.test_client() as c:
+ with pytest.raises(KeyboardInterrupt):
+ c.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 = flask.Flask(__name__)
@@ -910,64 +1011,129 @@ def test_enctype_debug_helper():
assert 'This was submitted: "index.txt"' in str(e.value)
-def test_response_creation():
+def test_response_types():
app = flask.Flask(__name__)
+ app.testing = True
- @app.route('/unicode')
- def from_unicode():
+ @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"
}
+ @app.route('/response_status')
+ def from_response_status():
+ return app.response_class('Hello world', 400), 500
+
+ @app.route('/wsgi')
+ def from_wsgi():
+ return NotFound()
+
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')
+
+ assert c.get('/text').data == u'Hällo Wörld'.encode('utf-8')
+ assert c.get('/bytes').data == u'Hällo Wörld'.encode('utf-8')
+
+ rv = c.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
+
+ rv = c.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 = c.get('/text_status')
+ assert rv.data == b'Hi, status!'
+ assert rv.status_code == 400
+ assert rv.mimetype == 'text/html'
+
+ rv = c.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
+
+ rv = c.get('/response_status')
+ assert rv.data == b'Hello world'
+ assert rv.status_code == 500
+
+ rv = c.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():
@@ -995,7 +1161,7 @@ def test_make_response_with_response_instance():
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.data == b'{"msg":"W00t"}\n'
assert rv.mimetype == 'application/json'
rv = flask.make_response(
@@ -1114,6 +1280,23 @@ def test_build_error_handler_reraise():
pytest.raises(BuildError, flask.url_for, 'not.existing')
+def test_url_for_passes_special_values_to_build_error_handler():
+ app = flask.Flask(__name__)
+
+ @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():
from werkzeug.routing import BaseConverter
@@ -1171,20 +1354,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():
@@ -1681,3 +1867,20 @@ def test_run_server_port(monkeypatch):
hostname, port = 'localhost', 8000
app.run(hostname, port, debug=True)
assert rv['result'] == 'running on %s:%s ...' % (hostname, port)
+
+
+@pytest.mark.parametrize('host,port,expect_host,expect_port', (
+ (None, None, 'pocoo.org', 8080),
+ ('localhost', None, 'localhost', 8080),
+ (None, 80, 'pocoo.org', 80),
+ ('localhost', 80, 'localhost', 80),
+))
+def test_run_from_config(monkeypatch, host, port, expect_host, expect_port):
+ 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..de293e7f 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -355,6 +355,25 @@ def test_route_decorator_custom_endpoint_with_dots():
rv = c.get('/py/bar/123')
assert rv.status_code == 404
+
+def test_endpoint_decorator():
+ from werkzeug.routing import Rule
+ app = flask.Flask(__name__)
+ 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')
+
+ c = app.test_client()
+ assert c.get('/foo').data == b'bar'
+ assert c.get('/bp_prefix/bar').status_code == 404
+
+
def test_template_filter():
bp = flask.Blueprint('bp', __name__)
@bp.app_template_filter()
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 18026a75..ab875cef 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
@@ -83,6 +89,7 @@ def test_locate_app(test_apps):
pytest.raises(NoAppException, locate_app, "notanpp.py")
pytest.raises(NoAppException, locate_app, "cliapp/app")
pytest.raises(RuntimeError, locate_app, "cliapp.app:notanapp")
+ pytest.raises(NoAppException, locate_app, "cliapp.importerrorapp")
def test_find_default_import_path(test_apps, monkeypatch, tmpdir):
@@ -128,7 +135,7 @@ 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
@@ -137,13 +144,12 @@ 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():
@@ -163,7 +169,6 @@ def test_appgroup():
obj = ScriptInfo(create_app=lambda info: Flask("testappgroup"))
- runner = CliRunner()
result = runner.invoke(cli, ['test'], obj=obj)
assert result.exit_code == 0
assert result.output == 'testappgroup\n'
@@ -173,7 +178,7 @@ def test_appgroup():
assert result.output == 'testappgroup\n'
-def test_flaskgroup():
+def test_flaskgroup(runner):
"""Test FlaskGroup."""
def create_app(info):
return Flask("flaskgroup")
@@ -186,7 +191,80 @@ 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_config.py b/tests/test_config.py
index 333a5cff..5c98db98 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -7,11 +7,14 @@
:license: BSD, see LICENSE for more details.
"""
-import pytest
-import os
from datetime import timedelta
+import os
+import textwrap
+
import flask
+from flask._compat import PY2
+import pytest
# config keys used for the TestConfig
@@ -187,3 +190,18 @@ def test_get_namespace():
assert 2 == len(bar_options)
assert 'bar stuff 1' == bar_options['BAR_STUFF_1']
assert 'bar stuff 2' == bar_options['BAR_STUFF_2']
+
+
+@pytest.mark.parametrize('encoding', ['utf-8', 'iso-8859-15', 'latin-1'])
+def test_from_pyfile_weird_encoding(tmpdir, encoding):
+ f = tmpdir.join('my_config.py')
+ f.write_binary(textwrap.dedent(u'''
+ # -*- coding: {0} -*-
+ TEST_VALUE = "föö"
+ '''.format(encoding)).encode(encoding))
+ app = flask.Flask(__name__)
+ app.config.from_pyfile(str(f))
+ value = app.config['TEST_VALUE']
+ if PY2:
+ value = value.decode(encoding)
+ assert value == u'föö'
diff --git a/tests/test_ext.py b/tests/test_ext.py
index d336e404..ebb5f02d 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -179,8 +179,8 @@ def test_flaskext_broken_package_no_module_caching(flaskext_broken):
def test_no_error_swallowing(flaskext_broken):
with pytest.raises(ImportError) as excinfo:
import flask.ext.broken
-
- assert excinfo.type is ImportError
+ # python3.6 raises a subclass of ImportError: 'ModuleNotFoundError'
+ assert issubclass(excinfo.type, ImportError)
if PY2:
message = 'No module named missing_module'
else:
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 8348331b..325713c0 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -35,6 +35,14 @@ def has_encoding(name):
class TestJSON(object):
+ def test_ignore_cached_json(self):
+ app = flask.Flask(__name__)
+ 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__)
app.config['DEBUG'] = True
@@ -113,20 +121,17 @@ class TestJSON(object):
rv = flask.json.load(out)
assert rv == test_data
- def test_jsonify_basic_types(self):
+ @pytest.mark.parametrize('test_value', [0, -1, 1, 23, 3.14, 's', "longer string", True, False, None])
+ def test_jsonify_basic_types(self, test_value):
"""Test jsonify with basic types."""
- # Should be able to use pytest parametrize on this, but I couldn't
- # figure out the correct syntax
- # https://pytest.org/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions
- test_data = (0, 1, 23, 3.14, 's', "longer string", True, False,)
app = flask.Flask(__name__)
c = app.test_client()
- for i, d in enumerate(test_data):
- url = '/jsonify_basic_types{0}'.format(i)
- app.add_url_rule(url, str(i), lambda x=d: flask.jsonify(x))
- rv = c.get(url)
- assert rv.mimetype == 'application/json'
- assert flask.json.loads(rv.data) == d
+
+ url = '/jsonify_basic_types'
+ app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x))
+ rv = c.get(url)
+ assert rv.mimetype == 'application/json'
+ assert flask.json.loads(rv.data) == test_value
def test_jsonify_dicts(self):
"""Test jsonify with dicts and kwargs unpacking."""
@@ -170,12 +175,10 @@ class TestJSON(object):
def test_jsonify_date_types(self):
"""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()
@@ -189,8 +192,7 @@ class TestJSON(object):
def test_jsonify_uuid_types(self):
"""Test jsonify with uuid.UUID types"""
- test_uuid = uuid.UUID(bytes=b'\xDE\xAD\xBE\xEF'*4)
-
+ 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))
@@ -265,6 +267,47 @@ class TestJSON(object):
}), content_type='application/json')
assert rv.data == b'"<42>"'
+ def test_blueprint_json_customization(self):
+ class X(object):
+ def __init__(self, val):
+ self.val = val
+
+ class MyEncoder(flask.json.JSONEncoder):
+ def default(self, o):
+ if isinstance(o, X):
+ return '<%d>' % o.val
+
+ return flask.json.JSONEncoder.default(self, o)
+
+ class MyDecoder(flask.json.JSONDecoder):
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('object_hook', self.object_hook)
+ flask.json.JSONDecoder.__init__(self, *args, **kwargs)
+
+ def object_hook(self, obj):
+ if len(obj) == 1 and '_foo' in obj:
+ return X(obj['_foo'])
+
+ return obj
+
+ bp = flask.Blueprint('bp', __name__)
+ bp.json_encoder = MyEncoder
+ bp.json_decoder = MyDecoder
+
+ @bp.route('/bp', methods=['POST'])
+ def index():
+ return flask.json.dumps(flask.request.get_json()['x'])
+
+ app = flask.Flask(__name__)
+ app.testing = True
+ app.register_blueprint(bp)
+
+ c = app.test_client()
+ rv = c.post('/bp', data=flask.json.dumps({
+ 'x': {'_foo': 42}
+ }), content_type='application/json')
+ assert rv.data == b'"<42>"'
+
def test_modified_url_encoding(self):
class ModifiedRequest(flask.Request):
url_charset = 'euc-kr'
@@ -287,6 +330,8 @@ class TestJSON(object):
def test_json_key_sorting(self):
app = flask.Flask(__name__)
app.testing = True
+ app.debug = True
+
assert app.config['JSON_SORT_KEYS'] == True
d = dict.fromkeys(range(20), 'foo')
@@ -513,7 +558,7 @@ class TestSendfile(object):
assert rv.status_code == 416
rv.close()
- last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(
+ last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime(
os.path.join(app.root_path, 'static/index.html'))).replace(
microsecond=0)
@@ -536,10 +581,11 @@ class TestSendfile(object):
value, options = \
parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
+ assert options['filename'] == 'index.html'
+ assert 'filename*' not in rv.headers['Content-Disposition']
rv.close()
with app.test_request_context():
- assert options['filename'] == 'index.html'
rv = flask.send_file('static/index.html', as_attachment=True)
value, options = parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
@@ -556,6 +602,19 @@ class TestSendfile(object):
assert options['filename'] == 'index.txt'
rv.close()
+ def test_attachment_with_utf8_filename(self):
+ app = flask.Flask(__name__)
+
+ with app.test_request_context():
+ rv = flask.send_file('static/index.html', as_attachment=True, attachment_filename=u'Ñandú/pingüino.txt')
+ content_disposition = set(rv.headers['Content-Disposition'].split('; '))
+ assert content_disposition == set((
+ 'attachment',
+ 'filename="Nandu/pinguino.txt"',
+ "filename*=UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt"
+ ))
+ rv.close()
+
def test_static_file(self):
app = flask.Flask(__name__)
# default cache timeout is 12 hours
diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py
index 4b2b1f87..48823fb2 100644
--- a/tests/test_reqctx.py
+++ b/tests/test_reqctx.py
@@ -12,6 +12,7 @@
import pytest
import flask
+from flask.sessions import SessionInterface
try:
from greenlet import greenlet
@@ -193,3 +194,27 @@ def test_greenlet_context_copying_api():
result = greenlets[0].run()
assert result == 42
+
+
+def test_session_error_pops_context():
+ class SessionError(Exception):
+ pass
+
+ class FailingSessionInterface(SessionInterface):
+ def open_session(self, app, request):
+ raise SessionError()
+
+ class CustomFlask(flask.Flask):
+ session_interface = FailingSessionInterface()
+
+ app = CustomFlask(__name__)
+
+ @app.route('/')
+ def index():
+ # shouldn't get here
+ assert False
+
+ response = app.test_client().get('/')
+ assert response.status_code == 500
+ assert not flask.request
+ assert not flask.current_app
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 7bb99e79..9d353904 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -11,6 +11,7 @@
import pytest
import flask
+import werkzeug
from flask._compat import text_type
@@ -43,6 +44,40 @@ def test_environ_defaults():
rv = c.get('/')
assert rv.data == b'http://localhost/'
+def test_environ_base_default():
+ app = flask.Flask(__name__)
+ app.testing = True
+ @app.route('/')
+ def index():
+ flask.g.user_agent = flask.request.headers["User-Agent"]
+ return flask.request.remote_addr
+
+ with app.test_client() as c:
+ rv = c.get('/')
+ assert rv.data == b'127.0.0.1'
+ assert flask.g.user_agent == 'werkzeug/' + werkzeug.__version__
+
+def test_environ_base_modified():
+ app = flask.Flask(__name__)
+ app.testing = True
+ @app.route('/')
+ def index():
+ flask.g.user_agent = flask.request.headers["User-Agent"]
+ return flask.request.remote_addr
+
+ with app.test_client() as c:
+ c.environ_base['REMOTE_ADDR'] = '0.0.0.0'
+ c.environ_base['HTTP_USER_AGENT'] = 'Foo'
+ rv = c.get('/')
+ assert rv.data == b'0.0.0.0'
+ assert flask.g.user_agent == 'Foo'
+
+ c.environ_base['REMOTE_ADDR'] = '0.0.0.1'
+ c.environ_base['HTTP_USER_AGENT'] = 'Bar'
+ rv = c.get('/')
+ assert rv.data == b'0.0.0.1'
+ assert flask.g.user_agent == 'Bar'
+
def test_redirect_keep_session():
app = flask.Flask(__name__)
app.secret_key = 'testing'
diff --git a/tests/test_views.py b/tests/test_views.py
index 8a66bd53..65981dbd 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -160,3 +160,45 @@ def test_endpoint_override():
# But these tests should still pass. We just log a warning.
common_test(app)
+
+def test_multiple_inheritance():
+ app = flask.Flask(__name__)
+
+ class GetView(flask.views.MethodView):
+ def get(self):
+ return 'GET'
+
+ class DeleteView(flask.views.MethodView):
+ def delete(self):
+ return 'DELETE'
+
+ class GetDeleteView(GetView, DeleteView):
+ pass
+
+ app.add_url_rule('/', view_func=GetDeleteView.as_view('index'))
+
+ c = app.test_client()
+ assert c.get('/').data == b'GET'
+ assert c.delete('/').data == b'DELETE'
+ assert sorted(GetDeleteView.methods) == ['DELETE', 'GET']
+
+def test_remove_method_from_parent():
+ app = flask.Flask(__name__)
+
+ class GetView(flask.views.MethodView):
+ def get(self):
+ return 'GET'
+
+ class OtherView(flask.views.MethodView):
+ def post(self):
+ return 'POST'
+
+ class View(GetView, OtherView):
+ methods = ['GET']
+
+ app.add_url_rule('/', view_func=View.as_view('index'))
+
+ c = app.test_client()
+ assert c.get('/').data == b'GET'
+ assert c.post('/').status_code == 405
+ assert sorted(View.methods) == ['GET']
diff --git a/tox.ini b/tox.ini
index 28942d3e..764b4030 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,20 @@
[tox]
-envlist = {py26,py27,pypy}-{lowest,release,devel}{,-simplejson}, {py33,py34,py35}-{release,devel}{,-simplejson}
+envlist = {py26,py27,pypy}-{lowest,release,devel}{,-simplejson}, {py33,py34,py35,py36}-{release,devel}{,-simplejson}
[testenv]
+passenv = LANG
+usedevelop=true
commands =
- py.test []
-
+ # We need to install those after Flask is installed.
+ pip install -e examples/flaskr
+ pip install -e examples/minitwit
+ pip install -e examples/patterns/largerapp
+ pytest --cov=flask --cov-report html []
deps=
pytest
+ pytest-cov
greenlet
lowest: Werkzeug==0.7