diff --git a/ChangeLog b/ChangeLog index 4ccd1229..c6e5ddbf 100755 --- a/ChangeLog +++ b/ChangeLog @@ -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: diff --git a/cement/core/foundation.py b/cement/core/foundation.py index b0cb0b8c..70538985 100644 --- a/cement/core/foundation.py +++ b/cement/core/foundation.py @@ -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 diff --git a/doc/source/dev/application_design.rst b/doc/source/dev/application_design.rst index fe24811d..06bec742 100644 --- a/doc/source/dev/application_design.rst +++ b/doc/source/dev/application_design.rst @@ -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() diff --git a/doc/source/dev/cleanup.rst b/doc/source/dev/cleanup.rst index cc3c716b..5d3fce7f 100644 --- a/doc/source/dev/cleanup.rst +++ b/doc/source/dev/cleanup.rst @@ -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) diff --git a/doc/source/dev/quickstart.rst b/doc/source/dev/quickstart.rst index a57e125f..b31f3ee1 100644 --- a/doc/source/dev/quickstart.rst +++ b/doc/source/dev/quickstart.rst @@ -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 diff --git a/doc/source/dev/signal_handling.rst b/doc/source/dev/signal_handling.rst index aad8d12c..d23a4ee1 100644 --- a/doc/source/dev/signal_handling.rst +++ b/doc/source/dev/signal_handling.rst @@ -2,43 +2,43 @@ Signal Handling =============== Python provides the `Signal `_ -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 `_ 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) ... diff --git a/tests/core/foundation_tests.py b/tests/core/foundation_tests.py index fb933a0f..7de5c0f8 100644 --- a/tests/core/foundation_tests.py +++ b/tests/core/foundation_tests.py @@ -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