diff --git a/CHANGES b/CHANGES index 24a6162e..30ba418b 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,11 @@ Release date to be announced, codename to be decided. same name on the application object. - added a :func:`flask.make_response` function that simplifies creating response object instances in views. +- added signalling support based on blinker. This feature is currently + optional and supposed to be used by extensions and applications. If + you want to use it, make sure to have `blinker`_ installed. + +.. _blinker: http://pypi.python.org/pypi/blinker Version 0.5.2 ------------- diff --git a/Makefile b/Makefile index 4b1d8081..ba898b9e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test upload-docs +.PHONY: clean-pyc test upload-docs docs all: clean-pyc test @@ -20,3 +20,6 @@ upload-docs: scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf scp -r docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/ + +docs: + $(MAKE) -C docs html diff --git a/docs/api.rst b/docs/api.rst index f31563b4..afcb4c20 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -351,3 +351,54 @@ Useful Internals information from the context local around for a little longer. Make sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in that situation, otherwise your unittests will leak memory. + +Signals +------- + +.. versionadded:: 0.6 + +.. data:: signals_available + + `True` if the signalling system is available. This is the case + when `blinker`_ is installed. + +.. data:: template_rendered + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + +.. data:: request_started + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + +.. data:: request_finished + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + +.. data:: got_request_exception + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + +.. class:: flask.signals.Namespace + + An alias for :class:`blinker.base.Namespace` if blinker is available, + otherwise a dummy class that creates fake signals. This class is + available for Flask extensions that want to provide the same fallback + system as Flask itself. + + .. method:: signal(name, doc=None) + + Creates a new signal for this namespace if blinker is available, + otherwise returns a fake signal that has a send method that will + do nothing but will fail with a :exc:`RuntimeError` for all other + operations, including connecting. + +.. _blinker: http://pypi.python.org/pypi/blinker diff --git a/docs/conf.py b/docs/conf.py index 6308beee..721c8369 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -245,7 +245,8 @@ intersphinx_mapping = { 'http://docs.python.org/dev': None, 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, - 'http://wtforms.simplecodes.com/docs/0.5/': None + 'http://wtforms.simplecodes.com/docs/0.5/': None, + 'http://discorporate.us/projects/Blinker/docs/1.0/': None } pygments_style = 'flask_theme_support.FlaskyStyle' diff --git a/flask/__init__.py b/flask/__init__.py index 93ada5f7..95497069 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -24,6 +24,10 @@ from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string +# the signals +from .signals import signals_available, template_rendered, request_started, \ + request_finished, got_request_exception + # only import json if it's available if json_available: from .helpers import json diff --git a/flask/app.py b/flask/app.py index 16da7939..5860eb05 100644 --- a/flask/app.py +++ b/flask/app.py @@ -32,6 +32,7 @@ from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor +from .signals import request_started, request_finished, got_request_exception # a lock used for logger initialization _logger_lock = Lock() @@ -657,6 +658,7 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ + got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) if self.debug: raise @@ -791,6 +793,7 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: + request_started.send(self) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() @@ -801,6 +804,7 @@ class Flask(_PackageBoundObject): response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) + request_finished.send(self, response=response) return response(environ, start_response) def request_context(self, environ): diff --git a/flask/signals.py b/flask/signals.py new file mode 100644 index 00000000..a7e459af --- /dev/null +++ b/flask/signals.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" + flask.signals + ~~~~~~~~~~~~~ + + Implements signals based on blinker if available, otherwise + falls silently back to a noop + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True + _signals = Namespace() +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = _fail + del _fail + +# the namespace for code signals. If you are not flask code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# core signals. For usage examples grep the sourcecode or consult +# the API documentation in docs/api.rst as well as docs/signals.rst +template_rendered = _signals.signal('template-rendered') +request_started = _signals.signal('request-started') +request_finished = _signals.signal('request-finished') +got_request_exception = _signals.signal('got-request-exception') diff --git a/flask/templating.py b/flask/templating.py index a6fd0982..41c0d39b 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -11,6 +11,7 @@ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from .globals import _request_ctx_stack +from .signals import template_rendered def _default_template_ctx_processor(): @@ -59,6 +60,13 @@ class _DispatchingJinjaLoader(BaseLoader): return result +def _render(template, context, app): + """Renders the template and fires the signal""" + rv = template.render(context) + template_rendered.send(app, template=template, context=context) + return rv + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -69,7 +77,8 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.get_template(template_name).render(context) + return _render(ctx.app.jinja_env.get_template(template_name), + context, ctx.app) def render_template_string(source, **context): @@ -83,4 +92,5 @@ def render_template_string(source, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.from_string(source).render(context) + return _render(ctx.app.jinja_env.from_string(source), + context, ctx.app) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ccf9b871..43665e1c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1024,6 +1024,83 @@ class SubdomainTestCase(unittest.TestCase): assert rv.data == 'index for mitsuhiko' +class TestSignals(unittest.TestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + with flask.template_rendered.temporarily_connected_to(record, app): + rv = app.test_client().get('/') + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == 'simple_template.html' + assert context['whiskey'] == 42 + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + assert response.data == 'stuff' + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + assert rv.data == 'stuff' + + assert calls == ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal'] + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + with flask.got_request_exception.temporarily_connected_to(record): + assert app.test_client().get('/').status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -1038,6 +1115,8 @@ def suite(): suite.addTest(unittest.makeSuite(SubdomainTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) + if flask.signals_available: + suite.addTest(unittest.makeSuite(TestSignals)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) suite.addTest(unittest.makeSuite(FlaskrTestCase)) return suite diff --git a/tests/templates/simple_template.html b/tests/templates/simple_template.html new file mode 100644 index 00000000..c24612cb --- /dev/null +++ b/tests/templates/simple_template.html @@ -0,0 +1 @@ +