Browse Source

Add a concept of application teardown

The main purpose of this is to have a single place where extensions
can register their cleanup functions. If you are creating many app
instances for testing purposes, you need to keep track of all
individual extensions and release the resources from all of them.
Allowing them to register a cleanup function inside init_app would
hide the complexity and app.close() would be the only thing tests
need to care about.
pull/2477/head
Lukáš Lalinský 7 years ago
parent
commit
129304cef7
  1. 15
      docs/api.rst
  2. 3
      flask/__init__.py
  3. 67
      flask/app.py
  4. 1
      flask/signals.py
  5. 13
      tests/test_basic.py
  6. 16
      tests/test_signals.py

15
docs/api.rst

@ -622,6 +622,21 @@ The following signals exist in Flask:
.. versionadded:: 0.10 .. versionadded:: 0.10
.. data:: app_tearing_down
This signal is sent when the application is being destroyed.
This is mostly useful for testing when you might be creating
a lot of application instances and want your code to be handle
some cleanup in between tests.
Example subscriber::
def close_db_connections(sender, **extra):
db.close()
from flask import app_tearing_down
app_tearing_down.connect(close_db_connections, app)
.. class:: signals.Namespace .. class:: signals.Namespace
An alias for :class:`blinker.base.Namespace` if blinker is available, An alias for :class:`blinker.base.Namespace` if blinker is available,

3
flask/__init__.py

@ -34,7 +34,8 @@ from .templating import render_template, render_template_string
from .signals import signals_available, template_rendered, request_started, \ from .signals import signals_available, template_rendered, request_started, \
request_finished, got_request_exception, request_tearing_down, \ request_finished, got_request_exception, request_tearing_down, \
appcontext_tearing_down, appcontext_pushed, \ appcontext_tearing_down, appcontext_pushed, \
appcontext_popped, message_flashed, before_render_template appcontext_popped, message_flashed, before_render_template, \
app_tearing_down
# We're not exposing the actual json module but a convenient wrapper around # We're not exposing the actual json module but a convenient wrapper around
# it. # it.

67
flask/app.py

@ -31,8 +31,9 @@ from .helpers import _PackageBoundObject, \
get_flashed_messages, locked_cached_property, url_for get_flashed_messages, locked_cached_property, url_for
from .logging import create_logger from .logging import create_logger
from .sessions import SecureCookieSessionInterface from .sessions import SecureCookieSessionInterface
from .signals import appcontext_tearing_down, got_request_exception, \ from .signals import app_tearing_down, appcontext_tearing_down, \
request_finished, request_started, request_tearing_down got_request_exception, request_finished, request_started, \
request_tearing_down
from .templating import DispatchingJinjaLoader, Environment, \ from .templating import DispatchingJinjaLoader, Environment, \
_default_template_ctx_processor _default_template_ctx_processor
from .wrappers import Request, Response from .wrappers import Request, Response
@ -449,6 +450,15 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.9 #: .. versionadded:: 0.9
self.teardown_appcontext_funcs = [] self.teardown_appcontext_funcs = []
#: A list of functions that are called when the application
#: is destroyed. Application can be only destroyed explicitly
#: by calling the :meth:`close` function. This is mostly useful
#: for testing when you might be creating a lot of application
#: instances and want extensions linked to them to be cleaned up.
#:
#: .. versionadded:: FIXME
self.teardown_app_funcs = []
#: A dictionary with lists of functions that are called before the #: A dictionary with lists of functions that are called before the
#: :attr:`before_request_funcs` functions. The key of the dictionary is #: :attr:`before_request_funcs` functions. The key of the dictionary is
#: the name of the blueprint this function is active for, or ``None`` #: the name of the blueprint this function is active for, or ``None``
@ -534,6 +544,9 @@ class Flask(_PackageBoundObject):
self._got_first_request = False self._got_first_request = False
self._before_request_lock = Lock() self._before_request_lock = Lock()
self._closed = False
self._closed_lock = Lock()
# Add a static route using the provided static_url_path, static_host, # Add a static route using the provided static_url_path, static_host,
# and static_folder if there is a configured static_folder. # and static_folder if there is a configured static_folder.
# Note we do this without checking if static_folder exists. # Note we do this without checking if static_folder exists.
@ -1494,6 +1507,47 @@ class Flask(_PackageBoundObject):
self.teardown_appcontext_funcs.append(f) self.teardown_appcontext_funcs.append(f)
return f return f
@setupmethod
def teardown_app(self, f):
"""Register a function that is called when the application
is destroyed. Application can be only destroyed explicitly
by calling the :meth:`close` function. This is mostly useful
for testing when you might be creating a lot of application
instances and want extensions linked to them to be cleaned up.
If you are developing an extension that keeps a pool of
connections to some service, you might want to register a
teardown function to cleanly close all connections from
the pool.
The return values of teardown functions are ignored.
.. versionadded:: FIXME
"""
self.teardown_app_funcs.append(f)
return f
def close(self):
"""Close the application and trigger application teardown
handler functions. This is mostly useful for testing
when you might be creating a lot of application instances
and want extensions linked to them to be cleaned up.
You might also use a hook in your framework to call this
function to gracefully shut down the server.
Example using Gunicorn server hooks::
def worker_exit(server, worker):
app.close()
.. versionadded:: FIXME
"""
with self._closed_lock:
if not self._closed:
self.do_teardown_app()
self._closed = True
@setupmethod @setupmethod
def context_processor(self, f): def context_processor(self, f):
"""Registers a template context processor function.""" """Registers a template context processor function."""
@ -2063,6 +2117,15 @@ class Flask(_PackageBoundObject):
func(exc) func(exc)
appcontext_tearing_down.send(self, exc=exc) appcontext_tearing_down.send(self, exc=exc)
def do_teardown_app(self):
"""Called when the application is closed by calling :meth:`close`.
.. versionadded:: FIXME
"""
for func in reversed(self.teardown_app_funcs):
func()
app_tearing_down.send(self)
def app_context(self): def app_context(self):
"""Binds the application only. For as long as the application is bound """Binds the application only. For as long as the application is bound
to the current context the :data:`flask.current_app` points to that to the current context the :data:`flask.current_app` points to that

1
flask/signals.py

@ -53,4 +53,5 @@ got_request_exception = _signals.signal('got-request-exception')
appcontext_tearing_down = _signals.signal('appcontext-tearing-down') appcontext_tearing_down = _signals.signal('appcontext-tearing-down')
appcontext_pushed = _signals.signal('appcontext-pushed') appcontext_pushed = _signals.signal('appcontext-pushed')
appcontext_popped = _signals.signal('appcontext-popped') appcontext_popped = _signals.signal('appcontext-popped')
app_tearing_down = _signals.signal('app-tearing-down')
message_flashed = _signals.signal('message-flashed') message_flashed = _signals.signal('message-flashed')

13
tests/test_basic.py

@ -774,6 +774,19 @@ def test_teardown_request_handler_error(app, client):
assert len(called) == 2 assert len(called) == 2
def test_teardown_app_handler(app, client):
called = []
@app.teardown_app
def teardown_app():
called.append(True)
return "Ignored"
app.close()
app.close()
assert len(called) == 1
def test_before_after_request_order(app, client): def test_before_after_request_order(app, client):
called = [] called = []

16
tests/test_signals.py

@ -201,3 +201,19 @@ def test_appcontext_tearing_down_signal():
assert recorded == [('tear_down', {'exc': None})] assert recorded == [('tear_down', {'exc': None})]
finally: finally:
flask.appcontext_tearing_down.disconnect(record_teardown, app) flask.appcontext_tearing_down.disconnect(record_teardown, app)
def test_app_tearing_down_signal():
app = flask.Flask(__name__)
recorded = []
def record_teardown(sender, **kwargs):
recorded.append(('tear_down', kwargs))
flask.app_tearing_down.connect(record_teardown, app)
try:
app.close()
app.close()
assert recorded == [('tear_down', {})]
finally:
flask.app_tearing_down.disconnect(record_teardown, app)

Loading…
Cancel
Save