Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added support for signals

  • Loading branch information...
commit e0712b47c6e5bc98f2afd8b0a82cc5005dc58722 1 parent a59dfe4
@mitsuhiko authored
View
5 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
-------------
View
5 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
View
51 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
View
3  docs/conf.py
@@ -245,7 +245,8 @@
'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'
View
4 flask/__init__.py
@@ -24,6 +24,10 @@
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
View
4 flask/app.py
@@ -32,6 +32,7 @@
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 @@ def handle_exception(self, e):
.. versionadded: 0.3
"""
+ got_request_exception.send(self, exception=e)
handler = self.error_handlers.get(500)
if self.debug:
raise
@@ -791,6 +793,7 @@ def wsgi_app(self, environ, start_response):
"""
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 @@ def wsgi_app(self, environ, start_response):
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):
View
50 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')
View
14 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 @@ def list_templates(self):
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)
View
79 tests/flask_tests.py
@@ -1024,6 +1024,83 @@ def index(user):
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
View
1  tests/templates/simple_template.html
@@ -0,0 +1 @@
+<h1>{{ whiskey }}</h1>
Please sign in to comment.
Something went wrong with that request. Please try again.