Skip to content

Commit

Permalink
Add support for PathLike objects in static file helpers
Browse files Browse the repository at this point in the history
See: https://www.python.org/dev/peps/pep-0519/

This is mostly encountered with pathlib in python 3, but this API
suggests any PathLike object can be treated like a filepath with
`__fspath__` function.
  • Loading branch information
mattrobenolt authored and davidism committed Jan 5, 2019
1 parent f7d50d4 commit 25de45c
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 5 deletions.
11 changes: 7 additions & 4 deletions CHANGES.rst
Expand Up @@ -10,14 +10,17 @@ Version 1.1
Unreleased

- :meth:`flask.RequestContext.copy` includes the current session
object in the request context copy. This prevents ``flask.session``
object in the request context copy. This prevents ``flask.session``
pointing to an out-of-date object. (`#2935`)
- Using built-in RequestContext, unprintable Unicode characters in Host
header will result in a HTTP 400 response and not HTTP 500 as previously.
(`#2994`)
- :func:`send_file` supports ``PathLike`` objects as describe in
PEP 0519, to support ``pathlib`` in Python 3. (`#3059`_)

.. _#2935: https://github.com/pallets/flask/issues/2935
.. _#2994: https://github.com/pallets/flask/pull/2994
.. _#3059: https://github.com/pallets/flask/pull/3059


Version 1.0.3
Expand Down Expand Up @@ -355,7 +358,7 @@ Released on December 21st 2016, codename Punsch.
- Add support for range requests in ``send_file``.
- ``app.test_client`` includes preset default environment, which can now be
directly set, instead of per ``client.get``.

.. _#1849: https://github.com/pallets/flask/pull/1849
.. _#1988: https://github.com/pallets/flask/pull/1988
.. _#1730: https://github.com/pallets/flask/pull/1730
Expand All @@ -376,7 +379,7 @@ Version 0.11.1
Bugfix release, released on June 7th 2016.

- Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from working. (`#1872`_)

.. _#1872: https://github.com/pallets/flask/pull/1872

Version 0.11
Expand Down Expand Up @@ -456,7 +459,7 @@ Released on May 29th 2016, codename Absinthe.
- Added the ``JSONIFY_MIMETYPE`` configuration variable (`#1728`_).
- Exceptions during teardown handling will no longer leave bad application
contexts lingering around.

.. _#1326: https://github.com/pallets/flask/pull/1326
.. _#1393: https://github.com/pallets/flask/pull/1393
.. _#1422: https://github.com/pallets/flask/pull/1422
Expand Down
9 changes: 9 additions & 0 deletions flask/_compat.py
Expand Up @@ -97,3 +97,12 @@ def __exit__(self, *args):
BROKEN_PYPY_CTXMGR_EXIT = True
except AssertionError:
pass


try:
from os import fspath
except ImportError:
# Backwards compatibility as proposed in PEP 0519:
# https://www.python.org/dev/peps/pep-0519/#backwards-compatibility
def fspath(path):
return path.__fspath__() if hasattr(path, '__fspath__') else path
11 changes: 10 additions & 1 deletion flask/helpers.py
Expand Up @@ -33,7 +33,7 @@
from .signals import message_flashed
from .globals import session, _request_ctx_stack, _app_ctx_stack, \
current_app, request
from ._compat import string_types, text_type, PY2
from ._compat import string_types, text_type, PY2, fspath

# sentinel
_missing = object()
Expand Down Expand Up @@ -510,6 +510,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
Filenames are encoded with ASCII instead of Latin-1 for broader
compatibility with WSGI servers.
.. versionchanged:: 1.1
Filenames may be a `PathLike` object.
: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.
Expand Down Expand Up @@ -538,6 +541,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
"""
mtime = None
fsize = None

if hasattr(filename_or_fp, '__fspath__'):
filename_or_fp = fspath(filename_or_fp)

if isinstance(filename_or_fp, string_types):
filename = filename_or_fp
if not os.path.isabs(filename):
Expand Down Expand Up @@ -705,6 +712,8 @@ def download_file(filename):
:param options: optional keyword arguments that are directly
forwarded to :func:`send_file`.
"""
filename = fspath(filename)
directory = fspath(directory)
filename = safe_join(directory, filename)
if not os.path.isabs(filename):
filename = os.path.join(current_app.root_path, filename)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_helpers.py
Expand Up @@ -36,6 +36,19 @@ def has_encoding(name):
return False


class FakePath(object):
"""Fake object to represent a ``PathLike object``.
This represents a ``pathlib.Path`` object in python 3.
See: https://www.python.org/dev/peps/pep-0519/
"""
def __init__(self, path):
self.path = path

def __fspath__(self):
return self.path


class FixedOffset(datetime.tzinfo):
"""Fixed offset in hours east from UTC.
Expand Down Expand Up @@ -527,6 +540,15 @@ def __getattr__(self, name):
assert 'x-sendfile' not in rv.headers
rv.close()

def test_send_file_pathlike(self, app, req_ctx):
rv = flask.send_file(FakePath('static/index.html'))
assert rv.direct_passthrough
assert rv.mimetype == 'text/html'
with app.open_resource('static/index.html') as f:
rv.direct_passthrough = False
assert rv.data == f.read()
rv.close()

@pytest.mark.skipif(
not callable(getattr(Range, 'to_content_range_header', None)),
reason="not implemented within werkzeug"
Expand Down Expand Up @@ -681,6 +703,12 @@ def test_static_file(self, app, req_ctx):
assert cc.max_age == 3600
rv.close()

# Test with static file handler.
rv = app.send_static_file(FakePath('index.html'))
cc = parse_cache_control_header(rv.headers['Cache-Control'])
assert cc.max_age == 3600
rv.close()

class StaticFileApp(flask.Flask):
def get_send_file_max_age(self, filename):
return 10
Expand All @@ -706,6 +734,14 @@ def test_send_from_directory(self, app, req_ctx):
assert rv.data.strip() == b'Hello Subdomain'
rv.close()

def test_send_from_directory_pathlike(self, app, req_ctx):
app.root_path = os.path.join(os.path.dirname(__file__),
'test_apps', 'subdomaintestmodule')
rv = flask.send_from_directory(FakePath('static'), FakePath('hello.txt'))
rv.direct_passthrough = False
assert rv.data.strip() == b'Hello Subdomain'
rv.close()

def test_send_from_directory_bad_request(self, app, req_ctx):
app.root_path = os.path.join(os.path.dirname(__file__),
'test_apps', 'subdomaintestmodule')
Expand Down

0 comments on commit 25de45c

Please sign in to comment.