forked from orbit-oss/flask
Merge branch 'master' of github.com:mitsuhiko/flask
This commit is contained in:
commit
3249eeb438
32 changed files with 626 additions and 259 deletions
23
flask/app.py
23
flask/app.py
|
|
@ -249,6 +249,7 @@ class Flask(_PackageBoundObject):
|
|||
'SESSION_COOKIE_HTTPONLY': True,
|
||||
'SESSION_COOKIE_SECURE': False,
|
||||
'MAX_CONTENT_LENGTH': None,
|
||||
'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours
|
||||
'TRAP_BAD_REQUEST_ERRORS': False,
|
||||
'TRAP_HTTP_EXCEPTIONS': False,
|
||||
'PREFERRED_URL_SCHEME': 'http'
|
||||
|
|
@ -1021,6 +1022,12 @@ class Flask(_PackageBoundObject):
|
|||
self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \
|
||||
.append((code_or_exception, f))
|
||||
|
||||
def get_send_file_options(self, filename):
|
||||
# Override: Hooks in SEND_FILE_MAX_AGE_DEFAULT config.
|
||||
options = super(Flask, self).get_send_file_options(filename)
|
||||
options['cache_timeout'] = self.config['SEND_FILE_MAX_AGE_DEFAULT']
|
||||
return options
|
||||
|
||||
@setupmethod
|
||||
def template_filter(self, name=None):
|
||||
"""A decorator that is used to register custom template filter.
|
||||
|
|
@ -1362,7 +1369,21 @@ class Flask(_PackageBoundObject):
|
|||
if isinstance(rv, basestring):
|
||||
return self.response_class(rv)
|
||||
if isinstance(rv, tuple):
|
||||
return self.response_class(*rv)
|
||||
if len(rv) > 0 and isinstance(rv[0], self.response_class):
|
||||
original = rv[0]
|
||||
new_response = self.response_class('', *rv[1:])
|
||||
if len(rv) < 3:
|
||||
# The args for the response class are
|
||||
# response=None, status=None, headers=None,
|
||||
# mimetype=None, content_type=None, ...
|
||||
# so if there's at least 3 elements the rv
|
||||
# tuple contains header information so the
|
||||
# headers from rv[0] "win."
|
||||
new_response.headers = original.headers
|
||||
new_response.response = original.response
|
||||
return new_response
|
||||
else:
|
||||
return self.response_class(*rv)
|
||||
return self.response_class.force_type(rv, request.environ)
|
||||
|
||||
def create_url_adapter(self, request):
|
||||
|
|
|
|||
|
|
@ -106,8 +106,7 @@ class Config(dict):
|
|||
'loaded. Set this variable and make it '
|
||||
'point to a configuration file' %
|
||||
variable_name)
|
||||
self.from_pyfile(rv)
|
||||
return True
|
||||
return self.from_pyfile(rv, silent=silent)
|
||||
|
||||
def from_pyfile(self, filename, silent=False):
|
||||
"""Updates the values in the config from a Python file. This function
|
||||
|
|
|
|||
49
flask/exceptions.py
Normal file
49
flask/exceptions.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.exceptions
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Flask specific additions to :class:`~werkzeug.exceptions.HTTPException`
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from werkzeug.exceptions import HTTPException, BadRequest
|
||||
from .helpers import json
|
||||
|
||||
|
||||
class JSONHTTPException(HTTPException):
|
||||
"""A base class for HTTP exceptions with ``Content-Type:
|
||||
application/json``.
|
||||
|
||||
The ``description`` attribute of this class must set to a string (*not* an
|
||||
HTML string) which describes the error.
|
||||
|
||||
"""
|
||||
|
||||
def get_body(self, environ):
|
||||
"""Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to
|
||||
return the description of this error in JSON format instead of HTML.
|
||||
|
||||
"""
|
||||
return json.dumps(dict(description=self.get_description(environ)))
|
||||
|
||||
def get_headers(self, environ):
|
||||
"""Returns a list of headers including ``Content-Type:
|
||||
application/json``.
|
||||
|
||||
"""
|
||||
return [('Content-Type', 'application/json')]
|
||||
|
||||
|
||||
class JSONBadRequest(JSONHTTPException, BadRequest):
|
||||
"""Represents an HTTP ``400 Bad Request`` error whose body contains an
|
||||
error message in JSON format instead of HTML format (as in the superclass).
|
||||
|
||||
"""
|
||||
|
||||
#: The description of the error which occurred as a string.
|
||||
description = (
|
||||
'The browser (or proxy) sent a request that this server could not '
|
||||
'understand.'
|
||||
)
|
||||
|
|
@ -118,9 +118,31 @@ def jsonify(*args, **kwargs):
|
|||
information about this, have a look at :ref:`json-security`.
|
||||
|
||||
.. versionadded:: 0.2
|
||||
|
||||
.. versionadded:: 0.9
|
||||
If the ``padded`` argument is true, the JSON object will be padded
|
||||
for JSONP calls and the response mimetype will be changed to
|
||||
``application/javascript``. By default, the request arguments ``callback``
|
||||
and ``jsonp`` will be used as the name for the callback function.
|
||||
This will work with jQuery and most other JavaScript libraries
|
||||
by default.
|
||||
|
||||
If the ``padded`` argument is a string, jsonify will look for
|
||||
the request argument with the same name and use that value as the
|
||||
callback-function name.
|
||||
"""
|
||||
if __debug__:
|
||||
_assert_have_json()
|
||||
if 'padded' in kwargs:
|
||||
if isinstance(kwargs['padded'], str):
|
||||
callback = request.args.get(kwargs['padded']) or 'jsonp'
|
||||
else:
|
||||
callback = request.args.get('callback') or \
|
||||
request.args.get('jsonp') or 'jsonp'
|
||||
del kwargs['padded']
|
||||
json_str = json.dumps(dict(*args, **kwargs), indent=None)
|
||||
content = str(callback) + "(" + json_str + ")"
|
||||
return current_app.response_class(content, mimetype='application/javascript')
|
||||
return current_app.response_class(json.dumps(dict(*args, **kwargs),
|
||||
indent=None if request.is_xhr else 2), mimetype='application/json')
|
||||
|
||||
|
|
@ -283,7 +305,16 @@ def flash(message, category='message'):
|
|||
messages and ``'warning'`` for warnings. However any
|
||||
kind of string can be used as category.
|
||||
"""
|
||||
session.setdefault('_flashes', []).append((category, message))
|
||||
# Original implementation:
|
||||
#
|
||||
# session.setdefault('_flashes', []).append((category, message))
|
||||
#
|
||||
# This assumed that changes made to mutable structures in the session are
|
||||
# are always in sync with the sess on object, which is not true for session
|
||||
# implementations that use external storage for keeping their keys/values.
|
||||
flashes = session.get('_flashes', [])
|
||||
flashes.append((category, message))
|
||||
session['_flashes'] = flashes
|
||||
|
||||
|
||||
def get_flashed_messages(with_categories=False, category_filter=[]):
|
||||
|
|
@ -341,6 +372,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
|||
guessing requires a `filename` or an `attachment_filename` to be
|
||||
provided.
|
||||
|
||||
Note `get_send_file_options` in :class:`flask.Flask` hooks the
|
||||
``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable to set the default
|
||||
cache_timeout.
|
||||
|
||||
Please never pass filenames to this function from user sources without
|
||||
checking them first. Something like this is usually sufficient to
|
||||
avoid security problems::
|
||||
|
|
@ -517,7 +552,8 @@ def send_from_directory(directory, filename, **options):
|
|||
filename = safe_join(directory, filename)
|
||||
if not os.path.isfile(filename):
|
||||
raise NotFound()
|
||||
return send_file(filename, conditional=True, **options)
|
||||
options.setdefault('conditional', True)
|
||||
return send_file(filename, **options)
|
||||
|
||||
|
||||
def get_root_path(import_name):
|
||||
|
|
@ -673,6 +709,25 @@ class _PackageBoundObject(object):
|
|||
return FileSystemLoader(os.path.join(self.root_path,
|
||||
self.template_folder))
|
||||
|
||||
def get_send_file_options(self, filename):
|
||||
"""Provides keyword arguments to send to :func:`send_from_directory`.
|
||||
|
||||
This allows subclasses to change the behavior when sending files based
|
||||
on the filename. For example, to set the cache timeout for .js files
|
||||
to 60 seconds (note the options are keywords for :func:`send_file`)::
|
||||
|
||||
class MyFlask(flask.Flask):
|
||||
def get_send_file_options(self, filename):
|
||||
options = super(MyFlask, self).get_send_file_options(filename)
|
||||
if filename.lower().endswith('.js'):
|
||||
options['cache_timeout'] = 60
|
||||
options['conditional'] = True
|
||||
return options
|
||||
|
||||
.. versionadded:: 0.9
|
||||
"""
|
||||
return {}
|
||||
|
||||
def send_static_file(self, filename):
|
||||
"""Function used internally to send static files from the static
|
||||
folder to the browser.
|
||||
|
|
@ -681,7 +736,8 @@ class _PackageBoundObject(object):
|
|||
"""
|
||||
if not self.has_static_folder:
|
||||
raise RuntimeError('No static folder for this object')
|
||||
return send_from_directory(self.static_folder, filename)
|
||||
return send_from_directory(self.static_folder, filename,
|
||||
**self.get_send_file_options(filename))
|
||||
|
||||
def open_resource(self, resource, mode='rb'):
|
||||
"""Opens a resource from the application's resource folder. To see
|
||||
|
|
|
|||
|
|
@ -659,6 +659,35 @@ class BasicFunctionalityTestCase(FlaskTestCase):
|
|||
self.assert_equal(rv.data, 'W00t')
|
||||
self.assert_equal(rv.mimetype, 'text/html')
|
||||
|
||||
def test_make_response_with_response_instance(self):
|
||||
app = flask.Flask(__name__)
|
||||
with app.test_request_context():
|
||||
rv = flask.make_response(
|
||||
flask.jsonify({'msg': 'W00t'}), 400)
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
self.assertEqual(rv.data,
|
||||
'{\n "msg": "W00t"\n}')
|
||||
self.assertEqual(rv.mimetype, 'application/json')
|
||||
|
||||
rv = flask.make_response(
|
||||
flask.Response(''), 400)
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
self.assertEqual(rv.data, '')
|
||||
self.assertEqual(rv.mimetype, 'text/html')
|
||||
|
||||
rv = flask.make_response(
|
||||
flask.Response('', headers={'Content-Type': 'text/html'}),
|
||||
400, None, 'application/json')
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
self.assertEqual(rv.headers['Content-Type'], 'application/json')
|
||||
|
||||
rv = flask.make_response(
|
||||
flask.Response('', mimetype='application/json'),
|
||||
400, {'Content-Type': 'text/html'})
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
self.assertEqual(rv.headers['Content-Type'], 'text/html')
|
||||
|
||||
|
||||
def test_url_generation(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/hello/<name>', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import unittest
|
|||
import warnings
|
||||
from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.http import parse_cache_control_header
|
||||
from jinja2 import TemplateNotFound
|
||||
|
||||
|
||||
|
|
@ -357,6 +358,19 @@ class BlueprintTestCase(FlaskTestCase):
|
|||
rv = c.get('/admin/static/css/test.css')
|
||||
self.assert_equal(rv.data.strip(), '/* nested file */')
|
||||
|
||||
# try/finally, in case other tests use this app for Blueprint tests.
|
||||
max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT']
|
||||
try:
|
||||
expected_max_age = 3600
|
||||
if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age:
|
||||
expected_max_age = 7200
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age
|
||||
rv = c.get('/admin/static/css/test.css')
|
||||
cc = parse_cache_control_header(rv.headers['Cache-Control'])
|
||||
self.assert_equal(cc.max_age, expected_max_age)
|
||||
finally:
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default
|
||||
|
||||
with app.test_request_context():
|
||||
self.assert_equal(flask.url_for('admin.static', filename='test.txt'),
|
||||
'/admin/static/test.txt')
|
||||
|
|
|
|||
|
|
@ -69,6 +69,24 @@ class ConfigTestCase(FlaskTestCase):
|
|||
finally:
|
||||
os.environ = env
|
||||
|
||||
def test_config_from_envvar_missing(self):
|
||||
env = os.environ
|
||||
try:
|
||||
os.environ = {'FOO_SETTINGS': 'missing.cfg'}
|
||||
try:
|
||||
app = flask.Flask(__name__)
|
||||
app.config.from_envvar('FOO_SETTINGS')
|
||||
except IOError, e:
|
||||
msg = str(e)
|
||||
self.assert_(msg.startswith('[Errno 2] Unable to load configuration '
|
||||
'file (No such file or directory):'))
|
||||
self.assert_(msg.endswith("missing.cfg'"))
|
||||
else:
|
||||
self.fail('expected IOError')
|
||||
self.assertFalse(app.config.from_envvar('FOO_SETTINGS', silent=True))
|
||||
finally:
|
||||
os.environ = env
|
||||
|
||||
def test_config_missing(self):
|
||||
app = flask.Flask(__name__)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import unittest
|
|||
from logging import StreamHandler
|
||||
from StringIO import StringIO
|
||||
from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr
|
||||
from werkzeug.http import parse_options_header
|
||||
from werkzeug.http import parse_cache_control_header, parse_options_header
|
||||
|
||||
|
||||
def has_encoding(name):
|
||||
|
|
@ -40,6 +40,18 @@ class JSONTestCase(FlaskTestCase):
|
|||
rv = c.post('/json', data='malformed', content_type='application/json')
|
||||
self.assert_equal(rv.status_code, 400)
|
||||
|
||||
def test_json_bad_requests_content_type(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/json', methods=['POST'])
|
||||
def return_json():
|
||||
return unicode(flask.request.json)
|
||||
c = app.test_client()
|
||||
rv = c.post('/json', data='malformed', content_type='application/json')
|
||||
self.assert_equal(rv.status_code, 400)
|
||||
self.assert_equal(rv.mimetype, 'application/json')
|
||||
self.assert_('description' in flask.json.loads(rv.data))
|
||||
self.assert_('<p>' not in flask.json.loads(rv.data)['description'])
|
||||
|
||||
def test_json_body_encoding(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.testing = True
|
||||
|
|
@ -61,11 +73,25 @@ class JSONTestCase(FlaskTestCase):
|
|||
@app.route('/dict')
|
||||
def return_dict():
|
||||
return flask.jsonify(d)
|
||||
@app.route("/padded")
|
||||
def return_padded_json():
|
||||
return flask.jsonify(d, padded=True)
|
||||
@app.route("/padded_custom")
|
||||
def return_padded_json_custom_callback():
|
||||
return flask.jsonify(d, padded='my_func_name')
|
||||
c = app.test_client()
|
||||
for url in '/kw', '/dict':
|
||||
rv = c.get(url)
|
||||
self.assert_equal(rv.mimetype, 'application/json')
|
||||
self.assert_equal(flask.json.loads(rv.data), d)
|
||||
for get_arg in 'callback=funcName', 'jsonp=funcName':
|
||||
rv = c.get('/padded?' + get_arg)
|
||||
self.assert_( rv.data.startswith("funcName(") )
|
||||
self.assert_( rv.data.endswith(")") )
|
||||
rv_json = rv.data.split('(')[1].split(')')[0]
|
||||
self.assert_equal(flask.json.loads(rv_json), d)
|
||||
rv = c.get('/padded_custom?my_func_name=funcName')
|
||||
self.assert_( rv.data.startswith("funcName(") )
|
||||
|
||||
def test_json_attr(self):
|
||||
app = flask.Flask(__name__)
|
||||
|
|
@ -204,6 +230,33 @@ class SendfileTestCase(FlaskTestCase):
|
|||
self.assert_equal(value, 'attachment')
|
||||
self.assert_equal(options['filename'], 'index.txt')
|
||||
|
||||
def test_static_file(self):
|
||||
app = flask.Flask(__name__)
|
||||
# default cache timeout is 12 hours
|
||||
with app.test_request_context():
|
||||
rv = app.send_static_file('index.html')
|
||||
cc = parse_cache_control_header(rv.headers['Cache-Control'])
|
||||
self.assert_equal(cc.max_age, 12 * 60 * 60)
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600
|
||||
with app.test_request_context():
|
||||
rv = app.send_static_file('index.html')
|
||||
cc = parse_cache_control_header(rv.headers['Cache-Control'])
|
||||
self.assert_equal(cc.max_age, 3600)
|
||||
# override get_send_file_options with some new values and check them
|
||||
class StaticFileApp(flask.Flask):
|
||||
def get_send_file_options(self, filename):
|
||||
opts = super(StaticFileApp, self).get_send_file_options(filename)
|
||||
opts['cache_timeout'] = 10
|
||||
# this test catches explicit inclusion of the conditional
|
||||
# keyword arg in the guts
|
||||
opts['conditional'] = True
|
||||
return opts
|
||||
app = StaticFileApp(__name__)
|
||||
with app.test_request_context():
|
||||
rv = app.send_static_file('index.html')
|
||||
cc = parse_cache_control_header(rv.headers['Cache-Control'])
|
||||
self.assert_equal(cc.max_age, 10)
|
||||
|
||||
|
||||
class LoggingTestCase(FlaskTestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class MethodViewType(type):
|
|||
rv = type.__new__(cls, name, bases, d)
|
||||
if 'methods' not in d:
|
||||
methods = set(rv.methods or [])
|
||||
for key, value in d.iteritems():
|
||||
for key in d:
|
||||
if key in http_method_funcs:
|
||||
methods.add(key.upper())
|
||||
# if we have no method at all in there we don't want to
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
"""
|
||||
|
||||
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from .exceptions import JSONBadRequest
|
||||
from .debughelpers import attach_enctype_error_multidict
|
||||
from .helpers import json, _assert_have_json
|
||||
from .globals import _request_ctx_stack
|
||||
|
|
@ -108,12 +108,22 @@ class Request(RequestBase):
|
|||
|
||||
def on_json_loading_failed(self, e):
|
||||
"""Called if decoding of the JSON data failed. The return value of
|
||||
this method is used by :attr:`json` when an error ocurred. The
|
||||
default implementation raises a :class:`~werkzeug.exceptions.BadRequest`.
|
||||
this method is used by :attr:`json` when an error ocurred. The default
|
||||
implementation raises a :class:`JSONBadRequest`, which is a subclass of
|
||||
:class:`~werkzeug.exceptions.BadRequest` which sets the
|
||||
``Content-Type`` to ``application/json`` and provides a JSON-formatted
|
||||
error description::
|
||||
|
||||
{"description": "The browser (or proxy) sent a request that \
|
||||
this server could not understand."}
|
||||
|
||||
.. versionchanged:: 0.9
|
||||
Return a :class:`JSONBadRequest` instead of a
|
||||
:class:`~werkzeug.exceptions.BadRequest` by default.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
raise BadRequest()
|
||||
raise JSONBadRequest()
|
||||
|
||||
def _load_form_data(self):
|
||||
RequestBase._load_form_data(self)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue