Resolves Issue #269

This commit is contained in:
BJ Dierkes 2014-08-12 15:27:01 -05:00
parent bf4ec46bb9
commit cadd307bb6
7 changed files with 121 additions and 66 deletions

View File

@ -44,6 +44,8 @@ Features:
of controller function.
* :issue:`259` - Add yaml and yaml_configobj config handlers.
* :issue:`262` - Add json and json_configobj config handlers.
* :issue:`269` - Allow app.close() to accept an exit code, and exit with
that code.
Incompatible:

View File

@ -526,12 +526,15 @@ class CementApp(meta.MetaMixin):
for res in hook.run('post_run', self):
pass
def close(self):
def close(self, code=None):
"""
Close the application. This runs the pre_close and post_close hooks
allowing plugins/extensions/etc to 'cleanup' at the end of program
execution.
:param code: An exit status to exit with (`int`). If a code is given
then call `sys.exit(code)`, otherwise `sys.exit()` is not called.
"""
for res in hook.run('pre_close', self):
pass
@ -541,6 +544,11 @@ class CementApp(meta.MetaMixin):
for res in hook.run('post_close', self):
pass
if code is not None:
assert type(code) == int, \
"Invalid exit status code (must be integer)"
sys.exit(code)
def render(self, data, template=None):
"""
This is a simple wrapper around self.output.render() which simply

View File

@ -182,11 +182,9 @@ The below example catches common framework exceptions that Cement might throw:
traceback.print_tb(exc_traceback, limit=20, file=sys.stdout)
print("")
# allow everything to cleanup nicely, so run the close() operations
app.close()
# exit with our return code (always after app.close() is called)
sys.exit(ret)
# allow everything to cleanup nicely, and exit with out custom
# error code
app.close(ret)
if __name__ == '__main__':
main()

View File

@ -3,10 +3,10 @@ Application Cleanup
The concept of 'cleanup' after application run time is nothing new. What
happens during 'cleanup' all depends on the application. This might mean
cleaning up temporary files, removing session data, or removing a PID
(Process ID) file.
cleaning up temporary files, removing session data, or removing a PID
(Process ID) file.
To allow for application cleanup not only within your program, but also
To allow for application cleanup not only within your program, but also
external plugins and extensions, there is the app.close() function that must
be called after app.run() and after program execution.
@ -15,27 +15,39 @@ For example:
.. code-block:: python
from cement.core import foundation
app = foundation.CementApp('helloworld')
try:
try:
app.setup()
app.run()
finally:
app.close()
You will note that we put app.run() within a 'try' block, and app.close() in
a 'finally' block. The important thing to note is that we put app.close()
within a 'finally' block so that regardless of whether an exception is
encountered or not, we always run app.close(). The primary purpose of
app.close() is that is where the 'pre_close' and 'post_close' hooks are run,
You will note that we put `app.run()` within a 'try' block, and app.close() in
a 'finally' block. The important thing to note is that we put `app.close()`
within a 'finally' block so that regardless of whether an exception is
encountered or not, we always run `app.close()`. The primary purpose of
`app.close()` is that is where the `pre_close` and `post_close` hooks are run,
allowing extensions/plugins/etc to cleanup after the program runs.
Also note that you can pass an `exit code` to `app.close()` to tell Cement
to exit the app here as well:
.. code-block:: python
# non-error exit status is generally 0
app.close(0)
# or exit with an error
app.close(1)
Running Cleanup Code
--------------------
Any extension, or plugin, or even the application itself that has 'cleanup'
Any extension, or plugin, or even the application itself that has 'cleanup'
code can do so within the 'pre_close' or 'post_close' hooks. For example:
.. code-block:: python
@ -45,5 +57,5 @@ code can do so within the 'pre_close' or 'post_close' hooks. For example:
def my_cleanup(app):
# do something when app.close() is called
pass
hook.register('pre_close', my_cleanup)

View File

@ -17,7 +17,22 @@ The following outlines creating a sample 'helloworld' application.
finally:
app.close()
Executing the application:
Note that `app.close()` by default does not `exit` the application, but you
can easily do that here also:
.. code-block:: python
# non-error exit status is generally 0
app.close(0)
# or exit with an error
app.close(1)
If an `exit code` is passed to `app.close()` then Cement with call
`sys.exit(code)` at the end of execution.
And running the application looks like:
.. code-block:: text

View File

@ -2,43 +2,43 @@ Signal Handling
===============
Python provides the `Signal <http://docs.python.org/library/signal.html>`_
library allowing developers to catch Unix signals and set handlers for
asynchronous events. For example, the 'SIGTERM' (Terminate) signal is
received when issuing a 'kill' command for a given Unix process. Via the
signal library, we can set a handler (function) callback that will be executed
when that signal is received. Some signals however can not be handled/caught,
such as the SIGKILL signal (kill -9). Please refer to the
library allowing developers to catch Unix signals and set handlers for
asynchronous events. For example, the 'SIGTERM' (Terminate) signal is
received when issuing a 'kill' command for a given Unix process. Via the
signal library, we can set a handler (function) callback that will be executed
when that signal is received. Some signals however can not be handled/caught,
such as the SIGKILL signal (kill -9). Please refer to the
`Signal <http://docs.python.org/library/signal.html>`_ library documentation
for a full understanding of its use and capabilities.
A caveat when setting a signal handler is that only one handler can be defined
for a given signal. Therefore, all handling must be done from a single
callback function. This is a slight roadblock for applications built on
Cement in that many pieces of the framework are broken out into independent
extensions as well as applications that have 3rd party plugins. The trouble
happens when the application, plugins, and framework extensions all need to
perform some action when a signal is caught. This section outlines the
recommended way of handling signals with Cement versus manually setting signal
for a given signal. Therefore, all handling must be done from a single
callback function. This is a slight roadblock for applications built on
Cement in that many pieces of the framework are broken out into independent
extensions as well as applications that have 3rd party plugins. The trouble
happens when the application, plugins, and framework extensions all need to
perform some action when a signal is caught. This section outlines the
recommended way of handling signals with Cement versus manually setting signal
handlers that may.
*Important Note*
It is important to note that it is not necessary to use the Cement mechanisms
for signal handling, what-so-ever. That said, the primary concern of the
framework is that app.close() is called no matter what the situation.
It is important to note that it is not necessary to use the Cement mechanisms
for signal handling, what-so-ever. That said, the primary concern of the
framework is that app.close() is called no matter what the situation.
Therefore, if you decide to disable signal handling all together you *must*
ensure that you at the very least catch signal.SIGTERM and signal.SIGINT with
the ability to call app.close(). You will likely find that it is more
ensure that you at the very least catch signal.SIGTERM and signal.SIGINT with
the ability to call app.close(). You will likely find that it is more
complex than you might think. The reason we put these mechanisms in place is
primarily that we found it was the best way to a) handle a signal, and b) have
access to our 'app' object in order to be able to call 'app.close()' when a
access to our 'app' object in order to be able to call 'app.close()' when a
process is terminated.
Signals Caught by Default
-------------------------
By default Cement catches the signals SIGTERM and SIGINT. When these signals
are caught, Cement raises the exception 'CaughtSignal(signum, frame)'
are caught, Cement raises the exception 'CaughtSignal(signum, frame)'
where 'signum' and 'frame' are the parameters passed to the signal handler.
By raising an exception, we are able to pass runtime back to our applications
main process (within a try/except block) and maintain the ability to access
@ -50,15 +50,15 @@ A basic application using default handling might look like:
import signal
from cement.core import foundation, exc
app = foundation.CementApp('myapp')
app.setup()
try:
app.setup()
app.run()
except exc.CaughtSignal as e:
# do something with e.signum or e.frame (passed from signal library)
if e.signum == signal.SIGTERM:
print("Caught signal SIGTERM...")
# do something to handle signal here
@ -67,12 +67,12 @@ A basic application using default handling might look like:
# do something to handle signal here
finally:
app.close()
As you can see, this provides a very simple means of handling the most common
signals allowing us to still call app.close() after handling the signal. This
is extremely important as 'app.close()' is where the 'pre_close' and
'post_close' hooks are called, allowing the
framework/application/extensions/plugins to all perform any cleanup actions
is extremely important as 'app.close()' is where the 'pre_close' and
'post_close' hooks are called, allowing the
framework/application/extensions/plugins to all perform any cleanup actions
they may need.
Using The Signal Hook
@ -87,8 +87,8 @@ is encountered.
import signal
from cement.core import foundation, exc, hook
app = foundation.CementApp('myapp')
app = foundation.CementApp('myapp')
def my_signal_handler(signum, frame):
# do something with signum/frame
@ -100,7 +100,7 @@ is encountered.
# do something to handle signal here
hook.register('signal', my_signal_handler)
try:
app.setup()
app.run()
@ -111,16 +111,16 @@ is encountered.
The key thing to note here is that the main application itself handles the
exc.CaughtSignal exception, where as using the cement 'signal' hook is
exc.CaughtSignal exception, where as using the cement 'signal' hook is
useful for plugins and extensions to be able to tie into the signal handling
outside of the main application. Both serve the same purpose however the
application object is not available (passed to) the cement 'signal' hook which
limits what can be done within the callback function. For this reason
any extensions or plugins should use the 'pre_close' hook as much as
possible as it is always run when app.close() is called and receives the
limits what can be done within the callback function. For this reason
any extensions or plugins should use the 'pre_close' hook as much as
possible as it is always run when app.close() is called and receives the
app object as one of its parameters.
Configuring Which Signals To Catch
----------------------------------
@ -131,16 +131,16 @@ foundation.CementApp():
import signal
from cement.core import foundation, exc
SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
app = foundation.CementApp('myapp', catch_signals=SIGNALS)
...
What happens is, Cement iterates over the catch_signals list and adds a
What happens is, Cement iterates over the catch_signals list and adds a
generic handler function (the same) for each signal. Because the handler
calls the cement 'signal' hook, and then raises an exception which both pass the
'signum' and 'frame' parameters, you are able to handle the logic elsewhere
calls the cement 'signal' hook, and then raises an exception which both pass the
'signum' and 'frame' parameters, you are able to handle the logic elsewhere
rather than assigning a unique callback function for every signal.
What If I Don't Like Your Signal Handler Callback?
@ -161,12 +161,12 @@ callback.
def my_signal_handler(signum, frame):
# do something with signum/frame
print 'Caught signal %s' % signum
# execute the cement signal hook
for res in hook.run('signal', signum, frame):
pass
pass
app = foundation.CementApp('myapp',
app = foundation.CementApp('myapp',
catch_signals=SIGNALS,
signal_handler=my_signal_handler)
...

View File

@ -247,3 +247,23 @@ class FoundationTestCase(test.CementCoreTestCase):
last_data, last_output = self.app.get_last_rendered()
self.eq({'foo':'bar'}, last_data)
self.eq(output_text, last_output)
@test.raises(SystemExit)
def test_close_with_code(self):
self.app.setup()
self.app.run()
try:
self.app.close(114)
except SystemExit as e:
self.eq(e.code, 114)
raise
@test.raises(AssertionError)
def test_close_with_bad_code(self):
self.app.setup()
self.app.run()
try:
self.app.close('Not An Int')
except AssertionError as e:
self.eq(e.args[0], "Invalid exit status code (must be integer)")
raise