diff --git a/CHANGES b/CHANGES index d646383e..a6fa7ec6 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,9 @@ Version 1.0 - Made Flask support custom JSON mimetypes for incoming data. - Added support for returning tuples in the form ``(response, headers)`` from a view function. +- Added :meth:`flask.Config.from_json`. +- Added :attr:`flask.Flask.config_class`. +- Added :meth:`flask.config.Config.get_namespace`. Version 0.10.2 -------------- diff --git a/docs/conf.py b/docs/conf.py index feed359f..16c841f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2013, Armin Ronacher' +copyright = u'2014, Armin Ronacher' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/config.rst b/docs/config.rst index 9cde9d24..85051e4a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -155,7 +155,7 @@ The following configuration values are used internally by Flask: ascii-encoded JSON. If this is set to ``False`` Flask will not encode to ASCII and output strings as-is and return - unicode strings. ``jsonfiy`` will + unicode strings. ``jsonify`` will automatically encode it in ``utf-8`` then for transport for instance. ``JSON_SORT_KEYS`` By default Flask will serialize JSON diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 9e26196d..4db7a209 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -269,7 +269,7 @@ of the box (see :ref:`debug-mode`). If you would like to use another Python debugger, note that debuggers interfere with each other. You have to set some options in order to use your favorite debugger: -* ``debug`` - whether to enable debug mode and catch exceptinos +* ``debug`` - whether to enable debug mode and catch exceptions * ``use_debugger`` - whether to use the internal Flask debugger * ``use_reloader`` - whether to reload and fork the process on exception diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c92784f3..a4494f43 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -258,6 +258,8 @@ accessing URLs. By default, a route only answers to `GET` requests, but that can be changed by providing the `methods` argument to the :meth:`~flask.Flask.route` decorator. Here are some examples:: + from flask import request + @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': diff --git a/flask/app.py b/flask/app.py index 0317075a..2bff6af0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -175,6 +175,17 @@ class Flask(_PackageBoundObject): _set_request_globals_class) del _get_request_globals_class, _set_request_globals_class + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 1.0 + config_class = Config + #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception occurs and the integrated server will automatically reload @@ -610,7 +621,7 @@ class Flask(_PackageBoundObject): root_path = self.root_path if instance_relative: root_path = self.instance_path - return Config(root_path, self.default_config) + return self.config_class(root_path, self.default_config) def auto_find_instance_path(self): """Tries to locate the instance path if it was not provided to the @@ -1208,9 +1219,11 @@ class Flask(_PackageBoundObject): @setupmethod def after_request(self, f): - """Register a function to be run after each request. Your function - must take one parameter, a :attr:`response_class` object and return - a new response object or the same (see :meth:`process_response`). + """Register a function to be run after each request. + + Your function must take one parameter, an instance of + :attr:`response_class` and return a new response object or the + same (see :meth:`process_response`). As of Flask 0.7 this function might not be executed at the end of the request in case an unhandled exception occurred. diff --git a/flask/blueprints.py b/flask/blueprints.py index bdf3a3c6..45faf2c5 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -79,7 +79,7 @@ class BlueprintSetupState(object): class Blueprint(_PackageBoundObject): """Represents a blueprint. A blueprint is an object that records functions that will be called with the - :class:`~flask.blueprint.BlueprintSetupState` later to register functions + :class:`~flask.blueprints.BlueprintSetupState` later to register functions or other things on the main application. See :ref:`blueprints` for more information. diff --git a/flask/config.py b/flask/config.py index 07d6fbc8..dfc2f6b3 100644 --- a/flask/config.py +++ b/flask/config.py @@ -14,7 +14,8 @@ import os import errno from werkzeug.utils import import_string -from ._compat import string_types +from ._compat import string_types, iteritems +from . import json class ConfigAttribute(object): @@ -164,5 +165,69 @@ class Config(dict): if key.isupper(): self[key] = getattr(obj, key) + def from_json(self, filename, silent=False): + """Updates the values in the config from a JSON file. This function + behaves as if the JSON object was a dictionary and passed ot the + :meth:`from_object` function. + + :param filename: the filename of the JSON file. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to `True` if you want silent failure for missing + files. + + .. versionadded:: 1.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename) as json_file: + obj = json.loads(json_file.read()) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + for key in obj.keys(): + if key.isupper(): + self[key] = obj[key] + return True + + def get_namespace(self, namespace, lowercase=True): + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + + .. versionadded:: 1.0 + """ + rv = {} + for k, v in iteritems(self): + if not k.startswith(namespace): + continue + key = k[len(namespace):] + if lowercase: + key = key.lower() + rv[key] = v + return rv + def __repr__(self): return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 695243c6..d0d62f29 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -157,6 +157,9 @@ class FlaskTestCase(unittest.TestCase): def assert_not_in(self, x, y): self.assertNotIn(x, y) + def assert_isinstance(self, obj, cls): + self.assertIsInstance(obj, cls) + if sys.version_info[:2] == (2, 6): def assertIn(self, x, y): assert x in y, "%r unexpectedly not in %r" % (x, y) @@ -164,6 +167,9 @@ class FlaskTestCase(unittest.TestCase): def assertNotIn(self, x, y): assert x not in y, "%r unexpectedly in %r" % (x, y) + def assertIsInstance(self, x, y): + assert isinstance(x, y), "not isinstance(%r, %r)" % (x, y) + class _ExceptionCatcher(object): diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index cdc6273f..d0542200 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -41,6 +41,12 @@ class ConfigTestCase(FlaskTestCase): app.config.from_object(__name__) self.common_object_test(app) + def test_config_from_json(self): + app = flask.Flask(__name__) + current_dir = os.path.dirname(os.path.abspath(__file__)) + app.config.from_json(os.path.join(current_dir, 'static', 'config.json')) + self.common_object_test(app) + def test_config_from_class(self): class Base(object): TEST_KEY = 'foo' @@ -100,11 +106,49 @@ class ConfigTestCase(FlaskTestCase): self.assert_true(0, 'expected config') self.assert_false(app.config.from_pyfile('missing.cfg', silent=True)) + def test_config_missing_json(self): + app = flask.Flask(__name__) + try: + app.config.from_json('missing.json') + except IOError as e: + msg = str(e) + self.assert_true(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_true(msg.endswith("missing.json'")) + else: + self.assert_true(0, 'expected config') + self.assert_false(app.config.from_json('missing.json', silent=True)) + + def test_custom_config_class(self): + class Config(flask.Config): + pass + class Flask(flask.Flask): + config_class = Config + app = Flask(__name__) + self.assert_isinstance(app.config, Config) + app.config.from_object(__name__) + self.common_object_test(app) + def test_session_lifetime(self): app = flask.Flask(__name__) app.config['PERMANENT_SESSION_LIFETIME'] = 42 self.assert_equal(app.permanent_session_lifetime.seconds, 42) + def test_get_namespace(self): + app = flask.Flask(__name__) + app.config['FOO_OPTION_1'] = 'foo option 1' + app.config['FOO_OPTION_2'] = 'foo option 2' + app.config['BAR_STUFF_1'] = 'bar stuff 1' + app.config['BAR_STUFF_2'] = 'bar stuff 2' + foo_options = app.config.get_namespace('FOO_') + self.assert_equal(2, len(foo_options)) + self.assert_equal('foo option 1', foo_options['option_1']) + self.assert_equal('foo option 2', foo_options['option_2']) + bar_options = app.config.get_namespace('BAR_', lowercase=False) + self.assert_equal(2, len(bar_options)) + self.assert_equal('bar stuff 1', bar_options['STUFF_1']) + self.assert_equal('bar stuff 2', bar_options['STUFF_2']) + class LimitedLoaderMockWrapper(object): def __init__(self, loader): diff --git a/flask/testsuite/static/config.json b/flask/testsuite/static/config.json new file mode 100644 index 00000000..4a9722ec --- /dev/null +++ b/flask/testsuite/static/config.json @@ -0,0 +1,4 @@ +{ + "TEST_KEY": "foo", + "SECRET_KEY": "devkey" +}