diff --git a/CHANGES.rst b/CHANGES.rst index 228022fd..33ad8829 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,15 +7,40 @@ Flask Changelog Version 1.1 ----------- -unreleased +Unreleased + + +Version 1.0.2 +------------- + +Unreleased 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 ----------- @@ -228,6 +253,14 @@ Released on April 26th 2018 .. _#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 -------------- diff --git a/docs/api.rst b/docs/api.rst index 982c07ba..cdb05638 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -717,7 +717,18 @@ definition for a URL that accepts an optional page:: pass 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/', methods=['GET', 'POST']) + def region(id): + pass Here are the parameters that :meth:`~flask.Flask.route` and :meth:`~flask.Flask.add_url_rule` accept. The only difference is that diff --git a/docs/cli.rst b/docs/cli.rst index b857fe9b..7ce63fd2 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -201,6 +201,30 @@ These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to 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 ------------------------------------- diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 3f6ee937..fef92c57 100644 --- a/docs/patterns/fileuploads.rst +++ b/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): filename = secure_filename(file.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)) return ''' diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 50db1dff..8f055d40 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -293,7 +293,7 @@ Python shell. See :ref:`context-locals`. :: @app.route('/user/') def profile(username): - return '{}'s profile'.format(username) + return '{}\'s profile'.format(username) with app.test_request_context(): 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. :: + from flask import request + @app.route('/login', methods=['GET', 'POST']) def login(): 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() 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. .. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt diff --git a/flask/app.py b/flask/app.py index c482484b..87c59003 100644 --- a/flask/app.py +++ b/flask/app.py @@ -27,9 +27,11 @@ 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_env, get_debug_flag, \ - get_flashed_messages, locked_cached_property, url_for +from .helpers import ( + _PackageBoundObject, + _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 .sessions import SecureCookieSessionInterface from .signals import appcontext_tearing_down, got_request_exception, \ @@ -904,7 +906,7 @@ class Flask(_PackageBoundObject): explain_ignored_app_run() return - if load_dotenv: + if get_load_dotenv(load_dotenv): cli.load_dotenv() # if set, let env vars override previous values @@ -1663,8 +1665,14 @@ class Flask(_PackageBoundObject): trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS'] - # if unset, trap based on debug mode - if (trap_bad_request is None and self.debug) or trap_bad_request: + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: return isinstance(e, BadRequest) return False @@ -1923,7 +1931,7 @@ class Flask(_PackageBoundObject): status = headers = None # unpack tuple returns - if isinstance(rv, (tuple, list)): + if isinstance(rv, tuple): len_rv = len(rv) # a 3-tuple is unpacked directly diff --git a/flask/blueprints.py b/flask/blueprints.py index 0a6ccfb7..3bdb3d9e 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -49,12 +49,10 @@ class BlueprintSetupState(object): url_prefix = self.options.get('url_prefix') if url_prefix is None: 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 #: blueprint. - if url_prefix and url_prefix[-1] == '/': - url_prefix = url_prefix[:-1] - self.url_prefix = url_prefix #: 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 blueprint's name. """ - if self.url_prefix: - rule = self.url_prefix + rule + if self.url_prefix is not None: + rule = '/'.join((self.url_prefix, rule.lstrip('/'))) options.setdefault('subdomain', self.subdomain) if endpoint is None: endpoint = _endpoint_from_view_func(view_func) diff --git a/flask/cli.py b/flask/cli.py index b3a89968..635abb13 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -28,7 +28,7 @@ from werkzeug.utils import import_string from . import __version__ from ._compat import getargspec, iteritems, reraise, text_type 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: import dotenv @@ -544,7 +544,7 @@ class FlaskGroup(AppGroup): # script that is loaded here also attempts to start a server. os.environ['FLASK_RUN_FROM_CLI'] = 'true' - if self.load_dotenv: + if get_load_dotenv(self.load_dotenv): load_dotenv() obj = kwargs.get('obj') @@ -583,12 +583,11 @@ def load_dotenv(path=None): .. versionadded:: 1.0 """ - if dotenv is None: if path or os.path.exists('.env') or os.path.exists('.flaskenv'): click.secho( ' * Tip: There are .env files present.' - ' Do "pip install python-dotenv" to use them', + ' Do "pip install python-dotenv" to use them.', fg='yellow') return diff --git a/flask/helpers.py b/flask/helpers.py index 88f2302a..df0b91fc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -68,6 +68,21 @@ def get_debug_flag(): 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): """Internal helper that returns the default endpoint for a given function. This always is the function name. diff --git a/tests/test_basic.py b/tests/test_basic.py index 9d6c58c1..c0168ae3 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1027,21 +1027,34 @@ def test_errorhandler_precedence(app, client): def test_trapping_of_bad_request_key_errors(app, client): - @app.route('/fail') + @app.route('/key') def fail(): 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 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: - client.get("/fail") - + client.get("/key") assert e.errisinstance(BadRequest) 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): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a2631241..46364cab 100644 --- a/tests/test_blueprints.py +++ b/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' -def test_blueprint_prefix_slash(app, client): - bp = flask.Blueprint('test', __name__, url_prefix='/bar/') - - @bp.route('/foo') - def foo(): +@pytest.mark.parametrize(('prefix', 'rule', 'url'), ( + ('/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(rule) + def index(): return '', 204 app.register_blueprint(bp) - app.register_blueprint(bp, url_prefix='/spam/') - assert client.get('/bar/foo').status_code == 204 - assert client.get('/spam/foo').status_code == 204 + assert client.get(url).status_code == 204 def test_blueprint_url_defaults(app, client): diff --git a/tests/test_cli.py b/tests/test_cli.py index f7755258..387eeeba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -474,6 +474,14 @@ def test_dotenv_optional(monkeypatch): 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(): # no key with pytest.raises(click.BadParameter):