summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--README10
-rw-r--r--cliapp/__init__.py8
-rw-r--r--cliapp/app.py160
-rw-r--r--cliapp/app_tests.py58
-rw-r--r--cliapp/fmt.py16
-rw-r--r--cliapp/fmt_tests.py8
-rw-r--r--cliapp/genman.py28
-rw-r--r--cliapp/hook.py22
-rw-r--r--cliapp/hook_tests.py14
-rw-r--r--cliapp/hookmgr.py18
-rw-r--r--cliapp/hookmgr_tests.py12
-rw-r--r--cliapp/plugin.py54
-rw-r--r--cliapp/plugin_tests.py8
-rw-r--r--cliapp/pluginmgr.py52
-rw-r--r--cliapp/pluginmgr_tests.py22
-rw-r--r--cliapp/runcmd.py38
-rw-r--r--cliapp/runcmd_tests.py24
-rw-r--r--cliapp/settings.py154
-rw-r--r--cliapp/settings_tests.py52
-rw-r--r--debian/changelog6
-rw-r--r--debian/control2
-rw-r--r--doc/example.rst6
-rw-r--r--doc/index.rst4
-rw-r--r--doc/profiling.rst4
-rw-r--r--doc/subcommands.rst4
-rw-r--r--doc/walkthrough.rst2
-rw-r--r--example.1.in6
-rw-r--r--example.py16
-rw-r--r--example2.1.in6
-rw-r--r--example2.py24
-rw-r--r--example3.py10
-rw-r--r--example4.py26
-rw-r--r--setup.py6
-rw-r--r--test-plugins/hello_plugin.py2
35 files changed, 444 insertions, 444 deletions
diff --git a/Makefile b/Makefile
index ce5ecd8..9ccb5fb 100644
--- a/Makefile
+++ b/Makefile
@@ -1,15 +1,15 @@
# 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.
diff --git a/README b/README
index 04c3e7a..540fdb0 100644
--- a/README
+++ b/README
@@ -12,14 +12,14 @@ See the API documentation on the `doc` subdirectory for details
Example
-------
-See the files `example.py` and `example2.py` for examples of how to
+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
+at 1, and gets incremented if the application API changes in an
incompatible way. `DATE` is the date of the release.
Legalese
@@ -27,17 +27,17 @@ Legalese
# 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.
diff --git a/cliapp/__init__.py b/cliapp/__init__.py
index 8b774b2..b5b3b2a 100644
--- a/cliapp/__init__.py
+++ b/cliapp/__init__.py
@@ -1,15 +1,15 @@
# 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.
@@ -19,7 +19,7 @@ __version__ = '1.20130424'
from fmt import TextFormat
-from settings import (Settings, log_group_name, config_group_name,
+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
diff --git a/cliapp/app.py b/cliapp/app.py
index 3c07310..42b120e 100644
--- a/cliapp/app.py
+++ b/cliapp/app.py
@@ -1,15 +1,15 @@
# 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.
@@ -34,27 +34,27 @@ 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,
+ logging.handlers.RotatingFileHandler.__init__(self, filename,
*args, **kwargs)
def _open(self):
@@ -68,25 +68,25 @@ class LogHandler(logging.handlers.RotatingFileHandler): # pragma: no cover
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
+ 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,
@@ -106,28 +106,28 @@ class Application(object):
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,
+
+ 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,
+ 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)
@@ -143,14 +143,14 @@ class Application(object):
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
@@ -164,7 +164,7 @@ class Application(object):
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,
@@ -181,7 +181,7 @@ class Application(object):
self.setup_logging()
self.log_config()
-
+
if self.settings['output']:
self.output = open(self.settings['output'], 'w')
else:
@@ -223,12 +223,12 @@ class Application(object):
stderr.write(traceback.format_exc())
sys.exit(1)
- logging.info('%s version %s ends normally' %
+ 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
@@ -237,23 +237,23 @@ class Application(object):
"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
@@ -274,7 +274,7 @@ class Application(object):
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]))
@@ -294,11 +294,11 @@ class Application(object):
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
+ return [x
+ for x in dir(self)
+ if x.startswith('cmd_') and
inspect.ismethod(getattr(self, x))]
def _normalize_cmd(self, cmd):
@@ -361,7 +361,7 @@ class Application(object):
def setup_logging(self): # pragma: no cover
'''Set up logging.'''
-
+
level_name = self.settings['log-level']
levels = {
'debug': logging.DEBUG,
@@ -379,7 +379,7 @@ class Application(object):
handler = LogHandler(
self.settings['log'],
perms=int(self.settings['log-mode'], 8),
- maxBytes=self.settings['log-max'],
+ maxBytes=self.settings['log-max'],
backupCount=self.settings['log-keep'],
delay=False)
fmt = '%(asctime)s %(levelname)s %(message)s'
@@ -418,14 +418,14 @@ class Application(object):
handler = LogHandler(
self.settings['log'],
perms=int(self.settings['log-mode'], 8),
- maxBytes=self.settings['log-max'],
+ 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
@@ -437,7 +437,7 @@ class Application(object):
return '%Y-%m-%d %H:%M:%S'
def log_config(self):
- logging.info('%s version %s starts' %
+ logging.info('%s version %s starts' %
(self.settings.progname, self.settings.version))
logging.debug('sys.argv: %s' % sys.argv)
logging.debug('environment variables:')
@@ -451,12 +451,12 @@ class Application(object):
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 '.'
@@ -480,9 +480,9 @@ class Application(object):
def parse_args(self, args, configs_only=False):
'''Parse the command line.
-
+
Return list of non-option arguments.
-
+
'''
return self.settings.parse_args(
@@ -492,34 +492,34 @@ class Application(object):
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')
@@ -532,7 +532,7 @@ class Application(object):
break
else:
raise SystemExit('unknown subcommand %s' % args[0])
-
+
method = self.subcommands[cmd]
method(args[1:])
else:
@@ -540,17 +540,17 @@ class Application(object):
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
+ 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 ['-']:
@@ -558,17 +558,17 @@ class Application(object):
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:
@@ -576,9 +576,9 @@ class Application(object):
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
@@ -593,15 +593,15 @@ class Application(object):
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)
@@ -617,12 +617,12 @@ class Application(object):
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']
@@ -630,7 +630,7 @@ class Application(object):
if kind == 'none':
return
-
+
now = time.time()
if self.last_memory_dump + interval > now:
return
@@ -647,7 +647,7 @@ class Application(object):
logging.debug('dumping memory profiling data: %s' % msg)
logging.debug('VmRSS: %s KiB' % self._vmrss())
-
+
if kind == 'simple':
return
diff --git a/cliapp/app_tests.py b/cliapp/app_tests.py
index 674e033..080a600 100644
--- a/cliapp/app_tests.py
+++ b/cliapp/app_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -33,7 +33,7 @@ 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))
@@ -45,9 +45,9 @@ class ApplicationTests(unittest.TestCase):
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
@@ -59,7 +59,7 @@ class ApplicationTests(unittest.TestCase):
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'], '')
@@ -77,34 +77,34 @@ class ApplicationTests(unittest.TestCase):
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):
@@ -180,7 +180,7 @@ class ApplicationTests(unittest.TestCase):
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')
@@ -196,7 +196,7 @@ class ApplicationTests(unittest.TestCase):
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():
@@ -235,10 +235,10 @@ class ApplicationTests(unittest.TestCase):
foo = Foo()
foo.run(args=['foo', 'bar'])
- self.assertEqual(counters,
- [(1, 1, 1),
- (1, 2, 2),
- (2, 3, 1),
+ self.assertEqual(counters,
+ [(1, 1, 1),
+ (1, 2, 2),
+ (2, 3, 1),
(2, 4, 2)])
def test_run_prints_out_error_for_appexception(self):
@@ -270,33 +270,33 @@ class ApplicationTests(unittest.TestCase):
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, [],
+ 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?'],
+ self.assertRaises(SystemExit, self.app.run, ['what?'],
stderr=self.trash, log=devnull)
def test_calls_subcommand_method(self):
@@ -320,10 +320,10 @@ 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)
diff --git a/cliapp/fmt.py b/cliapp/fmt.py
index 72a9265..1358b0a 100644
--- a/cliapp/fmt.py
+++ b/cliapp/fmt.py
@@ -1,15 +1,15 @@
# 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.
@@ -35,10 +35,10 @@ class Paragraph(object):
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
@@ -81,13 +81,13 @@ class TextFormat(object):
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):
diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py
index 0e109e7..79f1ab6 100644
--- a/cliapp/fmt_tests.py
+++ b/cliapp/fmt_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -24,7 +24,7 @@ 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(''), '')
diff --git a/cliapp/genman.py b/cliapp/genman.py
index efad53b..e81fcfe 100644
--- a/cliapp/genman.py
+++ b/cliapp/genman.py
@@ -1,15 +1,15 @@
# 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.
@@ -22,13 +22,13 @@ 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])
@@ -39,7 +39,7 @@ class ManpageGenerator(object):
@property
def options(self):
return self.option_list(self.parser)
-
+
def format_template(self):
sections = (('SYNOPSIS', self.format_synopsis()),
('OPTIONS', self.format_options()))
@@ -48,12 +48,12 @@ class ManpageGenerator(object):
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)
@@ -74,7 +74,7 @@ class ManpageGenerator(object):
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)
@@ -85,7 +85,7 @@ class ManpageGenerator(object):
def format_options(self):
lines = []
-
+
for option in self.sort_options(self.parser.option_list):
lines += self.format_option_for_options(option)
@@ -109,7 +109,7 @@ class ManpageGenerator(object):
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:
@@ -117,16 +117,16 @@ class ManpageGenerator(object):
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]+')
diff --git a/cliapp/hook.py b/cliapp/hook.py
index 21b1614..d453f2e 100644
--- a/cliapp/hook.py
+++ b/cliapp/hook.py
@@ -1,15 +1,15 @@
# 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.
@@ -33,10 +33,10 @@ class Hook(object):
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.
'''
@@ -44,12 +44,12 @@ class Hook(object):
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:
@@ -59,16 +59,16 @@ class Hook(object):
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)
diff --git a/cliapp/hook_tests.py b/cliapp/hook_tests.py
index c7e3a4d..a32f33a 100644
--- a/cliapp/hook_tests.py
+++ b/cliapp/hook_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -24,18 +24,18 @@ 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)
@@ -65,7 +65,7 @@ class FilterHookTests(unittest.TestCase):
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([])
diff --git a/cliapp/hookmgr.py b/cliapp/hookmgr.py
index c300886..80db2d0 100644
--- a/cliapp/hookmgr.py
+++ b/cliapp/hookmgr.py
@@ -1,15 +1,15 @@
# 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.
@@ -18,15 +18,15 @@
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:
@@ -35,11 +35,11 @@ class HookManager(object):
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)
diff --git a/cliapp/hookmgr_tests.py b/cliapp/hookmgr_tests.py
index 9957d00..7abdeba 100644
--- a/cliapp/hookmgr_tests.py
+++ b/cliapp/hookmgr_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -25,7 +25,7 @@ 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
@@ -33,10 +33,10 @@ class HookManagerTests(unittest.TestCase):
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])
diff --git a/cliapp/plugin.py b/cliapp/plugin.py
index c305841..c6ea66f 100644
--- a/cliapp/plugin.py
+++ b/cliapp/plugin.py
@@ -1,15 +1,15 @@
# 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.
@@ -34,7 +34,7 @@ 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,
@@ -42,83 +42,83 @@ class Plugin(object):
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
+ 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
+
+ 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()
diff --git a/cliapp/plugin_tests.py b/cliapp/plugin_tests.py
index 99799d8..8c98039 100644
--- a/cliapp/plugin_tests.py
+++ b/cliapp/plugin_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -24,7 +24,7 @@ class PluginTests(unittest.TestCase):
def setUp(self):
self.plugin = Plugin()
-
+
def test_name_is_class_name(self):
self.assertEqual(self.plugin.name, 'Plugin')
diff --git a/cliapp/pluginmgr.py b/cliapp/pluginmgr.py
index 1b30932..5e99523 100644
--- a/cliapp/pluginmgr.py
+++ b/cliapp/pluginmgr.py
@@ -1,15 +1,15 @@
# 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.
@@ -37,22 +37,22 @@ 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
+ 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):
@@ -83,14 +83,14 @@ class PluginManager(object):
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)
@@ -100,14 +100,14 @@ class PluginManager(object):
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:
@@ -128,10 +128,10 @@ class PluginManager(object):
name, ext = os.path.splitext(os.path.basename(pathname))
f = file(pathname, 'r')
- module = imp.load_module(name, f, pathname,
+ 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):
@@ -139,37 +139,37 @@ class PluginManager(object):
**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()
diff --git a/cliapp/pluginmgr_tests.py b/cliapp/pluginmgr_tests.py
index 5537f8c..d458ac3 100644
--- a/cliapp/pluginmgr_tests.py
+++ b/cliapp/pluginmgr_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -24,7 +24,7 @@ class PluginManagerInitialStateTests(unittest.TestCase):
def setUp(self):
self.pm = PluginManager()
-
+
def test_locations_is_empty_list(self):
self.assertEqual(self.pm.locations, [])
@@ -39,7 +39,7 @@ class PluginManagerInitialStateTests(unittest.TestCase):
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, {})
@@ -51,7 +51,7 @@ class PluginManagerTests(unittest.TestCase):
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',
@@ -100,19 +100,19 @@ 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'))
diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py
index 7565bbd..ebb06fc 100644
--- a/cliapp/runcmd.py
+++ b/cliapp/runcmd.py
@@ -1,16 +1,16 @@
# 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.
@@ -29,15 +29,15 @@ import cliapp
def runcmd(argv, *args, **kwargs):
'''Run external command or pipeline.
- Example: ``runcmd(['grep', 'foo'], ['wc', '-l'],
+ 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 = (
@@ -62,15 +62,15 @@ def runcmd(argv, *args, **kwargs):
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)
@@ -123,7 +123,7 @@ def _build_pipeline(argvs, pipe_stdin, pipe_stdout, pipe_stderr, kwargs):
stdin = procs[-1].stdout
stdout = subprocess.PIPE
stderr = pipe_stderr
- p = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
+ p = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
stderr=stderr, close_fds=True, **kwargs)
procs.append(p)
@@ -137,7 +137,7 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr):
err = []
pos = 0
io_size = 1024
-
+
def set_nonblocking(fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
flags = flags | os.O_NONBLOCK
@@ -149,7 +149,7 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr):
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()
@@ -168,7 +168,7 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr):
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)
@@ -222,7 +222,7 @@ def shell_quote(s):
digits = '0123456789'
punctuation = '-_/=.,:'
safe = set(lower_ascii + upper_ascii + digits + punctuation)
-
+
quoted = []
for c in s:
if c in safe:
@@ -237,7 +237,7 @@ def shell_quote(s):
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
@@ -255,15 +255,15 @@ def ssh_runcmd(target, argv, **kwargs): # pragma: no cover
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.
'''
@@ -271,7 +271,7 @@ def ssh_runcmd(target, argv, **kwargs): # pragma: no cover
tty = kwargs.get('tty', None)
if tty:
ssh_cmd = ['ssh', '-tt', target, '--']
- elif tty is False:
+ elif tty is False:
ssh_cmd = ['ssh', '-T', target, '--']
else:
ssh_cmd = ['ssh', target, '--']
diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py
index a8a15dc..e89b73e 100644
--- a/cliapp/runcmd_tests.py
+++ b/cliapp/runcmd_tests.py
@@ -1,16 +1,16 @@
# 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.
@@ -35,14 +35,14 @@ 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)
@@ -69,11 +69,11 @@ class RuncmdTests(unittest.TestCase):
self.assertEqual(cliapp.runcmd(['pwd'], cwd='/'), '/\n')
def test_runcmd_unchecked_returns_values_on_success(self):
- self.assertEqual(cliapp.runcmd_unchecked(['echo', 'foo']),
+ 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']),
+ self.assertEqual(cliapp.runcmd_unchecked(['false']),
(1, '', ''))
def test_runcmd_unchecked_runs_simple_pipeline(self):
@@ -82,7 +82,7 @@ class RuncmdTests(unittest.TestCase):
(0, '4\n', ''))
def test_runcmd_unchecked_runs_longer_pipeline(self):
- self.assertEqual(cliapp.runcmd_unchecked(['echo', 'foo'],
+ self.assertEqual(cliapp.runcmd_unchecked(['echo', 'foo'],
['cat'],
['wc', '-c']),
(0, '4\n', ''))
@@ -94,7 +94,7 @@ class RuncmdTests(unittest.TestCase):
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)
@@ -103,7 +103,7 @@ class RuncmdTests(unittest.TestCase):
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)
@@ -115,7 +115,7 @@ class RuncmdTests(unittest.TestCase):
def test_runcmd_unchecked_handles_stdout_err_redirected_to_same_file(self):
fd, filename = tempfile.mkstemp()
- exit, out, err = cliapp.runcmd_unchecked(['sleep', '2'],
+ exit, out, err = cliapp.runcmd_unchecked(['sleep', '2'],
stdout=fd,
stderr=subprocess.STDOUT)
os.close(fd)
diff --git a/cliapp/settings.py b/cliapp/settings.py
index 9fb8a4c..b58a3e7 100644
--- a/cliapp/settings.py
+++ b/cliapp/settings.py
@@ -1,15 +1,15 @@
# 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.
@@ -57,13 +57,13 @@ class Setting(object):
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)
@@ -88,7 +88,7 @@ class StringSetting(Setting):
class StringListSetting(Setting):
action = 'append'
-
+
def __init__(
self, names, default, help, metavar=None, group=None, hidden=False):
Setting.__init__(
@@ -104,17 +104,17 @@ class StringListSetting(Setting):
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)
@@ -122,7 +122,7 @@ class StringListSetting(Setting):
class ChoiceSetting(Setting):
type = 'choice'
-
+
def __init__(
self, names, choices, help, metavar=None, group=None, hidden=False):
Setting.__init__(
@@ -133,7 +133,7 @@ class ChoiceSetting(Setting):
def default_metavar(self):
return self.names[0].upper()
-
+
class BooleanSetting(Setting):
action = 'store_true'
@@ -145,7 +145,7 @@ class BooleanSetting(Setting):
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:
@@ -163,8 +163,8 @@ class ByteSizeSetting(Setting):
def parse_human_size(self, size):
'''Parse a size using suffix into plain bytes.'''
-
- m = re.match(r'''(?P<number>\d+(\.\d+)?) \s*
+
+ m = re.match(r'''(?P<number>\d+(\.\d+)?) \s*
(?P<unit>k|ki|m|mi|g|gi|t|ti)? b? \s*$''',
size.lower(), flags=re.X)
if not m:
@@ -189,7 +189,7 @@ class ByteSizeSetting(Setting):
def get_value(self):
return long(self._string_value)
-
+
def set_value(self, value):
if type(value) == str:
value = self.parse_human_size(value)
@@ -205,7 +205,7 @@ class IntegerSetting(Setting):
def get_value(self):
return long(self._string_value)
-
+
def set_value(self, value):
self._string_value = str(value)
@@ -226,40 +226,40 @@ class Settings(object):
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.
+ ``-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,
+ def __init__(self, progname, version, usage=None, description=None,
epilog=None):
self._settingses = dict()
self._canonical_names = list()
@@ -269,34 +269,34 @@ class Settings(object):
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'],
+ self.string(['output'],
'write output to FILE, instead of standard output',
metavar='FILE')
- self.string(['log'],
+ 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'],
+ 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'],
+ 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'],
+ self.string(['log-mode'],
'set permissions of new log files to MODE (octal; '
'default %default)',
metavar='MODE', default='0600', group=log_group_name)
@@ -327,10 +327,10 @@ class Settings(object):
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,
@@ -338,12 +338,12 @@ class Settings(object):
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))
@@ -354,11 +354,11 @@ class Settings(object):
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):
@@ -373,7 +373,7 @@ class Settings(object):
def __contains__(self, name):
return name in self._settingses
-
+
def __iter__(self):
'''Iterate over canonical settings names.'''
for name in self._canonical_names:
@@ -385,21 +385,21 @@ class Settings(object):
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
@@ -417,7 +417,7 @@ class Settings(object):
maybe = lambda func: (lambda *args: None) if configs_only else func
- # Maintain lists of callback function calls that are deferred.
+ # 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.
@@ -444,20 +444,20 @@ class Settings(object):
# 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
@@ -468,9 +468,9 @@ class Settings(object):
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)
@@ -545,7 +545,7 @@ class Settings(object):
metavar='TEMPLATE')
# Add --help-all.
-
+
def help_all(*args): # pragma: no cover
pp = self.build_parser(
configs_only=configs_only,
@@ -554,7 +554,7 @@ class Settings(object):
all_options=True)
sys.stdout.write(pp.format_help())
sys.exit(0)
-
+
config_group.add_option(
'--help-all',
action='callback',
@@ -581,7 +581,7 @@ class Settings(object):
def add_option(obj, s):
option_names = self._option_names(s.names)
- obj.add_option(*option_names,
+ obj.add_option(*option_names,
action='callback',
callback=maybe(set_value),
callback_args=(s,),
@@ -595,7 +595,7 @@ class Settings(object):
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
+ unused_names = [x for x in neg_names
if x[2:] not in self._settingses]
obj.add_option(*unused_names,
action='callback',
@@ -605,14 +605,14 @@ class Settings(object):
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)
@@ -625,10 +625,10 @@ class Settings(object):
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 = []
@@ -652,34 +652,34 @@ class Settings(object):
@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,
+
+ If dirname does not exist or is not a directory,
return empty list.
-
+
'''
-
+
if not os.path.isdir(dirname):
return []
@@ -696,7 +696,7 @@ class Settings(object):
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):
@@ -707,9 +707,9 @@ class Settings(object):
def load_configs(self, open=open):
'''Load all config files in self.config_files.
-
+
Silently ignore files that do not exist.
-
+
'''
cp = ConfigParser.ConfigParser()
diff --git a/cliapp/settings_tests.py b/cliapp/settings_tests.py
index 757811a..7a44d81 100644
--- a/cliapp/settings_tests.py
+++ b/cliapp/settings_tests.py
@@ -1,15 +1,15 @@
# 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.
@@ -27,14 +27,14 @@ 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')
@@ -146,14 +146,14 @@ class SettingsTests(unittest.TestCase):
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
@@ -173,14 +173,14 @@ class SettingsTests(unittest.TestCase):
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)
@@ -217,7 +217,7 @@ class SettingsTests(unittest.TestCase):
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)
@@ -261,27 +261,27 @@ class SettingsTests(unittest.TestCase):
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']
@@ -290,12 +290,12 @@ bar = ping, pong
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']
@@ -304,14 +304,14 @@ bar = ping, pong
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']
@@ -320,14 +320,14 @@ bar = ping, pong
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']
@@ -337,10 +337,10 @@ bar = ping, pong
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):
@@ -358,7 +358,7 @@ bar = ping, pong
def test_require_raises_error_if_string_unset(self):
self.settings.string(['foo'], 'foo help', default=None)
- self.assertRaises(cliapp.AppException, self.settings.require,
+ self.assertRaises(cliapp.AppException, self.settings.require,
'foo')
def test_require_is_ok_with_set_string(self):
@@ -407,7 +407,7 @@ bar = ping, pong
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]
@@ -416,7 +416,7 @@ 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)
diff --git a/debian/changelog b/debian/changelog
index b904ea2..6f6567c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -151,8 +151,8 @@ python-cliapp (0.14-1) unstable; urgency=low
- 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
+ - Subcommands may now be added explicitly, using the
+ `Application.add_subcommand` method. This is helpful for applications
that support plugins, for example.
-- Lars Wirzenius <liw@liw.fi> Wed, 20 Jul 2011 20:33:04 +0100
@@ -178,7 +178,7 @@ python-cliapp (0.12-1) unstable; urgency=low
python-cliapp (0.11) squeeze; urgency=low
- * New upstream release.
+ * New upstream release.
-- Lars Wirzenius <liw@liw.fi> Wed, 18 May 2011 06:34:55 +0100
diff --git a/debian/control b/debian/control
index a559611..a0fefb3 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Maintainer: Lars Wirzenius <liw@liw.fi>
Section: python
Priority: optional
Standards-Version: 3.9.2
-Build-Depends: debhelper (>= 7.3.8), python-all (>= 2.6.6-3~),
+Build-Depends: debhelper (>= 7.3.8), python-all (>= 2.6.6-3~),
python-coverage-test-runner, python-sphinx
X-Python-Version: >= 2.6
diff --git a/doc/example.rst b/doc/example.rst
index d22ed89..ee5882f 100644
--- a/doc/example.rst
+++ b/doc/example.rst
@@ -6,17 +6,17 @@ Example
class ExampleApp(cliapp.Application):
def add_settings(self):
- self.settings.string_list(['pattern', 'e'],
+ 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:
diff --git a/doc/index.rst b/doc/index.rst
index 70fa33e..c213294 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -5,7 +5,7 @@ Welcome to cliapp
which typically have the following characteristics:
* non-interactive
-* the programs read input files named on the command line,
+* 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
@@ -33,7 +33,7 @@ framework.
.. toctree::
:numbered:
-
+
example
walkthrough
logging
diff --git a/doc/profiling.rst b/doc/profiling.rst
index ed8403c..243fbce 100644
--- a/doc/profiling.rst
+++ b/doc/profiling.rst
@@ -1,8 +1,8 @@
Profiling support
=================
-
+
If ``sys.argv[0]`` is ``foo``, and the environment
-variable ``FOO_PROFILE`` is set, then the execution of the
+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.
diff --git a/doc/subcommands.rst b/doc/subcommands.rst
index 150357a..02d653d 100644
--- a/doc/subcommands.rst
+++ b/doc/subcommands.rst
@@ -8,14 +8,14 @@ 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
diff --git a/doc/walkthrough.rst b/doc/walkthrough.rst
index d86a8e4..e15a10a 100644
--- a/doc/walkthrough.rst
+++ b/doc/walkthrough.rst
@@ -13,7 +13,7 @@ summary is here:
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
+* ``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
diff --git a/example.1.in b/example.1.in
index e3aa6e1..34760ef 100644
--- a/example.1.in
+++ b/example.1.in
@@ -1,15 +1,15 @@
.\" 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.
diff --git a/example.py b/example.py
index c86aaea..14a56db 100644
--- a/example.py
+++ b/example.py
@@ -1,15 +1,15 @@
# 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.
@@ -29,9 +29,9 @@ import logging
class ExampleApp(cliapp.Application):
'''A little fgrep-like tool.'''
-
+
def add_settings(self):
- self.settings.string_list(['pattern', 'e'],
+ self.settings.string_list(['pattern', 'e'],
'search for regular expression PATTERN',
metavar='REGEXP')
@@ -39,7 +39,7 @@ class ExampleApp(cliapp.Application):
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
@@ -56,8 +56,8 @@ class ExampleApp(cliapp.Application):
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()
diff --git a/example2.1.in b/example2.1.in
index b7c45fe..b6b1c90 100644
--- a/example2.1.in
+++ b/example2.1.in
@@ -1,15 +1,15 @@
.\" 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.
diff --git a/example2.py b/example2.py
index f766d34..1f061b9 100644
--- a/example2.py
+++ b/example2.py
@@ -1,15 +1,15 @@
# 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.
@@ -32,28 +32,28 @@ class ExampleApp(cliapp.Application):
'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
+
+ 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.
diff --git a/example3.py b/example3.py
index 5de5a11..163231d 100644
--- a/example3.py
+++ b/example3.py
@@ -1,15 +1,15 @@
# 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.
@@ -29,7 +29,7 @@ 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')
@@ -38,7 +38,7 @@ class ExampleApp(cliapp.Application):
if not self.settings['protocol']:
schema = urlparse.urlparse(self.settings['url'])[0]
self.settings['protocol'] = schema
-
+
def process_args(self, args):
return
diff --git a/example4.py b/example4.py
index 14386df..f500bbd 100644
--- a/example4.py
+++ b/example4.py
@@ -1,15 +1,15 @@
# 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.
@@ -23,31 +23,31 @@ 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
+
+ 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()
diff --git a/setup.py b/setup.py
index 9ae8a69..1a97a42 100644
--- a/setup.py
+++ b/setup.py
@@ -1,16 +1,16 @@
#!/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.
diff --git a/test-plugins/hello_plugin.py b/test-plugins/hello_plugin.py
index 1a4f1ea..a102502 100644
--- a/test-plugins/hello_plugin.py
+++ b/test-plugins/hello_plugin.py
@@ -9,7 +9,7 @@ class Hello(cliapp.Plugin):
@property
def version(self):
return '0.0.1'
-
+
def setup(self):
self.setup_called = True