Browse Source

Merge branch '1.0-maintenance'

pull/2747/head
David Lord 7 years ago
parent
commit
1fa9185c7e
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 39
      CHANGES.rst
  2. 13
      docs/api.rst
  3. 24
      docs/cli.rst
  4. 2
      docs/patterns/fileuploads.rst
  5. 6
      docs/quickstart.rst
  6. 22
      flask/app.py
  7. 10
      flask/blueprints.py
  8. 7
      flask/cli.py
  9. 15
      flask/helpers.py
  10. 25
      tests/test_basic.py
  11. 21
      tests/test_blueprints.py
  12. 8
      tests/test_cli.py

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

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

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

2
docs/patterns/fileuploads.rst

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

6
docs/quickstart.rst

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

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

10
flask/blueprints.py

@ -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)

7
flask/cli.py

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

15
flask/helpers.py

@ -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.

25
tests/test_basic.py

@ -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):

21
tests/test_blueprints.py

@ -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'),
@bp.route('/foo') ('/foo', '/bar', '/foo/bar'),
def foo(): ('/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(rule)
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):

8
tests/test_cli.py

@ -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…
Cancel
Save