diff --git a/CHANGES b/CHANGES index b7be2c52..e4300363 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,9 @@ Relase date to be decided, codename to be chosen. show up normally in the traceback. - Flask in debug mode is now detecting some common problems and tries to warn you about them. +- Flask in debug mode will now complain with an assertion error if a view + was attached after the first request was handled. This gives earlier + feedback when users forget to import view code ahead of time. Version 0.7.3 ------------- diff --git a/flask/app.py b/flask/app.py index 1eed507d..df147863 100644 --- a/flask/app.py +++ b/flask/app.py @@ -15,6 +15,7 @@ import sys from threading import Lock from datetime import timedelta from itertools import chain +from functools import update_wrapper from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule @@ -38,6 +39,23 @@ from .signals import request_started, request_finished, got_request_exception, \ _logger_lock = Lock() +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + def wrapper_func(self, *args, **kwargs): + if self.debug and self._got_first_request: + raise AssertionError('A setup function was called after the ' + 'first request was handled. This usually indicates a bug ' + 'in the application where a module was not imported ' + 'and decorators or other functionality was called too late.\n' + 'To fix this make sure to import all your view modules, ' + 'database models and everything related at a central place ' + 'before the application starts serving requests.') + return f(self, *args, **kwargs) + return update_wrapper(wrapper_func, f) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -365,6 +383,10 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + # register the static folder for the application. Do that even # if the folder does not exist. First of all it might be created # while the server is running (usually happens during development) @@ -642,6 +664,7 @@ class Flask(_PackageBoundObject): self.register_blueprint(module, **options) + @setupmethod def register_blueprint(self, blueprint, **options): """Registers a blueprint on the application. @@ -659,6 +682,7 @@ class Flask(_PackageBoundObject): first_registration = True blueprint.register(self, options, first_registration) + @setupmethod def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the @@ -812,6 +836,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def endpoint(self, endpoint): """A decorator to register a function as an endpoint. Example:: @@ -827,6 +852,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def errorhandler(self, code_or_exception): """A decorator that is used to register a function give a given error code. Example:: @@ -877,6 +903,7 @@ class Flask(_PackageBoundObject): """ self._register_error_handler(None, code_or_exception, f) + @setupmethod def _register_error_handler(self, key, code_or_exception, f): if isinstance(code_or_exception, HTTPException): code_or_exception = code_or_exception.code @@ -889,6 +916,7 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .append((code_or_exception, f)) + @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function @@ -906,11 +934,13 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" self.before_request_funcs.setdefault(None, []).append(f) return f + @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 @@ -922,6 +952,7 @@ class Flask(_PackageBoundObject): self.after_request_funcs.setdefault(None, []).append(f) return f + @setupmethod def teardown_request(self, f): """Register a function to be run at the end of each request, regardless of whether there was an exception or not. These functions @@ -948,11 +979,13 @@ class Flask(_PackageBoundObject): self.teardown_request_funcs.setdefault(None, []).append(f) return f + @setupmethod def context_processor(self, f): """Registers a template context processor function.""" self.template_context_processors[None].append(f) return f + @setupmethod def url_value_preprocessor(self, f): """Registers a function as URL value preprocessor for all view functions of the application. It's called before the view functions @@ -961,6 +994,7 @@ class Flask(_PackageBoundObject): self.url_value_preprocessors.setdefault(None, []).append(f) return f + @setupmethod def url_defaults(self, f): """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should @@ -1097,6 +1131,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ + self._got_first_request = True try: request_started.send(self) rv = self.preprocess_request() diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3789cd97..59149286 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -944,6 +944,28 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(c.get('/de/about').data, '/foo') self.assertEqual(c.get('/foo').data, '/en/about') + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assertEqual(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assertEqual(app.test_client().get('/foo').data, 'Meh') + class JSONTestCase(unittest.TestCase):