Merge branch 'master' of github.com:mitsuhiko/flask

This commit is contained in:
Armin Ronacher 2012-04-09 15:26:09 +01:00
commit 3249eeb438
32 changed files with 626 additions and 259 deletions

View file

@ -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):

View file

@ -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
View 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.'
)

View file

@ -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

View file

@ -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'])

View file

@ -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')

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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)