Resolves Issue #281

This commit is contained in:
BJ Dierkes 2014-09-29 21:01:46 -05:00
parent 42c6298f51
commit c71ccfe42f
26 changed files with 453 additions and 520 deletions

View File

@ -18,7 +18,7 @@ See the :ref:`upgrading` section for more information related to
any incompatible changes, and how to update your application to fix them.
2.5.1 - DEVELOPMENT (will be released as 2.5./dev or 2.6.0/stable)
2.5.1 - DEVELOPMENT (will be released as 2.5.2/dev or 2.6.0/stable)
------------------------------------------------------------------------------
This is a branch off of the 2.4.x stable code base. Maintenance releases for
@ -31,7 +31,7 @@ Bugs:
Features:
* None
* :issue:`281` - Added support for Python `with` operation
2.4.0 - Wed Sep 17, 2014

View File

@ -125,21 +125,32 @@ class CementApp(meta.MetaMixin):
.. code-block:: python
from cement.core import foundation
app = foundation.CementApp('helloworld')
try:
app.setup()
from cement.core.foundation import CementApp
with CementApp('helloworld') as app:
app.run()
finally:
app.close()
Alternatively, the above could be written as:
.. code-block:: python
from cement.core.foundation import CementApp
app = foundation.CementApp('helloworld')
app.setup()
app.run()
app.close()
A more advanced example looks like:
.. code-block:: python
from cement.core import foundation, controller
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
class MyController(controller.CementBaseController):
class MyController(CementBaseController):
class Meta:
label = 'base'
arguments = [
@ -150,22 +161,18 @@ class CementApp(meta.MetaMixin):
some_config_param='some_value',
)
@controller.expose(help='This is the default command', hide=True)
@expose(help='This is the default command', hide=True)
def default(self):
print('Hello World')
class MyApp(foundation.CementApp):
class MyApp(CementApp):
class Meta:
label = 'helloworld'
extensions = ['daemon','json',]
base_controller = MyController
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
"""
class Meta:
@ -187,6 +194,13 @@ class CementApp(meta.MetaMixin):
Used internally, and should not be used by developers. This is set
to `True` if `--debug` is passed at command line."""
exit_on_close = True
"""
Whether or not to call ``sys.exit()`` when ``close()`` is called.
Generally only used for testing to avoid having to catch
``SystemExit`` three thousand times.
"""
config_files = None
"""
List of config files to parse.
@ -561,6 +575,7 @@ class CementApp(meta.MetaMixin):
self._loaded_bootstrap = None
self._parsed_args = None
self._last_rendered = None
self.exit_code = 0
self.ext = None
self.config = None
@ -698,14 +713,23 @@ class CementApp(meta.MetaMixin):
for res in hook.run('post_run', self):
pass
def __enter__(self):
self.setup()
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
# only close the app if there are no unhandled exceptions
if exc_type is None:
self.close()
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.
:param code: An exit code to exit with (`int`), if `None` is passed
then exit with whatever `self.exit_code` is currently set to.
"""
for res in hook.run('pre_close', self):
@ -719,7 +743,10 @@ class CementApp(meta.MetaMixin):
if code is not None:
assert type(code) == int, \
"Invalid exit status code (must be integer)"
sys.exit(code)
self.exit_code = code
if self._meta.exit_on_close is True:
sys.exit(self.exit_code)
def render(self, data, template=None, out=sys.stdout):
"""

View File

@ -77,12 +77,14 @@ trigger daemon functionality before app.run() is called.
.. code-block:: python
from time import sleep
from cement.core import foundation
from cement.core.foundation import CementApp
app = foundation.CementApp('myapp', extensions=['daemon'])
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['daemon']
try:
app.setup()
with MyApp() as app:
app.daemonize()
app.run()
@ -91,11 +93,10 @@ trigger daemon functionality before app.run() is called.
count = count + 1
print('Iteration: %s' % count)
sleep(10)
finally:
app.close()
An alternative to the above is to put app.daemonize() within a framework hook:
An alternative to the above is to put the ``daemonize()`` call within a
framework hook:
.. code-block:: python
@ -114,11 +115,16 @@ rather than the entire parent application. For example:
from cement.core import foundation, controller, handler
class MyAppBaseController(controller.CementBaseController):
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
class MyBaseController(CementBaseController):
class Meta:
label = 'base'
@controller.expose(help="run the daemon command.")
@expose(help="run the daemon command.")
def run_forever(self):
from time import sleep
self.app.daemonize()
@ -129,17 +135,15 @@ rather than the entire parent application. For example:
print(count)
sleep(10)
app = foundation.CementApp('myapp',
extensions=['daemon'],
base_controller=MyAppBaseController,
)
class MyApp(CementApp):
class Meta:
label = 'myapp'
base_controller = MyBaseController
extensions = ['daemon']
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
By default, even after app.daemonize() is called... the application will
continue to run in the foreground, but will still manage the pid and

View File

@ -86,9 +86,8 @@ class MemcachedCacheHandler(cache.CementCacheHandler):
extensions = ['memcached']
cache_handler = 'memcached'
app = MyApp()
try:
app.setup()
with MyApp() as app:
# Run the app
app.run()
# Set a cached value
@ -103,9 +102,6 @@ class MemcachedCacheHandler(cache.CementCacheHandler):
# Delete the entire cache
app.cache.purge()
finally:
app.close()
"""
class Meta:
interface = cache.ICache

View File

@ -25,6 +25,7 @@ class TestApp(foundation.CementApp):
argv = []
base_controller = None
arguments = []
exit_on_close = False
class CementTestCase(unittest.TestCase):

View File

@ -15,7 +15,7 @@ out of a single file. The following is a minimal example that creates a
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
@ -40,27 +40,16 @@ out of a single file. The following is a minimal example that creates a
def cmd2(self):
print("Inside BaseController.cmd2()")
class MyApp(foundation.CementApp):
class MyApp(CementApp):
class Meta:
label = 'myapp'
base_controller = BaseController
def main():
# create the app - before the try/except/finally block
app = MyApp()
try:
# setup the app
app.setup()
# run the app
with MyApp() as app:
app.run()
finally:
# close the app
app.close()
if __name__ == '__main__':
main()
@ -87,7 +76,7 @@ application layout, and is a great starting point for anyone new to Cement.
The primary detail about how to layout your code is this: All CLI/Cement
related code should live separate from the "core logic" of your application.
Most likely, you will have some code that is re-usable by other people and you
do not want to mix this with your Cement code, becuase that will rely on
do not want to mix this with your Cement code, because that will rely on
Cement being loaded to function properly (like it is when called from command
line).
@ -130,7 +119,8 @@ The following expands on the above to give an example of how you might handle
exceptions at the highest level (wrapped around the app object). It is very
well known that exception handling should happen as close to the source of the
exception as possible, and you should do that. However at the top level
(generally in your ``main.py`` or similar) you want to handle the exception so
(generally in your ``main.py`` or similar) you want to handle certain
exceptions (such as argument errors, or user interaction related errors) so
that they are presented nicely to the user. End-users don't like stack
traces!
@ -140,57 +130,40 @@ but you could also catch your own application specific exception this way:
.. code-block:: python
import sys
from cement.core.exc import FrameworkError, CaughtSignal
from cement.core.foundation import CementApp
from cement.core.exc import FrameworkError, CaughtSignal
def main():
# create the app
app = CementApp('myapp')
with CementApp('myapp') as app:
try:
app.run()
# default our exit status (return code) to 0 (non-error)
ret = 0
except CaughtSignal as e:
# determine what the signal is, and do something with it?
from signal import SIGINT, SIGABRT
try:
# setup the app
app.setup()
if e.signum == SIGINT:
# do something... maybe change the exit code?
app.exit_code = 110
elif e.signum == SIGABRT:
# do something else...
app.exit_code = 111
# run the app
app.run()
except FrameworkError as e:
# do something when a framework error happens
print("FrameworkError => %s" % e)
except CaughtSignal as e:
# maybe determine what the signal is, and do something with it?
from signal import SIGINT, SIGABRT
# and maybe set the exit code to something unique as well
app.exit_code = 300
if e.signum == SIGINT:
# do something... maybe change the return status
ret = 110
elif e.signum == SIGABRT:
# do something else...
ret = 111
except FrameworkError as e:
# do something when a framework error happens
print("FrameworkError => %s" % e)
# set the exit status to 1 (error)
ret = 1
finally:
# if --debug was passed, we want to see a full stack trace
if app.debug:
import traceback
print("")
print('TRACEBACK:')
print('-' * 77)
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback, limit=20, file=sys.stdout)
print("")
# allow everything to cleanup nicely, and exit with out custom
# error code
app.close(ret)
finally:
# Maybe we want to see a full-stack trace for the above
# exceptions, but only if --debug was passed?
if app.debug:
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

View File

@ -41,15 +41,9 @@ Example:
.. code-block:: python
from cement.core import foundation
# create the application
app = foundation.CementApp('myapp')
try:
# setup the application
app.setup()
from cement.core.foundation import CementApp
with CementApp('myapp') as app:
# run the application
app.run()
@ -65,5 +59,3 @@ Example:
# delete the entire cache
app.cache.purge()
finally:
app.close()

View File

