forked from orbit-oss/flask
Merge branch '1.0-maintenance'
This commit is contained in:
commit
1fa9185c7e
12 changed files with 153 additions and 37 deletions
39
CHANGES.rst
39
CHANGES.rst
|
|
@ -7,15 +7,40 @@ Flask Changelog
|
||||||
Version 1.1
|
Version 1.1
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
unreleased
|
Unreleased
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.0.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
|
||||||
|
|
||||||
Version 1.0.1
|
Version 1.0.1
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
unreleased
|
Released on April 29 2018
|
||||||
|
|
||||||
|
- Fix registering partials (with no ``__name__``) as view functions.
|
||||||
|
(`#2730`_)
|
||||||
|
- Don't treat lists returned from view functions the same as tuples.
|
||||||
|
Only tuples are interpreted as response data. (`#2736`_)
|
||||||
|
- Extra slashes between a blueprint's ``url_prefix`` and a route URL
|
||||||
|
are merged. This fixes some backwards compatibility issues with the
|
||||||
|
change in 1.0. (`#2731`_, `#2742`_)
|
||||||
|
- Only trap ``BadRequestKeyError`` errors in debug mode, not all
|
||||||
|
``BadRequest`` errors. This allows ``abort(400)`` to continue
|
||||||
|
working as expected. (`#2735`_)
|
||||||
|
- The ``FLASK_SKIP_DOTENV`` environment variable can be set to ``1``
|
||||||
|
to skip automatically loading dotenv files. (`#2722`_)
|
||||||
|
|
||||||
|
.. _#2722: https://github.com/pallets/flask/issues/2722
|
||||||
|
.. _#2730: https://github.com/pallets/flask/pull/2730
|
||||||
|
.. _#2731: https://github.com/pallets/flask/issues/2731
|
||||||
|
.. _#2735: https://github.com/pallets/flask/issues/2735
|
||||||
|
.. _#2736: https://github.com/pallets/flask/issues/2736
|
||||||
|
.. _#2742: https://github.com/pallets/flask/issues/2742
|
||||||
|
|
||||||
- Fix registering partials (with no ``__name__``) as view functions
|
|
||||||
|
|
||||||
Version 1.0
|
Version 1.0
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -228,6 +253,14 @@ Released on April 26th 2018
|
||||||
.. _#2709: https://github.com/pallets/flask/pull/2709
|
.. _#2709: https://github.com/pallets/flask/pull/2709
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.12.4
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released on April 29 2018
|
||||||
|
|
||||||
|
- Repackage 0.12.3 to fix package layout issue. (`#2728`_)
|
||||||
|
|
||||||
|
|
||||||
Version 0.12.3
|
Version 0.12.3
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
||||||
13
docs/api.rst
13
docs/api.rst
|
|
@ -717,7 +717,18 @@ definition for a URL that accepts an optional page::
|
||||||
pass
|
pass
|
||||||
|
|
||||||
This specifies that ``/users/`` will be the URL for page one and
|
This specifies that ``/users/`` will be the URL for page one and
|
||||||
``/users/page/N`` will be the URL for page `N`.
|
``/users/page/N`` will be the URL for page ``N``.
|
||||||
|
|
||||||
|
If a URL contains a default value, it will be redirected to its simpler
|
||||||
|
form with a 301 redirect. In the above example, ``/users/page/1`` will
|
||||||
|
be redirected to ``/users/``. If your route handles ``GET`` and ``POST``
|
||||||
|
requests, make sure the default route only handles ``GET``, as redirects
|
||||||
|
can't preserve form data. ::
|
||||||
|
|
||||||
|
@app.route('/region/', defaults={'id': 1})
|
||||||
|
@app.route('/region/<id>', methods=['GET', 'POST'])
|
||||||
|
def region(id):
|
||||||
|
pass
|
||||||
|
|
||||||
Here are the parameters that :meth:`~flask.Flask.route` and
|
Here are the parameters that :meth:`~flask.Flask.route` and
|
||||||
:meth:`~flask.Flask.add_url_rule` accept. The only difference is that
|
:meth:`~flask.Flask.add_url_rule` accept. The only difference is that
|
||||||
|
|
|
||||||
24
docs/cli.rst
24
docs/cli.rst
|
|
@ -201,6 +201,30 @@ These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to
|
||||||
control default command options.
|
control default command options.
|
||||||
|
|
||||||
|
|
||||||
|
Disable dotenv
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``flask`` command will show a message if it detects dotenv files but
|
||||||
|
python-dotenv is not installed.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
flask run
|
||||||
|
* Tip: There are .env files present. Do "pip install python-dotenv" to use them.
|
||||||
|
|
||||||
|
You can tell Flask not to load dotenv files even when python-dotenv is
|
||||||
|
installed by setting the ``FLASK_SKIP_DOTENV`` environment variable.
|
||||||
|
This can be useful if you want to load them manually, or if you're using
|
||||||
|
a project runner that loads them already. Keep in mind that the
|
||||||
|
environment variables must be set before the app loads or it won't
|
||||||
|
configure as expected.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
export FLASK_SKIP_DOTENV=1
|
||||||
|
flask run
|
||||||
|
|
||||||
|
|
||||||
Environment Variables From virtualenv
|
Environment Variables From virtualenv
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ the file and redirects the user to the URL for the uploaded file::
|
||||||
if file and allowed_file(file.filename):
|
if file and allowed_file(file.filename):
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
return redirect(url_for('upload_file',
|
return redirect(url_for('uploaded_file',
|
||||||
filename=filename))
|
filename=filename))
|
||||||
return '''
|
return '''
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ Python shell. See :ref:`context-locals`. ::
|
||||||
|
|
||||||
@app.route('/user/<username>')
|
@app.route('/user/<username>')
|
||||||
def profile(username):
|
def profile(username):
|
||||||
return '{}'s profile'.format(username)
|
return '{}\'s profile'.format(username)
|
||||||
|
|
||||||
with app.test_request_context():
|
with app.test_request_context():
|
||||||
print(url_for('index'))
|
print(url_for('index'))
|
||||||
|
|
@ -315,6 +315,8 @@ 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.
|
of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|
@ -323,7 +325,7 @@ of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods.
|
||||||
return show_the_login_form()
|
return show_the_login_form()
|
||||||
|
|
||||||
If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method
|
If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method
|
||||||
and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise,
|
and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise,
|
||||||
``OPTIONS`` is automatically implemented for you.
|
``OPTIONS`` is automatically implemented for you.
|
||||||
|
|
||||||
.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt
|
.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt
|
||||||
|
|
|
||||||
22
flask/app.py
22
flask/app.py
|
|
@ -27,9 +27,11 @@ from ._compat import integer_types, reraise, string_types, text_type
|
||||||
from .config import Config, ConfigAttribute
|
from .config import Config, ConfigAttribute
|
||||||
from .ctx import AppContext, RequestContext, _AppCtxGlobals
|
from .ctx import AppContext, RequestContext, _AppCtxGlobals
|
||||||
from .globals import _request_ctx_stack, g, request, session
|
from .globals import _request_ctx_stack, g, request, session
|
||||||
from .helpers import _PackageBoundObject, \
|
from .helpers import (
|
||||||
_endpoint_from_view_func, find_package, get_env, get_debug_flag, \
|
_PackageBoundObject,
|
||||||
get_flashed_messages, locked_cached_property, url_for
|
_endpoint_from_view_func, find_package, get_env, get_debug_flag,
|
||||||
|
get_flashed_messages, locked_cached_property, url_for, get_load_dotenv
|
||||||
|
)
|
||||||
from .logging import create_logger
|
from .logging import create_logger
|
||||||
from .sessions import SecureCookieSessionInterface
|
from .sessions import SecureCookieSessionInterface
|
||||||
from .signals import appcontext_tearing_down, got_request_exception, \
|
from .signals import appcontext_tearing_down, got_request_exception, \
|
||||||
|
|
@ -904,7 +906,7 @@ class Flask(_PackageBoundObject):
|
||||||
explain_ignored_app_run()
|
explain_ignored_app_run()
|
||||||
return
|
return
|
||||||
|
|
||||||
if load_dotenv:
|
if get_load_dotenv(load_dotenv):
|
||||||
cli.load_dotenv()
|
cli.load_dotenv()
|
||||||
|
|
||||||
# if set, let env vars override previous values
|
# if set, let env vars override previous values
|
||||||
|
|
@ -1663,8 +1665,14 @@ class Flask(_PackageBoundObject):
|
||||||
|
|
||||||
trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS']
|
trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS']
|
||||||
|
|
||||||
# if unset, trap based on debug mode
|
# if unset, trap key errors in debug mode
|
||||||
if (trap_bad_request is None and self.debug) or trap_bad_request:
|
if (
|
||||||
|
trap_bad_request is None and self.debug
|
||||||
|
and isinstance(e, BadRequestKeyError)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if trap_bad_request:
|
||||||
return isinstance(e, BadRequest)
|
return isinstance(e, BadRequest)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
@ -1923,7 +1931,7 @@ class Flask(_PackageBoundObject):
|
||||||
status = headers = None
|
status = headers = None
|
||||||
|
|
||||||
# unpack tuple returns
|
# unpack tuple returns
|
||||||
if isinstance(rv, (tuple, list)):
|
if isinstance(rv, tuple):
|
||||||
len_rv = len(rv)
|
len_rv = len(rv)
|
||||||
|
|
||||||
# a 3-tuple is unpacked directly
|
# a 3-tuple is unpacked directly
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,10 @@ class BlueprintSetupState(object):
|
||||||
url_prefix = self.options.get('url_prefix')
|
url_prefix = self.options.get('url_prefix')
|
||||||
if url_prefix is None:
|
if url_prefix is None:
|
||||||
url_prefix = self.blueprint.url_prefix
|
url_prefix = self.blueprint.url_prefix
|
||||||
|
if url_prefix:
|
||||||
|
url_prefix = url_prefix.rstrip('/')
|
||||||
#: The prefix that should be used for all URLs defined on the
|
#: The prefix that should be used for all URLs defined on the
|
||||||
#: blueprint.
|
#: blueprint.
|
||||||
if url_prefix and url_prefix[-1] == '/':
|
|
||||||
url_prefix = url_prefix[:-1]
|
|
||||||
|
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
|
|
||||||
#: A dictionary with URL defaults that is added to each and every
|
#: A dictionary with URL defaults that is added to each and every
|
||||||
|
|
@ -67,8 +65,8 @@ class BlueprintSetupState(object):
|
||||||
to the application. The endpoint is automatically prefixed with the
|
to the application. The endpoint is automatically prefixed with the
|
||||||
blueprint's name.
|
blueprint's name.
|
||||||
"""
|
"""
|
||||||
if self.url_prefix:
|
if self.url_prefix is not None:
|
||||||
rule = self.url_prefix + rule
|
rule = '/'.join((self.url_prefix, rule.lstrip('/')))
|
||||||
options.setdefault('subdomain', self.subdomain)
|
options.setdefault('subdomain', self.subdomain)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
endpoint = _endpoint_from_view_func(view_func)
|
endpoint = _endpoint_from_view_func(view_func)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ from werkzeug.utils import import_string
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from ._compat import getargspec, iteritems, reraise, text_type
|
from ._compat import getargspec, iteritems, reraise, text_type
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .helpers import get_debug_flag, get_env
|
from .helpers import get_debug_flag, get_env, get_load_dotenv
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import dotenv
|
import dotenv
|
||||||
|
|
@ -544,7 +544,7 @@ class FlaskGroup(AppGroup):
|
||||||
# script that is loaded here also attempts to start a server.
|
# script that is loaded here also attempts to start a server.
|
||||||
os.environ['FLASK_RUN_FROM_CLI'] = 'true'
|
os.environ['FLASK_RUN_FROM_CLI'] = 'true'
|
||||||
|
|
||||||
if self.load_dotenv:
|
if get_load_dotenv(self.load_dotenv):
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
obj = kwargs.get('obj')
|
obj = kwargs.get('obj')
|
||||||
|
|
@ -583,12 +583,11 @@ def load_dotenv(path=None):
|
||||||
|
|
||||||
.. versionadded:: 1.0
|
.. versionadded:: 1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if dotenv is None:
|
if dotenv is None:
|
||||||
if path or os.path.exists('.env') or os.path.exists('.flaskenv'):
|
if path or os.path.exists('.env') or os.path.exists('.flaskenv'):
|
||||||
click.secho(
|
click.secho(
|
||||||
' * Tip: There are .env files present.'
|
' * Tip: There are .env files present.'
|
||||||
' Do "pip install python-dotenv" to use them',
|
' Do "pip install python-dotenv" to use them.',
|
||||||
fg='yellow')
|
fg='yellow')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,21 @@ def get_debug_flag():
|
||||||
return val.lower() not in ('0', 'false', 'no')
|
return val.lower() not in ('0', 'false', 'no')
|
||||||
|
|
||||||
|
|
||||||
|
def get_load_dotenv(default=True):
|
||||||
|
"""Get whether the user has disabled loading dotenv files by setting
|
||||||
|
:envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the
|
||||||
|
files.
|
||||||
|
|
||||||
|
:param default: What to return if the env var isn't set.
|
||||||
|
"""
|
||||||
|
val = os.environ.get('FLASK_SKIP_DOTENV')
|
||||||
|
|
||||||
|
if not val:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return val.lower() in ('0', 'false', 'no')
|
||||||
|
|
||||||
|
|
||||||
def _endpoint_from_view_func(view_func):
|
def _endpoint_from_view_func(view_func):
|
||||||
"""Internal helper that returns the default endpoint for a given
|
"""Internal helper that returns the default endpoint for a given
|
||||||
function. This always is the function name.
|
function. This always is the function name.
|
||||||
|
|
|
||||||
|
|
@ -1027,21 +1027,34 @@ def test_errorhandler_precedence(app, client):
|
||||||
|
|
||||||
|
|
||||||
def test_trapping_of_bad_request_key_errors(app, client):
|
def test_trapping_of_bad_request_key_errors(app, client):
|
||||||
@app.route('/fail')
|
@app.route('/key')
|
||||||
def fail():
|
def fail():
|
||||||
flask.request.form['missing_key']
|
flask.request.form['missing_key']
|
||||||
|
|
||||||
rv = client.get('/fail')
|
@app.route('/abort')
|
||||||
|
def allow_abort():
|
||||||
|
flask.abort(400)
|
||||||
|
|
||||||
|
rv = client.get('/key')
|
||||||
assert rv.status_code == 400
|
assert rv.status_code == 400
|
||||||
assert b'missing_key' not in rv.data
|
assert b'missing_key' not in rv.data
|
||||||
|
rv = client.get('/abort')
|
||||||
|
assert rv.status_code == 400
|
||||||
|
|
||||||
app.config['TRAP_BAD_REQUEST_ERRORS'] = True
|
app.debug = True
|
||||||
|
|
||||||
with pytest.raises(KeyError) as e:
|
with pytest.raises(KeyError) as e:
|
||||||
client.get("/fail")
|
client.get("/key")
|
||||||
|
|
||||||
assert e.errisinstance(BadRequest)
|
assert e.errisinstance(BadRequest)
|
||||||
assert 'missing_key' in e.value.description
|
assert 'missing_key' in e.value.description
|
||||||
|
rv = client.get('/abort')
|
||||||
|
assert rv.status_code == 400
|
||||||
|
|
||||||
|
app.debug = False
|
||||||
|
app.config['TRAP_BAD_REQUEST_ERRORS'] = True
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
client.get('/key')
|
||||||
|
with pytest.raises(BadRequest):
|
||||||
|
client.get('/abort')
|
||||||
|
|
||||||
|
|
||||||
def test_trapping_of_all_http_exceptions(app, client):
|
def test_trapping_of_all_http_exceptions(app, client):
|
||||||
|
|
|
||||||
|
|
@ -115,17 +115,22 @@ def test_blueprint_app_error_handling(app, client):
|
||||||
assert client.get('/nope').data == b'you shall not pass'
|
assert client.get('/nope').data == b'you shall not pass'
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_prefix_slash(app, client):
|
@pytest.mark.parametrize(('prefix', 'rule', 'url'), (
|
||||||
bp = flask.Blueprint('test', __name__, url_prefix='/bar/')
|
('/foo/', '/bar', '/foo/bar'),
|
||||||
|
('/foo/', 'bar', '/foo/bar'),
|
||||||
|
('/foo', '/bar', '/foo/bar'),
|
||||||
|
('/foo/', '//bar', '/foo/bar'),
|
||||||
|
('/foo//', '/bar', '/foo/bar'),
|
||||||
|
))
|
||||||
|
def test_blueprint_prefix_slash(app, client, prefix, rule, url):
|
||||||
|
bp = flask.Blueprint('test', __name__, url_prefix=prefix)
|
||||||
|
|
||||||
@bp.route('/foo')
|
@bp.route(rule)
|
||||||
def foo():
|
def index():
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
app.register_blueprint(bp, url_prefix='/spam/')
|
assert client.get(url).status_code == 204
|
||||||
assert client.get('/bar/foo').status_code == 204
|
|
||||||
assert client.get('/spam/foo').status_code == 204
|
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_url_defaults(app, client):
|
def test_blueprint_url_defaults(app, client):
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,14 @@ def test_dotenv_optional(monkeypatch):
|
||||||
assert 'FOO' not in os.environ
|
assert 'FOO' not in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
@need_dotenv
|
||||||
|
def test_disable_dotenv_from_env(monkeypatch, runner):
|
||||||
|
monkeypatch.chdir(test_path)
|
||||||
|
monkeypatch.setitem(os.environ, 'FLASK_SKIP_DOTENV', '1')
|
||||||
|
runner.invoke(FlaskGroup())
|
||||||
|
assert 'FOO' not in os.environ
|
||||||
|
|
||||||
|
|
||||||
def test_run_cert_path():
|
def test_run_cert_path():
|
||||||
# no key
|
# no key
|
||||||
with pytest.raises(click.BadParameter):
|
with pytest.raises(click.BadParameter):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue