Browse Source

Merge pull request #1 from pallets/master

Update from upstream
pull/2157/head
Yingcai FENG 8 years ago committed by GitHub
parent
commit
770c134bd2
  1. 3
      CHANGES
  2. 25
      CONTRIBUTING.rst
  3. 8
      docs/api.rst
  4. 4
      docs/cli.rst
  5. 8
      docs/deploying/mod_wsgi.rst
  6. 2
      docs/installation.rst
  7. 8
      docs/quickstart.rst
  8. 2
      examples/flaskr/flaskr/__init__.py
  9. 2
      flask/app.py
  10. 2
      flask/cli.py
  11. 40
      flask/helpers.py
  12. 14
      flask/testing.py
  13. 2
      flask/views.py
  14. 65
      tests/test_helpers.py
  15. 35
      tests/test_testing.py

3
CHANGES

@ -18,6 +18,9 @@ Version 0.12
- Correctly invoke response handlers for both regular request dispatching as - Correctly invoke response handlers for both regular request dispatching as
well as error handlers. well as error handlers.
- Disable logger propagation by default for the app logger. - Disable logger propagation by default for the app logger.
- 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``.
Version 0.11.2 Version 0.11.2
-------------- --------------

25
CONTRIBUTING.rst

@ -87,3 +87,28 @@ Generate a HTML report can be done using this command::
Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io
Caution
=======
pushing
-------
This repository contains several zero-padded file modes that may cause issues when pushing this repository to git hosts other than github. Fixing this is destructive to the commit history, so we suggest ignoring these warnings. If it fails to push and you're using a self-hosted git service like Gitlab, you can turn off repository checks in the admin panel.
cloning
-------
The zero-padded file modes files above can cause issues while cloning, too. If you have
::
[fetch]
fsckobjects = true
or
::
[receive]
fsckObjects = true
set in your git configuration file, cloning this repository will fail. The only solution is to set both of the above settings to false while cloning, and then setting them back to true after the cloning is finished.

8
docs/api.rst

@ -316,13 +316,7 @@ Useful Functions and Classes
.. autofunction:: url_for .. autofunction:: url_for
.. function:: abort(code) .. autofunction:: abort
Raises an :exc:`~werkzeug.exceptions.HTTPException` for the given
status code. For example to abort request handling with a page not
found exception, you would call ``abort(404)``.
:param code: the HTTP error code.
.. autofunction:: redirect .. autofunction:: redirect

4
docs/cli.rst

@ -61,9 +61,7 @@ Debug Flag
The :command:`flask` script can also be instructed to enable the debug The :command:`flask` script can also be instructed to enable the debug
mode of the application automatically by exporting ``FLASK_DEBUG``. If mode of the application automatically by exporting ``FLASK_DEBUG``. If
set to ``1`` debug is enabled or ``0`` disables it. set to ``1`` debug is enabled or ``0`` disables it::
Or with a filename::
export FLASK_DEBUG=1 export FLASK_DEBUG=1

8
docs/deploying/mod_wsgi.rst

@ -130,12 +130,12 @@ to httpd 2.4 syntax
Require all granted Require all granted
For more information consult the `mod_wsgi wiki`_. For more information consult the `mod_wsgi documentation`_.
.. _mod_wsgi: http://code.google.com/p/modwsgi/ .. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi
.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide .. _installation instructions: http://modwsgi.readthedocs.io/en/develop/installation.html
.. _virtual python: https://pypi.python.org/pypi/virtualenv .. _virtual python: https://pypi.python.org/pypi/virtualenv
.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/w/list .. _mod_wsgi documentation: http://modwsgi.readthedocs.io/en/develop/index.html
Troubleshooting Troubleshooting
--------------- ---------------

2
docs/installation.rst

@ -72,7 +72,7 @@ corresponding environment. On OS X and Linux, do the following::
If you are a Windows user, the following command is for you:: If you are a Windows user, the following command is for you::
$ venv\scripts\activate $ venv\Scripts\activate
Either way, you should now be using your virtualenv (notice how the prompt of Either way, you should now be using your virtualenv (notice how the prompt of
your shell has changed to show the active environment). your shell has changed to show the active environment).

8
docs/quickstart.rst

@ -102,10 +102,10 @@ docs to see the alternative method for running a server.
Invalid Import Name Invalid Import Name
``````````````````` ```````````````````
The ``-a`` argument to :command:`flask` is the name of the module to The ``FLASK_APP`` environment variable is the name of the module to import at
import. In case that module is incorrectly named you will get an import :command:`flask run`. In case that module is incorrectly named you will get an
error upon start (or if debug is enabled when you navigate to the import error upon start (or if debug is enabled when you navigate to the
application). It will tell you what it tried to import and why it failed. application). It will tell you what it tried to import and why it failed.
The most common reason is a typo or because you did not actually create an The most common reason is a typo or because you did not actually create an
``app`` object. ``app`` object.

2
examples/flaskr/flaskr/__init__.py

@ -1 +1 @@
from flaskr import app from flaskr.flaskr import app

2
flask/app.py

@ -519,7 +519,7 @@ class Flask(_PackageBoundObject):
#: def to_python(self, value): #: def to_python(self, value):
#: return value.split(',') #: return value.split(',')
#: def to_url(self, values): #: def to_url(self, values):
#: return ','.join(BaseConverter.to_url(value) #: return ','.join(super(ListConverter, self).to_url(value)
#: for value in values) #: for value in values)
#: #:
#: app = Flask(__name__) #: app = Flask(__name__)

2
flask/cli.py

@ -469,7 +469,7 @@ def shell_command():
cli = FlaskGroup(help="""\ cli = FlaskGroup(help="""\
This shell command acts as general utility script for Flask applications. This shell command acts as general utility script for Flask applications.
It loads the application configured (either through the FLASK_APP environment It loads the application configured (through the FLASK_APP environment
variable) and then provides commands either provided by the application or variable) and then provides commands either provided by the application or
Flask itself. Flask itself.

40
flask/helpers.py

@ -25,8 +25,9 @@ try:
except ImportError: except ImportError:
from urlparse import quote as url_quote from urlparse import quote as url_quote
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers, Range
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound, \
RequestedRangeNotSatisfiable
# this was moved in 0.7 # this was moved in 0.7
try: try:
@ -39,7 +40,7 @@ from jinja2 import FileSystemLoader
from .signals import message_flashed from .signals import message_flashed
from .globals import session, _request_ctx_stack, _app_ctx_stack, \ from .globals import session, _request_ctx_stack, _app_ctx_stack, \
current_app, request current_app, request
from ._compat import string_types, text_type, PY2 from ._compat import string_types, text_type
# sentinel # sentinel
@ -446,6 +447,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
ETags will also be attached automatically if a `filename` is provided. You ETags will also be attached automatically if a `filename` is provided. You
can turn this off by setting `add_etags=False`. can turn this off by setting `add_etags=False`.
If `conditional=True` and `filename` is provided, this method will try to
upgrade the response stream to support range requests. This will allow
the request to be answered with partial content response.
Please never pass filenames to this function from user sources; Please never pass filenames to this function from user sources;
you should use :func:`send_from_directory` instead. you should use :func:`send_from_directory` instead.
@ -500,6 +505,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
If a file was passed, this overrides its mtime. If a file was passed, this overrides its mtime.
""" """
mtime = None mtime = None
fsize = None
if isinstance(filename_or_fp, string_types): if isinstance(filename_or_fp, string_types):
filename = filename_or_fp filename = filename_or_fp
if not os.path.isabs(filename): if not os.path.isabs(filename):
@ -535,13 +541,15 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
if file is not None: if file is not None:
file.close() file.close()
headers['X-Sendfile'] = filename headers['X-Sendfile'] = filename
headers['Content-Length'] = os.path.getsize(filename) fsize = os.path.getsize(filename)
headers['Content-Length'] = fsize
data = None data = None
else: else:
if file is None: if file is None:
file = open(filename, 'rb') file = open(filename, 'rb')
mtime = os.path.getmtime(filename) mtime = os.path.getmtime(filename)
headers['Content-Length'] = os.path.getsize(filename) fsize = os.path.getsize(filename)
headers['Content-Length'] = fsize
data = wrap_file(request.environ, file) data = wrap_file(request.environ, file)
rv = current_app.response_class(data, mimetype=mimetype, headers=headers, rv = current_app.response_class(data, mimetype=mimetype, headers=headers,
@ -559,7 +567,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
rv.cache_control.max_age = cache_timeout rv.cache_control.max_age = cache_timeout
rv.expires = int(time() + cache_timeout) rv.expires = int(time() + cache_timeout)
if add_etags and filename is not None and file is None: if add_etags and filename is not None:
from warnings import warn from warnings import warn
try: try:
@ -575,12 +583,22 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
warn('Access %s failed, maybe it does not exist, so ignore etags in ' warn('Access %s failed, maybe it does not exist, so ignore etags in '
'headers' % filename, stacklevel=2) 'headers' % filename, stacklevel=2)
if conditional: if conditional:
if callable(getattr(Range, 'to_content_range_header', None)):
# Werkzeug supports Range Requests
# Remove this test when support for Werkzeug <0.12 is dropped
try:
rv = rv.make_conditional(request, accept_ranges=True,
complete_length=fsize)
except RequestedRangeNotSatisfiable:
file.close()
raise
else:
rv = rv.make_conditional(request) rv = rv.make_conditional(request)
# make sure we don't send x-sendfile for servers that # make sure we don't send x-sendfile for servers that
# ignore the 304 status code for x-sendfile. # ignore the 304 status code for x-sendfile.
if rv.status_code == 304: if rv.status_code == 304:
rv.headers.pop('x-sendfile', None) rv.headers.pop('x-sendfile', None)
return rv return rv

14
flask/testing.py

@ -10,6 +10,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import werkzeug
from contextlib import contextmanager from contextlib import contextmanager
from werkzeug.test import Client, EnvironBuilder from werkzeug.test import Client, EnvironBuilder
from flask import _request_ctx_stack from flask import _request_ctx_stack
@ -43,11 +44,23 @@ class FlaskClient(Client):
information about how to use this class refer to information about how to use this class refer to
:class:`werkzeug.test.Client`. :class:`werkzeug.test.Client`.
.. versionchanged:: 0.12
`app.test_client()` includes preset default environment, which can be
set after instantiation of the `app.test_client()` object in
`client.environ_base`.
Basic usage is outlined in the :ref:`testing` chapter. Basic usage is outlined in the :ref:`testing` chapter.
""" """
preserve_context = False preserve_context = False
def __init__(self, *args, **kwargs):
super(FlaskClient, self).__init__(*args, **kwargs)
self.environ_base = {
"REMOTE_ADDR": "127.0.0.1",
"HTTP_USER_AGENT": "werkzeug/" + werkzeug.__version__
}
@contextmanager @contextmanager
def session_transaction(self, *args, **kwargs): def session_transaction(self, *args, **kwargs):
"""When used in combination with a ``with`` statement this opens a """When used in combination with a ``with`` statement this opens a
@ -101,6 +114,7 @@ class FlaskClient(Client):
def open(self, *args, **kwargs): def open(self, *args, **kwargs):
kwargs.setdefault('environ_overrides', {}) \ kwargs.setdefault('environ_overrides', {}) \
['flask._preserve_context'] = self.preserve_context ['flask._preserve_context'] = self.preserve_context
kwargs.setdefault('environ_base', self.environ_base)
as_tuple = kwargs.pop('as_tuple', False) as_tuple = kwargs.pop('as_tuple', False)
buffered = kwargs.pop('buffered', False) buffered = kwargs.pop('buffered', False)

2
flask/views.py

@ -123,7 +123,7 @@ class MethodViewType(type):
class MethodView(with_metaclass(MethodViewType, View)): class MethodView(with_metaclass(MethodViewType, View)):
"""Like a regular class-based view but that dispatches requests to """Like a regular class-based view but that dispatches requests to
particular methods. For instance if you implement a method called particular methods. For instance if you implement a method called
:meth:`get` it means you will response to ``'GET'`` requests and :meth:`get` it means it will respond to ``'GET'`` requests and
the :meth:`dispatch_request` implementation will automatically the :meth:`dispatch_request` implementation will automatically
forward your request to that. Also :attr:`options` is set for you forward your request to that. Also :attr:`options` is set for you
automatically:: automatically::

65
tests/test_helpers.py

@ -14,8 +14,10 @@ import pytest
import os import os
import uuid import uuid
import datetime import datetime
import flask import flask
from logging import StreamHandler from logging import StreamHandler
from werkzeug.datastructures import Range
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound
from werkzeug.http import parse_cache_control_header, parse_options_header from werkzeug.http import parse_cache_control_header, parse_options_header
from werkzeug.http import http_date from werkzeug.http import http_date
@ -462,6 +464,69 @@ class TestSendfile(object):
assert 'x-sendfile' not in rv.headers assert 'x-sendfile' not in rv.headers
rv.close() rv.close()
@pytest.mark.skipif(
not callable(getattr(Range, 'to_content_range_header', None)),
reason="not implement within werkzeug"
)
def test_send_file_range_request(self):
app = flask.Flask(__name__)
@app.route('/')
def index():
return flask.send_file('static/index.html', conditional=True)
c = app.test_client()
rv = c.get('/', headers={'Range': 'bytes=4-15'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:16]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-1000'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=-10'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[-10:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=1000-'})
assert rv.status_code == 416
rv.close()
rv = c.get('/', headers={'Range': 'bytes=-'})
assert rv.status_code == 416
rv.close()
rv = c.get('/', headers={'Range': 'somethingsomething'})
assert rv.status_code == 416
rv.close()
last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(
os.path.join(app.root_path, 'static/index.html'))).replace(
microsecond=0)
rv = c.get('/', headers={'Range': 'bytes=4-15',
'If-Range': http_date(last_modified)})
assert rv.status_code == 206
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-15', 'If-Range': http_date(
datetime.datetime(1999, 1, 1))})
assert rv.status_code == 200
rv.close()
def test_attachment(self): def test_attachment(self):
app = flask.Flask(__name__) app = flask.Flask(__name__)
with app.test_request_context(): with app.test_request_context():

35
tests/test_testing.py

@ -11,6 +11,7 @@
import pytest import pytest
import flask import flask
import werkzeug
from flask._compat import text_type from flask._compat import text_type
@ -43,6 +44,40 @@ def test_environ_defaults():
rv = c.get('/') rv = c.get('/')
assert rv.data == b'http://localhost/' assert rv.data == b'http://localhost/'
def test_environ_base_default():
app = flask.Flask(__name__)
app.testing = True
@app.route('/')
def index():
flask.g.user_agent = flask.request.headers["User-Agent"]
return flask.request.remote_addr
with app.test_client() as c:
rv = c.get('/')
assert rv.data == b'127.0.0.1'
assert flask.g.user_agent == 'werkzeug/' + werkzeug.__version__
def test_environ_base_modified():
app = flask.Flask(__name__)
app.testing = True
@app.route('/')
def index():
flask.g.user_agent = flask.request.headers["User-Agent"]
return flask.request.remote_addr
with app.test_client() as c:
c.environ_base['REMOTE_ADDR'] = '0.0.0.0'
c.environ_base['HTTP_USER_AGENT'] = 'Foo'
rv = c.get('/')
assert rv.data == b'0.0.0.0'
assert flask.g.user_agent == 'Foo'
c.environ_base['REMOTE_ADDR'] = '0.0.0.1'
c.environ_base['HTTP_USER_AGENT'] = 'Bar'
rv = c.get('/')
assert rv.data == b'0.0.0.1'
assert flask.g.user_agent == 'Bar'
def test_redirect_keep_session(): def test_redirect_keep_session():
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.secret_key = 'testing' app.secret_key = 'testing'

Loading…
Cancel
Save