mirror of
https://github.com/datafolklabs/cement.git
synced 2026-02-06 15:56:47 +00:00
Resolves Issue #269
This commit is contained in:
parent
bf4ec46bb9
commit
cadd307bb6
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
...
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user