@ -3,7 +3,7 @@ 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
closing and deleting temporary files, removing session data, or deleting a PID
(Process ID) file.
To allow for application cleanup not only within your program, but also
@ -15,33 +15,52 @@ For example:
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
app = foundation.CementApp('helloworld')
try:
app.setup()
app.run()
finally:
app.close()
app = CementApp('helloworld')
app.setup()
app.run()
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
regardless of whether an exception is encountered or not, we always run
``app.close()``. This is where the ``pre_close`` and ``post_close`` hooks are
run, allowing extensions/plugins/etc to cleanup after the program runs.
Calling ``app.close()`` ensures that the ``pre_close`` and ``post_close``
framework hooks are run, allowing extensions/plugins/etc to cleanup after the
program runs.
Also note that you can optionally pass an exit code to ``app.close()`` to tell
Cement to exit the app here as well:
Note that when using the Python ``with`` operator, the ``setup()`` and
``close()`` methods are automatically called. For example, the following is
exactly the same as the above example:
.. code-block:: python
# non-error exit status is generally 0
app.close(0)
from cement.core.foundation import CementApp
# or exit with an error
app.close(1)
with CementApp('helloworld') as app:
app.run()
Also note that you can optionally set an exit code to alter that status in
which your application exits:
.. code-block:: python
app = CementApp('helloworld')
app.setup()
app.run()
app.close(27)
Or Alternatively:
.. code-block:: python
with CementApp('helloworld') as app:
app.run()
app.exit_code = 123
The default exit code is ``0``, however any uncaught exceptions will cause the
application to exit with a code of ``1`` (error).
Running Cleanup Code

View File

@ -265,37 +265,28 @@ happen in *all* config sections if enabled:
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
from cement.utils.misc import init_defaults
defaults = init_defaults('myapp')
defaults['myapp']['foo'] = 'bar'
app = foundation.CementApp(
label='myapp',
config_defaults=defaults,
arguments_override_config=True,
)
class MyApp(CementApp):
class Meta:
label = 'myapp'
config_defaults = defaults
arguments_override_config = True
try:
# First setup the application
app.setup()
# Add arguments
with MyApp() as app:
app.args.add_argument('--foo', action='store', dest='foo')
# Run the application (this parses command line, among other things)
app.run()
finally:
# close the application
app.close()
With ``arguments_override_config`` enabled, running the application and
With ``arguments_override_config`` enabled, running the above application and
passing the ``--foo=some_value`` option will override the ``foo`` setting
under the ``[myapp]`` section as well as any other section that has a matching
key.
under a ``[myapp]`` configuration section as well as any other section that
has a matching ``foo`` key.
Configuration Options Versus Meta Options

View File

@ -70,18 +70,15 @@ handle command dispatch and rapid development.
def command2(self):
self.app.log.info("Inside base.command2 function.")
# create an application
app = CementApp('example', base_controller=MyAppBaseController)
try:
# setup the application
app.setup()
class MyApp(CementApp):
class Meta:
label = 'example'
base_controller = MyAppBaseController
# run the application
with MyApp() as app:
app.run()
finally:
# close the application
app.close()
As you can see, we're able to build out the core functionality of our app

View File

@ -60,22 +60,20 @@ The following example shows how to alter these settings for your application:
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
from cement.core.ext import CementExtensionHandler
class MyExtensionHandler(CementExtensionHandler):
pass
app = foundation.CementApp('myapp',
extension_handler = MyExtensionHandler
extensions = ['myapp.ext.ext_something_fancy']
)
class MyApp(CementApp):
class Meta:
label = 'myapp'
extension_handler = MyExtensionHandler
extensions = ['myapp.ext.ext_something_fancy']
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
Creating an Extension
@ -145,17 +143,15 @@ in ``myapp/ext/ext_something_fancy.py``:
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
app = foundation.CementApp('myapp',
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['myapp.ext.ext_something_fancy']
)
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
Note that Cement provides a shortcut for Cement extensions. For example, the
@ -163,14 +159,14 @@ following:
.. code-block:: python
app = foundation.CementApp('myapp', extensions=['json', 'daemon'])
CementApp('myapp', extensions=['json', 'daemon'])
Is equivalent to:
.. code-block:: python
app = foundation.CementApp('myapp',
CementApp('myapp',
extensions=[
'cement.ext.ext_json',
'cement.ext.ext_daemon',
@ -211,12 +207,8 @@ configuration files are loaded).
]
def main():
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
if __name__ == '__main__':
main()

View File

@ -22,60 +22,47 @@ API Reference:
Defining a Hook
---------------
A hook can be defined as follows:
A hook can be defined anywhere, however it is generally recommended to define
the hook as early as possible, and within the application setup if possible:
.. code-block:: python
from cement.core import foundation, hook
from cement.core.foundation import CementApp
from cement.core import hook
# First create the application
app = foundation.CementApp('myapp')
class MyApp(CementApp):
class Meta:
label = 'myapp'
# Then define any application hooks
hook.define('my_example_hook')
def setup(self):
# always run core setup first
super(MyApp, self).setup()
# Setup the application
app.setup()
# define application hooks here
hook.define('my_example_hook')
Hooks should be defined as early on in the bootstrap process as possible,
after the CementApp() is instantiated, but before 'app.setup()' is called.
Registering Functions to a Hook
-------------------------------
A hook is just an identifier, but the functions registered to that hook are
what get run when the hook is called. Registering a hook function should also
be done early on in the bootstrap process (before the hook is called
obviously).
be done early on in the bootstrap process, any time after the application has
been created, after the hook is defined, and before the hook is run. Note
that every hook is different, and therefore should be clearly documented by
the 'owner' of the hook (application developer, plugin developer, etc).
.. code-block:: python
from cement.core import foundation, hook
from cement.core import hook
# First create the application
app = foundation.CementApp('myapp')
# Then define any application hooks
hook.define('my_example_hook')
def my_func():
# do something
something = "The result of my hook."
return something
def my_other_func():
# do something
def my_func(app):
pass
# Register any hook functions. In the real world, this would likely be
# done elsewhere in the application such as in plugins.
hook.register('my_example_hook', my_func)
hook.register('my_example_hook', my_other_func)
# Setup the application
app.setup()
with CementApp('myapp') as app:
hook.register('my_example_hook', my_func)
app.run()
What you return depends on what the developer defining the hook is expecting.
@ -108,10 +95,11 @@ That said, this is how you run a hook:
# do something with res?
pass
As you can see we iterate over the hook, rather than just calling
``hook.run()``. This is necessary because ``hook.run()`` yields the results
from each hook as they are run. Hooks can be run anywhere *after* the hook is
defined, and hooks are registered to that hook.
from each hook function as they are run. Hooks can be run anywhere *after*
the hook is defined, and hooks are registered to that hook.
Controlling Hook Run Order
@ -127,62 +115,55 @@ a custom application hook:
.. code-block:: python
from cement.core import foundation, controller, handler, hook
from cement.core.foundation import CementApp
from cement.core import handler, hook
# define an application base controller
class MyAppBaseController(controller.CementBaseController):
class MyApp(CementApp):
class Meta:
interface = controller.IController
label = 'base'
description = "My Application does amazing things!"
label = 'myapp'
config_defaults = {}
arguments = []
def setup(self):
# always run core setup
super(MyApp, self).setup()
@controller.expose(hide=True, aliases=['run'])
def default(self):
for res in hook.run('myapp_default_command_hook', self.app):
pass
# define hooks in setup
hook.define('my_hook')
# create an application
app = foundation.CementApp('myapp', base_controller=MyAppBaseController)
# define a hook
hook.define('my_hook')
# the following are the function that will run when ``my_hook`` is called
def func1(app):
print 'Inside func1 of %s.' % app.name
print 'Inside hook func1'
def func2(app):
print 'Inside func2 of %s.' % app.name
print 'Inside hook func2'
def func3(app):
print 'Inside func3 of %s.' % app.name
print 'Inside hook func3'
# register some hook functions
hook.register('my_hook', func1, weight=0)
hook.register('my_hook', func2, weight=100)
hook.register('my_hook', func3, weight=-99)
try:
# setup the application
app.setup()
with MyApp() as app:
# register all hook functions *after* the hook is defined (setup) but
# also *before* the hook is called (different for every hook)
hook.register('my_hook', func1, weight=0)
hook.register('my_hook', func2, weight=100)
hook.register('my_hook', func3, weight=-99)
# run the application
app.run()
finally:
# close the application
app.close()
# run our custom hook
for res in hook.run('my_hook', app):
pass
And the result is:
.. code-block:: text
$ python test.py
Inside func3 of myapp.
Inside func1 of myapp.
Inside func2 of myapp.
$ python my_hook_test.py
Inside hook func3
Inside hook func1
Inside hook func2
As you can see, it doesnt matter what order we register the hook, the

View File

@ -377,26 +377,17 @@ already setup by Cement, but we're putting it here for clarity:
)
# create the app
app = MyApp()
try:
# setup the app
app.setup()
with MyApp() as app:
# run the application
app.run()
# define some data for the output handler
data = dict(foo='bar')
# run the app
app.run()
# render something using our output handlers, using mustache by
# default which use the default.m template
# render something using out output handlers, using mustache by
# default which will use the default.m templae
app.render(data, 'default.m')
finally:
# close the app
app.close()
Note what we see at command line:

View File

@ -31,10 +31,7 @@ Example Usage
class Meta:
label = 'myapp'
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
# send an email message
@ -46,9 +43,6 @@ Example Usage
bcc=['boss@example.com']
)
finally:
app.close()
Note that the default mail handler simply prints messages to the screen, and
does not actually send anything. You can override this pretty easily without

View File

