cliapp-1.20130808/0000755000175000017500000000000012200776452013261 5ustar jenkinsjenkinscliapp-1.20130808/cliapp/0000755000175000017500000000000012200776452014531 5ustar jenkinsjenkinscliapp-1.20130808/cliapp/__init__.py0000644000175000017500000000224412200776452016644 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. __version__ = '1.20130808' from fmt import TextFormat from settings import (Settings, log_group_name, config_group_name, perf_group_name) from runcmd import runcmd, runcmd_unchecked, shell_quote, ssh_runcmd from app import Application, AppException # The plugin system from hook import Hook, FilterHook from hookmgr import HookManager from plugin import Plugin from pluginmgr import PluginManager __all__ = locals() cliapp-1.20130808/cliapp/runcmd.py0000644000175000017500000002112612200776452016375 0ustar jenkinsjenkins# Copyright (C) 2011, 2012 Lars Wirzenius # Copyright (C) 2012 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno import fcntl import logging import os import select import subprocess import cliapp def runcmd(argv, *args, **kwargs): '''Run external command or pipeline. Example: ``runcmd(['grep', 'foo'], ['wc', '-l'], feed_stdin='foo\nbar\n')`` Return the standard output of the command. Raise ``cliapp.AppException`` if external command returns non-zero exit code. ``*args`` and ``**kwargs`` are passed onto ``subprocess.Popen``. ''' our_options = ( ('ignore_fail', False), ('log_error', True), ) opts = {} for name, default in our_options: opts[name] = default if name in kwargs: opts[name] = kwargs[name] del kwargs[name] exit, out, err = runcmd_unchecked(argv, *args, **kwargs) if exit != 0: msg = 'Command failed: %s\n%s' % (' '.join(argv), err) if opts['ignore_fail']: if opts['log_error']: logging.info(msg) else: if opts['log_error']: logging.error(msg) raise cliapp.AppException(msg) return out def runcmd_unchecked(argv, *argvs, **kwargs): '''Run external command or pipeline. Return the exit code, and contents of standard output and error of the command. See also ``runcmd``. ''' argvs = [argv] + list(argvs) logging.debug('run external command: %s' % repr(argvs)) def pop_kwarg(name, default): if name in kwargs: value = kwargs[name] del kwargs[name] return value else: return default feed_stdin = pop_kwarg('feed_stdin', '') pipe_stdin = pop_kwarg('stdin', subprocess.PIPE) pipe_stdout = pop_kwarg('stdout', subprocess.PIPE) pipe_stderr = pop_kwarg('stderr', subprocess.PIPE) try: pipeline = _build_pipeline(argvs, pipe_stdin, pipe_stdout, pipe_stderr, kwargs) return _run_pipeline(pipeline, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr) except OSError, e: # pragma: no cover if e.errno == errno.ENOENT and e.filename is None: e.filename = argv[0] raise e else: raise def _build_pipeline(argvs, pipe_stdin, pipe_stdout, pipe_stderr, kwargs): procs = [] for i, argv in enumerate(argvs): if i == 0 and i == len(argvs) - 1: stdin = pipe_stdin stdout = pipe_stdout stderr = pipe_stderr elif i == 0: stdin = pipe_stdin stdout = subprocess.PIPE stderr = pipe_stderr elif i == len(argvs) - 1: stdin = procs[-1].stdout stdout = pipe_stdout stderr = pipe_stderr else: stdin = procs[-1].stdout stdout = subprocess.PIPE stderr = pipe_stderr p = subprocess.Popen(argv, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, **kwargs) procs.append(p) return procs def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): stdout_eof = False stderr_eof = False out = [] err = [] pos = 0 io_size = 1024 def set_nonblocking(fd): flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) flags = flags | os.O_NONBLOCK fcntl.fcntl(fd, fcntl.F_SETFL, flags) if feed_stdin and pipe_stdin == subprocess.PIPE: set_nonblocking(procs[0].stdin.fileno()) if pipe_stdout == subprocess.PIPE: set_nonblocking(procs[-1].stdout.fileno()) if pipe_stderr == subprocess.PIPE: set_nonblocking(procs[-1].stderr.fileno()) def still_running(): for p in procs: p.poll() for p in procs: if p.returncode is None: return True if pipe_stdout == subprocess.PIPE and not stdout_eof: return True if pipe_stderr == subprocess.PIPE and not stderr_eof: return True # pragma: no cover return False while still_running(): rlist = [] if not stdout_eof and pipe_stdout == subprocess.PIPE: rlist.append(procs[-1].stdout) if not stderr_eof and pipe_stderr == subprocess.PIPE: rlist.append(procs[-1].stderr) wlist = [] if pipe_stdin == subprocess.PIPE and pos < len(feed_stdin): wlist.append(procs[0].stdin) if rlist or wlist: try: r, w, x = select.select(rlist, wlist, []) except select.error, e: # pragma: no cover err, msg = e.args if err == errno.EINTR: break raise else: break # Let's not busywait waiting for processes to die. if procs[0].stdin in w and pos < len(feed_stdin): data = feed_stdin[pos : pos+io_size] procs[0].stdin.write(data) pos += len(data) if pos >= len(feed_stdin): procs[0].stdin.close() if procs[-1].stdout in r: data = procs[-1].stdout.read(io_size) if data: out.append(data) else: stdout_eof = True if procs[-1].stderr in r: data = procs[-1].stderr.read(io_size) if data: err.append(data) else: stderr_eof = True while still_running(): for p in procs: if p.returncode is None: p.wait() errorcodes = [p.returncode for p in procs if p.returncode != 0] or [0] return errorcodes[-1], ''.join(out), ''.join(err) def shell_quote(s): '''Return a shell-quoted version of s.''' lower_ascii = 'abcdefghijklmnopqrstuvwxyz' upper_ascii = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' digits = '0123456789' punctuation = '-_/=.,:' safe = set(lower_ascii + upper_ascii + digits + punctuation) quoted = [] for c in s: if c in safe: quoted.append(c) elif c == "'": quoted.append('"\'"') else: quoted.append("'%c'" % c) return ''.join(quoted) def ssh_runcmd(target, argv, **kwargs): # pragma: no cover '''Run command in argv on remote host target. This is similar to runcmd, but the command is run on the remote machine. The command is given as an argv array; elements in the array are automatically quoted so they get passed to the other side correctly. An optional ``tty=`` parameter can be passed to ``ssh_runcmd`` in order to force or disable pseudo-tty allocation. This is often required to run ``sudo`` on another machine and might be useful in other situations as well. Supported values are ``tty=True`` for forcing tty allocation, ``tty=False`` for disabling it and ``tty=None`` for not passing anything tty related to ssh. With the ``tty`` option, ``cliapp.runcmd(['ssh', '-tt', 'user@host', '--', 'sudo', 'ls'])`` can be written as ``cliapp.ssh_runcmd('user@host', ['sudo', 'ls'], tty=True)`` which is more intuitive. The target is given as-is to ssh, and may use any syntax ssh accepts. Environment variables may or may not be passed to the remote machine: this is dependent on the ssh and sshd configurations. Invoke env(1) explicitly to pass in the variables you need to exist on the other end. Pipelines are not supported. ''' tty = kwargs.get('tty', None) if tty: ssh_cmd = ['ssh', '-tt', target, '--'] elif tty is False: ssh_cmd = ['ssh', '-T', target, '--'] else: ssh_cmd = ['ssh', target, '--'] if 'tty' in kwargs: del kwargs['tty'] local_argv = ssh_cmd + map(shell_quote, argv) return runcmd(local_argv, **kwargs) cliapp-1.20130808/cliapp/app.py0000644000175000017500000005651712200776452015701 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno import gc import inspect import logging import logging.handlers import os import StringIO import sys import traceback import time import platform import textwrap import cliapp class AppException(Exception): '''Base class for application specific exceptions. Any exceptions that are subclasses of this one get printed as nice errors to the user. Any other exceptions cause a Python stack trace to be written to stderr. ''' def __init__(self, msg): self.msg = msg def __str__(self): return self.msg class LogHandler(logging.handlers.RotatingFileHandler): # pragma: no cover '''Like RotatingFileHandler, but set permissions of new files.''' def __init__(self, filename, perms=0600, *args, **kwargs): self._perms = perms logging.handlers.RotatingFileHandler.__init__(self, filename, *args, **kwargs) def _open(self): if not os.path.exists(self.baseFilename): flags = os.O_CREAT | os.O_WRONLY fd = os.open(self.baseFilename, flags, self._perms) os.close(fd) return logging.handlers.RotatingFileHandler._open(self) class Application(object): '''A framework for Unix-like command line programs. The user should subclass this base class for each application. The subclass does not need code for the mundane, boilerplate parts that are the same in every utility, and can concentrate on the interesting part that is unique to it. To start the application, call the `run` method. The ``progname`` argument sets tne name of the program, which is used for various purposes, such as determining the name of the configuration file. Similarly, ``version`` sets the version number of the program. ``description`` and ``epilog`` are included in the output of ``--help``. They are formatted to fit the screen. Unlike the default behavior of ``optparse``, empty lines separate paragraphs. ''' def __init__(self, progname=None, version='0.0.0', description=None, epilog=None): self.fileno = 0 self.global_lineno = 0 self.lineno = 0 self._description = description if not hasattr(self, 'arg_synopsis'): self.arg_synopsis = '[FILE]...' if not hasattr(self, 'cmd_synopsis'): self.cmd_synopsis = {} self.subcommands = {} self.subcommand_aliases = {} self.hidden_subcommands = set() for method_name in self._subcommand_methodnames(): cmd = self._unnormalize_cmd(method_name) self.subcommands[cmd] = getattr(self, method_name) self.settings = cliapp.Settings(progname, version, usage=self._format_usage, description=self._format_description, epilog=epilog) self.plugin_subdir = 'plugins' # For meliae memory dumps. self.memory_dump_counter = 0 self.last_memory_dump = 0 # For process duration. self._started = os.times()[-1] def add_settings(self): '''Add application specific settings.''' def run(self, args=None, stderr=sys.stderr, sysargv=sys.argv, log=logging.critical): '''Run the application.''' def run_it(): self._run(args=args, stderr=stderr, log=log) if self.settings.progname is None and sysargv: self.settings.progname = os.path.basename(sysargv[0]) envname = '%s_PROFILE' % self._envname(self.settings.progname) profname = os.environ.get(envname, '') if profname: # pragma: no cover import cProfile cProfile.runctx('run_it()', globals(), locals(), profname) else: run_it() def _envname(self, progname): '''Create an environment variable name of the name of a program.''' basename = os.path.basename(progname) if '.' in basename: basename = basename.split('.')[0] ok = 'abcdefghijklmnopqrstuvwxyz0123456789' ok += ok.upper() return ''.join(x.upper() if x in ok else '_' for x in basename) def _set_process_name(self): # pragma: no cover comm = '/proc/self/comm' if platform.system() == 'Linux' and os.path.exists(comm): with open('/proc/self/comm', 'w', 0) as f: f.write(self.settings.progname[:15]) def _run(self, args=None, stderr=sys.stderr, log=logging.critical): try: self._set_process_name() self.add_settings() self.setup_plugin_manager() # A little bit of trickery here to make --no-default-configs and # --config=foo work right: we first parse the command line once, # and pick up any config files. Then we read configs. Finally, # we re-parse the command line to allow any options to override # config file settings. self.setup() self.enable_plugins() if self.subcommands: self.add_default_subcommands() args = sys.argv[1:] if args is None else args self.parse_args(args, configs_only=True) self.settings.load_configs() args = self.parse_args(args) self.setup_logging() self.log_config() if self.settings['output']: self.output = open(self.settings['output'], 'w') else: self.output = sys.stdout self.process_args(args) self.cleanup() self.disable_plugins() except AppException, e: log(traceback.format_exc()) stderr.write('ERROR: %s\n' % str(e)) sys.exit(1) except SystemExit, e: if e.code is not None and type(e.code) != int: log(str(e)) stderr.write('ERROR: %s\n' % str(e)) sys.exit(e.code if type(e.code) == int else 1) except KeyboardInterrupt, e: sys.exit(255) except IOError, e: # pragma: no cover if e.errno == errno.EPIPE and e.filename is None: # We're writing to stdout, and it broke. This almost always # happens when we're being piped to less, and the user quits # less before we finish writing everything out. So we ignore # the error in that case. sys.exit(1) log(traceback.format_exc()) stderr.write('ERROR: %s\n' % str(e)) sys.exit(1) except OSError, e: # pragma: no cover log(traceback.format_exc()) if hasattr(e, 'filename') and e.filename: stderr.write('ERROR: %s: %s\n' % (e.filename, e.strerror)) else: stderr.write('ERROR: %s\n' % e.strerror) sys.exit(1) except BaseException, e: # pragma: no cover log(traceback.format_exc()) stderr.write(traceback.format_exc()) sys.exit(1) logging.info('%s version %s ends normally' % (self.settings.progname, self.settings.version)) def compute_setting_values(self, settings): '''Compute setting values after configs and options are parsed. You can override this method to implement a default value for a setting that is dependent on another setting. For example, you might have settings "url" and "protocol", where protocol gets set based on the schema of the url, unless explicitly set by the user. So if the user sets just the url, to "http://www.example.com/", the protocol would be set to "http". If the user sets both url and protocol, the protocol does not get modified by compute_setting_values. ''' def add_subcommand( self, name, func, arg_synopsis=None, aliases=None, hidden=False): '''Add a subcommand. Normally, subcommands are defined by add ``cmd_foo`` methods to the application class. However, sometimes it is more convenient to have them elsewhere (e.g., in plugins). This method allows doing that. The callback function must accept a list of command line non-option arguments. ''' if name not in self.subcommands: self.subcommands[name] = func self.cmd_synopsis[name] = arg_synopsis self.subcommand_aliases[name] = aliases or [] if hidden: # pragma: no cover self.hidden_subcommands.add(name) def add_default_subcommands(self): if 'help' not in self.subcommands: self.add_subcommand('help', self.help) if 'help-all' not in self.subcommands: self.add_subcommand('help-all', self.help_all) def _help_helper(self, args, show_all): # pragma: no cover try: width = int(os.environ.get('COLUMNS', '78')) except ValueError: width = 78 fmt = cliapp.TextFormat(width=width) if args: usage = self._format_usage_for(args[0]) description = fmt.format(self._format_subcommand_help(args[0])) text = '%s\n\n%s' % (usage, description) else: usage = self._format_usage(all=show_all) description = fmt.format(self._format_description(all=show_all)) text = '%s\n\n%s' % (usage, description) text = self.settings.progname.join(text.split('%prog')) self.output.write(text) def help(self, args): # pragma: no cover '''Print help.''' self._help_helper(args, False) def help_all(self, args): # pragma: no cover '''Print help, including hidden subcommands.''' self._help_helper(args, True) def _subcommand_methodnames(self): return [x for x in dir(self) if x.startswith('cmd_') and inspect.ismethod(getattr(self, x))] def _normalize_cmd(self, cmd): return 'cmd_%s' % cmd.replace('-', '_') def _unnormalize_cmd(self, method): assert method.startswith('cmd_') return method[len('cmd_'):].replace('_', '-') def _format_usage(self, all=False): '''Format usage, possibly also subcommands, if any.''' if self.subcommands: lines = [] prefix = 'Usage:' for cmd in sorted(self.subcommands.keys()): if all or cmd not in self.hidden_subcommands: args = self.cmd_synopsis.get(cmd, '') or '' lines.append( '%s %%prog [options] %s %s' % (prefix, cmd, args)) prefix = ' ' * len(prefix) return '\n'.join(lines) else: return None def _format_usage_for(self, cmd): # pragma: no cover args = self.cmd_synopsis.get(cmd, '') or '' return 'Usage: %%prog [options] %s %s' % (cmd, args) def _format_description(self, all=False): '''Format OptionParser description, with subcommand support.''' if self.subcommands: summaries = [] for cmd in sorted(self.subcommands.keys()): if all or cmd not in self.hidden_subcommands: summaries.append(self._format_subcommand_summary(cmd)) cmd_desc = ''.join(summaries) return '%s\n%s' % (self._description or '', cmd_desc) else: return self._description def _format_subcommand_summary(self, cmd): # pragma: no cover method = self.subcommands[cmd] doc = method.__doc__ or '' lines = doc.splitlines() if lines: summary = lines[0].strip() else: summary = '' return '* %%prog %s: %s\n' % (cmd, summary) def _format_subcommand_help(self, cmd): # pragma: no cover if cmd not in self.subcommands: raise cliapp.AppException('Unknown subcommand %s' % cmd) method = self.subcommands[cmd] doc = method.__doc__ or '' t = doc.split('\n', 1) if len(t) == 1: return doc else: first, rest = t return first + '\n' + textwrap.dedent(rest) def setup_logging(self): # pragma: no cover '''Set up logging.''' level_name = self.settings['log-level'] levels = { 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL, 'fatal': logging.FATAL, } level = levels.get(level_name, logging.INFO) if self.settings['log'] == 'syslog': handler = self.setup_logging_handler_for_syslog() elif self.settings['log'] and self.settings['log'] != 'none': handler = LogHandler( self.settings['log'], perms=int(self.settings['log-mode'], 8), maxBytes=self.settings['log-max'], backupCount=self.settings['log-keep'], delay=False) fmt = '%(asctime)s %(levelname)s %(message)s' datefmt = '%Y-%m-%d %H:%M:%S' formatter = logging.Formatter(fmt, datefmt) handler.setFormatter(formatter) else: handler = self.setup_logging_handler_to_none() # reduce amount of pointless I/O level = logging.FATAL logger = logging.getLogger() logger.addHandler(handler) logger.setLevel(level) def setup_logging_handler_for_syslog(self): # pragma: no cover '''Setup a logging.Handler for logging to syslog.''' handler = logging.handlers.SysLogHandler(address='/dev/log') progname = '%%'.join(self.settings.progname.split('%')) fmt = progname + ": %(levelname)s %(message)s" formatter = logging.Formatter(fmt) handler.setFormatter(formatter) return handler def setup_logging_handler_to_none(self): # pragma: no cover '''Setup a logging.Handler that does not log anything anywhere.''' handler = logging.FileHandler('/dev/null') return handler def setup_logging_handler_to_file(self): # pragma: no cover '''Setup a logging.Handler for logging to a named file.''' handler = LogHandler( self.settings['log'], perms=int(self.settings['log-mode'], 8), maxBytes=self.settings['log-max'], backupCount=self.settings['log-keep'], delay=False) fmt = self.setup_logging_format() datefmt = self.setup_logging_timestamp() formatter = logging.Formatter(fmt, datefmt) handler.setFormatter(formatter) return handler def setup_logging_format(self): # pragma: no cover '''Return format string for log messages.''' return '%(asctime)s %(levelname)s %(message)s' def setup_logging_timestamp(self): # pragma: no cover '''Return timestamp format string for log message.''' return '%Y-%m-%d %H:%M:%S' def log_config(self): logging.info('%s version %s starts' % (self.settings.progname, self.settings.version)) logging.debug('sys.argv: %s' % sys.argv) logging.debug('environment variables:') for name in os.environ: logging.debug('environment: %s=%s' % (name, os.environ[name])) cp = self.settings.as_cp() f = StringIO.StringIO() cp.write(f) logging.debug('Config:\n%s' % f.getvalue()) logging.debug('Python version: %s' % sys.version) def app_directory(self): '''Return the directory where the application class is defined. Plugins are searched relative to this directory, in the subdirectory specified by self.plugin_subdir. ''' module_name = self.__class__.__module__ module = sys.modules[module_name] dirname = os.path.dirname(module.__file__) or '.' return dirname def setup_plugin_manager(self): '''Create a plugin manager.''' self.pluginmgr = cliapp.PluginManager() dirname = os.path.join(self.app_directory(), self.plugin_subdir) self.pluginmgr.locations = [dirname] def enable_plugins(self): # pragma: no cover '''Load plugins.''' for plugin in self.pluginmgr.plugins: plugin.app = self plugin.setup() self.pluginmgr.enable_plugins() def disable_plugins(self): self.pluginmgr.disable_plugins() def parse_args(self, args, configs_only=False): '''Parse the command line. Return list of non-option arguments. ''' return self.settings.parse_args( args, configs_only=configs_only, arg_synopsis=self.arg_synopsis, cmd_synopsis=self.cmd_synopsis, compute_setting_values=self.compute_setting_values) def setup(self): '''Prepare for process_args. This method is called just before enabling plugins. By default it does nothing, but subclasses may override it with a suitable implementation. This is easier than overriding process_args itself. ''' def cleanup(self): '''Clean up after process_args. This method is called just after process_args. By default it does nothing, but subclasses may override it with a suitable implementation. This is easier than overriding process_args itself. ''' def process_args(self, args): '''Process command line non-option arguments. The default is to call process_inputs with the argument list, or to invoke the requested subcommand, if subcommands have been defined. ''' if self.subcommands: if not args: raise SystemExit('must give subcommand') cmd = args[0] if cmd not in self.subcommands: for name in self.subcommand_aliases: if cmd in self.subcommand_aliases[name]: cmd = name break else: raise SystemExit('unknown subcommand %s' % args[0]) method = self.subcommands[cmd] method(args[1:]) else: self.process_inputs(args) def process_inputs(self, args): '''Process all arguments as input filenames. The default implementation calls process_input for each input filename. If no filenames were given, then process_input is called with ``-`` as the argument name. This implements the usual Unix command line practice of reading from stdin if no inputs are named. The attributes ``fileno``, ``global_lineno``, and ``lineno`` are set, and count files and lines. The global line number is the line number as if all input files were one. ''' for arg in args or ['-']: self.process_input(arg) def open_input(self, name, mode='r'): '''Open an input file for reading. The default behaviour is to open a file named on the local filesystem. A subclass might override this behavior for URLs, for example. The optional mode argument speficies the mode in which the file gets opened. It should allow reading. Some files should perhaps be opened in binary mode ('rb') instead of the default text mode. ''' if name == '-': return sys.stdin else: return open(name, mode) def process_input(self, name, stdin=sys.stdin): '''Process a particular input file. The ``stdin`` argument is meant for unit test only. ''' self.fileno += 1 self.lineno = 0 f = self.open_input(name) for line in f: self.global_lineno += 1 self.lineno += 1 self.process_input_line(name, line) if f != stdin: f.close() def process_input_line(self, filename, line): '''Process one line of the input file. Applications that are line-oriented can redefine only this method in a subclass, and should not need to care about the other methods. ''' def runcmd(self, *args, **kwargs): # pragma: no cover return cliapp.runcmd(*args, **kwargs) def runcmd_unchecked(self, *args, **kwargs): # pragma: no cover return cliapp.runcmd_unchecked(*args, **kwargs) def _vmrss(self): # pragma: no cover '''Return current resident memory use, in KiB.''' f = open('/proc/self/status') rss = 0 for line in f: if line.startswith('VmRSS'): rss = line.split()[1] f.close() return rss def dump_memory_profile(self, msg): # pragma: no cover '''Log memory profiling information. Get the memory profiling method from the dump-memory-profile setting, and log the results at DEBUG level. ``msg`` is a message the caller provides to identify at what point the profiling happens. ''' kind = self.settings['dump-memory-profile'] interval = self.settings['memory-dump-interval'] if kind == 'none': return now = time.time() if self.last_memory_dump + interval > now: return self.last_memory_dump = now # Log wall clock and CPU times for self, children. utime, stime, cutime, cstime, elapsed_time = os.times() duration = elapsed_time - self._started logging.debug('process duration: %s s' % duration) logging.debug('CPU time, in process: %s s' % utime) logging.debug('CPU time, in system: %s s' % stime) logging.debug('CPU time, in children: %s s' % cutime) logging.debug('CPU time, in system for children: %s s' % cstime) logging.debug('dumping memory profiling data: %s' % msg) logging.debug('VmRSS: %s KiB' % self._vmrss()) if kind == 'simple': return # These are fairly expensive operations, so we only log them # if we're doing expensive stuff anyway. logging.debug('# objects: %d' % len(gc.get_objects())) logging.debug('# garbage: %d' % len(gc.garbage)) if kind == 'heapy': from guppy import hpy h = hpy() logging.debug('memory profile:\n%s' % h.heap()) elif kind == 'meliae': filename = 'obnam-%d.meliae' % self.memory_dump_counter logging.debug('memory profile: see %s' % filename) from meliae import scanner scanner.dump_all_objects(filename) self.memory_dump_counter += 1 cliapp-1.20130808/cliapp/app_tests.py0000644000175000017500000002650212200776452017112 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import optparse import os import StringIO import sys import tempfile import unittest import cliapp def devnull(msg): pass class AppExceptionTests(unittest.TestCase): def setUp(self): self.e = cliapp.AppException('foo') def test_error_message_contains_foo(self): self.assert_('foo' in str(self.e)) class ApplicationTests(unittest.TestCase): def setUp(self): self.app = cliapp.Application() def test_creates_settings(self): self.assert_(isinstance(self.app.settings, cliapp.Settings)) def test_calls_add_settings_only_in_run(self): class Foo(cliapp.Application): def process_args(self, args): pass def add_settings(self): self.settings.string(['foo'], '') foo = Foo() self.assertFalse('foo' in foo.settings) foo.run(args=[]) self.assert_('foo' in foo.settings) def test_run_uses_string_list_options_only_once(self): class Foo(cliapp.Application): def add_settings(self): self.settings.string_list(['foo'], '') def process_args(self, args): pass foo = Foo() foo.run(args=['--foo=yo']) self.assertEqual(foo.settings['foo'], ['yo']) def test_run_sets_up_logging(self): self.called = False def setup(): self.called = True self.app.setup_logging = setup self.app.process_args = lambda args: None self.app.run([]) self.assert_(self.called) def test_run_sets_progname_from_sysargv0(self): self.app.process_args = lambda args: None self.app.run(args=[], sysargv=['foo']) self.assertEqual(self.app.settings.progname, 'foo') def test_run_calls_process_args(self): self.called = None self.app.process_args = lambda args: setattr(self, 'called', args) self.app.run(args=['foo', 'bar']) self.assertEqual(self.called, ['foo', 'bar']) def test_run_processes_input_files(self): self.inputs = [] self.app.process_input = lambda name: self.inputs.append(name) self.app.run(args=['foo', 'bar']) self.assertEqual(self.inputs, ['foo', 'bar']) def test_run_sets_output_attribute(self): self.app.process_args = lambda args: None self.app.run(args=[]) self.assertEqual(self.app.output, sys.stdout) def test_run_sets_output_to_file_if_output_option_is_set(self): self.app.process_args = lambda args: None self.app.run(args=['--output=/dev/null']) self.assertEqual(self.app.output.name, '/dev/null') def test_run_calls_parse_args(self): class DummyOptions(object): def __init__(self): self.output = None self.log = None self.called = None self.app.parse_args = lambda args, **kw: setattr(self, 'called', args) self.app.process_args = lambda args: None self.app.settings.options = DummyOptions() self.app.run(args=['foo', 'bar']) self.assertEqual(self.called, ['foo', 'bar']) def test_makes_envname_correctly(self): self.assertEqual(self.app._envname('foo'), 'FOO') self.assertEqual(self.app._envname('foo.py'), 'FOO') self.assertEqual(self.app._envname('foo bar'), 'FOO_BAR') self.assertEqual(self.app._envname('foo-bar'), 'FOO_BAR') self.assertEqual(self.app._envname('foo/bar'), 'BAR') self.assertEqual(self.app._envname('foo_bar'), 'FOO_BAR') def test_parses_options(self): self.app.settings.string(['foo'], 'foo help') self.app.settings.boolean(['bar'], 'bar help') self.app.parse_args(['--foo=foovalue', '--bar']) self.assertEqual(self.app.settings['foo'], 'foovalue') self.assertEqual(self.app.settings['bar'], True) def test_calls_setup(self): class App(cliapp.Application): def setup(self): self.setup_called = True def process_inputs(self, args): pass app = App() app.run(args=[]) self.assertTrue(app.setup_called) def test_calls_cleanup(self): class App(cliapp.Application): def cleanup(self): self.cleanup_called = True def process_inputs(self, args): pass app = App() app.run(args=[]) self.assertTrue(app.cleanup_called) def test_process_args_calls_process_inputs(self): self.called = False def process_inputs(args): self.called = True self.app.process_inputs = process_inputs self.app.process_args([]) self.assert_(self.called) def test_process_inputs_calls_process_input_for_each_arg(self): self.args = [] def process_input(arg): self.args.append(arg) self.app.process_input = process_input self.app.process_inputs(['foo', 'bar']) self.assertEqual(self.args, ['foo', 'bar']) def test_process_inputs_calls_process_input_with_dash_if_no_inputs(self): self.args = [] def process_input(arg): self.args.append(arg) self.app.process_input = process_input self.app.process_inputs([]) self.assertEqual(self.args, ['-']) def test_open_input_opens_file(self): f = self.app.open_input('/dev/null') self.assert_(isinstance(f, file)) self.assertEqual(f.mode, 'r') def test_open_input_opens_file_in_binary_mode_when_requested(self): f = self.app.open_input('/dev/null', mode='rb') self.assertEqual(f.mode, 'rb') def test_open_input_opens_stdin_if_dash_given(self): self.assertEqual(self.app.open_input('-'), sys.stdin) def test_process_input_calls_open_input(self): self.called = None def open_input(name): self.called = name return StringIO.StringIO('') self.app.open_input = open_input self.app.process_input('foo') self.assertEqual(self.called, 'foo') def test_process_input_does_not_close_stdin(self): self.closed = False def close(): self.closed = True f = StringIO.StringIO('') f.close = close def open_input(name): if name == '-': return f self.app.open_input = open_input self.app.process_input('-', stdin=f) self.assertEqual(self.closed, False) def test_processes_input_lines(self): lines = [] class Foo(cliapp.Application): def open_input(self, name): return StringIO.StringIO(''.join('%s%d\n' % (name, i) for i in range(2))) def process_input_line(self, name, line): lines.append(line) foo = Foo() foo.run(args=['foo', 'bar']) self.assertEqual(lines, ['foo0\n', 'foo1\n', 'bar0\n', 'bar1\n']) def test_process_input_line_can_access_counters(self): counters = [] class Foo(cliapp.Application): def open_input(self, name): return StringIO.StringIO(''.join('%s%d\n' % (name, i) for i in range(2))) def process_input_line(self, name, line): counters.append((self.fileno, self.global_lineno, self.lineno)) foo = Foo() foo.run(args=['foo', 'bar']) self.assertEqual(counters, [(1, 1, 1), (1, 2, 2), (2, 3, 1), (2, 4, 2)]) def test_run_prints_out_error_for_appexception(self): def raise_error(args): raise cliapp.AppException('xxx') self.app.process_args = raise_error f = StringIO.StringIO() self.assertRaises(SystemExit, self.app.run, [], stderr=f, log=devnull) self.assert_('xxx' in f.getvalue()) def test_run_prints_out_stack_trace_for_not_appexception(self): def raise_error(args): raise Exception('xxx') self.app.process_args = raise_error f = StringIO.StringIO() self.assertRaises(SystemExit, self.app.run, [], stderr=f, log=devnull) self.assert_('Traceback' in f.getvalue()) def test_run_raises_systemexit_for_systemexit(self): def raise_error(args): raise SystemExit(123) self.app.process_args = raise_error f = StringIO.StringIO() self.assertRaises(SystemExit, self.app.run, [], stderr=f, log=devnull) def test_run_raises_systemexit_for_keyboardint(self): def raise_error(args): raise KeyboardInterrupt() self.app.process_args = raise_error f = StringIO.StringIO() self.assertRaises(SystemExit, self.app.run, [], stderr=f, log=devnull) class DummySubcommandApp(cliapp.Application): def cmd_foo(self, args): self.foo_called = True class SubcommandTests(unittest.TestCase): def setUp(self): self.app = DummySubcommandApp() self.trash = StringIO.StringIO() def test_lists_subcommands(self): self.assertEqual(self.app._subcommand_methodnames(), ['cmd_foo']) def test_normalizes_subcommand(self): self.assertEqual(self.app._normalize_cmd('foo'), 'cmd_foo') self.assertEqual(self.app._normalize_cmd('foo-bar'), 'cmd_foo_bar') def test_raises_error_for_no_subcommand(self): self.assertRaises(SystemExit, self.app.run, [], stderr=self.trash, log=devnull) def test_raises_error_for_unknown_subcommand(self): self.assertRaises(SystemExit, self.app.run, ['what?'], stderr=self.trash, log=devnull) def test_calls_subcommand_method(self): self.app.run(['foo'], stderr=self.trash, log=devnull) self.assert_(self.app.foo_called) def test_calls_subcommand_method_via_alias(self): self.bar_called = False def bar(*args): self.bar_called = True self.app.add_subcommand('bar', bar, aliases=['yoyo']) self.app.run(['yoyo'], stderr=self.trash, log=devnull) self.assertTrue(self.bar_called) def test_adds_default_subcommand_help(self): self.app.run(['foo'], stderr=self.trash, log=devnull) self.assertTrue('help' in self.app.subcommands) class ExtensibleSubcommandTests(unittest.TestCase): def setUp(self): self.app = cliapp.Application() def test_lists_no_subcommands(self): self.assertEqual(self.app.subcommands, {}) def test_adds_subcommand(self): help = lambda args: None self.app.add_subcommand('foo', help) self.assertEqual(self.app.subcommands, {'foo': help}) cliapp-1.20130808/cliapp/fmt.py0000644000175000017500000000667412200776452015706 0ustar jenkinsjenkins# Copyright (C) 2013 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Simplistic text re-formatter. This module format text, paragraph by paragraph, so it is somewhat nice-looking, with no line too long, and short lines joined. In other words, like what the textwrap library does. However, it extends textwrap by recognising bulleted lists. ''' import textwrap class Paragraph(object): def __init__(self): self._lines = [] def append(self, line): self._lines.append(line) def _oneliner(self): return ' '.join(' '.join(x.split()) for x in self._lines) def fill(self, width): filled = textwrap.fill(self._oneliner(), width=width) return filled class BulletPoint(Paragraph): def fill(self, width): text = self._oneliner() assert text.startswith('* ') filled = textwrap.fill(text[2:], width=width - 2) lines = [' %s' % x for x in filled.splitlines(True)] lines[0] = '* %s' % lines[0][2:] return ''.join(lines) class EmptyLine(Paragraph): def fill(self, width): return '' class TextFormat(object): def __init__(self, width=78): self._width = width def format(self, text): '''Return input string, but formatted nicely.''' filled_paras = [] for para in self._paragraphs(text): filled_paras.append(para.fill(self._width)) filled = '\n'.join(filled_paras) if text and not filled.endswith('\n'): filled += '\n' return filled def _paragraphs(self, text): def is_empty(line): return line.strip() == '' def is_bullet(line): return line.startswith('* ') def is_continuation(line): return line.startswith(' ') current = None in_list = False for line in text.splitlines(True): if in_list and is_continuation(line): assert current is not None current.append(line) elif is_bullet(line): if current: yield current if not in_list: yield EmptyLine() current = BulletPoint() current.append(line) in_list = True elif is_empty(line): if current: yield current yield EmptyLine() current = None in_list = False else: if in_list: yield current yield EmptyLine() current = None if not current: current = Paragraph() current.append(line) in_list = False if current: yield current cliapp-1.20130808/cliapp/fmt_tests.py0000644000175000017500000000410212200776452017110 0ustar jenkinsjenkins# Copyright (C) 2013 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import cliapp class TextFormatTests(unittest.TestCase): def setUp(self): self.fmt = cliapp.TextFormat(width=10) def test_returns_empty_string_for_empty_string(self): self.assertEqual(self.fmt.format(''), '') def test_returns_short_one_line_paragraph_as_is(self): self.assertEqual(self.fmt.format('foo bar'), 'foo bar\n') def test_collapse_multiple_spaces_into_one(self): self.assertEqual(self.fmt.format('foo bar'), 'foo bar\n') def test_wraps_long_line(self): self.assertEqual(self.fmt.format('foobar word'), 'foobar\nword\n') def test_handles_paragraphs(self): self.assertEqual( self.fmt.format('foo\nbar\n\nyo\nyo\n'), 'foo bar\n\nyo yo\n') def test_collapses_more_than_two_empty_lines(self): self.assertEqual( self.fmt.format('foo\nbar\n\n\n\n\n\n\n\n\n\nyo\nyo\n'), 'foo bar\n\nyo yo\n') def test_handles_bulleted_lists(self): self.assertEqual( self.fmt.format('foo\nbar\n\n* yo\n* a\n and b\n\nword'), 'foo bar\n\n* yo\n* a and b\n\nword\n') def test_handles_bulleted_lists_without_surrounding_empty_lines(self): self.assertEqual( self.fmt.format('foo\nbar\n* yo\n* a\n and b\nword'), 'foo bar\n\n* yo\n* a and b\n\nword\n') cliapp-1.20130808/cliapp/genman.py0000644000175000017500000001212712200776452016353 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import optparse import re class ManpageGenerator(object): '''Fill in a manual page template from an OptionParser instance.''' def __init__(self, template, parser, arg_synopsis, cmd_synopsis): self.template = template self.parser = parser self.arg_synopsis = arg_synopsis self.cmd_synopsis = cmd_synopsis def sort_options(self, options): return sorted(options, key=lambda o: (o._long_opts + o._short_opts)[0]) def option_list(self, container): return self.sort_options(container.option_list) @property def options(self): return self.option_list(self.parser) def format_template(self): sections = (('SYNOPSIS', self.format_synopsis()), ('OPTIONS', self.format_options())) text = self.template for section, contents in sections: pattern = '\n.SH %s\n' % section text = text.replace(pattern, pattern + contents) return text def format_synopsis(self): lines = [] lines += ['.nh'] lines += ['.B %s' % self.esc_dashes(self.parser.prog)] all_options = self.option_list(self.parser) for group in self.parser.option_groups: all_options += self.option_list(group) for option in self.sort_options(all_options): for spec in self.format_option_for_synopsis(option): lines += ['.RB [ %s ]' % spec] if self.cmd_synopsis: lines += ['.PP'] for cmd in sorted(self.cmd_synopsis): lines += ['.br', '.B %s' % self.esc_dashes(self.parser.prog), '.RI [ options ]', self.esc_dashes(cmd)] lines += self.format_argspec(self.cmd_synopsis[cmd]) elif self.arg_synopsis: lines += self.format_argspec(self.arg_synopsis) lines += ['.hy'] return ''.join('%s\n' % line for line in lines) def format_option_for_synopsis(self, option): if option.metavar: suffix = '\\fR=\\fI%s' % self.esc_dashes(option.metavar) else: suffix = '' for name in option._short_opts + option._long_opts: yield '%s%s' % (self.esc_dashes(name), suffix) def format_options(self): lines = [] for option in self.sort_options(self.parser.option_list): lines += self.format_option_for_options(option) for group in self.parser.option_groups: lines += ['.SS "%s"' % group.title] for option in self.sort_options(group.option_list): lines += self.format_option_for_options(option) return ''.join('%s\n' % line for line in lines) def format_option_for_options(self, option): lines = [] lines += ['.TP'] shorts = [self.esc_dashes(x) for x in option._short_opts] if option.metavar: longs = ['%s =\\fI%s' % (self.esc_dashes(x), option.metavar) for x in option._long_opts] else: longs = ['%s' % self.esc_dashes(x) for x in option._long_opts] lines += ['.BR ' + ' ", " '.join(shorts + longs)] lines += [self.esc_dots(self.expand_default(option).strip())] return lines def expand_default(self, option): default = self.parser.defaults.get(option.dest) if default is optparse.NO_DEFAULT or default is None: default = 'none' else: default = str(default) return option.help.replace('%default', default) def esc_dashes(self, optname): return '\\-'.join(optname.split('-')) def esc_dots(self, line): if line.startswith('.'): return '\\' + line else: return line def format_argspec(self, argspec): roman = re.compile(r'[^A-Z]+') italic = re.compile(r'[A-Z]+') words = ['.RI'] while argspec: m = roman.match(argspec) if m: words += [self.esc_dashes(m.group(0))] argspec = argspec[m.end():] else: words += ['""'] m = italic.match(argspec) if m: words += [self.esc_dashes(m.group(0))] argspec = argspec[m.end():] else: words += ['""'] return [' '.join(words)] cliapp-1.20130808/cliapp/hook.py0000644000175000017500000000462112200776452016046 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Hooks with callbacks. In order to de-couple parts of the application, especially when plugins are used, hooks can be used. A hook is a location in the application code where plugins may want to do something. Each hook has a name and a list of callbacks. The application defines the name and the location where the hook will be invoked, and the plugins (or other parts of the application) will register callbacks. ''' class Hook(object): '''A hook.''' def __init__(self): self.callbacks = [] def add_callback(self, callback): '''Add a callback to this hook. Return an identifier that can be used to remove this callback. ''' if callback not in self.callbacks: self.callbacks.append(callback) return callback def call_callbacks(self, *args, **kwargs): '''Call all callbacks with the given arguments.''' for callback in self.callbacks: callback(*args, **kwargs) def remove_callback(self, callback_id): '''Remove a specific callback.''' if callback_id in self.callbacks: self.callbacks.remove(callback_id) class FilterHook(Hook): '''A hook which filters data through callbacks. Every hook of this type accepts a piece of data as its first argument Each callback gets the return value of the previous one as its argument. The caller gets the value of the final callback. Other arguments (with or without keywords) are passed as-is to each callback. ''' def call_callbacks(self, data, *args, **kwargs): for callback in self.callbacks: data = callback(data, *args, **kwargs) return data cliapp-1.20130808/cliapp/hook_tests.py0000644000175000017500000000520712200776452017271 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest from cliapp import Hook, FilterHook class HookTests(unittest.TestCase): def setUp(self): self.hook = Hook() def callback(self, *args, **kwargs): self.args = args self.kwargs = kwargs def test_has_no_callbacks_by_default(self): self.assertEqual(self.hook.callbacks, []) def test_adds_callback(self): self.hook.add_callback(self.callback) self.assertEqual(self.hook.callbacks, [self.callback]) def test_adds_callback_only_once(self): self.hook.add_callback(self.callback) self.hook.add_callback(self.callback) self.assertEqual(self.hook.callbacks, [self.callback]) def test_calls_callback(self): self.hook.add_callback(self.callback) self.hook.call_callbacks('bar', kwarg='foobar') self.assertEqual(self.args, ('bar',)) self.assertEqual(self.kwargs, { 'kwarg': 'foobar' }) def test_removes_callback(self): cb_id = self.hook.add_callback(self.callback) self.hook.remove_callback(cb_id) self.assertEqual(self.hook.callbacks, []) class FilterHookTests(unittest.TestCase): def setUp(self): self.hook = FilterHook() def callback(self, data, *args, **kwargs): self.args = args self.kwargs = kwargs return data + ['callback'] def test_returns_argument_if_no_callbacks(self): self.assertEqual(self.hook.call_callbacks(['foo']), ['foo']) def test_calls_callback_and_returns_modified_data(self): self.hook.add_callback(self.callback) data = self.hook.call_callbacks([]) self.assertEqual(data, ['callback']) def test_calls_callback_with_extra_args(self): self.hook.add_callback(self.callback) self.hook.call_callbacks(['data'], 'extra', kwextra='kwextra') self.assertEqual(self.args, ('extra',)) self.assertEqual(self.kwargs, { 'kwextra': 'kwextra' }) cliapp-1.20130808/cliapp/hookmgr.py0000644000175000017500000000310212200776452016545 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. class HookManager(object): '''Manage the set of hooks the application defines.''' def __init__(self): self.hooks = {} def new(self, name, hook): '''Add a new hook to the manager. If a hook with that name already exists, nothing happens. ''' if name not in self.hooks: self.hooks[name] = hook def add_callback(self, name, callback): '''Add a callback to a named hook.''' return self.hooks[name].add_callback(callback) def remove_callback(self, name, callback_id): '''Remove a specific callback from a named hook.''' self.hooks[name].remove_callback(callback_id) def call(self, name, *args, **kwargs): '''Call callbacks for a named hook, using given arguments.''' return self.hooks[name].call_callbacks(*args, **kwargs) cliapp-1.20130808/cliapp/hookmgr_tests.py0000644000175000017500000000406712200776452020002 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest from cliapp import HookManager, FilterHook class HookManagerTests(unittest.TestCase): def setUp(self): self.hooks = HookManager() self.hooks.new('foo', FilterHook()) def callback(self, *args, **kwargs): self.args = args self.kwargs = kwargs def test_has_no_tests_initially(self): hooks = HookManager() self.assertEqual(hooks.hooks, {}) def test_adds_new_hook(self): self.assert_(self.hooks.hooks.has_key('foo')) def test_adds_callback(self): self.hooks.add_callback('foo', self.callback) self.assertEqual(self.hooks.hooks['foo'].callbacks, [self.callback]) def test_removes_callback(self): cb_id = self.hooks.add_callback('foo', self.callback) self.hooks.remove_callback('foo', cb_id) self.assertEqual(self.hooks.hooks['foo'].callbacks, []) def test_calls_callbacks(self): self.hooks.add_callback('foo', self.callback) self.hooks.call('foo', 'bar', kwarg='foobar') self.assertEqual(self.args, ('bar',)) self.assertEqual(self.kwargs, { 'kwarg': 'foobar' }) def test_call_returns_value_of_callbacks(self): self.hooks.new('bar', FilterHook()) self.hooks.add_callback('bar', lambda data: data + 1) self.assertEqual(self.hooks.call('bar', 1), 2) cliapp-1.20130808/cliapp/plugin.py0000644000175000017500000000775112200776452016413 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''A generic plugin manager. The plugin manager finds files with plugins and loads them. It looks for plugins in a number of locations specified by the caller. To add a plugin to be loaded, it is enough to put it in one of the locations, and name it *_plugin.py. (The naming convention is to allow having other modules as well, such as unit tests, in the same locations.) ''' import imp import inspect import os class Plugin(object): '''Base class for plugins. A plugin MUST NOT have any side effects when it is instantiated. This is necessary so that it can be safely loaded by unit tests, and so that a user interface can allow the user to disable it, even if it is installed, with no ill effects. Any side effects that would normally happen should occur in the enable() method, and be undone by the disable() method. These methods must be callable any number of times. The subclass MAY define the following attributes: * name * description * version * required_application_version name is the user-visible identifier for the plugin. It defaults to the plugin's classname. description is the user-visible description of the plugin. It may be arbitrarily long, and can use pango markup language. Defaults to the empty string. version is the plugin version. Defaults to '0.0.0'. It MUST be a sequence of integers separated by periods. If several plugins with the same name are found, the newest version is used. Versions are compared integer by integer, starting with the first one, and a missing integer treated as a zero. If two plugins have the same version, either might be used. required_application_version gives the version of the minimal application version the plugin is written for. The first integer must match exactly: if the application is version 2.3.4, the plugin's required_application_version must be at least 2 and at most 2.3.4 to be loaded. Defaults to 0. ''' @property def name(self): return self.__class__.__name__ @property def description(self): return '' @property def version(self): return '0.0.0' @property def required_application_version(self): return '0.0.0' def setup(self): '''Setup plugin. This is called at plugin load time. It should not yet enable the plugin (the ``enable`` method does that), but it might do things like add itself into a hook that adds command line arguments to the application. ''' def enable_wrapper(self): '''Enable plugin. The plugin manager will call this method, which then calls the enable method. Plugins should implement the enable method. The wrapper method is there to allow an application to provide an extended base class that does some application specific magic when plugins are enabled or disabled. ''' self.enable() def disable_wrapper(self): '''Corresponds to enable_wrapper, but for disabling a plugin.''' self.disable() def enable(self): '''Enable the plugin.''' raise NotImplemented() def disable(self): '''Disable the plugin.''' raise NotImplemented() cliapp-1.20130808/cliapp/plugin_tests.py0000644000175000017500000000357312200776452017633 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest from cliapp import Plugin class PluginTests(unittest.TestCase): def setUp(self): self.plugin = Plugin() def test_name_is_class_name(self): self.assertEqual(self.plugin.name, 'Plugin') def test_description_is_empty_string(self): self.assertEqual(self.plugin.description, '') def test_version_is_zeroes(self): self.assertEqual(self.plugin.version, '0.0.0') def test_required_application_version_is_zeroes(self): self.assertEqual(self.plugin.required_application_version, '0.0.0') def test_enable_raises_exception(self): self.assertRaises(Exception, self.plugin.enable) def test_disable_raises_exception(self): self.assertRaises(Exception, self.plugin.disable) def test_enable_wrapper_calls_enable(self): self.plugin.enable = lambda: setattr(self, 'enabled', True) self.plugin.enable_wrapper() self.assert_(self.enabled, True) def test_disable_wrapper_calls_disable(self): self.plugin.disable = lambda: setattr(self, 'disabled', True) self.plugin.disable_wrapper() self.assert_(self.disabled, True) cliapp-1.20130808/cliapp/pluginmgr.py0000644000175000017500000001277712200776452017125 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''A generic plugin manager. The plugin manager finds files with plugins and loads them. It looks for plugins in a number of locations specified by the caller. To add a plugin to be loaded, it is enough to put it in one of the locations, and name it *_plugin.py. (The naming convention is to allow having other modules as well, such as unit tests, in the same locations.) ''' import imp import inspect import os from cliapp import Plugin class PluginManager(object): '''Manage plugins. This class finds and loads plugins, and keeps a list of them that can be accessed in various ways. The locations are set via the locations attribute, which is a list. When a plugin is loaded, an instance of its class is created. This instance is initialized using normal and keyword arguments specified in the plugin manager attributes plugin_arguments and plugin_keyword_arguments. The version of the application using the plugin manager is set via the application_version attribute. This defaults to '0.0.0'. ''' suffix = '_plugin.py' def __init__(self): self.locations = [] self._plugins = None self._plugin_files = None self.plugin_arguments = [] self.plugin_keyword_arguments = {} self.application_version = '0.0.0' @property def plugin_files(self): if self._plugin_files is None: self._plugin_files = self.find_plugin_files() return self._plugin_files @property def plugins(self): if self._plugins is None: self._plugins = self.load_plugins() return self._plugins def __getitem__(self, name): for plugin in self.plugins: if plugin.name == name: return plugin raise KeyError('Plugin %s is not known' % name) def find_plugin_files(self): '''Find files that may contain plugins. This finds all files named *_plugin.py in all locations. The returned list is sorted. ''' pathnames = [] for location in self.locations: try: basenames = os.listdir(location) except os.error: continue for basename in basenames: s = os.path.join(location, basename) if s.endswith(self.suffix) and os.path.exists(s): pathnames.append(s) return sorted(pathnames) def load_plugins(self): '''Load plugins from all plugin files.''' plugins = dict() for pathname in self.plugin_files: for plugin in self.load_plugin_file(pathname): if plugin.name in plugins: p = plugins[plugin.name] if self.is_older(p.version, plugin.version): plugins[plugin.name] = plugin else: plugins[plugin.name] = plugin return plugins.values() def is_older(self, version1, version2): '''Is version1 older than version2?''' return self.parse_version(version1) < self.parse_version(version2) def load_plugin_file(self, pathname): '''Return plugin classes in a plugin file.''' name, ext = os.path.splitext(os.path.basename(pathname)) f = file(pathname, 'r') module = imp.load_module(name, f, pathname, ('.py', 'r', imp.PY_SOURCE)) f.close() plugins = [] for dummy, member in inspect.getmembers(module, inspect.isclass): if issubclass(member, Plugin): p = member(*self.plugin_arguments, **self.plugin_keyword_arguments) if self.compatible_version(p.required_application_version): plugins.append(p) return plugins def compatible_version(self, required_application_version): '''Check that the plugin is version-compatible with the application. This checks the plugin's required_application_version against the declared application version and returns True if they are compatible, and False if not. ''' req = self.parse_version(required_application_version) app = self.parse_version(self.application_version) return app[0] == req[0] and app >= req def parse_version(self, version): '''Parse a string represenation of a version into list of ints.''' return [int(s) for s in version.split('.')] def enable_plugins(self, plugins=None): '''Enable all or selected plugins.''' for plugin in plugins or self.plugins: plugin.enable_wrapper() def disable_plugins(self, plugins=None): '''Disable all or selected plugins.''' for plugin in plugins or self.plugins: plugin.disable_wrapper() cliapp-1.20130808/cliapp/pluginmgr_tests.py0000644000175000017500000001004612200776452020332 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest from cliapp import PluginManager class PluginManagerInitialStateTests(unittest.TestCase): def setUp(self): self.pm = PluginManager() def test_locations_is_empty_list(self): self.assertEqual(self.pm.locations, []) def test_plugins_is_empty_list(self): self.assertEqual(self.pm.plugins, []) def test_application_version_is_zeroes(self): self.assertEqual(self.pm.application_version, '0.0.0') def test_plugin_files_is_empty(self): self.assertEqual(self.pm.plugin_files, []) def test_plugin_arguments_is_empty(self): self.assertEqual(self.pm.plugin_arguments, []) def test_plugin_keyword_arguments_is_empty(self): self.assertEqual(self.pm.plugin_keyword_arguments, {}) class PluginManagerTests(unittest.TestCase): def setUp(self): self.pm = PluginManager() self.pm.locations = ['test-plugins', 'not-exist'] self.pm.plugin_arguments = ('fooarg',) self.pm.plugin_keyword_arguments = { 'bar': 'bararg' } self.files = sorted(['test-plugins/hello_plugin.py', 'test-plugins/aaa_hello_plugin.py', 'test-plugins/oldhello_plugin.py', 'test-plugins/wrongversion_plugin.py']) def test_finds_the_right_plugin_files(self): self.assertEqual(self.pm.find_plugin_files(), self.files) def test_plugin_files_attribute_implicitly_searches(self): self.assertEqual(self.pm.plugin_files, self.files) def test_loads_hello_plugin(self): plugins = self.pm.load_plugins() self.assertEqual(len(plugins), 1) self.assertEqual(plugins[0].name, 'Hello') def test_plugins_attribute_implicitly_searches(self): self.assertEqual(len(self.pm.plugins), 1) self.assertEqual(self.pm.plugins[0].name, 'Hello') def test_initializes_hello_with_correct_args(self): plugin = self.pm['Hello'] self.assertEqual(plugin.foo, 'fooarg') self.assertEqual(plugin.bar, 'bararg') def test_raises_keyerror_for_unknown_plugin(self): self.assertRaises(KeyError, self.pm.__getitem__, 'Hithere') def test_enable_plugins_enables_all_plugins(self): enabled = set() for plugin in self.pm.plugins: plugin.enable = lambda: enabled.add(plugin) self.pm.enable_plugins() self.assertEqual(enabled, set(self.pm.plugins)) def test_disable_plugins_disables_all_plugins(self): disabled = set() for plugin in self.pm.plugins: plugin.disable = lambda: disabled.add(plugin) self.pm.disable_plugins() self.assertEqual(disabled, set(self.pm.plugins)) class PluginManagerCompatibleApplicationVersionTests(unittest.TestCase): def setUp(self): self.pm = PluginManager() self.pm.application_version = '1.2.3' def test_rejects_zero(self): self.assertFalse(self.pm.compatible_version('0')) def test_rejects_two(self): self.assertFalse(self.pm.compatible_version('2')) def test_rejects_one_two_four(self): self.assertFalse(self.pm.compatible_version('1.2.4')) def test_accepts_one(self): self.assert_(self.pm.compatible_version('1')) def test_accepts_one_two_three(self): self.assert_(self.pm.compatible_version('1.2.3')) cliapp-1.20130808/cliapp/runcmd_tests.py0000644000175000017500000001215012200776452017614 0ustar jenkinsjenkins# Copyright (C) 2011, 2012 Lars Wirzenius # Copyright (C) 2012 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import optparse import os import StringIO import subprocess import sys import tempfile import unittest import cliapp def devnull(msg): pass class RuncmdTests(unittest.TestCase): def test_runcmd_executes_true(self): self.assertEqual(cliapp.runcmd(['true']), '') def test_runcmd_raises_error_on_failure(self): self.assertRaises(cliapp.AppException, cliapp.runcmd, ['false']) def test_runcmd_returns_stdout_of_command(self): self.assertEqual(cliapp.runcmd(['echo', 'hello', 'world']), 'hello world\n') def test_runcmd_returns_stderr_of_command(self): exit, out, err = cliapp.runcmd_unchecked(['ls', 'notexist']) self.assertNotEqual(exit, 0) self.assertEqual(out, '') self.assertNotEqual(err, '') def test_runcmd_pipes_stdin_through_command(self): self.assertEqual(cliapp.runcmd(['cat'], feed_stdin='hello, world'), 'hello, world') def test_runcmd_pipes_stdin_through_two_commands(self): self.assertEqual(cliapp.runcmd( ['cat'], ['cat'], feed_stdin='hello, world'), 'hello, world') def test_runcmd_pipes_stdin_through_command_with_lots_of_data(self): data = 'x' * (1024**2) self.assertEqual(cliapp.runcmd(['cat'], feed_stdin=data), data) def test_runcmd_ignores_failures_on_request(self): self.assertEqual(cliapp.runcmd(['false'], ignore_fail=True), '') def test_runcmd_obeys_cwd(self): self.assertEqual(cliapp.runcmd(['pwd'], cwd='/'), '/\n') def test_runcmd_unchecked_returns_values_on_success(self): self.assertEqual(cliapp.runcmd_unchecked(['echo', 'foo']), (0, 'foo\n', '')) def test_runcmd_unchecked_returns_values_on_failure(self): self.assertEqual(cliapp.runcmd_unchecked(['false']), (1, '', '')) def test_runcmd_unchecked_runs_simple_pipeline(self): self.assertEqual(cliapp.runcmd_unchecked( ['echo', 'foo'], ['wc', '-c']), (0, '4\n', '')) def test_runcmd_unchecked_runs_longer_pipeline(self): self.assertEqual(cliapp.runcmd_unchecked(['echo', 'foo'], ['cat'], ['wc', '-c']), (0, '4\n', '')) def test_runcmd_redirects_stdin_from_file(self): fd, filename = tempfile.mkstemp() os.write(fd, 'foobar') os.lseek(fd, 0, os.SEEK_SET) self.assertEqual(cliapp.runcmd_unchecked(['cat'], stdin=fd), (0, 'foobar', '')) os.close(fd) def test_runcmd_redirects_stdout_to_file(self): fd, filename = tempfile.mkstemp() exit, out, err = cliapp.runcmd_unchecked(['echo', 'foo'], stdout=fd) os.close(fd) with open(filename) as f: data = f.read() self.assertEqual(exit, 0) self.assertEqual(data, 'foo\n') def test_runcmd_redirects_stderr_to_file(self): fd, filename = tempfile.mkstemp() exit, out, err = cliapp.runcmd_unchecked(['ls', 'notexist'], stderr=fd) os.close(fd) with open(filename) as f: data = f.read() self.assertNotEqual(exit, 0) self.assertNotEqual(data, '') def test_runcmd_unchecked_handles_stdout_err_redirected_to_same_file(self): fd, filename = tempfile.mkstemp() exit, out, err = cliapp.runcmd_unchecked(['sleep', '2'], stdout=fd, stderr=subprocess.STDOUT) os.close(fd) with open(filename) as f: data = f.read() self.assertEqual(exit, 0) self.assertEqual(data, '') class ShellQuoteTests(unittest.TestCase): def test_returns_empty_string_for_empty_string(self): self.assertEqual(cliapp.shell_quote(''), '') def test_returns_same_string_when_safe(self): self.assertEqual(cliapp.shell_quote('abc123'), 'abc123') def test_quotes_space(self): self.assertEqual(cliapp.shell_quote(' '), "' '") def test_quotes_double_quote(self): self.assertEqual(cliapp.shell_quote('"'), "'\"'") def test_quotes_single_quote(self): self.assertEqual(cliapp.shell_quote("'"), '"\'"') cliapp-1.20130808/cliapp/settings.py0000644000175000017500000006064212200776452016753 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import ConfigParser import optparse import os import re import sys import cliapp from cliapp.genman import ManpageGenerator log_group_name = 'Logging' config_group_name = 'Configuration files and settings' perf_group_name = 'Peformance' default_group_names = [ log_group_name, config_group_name, perf_group_name, ] class Setting(object): action = 'store' type = 'string' nargs = 1 choices = None def __init__( self, names, default, help, metavar=None, group=None, hidden=False): self.names = names self.set_value(default) self.help = help self.metavar = metavar or self.default_metavar() self.group = group self.hidden = hidden def default_metavar(self): return None def get_value(self): return self._string_value def set_value(self, value): self._string_value = value def call_get_value(self): return self.get_value() def call_set_value(self, value): self.set_value(value) value = property(call_get_value, call_set_value) def has_value(self): return self.value is not None def parse_value(self, string): self.value = string def format(self): # pragma: no cover return str(self.value) class StringSetting(Setting): def default_metavar(self): return self.names[0].upper() class StringListSetting(Setting): action = 'append' def __init__( self, names, default, help, metavar=None, group=None, hidden=False): Setting.__init__( self, names, [], help, metavar=metavar, group=group, hidden=hidden) self.default = default self.using_default_value = True def default_metavar(self): return self.names[0].upper() def get_value(self): if self._string_value.strip(): return [s.strip() for s in self._string_value.split(',')] else: return self.default def set_value(self, strings): self._string_value = ','.join(strings) self.using_default_value = False def has_value(self): return self.value != [] def parse_value(self, string): self.value = [s.strip() for s in string.split(',')] def format(self): # pragma: no cover return ', '.join(self.value) class ChoiceSetting(Setting): type = 'choice' def __init__( self, names, choices, help, metavar=None, group=None, hidden=False): Setting.__init__( self, names, choices[0], help, metavar=metavar, group=group, hidden=hidden) self.choices = choices def default_metavar(self): return self.names[0].upper() class BooleanSetting(Setting): action = 'store_true' nargs = None type = None _trues = ['yes', 'on', '1', 'true'] _false = 'no' def get_value(self): return self._string_value.lower() in self._trues def set_value(self, value): def is_true(): if value is True or value is False: return value if type(value) in [str, unicode]: return value.lower() in self._trues return value if is_true(): self._string_value = self._trues[0] else: self._string_value = self._false class ByteSizeSetting(Setting): def parse_human_size(self, size): '''Parse a size using suffix into plain bytes.''' m = re.match(r'''(?P\d+(\.\d+)?) \s* (?Pk|ki|m|mi|g|gi|t|ti)? b? \s*$''', size.lower(), flags=re.X) if not m: return 0 else: number = float(m.group('number')) unit = m.group('unit') units = { 'k': 10**3, 'm': 10**6, 'g': 10**9, 't': 10**12, 'ki': 2**10, 'mi': 2**20, 'gi': 2**30, 'ti': 2**40, } return long(number * units.get(unit, 1)) def default_metavar(self): return 'SIZE' def get_value(self): return long(self._string_value) def set_value(self, value): if type(value) == str: value = self.parse_human_size(value) self._string_value = str(value) class IntegerSetting(Setting): type = 'int' def default_metavar(self): return self.names[0].upper() def get_value(self): return long(self._string_value) def set_value(self, value): self._string_value = str(value) class FormatHelpParagraphs(optparse.IndentedHelpFormatter): def _format_text(self, text): # pragma: no cover '''Like the default, except handle paragraphs.''' fmt = cliapp.TextFormat(width=self.width) formatted = fmt.format(text) return formatted.rstrip('\n') class Settings(object): '''Settings for a cliapp application. You probably don't need to create a settings object yourself, since ``cliapp.Application`` does it for you. Settings are read from configuration files, and parsed from the command line. Every setting has a type, name, and help text, and may have a default value as well. For example:: settings.boolean(['verbose', 'v'], 'show what is going on') This would create a new setting, ``verbose``, with a shorter alias ``v``. On the command line, the options ``--verbose`` and ``-v`` would work equally well. There can be any number of aliases. The help text is shown if the user uses ``--help`` or ``--generate-manpage``. You can use the ``metavar`` keyword argument to set the name shown in the generated option lists; the default name is whatever ``optparse`` decides (i.e., name of option). Use ``load_configs`` to read configuration files, and ``parse_args`` to parse command line arguments. The current value of a setting can be accessed by indexing the settings class:: settings['verbose'] The list of configuration files for the appliation is stored in ``config_files``. Add or remove from the list if you wish. The files need to exist: those that don't are silently ignored. ''' def __init__(self, progname, version, usage=None, description=None, epilog=None): self._settingses = dict() self._canonical_names = list() self.version = version self.progname = progname self.usage = usage self.description = description self.epilog = epilog self._add_default_settings() self._config_files = None self._cp = ConfigParser.ConfigParser() def _add_default_settings(self): self.string(['output'], 'write output to FILE, instead of standard output', metavar='FILE') self.string(['log'], 'write log entries to FILE (default is to not write log ' 'files at all); use "syslog" to log to system log, ' 'or "none" to disable logging', metavar='FILE', group=log_group_name) self.choice(['log-level'], ['debug', 'info', 'warning', 'error', 'critical', 'fatal'], 'log at LEVEL, one of debug, info, warning, ' 'error, critical, fatal (default: %default)', metavar='LEVEL', group=log_group_name) self.bytesize(['log-max'], 'rotate logs larger than SIZE, ' 'zero for never (default: %default)', metavar='SIZE', default=0, group=log_group_name) self.integer(['log-keep'], 'keep last N logs (%default)', metavar='N', default=10, group=log_group_name) self.string(['log-mode'], 'set permissions of new log files to MODE (octal; ' 'default %default)', metavar='MODE', default='0600', group=log_group_name) self.choice(['dump-memory-profile'], ['simple', 'none', 'meliae', 'heapy'], 'make memory profiling dumps using METHOD, which is one ' 'of: none, simple, meliae, or heapy ' '(default: %default)', metavar='METHOD', group=perf_group_name) self.integer(['memory-dump-interval'], 'make memory profiling dumps at least SECONDS apart', metavar='SECONDS', default=300, group=perf_group_name) def _add_setting(self, setting): '''Add a setting to self._cp.''' self._canonical_names.append(setting.names[0]) for name in setting.names: self._settingses[name] = setting def string(self, names, help, default='', **kwargs): '''Add a setting with a string value.''' self._add_setting(StringSetting(names, default, help, **kwargs)) def string_list(self, names, help, default=None, **kwargs): '''Add a setting which have multiple string values. An example would be an option that can be given multiple times on the command line, e.g., "--exclude=foo --exclude=bar". ''' self._add_setting(StringListSetting(names, default or [], help, **kwargs)) def choice(self, names, possibilities, help, **kwargs): '''Add a setting which chooses from list of acceptable values. An example would be an option to set debugging level to be one of a set of accepted names: debug, info, warning, etc. The default value is the first possibility. ''' self._add_setting(ChoiceSetting(names, possibilities, help, **kwargs)) def boolean(self, names, help, default=False, **kwargs): '''Add a setting with a boolean value.''' self._add_setting(BooleanSetting(names, default, help, **kwargs)) def bytesize(self, names, help, default=0, **kwargs): '''Add a setting with a size in bytes. The user can use suffixes for kilo/mega/giga/tera/kibi/mibi/gibi/tibi. ''' self._add_setting(ByteSizeSetting(names, default, help, **kwargs)) def integer(self, names, help, default=0, **kwargs): '''Add an integer setting.''' self._add_setting(IntegerSetting(names, default, help, **kwargs)) def __getitem__(self, name): return self._settingses[name].value def __setitem__(self, name, value): self._settingses[name].value = value def __contains__(self, name): return name in self._settingses def __iter__(self): '''Iterate over canonical settings names.''' for name in self._canonical_names: yield name def keys(self): '''Return canonical settings names.''' return self._canonical_names[:] def require(self, name): '''Raise exception if setting has not been set. Option must have a value, and a default value is OK. ''' if not self._settingses[name].has_value(): raise cliapp.AppException('Setting %s has no value, ' 'but one is required' % name) def _option_names(self, names): '''Turn setting names into option names. Names with a single letter are short options, and get prefixed with one dash. The rest get prefixed with two dashes. ''' return ['--%s' % name if len(name) > 1 else '-%s' % name for name in names] def _destname(self, name): name = '_'.join(name.split('-')) return name def build_parser(self, configs_only=False, arg_synopsis=None, cmd_synopsis=None, deferred_last=[], all_options=False): '''Build OptionParser for parsing command line.''' # Call a callback function unless we're in configs_only mode. maybe = lambda func: (lambda *args: None) if configs_only else func # Maintain lists of callback function calls that are deferred. # We call them ourselves rather than have OptionParser call them # directly so that we can do things like --dump-config only # after the whole command line is parsed. def defer_last(func): # pragma: no cover def callback(*args): deferred_last.append(lambda: func(*args)) return callback # Create the command line parser. def getit(x): if x is None or type(x) in [str, unicode]: return x else: return x() usage = getit(self.usage) description = getit(self.description) p = optparse.OptionParser(prog=self.progname, version=self.version, formatter=FormatHelpParagraphs(), usage=usage, description=description, epilog=self.epilog) # Create all OptionGroup objects. This way, the user code can # add settings to built-in option groups. group_names = set(default_group_names) for name in self._canonical_names: s = self._settingses[name] if s.group is not None: group_names.add(s.group) group_names = sorted(group_names) option_groups = {} for name in group_names: group = optparse.OptionGroup(p, name) p.add_option_group(group) option_groups[name] = group config_group = option_groups[config_group_name] # Return help text, unless setting/option is hidden, in which # case return optparse.SUPPRESS_HELP. def help_text(text, hidden): if all_options or not hidden: return text else: return optparse.SUPPRESS_HELP # Add --dump-setting-names. def dump_setting_names(*args): # pragma: no cover for name in self._canonical_names: sys.stdout.write('%s\n' % name) sys.exit(0) config_group.add_option('--dump-setting-names', action='callback', nargs=0, callback=defer_last(maybe(dump_setting_names)), help=help_text( 'write out all names of settings and quit', True)) # Add --dump-config. def call_dump_config(*args): # pragma: no cover self.dump_config(sys.stdout) sys.exit(0) config_group.add_option('--dump-config', action='callback', nargs=0, callback=defer_last(maybe(call_dump_config)), help='write out the entire current configuration') # Add --no-default-configs. def reset_configs(option, opt_str, value, parser): self.config_files = [] config_group.add_option('--no-default-configs', action='callback', nargs=0, callback=reset_configs, help='clear list of configuration files to read') # Add --config. def append_to_configs(option, opt_str, value, parser): self.config_files.append(value) config_group.add_option('--config', action='callback', nargs=1, type='string', callback=append_to_configs, help='add FILE to config files', metavar='FILE') # Add --list-config-files. def list_config_files(*args): # pragma: no cover for filename in self.config_files: print filename sys.exit(0) config_group.add_option('--list-config-files', action='callback', nargs=0, callback=defer_last(maybe(list_config_files)), help=help_text('list all possible config files', True)) # Add --generate-manpage. self._arg_synopsis = arg_synopsis self._cmd_synopsis = cmd_synopsis p.add_option('--generate-manpage', action='callback', nargs=1, type='string', callback=maybe(self._generate_manpage), help=help_text('fill in manual page TEMPLATE', True), metavar='TEMPLATE') # Add --help-all. def help_all(*args): # pragma: no cover pp = self.build_parser( configs_only=configs_only, arg_synopsis=arg_synopsis, cmd_synopsis=cmd_synopsis, all_options=True) sys.stdout.write(pp.format_help()) sys.exit(0) config_group.add_option( '--help-all', action='callback', help='show all options', callback=defer_last(maybe(help_all))) # Add other options, from the user-defined and built-in # settingses. def set_value(option, opt_str, value, parser, setting): if setting.action == 'append': if setting.using_default_value: setting.value = [value] else: setting.value += [value] elif setting.action == 'store_true': setting.value = True else: assert setting.action == 'store' setting.value = value def set_false(option, opt_str, value, parser, setting): setting.value = False def add_option(obj, s): option_names = self._option_names(s.names) obj.add_option(*option_names, action='callback', callback=maybe(set_value), callback_args=(s,), type=s.type, nargs=s.nargs, choices=s.choices, help=help_text(s.help, s.hidden), metavar=s.metavar) def add_negation_option(obj, s): option_names = self._option_names(s.names) long_names = [x for x in option_names if x.startswith('--')] neg_names = ['--no-' + x[2:] for x in long_names] unused_names = [x for x in neg_names if x[2:] not in self._settingses] obj.add_option(*unused_names, action='callback', callback=maybe(set_false), callback_args=(s,), type=s.type, help=help_text('', s.hidden)) # Add options for every setting. for name in self._canonical_names: s = self._settingses[name] if s.group is None: obj = p else: obj = option_groups[s.group] add_option(obj, s) if type(s) is BooleanSetting: add_negation_option(obj, s) p.set_defaults(**{self._destname(name): s.value}) return p def parse_args(self, args, parser=None, suppress_errors=False, configs_only=False, arg_synopsis=None, cmd_synopsis=None, compute_setting_values=None, all_options=False): '''Parse the command line. Return list of non-option arguments. ``args`` would usually be ``sys.argv[1:]``. ''' deferred_last = [] p = parser or self.build_parser(configs_only=configs_only, arg_synopsis=arg_synopsis, cmd_synopsis=cmd_synopsis, deferred_last=deferred_last, all_options=all_options) if suppress_errors: p.error = lambda msg: sys.exit(1) options, args = p.parse_args(args) if compute_setting_values: # pragma: no cover compute_setting_values(self) for callback in deferred_last: # pragma: no cover callback() return args @property def _default_config_files(self): '''Return list of default config files to read. The names of the files are dependent on the name of the program, as set in the progname attribute. The files may or may not exist. ''' configs = [] configs.append('/etc/%s.conf' % self.progname) configs += self._listconfs('/etc/%s' % self.progname) configs.append(os.path.expanduser('~/.%s.conf' % self.progname)) configs += self._listconfs( os.path.expanduser('~/.config/%s' % self.progname)) return configs def _listconfs(self, dirname, listdir=os.listdir): '''Return list of pathnames to config files in dirname. Config files are expectd to have names ending in '.conf'. If dirname does not exist or is not a directory, return empty list. ''' if not os.path.isdir(dirname): return [] basenames = listdir(dirname) basenames.sort(key=lambda s: [ord(c) for c in s]) return [os.path.join(dirname, x) for x in basenames if x.endswith('.conf')] def _get_config_files(self): if self._config_files is None: self._config_files = self._default_config_files return self._config_files def _set_config_files(self, config_files): self._config_files = config_files config_files = property(_get_config_files, _set_config_files) def set_from_raw_string(self, name, raw_string): '''Set value of a setting from a raw, unparsed string value.''' s = self._settingses[name] s.parse_value(raw_string) return s def load_configs(self, open=open): '''Load all config files in self.config_files. Silently ignore files that do not exist. ''' cp = ConfigParser.ConfigParser() cp.add_section('config') for pathname in self.config_files: try: f = open(pathname) except IOError: pass else: cp.readfp(f) f.close() for name in cp.options('config'): value = cp.get('config', name) s = self.set_from_raw_string(name, value) if hasattr(s, 'using_default_value'): s.using_default_value = True # Remember the ConfigParser for use in as_cp later on. self._cp = cp def _generate_manpage(self, o, os, value, p): # pragma: no cover template = open(value).read() generator = ManpageGenerator(template, p, self._arg_synopsis, self._cmd_synopsis) sys.stdout.write(generator.format_template()) sys.exit(0) def as_cp(self): '''Return a ConfigParser instance with current values of settings. Any sections outside of ``[config]`` are preserved as is. This lets the application use those as it wishes, and assign any meanings it desires to the section names. ''' cp = ConfigParser.ConfigParser() cp.add_section('config') for name in self._canonical_names: cp.set('config', name, self._settingses[name].format()) for section in self._cp.sections(): if section != 'config': cp.add_section(section) for option in self._cp.options(section): value = self._cp.get(section, option) cp.set(section, option, value) return cp def dump_config(self, output): # pragma: no cover cp = self.as_cp() cp.write(output) cliapp-1.20130808/cliapp/settings_tests.py0000644000175000017500000004051212200776452020167 0ustar jenkinsjenkins# Copyright (C) 2009-2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import optparse import StringIO import sys import unittest import cliapp class SettingsTests(unittest.TestCase): def setUp(self): self.settings = cliapp.Settings('appname', '1.0') def test_has_progname(self): self.assertEqual(self.settings.progname, 'appname') def test_sets_progname(self): self.settings.progname = 'foo' self.assertEqual(self.settings.progname, 'foo') def test_has_version(self): self.assertEqual(self.settings.version, '1.0') def test_sets_usage_from_func(self): s = cliapp.Settings('appname', '1.0', usage=lambda: 'xyzzy') p = s.build_parser() self.assert_('xyzzy' in p.usage) def test_adds_default_options_and_settings(self): self.assert_('output' in self.settings) self.assert_('log' in self.settings) self.assert_('log-level' in self.settings) def test_iterates_over_canonical_settings_names(self): known = ['output', 'log', 'log-level'] self.assertEqual(sorted(x for x in self.settings if x in known), sorted(known)) def test_keys_returns_canonical_names(self): known = ['output', 'log', 'log-level'] self.assertEqual(sorted(x for x in self.settings.keys() if x in known), sorted(known)) def test_parses_options(self): self.settings.string(['foo'], 'foo help', group='foo') self.settings.boolean(['bar'], 'bar help') self.settings.parse_args(['--foo=foovalue', '--bar']) self.assertEqual(self.settings['foo'], 'foovalue') self.assertEqual(self.settings['bar'], True) def test_parses_boolean_negation_option(self): self.settings.boolean(['bar'], 'bar help') self.settings.parse_args(['--bar', '--no-bar']) self.assertEqual(self.settings['bar'], False) def test_parses_boolean_negation_option_in_group(self): self.settings.boolean(['bar'], 'bar help', group='bar') self.settings.parse_args(['--bar', '--no-bar']) self.assertEqual(self.settings['bar'], False) def test_does_not_have_foo_setting_by_default(self): self.assertFalse('foo' in self.settings) def test_raises_keyerror_for_getting_unknown_setting(self): self.assertRaises(KeyError, self.settings.__getitem__, 'foo') def test_raises_keyerror_for_setting_unknown_setting(self): self.assertRaises(KeyError, self.settings.__setitem__, 'foo', 'bar') def test_adds_string_setting(self): self.settings.string(['foo'], 'foo help') self.assert_('foo' in self.settings) def test_adds_string_list_setting(self): self.settings.string_list(['foo'], 'foo help') self.assert_('foo' in self.settings) def test_string_list_is_empty_list_by_default(self): self.settings.string_list(['foo'], '') self.settings.parse_args([]) self.assertEqual(self.settings['foo'], []) def test_string_list_parses_one_item(self): self.settings.string_list(['foo'], '') self.settings.parse_args(['--foo=foo']) self.assertEqual(self.settings['foo'], ['foo']) def test_string_list_parses_two_items(self): self.settings.string_list(['foo'], '') self.settings.parse_args(['--foo=foo', '--foo', 'bar']) self.assertEqual(self.settings['foo'], ['foo', 'bar']) def test_string_list_uses_nonempty_default_if_given(self): self.settings.string_list(['foo'], '', default=['bar']) self.settings.parse_args([]) self.assertEqual(self.settings['foo'], ['bar']) def test_string_list_uses_ignores_default_if_user_provides_values(self): self.settings.string_list(['foo'], '', default=['bar']) self.settings.parse_args(['--foo=pink', '--foo=punk']) self.assertEqual(self.settings['foo'], ['pink', 'punk']) def test_adds_choice_setting(self): self.settings.choice(['foo'], ['foo', 'bar'], 'foo help') self.assert_('foo' in self.settings) def test_choice_defaults_to_first_one(self): self.settings.choice(['foo'], ['foo', 'bar'], 'foo help') self.settings.parse_args([]) self.assertEqual(self.settings['foo'], 'foo') def test_choice_accepts_any_valid_value(self): self.settings.choice(['foo'], ['foo', 'bar'], 'foo help') self.settings.parse_args(['--foo=foo']) self.assertEqual(self.settings['foo'], 'foo') self.settings.parse_args(['--foo=bar']) self.assertEqual(self.settings['foo'], 'bar') def test_choice_raises_error_for_unacceptable_value(self): self.settings.choice(['foo'], ['foo', 'bar'], 'foo help') self.assertRaises(SystemExit, self.settings.parse_args, ['--foo=xyzzy'], suppress_errors=True) def test_adds_boolean_setting(self): self.settings.boolean(['foo'], 'foo help') self.assert_('foo' in self.settings) def test_boolean_setting_is_false_by_default(self): self.settings.boolean(['foo'], 'foo help') self.assertFalse(self.settings['foo']) def test_sets_boolean_setting_to_true_for_many_true_values(self): self.settings.boolean(['foo'], 'foo help') self.settings['foo'] = True self.assert_(self.settings['foo']) self.settings['foo'] = 1 self.assert_(self.settings['foo']) def test_sets_boolean_setting_to_false_for_many_false_values(self): self.settings.boolean(['foo'], 'foo help') self.settings['foo'] = False self.assertFalse(self.settings['foo']) self.settings['foo'] = 0 self.assertFalse(self.settings['foo']) self.settings['foo'] = () self.assertFalse(self.settings['foo']) self.settings['foo'] = [] self.assertFalse(self.settings['foo']) self.settings['foo'] = '' self.assertFalse(self.settings['foo']) def test_sets_boolean_to_true_from_config_file(self): def fake_open(filename): return StringIO.StringIO('[config]\nfoo = yes\n') self.settings.boolean(['foo'], 'foo help') self.settings.load_configs(open=fake_open) self.assertEqual(self.settings['foo'], True) def test_sets_boolean_to_false_from_config_file(self): def fake_open(filename): return StringIO.StringIO('[config]\nfoo = False\n') self.settings.boolean(['foo'], 'foo help') self.settings.load_configs(open=fake_open) self.assertEqual(self.settings['foo'], False) def test_adds_bytesize_setting(self): self.settings.bytesize(['foo'], 'foo help') self.assert_('foo' in self.settings) def test_parses_bytesize_option(self): self.settings.bytesize(['foo'], 'foo help') self.settings.parse_args(args=['--foo=xyzzy']) self.assertEqual(self.settings['foo'], 0) self.settings.parse_args(args=['--foo=123']) self.assertEqual(self.settings['foo'], 123) self.settings.parse_args(args=['--foo=123k']) self.assertEqual(self.settings['foo'], 123 * 1000) self.settings.parse_args(args=['--foo=123m']) self.assertEqual(self.settings['foo'], 123 * 1000**2) self.settings.parse_args(args=['--foo=123g']) self.assertEqual(self.settings['foo'], 123 * 1000**3) self.settings.parse_args(args=['--foo=123t']) self.assertEqual(self.settings['foo'], 123 * 1000**4) self.settings.parse_args(args=['--foo=123kib']) self.assertEqual(self.settings['foo'], 123 * 1024) self.settings.parse_args(args=['--foo=123mib']) self.assertEqual(self.settings['foo'], 123 * 1024**2) self.settings.parse_args(args=['--foo=123gib']) self.assertEqual(self.settings['foo'], 123 * 1024**3) self.settings.parse_args(args=['--foo=123tib']) self.assertEqual(self.settings['foo'], 123 * 1024**4) def test_adds_integer_setting(self): self.settings.integer(['foo'], 'foo help') self.assert_('foo' in self.settings) def test_parses_integer_option(self): self.settings.integer(['foo'], 'foo help', default=123) self.settings.parse_args(args=[]) self.assertEqual(self.settings['foo'], 123) self.settings.parse_args(args=['--foo=123']) self.assertEqual(self.settings['foo'], 123) def test_has_list_of_default_config_files(self): defaults = self.settings._default_config_files self.assert_(isinstance(defaults, list)) self.assert_(len(defaults) > 0) def test_listconfs_returns_empty_list_for_nonexistent_directory(self): self.assertEqual(self.settings._listconfs('notexist'), []) def test_listconfs_lists_config_files_only(self): def mock_listdir(dirname): return ['foo.conf', 'foo.notconf'] names = self.settings._listconfs('.', listdir=mock_listdir) self.assertEqual(names, ['./foo.conf']) def test_listconfs_sorts_names_in_C_locale(self): def mock_listdir(dirname): return ['foo.conf', 'bar.conf'] names = self.settings._listconfs('.', listdir=mock_listdir) self.assertEqual(names, ['./bar.conf', './foo.conf']) def test_has_config_files_attribute(self): self.assertEqual(self.settings.config_files, self.settings._default_config_files) def test_has_config_files_list_can_be_changed(self): self.settings.config_files += ['./foo'] self.assertEqual(self.settings.config_files, self.settings._default_config_files + ['./foo']) def test_loads_config_files(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] foo = yeehaa ''') self.settings.string(['foo'], 'foo help') self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) self.assertEqual(self.settings['foo'], 'yeehaa') def test_loads_string_list_from_config_files(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] foo = yeehaa bar = ping, pong ''') self.settings.string_list(['foo'], 'foo help') self.settings.string_list(['bar'], 'bar help') self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) self.assertEqual(self.settings['foo'], ['yeehaa']) self.assertEqual(self.settings['bar'], ['ping', 'pong']) def test_handles_defaults_with_config_files(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] ''') self.settings.string(['foo'], 'foo help', default='foo') self.settings.string_list(['bar'], 'bar help', default=['bar']) self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) self.assertEqual(self.settings['foo'], 'foo') self.assertEqual(self.settings['bar'], ['bar']) def test_handles_overridden_defaults_with_config_files(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] foo = yeehaa bar = ping, pong ''') self.settings.string(['foo'], 'foo help', default='foo') self.settings.string_list(['bar'], 'bar help', default=['bar']) self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) self.assertEqual(self.settings['foo'], 'yeehaa') self.assertEqual(self.settings['bar'], ['ping', 'pong']) def test_handles_values_from_config_files_overridden_on_command_line(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] foo = yeehaa bar = ping, pong ''') self.settings.string(['foo'], 'foo help', default='foo') self.settings.string_list(['bar'], 'bar help', default=['bar']) self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) self.settings.parse_args(['--foo=red', '--bar=blue', '--bar=white']) self.assertEqual(self.settings['foo'], 'red') self.assertEqual(self.settings['bar'], ['blue', 'white']) def test_load_configs_ignore_errors_opening_a_file(self): def mock_open(filename, mode=None): raise IOError() self.assertEqual(self.settings.load_configs(open=mock_open), None) def test_adds_config_file_with_dash_dash_config(self): self.settings.parse_args(['--config=foo.conf']) self.assertEqual(self.settings.config_files, self.settings._default_config_files + ['foo.conf']) def test_ignores_default_configs(self): self.settings.parse_args(['--no-default-configs']) self.assertEqual(self.settings.config_files, []) def test_ignores_then_adds_configs_works(self): self.settings.parse_args(['--no-default-configs', '--config=foo.conf']) self.assertEqual(self.settings.config_files, ['foo.conf']) def test_require_raises_error_if_string_unset(self): self.settings.string(['foo'], 'foo help', default=None) self.assertRaises(cliapp.AppException, self.settings.require, 'foo') def test_require_is_ok_with_set_string(self): self.settings.string(['foo'], 'foo help', default=None) self.settings['foo'] = 'bar' self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_default_string(self): self.settings.string(['foo'], 'foo help', default='foo default') self.assertEqual(self.settings.require('foo'), None) def test_require_raises_error_if_string_list_unset(self): self.settings.string_list(['foo'], 'foo help') self.assertRaises(cliapp.AppException, self.settings.require, 'foo') def test_require_is_ok_with_set_string_list(self): self.settings.string(['foo'], 'foo help') self.settings['foo'] = ['foo', 'bar'] self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_default_string_list(self): self.settings.string(['foo'], 'foo help', default=['foo']) self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_unset_choice(self): self.settings.choice(['foo'], ['foo', 'bar'], 'foo help') self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_unset_boolean(self): self.settings.boolean(['foo'], 'foo help') self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_unset_bytesize(self): self.settings.bytesize(['foo'], 'foo help') self.assertEqual(self.settings.require('foo'), None) def test_require_is_ok_with_unset_integer(self): self.settings.integer(['foo'], 'foo help') self.assertEqual(self.settings.require('foo'), None) def test_exports_configparser_with_settings(self): self.settings.integer(['foo'], 'foo help', default=1) self.settings.string(['bar'], 'bar help', default='yo') cp = self.settings.as_cp() self.assertEqual(cp.get('config', 'foo'), '1') self.assertEqual(cp.get('config', 'bar'), 'yo') def test_exports_all_config_sections_via_as_cp(self): def mock_open(filename, mode=None): return StringIO.StringIO('''\ [config] foo = yeehaa [other] bar = dodo ''') self.settings.string(['foo'], 'foo help', default='foo') self.settings.config_files = ['whatever.conf'] self.settings.load_configs(open=mock_open) cp = self.settings.as_cp() self.assertEqual(sorted(cp.sections()), ['config', 'other']) self.assertEqual(cp.get('config', 'foo'), 'yeehaa') self.assertEqual(cp.options('other'), ['bar']) self.assertEqual(cp.get('other', 'bar'), 'dodo') cliapp-1.20130808/doc/0000755000175000017500000000000012200776452014026 5ustar jenkinsjenkinscliapp-1.20130808/doc/_build/0000755000175000017500000000000012200776452015264 5ustar jenkinsjenkinscliapp-1.20130808/doc/_static/0000755000175000017500000000000012200776452015454 5ustar jenkinsjenkinscliapp-1.20130808/doc/_templates/0000755000175000017500000000000012200776452016163 5ustar jenkinsjenkinscliapp-1.20130808/doc/conf.py0000644000175000017500000001430712200776452015332 0ustar jenkinsjenkins# -*- coding: utf-8 -*- # # cliapp documentation build configuration file, created by # sphinx-quickstart on Sun May 29 13:50:03 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) import cliapp # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'cliapp' copyright = u'2011, Lars Wirzenius' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = cliapp.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'cliappdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'cliapp.tex', u'cliapp Documentation', u'Lars Wirzenius', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True cliapp-1.20130808/doc/example.rst0000644000175000017500000000173112200776452016215 0ustar jenkinsjenkinsExample ======= :: class ExampleApp(cliapp.Application): def add_settings(self): self.settings.string_list(['pattern', 'e'], 'search for regular expression PATTERN', metavar='REGEXP') # We override process_inputs to be able to do something after the last # input line. def process_inputs(self, args): self.matches = 0 cliapp.Application.process_inputs(self, args) self.output.write('There were %s matches.\\n' % self.matches) def process_input_line(self, name, line): for pattern in self.settings['pattern']: if pattern in line: self.output.write('%s:%s: %s' % (name, self.lineno, line)) self.matches += 1 logging.debug('Match: %s line %d' % (name, self.lineno)) if __name__ == '__main__': ExampleApp().run() cliapp-1.20130808/doc/index.rst0000644000175000017500000000275512200776452015700 0ustar jenkinsjenkinsWelcome to cliapp ================= ``cliapp`` is a Python framework for Unix-like command line programs, which typically have the following characteristics: * non-interactive * the programs read input files named on the command line, or the standard input * each line of input is processed individually * output is to the standard output * there are various options to modify how the program works * certain options are common to all: ``--help``, ``--version`` Programs like the above are often used as filters in a pipeline. The scaffolding to set up a command line parser, open each input file, read each line of input, etc, is the same in each program. Only the logic of what to do with each line differs. ``cliapp`` is not restricted to line-based filters, but is a more general framework. It provides ways for its users to override most behavior. For example: * you can treat command line arguments as URLs, or record identfiers in a database, or whatever you like * you can read input files in whatever chunks you like, or not at all, rather than forcing a line-based paradigm Despite all the flexibility, writing simple line-based filters remains very straightforward. The point is to get the framework to do all the usual things, and avoid repeating code across users of the framework. .. toctree:: :numbered: example walkthrough logging subcommands manpages profiling refman Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` cliapp-1.20130808/doc/logging.rst0000644000175000017500000000022312200776452016203 0ustar jenkinsjenkinsLogging ======= Logging support: by default, no log file is written, it must be requested explicitly by the user. The default log level is info. cliapp-1.20130808/doc/Makefile0000644000175000017500000000606612200776452015476 0ustar jenkinsjenkins# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cliapp.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cliapp.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." cliapp-1.20130808/doc/manpages.rst0000644000175000017500000000140012200776452016346 0ustar jenkinsjenkinsManual pages ============ ``cliapp`` provides a way to fill in a manual page template, in **troff** format, with information about all options. This allows you to write the rest of the manual page without having to remember to update all options. This is a compromise between ease-of-development and manual page quality. A high quality manual page probably needs to be written from scratch. For example, the description of each option in a manual page should usually be longer than what is suitable for ``--help`` output. However, it is tedious to write option descriptions many times. To use this, use the ``--generate-manpage=TEMPLATE`` option, where ``TEMPLATE`` is the name of the template file. See ``example.1`` in the ``cliapp`` source tree for an example. cliapp-1.20130808/doc/profiling.rst0000644000175000017500000000043512200776452016553 0ustar jenkinsjenkinsProfiling support ================= If ``sys.argv[0]`` is ``foo``, and the environment variable ``FOO_PROFILE`` is set, then the execution of the application (the ``run`` method) is profiled, using ``cProfile``, and the profile written to the file named in the environment variable. cliapp-1.20130808/doc/refman.rst0000644000175000017500000000041612200776452016031 0ustar jenkinsjenkinsReference manual ================ .. automodule:: cliapp :members: :undoc-members: :exclude-members: add_boolean_setting, add_bytesize_setting, add_choice_setting, add_integer_setting, add_string_list_setting, add_string_setting, config_files cliapp-1.20130808/doc/subcommands.rst0000644000175000017500000000221212200776452017070 0ustar jenkinsjenkinsSubcommands =========== Sometimes a command line tool needs to support subcommands. For example, version control tools often do this: ``git commit``, ``git clone``, etc. To do this with ``cliapp``, you need to add methods with names like ``cmd_commit`` and ``cmd_clone``:: class VersionControlTool(cliapp.Application): def cmd_commit(self, args): '''commit command description''' pass def cmd_clone(self, args): '''clone command description''' pass If any such methods exist, ``cliapp`` automatically supports subcommands. The name of the method, without the ``cmd_`` prefix, forms the name of the subcommand. Any underscores in the method name get converted to dashes in the command line. Case is preserved. Subcommands may also be added using the ``add_subcommand`` method. All options are global, not specific to the subcommand. All non-option arguments are passed to the method in its only argument. Subcommands are implemented by the ``process_args`` method. If you override that method, you need to support subcommands yourself (perhaps by calling the ``cliapp`` implementation). cliapp-1.20130808/doc/walkthrough.rst0000644000175000017500000000241512200776452017121 0ustar jenkinsjenkinsWalkthrough =========== Every application should be a class that subclasses ``cliapp.Application``. The subclass should provide specific methods. Read the documentation for the ``cliapp.Application`` class to see all methods, but a rough summary is here: * the ``settings`` attribute is the ``cliapp.Settings`` instance used by the application * override ``add_settings`` to add new settings for the application * override ``process_*`` methods to override various stages in how arguments and input files are processed * override ``process_args`` to decide how each argument is processed; by default, this called ``process_inputs`` or handles subcommands * ``process_inputs`` calls ``process_input`` (note singular) for each argument, or on ``-`` to process standard input if no files are named on the command line * ``process_input`` calls ``open_input`` to open each file, then calls ``process_input_line`` for each input line * ``process_input_line`` does nothing, by default This cascade of overrideable methods is started by the `run` method, which also sets up logging, loads configuration files, parses the command line, and handles reporting of exceptions. It can also run the rest of the code under the Python profiler, if the appropriate environment variable is set. cliapp-1.20130808/test-plugins/0000755000175000017500000000000012200776452015717 5ustar jenkinsjenkinscliapp-1.20130808/test-plugins/aaa_hello_plugin.py0000644000175000017500000000020212200776452021546 0ustar jenkinsjenkinsimport cliapp class Hello(cliapp.Plugin): def __init__(self, foo, bar=None): self.foo = foo self.bar = bar cliapp-1.20130808/test-plugins/hello_plugin.py0000644000175000017500000000036612200776452020757 0ustar jenkinsjenkinsimport cliapp class Hello(cliapp.Plugin): def __init__(self, foo, bar=None): self.foo = foo self.bar = bar @property def version(self): return '0.0.1' def setup(self): self.setup_called = True cliapp-1.20130808/test-plugins/oldhello_plugin.py0000644000175000017500000000020312200776452021444 0ustar jenkinsjenkinsimport cliapp class Hello(cliapp.Plugin): def __init__(self, foo, bar=None): self.foo = foo self.bar = bar cliapp-1.20130808/test-plugins/wrongversion_plugin.py0000644000175000017500000000040412200776452022407 0ustar jenkinsjenkins# This is a test plugin that requires a newer application version than # what the test harness specifies. import cliapp class WrongVersion(cliapp.Plugin): required_application_version = '9999.9.9' def __init__(self, *args, **kwargs): pass cliapp-1.20130808/NEWS0000644000175000017500000004237412200776452013772 0ustar jenkinsjenkinsNEWS for cliapp =============== Version 1.20130808 ------------------ * Bug fix to cliapp.runcmd pipeline handling: the pipeline now returns failure if any of the processes fails, rather than only the last one. Found and reported by Richard Maw. * Fix a problem in the pipeline code in runcmd. Reported and fix provided by Richard Maw. Version 1.20130613 ------------------ * cliapp(5) now mentions subcommands and the automatic subcommand "help". * `ssh_runcmd` now has the `tty` keyword argument to enable ssh allocation of pseudo-TTYs. Patch from Jannis Pohlmann. * The `help` subcommand now writes a useful error message, instead of a stack trace, if given an unknown subcommand. Reported by Rob Taylor. Version 1.20130424 ------------------ * The API documentation has been split into more than one page. Bug fixes: * `cliapp.runcmd` no longer dies from the `SIGWINCH` signal. Version 1.20130313 ------------------ * Add `cliapp.Application.compute_setting_values` method. This allows the application to have settings with values that are computed after configuration files and the command line are parsed. * Cliapp now logs the Python version at startup, to aid debugging. * `cliapp.runcmd` now logs much less during execution of a command. The verbose logging was useful while developing pipeline support, but has now not been useful for months. * More default settings and options have an option group now, making `--help` output prettier. * The `--help` output and the output of the `help` subcommand now only list summaries for subcommands. The full documentation for a subcommand can be seen by giving the name of the subcommand to `help`. * Logging setup is now more overrideable. The `setup_logging` method calls `setup_logging_handler_for_syslog`, `setup_logging_handler_for_syslog`, or `setup_logging_handler_to_file`, and the last one calls `setup_logging_format` and `setup_logging_timestamp` to create the format strings for messages and timestamps. This allows applications to add, for example, more detailed timestamps easily. * The process and system CPU times, and those of the child processes, and the process wall clock duration, are now logged when the memory profiling information is logged. * Subcommands added with `add_subcommand` may now have aliases. Subcommands defined using `Application` class methods named `cmd_*` cannot have aliases. * Settings and subcommands may now be hidden from `--help` and `help` output. New option `--help-all` and new subcommand `help-all` show everything. * cliapp(5) now explains how `--generate-manpage` is used. Thanks to Enrico Zini for the suggestion. * New function `cliapp.ssh_runcmd` for executing a command remotely over ssh. The function automatically shell-quotes the argv array given to it so that arguments with spaces and other shell meta-characters work over ssh. * New function `cliapp.shell_quote` quotes strings for passing as shell arguments. * `cliapp.runcmd` now has a new keyword argument: `log_error`. If set to false, errors are not logged. Defaults to true. Bug fixes: * The process title is now set only if `/proc/self/comm` exists. Previously, on the kernel in Debian squeeze (2.6.32), setting the process title would fail, and the error would be logged to the terminal. Reported by William Boughton. * A setting may no longer have a default value of None. Version 1.20121216 ------------------ * Options in option groups are now included in manual page SYNOPSIS and OPTIONS sections. * `--log=syslog` message format improvement by Daniel Silverstone. No longer includes a timestamp, since syslog adds it anyway. Also, the process name is now set on Linux. * Make the default subcommand argument synopsis be an empty string, instead of None. Reported by Sam Thursfield. * Meliae memory dumping support has been fixed. Reported by Joey Hess. * Memory profiling reports can now be done at minimum intervals in seconds, rather than every time the code asks for them. This can reduce the overhead of memory profiling quite a lot. * If there are any subcommands, cliapp now adds a subcommand called `help`, unless one already exists. * For every boolean setting foo, there will no be a --no-foo option to be used on the command line. Version 1.20120929 ------------------ * Print error messages from `SystemExit` exceptions. As a side effect, this fixes the problem that an unknown subcommand would not cause an error message to be printed. * Fix a busy-wait problem when `runcmd` runs a command with both standard output and error redirected to files and there was nothing to feed to its standard input (i.e., `runcmd` didn't need to write to the command, or read any of its output). This problem was found and fixed during work done for Codethink Limited. Version 1.20120630 ------------------ * New version numbering scheme: `API.DATE`. * cliapp now logs the command line and all settings, and the environment variables when the application starts. This helps when debugging problems other people are having. * The `cliapp.runcmd` and `cliapp.runcmd_unchecked` functions are now callable without having a `cliapp.Application` instance. This makes it easier to use them in large programs. * All types of settings can now have a `group` keyword argument for grouping them in the `--help` output. Version 0.29, released 2012-05-09 --------------------------------- * Pointless error message about exit code of an application removed. It is one of the mysteries of the universe why this ever made sense at all. Version 0.28, released 2012-05-08 --------------------------------- * Log files are now created using a 0600 file mode by default. The new option `--log-mode` can be used to change that. * Logging can now be disabled using `--log=none`. This is useful for overriding on the command a logging setting from a configuration file. * `for name in settings` and `settings.keys()` now work, making the settings class act more like an object. Thanks to Jannis Pohlmann for giving the inpiration for this change. * Settingses can now be grouped, using the new keyword argment `group` to the various methods to add a new setting. The grouping affects `--help` output only. The value to `group` is the title of the group. * There is now a plugin system included in cliapp. Version 0.27, released 2012-02-17 --------------------------------- * Bug fix: The `runcmd` and `runcmd_unchecked` methods now properly add the name of the program that failed to execute to the error message. This did not always happen previously, depending on race conditions on getting errors from a child process. Version 0.26, released 2012-02-11 --------------------------------- * `cliapp.Settings` now allows access to all sections in configuration files. The new `as_cp` method returns a `ConfigParser` instance, which is set to the current value of every registered setting, in the `config` section, plus any additional sections in the configuration files that have been read. Version 0.25.2, released 2012-02-08 --------------------------------- * Fixed another bug in the process pipelining support. This time it was a silly Python optional argument handling problem. Version 0.25.1, released 2012-02-08 --------------------------------- * Fix bug in the process pipelining support. Version 0.25, released 2012-02-06 --------------------------------- * Improved error message for when executing a non-existent command. * `cliapp.Application.runcmd` and `runcmd_unchecked` can now execute a pipeline. * New overrideable methods `cliapp.Application.setup` and `cleanup` make it easier to add code to be run just before and after `process_args`. Version 0.24, released 2012-01-14 --------------------------------- * Show the subcommand synopsis in --help output. * Bug fix: setting a boolean setting to false in a configuration file now works. Thanks to Richard Maw for the bug report. Version 0.23, released 2011-12-18 --------------------------------- * Back off from using the `logging.NullHandler` class, since that exists only in Python 2.7, and we want to support 2.6 too. Version 0.22, released 2011-12-03 --------------------------------- * The `runcmd` and `runcmd_unchecked` methods have had an API change: the `stdin` argument is now called `feed_stdin`. This is so that callers may use `stdin` to control how the `subprocess.Popen` module sets up the child program's standard input. * Syslog support has been added. Use `--log=syslog`. Version 0.21, released 2011-10-02 --------------------------------- * License changed to GPL version 2 or later. Version 0.20, released 2011-09-17 --------------------------------- * `cliapp(5)` manual page added, to explain in one place how applications built on cliapp will parse command lines and configuration files. * The manual pages can now specify how the non-option arguments are formatted, in manual pages, including for each subcommand separately. Version 0.19, released 2011-09-04 --------------------------------- * Subcommand descriptions are formatted more prettily, in --help output. * When a string list setting is set in a configuration file, any use of the corresponding option overrides the value from the configuration file, rather than appending to it. Version 0.18, released 2011-08-24 --------------------------------- * New `cliapp.Settings.dump_config` method, which can be useful, for example, if an application wants to save its configuration, or log it at startup. Version 0.17, released 2011-08-22 --------------------------------- * Give more humane error messages for IOError and OSError exceptions. * The `runcmd` and `runcmd_unchecked` methods now allow overriding the standard output and error redirections. * Memory profiling statistics can now be logged by the application, by calling the `cliapp.Application.dump_memory_profile` method. The `--dump-memory-profile` setting is provided by the user to specify how the profiling is done: simple RSS memory size, and meliae and heapy Python memory profilers are supported. Version 0.16, released 2011-08-19 --------------------------------- * An EPIPE error when writing to stdout is suppressed. This avoids a Python stack trace or other error when user pipes output to, say, less, and quits less before the application finishes writing everything to stdout. * Improve description of --log. (Thanks, Tapani Tarvainen.) Version 0.15.1, released 2011-08-03 ----------------------------------- * Fix parsing of string list options: their values were being added twice so `-foo=bar` would result in `settings['foo']` having the values `['bar', 'bar']`. Oops. As a result, the `parse_args` method has a new keyword argument, `configs_only`, which it should pass onto `Settings.parse_args`. * The argument names for the `process_input_line` method have been improved. Version 0.15, released 2011-08-02 --------------------------------- * `cliapp.Application` now has a `subcommands` attribute, which is a directory mapping subcommand names to the functions that implement them. This provides an alternative way to define new subcommands, which may be useful in plugin-based applications. * New method `cliapp.Application.runcmd_unchecked`. * There are some new options provided by defaults: - `--list-config-files` lists the config files the application will try to read - `--config=FILE` adds a file to the list of configuration files to be read - `--no-default-configs` prevents any of the default configuration files from being used; any `--config` options used after this one will still be read * Parsing of string list values in config files has been fixed. INI files have no standard syntax for presenting files (maybe JSON would be a better option), but I invented something. How very clever of me. At the same time, `--dump-config` now formats string list values properly. * Default values for string lists work better now: the default is used, unless the user specifies some values, in which only the values from the user are used. Version 0.14, released 2011-07-20 --------------------------------- * Start and end of program are now logged. This makes it easier to read log files. * Commands run by the `runcmd` method are now logged. * Bugfix: `runcmd` now passes extra arguments to `subprocess.Popen`. * A `Settings.require` method is added, so that it's easy to fail if the user has failed the give an option that is required in a given situation. (Mandatory options are a bit weird, but sometimes they happen.) * Subcommands may now be added explicitly, using the `Application.add_subcommand` method. This is helpful for applications that support plugins, for example. Version 0.13, released 2011-06-18 --------------------------------- * Change default log level to be `debug`. Nothing is logged unless the user requests it, and if they do, they probably want to debug something, so this seems like the natural default. * Log files are now rotated. See options --log-max, --log-keep. * Log files are formatted in a nicer way now. * Now registered with PyPI. * String list settings now have a more sensible handling of default values. Previously the default value would always be used and user-supplied values would be appended to the default. Now user-supplied values replace the default value entirely. * The old API for adding settings (`self.settings.add_string_setting` etc) is gone. This should not bother anyone, since I am the only known user of cliapp so far, and I've fixed my stuff already. * A `metavar` is provided if caller does not provide one. This fixes `--generate-manpage` issue where an option would be documented as not having an argument unless `metavar` was set explicitly. * `cliapp.Application.runcmd` runs external programs and captures their standard output. * The option parser is now created in a separate method than the one that uses it. This allows applications to modify the option parser in whatever way they want. * Only the basename of a program (from `sys.argv[0]`) is used when determining `progname`. This fixes a problem where config files could not be found because `progname` contained slashes. Version 0.12, released 2011-05-29 --------------------------------- * The new option `--generate-manpage` allows one to fill in the OPTIONS section of a manpage. * `cliapp` now supports **subcommands**, e.g., "git help". * API documentation is now formatted using Sphinx. Version 0.11, released 2011-03-21 --------------------------------- * `pydoc cliapp` now works more usefully, and includes documentation for the various classes exported, not just a list of packages. * Bugfix: if user specifies no log file, logging no longer happens to the standard output. This prevents exceptions from being reported twice. * Log format now includes timestamps. * New settings can now be added using shorter method names: `self.settings.string` rather than `self.settins.add_string_setting`. The old method names continue to work. Version 0.10, released 2011-03-21 -------------------------------- * The `metavar` argument for `optparse.OptionParser` can now be set for settings. This makes for prettier `--help` output. * The default value for integer settings is now 0, not `None`. * [[README]] now has some more documentation of how to use the framework. Version 0.9, released 2011-03-21 -------------------------------- * Bugfix: Boolean options now work correctly, even in --help output. Version 0.8, released 2011-03-20 -------------------------------- * Bugfix: duplicate option names in --help output fixed. * Exception handling has been improved: stack traces are now logged, and there's a `cliapp.AppException` base class for application exceptions, which will cause an error message to be written out, but no stack trace. All other exceptions cause a stack trace, so as to make it easier to debug things. Version 0.7, released 2011-03-12 -------------------------------- * Add configuration file support. * API change: all settings are now in a class of their own, and are accessed via `app.settings`, e.g., `app.settings['output']`. See the `cliapp.Settings` class. This change breaks old code, sorry. But since I am still the only user, nobody minds. * The callback setting type is now gone. * Application name can now be set via the Application class's initializer (optional `progname` argument), or by assigning to `cliapp.Settings.progname`. If not set explicitly, it is set from `sys.argv[0]`. Version 0.6, released 2011-02-19 -------------------------------- * New option types: list of strings, choice of strings, callback. * New standard option: --dump-setting-names. * Python profiling support: automatically if the right environment variable is set. If the process's argv[0] is 'foo', and 'FOO_PROFILE' is set, then profiling happens. * Documentation improvments. Version 0.5, released 2011-02-13 -------------------------------- * Catches exceptions and prints error message, instead of having the Python interpreter do a stack trace. * Support more traditional Unix command line filter behavior: read from stdin if no arguments are given, or a dash ('-') is given as the filename. * Count files and lines in files. * New options: --output, --log, --log-level. Version 0.4, released 2011-01-30 -------------------------------- * Bugfix: Subclasses can now actually set the application version, so that --version works. cliapp-1.20130808/README0000644000175000017500000000303712200776452014144 0ustar jenkinsjenkinsREADME for cliapp ================= cliapp is a Python framework for Unix-like command line programs. The scaffolding to set up a command line parser, open each input file, read each line of input, etc, is the same in each program. Only the logic of what to do with each line differs. See the API documentation on the `doc` subdirectory for details (or see for a pre-formatted version). Example ------- See the files `example.py` and `example2.py` for examples of how to use the framework. Version numbering ----------------- cliapp version numbers are of the form `API.DATE`, where `API` starts at 1, and gets incremented if the application API changes in an incompatible way. `DATE` is the date of the release. Legalese -------- # Copyright (C) 2011-2013 Lars Wirzenius # Copyright (C) 2012-2013 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. cliapp-1.20130808/cliapp.50000644000175000017500000001656512200776452014634 0ustar jenkinsjenkins.\" Copyright (C) 2011, 2012 Lars Wirzenius .\" .\" This program is free software; you can redistribute it and/or modify .\" it under the terms of the GNU General Public License as published by .\" the Free Software Foundation; either version 2 of the License, or .\" (at your option) any later version. .\" .\" This program is distributed in the hope that it will be useful, .\" but WITHOUT ANY WARRANTY; without even the implied warranty of .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .\" GNU General Public License for more details. .\" .\" You should have received a copy of the GNU General Public License along .\" with this program; if not, write to the Free Software Foundation, Inc., .\" 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. .\" .TH CLIAPP 5 .SH NAME cliapp \- config file and option conventions for Python command line framework .SH DESCRIPTION .B cliapp is a Python programming framework for writing command line applications for Unix-like operating systems. This manual page describes the conventions for configuration files and command line parsing provided by .BR cliapp . .PP .I "Configuration file variables" and .I "command line options" are handled by .B cliapp under a uniform abstraction: every setting is available both in configuration files and command line options. There are a few settings, provided by the framework itself, which are only available on the command line. For example, .B \-\-help outputs a short help text, listing all the available options, and .B \-\-dump\-config outputs a list of current configuration settings. .PP .I "Command line parsing" follows GNU conventions: short options start with a single dash, long options with two dashes, and options may be used anywhere on the command line. The order of options versus non-options does not matter. The exception is some of the options provided by the framework, which are executed immediately when found, and may be prevent the rest of the options from being parsed. .RB ( \-\-dump\-config is one of these, so use it at the end of the command line only.) Use .B -- on the command line to signal the end of options: no arguments after that are considered to be option. .PP Some settings may have aliases, which can be only a single character, and in that case they're parsed as single-character option names. .PP Some applications have .IR subcommands , which means that the first non-option argument is used to tell the application what to do. This is similar to what many version control systems do, for example CVS, svn, bzr, and git. Options are global, and are not specific to subcommands. Thus, .B \-\-foo means the same thing, regardless of what subcommand is being used. .SS "Configuration files" Configuration files use INI file syntax. All the settings are in the .B [config] section. Other sections are allowed, but it is up to the application to give meaning to them. .PP Multiple configuration files may be read. Settings from later ones override settings from earlier ones. Options override settings from the configuration files. .SS "String list settings" Some settings may be a list of values (each value being a string). For example, there might be a setting for patterns to search for, and multiple patterns are allowed. On the command line, that happens by using the option multiple times. In the configuration file, all values are given on one line, separated by commas. This is a non-standard extension to the INI file syntax. There is no way to escape commas. .PP Example: .IP .nf [config] pattern = foo, bar, foobar .SS "Boolean (true/false or on/off or yes/no) settings" When a setting can be either on or off, it's called a Boolean setting. Such settings are turned off by default, and turned on if used on the command line. In a configuration file, they need to be set to a value: if the value is one of .BR yes , .BR on , .BR true , or the number 1, the setting is turned on. Any other value means it is turned off. .PP .IP .nf [config] verbose = true attack-kittens = no .fi .PP This turns the verbose setting on, but does not launch attack kittens. .PP For every boolean setting, two command line options are added. If the setting is called .IR foo , the option .I \-\-foo will turn the setting on, and .I \-\-no\-foo will turn it off. The negation is only usable on the command line: its purpose is to allow the command line to override a setting from the configuration file. .SS "Logging and log files" Programs using .B cliapp automatically support several options for configuring the Python .B logging module. See the .B \-\-help output for options starting with .BR "log" for details. Logging can happen to a file or the system log. Log files can be rotated automatically based on size. .PP The .B \-\-trace option enables additional debug logging, which is usually only useful for programmers. The option configures the .B tracing library for Python, by Lars Wirzenius, which allows logging values of variables and other debug information in a way that is very lightweight when the tracing is turned off. The option specifies for which source code files to turn on tracing. The actual logging happens via the normal Python logging facilities, at the debug level. .SS "Python profiling support" You can run the application under the Python profiler .RB ( cProfile ) by setting an environment variable. The name of the variable is .BR FOO_PROFILE , where .B FOO is the name of the program, as set by the application code or determined by .B cliapp automatically. The value of the environment variable is the name of the file to which the resulting profile is to be written. .SS "Manual page generation" .B cliapp can generate parts of a manual page: the .I SYNOPSIS and .I OPTIONS sections. It fills these in automatically based on the subcommand and settings that a program supports. Use the .BR \-\-generate\-manpage =\fIFILE option, which is added automatically by .BR cliapp . The .I FILE is a manual page marked up using the .B -man macros for .BR troff (1). It should have empty .I SYNOPSIS and .I OPTIONS sections, and .B cliapp will fill them in. The output it to the standard output. .PP For example: .PP .RS foo --generate-manpage=foo.1.in > foo.1 .RE .PP You would keep the source code for the manual page in .I foo.1.in and have your Makefile produce .I foo.1 as shown above. .SS "Subcommands" .BR cliapp provides a way for the application to have .IR subcommands , in the style of .BR git (1), for example. If the application is called .IR foo , then it can have subcommands such as .IR "foo search" , and .IR "foo print" . The application gets to define the name and meaning of each subcommand. However, all settings (options and configuration files) are global, and can be used with all subcommands. It is up to each subcommand what settings it obeys. .PP If there are any subcommands, .B cliapp automatically adds the .B help subcommand. It allows you to get the help text for a specific subommand: .IR "foo help print" , for example. .SH FILES .B cliapp reads a list of configuration files at startup, on behalf of the application. The name of the application is included in the name. In the filenames below, the application name is .IR progname . .TP .BR /etc/progname.conf Global configuration file. .TP .BR /etc/progname/*.conf More global configuration files. These are read in ASCII sorted order. .TP .BR ~/.progname.conf Per-user configuration file. .TP .BR ~/.config/progname/*.conf More per-user configuration files. Again, ASCII sorted order. cliapp-1.20130808/COPYING0000644000175000017500000004325412200776452014324 0ustar jenkinsjenkins GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. cliapp-1.20130808/example.1.in0000644000175000017500000000207712200776452015411 0ustar jenkinsjenkins.\" Copyright (C) 2011 Lars Wirzenius .\" .\" This program is free software; you can redistribute it and/or modify .\" it under the terms of the GNU General Public License as published by .\" the Free Software Foundation; either version 2 of the License, or .\" (at your option) any later version. .\" .\" This program is distributed in the hope that it will be useful, .\" but WITHOUT ANY WARRANTY; without even the implied warranty of .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .\" GNU General Public License for more details. .\" .\" You should have received a copy of the GNU General Public License along .\" with this program; if not, write to the Free Software Foundation, Inc., .\" 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. .\" .TH EXAMPLE.PY 1 .SH NAME example.py \- cliapp-based simplistic fgrep clone .SH SYNOPSIS .SH DESCRIPTION .B example.py is an example program for .BR cliapp , a Python framework for command line utilities. .SH OPTIONS .SH EXAMPLE To find a pattern in a file: .IP python example.py --pattern=needle haystack.txt cliapp-1.20130808/example.py0000644000175000017500000000414112200776452015266 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Example for cliapp framework. This implements an fgrep-like utility. ''' import cliapp import logging class ExampleApp(cliapp.Application): '''A little fgrep-like tool.''' def add_settings(self): self.settings.string_list(['pattern', 'e'], 'search for regular expression PATTERN', metavar='REGEXP') self.settings.boolean(['dummy'], 'this setting is ignored', group='Test Group') self.settings.string(['yoyo'], 'yoyo', group=cliapp.config_group_name) self.settings.string(['nono'], 'nono', default=None) # We override process_inputs to be able to do something after the last # input line. def process_inputs(self, args): self.matches = 0 cliapp.Application.process_inputs(self, args) self.output.write('There were %s matches.\n' % self.matches) def process_input_line(self, name, line): logging.debug('processing %s:%s' % (name, self.lineno)) for pattern in self.settings['pattern']: if pattern in line: self.output.write('%s:%s: %s' % (name, self.lineno, line)) self.matches += 1 logging.debug('Match: %s line %d' % (name, self.lineno)) app = ExampleApp(version='0.1.2') app.settings.config_files = ['example.conf'] app.run() cliapp-1.20130808/example2.1.in0000644000175000017500000000211212200776452015461 0ustar jenkinsjenkins.\" Copyright (C) 2011 Lars Wirzenius .\" .\" This program is free software; you can redistribute it and/or modify .\" it under the terms of the GNU General Public License as published by .\" the Free Software Foundation; either version 2 of the License, or .\" (at your option) any later version. .\" .\" This program is distributed in the hope that it will be useful, .\" but WITHOUT ANY WARRANTY; without even the implied warranty of .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .\" GNU General Public License for more details. .\" .\" You should have received a copy of the GNU General Public License along .\" with this program; if not, write to the Free Software Foundation, Inc., .\" 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. .\" .TH EXAMPLE2.PY 1 .SH NAME example2.py \- cliapp-based simplistic hello, world app .SH SYNOPSIS .SH DESCRIPTION .B example2.py is an example program for .BR cliapp , a Python framework for command line utilities. .SH OPTIONS .SH EXAMPLE To greet: .IP python example2.py greet .PP To insult: .IP python example2.py insult cliapp-1.20130808/example2.py0000644000175000017500000000322512200776452015352 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Example for cliapp framework. Greet or insult people. ''' import cliapp import logging class ExampleApp(cliapp.Application): cmd_synopsis = { 'greet': '[USER]...', 'insult': '[USER]...', } def cmd_greet(self, args): '''Greet the user. The user is treated to a a courteus, but terse form of greeting. ''' for arg in args: self.output.write('greetings, %s\n' % arg) def cmd_insult(self, args): '''Insult the user. Sometimes, though rarely, it happens that a user is really a bit of a prat, and needs to be told off. This is the command for that. ''' for arg in args: self.output.write('you suck, %s\n' % arg) app = ExampleApp(version='0.1.2', description=''' Greet the user. Or possibly insult them. User's choice. ''', epilog=''' This is the epilog. I hope you like it. ''') app.run() cliapp-1.20130808/example3.py0000644000175000017500000000251712200776452015356 0ustar jenkinsjenkins# Copyright (C) 2012 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Example for cliapp framework. Demonstrate the compute_setting_values method. ''' import cliapp import urlparse class ExampleApp(cliapp.Application): '''A little fgrep-like tool.''' def add_settings(self): self.settings.string(['url'], 'a url') self.settings.string(['protocol'], 'the protocol') def compute_setting_values(self, settings): if not self.settings['protocol']: schema = urlparse.urlparse(self.settings['url'])[0] self.settings['protocol'] = schema def process_args(self, args): return app = ExampleApp() app.run() cliapp-1.20130808/example4.py0000644000175000017500000000313712200776452015356 0ustar jenkinsjenkins# Copyright (C) 2013 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import cliapp import logging class ExampleApp(cliapp.Application): def setup(self): self.add_subcommand('insult', self.insult, hidden=True) def add_settings(self): self.settings.string(['yoyo'], 'yoyo help', hidden=True) self.settings.boolean(['blip'], 'blip help', hidden=True) def cmd_greet(self, args): '''Greet the user. The user is treated to a a courteus, but terse form of greeting. ''' for arg in args: self.output.write('greetings, %s\n' % arg) def insult(self, args): '''Insult the user. (hidden command) Sometimes, though rarely, it happens that a user is really a bit of a prat, and needs to be told off. This is the command for that. ''' for arg in args: self.output.write('you suck, %s\n' % arg) ExampleApp().run() cliapp-1.20130808/Makefile0000644000175000017500000000164112200776452014723 0ustar jenkinsjenkins# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. all: $(MAKE) -C doc html clean: rm -rf cliapp/*.py[co] .coverage build $(MAKE) -C doc clean check: python -m CoverageTestRunner --ignore-missing-from=without-tests rm .coverage cliapp-1.20130808/setup.py0000644000175000017500000000361712200776452015002 0ustar jenkinsjenkins#!/usr/bin/python # Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from distutils.core import setup import glob import cliapp setup(name='cliapp', version=cliapp.__version__, author='Lars Wirzenius', author_email='liw@liw.fi', url='http://liw.fi/cliapp/', description='framework for Unix command line programs', long_description='''\ cliapp makes it easier to write typical Unix command line programs, by taking care of the common tasks they need to do, such as parsing the command line, reading configuration files, setting up logging, iterating over lines of input files, and so on. ''', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: Unix', 'Programming Language :: Python :: 2', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: User Interfaces', 'Topic :: Text Processing :: Filters', 'Topic :: Utilities', ], packages=['cliapp'], data_files=[('share/man/man5', glob.glob('*.5'))], ) cliapp-1.20130808/without-tests0000644000175000017500000000037612200776452016055 0ustar jenkinsjenkins./cliapp/__init__.py ./example.py ./example2.py ./setup.py ./cliapp/genman.py ./doc/conf.py ./test-plugins/oldhello_plugin.py ./test-plugins/hello_plugin.py ./test-plugins/wrongversion_plugin.py ./test-plugins/aaa_hello_plugin.py example3.py example4.py