diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..7f9f14e1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] + +# These are problematic for coverage reporting +omit = cement/ext/ext_daemon.py diff --git a/ChangeLog b/ChangeLog index f9d0bfaa..33a7c72e 100755 --- a/ChangeLog +++ b/ChangeLog @@ -29,11 +29,12 @@ Bugs: Features: + * :issue:`190` - Merge daemon extension into core * :issue:`194` - Added pre_argument_parsing/post_argument_parsing hooks * :issue:`196` - Added utils.misc.wrap * :issue:`200` - Merge ext.mustache into mainline. * :issue:`203` - Added support for external template directory - + Incompatible: * :issue:`173` - Deprecated 'has_key()' from configparser extension diff --git a/cement/core/arg.py b/cement/core/arg.py index 465cc92e..99065370 100644 --- a/cement/core/arg.py +++ b/cement/core/arg.py @@ -23,6 +23,7 @@ def argument_validator(klass, obj): # pylint: disable=W0105,W0232,W0232,R0903,E0213,R0923 class IArgument(interface.Interface): + """ This class defines the Argument Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -41,6 +42,7 @@ class IArgument(interface.Interface): """ class IMeta: + """Interface meta-data options.""" label = 'argument' @@ -101,9 +103,11 @@ class IArgument(interface.Interface): # pylint: disable=W0105 class CementArgumentHandler(handler.CementBaseHandler): + """Base class that all Argument Handlers should sub-class from.""" class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/cache.py b/cement/core/cache.py index 9eb17a67..8641ddf8 100644 --- a/cement/core/cache.py +++ b/cement/core/cache.py @@ -20,6 +20,7 @@ def cache_validator(klass, obj): class ICache(interface.Interface): + """ This class defines the Cache Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -42,6 +43,7 @@ class ICache(interface.Interface): """ # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'cache' @@ -109,11 +111,13 @@ class ICache(interface.Interface): class CementCacheHandler(handler.CementBaseHandler): + """ Base class that all Cache Handlers should sub-class from. """ class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/config.py b/cement/core/config.py index 366bc8b8..4f807d7e 100644 --- a/cement/core/config.py +++ b/cement/core/config.py @@ -21,6 +21,7 @@ def config_validator(klass, obj): class IConfig(interface.Interface): + """ This class defines the Config Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -51,6 +52,7 @@ class IConfig(interface.Interface): """ # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'config' """The string identifier of the interface.""" @@ -169,11 +171,13 @@ class IConfig(interface.Interface): class CementConfigHandler(handler.CementBaseHandler): + """ Base class that all Config Handlers should sub-class from. """ class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/controller.py b/cement/core/controller.py index 51a0f338..97daa959 100644 --- a/cement/core/controller.py +++ b/cement/core/controller.py @@ -51,6 +51,7 @@ def controller_validator(klass, obj): class IController(interface.Interface): + """ This class defines the Controller Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -71,6 +72,7 @@ class IController(interface.Interface): """ # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'controller' @@ -112,6 +114,7 @@ class IController(interface.Interface): class expose(object): + """ Used to expose controller functions to be listed as commands, and to decorate the function with Meta data for the argument parser. @@ -143,6 +146,7 @@ class expose(object): """ # pylint: disable=W0622 + def __init__(self, help='', hide=False, aliases=[]): self.hide = hide self.help = help @@ -163,6 +167,7 @@ class expose(object): # pylint: disable=R0921 class CementBaseController(handler.CementBaseHandler): + """ This is an implementation of the `IControllerHandler <#cement.core.controller.IController>`_ interface, but @@ -198,6 +203,7 @@ class CementBaseController(handler.CementBaseHandler): """ class Meta: + """ Controller meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/exc.py b/cement/core/exc.py index 811b400e..569f73d1 100644 --- a/cement/core/exc.py +++ b/cement/core/exc.py @@ -2,12 +2,14 @@ class FrameworkError(Exception): + """ General framework (non-application) related errors. :param msg: The error message. """ + def __init__(self, msg): Exception.__init__(self) self.msg = msg @@ -17,11 +19,13 @@ class FrameworkError(Exception): class InterfaceError(FrameworkError): + """Interface related errors.""" pass class CaughtSignal(FrameworkError): + """ Raised when a defined signal is caught. For more information regarding signals, reference the @@ -31,6 +35,7 @@ class CaughtSignal(FrameworkError): :param frame: The signal frame. """ + def __init__(self, signum, frame): msg = 'Caught signal %s' % signum super(CaughtSignal, self).__init__(msg) diff --git a/cement/core/extension.py b/cement/core/extension.py index 333bb1c2..3f7f3d2a 100644 --- a/cement/core/extension.py +++ b/cement/core/extension.py @@ -25,6 +25,7 @@ def extension_validator(klass, obj): class IExtension(interface.Interface): + """ This class defines the Extension Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -48,6 +49,7 @@ class IExtension(interface.Interface): # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'extension' @@ -93,7 +95,9 @@ class IExtension(interface.Interface): class CementExtensionHandler(handler.CementBaseHandler): + class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/foundation.py b/cement/core/foundation.py index b9174f56..a1786a4c 100644 --- a/cement/core/foundation.py +++ b/cement/core/foundation.py @@ -19,6 +19,7 @@ LOG = minimal_logger(__name__) class NullOut(object): + def write(self, s): pass @@ -45,6 +46,7 @@ def cement_signal_handler(signum, frame): class CementApp(meta.MetaMixin): + """ The primary class to build applications from. @@ -98,6 +100,7 @@ class CementApp(meta.MetaMixin): """ class Meta: + """ Application meta-data (can also be passed as keyword arguments to the parent class). @@ -543,10 +546,10 @@ class CementApp(meta.MetaMixin): def get_last_rendered(self): """ DEPRECATION WARNING: This function is deprecated as of Cement 2.1.3 - in favor of the `self.last_rendered` property, and will be removed in + in favor of the `self.last_rendered` property, and will be removed in future versions of Cement. - - Return the (data, output_text) tuple of the last time self.render() + + Return the (data, output_text) tuple of the last time self.render() was called. :returns: tuple (data, output_text) @@ -587,7 +590,7 @@ class CementApp(meta.MetaMixin): LOG.debug("laying cement for the '%s' application" % self._meta.label) - ### overrides via command line + # overrides via command line suppress_output = False if '--debug' in self._meta.argv: diff --git a/cement/core/handler.py b/cement/core/handler.py index 986f8abb..fa5a407d 100644 --- a/cement/core/handler.py +++ b/cement/core/handler.py @@ -11,9 +11,11 @@ LOG = minimal_logger(__name__) class CementBaseHandler(meta.MetaMixin): + """Base handler class that all Cement Handlers should subclass from.""" class Meta: + """ Handler meta-data (can also be passed as keyword arguments to the parent class). diff --git a/cement/core/interface.py b/cement/core/interface.py index 60243c5d..850afa16 100644 --- a/cement/core/interface.py +++ b/cement/core/interface.py @@ -9,22 +9,26 @@ DEFAULT_META = ['interface', 'label', 'config_defaults', 'config_section'] class Interface(object): + """ An interface definition class. All Interfaces should subclass from here. Note that this is not an implementation and should never be used directly. """ + def __init__(self): raise exc.InterfaceError("Interfaces can not be used directly.") class Attribute(object): + """ An interface attribute definition. :param description: The description of the attribute. """ + def __init__(self, description): self.description = description diff --git a/cement/core/log.py b/cement/core/log.py index f2170d28..a7ebf37c 100644 --- a/cement/core/log.py +++ b/cement/core/log.py @@ -24,6 +24,7 @@ def log_validator(klass, obj): class ILog(interface.Interface): + """ This class defines the Log Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -46,6 +47,7 @@ class ILog(interface.Interface): """ # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'log' @@ -122,11 +124,13 @@ class ILog(interface.Interface): class CementLogHandler(handler.CementBaseHandler): + """ Base class that all Log Handlers should sub-class from. """ class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/core/meta.py b/cement/core/meta.py index 47bab1e1..48f9c426 100644 --- a/cement/core/meta.py +++ b/cement/core/meta.py @@ -2,6 +2,7 @@ class Meta(object): + """ Model that acts as a container class for a meta attributes for a larger class. It stuffs any kwarg it gets in it's init as an attribute of itself. @@ -17,6 +18,7 @@ class Meta(object): class MetaMixin(object): + """ Mixin that provides the Meta class support to add settings to instances of slumber objects. Meta settings cannot start with a _. diff --git a/cement/core/output.py b/cement/core/output.py index 1151ce33..ed6ead63 100644 --- a/cement/core/output.py +++ b/cement/core/output.py @@ -21,6 +21,7 @@ def output_validator(klass, obj): class IOutput(interface.Interface): + """ This class defines the Output Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -43,6 +44,7 @@ class IOutput(interface.Interface): """ # pylint: disable=W0232, C0111, R0903 class IMeta: + """Interface meta-data.""" label = 'output' @@ -76,11 +78,13 @@ class IOutput(interface.Interface): class CementOutputHandler(handler.CementBaseHandler): + """ Base class that all Output Handlers should sub-class from. """ class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). @@ -97,10 +101,12 @@ class CementOutputHandler(handler.CementBaseHandler): class TemplateOutputHandler(CementOutputHandler): + """ Base class for template base output handlers. """ + def _load_template_from_file(self, template_path): template_prefix = self.app._meta.template_dir.rstrip('/') template_path = template_path.lstrip('/') diff --git a/cement/core/plugin.py b/cement/core/plugin.py index 7a29d560..f6914e04 100644 --- a/cement/core/plugin.py +++ b/cement/core/plugin.py @@ -21,6 +21,7 @@ def plugin_validator(klass, obj): class IPlugin(interface.Interface): + """ This class defines the Plugin Handler Interface. Classes that implement this handler must provide the methods and attributes defined @@ -86,12 +87,14 @@ class IPlugin(interface.Interface): class CementPluginHandler(handler.CementBaseHandler): + """ Base class that all Plugin Handlers should sub-class from. """ class Meta: + """ Handler meta-data (can be passed as keyword arguments to the parent class). diff --git a/cement/ext/ext_argparse.py b/cement/ext/ext_argparse.py index b2db1c2b..89e282d9 100644 --- a/cement/ext/ext_argparse.py +++ b/cement/ext/ext_argparse.py @@ -8,6 +8,7 @@ LOG = minimal_logger(__name__) class ArgParseArgumentHandler(arg.CementArgumentHandler, ArgumentParser): + """ This class implements the :ref:`IArgument ` interface, and sub-classes from `argparse.ArgumentParser @@ -20,6 +21,7 @@ class ArgParseArgumentHandler(arg.CementArgumentHandler, ArgumentParser): """ class Meta: + """Handler meta-data.""" interface = arg.IArgument diff --git a/cement/ext/ext_configparser.py b/cement/ext/ext_configparser.py index c0779746..1423db68 100644 --- a/cement/ext/ext_configparser.py +++ b/cement/ext/ext_configparser.py @@ -14,6 +14,7 @@ LOG = minimal_logger(__name__) class ConfigParserConfigHandler(config.CementConfigHandler, RawConfigParser): + """ This class is an implementation of the :ref:`IConfig ` interface. It handles configuration file parsing and the like by @@ -26,6 +27,7 @@ class ConfigParserConfigHandler(config.CementConfigHandler, RawConfigParser): RawConfigParser on initialization. """ class Meta: + """Handler meta-data.""" interface = config.IConfig diff --git a/cement/ext/ext_daemon.py b/cement/ext/ext_daemon.py new file mode 100644 index 00000000..de6cc000 --- /dev/null +++ b/cement/ext/ext_daemon.py @@ -0,0 +1,266 @@ +"""Daemon Framework Extension""" + +import os +import sys +import io +import pwd +import grp +from ..core import handler, hook, backend, exc +from ..utils.misc import minimal_logger + +Log = minimal_logger(__name__) +Log = minimal_logger(__name__) +CEMENT_DAEMON_ENV = None +CEMENT_DAEMON_APP = None + + +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 + 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 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 = kw.get('user', 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) + except KeyError as e: + raise exc.FrameworkError("Daemon user '%s' doesn't exist." % + self.user) + + try: + self.group = kw.get('group', + grp.getgrgid(self.user.pw_gid).gr_name) + self.group = grp.getgrnam(self.group) + except KeyError as e: + raise exc.FrameworkError("Daemon group '%s' doesn't exist." % + self.group) + + 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() + os.chown(os.path.dirname(self.pid_file), + self.user.pw_uid, self.group.gr_gid) + + def switch(self): + """ + Switch the current process's user/group to self.user, and + self.group. 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.FrameworkError("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() + + +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=app.config.get('daemon', 'user'), + group=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() + + +def extend_app(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) + + +def cleanup(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) + + +def load(): + hook.register('post_setup', extend_app) + hook.register('pre_close', cleanup) diff --git a/cement/ext/ext_json.py b/cement/ext/ext_json.py index 036e0ea8..840cda63 100644 --- a/cement/ext/ext_json.py +++ b/cement/ext/ext_json.py @@ -9,6 +9,7 @@ LOG = minimal_logger(__name__) class JsonOutputHandler(output.CementOutputHandler): + """ This class implements the :ref:`IOutput ` interface. It provides JSON output from a data dictionary using the @@ -22,6 +23,7 @@ class JsonOutputHandler(output.CementOutputHandler): """ class Meta: + """Handler meta-data""" interface = output.IOutput diff --git a/cement/ext/ext_logging.py b/cement/ext/ext_logging.py index a4ad4aa9..5fcfd3ed 100644 --- a/cement/ext/ext_logging.py +++ b/cement/ext/ext_logging.py @@ -11,6 +11,7 @@ try: # pragma: no cover except AttributeError as e: # pragma: no cover # Not supported on Python < 3.1/2.7 # pragma: no cover class NullHandler(logging.Handler): # pragma: no cover + def handle(self, record): # pragma: no cover pass # pragma: no cover # pragma: no cover @@ -24,6 +25,7 @@ except AttributeError as e: # pragma: no cover class LoggingLogHandler(log.CementLogHandler): + """ This class is an implementation of the :ref:`ILog ` interface, and sets up the logging facility using the standard Python @@ -61,6 +63,7 @@ class LoggingLogHandler(log.CementLogHandler): """ class Meta: + """Handler meta-data.""" interface = log.ILog diff --git a/cement/ext/ext_mustache.py b/cement/ext/ext_mustache.py index 4aaa2220..b0fa599f 100644 --- a/cement/ext/ext_mustache.py +++ b/cement/ext/ext_mustache.py @@ -9,6 +9,7 @@ LOG = minimal_logger(__name__) class MustacheOutputHandler(output.TemplateOutputHandler): + """ This class implements the :ref:`IOutput ` interface. It provides text output from template and uses the diff --git a/cement/ext/ext_nulloutput.py b/cement/ext/ext_nulloutput.py index 15121b8a..ea4a28bb 100644 --- a/cement/ext/ext_nulloutput.py +++ b/cement/ext/ext_nulloutput.py @@ -7,6 +7,7 @@ LOG = minimal_logger(__name__) class NullOutputHandler(output.CementOutputHandler): + """ This class is an internal implementation of the :ref:`IOutput ` interface. It does not take any @@ -14,6 +15,7 @@ class NullOutputHandler(output.CementOutputHandler): """ class Meta: + """Handler meta-data""" interface = output.IOutput diff --git a/cement/ext/ext_plugin.py b/cement/ext/ext_plugin.py index da84525d..4ef9332a 100644 --- a/cement/ext/ext_plugin.py +++ b/cement/ext/ext_plugin.py @@ -10,10 +10,11 @@ from ..utils.fs import abspath LOG = minimal_logger(__name__) -### FIX ME: This is a redundant name... ? +# FIX ME: This is a redundant name... ? class CementPluginHandler(plugin.CementPluginHandler): + """ This class is an internal implementation of the :ref:`IPlugin ` interface. It does not take any @@ -22,6 +23,7 @@ class CementPluginHandler(plugin.CementPluginHandler): """ class Meta: + """Handler meta-data.""" interface = plugin.IPlugin diff --git a/cement/utils/test.py b/cement/utils/test.py index 1ab66e7a..368fa974 100644 --- a/cement/utils/test.py +++ b/cement/utils/test.py @@ -4,13 +4,15 @@ import unittest from ..core import backend, foundation # shortcuts +from nose import SkipTest from nose.tools import ok_ as ok from nose.tools import eq_ as eq from nose.tools import raises -from nose import SkipTest +from nose.plugins.attrib import attr class TestApp(foundation.CementApp): + """ Basic CementApp for generic testing. @@ -24,6 +26,7 @@ class TestApp(foundation.CementApp): class CementTestCase(unittest.TestCase): + """ A sub-class of unittest.TestCase. @@ -69,3 +72,15 @@ class CementTestCase(unittest.TestCase): def eq(self, a, b, msg=None): """Shorthand for 'assert a == b, "%r != %r" % (a, b)'. """ return eq(a, b, msg) + +# The following are for internal, Cement unit testing only + + +@attr('core') +class CementCoreTestCase(CementTestCase): + pass + + +@attr('ext') +class CementExtTestCase(CementTestCase): + pass diff --git a/doc/source/api/ext/ext_daemon.rst b/doc/source/api/ext/ext_daemon.rst new file mode 100644 index 00000000..233b6d73 --- /dev/null +++ b/doc/source/api/ext/ext_daemon.rst @@ -0,0 +1,10 @@ +.. _cement.ext.ext_daemon: + +:mod:`cement.ext.ext_daemon` +------------------------------ + +.. automodule:: cement.ext.ext_daemon + :members: + :undoc-members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index 040e3a74..cf11fc30 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -48,6 +48,7 @@ Cement Extension Modules ext/ext_argparse ext/ext_configparser + ext/ext_daemon ext/ext_json ext/ext_logging ext/ext_mustache diff --git a/setup.cfg b/setup.cfg index 41f5280d..077923c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ debug=0 detailed-errors=1 with-coverage=1 cover-package=cement +cover-inclusive=1 cover-erase=1 cover-html=1 cover-html-dir=coverage_report/ diff --git a/tests/core/cache_tests.py b/tests/core/cache_tests.py index 5017d3ea..05f1f1ba 100644 --- a/tests/core/cache_tests.py +++ b/tests/core/cache_tests.py @@ -19,7 +19,8 @@ class MyCacheHandler(cache.CementCacheHandler): def purge(self): pass -class CacheTestCase(test.CementTestCase): +@test.attr('core') +class CacheTestCase(test.CementCoreTestCase): def setUp(self): super(CacheTestCase, self).setUp() self.app = self.make_app(cache_handler=MyCacheHandler) diff --git a/tests/core/config_tests.py b/tests/core/config_tests.py index 69be46fa..5870bcec 100644 --- a/tests/core/config_tests.py +++ b/tests/core/config_tests.py @@ -14,7 +14,7 @@ class BogusConfigHandler(config.CementConfigHandler): class Meta: label = 'bogus' -class ConfigTestCase(test.CementTestCase): +class ConfigTestCase(test.CementCoreTestCase): @test.raises(exc.InterfaceError) def test_invalid_config_handler(self): handler.register(BogusConfigHandler) diff --git a/tests/core/controller_tests.py b/tests/core/controller_tests.py index 3faa788e..1cf24b4b 100644 --- a/tests/core/controller_tests.py +++ b/tests/core/controller_tests.py @@ -87,7 +87,7 @@ class ArgumentConflict(controller.CementBaseController): stacked_type = 'embedded' arguments = [(['-f', '--foo'], dict())] -class ControllerTestCase(test.CementTestCase): +class ControllerTestCase(test.CementCoreTestCase): def test_default(self): app = self.make_app(base_controller=TestController) app.setup() diff --git a/tests/core/exc_tests.py b/tests/core/exc_tests.py index 4beec5fc..d612c236 100644 --- a/tests/core/exc_tests.py +++ b/tests/core/exc_tests.py @@ -3,7 +3,7 @@ from cement.core import exc from cement.utils import test -class ExceptionTestCase(test.CementTestCase): +class ExceptionTestCase(test.CementCoreTestCase): @test.raises(exc.FrameworkError) def test_cement_runtime_error(self): try: diff --git a/tests/core/extension_tests.py b/tests/core/extension_tests.py index a1c9f216..890686de 100644 --- a/tests/core/extension_tests.py +++ b/tests/core/extension_tests.py @@ -12,7 +12,7 @@ class BogusExtensionHandler(extension.CementExtensionHandler): interface = IBogus label = 'bogus' -class ExtensionTestCase(test.CementTestCase): +class ExtensionTestCase(test.CementCoreTestCase): @test.raises(exc.FrameworkError) def test_invalid_extension_handler(self): # the handler type bogus doesn't exist diff --git a/tests/core/foundation_tests.py b/tests/core/foundation_tests.py index bb6e88db..50036521 100644 --- a/tests/core/foundation_tests.py +++ b/tests/core/foundation_tests.py @@ -40,7 +40,7 @@ def my_hook_two(app): def my_hook_three(app): return 3 -class FoundationTestCase(test.CementTestCase): +class FoundationTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app('my_app') diff --git a/tests/core/handler_tests.py b/tests/core/handler_tests.py index 51fc4f47..14d05ae0 100644 --- a/tests/core/handler_tests.py +++ b/tests/core/handler_tests.py @@ -50,7 +50,7 @@ class TestHandler(meta.MetaMixin): interface = TestInterface label = 'test' -class HandlerTestCase(test.CementTestCase): +class HandlerTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() diff --git a/tests/core/hook_tests.py b/tests/core/hook_tests.py index d0fee158..b8363645 100644 --- a/tests/core/hook_tests.py +++ b/tests/core/hook_tests.py @@ -19,7 +19,7 @@ def nosetests_hook(*args, **kw): def cement_hook_five(app, data): return data -class HookTestCase(test.CementTestCase): +class HookTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() hook.define('nosetests_hook') diff --git a/tests/core/interface_tests.py b/tests/core/interface_tests.py index 31fe8b03..f2d001ed 100644 --- a/tests/core/interface_tests.py +++ b/tests/core/interface_tests.py @@ -20,7 +20,7 @@ class TestHandler2(handler.CementBaseHandler): class TestHandler3(): pass -class InterfaceTestCase(test.CementTestCase): +class InterfaceTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() diff --git a/tests/core/log_tests.py b/tests/core/log_tests.py index b9c8b546..6a4690da 100644 --- a/tests/core/log_tests.py +++ b/tests/core/log_tests.py @@ -10,7 +10,7 @@ class BogusHandler1(log.CementLogHandler): interface = log.ILog label = 'bogus' -class LogTestCase(test.CementTestCase): +class LogTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() diff --git a/tests/core/meta_tests.py b/tests/core/meta_tests.py index f3fe2dda..2124a38e 100644 --- a/tests/core/meta_tests.py +++ b/tests/core/meta_tests.py @@ -12,7 +12,7 @@ class TestMeta(meta.MetaMixin): super(TestMeta, self).__init__(**kw) self.option_three = kw.get('option_three', None) -class MetaTestCase(test.CementTestCase): +class MetaTestCase(test.CementCoreTestCase): def test_passed_kwargs(self): t = TestMeta(option_two='some other value', option_three='value three') self.eq(t._meta.option_one, 'value one') diff --git a/tests/core/output_tests.py b/tests/core/output_tests.py index 270ede4e..66249058 100644 --- a/tests/core/output_tests.py +++ b/tests/core/output_tests.py @@ -16,7 +16,7 @@ class TestOutputHandler(output.TemplateOutputHandler): TEST_TEMPLATE = "%(foo)s" -class OutputTestCase(test.CementTestCase): +class OutputTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() diff --git a/tests/core/plugin_tests.py b/tests/core/plugin_tests.py index d51392c8..64363f95 100644 --- a/tests/core/plugin_tests.py +++ b/tests/core/plugin_tests.py @@ -54,7 +54,7 @@ def load(): """ -class PluginTestCase(test.CementTestCase): +class PluginTestCase(test.CementCoreTestCase): def setUp(self): self.app = self.make_app() diff --git a/tests/ext/configparser_tests.py b/tests/ext/configparser_tests.py index f46ec5e8..98df66ed 100644 --- a/tests/ext/configparser_tests.py +++ b/tests/ext/configparser_tests.py @@ -2,5 +2,6 @@ from cement.utils import test from cement.utils.misc import init_defaults -class ConfigParserConfigHandlerTestCase(test.CementTestCase): +@test.attr('core') +class ConfigParserConfigHandlerTestCase(test.CementExtTestCase): pass \ No newline at end of file diff --git a/tests/ext/daemon_tests.py b/tests/ext/daemon_tests.py new file mode 100644 index 00000000..02806af0 --- /dev/null +++ b/tests/ext/daemon_tests.py @@ -0,0 +1,98 @@ +"""Tests for cement.ext.ext_daemon.""" + +import os +import tempfile +from random import random +from cement.core import handler, backend, log, hook, exc +from cement.utils import test +from cement.ext import ext_daemon + +class DaemonExtTestCase(test.CementExtTestCase): + def setUp(self): + self.app = self.make_app() + + def test_switch(self): + env = ext_daemon.Environment() + env.switch() + + def test_switch_with_pid(self): + (_, tmpfile) = tempfile.mkstemp() + os.remove(tmpfile) + env = ext_daemon.Environment(pid_file=tmpfile) + env.switch() + + try: + self.ok(os.path.exists(tmpfile)) + finally: + os.remove(tmpfile) + + @test.raises(exc.FrameworkError) + def test_pid_exists(self): + (_, tmpfile) = tempfile.mkstemp() + + env = ext_daemon.Environment(pid_file=tmpfile) + env.switch() + + try: + self.ok(os.path.exists(tmpfile)) + except exc.FrameworkError as e: + self.ok(e.msg.startswith('Process already running')) + raise + finally: + env = ext_daemon.Environment() + env.switch() + os.remove(tmpfile) + + @test.raises(exc.FrameworkError) + def test_bogus_user(self): + rand = random() + + try: + env = ext_daemon.Environment(user='cement_test_user%s' % rand) + except exc.FrameworkError as e: + self.ok(e.msg.startswith('Daemon user')) + raise + finally: + env = ext_daemon.Environment() + env.switch() + + @test.raises(exc.FrameworkError) + def test_bogus_group(self): + rand = random() + + try: + env = ext_daemon.Environment(group='cement_test_group%s' % rand) + except exc.FrameworkError as e: + self.ok(e.msg.startswith('Daemon group')) + raise + finally: + env = ext_daemon.Environment() + env.switch() + + def test_daemon(self): + (_, tmpfile) = tempfile.mkstemp() + os.remove(tmpfile) + + app = self.make_app('test', argv=['--daemon'], extensions=['daemon']) + + app.setup() + app.config.set('daemon', 'pid_file', tmpfile) + + try: + ### FIX ME: Can't daemonize, because nose loses sight of it + #app.daemonize() + app.run() + finally: + app.close() + ext_daemon.cleanup(app) + + def test_daemon_not_passed(self): + app = self.make_app('myapp', extensions=['daemon']) + + app.setup() + app.config.set('daemon', 'pid_file', None) + + try: + app.run() + finally: + ext_daemon.cleanup(app) \ No newline at end of file diff --git a/tests/ext/json_tests.py b/tests/ext/json_tests.py index 22dad3eb..a594d63a 100644 --- a/tests/ext/json_tests.py +++ b/tests/ext/json_tests.py @@ -5,7 +5,7 @@ import sys from cement.core import handler, backend, hook from cement.utils import test -class JsonExtTestCase(test.CementTestCase): +class JsonExtTestCase(test.CementExtTestCase): def setUp(self): self.app = self.make_app('tests', extensions=['json'], diff --git a/tests/ext/logging_tests.py b/tests/ext/logging_tests.py index 6086b1f7..c9863f24 100644 --- a/tests/ext/logging_tests.py +++ b/tests/ext/logging_tests.py @@ -16,7 +16,8 @@ class MyLog(ext_logging.LoggingLogHandler): def __init__(self, *args, **kw): super(MyLog, self).__init__(*args, **kw) -class LoggingExtTestCase(test.CementTestCase): +@test.attr('core') +class LoggingExtTestCase(test.CementExtTestCase): def test_alternate_namespaces(self): defaults = init_defaults('myapp', 'log') defaults['log']['to_console'] = False diff --git a/tests/ext/mustache_tests.py b/tests/ext/mustache_tests.py index 617f023f..137f78f2 100644 --- a/tests/ext/mustache_tests.py +++ b/tests/ext/mustache_tests.py @@ -7,7 +7,7 @@ from cement.core import exc, foundation, handler, backend, controller from cement.utils import test -class MustacheExtTestCase(test.CementTestCase): +class MustacheExtTestCase(test.CementExtTestCase): def setUp(self): self.app = self.make_app('tests', extensions=['mustache'], diff --git a/tests/utils/fs_tests.py b/tests/utils/fs_tests.py index 67d9d84d..048b0ed0 100644 --- a/tests/utils/fs_tests.py +++ b/tests/utils/fs_tests.py @@ -4,7 +4,7 @@ import os import tempfile from cement.utils import fs, test -class FsUtilsTestCase(test.CementTestCase): +class FsUtilsTestCase(test.CementCoreTestCase): def test_abspath(self): path = fs.abspath('.') self.ok(path.startswith('/')) diff --git a/tests/utils/misc_tests.py b/tests/utils/misc_tests.py index 079f5d9b..557942f4 100644 --- a/tests/utils/misc_tests.py +++ b/tests/utils/misc_tests.py @@ -2,7 +2,7 @@ from cement.utils import test, misc -class BackendTestCase(test.CementTestCase): +class BackendTestCase(test.CementCoreTestCase): def test_defaults(self): defaults = misc.init_defaults('myapp', 'section2', 'section3') defaults['myapp']['debug'] = True diff --git a/tests/utils/shell_tests.py b/tests/utils/shell_tests.py index 3968b841..715ae83a 100644 --- a/tests/utils/shell_tests.py +++ b/tests/utils/shell_tests.py @@ -6,7 +6,7 @@ from cement.utils import shell, test def add(a, b): return a + b -class ShellUtilsTestCase(test.CementTestCase): +class ShellUtilsTestCase(test.CementCoreTestCase): def test_exec_cmd(self): out, err, ret = shell.exec_cmd(['echo', 'KAPLA!']) self.eq(ret, 0) diff --git a/tests/utils/version_tests.py b/tests/utils/version_tests.py index 8ba15772..01417950 100644 --- a/tests/utils/version_tests.py +++ b/tests/utils/version_tests.py @@ -2,7 +2,7 @@ from cement.utils import version, test -class VersionUtilsTestCase(test.CementTestCase): +class VersionUtilsTestCase(test.CementCoreTestCase): def test_get_version(self): ver = version.get_version() self.ok(ver.startswith('2.1'))