@ -132,12 +132,8 @@ the Mustache templating langugage, as well as Json output handling.
output_handler = 'mustache'
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
**/usr/lib/myapp/templates/default.m**

View File

@ -206,12 +206,8 @@ same if not defined:
def main():
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
if __name__ == '__main__':
main()

View File

@ -1,38 +1,32 @@
Quick Start
===========
The following outlines creating a sample 'helloworld' application.
The following creates and runs a sample 'helloworld' application.
*helloworld.py*
.. code-block:: python
from cement.core import foundation
from cement.core.foundation import CementApp
app = foundation.CementApp('helloworld')
try:
app.setup()
with CementApp('helloworld') as app:
app.run()
print('Hello World')
finally:
app.close()
Note that `app.close()` by default does not `exit` the application, but you
can easily do that here also:
The above is equivalent to (should you need more control over setup and
closing an application):
.. code-block:: python
# non-error exit status is generally 0
app.close(0)
from cement.core.foundation import CementApp
# or exit with an error
app.close(1)
app = CementApp('helloworld')
app.setup()
app.run()
app.close()
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:
Running the application looks like:
.. code-block:: text
@ -125,43 +119,44 @@ config creation, and logging.
.. code-block:: python
from cement.core import foundation, hook
from cement.core.foundation import CementApp
from cement.core import hook
from cement.utils.misc import init_defaults
# set default config options
# define our default configuration options
defaults = init_defaults('myapp')
defaults['myapp']['debug'] = False
defaults['myapp']['foo'] = 'bar'
defaults['myapp']['some_param'] = 'some value'
# create an application
app = foundation.CementApp('myapp', config_defaults=defaults)
# define any hook functions here
def my_cleanup_hook(app):
pass
# register any framework hook functions after app creation, and before
# app.setup()
def my_hook(app):
assert 'foo' in app.config.keys('myapp')
# define the application class
class MyApp(CementApp):
class Meta:
label = 'myapp'
config_defaults = defaults
extensions = ['daemon', memcached', 'json', 'yaml']
hook.register('post_setup', my_hook)
with MyApp() as app:
# register framework or custom application hooks
hook.register('pre_close', my_cleanup_hook)
try:
# setup the application
app.setup()
# add arguments
# add arguments to the parser
app.args.add_argument('-f', '--foo', action='store', metavar='STR',
help='the notorious foo option')
# log stuff
app.log.debug("About to run my myapp application!")
# run the application
app.run()
# add application logic
# continue with additional application logic
if app.pargs.foo:
app.log.info("Received the 'foo' option with value '%s'." % app.pargs.foo)
else:
app.log.warn("Did not receive a value for 'foo' option.")
app.log.info("Received option: foo => %s" % app.pargs.foo)
finally:
# close the application
app.close()
And execution:
@ -177,7 +172,7 @@ And execution:
-f STR, --foo STR the notorious foo option
$ python myapp.py --foo=bar
INFO: Received the 'foo' option with value 'bar'.
INFO: Received option: foo => bar
Diving Right In
@ -190,53 +185,52 @@ handle command dispatch and rapid development.
.. code-block:: python
from cement.core import backend, foundation, controller, handler
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
# define an application base controller
class MyAppBaseController(controller.CementBaseController):
class MyBaseController(CementBaseController):
class Meta:
label = 'base'
description = "My Application does amazing things!"
config_defaults = dict(
foo='bar',
some_other_option='my default value',
)
arguments = [
(['-f', '--foo'], dict(action='store', help='the notorious foo option')),
(['-C'], dict(action='store_true', help='the big C option'))
( ['-f', '--foo'],
dict(action='store', help='the notorious foo option') ),
( ['-C'],
dict(action='store_true', help='the big C option') ),
]
@controller.expose(hide=True, aliases=['run'])
@expose(hide=True)
def default(self):
self.app.log.info('Inside base.default function.')
self.app.log.info('Inside MyBaseController.default()')
if self.app.pargs.foo:
self.app.log.info("Recieved option 'foo' with value '%s'." % \
self.pargs.foo)
print("Recieved option: foo => %s" % self.app.pargs.foo)
@controller.expose(help="this command does relatively nothing useful.")
@expose(help="this command does relatively nothing useful")
def command1(self):
self.app.log.info("Inside base.command1 function.")
self.app.log.info("Inside MyBaseController.command1()")
@controller.expose(aliases=['cmd2'], help="more of nothing.")
@expose(aliases=['cmd2'], help="more of nothing")
def command2(self):
self.app.log.info("Inside base.command2 function.")
self.app.log.info("Inside MyBaseController.command2()")
# define a second controller
class MySecondController(controller.CementBaseController):
class MySecondController(CementBaseController):
class Meta:
label = 'secondary'
label = 'second'
stacked_on = 'base'
@controller.expose(help='this is some command', aliases=['some-cmd'])
def some_other_command(self):
pass
@expose(help='this is some command', aliases=['some-cmd'])
def second_cmd1(self):
self.app.log.info("Inside MySecondController.second_cmd1")
class MyApp(foundation.CementApp):
class MyApp(CementApp):
class Meta:
label = 'helloworld'
base_controller = MyAppBaseController
label = 'myapp'
base_controller = MyBaseController
# create the app
app = MyApp()
@ -244,15 +238,15 @@ handle command dispatch and rapid development.
# Register any handlers that aren't passed directly to CementApp
handler.register(MySecondController)
try:
# setup the application
app.setup()
# setup the application
app.setup()
# run the application
app.run()
# close the app
app.close()
# run the application
app.run()
finally:
# close the app
app.close()
As you can see, we're able to build out the core functionality of our app such
as arguments and sub-commands via controller classes.
@ -269,10 +263,10 @@ Lets see what this looks like:
commands:
command1
this command does relatively nothing useful.
this command does relatively nothing useful
command2 (aliases: cmd2)
more of nothing.
more of nothing
some-other-command (aliases: some-cmd)
this is some command
@ -284,11 +278,14 @@ Lets see what this looks like:
-f FOO, --foo FOO the notorious foo option
-C the big C option
$ python myapp.py
INFO: Inside MyBaseController.default()
$ python myapp.py command1
INFO: Inside base.command1 function.
INFO: Inside MyBaseController.command1()
$ python myapp.py command2
INFO: Inside base.command2 function.
INFO: Inside MyBaseController.command2()
$ python myapp.py cmd2
INFO: Inside base.command2 function.
$ python myapp.py second-cmd1
INFO: Inside MySecondController.second_cmd1()

View File

@ -49,76 +49,61 @@ A basic application using default handling might look like:
.. code-block:: python
import signal
from cement.core import foundation, exc
from cement.core.foundation import CementApp
from cement.core.exc import CaughtSignal
app = foundation.CementApp('myapp')
with CementApp('myapp') as app:
try:
app.run()
except CaughtSignal as e:
# do something with e.signum or e.frame (passed from signal)
if e.signum == signal.SIGTERM:
print("Caught SIGTERM...")
elif e.signum == signal.SIGINT:
print("Caught SIGINT...")
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
elif e.signum == signal.SIGINT:
print("Caught signal SIGINT...")
# do something to handle signal here
finally:
app.close()
The above provides a very simple means of handling the most common
signals, which in turns allowes our application to "exit clean" by running
``app.close()`` and any ``pre_close`` or ``post_close`` hooks. If we don't
catch the signals, then the exceptions will be unhandled and the application
will not exit clean.
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
they may need.
Using The Signal Hook
---------------------
An alternative way of adding multiple callbacks to a signal handler is by
using the cement signal hook. This hook is called anytime a handled signal
using the Cement signal hook. This hook is called anytime a handled signal
is encountered.
.. code-block:: python
import signal
from cement.core import foundation, exc, hook
app = foundation.CementApp('myapp')
from cement.core.foundation import CementApp
from cement.core.exc import CaughtSignal
from cement.core import hook
def my_signal_handler(signum, frame):
# do something with signum/frame
if signum == signal.SIGTERM:
print("Caught signal SIGTERM...")
# do something to handle signal here
print("Caught SIGTERM...")
elif signum == signal.SIGINT:
print("Caught signal SIGINT...")
# do something to handle signal here
print("Caught SIGINT...")
hook.register('signal', my_signal_handler)
try:
app.setup()
with CementApp('myapp') as app:
hook.register('signal', my_signal_handler)
app.run()
except exc.CaughtSignal as e:
pass
finally:
app.close()
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
``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
app object as one of its parameters.
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
``app`` object as one of its parameters.
Configuring Which Signals To Catch
@ -130,19 +115,21 @@ foundation.CementApp():
.. code-block:: python
import signal
from cement.core import foundation, exc
from cement.core.foundation import CementApp
SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
app = foundation.CementApp('myapp', catch_signals=SIGNALS)
CementApp('myapp', catch_signals=SIGNALS)
...
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
rather than assigning a unique callback function for every signal.
What If I Don't Like Your Signal Handler Callback?
--------------------------------------------------
@ -154,33 +141,34 @@ callback.
.. code-block:: python
import signal
from cement.core import foundation, exc, hook
from cement.core.foundation import CementApp
from cement.core import hook
SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
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
app = foundation.CementApp('myapp',
catch_signals=SIGNALS,
signal_handler=my_signal_handler)
...
class MyApp(CementApp):
class Meta:
label = 'myapp'
catch_signals = SIGNALS
signal_handler = my_signal_handler
This Is Stupid, and UnPythonic - How Do I Disable It?
-----------------------------------------------------
To each their own. If you simply do not want any kind of signal handling
performed, just set 'catch_signals=None'.
performed, just set ``catch_signals=None``.
.. code-block:: python
import signal
from cement.core import foundation, exc
from cement.core.foundation import foundation
app = foundation.CementApp('myapp', catch_signals=None)
CementApp('myapp', catch_signals=None)

View File

@ -12,8 +12,10 @@ still maintain the existing shared commands (or override them as necessary).
.. code-block:: python
from cement.core import foundation, handler
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
class AbstractBaseController(CementBaseController):
"""
@ -95,18 +97,16 @@ still maintain the existing shared commands (or override them as necessary).
print("Inside Controller2.command2()")
def main():
app = foundation.CementApp('myapp')
app = CementApp('myapp')
try:
# register controllers handlers
handler.register(MyAppBaseController)
handler.register(Controller1)
handler.register(Controller2)
# register controllers handlers
handler.register(MyAppBaseController)
handler.register(Controller1)
handler.register(Controller2)
app.setup()
app.run()
finally:
app.close()
app.setup()
app.run()
app.close()
if __name__ == '__main__':
main()

View File

@ -31,12 +31,9 @@ include, so we've added an example below.
base_controller = MyBaseController
app = MyApp()
try:
app.setup()
with MyApp() as app:
app.run()
finally:
app.close()
This looks like:

View File

@ -73,11 +73,9 @@ Example
app = MyApp()
handler.register(MySecondController)
try:
app.setup()
app.run()
finally:
app.close()
app.setup()
app.run()
app.close()
if __name__ == '__main__':
main()

View File

@ -23,8 +23,9 @@ sub-commands, that are implemented via nested-controllers.
.. code-block:: python
from cement.core import foundation, handler
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
class BaseController(CementBaseController):
class Meta:
@ -79,31 +80,30 @@ sub-commands, that are implemented via nested-controllers.
def third_cmd7(self):
print("Inside ThirdLevelController.third_cmd7()")
class MyApp(foundation.CementApp):
class MyApp(CementApp):
class Meta:
label = 'myapp'
def main():
try:
# create the app
app = MyApp()
# create the app
app = MyApp()
# register controllers to the app
handler.register(BaseController)
handler.register(EmbeddedController)
handler.register(SecondLevelController)
handler.register(ThirdLevelController)
# register controllers to the app
handler.register(BaseController)
handler.register(EmbeddedController)
handler.register(SecondLevelController)
handler.register(ThirdLevelController)
# setup the app
app.setup()
# setup the app
app.setup()
# run the app
app.run()
# run the app
app.run()
finally:
# close the app
app.close()
# close the app
app.close()
if __name__ == '__main__':
main()

View File

@ -26,8 +26,9 @@ and a 'hosts' controller and we want to have a 'list' sub-command under both:
.. code-block:: python
from cement.core import foundation, handler
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
# define application controllers
class MyAppBaseController(CementBaseController):
@ -75,25 +76,24 @@ and a 'hosts' controller and we want to have a 'list' sub-command under both:
print "Inside HostsListController.default()"
def main():
try:
# create the application
app = foundation.CementApp('myapp')
# create the application
app = CementApp('myapp')
# register non-base controllers
handler.register(MyAppBaseController)
handler.register(UsersController)
handler.register(HostsController)
handler.register(UsersListController)
handler.register(HostsListController)
# register non-base controllers
handler.register(MyAppBaseController)
handler.register(UsersController)
handler.register(HostsController)
handler.register(UsersListController)
handler.register(HostsListController)
# setup the application
app.setup()
# setup the application
app.setup()
# run it
app.run()
finally:
# close it
app.close()
# run it
app.run()
# close it
app.close()
if __name__ == '__main__':
main()

View File

@ -5,8 +5,9 @@ Multiple Stacked Controllers
.. code-block:: python
from cement.core import foundation, controller, handler
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
# define application controllers
class MyAppBaseController(CementBaseController):
@ -74,7 +75,7 @@ Multiple Stacked Controllers
def main():
try:
# create the application
app = foundation.CementApp('myapp')
app = CementApp('myapp')
# register controllers
handler.register(MyAppBaseController)

View File

@ -16,50 +16,50 @@ class CementDevtoolsController(CementBaseController):
class Meta:
label = 'base'
arguments = [
(['-y, --noprompt'],
dict(help='answer yes to prompts.', action='store_true',
(['-y, --noprompt'],
dict(help='answer yes to prompts.', action='store_true',
dest='noprompt')),
(['--ignore-errors'],
dict(help="don't stop operations because of errors",
dict(help="don't stop operations because of errors",
action='store_true', dest='ignore_errors')),
(['--loud'], dict(help='add more verbose output',
(['--loud'], dict(help='add more verbose output',
action='store_true', dest='loud')),
(['modifier1'],
(['modifier1'],
dict(help='command modifier positional argument', nargs='?')),
]
def _do_error(self, msg):
if self.app.pargs.ignore_errors:
self.app.log.error(msg)
else:
raise Exception(msg)
@expose(hide=True)
@expose(hide=True)
def default(self):
raise AssertionError("A sub-command is required. See --help.")
def _do_git(self):
# make sure we don't have any uncommitted changes
print('Checking for Uncommitted Changes')
out, err, res = shell.exec_cmd(['git', '--no-pager', 'diff'])
if len(out) > 0:
self._do_error('There are uncommitted changes. See `git status`.')
# make sure we don't have any un-added files
print('Checking for Untracked Files')
out, err, res = shell.exec_cmd(['git', 'status'])
if re.match('Untracked files', out):
self._do_error('There are untracked files. See `git status`.')
# make sure there isn't an existing tag
print("Checking for Duplicate Git Tag")
out, err, res = shell.exec_cmd(['git', 'tag'])
for ver in out.split('\n'):
if ver == VERSION:
self._do_error("Tag %s already exists" % VERSION)
print("Tagging Git Release")
out, err, res = shell.exec_cmd(['git', 'tag', '-a', '-m', VERSION,
out, err, res = shell.exec_cmd(['git', 'tag', '-a', '-m', VERSION,
VERSION])
if res > 0:
self._do_error("Unable to tag release with git.")
@ -67,7 +67,7 @@ class CementDevtoolsController(CementBaseController):
def _do_tests(self):
print('Running Nose Tests')
out, err, res = shell.exec_cmd(['which', 'nosetests'])
if self.app.pargs.loud:
cmd_args = ['coverage', 'run', out.strip(), '--verbosity=3']
res = shell.exec_cmd2(cmd_args)
@ -77,7 +77,7 @@ class CementDevtoolsController(CementBaseController):
if res > 0:
self._do_error("\n\nNose tests did not pass.\n\n" +
"$ %s\n%s" % (' '.join(cmd_args), err))
def _do_pep8(self):
print("Checking PEP8 Compliance")
cmd_args = ['pep8', '-r', 'cement/', '--exclude=*.pyc']
@ -105,7 +105,7 @@ class CementDevtoolsController(CementBaseController):
if res > 0:
self._do_error("\n\nFailed to build sphinx documentation\n\n" +
"$ %s\n%s" % (' '.join(cmd_args), out))
@expose(help='create a cement release')
def make_release(self):
print('')
@ -115,26 +115,26 @@ class CementDevtoolsController(CementBaseController):
res = raw_input("Continue? [yN] ")
if res not in ['Y', 'y', '1']:
sys.exit(1)
tmp = tempfile.mkdtemp()
print("Destination: %s" % tmp)
os.makedirs(os.path.join(tmp, 'source'))
os.makedirs(os.path.join(tmp, 'doc'))
self._do_pep8()
self._do_tests()
self._do_git()
self._do_sphinx(os.path.join(tmp, 'doc'))
tar_path = os.path.join(tmp, 'source', 'cement-%s.tar' % VERSION)
gzip_path = "%s.gz" % tar_path
print("Generating Release Files")
cmd_args = ['git', 'archive', VERSION,
cmd_args = ['git', 'archive', VERSION,
'--prefix=cement-%s/' % VERSION,
'--output=%s' % tar_path]
out, err, res = shell.exec_cmd(cmd_args)
cmd_args = ['gzip', tar_path]
out, err, res = shell.exec_cmd(cmd_args)
if res > 0:
@ -142,26 +142,23 @@ class CementDevtoolsController(CementBaseController):
"$ %s" % (' '.join(cmd_args), err))
print('')
@expose(help='get the current version of the sources')
def get_version(self):
print(VERSION)
class CementDevtoolsApp(CementApp):
class Meta:
label = 'cement-devtools'
base_controller = CementDevtoolsController
def main():
app = CementDevtoolsApp('cement-devtools')
try:
app.setup()
app.run()
except AssertionError as e:
print("AssertionError => %s" % e.args[0])
finally:
app.close()
with CementDevtoolsApp() as app:
try:
app.run()
except AssertionError as e:
print("AssertionError => %s" % e.args[0])
if __name__ == '__main__':
main()
main()

View File

@ -281,12 +281,17 @@ class FoundationTestCase(test.CementCoreTestCase):
self.eq({'foo':'bar'}, last_data)
self.eq(output_text, last_output)
def test_with_operator(self):
with self.app_class() as app:
app.run()
@test.raises(SystemExit)
def test_close_with_code(self):
self.app.setup()
self.app.run()
app = self.make_app(APP, exit_on_close=True)
app.setup()
app.run()
try:
self.app.close(114)
app.close(114)
except SystemExit as e:
self.eq(e.code, 114)
raise