diff --git a/.appveyor.yml b/.appveyor.yml index 3a7d2f63..11bb6d4b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,16 +1,16 @@ environment: global: - TOXENV: py + TOXENV: py,codecov matrix: - - PYTHON: C:\Python36 - - PYTHON: C:\Python27 + - PYTHON: C:\Python36-x64 + - PYTHON: C:\Python27-x64 init: - SET PATH=%PYTHON%;%PATH% install: - - python -m pip install -U pip setuptools wheel tox + - python -m pip install -U tox build: false @@ -21,3 +21,6 @@ branches: only: - master - /^.*-maintenance$/ + +cache: + - '%LOCALAPPDATA%\pip\Cache' diff --git a/.travis.yml b/.travis.yml index 0ff95ecd..b3ba1e19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,27 +14,30 @@ matrix: env: TOXENV=py,codecov - python: 2.7 env: TOXENV=py,simplejson,devel,lowest,codecov - - python: pypy + - python: pypy3 env: TOXENV=py,codecov - python: nightly env: TOXENV=py - os: osx language: generic - env: TOXENV=py + env: TOXENV=py3,py2,codecov + cache: + pip: false + directories: + - $HOME/Library/Caches/Homebrew + - $HOME/Library/Caches/pip allow_failures: + - python: pypy3 - python: nightly - env: TOXENV=py - os: osx - language: generic - env: TOXENV=py fast_finish: true before_install: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - brew update; - brew install python3 redis memcached; - virtualenv -p python3 ~/py-env; - . ~/py-env/bin/activate; + - | + if [[ $TRAVIS_OS_NAME == 'osx' ]]; then + brew upgrade python + brew install python@2; + export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: diff --git a/CHANGES.rst b/CHANGES.rst index 44fecf55..1902cc85 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,15 @@ Version 1.0.3 Unreleased +- :func:`send_file` encodes filenames as ASCII instead of Latin-1 + (ISO-8859-1). This fixes compatibility with Gunicorn, which is + stricter about header encodings than PEP 3333. (`#2766`_) +- Allow custom CLIs using ``FlaskGroup`` to set the debug flag without + it always being overwritten based on environment variables. (`#2765`_) + +.. _#2766: https://github.com/pallets/flask/issues/2766 +.. _#2765: https://github.com/pallets/flask/pull/2765 + Version 1.0.2 ------------- diff --git a/docs/installation.rst b/docs/installation.rst index 15b2f8f7..0014f136 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -132,6 +132,9 @@ Within the activated environment, use the following command to install Flask: pip install Flask +Flask is now installed. Check out the :doc:`/quickstart` or go to the +:doc:`Documentation Overview `. + Living on the edge ~~~~~~~~~~~~~~~~~~ @@ -177,7 +180,7 @@ On Windows, as an administrator: \Python27\python.exe Downloads\get-pip.py \Python27\python.exe -m pip install virtualenv -Now you can continue to :ref:`install-create-env`. +Now you can return above and :ref:`install-create-env`. .. _virtualenv: https://virtualenv.pypa.io/ .. _get-pip.py: https://bootstrap.pypa.io/get-pip.py diff --git a/flask/cli.py b/flask/cli.py index fa006c57..b3066477 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -340,7 +340,8 @@ class ScriptInfo(object): onwards as click object. """ - def __init__(self, app_import_path=None, create_app=None): + def __init__(self, app_import_path=None, create_app=None, + set_debug_flag=True): #: Optionally the import path for the Flask application. self.app_import_path = app_import_path or os.environ.get('FLASK_APP') #: Optionally a function that is passed the script info to create @@ -349,6 +350,7 @@ class ScriptInfo(object): #: A dictionary with arbitrary data that can be associated with #: this script info. self.data = {} + self.set_debug_flag = set_debug_flag self._loaded_app = None def load_app(self): @@ -386,12 +388,10 @@ class ScriptInfo(object): '"app.py" module was not found in the current directory.' ) - debug = get_debug_flag() - - # Update the app's debug flag through the descriptor so that other - # values repopulate as well. - if debug is not None: - app.debug = debug + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() self._loaded_app = app return app @@ -459,6 +459,8 @@ class FlaskGroup(AppGroup): :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` files to set environment variables. Will also change the working directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag based on the active + environment .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables @@ -466,7 +468,8 @@ class FlaskGroup(AppGroup): """ def __init__(self, add_default_commands=True, create_app=None, - add_version_option=True, load_dotenv=True, **extra): + add_version_option=True, load_dotenv=True, + set_debug_flag=True, **extra): params = list(extra.pop('params', None) or ()) if add_version_option: @@ -475,6 +478,7 @@ class FlaskGroup(AppGroup): AppGroup.__init__(self, params=params, **extra) self.create_app = create_app self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag if add_default_commands: self.add_command(run_command) @@ -550,7 +554,8 @@ class FlaskGroup(AppGroup): obj = kwargs.get('obj') if obj is None: - obj = ScriptInfo(create_app=self.create_app) + obj = ScriptInfo(create_app=self.create_app, + set_debug_flag=self.set_debug_flag) kwargs['obj'] = obj kwargs.setdefault('auto_envvar_prefix', 'FLASK') @@ -670,7 +675,7 @@ class CertParamType(click.ParamType): obj = import_string(value, silent=True) - if sys.version_info < (2, 7): + if sys.version_info < (2, 7, 9): if obj: return obj else: @@ -687,7 +692,7 @@ def _validate_key(ctx, param, value): cert = ctx.params.get('cert') is_adhoc = cert == 'adhoc' - if sys.version_info < (2, 7): + if sys.version_info < (2, 7, 9): is_context = cert and not isinstance(cert, (text_type, bytes)) else: is_context = isinstance(cert, ssl.SSLContext) diff --git a/flask/helpers.py b/flask/helpers.py index df0b91fc..7679a496 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -506,6 +506,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4 + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + :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. @@ -564,11 +568,11 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, 'sending as attachment') try: - attachment_filename = attachment_filename.encode('latin-1') + attachment_filename = attachment_filename.encode('ascii') except UnicodeEncodeError: filenames = { 'filename': unicodedata.normalize( - 'NFKD', attachment_filename).encode('latin-1', 'ignore'), + 'NFKD', attachment_filename).encode('ascii', 'ignore'), 'filename*': "UTF-8''%s" % url_quote(attachment_filename), } else: diff --git a/tests/test_cli.py b/tests/test_cli.py index f7ebc6bd..83151f6f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -356,6 +356,28 @@ def test_flaskgroup(runner): assert result.output == 'flaskgroup\n' +@pytest.mark.parametrize('set_debug_flag', (True, False)) +def test_flaskgroup_debug(runner, set_debug_flag): + """Test FlaskGroup debug flag behavior.""" + + def create_app(info): + app = Flask("flaskgroup") + app.debug = True + return app + + @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag) + def cli(**params): + pass + + @cli.command() + def test(): + click.echo(str(current_app.debug)) + + result = runner.invoke(cli, ['test']) + assert result.exit_code == 0 + assert result.output == '%s\n' % str(not set_debug_flag) + + def test_print_exceptions(runner): """Print the stacktrace if the CLI.""" @@ -537,12 +559,12 @@ def test_run_cert_import(monkeypatch): run_command.make_context('run', ['--cert', 'not_here']) # not an SSLContext - if sys.version_info >= (2, 7): + if sys.version_info >= (2, 7, 9): with pytest.raises(click.BadParameter): run_command.make_context('run', ['--cert', 'flask']) # SSLContext - if sys.version_info < (2, 7): + if sys.version_info < (2, 7, 9): ssl_context = object() else: ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b3535b28..ae1c0805 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -638,15 +638,22 @@ class TestSendfile(object): assert options['filename'] == 'index.txt' rv.close() - def test_attachment_with_utf8_filename(self, app, req_ctx): - 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" - )) + @pytest.mark.usefixtures('req_ctx') + @pytest.mark.parametrize(('filename', 'ascii', 'utf8'), ( + ('index.html', 'index.html', False), + (u'Ñandú/pingüino.txt', '"Nandu/pinguino.txt"', + '%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt'), + (u'Vögel.txt', 'Vogel.txt', 'V%C3%B6gel.txt'), + )) + def test_attachment_filename_encoding(self, filename, ascii, utf8): + rv = flask.send_file('static/index.html', as_attachment=True, attachment_filename=filename) rv.close() + content_disposition = rv.headers['Content-Disposition'] + assert 'filename=%s' % ascii in content_disposition + if utf8: + assert "filename*=UTF-8''" + utf8 in content_disposition + else: + assert "filename*=UTF-8''" not in content_disposition def test_static_file(self, app, req_ctx): # default cache timeout is 12 hours diff --git a/tox.ini b/tox.ini index 03e53d90..546b9279 100644 --- a/tox.ini +++ b/tox.ini @@ -60,7 +60,7 @@ commands = coverage html [testenv:codecov] -passenv = CI TRAVIS TRAVIS_* +passenv = CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* deps = codecov skip_install = true commands =