diff --git a/.travis.yml b/.travis.yml index 02d2c48f..c1d41f99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: python -before_script: ./utils/travis/bootstrap.sh -script: ./utils/run-tests.sh -install: sudo apt-get install memcached libmemcached-dev -y +script: ./scripts/travis.sh python: - "2.6" - "2.7" diff --git a/ChangeLog b/ChangeLog index 4cf04a71..3955fcaf 100755 --- a/ChangeLog +++ b/ChangeLog @@ -21,8 +21,16 @@ is available online at: Misc: - * :issue:`111` - Use relative imports (makes cement more portable as it - can be included and distributed with 3rd party sources). + * :issue:`111` - Use relative imports (makes cement more portable as it + can be included and distributed with 3rd party sources). + + +Incompatible Changes: + + * Namespace reverted from 'cement2' back to 'cement'. + * The following extensions have been removed from the cement source tree, + and are now available externally (see: http://github.com/cement): daemon, + memcached, configobj, yaml, genshi. 1.9.8 - Thu May 3, 2012 diff --git a/README.md b/README.md index c72f19e0..2cc61a8b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Cement core features include (but are not limited to): * Handler system connects implementation classes with Interfaces * Output handler interface renders return dictionaries to console * Cache handler interface adds caching support for improved performance - * Zero external dependencies* (ext's with dependencies ship separately) * Controller handler supports sub-commands, and nested controllers - * Almost 100% test coverage + * Zero external dependencies* (ext's with dependencies ship separately) + * 99% test coverage * Extensive Sphinx documentation * Tested on Python 2.6, 2.7, 3.1, and 3.2 diff --git a/cement/__init__.py b/cement/__init__.py index a7fff560..2cdb0e40 100644 --- a/cement/__init__.py +++ b/cement/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: nocover +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover diff --git a/cement/ext/__init__.py b/cement/ext/__init__.py index a7fff560..2cdb0e40 100644 --- a/cement/ext/__init__.py +++ b/cement/ext/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: nocover +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover diff --git a/cement/ext/ext_daemon.py b/cement/ext/ext_daemon.py deleted file mode 100644 index 2c9982cc..00000000 --- a/cement/ext/ext_daemon.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -This module provides any dynamically loadable code for the Daemon -Framework Extension such as hook and handler registration. Additional -classes and functions exist in cement.lib.ext_daemon. - -""" - -import os -import sys -import pwd -import grp -from ..core import handler, hook, backend -from ..lib.ext_daemon import Environment - -Log = backend.minimal_logger(__name__) -CEMENT_DAEMON_ENV = None -CEMENT_DAEMON_APP = None - -def daemonize(): - """ - This function switches the running user/group to that configured in - config['daemon']['user'] and config['daemon']['group']. The default user - is os.environ['USER'] and the default group is that user's primary group. - A pid_file and directory to run in is also passed to the environment. - - It is important to note that with the daemon extension enabled, the - environment will switch user/group/set pid/etc regardless of whether - the --daemon option was passed at command line or not. However, the - process will only 'daemonize' if the option is passed to do so. This - allows the program to run exactly the same in forground or background. - - """ - # We want to honor the runtime user/group/etc even if --daemon is not - # passed... but only daemonize if it is. - global CEMENT_DAEMON_ENV - global CEMENT_DAEMON_APP - - app = CEMENT_DAEMON_APP - CEMENT_DAEMON_ENV = Environment( - user_name=app.config.get('daemon', 'user'), - group_name=app.config.get('daemon', 'group'), - pid_file=app.config.get('daemon', 'pid_file'), - dir=app.config.get('daemon', 'dir'), - umask=app.config.get('daemon', 'umask'), - ) - - CEMENT_DAEMON_ENV.switch() - - if '--daemon' in app.argv: - CEMENT_DAEMON_ENV.daemonize() - -@hook.register() -def cement_post_setup_hook(app): - """ - Adds the '--daemon' argument to the argument object, and sets the default - [daemon] config section options. - - """ - global CEMENT_DAEMON_APP - CEMENT_DAEMON_APP = app - - app.args.add_argument('--daemon', dest='daemon', - action='store_true', help='daemonize the process') - - # Add default config - user = pwd.getpwnam(os.environ['USER']) - group = grp.getgrgid(user.pw_gid) - - defaults = dict() - defaults['daemon'] = dict() - defaults['daemon']['user'] = user.pw_name - defaults['daemon']['group'] = group.gr_name - defaults['daemon']['pid_file'] = None - defaults['daemon']['dir'] = '/' - defaults['daemon']['umask'] = 0 - app.config.merge(defaults, override=False) - app.extend('daemonize', daemonize) - -@hook.register() -def cement_on_close_hook(app): - """ - After application run time, this hook just attempts to clean up the - pid_file if one was set, and exists. - - """ - global CEMENT_DAEMON_ENV - - if CEMENT_DAEMON_ENV and CEMENT_DAEMON_ENV.pid_file: - if os.path.exists(CEMENT_DAEMON_ENV.pid_file): - Log.debug('Cleaning up pid_file...') - pid = open(CEMENT_DAEMON_ENV.pid_file, 'r').read().strip() - - # only remove it if we created it. - if int(pid) == int(os.getpid()): - os.remove(CEMENT_DAEMON_ENV.pid_file) diff --git a/cement/lib/__init__.py b/cement/lib/__init__.py index 3eee8f5c..8fc4797b 100644 --- a/cement/lib/__init__.py +++ b/cement/lib/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: nocover \ No newline at end of file +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover \ No newline at end of file diff --git a/cement/lib/ext_daemon.py b/cement/lib/ext_daemon.py deleted file mode 100644 index d00522e3..00000000 --- a/cement/lib/ext_daemon.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -Daemon Framework Extension Library. - -""" - -import os -import io -import sys -import pwd -import grp -from ..core import backend, exc - -Log = backend.minimal_logger(__name__) - -class Environment(object): - """ - This class provides a mechanism for altering the running processes - environment. - - Optional Arguments: - - stdin - A file to read STDIN from. Default: /dev/null - - stdout - A file to write STDOUT to. Default: /dev/null - - stderr - A file to write STDERR to. Default: /dev/null - - dir - The directory to run the process in. - - pid_file - The filesystem path to where the PID (Process ID) should be - written to. Default: None - - user_name - The user name to run the process as. Default: os.environ['USER'] - - group_name - The group name to run the process as. Default: The primary group - of os.environ['USER']. - - umask - The umask to pass to os.umask(). Default: 0 - - """ - def __init__(self, **kw): - self.stdin = kw.get('stdin', '/dev/null') - self.stdout = kw.get('stdout', '/dev/null') - self.stderr = kw.get('stderr', '/dev/null') - self.dir = kw.get('dir', os.curdir) - self.pid_file = kw.get('pid_file', None) - self.umask = kw.get('umask', 0) - self.user_name = kw.get('user_name', - os.environ['USER']) - - # clean up - self.dir = os.path.abspath(os.path.expanduser(self.dir)) - if self.pid_file: - self.pid_file = os.path.abspath(os.path.expanduser(self.pid_file)) - - try: - self.user = pwd.getpwnam(self.user_name) - except KeyError as e: - raise exc.CementRuntimeError("Daemon user '%s' doesn't exist." % \ - self.user_name) - - try: - self.group_name = kw.get('group_name', - grp.getgrgid(self.user.pw_gid).gr_name) - self.group = grp.getgrnam(self.group_name) - except KeyError as e: - raise exc.CementRuntimeError("Daemon group '%s' doesn't exist." % \ - self.group_name) - - def _write_pid_file(self): - """ - Writes os.getpid() out to self.pid_file. - - """ - pid = str(os.getpid()) - Log.debug('writing pid (%s) out to %s' % (pid, self.pid_file)) - - # setup pid - if self.pid_file: - f = open(self.pid_file, 'w') - f.write(pid) - f.close() - try: - os.chown(os.path.dirname(self.pid_file), - self.user.pw_uid, self.group.gr_gid) - except OSError as e: - Log.debug("unable to chown %s:%s %s" % \ - (self.user.pw_uid, self.group.gr_gid, self.pid_file)) - - def switch(self): - """ - Switch the current process's user/group to self.user_name, and - self.group_name. Change directory to self.dir, and write the - current pid out to self.pid_file. - - """ - # set the running uid/gid - Log.debug('setting process uid(%s) and gid(%s)' % \ - (self.user.pw_uid, self.group.gr_gid)) - os.setgid(self.group.gr_gid) - os.setuid(self.user.pw_uid) - os.environ['HOME'] = self.user.pw_dir - os.chdir(self.dir) - if self.pid_file and os.path.exists(self.pid_file): - raise exc.CementRuntimeError("Process already running (%s)" % \ - self.pid_file) - else: - self._write_pid_file() - - def daemonize(self): - """ - Fork the current process into a daemon. - - References: - - UNIX Programming FAQ - 1.7 How do I get my program to act like a daemon? - http://www.unixguide.net/unix/programming/1.7.shtml - http://www.faqs.org/faqs/unix-faq/programmer/faq/ - - Advanced Programming in the Unix Environment - W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7. - - """ - Log.debug('attempting to daemonize the current process') - # Do first fork. - try: - pid = os.fork() - if pid > 0: - Log.debug('successfully detached from first parent') - os._exit(os.EX_OK) - except OSError as e: - sys.stderr.write("Fork #1 failed: (%d) %s\n" % \ - (e.errno, e.strerror)) - sys.exit(1) - - # Decouple from parent environment. - os.chdir(self.dir) - os.umask(int(self.umask)) - os.setsid() - - # Do second fork. - try: - pid = os.fork() - if pid > 0: - Log.debug('successfully detached from second parent') - os._exit(os.EX_OK) - except OSError as e: - sys.stderr.write("Fork #2 failed: (%d) %s\n" % \ - (e.errno, e.strerror)) - sys.exit(1) - - # Redirect standard file descriptors. - stdin = open(self.stdin, 'r') - stdout = open(self.stdout, 'a+') - stderr = open(self.stderr, 'a+') - - if hasattr(sys.stdin, 'fileno'): - try: - os.dup2(stdin.fileno(), sys.stdin.fileno()) - except io.UnsupportedOperation as e: - # FIXME: ? - pass - if hasattr(sys.stdout, 'fileno'): - try: - os.dup2(stdout.fileno(), sys.stdout.fileno()) - except io.UnsupportedOperation as e: - # FIXME: ? - pass - if hasattr(sys.stderr, 'fileno'): - try: - os.dup2(stderr.fileno(), sys.stderr.fileno()) - except io.UnsupportedOperation as e: - # FIXME: ? - pass - - # Update our pid file - self._write_pid_file() \ No newline at end of file diff --git a/cement/test_helper.py b/cement/test_helper.py index 1144ce26..fd0bc542 100644 --- a/cement/test_helper.py +++ b/cement/test_helper.py @@ -14,7 +14,6 @@ class TestApp(foundation.CementApp): from cement.ext import ext_configparser from cement.ext import ext_logging from cement.ext import ext_optparse - from cement.ext import ext_daemon from cement.ext import ext_json if not 'configparser' in backend.handlers['config']: handler.register(ext_configparser.ConfigParserConfigHandler) diff --git a/doc/source/api/core.rst b/doc/source/api/core.rst index 979c57dd..4493c81e 100644 --- a/doc/source/api/core.rst +++ b/doc/source/api/core.rst @@ -115,10 +115,10 @@ Cement Core Modules .. _cement.core.util: -:mod:`cement.core.util` +:mod:`cement.utils` ------------------------ -.. automodule:: cement.core.util +.. automodule:: cement.utils :members: diff --git a/doc/source/api/extensions.rst b/doc/source/api/extensions.rst index 7dc7a8c6..55351ffc 100644 --- a/doc/source/api/extensions.rst +++ b/doc/source/api/extensions.rst @@ -1,19 +1,18 @@ - Cement Extensions ================== +The following are extensions that are included within the Cement source tree +and shipped with Cement core libraries. Additional 'external' extensions +are also available at http://github.com/cement. + + .. toctree:: :maxdepth: 1 extensions/argparse extensions/cement_plugin extensions/configparser - extensions/configobj - extensions/daemon - extensions/genshi extensions/json extensions/logging extensions/nulloutput extensions/optparse - extensions/yaml - extensions/memcached diff --git a/doc/source/api/extensions/configobj.rst b/doc/source/api/extensions/configobj.rst deleted file mode 100644 index 99364e31..00000000 --- a/doc/source/api/extensions/configobj.rst +++ /dev/null @@ -1,26 +0,0 @@ -ConfigObj -========= - -The ConfigObj extension provides a 'config' handler based on `ConfigObj `_. -It is supported on Python 2.6+, however currently does not support Python 3. - - -API Reference -------------- - -.. _cement.ext.ext_configobj: - -:mod:`cement.ext.ext_configobj` --------------------------------- - -.. automodule:: cement.ext.ext_configobj - :members: - -.. _cement.lib.ext_configobj: - -:mod:`cement.lib.ext_configobj` --------------------------------- - -.. automodule:: cement.lib.ext_configobj - :members: - diff --git a/doc/source/api/extensions/daemon.rst b/doc/source/api/extensions/daemon.rst deleted file mode 100644 index 5c9d52ff..00000000 --- a/doc/source/api/extensions/daemon.rst +++ /dev/null @@ -1,215 +0,0 @@ -Daemon -====== - -The Daemon Framework Extension enables applications built on cement to easily -perform standard 'daemon' functions. Features include: - - * Configurable runtime user and group - * Adds the --daemon command line option - * Adds app.daemonize() function to trigger daemon functionality where - necessary (either in a cement_pre_run_hook or an application controller - sub-command, etc). - * Manages a pid file including cleanup on app.close() - -Configuration -------------- - -The daemon extension is configurable with the following config settings under -the [daemon] section. - - user - The user name to run the process as. Default: os.environ['USER'] - - group - The group name to run the process as. Default: The primary group of - the 'user'. - - dir - The directory to run the process in. Default: / - - pid_file - The filesystem path to store the PID (Process ID) file. Default: None - - umask - The umask value to pass to os.umask(). Default: 0 - - -The configurations can be passed as defaults: - -.. code-block:: python - - from cement.core import foundation, backend - - defaults = backend.defaults('myapp', 'daemon') - defaults['daemon']['user'] = 'myuser' - defaults['daemon']['group'] = 'mygroup' - defaults['daemon']['dir'] = '/var/lib/myapp/' - defaults['daemon']['pid_file'] = '/var/run/myapp/myapp.pid' - defaults['daemon']['umask'] = 0 - - app = foundation.CementApp('myapp', config_defaults=defaults) - - -Additionally, an application configuration file might have a section like the -following: - -.. code-block:: text - - [daemon] - user = myuser - group = mygroup - dir = /var/lib/myapp/ - pid_file = /var/run/myapp/myapp.pid - umask = 0 - - -Example Usage -------------- - -The following example shows how to add the ext_daemon extension, as well as -trigger daemon functionality before app.run() is called. - -.. code-block:: python - - from time import sleep - from cement.core import foundation, backend - - try: - app = foundation.CementApp('myapp', extensions=['daemon']) - app.setup() - app.daemonize() - app.run() - - count = 0 - while True: - 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: - -.. code-block:: python - - from cement.core import hook - - @hook.register() - def cement_pre_run_hook(app): - app.daemonize() - - -Wherever the 'app' is created, the ext_daemon extension must be added to -config['base']['extensions']. Finally, some applications may prefer to only -daemonize certain sub-commands rather than the entire parent application. -For example: - -.. code-block:: python - - from cement.core import backend, foundation, controller, handler - - # create an application - app = foundation.CementApp('myapp', extensions=['daemon']) - - # define an application base controller - class MyAppBaseController(controller.CementBaseController): - class Meta: - interface = controller.IController - 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')) - ] - - @controller.expose(hide=True, aliases=['run']) - def default(self): - self.log.info('Inside base.default function.') - if self.pargs.foo: - self.log.info("Recieved option 'foo' with value '%s'." % \ - self.pargs.foo) - - @controller.expose(help="run the daemon command.") - def run_forever(self): - from time import sleep - self.app.daemonize() - - count = 0 - while True: - count = count + 1 - print(count) - sleep(10) - - handler.register(MyAppBaseController) - - # setup the application - app.setup() - - # run the application, and close - try: - app.run() - finally: - app.close() - - -Running The Daemon ------------------- - -By default, even after app.daemonize() is called... the application will -continue to run in the foreground, but will still manage the pid and -user/group switching. To detach a process and send it to the background you -simply pass the '--daemon' option at command line. - -.. code-block:: text - - $ python example.py --daemon - - $ ps -x | grep example - 37421 ?? 0:00.01 python example2.py --daemon - 37452 ttys000 0:00.00 grep example - - -You will return to your shell, however you can see the process continues to -run. The debug output also shows a bit more of what's happening behind the -scenes: - -.. code-block:: text - - 2011-12-21 17:44:14,348 (DEBUG) cement.core.extension : loading the 'cement.ext.ext_daemon' framework extension - 2011-12-21 17:44:14,349 (DEBUG) cement.core.hook : registering hook func 'cement_post_setup_hook' from cement.ext.ext_daemon into hooks['cement_post_setup_hook'] - 2011-12-21 17:44:14,349 (DEBUG) cement.core.hook : registering hook func 'cement_on_close_hook' from cement.ext.ext_daemon into hooks['cement_on_close_hook'] - 2011-12-21 17:44:14,353 (DEBUG) cement.core.hook : running hook 'cement_post_setup_hook' () from cement.ext.ext_daemon - 2011-12-21 17:44:14,354 (DEBUG) cement.lib.ext_daemon : setting process uid(501) and gid(20) - 2011-12-21 17:44:14,355 (DEBUG) cement.lib.ext_daemon : writing pid (42662) out to /Users/wdierkes/tmp/myapp.pid - 2011-12-21 17:44:14,355 (DEBUG) cement.lib.ext_daemon : attempting to daemonize the current process - 2011-12-21 17:44:14,356 (DEBUG) cement.lib.ext_daemon : successfully detached from first parent - 2011-12-21 17:44:14,358 (DEBUG) cement.lib.ext_daemon : successfully detached from second parent - - -API Reference -------------- - -.. _cement.ext.ext_daemon: - -:mod:`cement.ext.ext_daemon` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: cement.ext.ext_daemon - :members: - -.. _cement.lib.ext_daemon: - -:mod:`cement.lib.ext_daemon` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: cement.lib.ext_daemon - :members: - - - diff --git a/doc/source/api/extensions/genshi.rst b/doc/source/api/extensions/genshi.rst deleted file mode 100644 index 5a77d7ab..00000000 --- a/doc/source/api/extensions/genshi.rst +++ /dev/null @@ -1,199 +0,0 @@ -Genshi Templating Language -========================== - -The Genshi Framework Extension enables applications built on Cement to render -console output from text templates. - -Configuration -------------- - -By default, templates are search for in the '.templates' python -module. To override this, you can subclass and pass to CementApp(). - -.. code-block:: python - - from cement.core import foundation, backend - from cement.lib.ext_genshi import GenshiOutputHandler - - class MyOutputHandler(GenshiOutputHandler): - class Meta: - label = 'my_output_handler' - template_module = 'myapp.cli.templates' - - app = foundation.CementApp('myapp', output_handler=MyOutputHandler) - - -Example Usage -------------- - -The following is an example application within a python package. - -*myapp/appmain.py* - -.. code-block:: python - - from cement.core import foundation, controller, handler, backend - - # define application controllers - class MyAppBaseController(controller.CementBaseController): - class Meta: - label = 'base' - description = "My Application Does Amazing Things" - arguments = [] - - @controller.expose(help="base controller default command", hide=True) - def default(self): - data = dict( - controller='base', - command='default', - ) - print self.render(data, 'default.txt') - - # define the application - class MyApp(foundation.CementApp): - class Meta: - label = 'myapp' - output_handler = 'genshi' - extensions = ['genshi'] - base_controller = MyAppBaseController - - try: - app = MyApp() - app.setup() - app.run() - finally: - app.close() - - -*myapp/templates/default.txt* - -.. code-block:: text - - Inside the ${controller}.${command} function! - - -And the output: - ->>> from myapp import appmain -Inside the base.default function! - - -Genshi Syntax Basics --------------------- - -As noted in the example template, documentation on Genshi Text Templating can -be found at: - - http://genshi.edgewall.org/wiki/Documentation/text-templates.html - -**Printing Variables** - -.. code-block:: text - - Hello ${user_name} - -Where 'user_name' is a variable returned from the controller. Will display: - -.. code-block:: text - - Hello Johnny - - -**if statements** - -.. code-block:: text - - {% if foo %}\ - Label: ${example.label} - {% end %}\ - -Will only output 'Label: