From 81a0ee0e84365f896e798bad3d90c5c136efca23 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 24 Nov 2012 18:42:08 +0000 Subject: Include options from option groups in synopsis --- cliapp/genman.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cliapp/genman.py b/cliapp/genman.py index 8d604f8..65f42df 100644 --- a/cliapp/genman.py +++ b/cliapp/genman.py @@ -29,10 +29,16 @@ class ManpageGenerator(object): 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 sorted(self.parser.option_list, - key=lambda o: (o._long_opts + o._short_opts)[0]) + return self.option_list(self.parser) def format_template(self): sections = (('SYNOPSIS', self.format_synopsis()), @@ -48,7 +54,10 @@ class ManpageGenerator(object): lines += ['.nh'] lines += ['.B %s' % self.esc_dashes(self.parser.prog)] - for option in self.options: + 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] -- cgit v1.2.1 From a37b41cbcda8bf84785d00c986d451f9c371a61d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 24 Nov 2012 18:46:01 +0000 Subject: Include grouped options in OPTIONS section --- cliapp/genman.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cliapp/genman.py b/cliapp/genman.py index 65f42df..efad53b 100644 --- a/cliapp/genman.py +++ b/cliapp/genman.py @@ -84,8 +84,17 @@ class ManpageGenerator(object): yield '%s%s' % (self.esc_dashes(name), suffix) def format_options(self): - return ''.join(self.format_option_for_options(option) - for option in self.options) + 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 = [] @@ -99,7 +108,7 @@ class ManpageGenerator(object): for x in option._long_opts] lines += ['.BR ' + ' ", " '.join(shorts + longs)] lines += [self.esc_dots(self.expand_default(option).strip())] - return ''.join('%s\n' % line for line in lines) + return lines def expand_default(self, option): default = self.parser.defaults.get(option.dest) -- cgit v1.2.1 From c34a8c2d5c52dfe9c45e60c5b0edf425dbb2ee27 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 24 Nov 2012 18:47:02 +0000 Subject: Update NEWS about bug fix --- NEWS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS b/NEWS index 0926783..7c9433e 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,12 @@ NEWS for cliapp =============== +Version UNRELEASED +------------------ + +* Options in option groups are now included in manual page SYNOPSIS and + OPTIONS sections. + Version 1.20120929 ------------------ -- cgit v1.2.1 From 6d6c4f597a8aed8fe77fbd1e68692d0827db1c5b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 3 Dec 2012 14:55:08 +0000 Subject: Fix meliae memory dumping Reported-By: Joey Hess --- NEWS | 1 + cliapp/app.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/NEWS b/NEWS index af6b697..e686b7f 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,7 @@ Version UNRELEASED 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. Version 1.20120929 ------------------ diff --git a/cliapp/app.py b/cliapp/app.py index 9e9e28b..7d406fa 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -110,6 +110,9 @@ class Application(object): self.plugin_subdir = 'plugins' + # For meliae memory dumps. + self.memory_dump_counter = 0 + def add_settings(self): '''Add application specific settings.''' -- cgit v1.2.1 From 68b49e6fd47fc369507b173cd33b90b772cea6c5 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 3 Dec 2012 22:24:02 +0000 Subject: Add memory-profiling-interval setting --- NEWS | 3 +++ cliapp/app.py | 8 ++++++++ cliapp/settings.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/NEWS b/NEWS index e686b7f..bf8b713 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,9 @@ Version UNRELEASED * 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. Version 1.20120929 ------------------ diff --git a/cliapp/app.py b/cliapp/app.py index 7d406fa..93fc9d4 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -24,6 +24,7 @@ import os import StringIO import sys import traceback +import time import platform import cliapp @@ -112,6 +113,7 @@ class Application(object): # For meliae memory dumps. self.memory_dump_counter = 0 + self.last_memory_dump = time.time() def add_settings(self): '''Add application specific settings.''' @@ -526,9 +528,15 @@ class Application(object): ''' kind = self.settings['dump-memory-profile'] + interval = self.setting['memory-dump-interval'] if kind == 'none': return + + now = time.time() + if self.last_memory_dump + interval < now: + return + self.last_memory_dump = now logging.debug('dumping memory profiling data: %s' % msg) logging.debug('VmRSS: %s KiB' % self._vmrss()) diff --git a/cliapp/settings.py b/cliapp/settings.py index 5d52098..dc9e656 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -301,6 +301,10 @@ class Settings(object): 'of: none, simple, meliae, or heapy ' '(default: %default)', metavar='METHOD') + self.integer(['memory-dump-interval'], + 'make memory profiling dumps at least SECONDS apart', + metavar='SECONDS', + default=300) def _add_setting(self, setting): '''Add a setting to self._cp.''' -- cgit v1.2.1 From 0c484142cddcef77c588b2dd13570692fe7f6ba9 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 3 Dec 2012 23:26:55 +0000 Subject: Fix attribute name --- cliapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index 93fc9d4..111573b 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -528,7 +528,7 @@ class Application(object): ''' kind = self.settings['dump-memory-profile'] - interval = self.setting['memory-dump-interval'] + interval = self.settings['memory-dump-interval'] if kind == 'none': return -- cgit v1.2.1 From 31c8143987a282bde687ccf1cceb024ef01dbfcb Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 4 Dec 2012 13:04:32 +0000 Subject: Fix test for memory dump interval --- cliapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index 111573b..fd04be6 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -534,7 +534,7 @@ class Application(object): return now = time.time() - if self.last_memory_dump + interval < now: + if self.last_memory_dump + interval > now: return self.last_memory_dump = now -- cgit v1.2.1 From adbe89cd936c0acaefa482a64d57c82ad36b3bf9 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 4 Dec 2012 13:05:21 +0000 Subject: Trigger memory dump at the beginning --- cliapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index fd04be6..f121cda 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -113,7 +113,7 @@ class Application(object): # For meliae memory dumps. self.memory_dump_counter = 0 - self.last_memory_dump = time.time() + self.last_memory_dump = 0 def add_settings(self): '''Add application specific settings.''' -- cgit v1.2.1 From 8482ab4bce6a9fd69d9a6cf9bdd0b70ea5b6fcc6 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 8 Dec 2012 22:29:41 +0000 Subject: Add a default subcommand 'help' But only if there are other subcommands. --- cliapp/app.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cliapp/app.py b/cliapp/app.py index f121cda..a77bda8 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -168,6 +168,8 @@ class Application(object): # 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() @@ -236,6 +238,17 @@ class Application(object): if name not in self.subcommands: self.subcommands[name] = func self.cmd_synopsis[name] = arg_synopsis + + def add_default_subcommands(self): + if 'help' not in self.subcommands: + self.add_subcommand('help', self.help) + + def help(self, args): + '''Print help.''' + + text = '%s\n%s\n' % (self._format_usage(), self._format_description()) + text = self.settings.progname.join(text.split('%prog')) + self.output.write(text) def _subcommand_methodnames(self): return [x -- cgit v1.2.1 From d01c1a28633602847fe19e6f354e0eda95ea25d2 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 8 Dec 2012 22:30:30 +0000 Subject: Update NEWS about default help subcommand --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index bf8b713..9841f08 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ Version UNRELEASED * 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. Version 1.20120929 ------------------ -- cgit v1.2.1 From b57b21a95ed3eb5b9a2a6bc0ad8e6e2496e3b116 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 8 Dec 2012 22:32:46 +0000 Subject: Make tests use a foo subcommand instead of help --- cliapp/app_tests.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cliapp/app_tests.py b/cliapp/app_tests.py index 6783514..c30ec19 100644 --- a/cliapp/app_tests.py +++ b/cliapp/app_tests.py @@ -274,8 +274,8 @@ class ApplicationTests(unittest.TestCase): class DummySubcommandApp(cliapp.Application): - def cmd_help(self, args): - self.help_called = True + def cmd_foo(self, args): + self.foo_called = True class SubcommandTests(unittest.TestCase): @@ -285,10 +285,10 @@ class SubcommandTests(unittest.TestCase): self.trash = StringIO.StringIO() def test_lists_subcommands(self): - self.assertEqual(self.app._subcommand_methodnames(), ['cmd_help']) + self.assertEqual(self.app._subcommand_methodnames(), ['cmd_foo']) def test_normalizes_subcommand(self): - self.assertEqual(self.app._normalize_cmd('help'), 'cmd_help') + 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): @@ -300,8 +300,8 @@ class SubcommandTests(unittest.TestCase): stderr=self.trash, log=devnull) def test_calls_subcommand_method(self): - self.app.run(['help'], stderr=self.trash, log=devnull) - self.assert_(self.app.help_called) + self.app.run(['foo'], stderr=self.trash, log=devnull) + self.assert_(self.app.foo_called) class ExtensibleSubcommandTests(unittest.TestCase): @@ -314,6 +314,6 @@ class ExtensibleSubcommandTests(unittest.TestCase): def test_adds_subcommand(self): help = lambda args: None - self.app.add_subcommand('help', help) - self.assertEqual(self.app.subcommands, {'help': help}) + self.app.add_subcommand('foo', help) + self.assertEqual(self.app.subcommands, {'foo': help}) -- cgit v1.2.1 From 978d47b87070520a43b5c8d0ed29f072fb2f3d84 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 8 Dec 2012 22:35:02 +0000 Subject: Add test for default subcommand being added --- cliapp/app_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cliapp/app_tests.py b/cliapp/app_tests.py index c30ec19..c8d5220 100644 --- a/cliapp/app_tests.py +++ b/cliapp/app_tests.py @@ -303,6 +303,10 @@ class SubcommandTests(unittest.TestCase): self.app.run(['foo'], stderr=self.trash, log=devnull) self.assert_(self.app.foo_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): -- cgit v1.2.1 From 886c40bc9ea85f2e8119b8073131e22692cc1d3c Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 8 Dec 2012 22:35:23 +0000 Subject: Exclude the help subcommand code from test coverage There's no useful way to test it. --- cliapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index a77bda8..88eed13 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -243,7 +243,7 @@ class Application(object): if 'help' not in self.subcommands: self.add_subcommand('help', self.help) - def help(self, args): + def help(self, args): # pragma: no cover '''Print help.''' text = '%s\n%s\n' % (self._format_usage(), self._format_description()) -- cgit v1.2.1 From fa7970ce20a024a82d689fe9f3428845fe8752df Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 11 Dec 2012 22:25:50 +0000 Subject: Add --no-foo for every boolean --foo --- cliapp/settings.py | 20 ++++++++++++++++++++ cliapp/settings_tests.py | 12 +++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index dc9e656..bc15b3a 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -494,6 +494,9 @@ class Settings(object): 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, @@ -505,11 +508,26 @@ class Settings(object): choices=s.choices, help=s.help, 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='') for name in self._canonical_names: s = self._settingses[name] if s.group is None: add_option(p, s) + if type(s) is BooleanSetting: + add_negation_option(p, s) p.set_defaults(**{self._destname(name): s.value}) groups = {} @@ -524,6 +542,8 @@ class Settings(object): p.add_option_group(group) for name, s in groups[groupname]: add_option(group, s) + if type(s) is BooleanSetting: + add_negation_option(group, s) p.set_defaults(**{self._destname(name): s.value}) return p diff --git a/cliapp/settings_tests.py b/cliapp/settings_tests.py index b831ad3..757811a 100644 --- a/cliapp/settings_tests.py +++ b/cliapp/settings_tests.py @@ -65,6 +65,16 @@ class SettingsTests(unittest.TestCase): 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) @@ -132,7 +142,7 @@ class SettingsTests(unittest.TestCase): 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']) -- cgit v1.2.1 From 9c9ad5f386333a1102fbb63f5869306fa8413938 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 11 Dec 2012 22:26:28 +0000 Subject: Update NEWS about new feature --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 9841f08..29986a5 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,8 @@ Version UNRELEASED 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 ------------------ -- cgit v1.2.1 From eb0844aec59e5bd675085fe5af7c0d121e354d34 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 11 Dec 2012 22:29:09 +0000 Subject: Add --no-foo to manpage --- cliapp.5 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cliapp.5 b/cliapp.5 index cf2051a..7950033 100644 --- a/cliapp.5 +++ b/cliapp.5 @@ -130,6 +130,20 @@ attack-kittens = no .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 -- cgit v1.2.1 From ee1dba1418dfdfca8c6916c3de4f87c40de924a8 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 16 Dec 2012 16:42:58 +0000 Subject: Prepare release version 1.20121216 --- NEWS | 2 +- README | 3 --- cliapp/__init__.py | 2 +- debian/changelog | 6 ++++++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 29986a5..3f93eb9 100644 --- a/NEWS +++ b/NEWS @@ -1,7 +1,7 @@ NEWS for cliapp =============== -Version UNRELEASED +Version 1.20121216 ------------------ * Options in option groups are now included in manual page SYNOPSIS and diff --git a/README b/README index fc2c71c..04c3e7a 100644 --- a/README +++ b/README @@ -22,9 +22,6 @@ 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. -The `cliapp.__version__` string is set to `1.DEVEL` in version control, -to differentiate development versions and release versions. - Legalese -------- diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 4c92ef9..0be7852 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -15,7 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -__version__ = '1.20120929' +__version__ = '1.20121216' from settings import Settings diff --git a/debian/changelog b/debian/changelog index ef606d2..0a2a142 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cliapp (1.20121216-1) unstable; urgency=low + + * New upstream release. + + -- Lars Wirzenius Sun, 16 Dec 2012 16:41:36 +0000 + python-cliapp (1.20120929-1) unstable; urgency=low * New upstream release. -- cgit v1.2.1 From 243e6d0d0b199cd1cf670b9a8ae8afdbae89f2c6 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 19 Dec 2012 10:59:22 +0000 Subject: Re-implement automatically added options Add a way to defer such options to be called after OptionParser is finished parsing the entire command line. The user-visible change this has is that --dump-config includes changes made by options later on the command line. --- cliapp/settings.py | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index bc15b3a..fba0d59 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -402,11 +402,25 @@ class Settings(object): return name def build_parser(self, configs_only=False, arg_synopsis=None, - cmd_synopsis=None): + cmd_synopsis=None, deferred_last=[]): '''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 @@ -419,6 +433,8 @@ class Settings(object): usage=usage, description=description, epilog=self.epilog) + + # Add --dump-setting-names. def dump_setting_names(*args): # pragma: no cover for name in self._canonical_names: @@ -428,9 +444,11 @@ class Settings(object): p.add_option('--dump-setting-names', action='callback', nargs=0, - callback=maybe(dump_setting_names), + callback=defer_last(maybe(dump_setting_names)), help='write out all names of settings and quit') + # Add --dump-config. + def call_dump_config(*args): # pragma: no cover self.dump_config(sys.stdout) sys.exit(0) @@ -438,9 +456,11 @@ class Settings(object): p.add_option('--dump-config', action='callback', nargs=0, - callback=maybe(call_dump_config), + 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 = [] @@ -450,6 +470,8 @@ class Settings(object): 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) @@ -461,6 +483,8 @@ class Settings(object): 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 @@ -469,9 +493,11 @@ class Settings(object): p.add_option('--list-config-files', action='callback', nargs=0, - callback=maybe(list_config_files), + callback=defer_last(maybe(list_config_files)), help='list all possible config files') + # Add --generate-manpage. + self._arg_synopsis = arg_synopsis self._cmd_synopsis = cmd_synopsis p.add_option('--generate-manpage', @@ -482,6 +508,9 @@ class Settings(object): help='fill in manual page TEMPLATE', metavar='TEMPLATE') + # 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: @@ -558,14 +587,19 @@ class Settings(object): ''' + deferred_last = [] + p = parser or self.build_parser(configs_only=configs_only, arg_synopsis=arg_synopsis, - cmd_synopsis=cmd_synopsis) + cmd_synopsis=cmd_synopsis, + deferred_last=deferred_last) if suppress_errors: p.error = lambda msg: sys.exit(1) options, args = p.parse_args(args) + for callback in deferred_last: # pragma: no cover + callback() return args @property -- cgit v1.2.1 From c301c24bd07029d7e2e309d56e36ef34e6097b01 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 19 Dec 2012 11:11:06 +0000 Subject: Add a way to compute setting values --- cliapp/app.py | 21 ++++++++++++++++++--- cliapp/settings.py | 4 +++- example3.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 example3.py diff --git a/cliapp/app.py b/cliapp/app.py index 88eed13..f21726a 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -222,6 +222,20 @@ class Application(object): 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): '''Add a subcommand. @@ -408,9 +422,10 @@ class Application(object): ''' - return self.settings.parse_args(args, configs_only=configs_only, - arg_synopsis=self.arg_synopsis, - cmd_synopsis=self.cmd_synopsis) + 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. diff --git a/cliapp/settings.py b/cliapp/settings.py index fba0d59..27abe58 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -579,7 +579,7 @@ class Settings(object): def parse_args(self, args, parser=None, suppress_errors=False, configs_only=False, arg_synopsis=None, - cmd_synopsis=None): + cmd_synopsis=None, compute_setting_values=None): '''Parse the command line. Return list of non-option arguments. ``args`` would usually @@ -598,6 +598,8 @@ class Settings(object): 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 diff --git a/example3.py b/example3.py new file mode 100644 index 0000000..5de5a11 --- /dev/null +++ b/example3.py @@ -0,0 +1,48 @@ +# 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() + -- cgit v1.2.1 From de34dddb0d246d972f3417ec9107eada69161fd8 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 19 Dec 2012 11:11:21 +0000 Subject: Exclude new example from unit test coverage --- without-tests | 1 + 1 file changed, 1 insertion(+) diff --git a/without-tests b/without-tests index 7c5b978..b362003 100644 --- a/without-tests +++ b/without-tests @@ -8,3 +8,4 @@ ./test-plugins/hello_plugin.py ./test-plugins/wrongversion_plugin.py ./test-plugins/aaa_hello_plugin.py +example3.py -- cgit v1.2.1 From a084ea25b440bf7fe9e4c833146ec540bec0c143 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 19 Dec 2012 11:13:09 +0000 Subject: Update NEWS --- NEWS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS b/NEWS index 3f93eb9..dd45d21 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,13 @@ NEWS for cliapp =============== +Version UNRELEASED +------------------ + +* 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. + Version 1.20121216 ------------------ -- cgit v1.2.1 From b74026e77a96ba0bbc09624f272c28b82c4a4f92 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 25 Dec 2012 18:06:13 +0000 Subject: Set process title only if /proc/self/comm exists It does not exist on, e.g., the Debian squeeze kernel. --- cliapp/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index f21726a..502759f 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -148,12 +148,10 @@ class Application(object): return ''.join(x.upper() if x in ok else '_' for x in basename) def _set_process_name(self): # pragma: no cover - if platform.system() == 'Linux': - try: - with open('/proc/self/comm', 'w', 0) as f: - f.write(self.settings.progname[:15]) - except IOError, e: - logging.warning(str(e)) + 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: -- cgit v1.2.1 From b23f2a6ba0fbaf8efcd1014832a725ceb984b5f1 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 25 Dec 2012 18:08:39 +0000 Subject: Update NEWS --- NEWS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS b/NEWS index dd45d21..6c1d3e4 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,13 @@ Version UNRELEASED the application to have settings with values that are computed after configuration files and the command line are parsed. +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. + Version 1.20121216 ------------------ -- cgit v1.2.1 From 6d17e1e88253a1b7e8bc130f55cbdf8b13c77751 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 10 Jan 2013 21:38:28 +0000 Subject: Log Python version at startup --- NEWS | 1 + cliapp/app.py | 1 + 2 files changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 6c1d3e4..0bbd301 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,7 @@ Version UNRELEASED * 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. Bug fixes: diff --git a/cliapp/app.py b/cliapp/app.py index 502759f..ed3cbb3 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -383,6 +383,7 @@ class Application(object): 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. -- cgit v1.2.1 From 58cd4687cbf2059346303b706bd9873a4b3e6926 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 10 Jan 2013 21:41:26 +0000 Subject: Reduce runcmd debug logging --- NEWS | 3 +++ cliapp/runcmd.py | 8 -------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/NEWS b/NEWS index 0bbd301..abd8e9a 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,9 @@ Version UNRELEASED 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. Bug fixes: diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index f45d42b..083dbca 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -124,12 +124,6 @@ def _build_pipeline(argvs, pipe_stdin, pipe_stdout, pipe_stderr, kwargs): def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): - logging.debug('PIPE=%d' % subprocess.PIPE) - logging.debug('STDOUT=%d' % subprocess.STDOUT) - logging.debug('pipe_stdin=%s' % repr(pipe_stdin)) - logging.debug('pipe_stdout=%s' % repr(pipe_stdout)) - logging.debug('pipe_stderr=%s' % repr(pipe_stderr)) - stdout_eof = False stderr_eof = False out = [] @@ -138,7 +132,6 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): io_size = 1024 def set_nonblocking(fd): - logging.debug('set nonblocking fd=%d' % fd) flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) flags = flags | os.O_NONBLOCK fcntl.fcntl(fd, fcntl.F_SETFL, flags) @@ -202,7 +195,6 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): while still_running(): for p in procs: if p.returncode is None: - logging.debug('Waiting for child pid=%d to terminate' % p.pid) p.wait() return procs[-1].returncode, ''.join(out), ''.join(err) -- cgit v1.2.1 From d8285cfe4fe72b910046fc025765c9369b5fc245 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:09:58 +0000 Subject: Add a "Logging" group for logging options --- cliapp/__init__.py | 2 +- cliapp/settings.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 0be7852..fef3280 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -18,7 +18,7 @@ __version__ = '1.20121216' -from settings import Settings +from settings import Settings, log_group from runcmd import runcmd, runcmd_unchecked from app import Application, AppException diff --git a/cliapp/settings.py b/cliapp/settings.py index 27abe58..eda188b 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -24,6 +24,10 @@ import sys import cliapp from cliapp.genman import ManpageGenerator + +log_group = 'Logging' + + class Setting(object): action = 'store' @@ -278,22 +282,22 @@ class Settings(object): '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') + metavar='FILE', group=log_group) 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') + metavar='LEVEL', group=log_group) self.bytesize(['log-max'], 'rotate logs larger than SIZE, ' 'zero for never (default: %default)', - metavar='SIZE', default=0) + metavar='SIZE', default=0, group=log_group) self.integer(['log-keep'], 'keep last N logs (%default)', - metavar='N', default=10) + metavar='N', default=10, group=log_group) self.string(['log-mode'], 'set permissions of new log files to MODE (octal; ' 'default %default)', - metavar='MODE', default='0600') + metavar='MODE', default='0600', group=log_group) self.choice(['dump-memory-profile'], ['simple', 'none', 'meliae', 'heapy'], -- cgit v1.2.1 From 7c3f3b2dbb4d838bade1df787a7e5bcb754cfd83 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:14:11 +0000 Subject: Add option group for config files and settings options --- cliapp/__init__.py | 2 +- cliapp/settings.py | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cliapp/__init__.py b/cliapp/__init__.py index fef3280..4ad3d07 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -18,7 +18,7 @@ __version__ = '1.20121216' -from settings import Settings, log_group +from settings import Settings, log_group_name, config_group_name from runcmd import runcmd, runcmd_unchecked from app import Application, AppException diff --git a/cliapp/settings.py b/cliapp/settings.py index eda188b..be277f6 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -25,7 +25,8 @@ import cliapp from cliapp.genman import ManpageGenerator -log_group = 'Logging' +log_group_name = 'Logging' +config_group_name = 'Configuration files and settings' class Setting(object): @@ -282,22 +283,22 @@ class Settings(object): '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) + 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) + 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) + metavar='SIZE', default=0, group=log_group_name) self.integer(['log-keep'], 'keep last N logs (%default)', - metavar='N', default=10, group=log_group) + 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) + metavar='MODE', default='0600', group=log_group_name) self.choice(['dump-memory-profile'], ['simple', 'none', 'meliae', 'heapy'], @@ -438,6 +439,12 @@ class Settings(object): description=description, epilog=self.epilog) + # Create an OptionGroup for the config file options. These can't + # be normal settings, since they only ever apply on the command line. + + config_group = optparse.OptionGroup(p, config_group_name) + p.add_option_group(config_group) + # Add --dump-setting-names. def dump_setting_names(*args): # pragma: no cover @@ -445,7 +452,7 @@ class Settings(object): sys.stdout.write('%s\n' % name) sys.exit(0) - p.add_option('--dump-setting-names', + config_group.add_option('--dump-setting-names', action='callback', nargs=0, callback=defer_last(maybe(dump_setting_names)), @@ -457,7 +464,7 @@ class Settings(object): self.dump_config(sys.stdout) sys.exit(0) - p.add_option('--dump-config', + config_group.add_option('--dump-config', action='callback', nargs=0, callback=defer_last(maybe(call_dump_config)), @@ -468,7 +475,7 @@ class Settings(object): def reset_configs(option, opt_str, value, parser): self.config_files = [] - p.add_option('--no-default-configs', + config_group.add_option('--no-default-configs', action='callback', nargs=0, callback=reset_configs, @@ -479,7 +486,7 @@ class Settings(object): def append_to_configs(option, opt_str, value, parser): self.config_files.append(value) - p.add_option('--config', + config_group.add_option('--config', action='callback', nargs=1, type='string', @@ -494,7 +501,7 @@ class Settings(object): print filename sys.exit(0) - p.add_option('--list-config-files', + config_group.add_option('--list-config-files', action='callback', nargs=0, callback=defer_last(maybe(list_config_files)), -- cgit v1.2.1 From ef5c5d523598fecf69e7f484764a8e95fcf03cbc Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:27:06 +0000 Subject: Add config_group_name --- cliapp/settings.py | 59 ++++++++++++++++++++++++++++++------------------------ example.py | 2 ++ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index be277f6..3f267bc 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -28,6 +28,11 @@ from cliapp.genman import ManpageGenerator log_group_name = 'Logging' config_group_name = 'Configuration files and settings' +default_group_names = [ + log_group_name, + config_group_name, +] + class Setting(object): @@ -439,12 +444,24 @@ class Settings(object): description=description, epilog=self.epilog) - # Create an OptionGroup for the config file options. These can't - # be normal settings, since they only ever apply on the command line. - - config_group = optparse.OptionGroup(p, config_group_name) - p.add_option_group(config_group) - + # 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] + # Add --dump-setting-names. def dump_setting_names(*args): # pragma: no cover @@ -561,30 +578,20 @@ class Settings(object): callback_args=(s,), type=s.type, help='') + + # Add options for every setting. for name in self._canonical_names: s = self._settingses[name] if s.group is None: - add_option(p, s) - if type(s) is BooleanSetting: - add_negation_option(p, s) - p.set_defaults(**{self._destname(name): s.value}) - - groups = {} - for name in self._canonical_names: - s = self._settingses[name] - if s.group is not None: - groups[s.group] = groups.get(s.group, []) + [(name, s)] - - groupnames = sorted(groups.keys()) - for groupname in groupnames: - group = optparse.OptionGroup(p, groupname) - p.add_option_group(group) - for name, s in groups[groupname]: - add_option(group, s) - if type(s) is BooleanSetting: - add_negation_option(group, s) - p.set_defaults(**{self._destname(name): s.value}) + 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 diff --git a/example.py b/example.py index a6fd2c2..7e2a4a7 100644 --- a/example.py +++ b/example.py @@ -38,6 +38,8 @@ class ExampleApp(cliapp.Application): self.settings.boolean(['dummy'], 'this setting is ignored', group='Test Group') + self.settings.string(['yoyo'], 'yoyo', group=cliapp.config_group_name) + # We override process_inputs to be able to do something after the last # input line. def process_inputs(self, args): -- cgit v1.2.1 From febb1ec3d332c41d6dafacea269868f62af56600 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:29:23 +0000 Subject: Add performance group for settings --- cliapp/__init__.py | 3 ++- cliapp/settings.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 4ad3d07..1e77632 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -18,7 +18,8 @@ __version__ = '1.20121216' -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 from app import Application, AppException diff --git a/cliapp/settings.py b/cliapp/settings.py index 3f267bc..8c1767c 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -27,10 +27,12 @@ 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, ] @@ -310,11 +312,13 @@ class Settings(object): 'make memory profiling dumps using METHOD, which is one ' 'of: none, simple, meliae, or heapy ' '(default: %default)', - metavar='METHOD') + metavar='METHOD', + group=perf_group_name) self.integer(['memory-dump-interval'], 'make memory profiling dumps at least SECONDS apart', metavar='SECONDS', - default=300) + default=300, + group=perf_group_name) def _add_setting(self, setting): '''Add a setting to self._cp.''' -- cgit v1.2.1 From 28250529aa1a6b06e2d5543c97a404de1efd2394 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:30:58 +0000 Subject: Update NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index abd8e9a..7255460 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,8 @@ Version UNRELEASED * `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. Bug fixes: -- cgit v1.2.1 From 82c65622d8748ac4d8672e97dcf3131b183f575e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:41:16 +0000 Subject: Make logging setup more overrideable by app --- cliapp/app.py | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index ed3cbb3..dce8ada 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -347,11 +347,7 @@ class Application(object): level = levels.get(level_name, logging.INFO) if self.settings['log'] == '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) + handler = self.setup_logging_handler_for_syslog() elif self.settings['log'] and self.settings['log'] != 'none': handler = LogHandler( self.settings['log'], @@ -364,7 +360,7 @@ class Application(object): formatter = logging.Formatter(fmt, datefmt) handler.setFormatter(formatter) else: - handler = logging.FileHandler('/dev/null') + handler = self.setup_logging_handler_to_none() # reduce amount of pointless I/O level = logging.FATAL @@ -372,6 +368,47 @@ class Application(object): 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)) -- cgit v1.2.1 From 027c3cbedaa12765e0fb93442efce5047956f62d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:43:51 +0000 Subject: Update NEWS about new API methods --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 7255460..ac47aa4 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,14 @@ Version UNRELEASED now not been useful for months. * More default settings and options have an option group now, making `--help` output prettier. +* 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. + Bug fixes: -- cgit v1.2.1 From bce0cfb6dede01608bc90b8e28134413688c2e9e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:54:42 +0000 Subject: Log process times when logging memory info --- cliapp/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cliapp/app.py b/cliapp/app.py index dce8ada..32c8ab5 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -115,6 +115,9 @@ class Application(object): 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.''' @@ -602,6 +605,15 @@ class Application(object): 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()) -- cgit v1.2.1 From ab97ae387747ac3f7890ab286b25590776af7fe5 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 11:55:33 +0000 Subject: Update NEWS --- NEWS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index ac47aa4..038b1b9 100644 --- a/NEWS +++ b/NEWS @@ -20,7 +20,9 @@ Version UNRELEASED `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. Bug fixes: -- cgit v1.2.1 From 1b9b4ae188d61e4c02032baef28ffdbfd68c17c7 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 18:48:11 +0000 Subject: Disallow a default value of None None is not a valid value for any setting type, and it can't be dumped or expressed in config files at all. --- example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example.py b/example.py index 7e2a4a7..c86aaea 100644 --- a/example.py +++ b/example.py @@ -39,6 +39,8 @@ 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 # input line. -- cgit v1.2.1 From eac269b3b885b511a99c472168d0879c52c10248 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 18:48:33 +0000 Subject: Update NEWS --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index 038b1b9..fa8aaff 100644 --- a/NEWS +++ b/NEWS @@ -30,6 +30,7 @@ Bug fixes: 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 ------------------ -- cgit v1.2.1 From 87eb9dba090cd16748d0ef01b0980dc89762fea8 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:08:54 +0000 Subject: Start a text formatting class --- cliapp/__init__.py | 1 + cliapp/app.py | 17 ++++++++++++++--- cliapp/fmt.py | 36 ++++++++++++++++++++++++++++++++++++ cliapp/fmt_tests.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 cliapp/fmt.py create mode 100644 cliapp/fmt_tests.py diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 1e77632..fbf1af3 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -18,6 +18,7 @@ __version__ = '1.20121216' +from fmt import TextFormat from settings import (Settings, log_group_name, config_group_name, perf_group_name) from runcmd import runcmd, runcmd_unchecked diff --git a/cliapp/app.py b/cliapp/app.py index 32c8ab5..bc43f21 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -294,14 +294,25 @@ class Application(object): def _format_description(self): '''Format OptionParser description, with subcommand support.''' if self.subcommands: - paras = [] + summaries = [] for cmd in sorted(self.subcommands.keys()): - paras.append(self._format_subcommand_description(cmd)) - cmd_desc = '\n\n'.join(paras) + summaries.append( + ' %s\n' % self._format_subcommand_summary(cmd)) + cmd_desc = ''.join(summaries) return '%s\n\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 '* %s: %s' % (cmd, summary) + def _format_subcommand_description(self, cmd): # pragma: no cover def remove_empties(lines): diff --git a/cliapp/fmt.py b/cliapp/fmt.py new file mode 100644 index 0000000..2e6ef4f --- /dev/null +++ b/cliapp/fmt.py @@ -0,0 +1,36 @@ +# 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. + +''' + + +class TextFormat(object): + + def __init__(self, width=78): + self._width = width + + def format(self, text): + '''Return input string, but formatted nicely.''' + return text + diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py new file mode 100644 index 0000000..cd22058 --- /dev/null +++ b/cliapp/fmt_tests.py @@ -0,0 +1,30 @@ +# 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(''), '') + -- cgit v1.2.1 From e7f2131031b0753fb0ad020976c3b930b3152bfe Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:09:34 +0000 Subject: Add test for short one-line input --- cliapp/fmt_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index cd22058..7ed722e 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -28,3 +28,6 @@ class TextFormatTests(unittest.TestCase): 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') + -- cgit v1.2.1 From af4ef7b06015562612402c309bdc9da6702641ce Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:11:35 +0000 Subject: Implement basic re-filling using textwrap --- cliapp/fmt.py | 6 +++++- cliapp/fmt_tests.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 2e6ef4f..27f7522 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -25,6 +25,9 @@ extends textwrap by recognising bulleted lists. ''' +import textwrap + + class TextFormat(object): def __init__(self, width=78): @@ -32,5 +35,6 @@ class TextFormat(object): def format(self, text): '''Return input string, but formatted nicely.''' - return text + + return textwrap.fill(text, width=self._width) diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 7ed722e..014cd55 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -31,3 +31,6 @@ class TextFormatTests(unittest.TestCase): def test_returns_short_one_line_paragraph_as_is(self): self.assertEqual(self.fmt.format('foo bar'), 'foo bar') + def test_wraps_long_line(self): + self.assertEqual(self.fmt.format('foobar word'), 'foobar\nword') + -- cgit v1.2.1 From 8af8d5a075b6a5c9c098fabd9b25c023e9ee9b44 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:13:07 +0000 Subject: Make sure output ends with newline, if input did --- cliapp/fmt.py | 6 +++++- cliapp/fmt_tests.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 27f7522..759df5a 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -36,5 +36,9 @@ class TextFormat(object): def format(self, text): '''Return input string, but formatted nicely.''' - return textwrap.fill(text, width=self._width) + ends_with_newline = text.endswith('\n') + filled = textwrap.fill(text, width=self._width) + if ends_with_newline and not filled.endswith('\n'): + filled += '\n' + return filled diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 014cd55..d0de30e 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -34,3 +34,6 @@ class TextFormatTests(unittest.TestCase): def test_wraps_long_line(self): self.assertEqual(self.fmt.format('foobar word'), 'foobar\nword') + def test_retains_final_newline_on_short_input(self): + self.assertEqual(self.fmt.format('foo bar\n'), 'foo bar\n') + -- cgit v1.2.1 From fcc320b3a667a889d16ffcc4bc3043424b20466f Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:13:38 +0000 Subject: Add test for long input and trailing newline --- cliapp/fmt_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index d0de30e..4e2474c 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -37,3 +37,6 @@ class TextFormatTests(unittest.TestCase): def test_retains_final_newline_on_short_input(self): self.assertEqual(self.fmt.format('foo bar\n'), 'foo bar\n') + def test_retains_final_newline_on_long_input(self): + self.assertEqual(self.fmt.format('foobar word\n'), 'foobar\nword\n') + -- cgit v1.2.1 From 7d66ecf013b9a7992e49db412d8c3ad50dc1fe67 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:20:39 +0000 Subject: Format each paragraph separately --- cliapp/fmt.py | 14 +++++++++++--- cliapp/fmt_tests.py | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 759df5a..aafc2e9 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -36,9 +36,17 @@ class TextFormat(object): def format(self, text): '''Return input string, but formatted nicely.''' - ends_with_newline = text.endswith('\n') - filled = textwrap.fill(text, width=self._width) - if ends_with_newline and not filled.endswith('\n'): + filled_paras = [] + for para in self._paragraphs(text): + filled_paras.append(self._format_paragraph(para)) + return '\n\n'.join(filled_paras) + + def _paragraphs(self, text): + return text.split('\n\n') + + def _format_paragraph(self, paragraph): + filled = textwrap.fill(paragraph, width=self._width) + if paragraph.endswith('\n') and not filled.endswith('\n'): filled += '\n' return filled diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 4e2474c..198bc47 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -40,3 +40,8 @@ class TextFormatTests(unittest.TestCase): def test_retains_final_newline_on_long_input(self): self.assertEqual(self.fmt.format('foobar word\n'), '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') + -- cgit v1.2.1 From 2ea6427d780dcec4dc8873d21fde38d13df8e2b8 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:28:15 +0000 Subject: Always add trailing newline if input is not empty Also, handle multiple empty lines as if there was one. --- cliapp/fmt.py | 15 +++++++++++++-- cliapp/fmt_tests.py | 15 +++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index aafc2e9..9071297 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -39,10 +39,21 @@ class TextFormat(object): filled_paras = [] for para in self._paragraphs(text): filled_paras.append(self._format_paragraph(para)) - return '\n\n'.join(filled_paras) + return '\n'.join(filled_paras) def _paragraphs(self, text): - return text.split('\n\n') + paras = [] + current = [] + for line in text.splitlines(): + if not line.strip(): + if current: + paras.append(''.join(current)) + current = [] + else: + current.append(line + '\n') + if current: + paras.append(''.join(current)) + return paras def _format_paragraph(self, paragraph): filled = textwrap.fill(paragraph, width=self._width) diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 198bc47..5d67f34 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -29,19 +29,18 @@ class TextFormatTests(unittest.TestCase): self.assertEqual(self.fmt.format(''), '') def test_returns_short_one_line_paragraph_as_is(self): - self.assertEqual(self.fmt.format('foo bar'), 'foo bar') + 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') - - def test_retains_final_newline_on_short_input(self): - self.assertEqual(self.fmt.format('foo bar\n'), 'foo bar\n') - - def test_retains_final_newline_on_long_input(self): - self.assertEqual(self.fmt.format('foobar word\n'), 'foobar\nword\n') + 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') + -- cgit v1.2.1 From 4865243322d1c07b77a60115fc6b372fb1436e12 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:47:57 +0000 Subject: Support bulleted lists --- cliapp/fmt.py | 76 ++++++++++++++++++++++++++++++++++++++++++----------- cliapp/fmt_tests.py | 5 ++++ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 9071297..32f5df7 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -28,6 +28,33 @@ 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 fill(self, width): + text = ''.join(self._lines) + filled = textwrap.fill(text, width=width) + if not filled.endswith('\n'): + filled += '\n' + return filled + + +class BulletPoint(Paragraph): + + def fill(self, width): + text = ' '.join(x.strip() for x in self._lines) + 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 TextFormat(object): def __init__(self, width=78): @@ -38,26 +65,43 @@ class TextFormat(object): filled_paras = [] for para in self._paragraphs(text): - filled_paras.append(self._format_paragraph(para)) + filled_paras.append(para.fill(self._width)) return '\n'.join(filled_paras) def _paragraphs(self, text): - paras = [] - current = [] - for line in text.splitlines(): - if not line.strip(): + + 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: - paras.append(''.join(current)) - current = [] + yield current + current = BulletPoint() + current.append(line) + in_list = True + elif is_empty(line): + if current: + yield current + current = None + in_list = False else: - current.append(line + '\n') - if current: - paras.append(''.join(current)) - return paras + if not current: + current = Paragraph() + current.append(line) + in_list = False - def _format_paragraph(self, paragraph): - filled = textwrap.fill(paragraph, width=self._width) - if paragraph.endswith('\n') and not filled.endswith('\n'): - filled += '\n' - return filled + if current: + yield current diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 5d67f34..cc0e379 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -44,3 +44,8 @@ class TextFormatTests(unittest.TestCase): 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_handle_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\nword\n') + -- cgit v1.2.1 From 62edb00f1313241c300ace91de9dbf4bd6480017 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 19:50:16 +0000 Subject: Collapse sequences of spaces into one space --- cliapp/fmt.py | 8 +++++--- cliapp/fmt_tests.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 32f5df7..30af75f 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -36,9 +36,11 @@ 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): - text = ''.join(self._lines) - filled = textwrap.fill(text, width=width) + filled = textwrap.fill(self._oneliner(), width=width) if not filled.endswith('\n'): filled += '\n' return filled @@ -47,7 +49,7 @@ class Paragraph(object): class BulletPoint(Paragraph): def fill(self, width): - text = ' '.join(x.strip() for x in self._lines) + text = self._oneliner() assert text.startswith('* ') filled = textwrap.fill(text[2:], width=width - 2) lines = [' %s' % x for x in filled.splitlines(True)] diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index cc0e379..50e4372 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -31,6 +31,9 @@ class TextFormatTests(unittest.TestCase): 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') -- cgit v1.2.1 From 3cb6058de605170e6165dfbb5bc9dd2f24ee6cfe Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:03:49 +0000 Subject: Ensure empty lines before and after bullet lists --- cliapp/fmt.py | 22 +++++++++++++++++++--- cliapp/fmt_tests.py | 9 +++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/cliapp/fmt.py b/cliapp/fmt.py index 30af75f..72a9265 100644 --- a/cliapp/fmt.py +++ b/cliapp/fmt.py @@ -41,8 +41,6 @@ class Paragraph(object): def fill(self, width): filled = textwrap.fill(self._oneliner(), width=width) - if not filled.endswith('\n'): - filled += '\n' return filled @@ -57,6 +55,12 @@ class BulletPoint(Paragraph): return ''.join(lines) +class EmptyLine(Paragraph): + + def fill(self, width): + return '' + + class TextFormat(object): def __init__(self, width=78): @@ -68,7 +72,10 @@ class TextFormat(object): filled_paras = [] for para in self._paragraphs(text): filled_paras.append(para.fill(self._width)) - return '\n'.join(filled_paras) + filled = '\n'.join(filled_paras) + if text and not filled.endswith('\n'): + filled += '\n' + return filled def _paragraphs(self, text): @@ -90,15 +97,23 @@ class TextFormat(object): 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) @@ -107,3 +122,4 @@ class TextFormat(object): if current: yield current + diff --git a/cliapp/fmt_tests.py b/cliapp/fmt_tests.py index 50e4372..0e109e7 100644 --- a/cliapp/fmt_tests.py +++ b/cliapp/fmt_tests.py @@ -47,8 +47,13 @@ class TextFormatTests(unittest.TestCase): 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_handle_bulleted_lists(self): + 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\nword\n') + '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') -- cgit v1.2.1 From b9744094f060bcea9cb5d42df1c7a9cec2c08364 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:17:35 +0000 Subject: Use new formatter for --help output to list subcommand summaries --- cliapp/app.py | 7 +++---- cliapp/settings.py | 20 ++++---------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index bc43f21..832e8ad 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -296,10 +296,9 @@ class Application(object): if self.subcommands: summaries = [] for cmd in sorted(self.subcommands.keys()): - summaries.append( - ' %s\n' % self._format_subcommand_summary(cmd)) + summaries.append(self._format_subcommand_summary(cmd)) cmd_desc = ''.join(summaries) - return '%s\n\n%s' % (self._description or '', cmd_desc) + return '%s\n%s' % (self._description or '', cmd_desc) else: return self._description @@ -311,7 +310,7 @@ class Application(object): summary = lines[0].strip() else: summary = '' - return '* %s: %s' % (cmd, summary) + return '* %%prog %s: %s\n' % (cmd, summary) def _format_subcommand_description(self, cmd): # pragma: no cover diff --git a/cliapp/settings.py b/cliapp/settings.py index 8c1767c..2dbab45 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -208,22 +208,10 @@ class FormatHelpParagraphs(optparse.IndentedHelpFormatter): def _format_text(self, text): # pragma: no cover '''Like the default, except handle paragraphs.''' - - def format_para(lines): - para = '\n'.join(lines) - return optparse.IndentedHelpFormatter._format_text(self, para) - - paras = [] - cur = [] - for line in text.splitlines(): - if line.strip(): - cur.append(line) - elif cur: - paras.append(format_para(cur)) - cur = [] - if cur: - paras.append(format_para(cur)) - return '\n\n'.join(paras) + + fmt = cliapp.TextFormat(width=self.width) + formatted = fmt.format(text) + return formatted.rstrip('\n') class Settings(object): -- cgit v1.2.1 From 1c39096b777c4d704dbf96437e35a008cd50ab4c Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:20:29 +0000 Subject: Format output of the help subcommand --- cliapp/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index 832e8ad..b0106ee 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -260,8 +260,11 @@ class Application(object): def help(self, args): # pragma: no cover '''Print help.''' - - text = '%s\n%s\n' % (self._format_usage(), self._format_description()) + + fmt = cliapp.TextFormat(width=78) + usage = self._format_usage() + description = fmt.format(self._format_description()) + text = '%s\n\n%s' % (usage, description) text = self.settings.progname.join(text.split('%prog')) self.output.write(text) -- cgit v1.2.1 From e5cf4ec31dbd7ddd8b4a97b7da2595f860015726 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:22:29 +0000 Subject: Obey COLUMNS for help output --- cliapp/app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index b0106ee..e01d48a 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -261,7 +261,12 @@ class Application(object): def help(self, args): # pragma: no cover '''Print help.''' - fmt = cliapp.TextFormat(width=78) + try: + width = int(os.environ.get('COLUMNS', '78')) + except ValueError: + width = 78 + + fmt = cliapp.TextFormat(width=width) usage = self._format_usage() description = fmt.format(self._format_description()) text = '%s\n\n%s' % (usage, description) -- cgit v1.2.1 From e1ad86a167c4179e9ea521c56b460293f3f19796 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:31:51 +0000 Subject: Make help subcommand show full documentation for named commands --- cliapp/app.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index e01d48a..ab1c24b 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -26,6 +26,7 @@ import sys import traceback import time import platform +import textwrap import cliapp @@ -267,9 +268,16 @@ class Application(object): width = 78 fmt = cliapp.TextFormat(width=width) - usage = self._format_usage() - description = fmt.format(self._format_description()) - text = '%s\n\n%s' % (usage, description) + + 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() + description = fmt.format(self._format_description()) + text = '%s\n\n%s' % (usage, description) + text = self.settings.progname.join(text.split('%prog')) self.output.write(text) @@ -299,6 +307,10 @@ class Application(object): else: return None + def _format_usage_for(self, cmd): + args = self.cmd_synopsis.get(cmd, '') or '' + return 'Usage: %%prog [options] %s %s' % (cmd, args) + def _format_description(self): '''Format OptionParser description, with subcommand support.''' if self.subcommands: @@ -320,6 +332,12 @@ class Application(object): summary = '' return '* %%prog %s: %s\n' % (cmd, summary) + def _format_subcommand_help(self, cmd): # pragma: no cover + method = self.subcommands[cmd] + doc = method.__doc__ or '' + first, rest = doc.split('\n', 1) + return first + '\n' + textwrap.dedent(rest) + def _format_subcommand_description(self, cmd): # pragma: no cover def remove_empties(lines): -- cgit v1.2.1 From a16ce809cf26468322b61f252b5610d13a89f14c Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:32:23 +0000 Subject: Remove dead code --- cliapp/app.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index ab1c24b..591b984 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -338,39 +338,6 @@ class Application(object): first, rest = doc.split('\n', 1) return first + '\n' + textwrap.dedent(rest) - def _format_subcommand_description(self, cmd): # pragma: no cover - - def remove_empties(lines): - while lines and not lines[0].strip(): - del lines[0] - - def split_para(lines): - para = [] - while lines and lines[0].strip(): - para.append(lines[0].strip()) - del lines[0] - return para - - indent = ' ' * 4 - method = self.subcommands[cmd] - doc = method.__doc__ or '' - lines = doc.splitlines() - remove_empties(lines) - if lines: - heading = '* %s -- %s' % (cmd, lines[0]) - result = [heading] - del lines[0] - remove_empties(lines) - while lines: - result.append('') - para_lines = split_para(lines) - para_text = ' '.join(para_lines) - result.append(para_text) - remove_empties(lines) - return '\n'.join(result) - else: - return '* %s' % cmd - def setup_logging(self): # pragma: no cover '''Set up logging.''' -- cgit v1.2.1 From a3ba49cba0ccbd568066796cf3c87914830a951a Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:32:58 +0000 Subject: Exclude from test coverage --- cliapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/app.py b/cliapp/app.py index 591b984..fa93e30 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -307,7 +307,7 @@ class Application(object): else: return None - def _format_usage_for(self, cmd): + def _format_usage_for(self, cmd): # pragma: no cover args = self.cmd_synopsis.get(cmd, '') or '' return 'Usage: %%prog [options] %s %s' % (cmd, args) -- cgit v1.2.1 From cb749311e4aa9596856f378802e2af8b9a3cfb7b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:34:35 +0000 Subject: Update NEWS --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index fa8aaff..b790e9f 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,9 @@ Version UNRELEASED 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 -- cgit v1.2.1 From 16b4c16cada55def53d488977603290d07a62388 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 19 Jan 2013 20:36:13 +0000 Subject: Handle newline-less docstrings --- cliapp/app.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index fa93e30..e9e19db 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -335,8 +335,12 @@ class Application(object): def _format_subcommand_help(self, cmd): # pragma: no cover method = self.subcommands[cmd] doc = method.__doc__ or '' - first, rest = doc.split('\n', 1) - return first + '\n' + textwrap.dedent(rest) + 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.''' -- cgit v1.2.1 From 9db19f80e5fcb6f9782b82dccff4f776e06f5a0e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:08:35 +0000 Subject: Add subcommand aliases --- cliapp/app.py | 21 +++++++++++++++------ cliapp/app_tests.py | 8 ++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index e9e19db..93ba966 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -101,6 +101,7 @@ class Application(object): self.cmd_synopsis = {} self.subcommands = {} + self.subcommand_aliases = {} for method_name in self._subcommand_methodnames(): cmd = self._unnormalize_cmd(method_name) self.subcommands[cmd] = getattr(self, method_name) @@ -238,7 +239,7 @@ class Application(object): ''' - def add_subcommand(self, name, func, arg_synopsis=None): + def add_subcommand(self, name, func, arg_synopsis=None, aliases=None): '''Add a subcommand. Normally, subcommands are defined by add ``cmd_foo`` methods @@ -254,6 +255,7 @@ class Application(object): if name not in self.subcommands: self.subcommands[name] = func self.cmd_synopsis[name] = arg_synopsis + self.subcommand_aliases[name] = aliases or [] def add_default_subcommands(self): if 'help' not in self.subcommands: @@ -506,11 +508,18 @@ class Application(object): if self.subcommands: if not args: raise SystemExit('must give subcommand') - if args[0] in self.subcommands: - method = self.subcommands[args[0]] - method(args[1:]) - else: - raise SystemExit('unknown subcommand %s' % args[0]) + + 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) diff --git a/cliapp/app_tests.py b/cliapp/app_tests.py index c8d5220..674e033 100644 --- a/cliapp/app_tests.py +++ b/cliapp/app_tests.py @@ -303,6 +303,14 @@ class SubcommandTests(unittest.TestCase): 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) -- cgit v1.2.1 From f3fa82e7799897b6a9ab3a02ce6e4519bc81401d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:09:29 +0000 Subject: Update NEWS --- NEWS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b790e9f..235f8bd 100644 --- a/NEWS +++ b/NEWS @@ -25,7 +25,10 @@ Version UNRELEASED 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. + profiling information is logged. +* Subcommands added with `add_subcommand` may now have aliases. + Subcommands defined using `Application` class methods named `cmd_*` + cannot have aliases. Bug fixes: -- cgit v1.2.1 From 3c95da92a2c7cd74e65788d3c3995a07f413ff42 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:22:56 +0000 Subject: Allow hiding of subcommands from help output --- cliapp/app.py | 21 ++++++++++++++------- example4.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ without-tests | 1 + 3 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 example4.py diff --git a/cliapp/app.py b/cliapp/app.py index 93ba966..e6eaeab 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -102,6 +102,7 @@ class Application(object): 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) @@ -239,7 +240,8 @@ class Application(object): ''' - def add_subcommand(self, name, func, arg_synopsis=None, aliases=None): + 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 @@ -256,6 +258,8 @@ class Application(object): 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: @@ -296,15 +300,17 @@ class Application(object): assert method.startswith('cmd_') return method[len('cmd_'):].replace('_', '-') - def _format_usage(self): + 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()): - args = self.cmd_synopsis.get(cmd, '') or '' - lines.append('%s %%prog [options] %s %s' % (prefix, cmd, args)) - prefix = ' ' * len(prefix) + 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 @@ -313,12 +319,13 @@ class Application(object): args = self.cmd_synopsis.get(cmd, '') or '' return 'Usage: %%prog [options] %s %s' % (cmd, args) - def _format_description(self): + def _format_description(self, all=False): '''Format OptionParser description, with subcommand support.''' if self.subcommands: summaries = [] for cmd in sorted(self.subcommands.keys()): - summaries.append(self._format_subcommand_summary(cmd)) + 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: diff --git a/example4.py b/example4.py new file mode 100644 index 0000000..94e009c --- /dev/null +++ b/example4.py @@ -0,0 +1,49 @@ +# 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 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() + diff --git a/without-tests b/without-tests index b362003..9f87e60 100644 --- a/without-tests +++ b/without-tests @@ -9,3 +9,4 @@ ./test-plugins/wrongversion_plugin.py ./test-plugins/aaa_hello_plugin.py example3.py +example4.py -- cgit v1.2.1 From f8a7709e6c854180c7b0530370612d8c80422514 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:31:35 +0000 Subject: Add a hidden attribute to settingses --- cliapp/settings.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index 2dbab45..dd0f8ed 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -43,12 +43,14 @@ class Setting(object): nargs = 1 choices = None - def __init__(self, names, default, help, metavar=None, group=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 @@ -87,8 +89,10 @@ class StringListSetting(Setting): action = 'append' - def __init__(self, names, default, help, metavar=None, group=None): - Setting.__init__(self, names, [], help, metavar=metavar, group=group) + 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 @@ -119,9 +123,11 @@ class ChoiceSetting(Setting): type = 'choice' - def __init__(self, names, choices, help, metavar=None, group=None): - Setting.__init__(self, names, choices[0], help, metavar=metavar, - group=group) + 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): -- cgit v1.2.1 From ca686ad12966980517fb6d2412b485023646bfcf Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:33:11 +0000 Subject: Add hidden setting to example4.py --- example4.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example4.py b/example4.py index 94e009c..9f3ddf1 100644 --- a/example4.py +++ b/example4.py @@ -23,6 +23,9 @@ 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) def cmd_greet(self, args): '''Greet the user. -- cgit v1.2.1 From fc455a987626dcb71dee96b69dac88d11294a9da Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:35:47 +0000 Subject: Actually suppress help for hidden options --- cliapp/settings.py | 8 ++++++-- example4.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index dd0f8ed..2890b42 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -554,6 +554,10 @@ class Settings(object): def add_option(obj, s): option_names = self._option_names(s.names) + if s.hidden: + help = optparse.SUPPRESS_HELP + else: + help = s.help obj.add_option(*option_names, action='callback', callback=maybe(set_value), @@ -561,7 +565,7 @@ class Settings(object): type=s.type, nargs=s.nargs, choices=s.choices, - help=s.help, + help=help, metavar=s.metavar) def add_negation_option(obj, s): @@ -575,7 +579,7 @@ class Settings(object): callback=maybe(set_false), callback_args=(s,), type=s.type, - help='') + help=optparse.SUPPRESS_HELP if s.hidden else '') # Add options for every setting. diff --git a/example4.py b/example4.py index 9f3ddf1..14386df 100644 --- a/example4.py +++ b/example4.py @@ -26,6 +26,7 @@ class ExampleApp(cliapp.Application): 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. -- cgit v1.2.1 From 61413e5aa9a7f25352e6ee18d9e9396b3d21872e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:42:56 +0000 Subject: Show/hide hidden options as requested Including some built-in ones. --- cliapp/settings.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cliapp/settings.py b/cliapp/settings.py index 2890b42..3b96e7b 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -410,7 +410,7 @@ class Settings(object): return name def build_parser(self, configs_only=False, arg_synopsis=None, - cmd_synopsis=None, deferred_last=[]): + 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. @@ -459,6 +459,15 @@ class Settings(object): 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. @@ -471,7 +480,8 @@ class Settings(object): action='callback', nargs=0, callback=defer_last(maybe(dump_setting_names)), - help='write out all names of settings and quit') + help=help_text( + 'write out all names of settings and quit', True)) # Add --dump-config. @@ -520,7 +530,7 @@ class Settings(object): action='callback', nargs=0, callback=defer_last(maybe(list_config_files)), - help='list all possible config files') + help=help_text('list all possible config files', True)) # Add --generate-manpage. @@ -531,7 +541,7 @@ class Settings(object): nargs=1, type='string', callback=maybe(self._generate_manpage), - help='fill in manual page TEMPLATE', + help=help_text('fill in manual page TEMPLATE', True), metavar='TEMPLATE') # Add other options, from the user-defined and built-in @@ -554,10 +564,6 @@ class Settings(object): def add_option(obj, s): option_names = self._option_names(s.names) - if s.hidden: - help = optparse.SUPPRESS_HELP - else: - help = s.help obj.add_option(*option_names, action='callback', callback=maybe(set_value), @@ -565,7 +571,7 @@ class Settings(object): type=s.type, nargs=s.nargs, choices=s.choices, - help=help, + help=help_text(s.help, s.hidden), metavar=s.metavar) def add_negation_option(obj, s): @@ -579,7 +585,7 @@ class Settings(object): callback=maybe(set_false), callback_args=(s,), type=s.type, - help=optparse.SUPPRESS_HELP if s.hidden else '') + help=help_text('', s.hidden)) # Add options for every setting. @@ -599,7 +605,8 @@ class Settings(object): def parse_args(self, args, parser=None, suppress_errors=False, configs_only=False, arg_synopsis=None, - cmd_synopsis=None, compute_setting_values=None): + cmd_synopsis=None, compute_setting_values=None, + all_options=False): '''Parse the command line. Return list of non-option arguments. ``args`` would usually @@ -612,7 +619,8 @@ class Settings(object): p = parser or self.build_parser(configs_only=configs_only, arg_synopsis=arg_synopsis, cmd_synopsis=cmd_synopsis, - deferred_last=deferred_last) + deferred_last=deferred_last, + all_options=all_options) if suppress_errors: p.error = lambda msg: sys.exit(1) -- cgit v1.2.1 From a1347e9330e00529ccbd0feaf3f05b3c98d52062 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:51:43 +0000 Subject: Add --help-all option to show hidden options too --- cliapp/settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cliapp/settings.py b/cliapp/settings.py index 3b96e7b..0c54d84 100644 --- a/cliapp/settings.py +++ b/cliapp/settings.py @@ -544,6 +544,23 @@ class Settings(object): 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. -- cgit v1.2.1 From 090dfa309e03eda0637bab9932d2982e2d5aab22 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:54:37 +0000 Subject: Add help-all subcommand --- cliapp/app.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cliapp/app.py b/cliapp/app.py index e6eaeab..cf970e0 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -264,6 +264,8 @@ class Application(object): 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(self, args): # pragma: no cover '''Print help.''' @@ -286,6 +288,28 @@ class Application(object): text = self.settings.progname.join(text.split('%prog')) self.output.write(text) + + def help_all(self, args): # pragma: no cover + '''Print help, including hidden subcommands.''' + + 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=True) + description = fmt.format(self._format_description(all=True)) + text = '%s\n\n%s' % (usage, description) + + text = self.settings.progname.join(text.split('%prog')) + self.output.write(text) def _subcommand_methodnames(self): return [x -- cgit v1.2.1 From 9eb472a7d1e4f90effd862522ecb898128e36704 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:56:18 +0000 Subject: Refactor to avoid duplicated code --- cliapp/app.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/cliapp/app.py b/cliapp/app.py index cf970e0..3c07310 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -267,9 +267,7 @@ class Application(object): if 'help-all' not in self.subcommands: self.add_subcommand('help-all', self.help_all) - def help(self, args): # pragma: no cover - '''Print help.''' - + def _help_helper(self, args, show_all): # pragma: no cover try: width = int(os.environ.get('COLUMNS', '78')) except ValueError: @@ -282,34 +280,20 @@ class Application(object): description = fmt.format(self._format_subcommand_help(args[0])) text = '%s\n\n%s' % (usage, description) else: - usage = self._format_usage() - description = fmt.format(self._format_description()) + 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.''' - - 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=True) - description = fmt.format(self._format_description(all=True)) - text = '%s\n\n%s' % (usage, description) - - text = self.settings.progname.join(text.split('%prog')) - self.output.write(text) + self._help_helper(args, True) def _subcommand_methodnames(self): return [x -- cgit v1.2.1 From 499e317c1b1a1af0e99abe22b742262daeb2959f Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 10:57:11 +0000 Subject: Update NEWS --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 235f8bd..ec86219 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,9 @@ Version UNRELEASED * 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. Bug fixes: -- cgit v1.2.1 From 25272d0b6244741ba8dbdb3c89878c966cee046b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 11:07:05 +0000 Subject: Document how --generate-manpage is used in cliapp(5) Suggested-By: Enrico Zini --- NEWS | 2 ++ cliapp.5 | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/NEWS b/NEWS index ec86219..cf7600c 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,8 @@ Version UNRELEASED * 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. Bug fixes: diff --git a/cliapp.5 b/cliapp.5 index 7950033..2da576c 100644 --- a/cliapp.5 +++ b/cliapp.5 @@ -185,6 +185,49 @@ as set by the application code or determined by 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. .SH FILES .B cliapp reads a list of configuration files at startup, -- cgit v1.2.1 From 366925616dd4382214cc1444a0105d39a9474259 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:26:00 +0000 Subject: Fix test class name --- cliapp/runcmd_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index 99dc98a..ede0c3e 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -31,7 +31,7 @@ def devnull(msg): pass -class ApplicationTests(unittest.TestCase): +class RuncmdTests(unittest.TestCase): def test_runcmd_executes_true(self): self.assertEqual(cliapp.runcmd(['true']), '') -- cgit v1.2.1 From 694dc0d1d744e20e5bf1722e928afa4cbcb596a0 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:28:34 +0000 Subject: Start a function to safely quote arbitrary strings for shell --- cliapp/__init__.py | 2 +- cliapp/runcmd.py | 6 ++++++ cliapp/runcmd_tests.py | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cliapp/__init__.py b/cliapp/__init__.py index fbf1af3..9ab5cc4 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -21,7 +21,7 @@ __version__ = '1.20121216' from fmt import TextFormat from settings import (Settings, log_group_name, config_group_name, perf_group_name) -from runcmd import runcmd, runcmd_unchecked +from runcmd import runcmd, runcmd_unchecked, shell_quote from app import Application, AppException # The plugin system diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index 083dbca..081d14b 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -199,3 +199,9 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): return procs[-1].returncode, ''.join(out), ''.join(err) + + +def shell_quote(s): + '''Return a shell-quoted version of s.''' + return s + diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index ede0c3e..8f8a8a9 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -124,3 +124,9 @@ class RuncmdTests(unittest.TestCase): 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(''), '') + -- cgit v1.2.1 From ad7c76554c65f230cfc78eb457a268a988bca380 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:29:32 +0000 Subject: Allow safe characters as-is --- cliapp/runcmd_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index 8f8a8a9..44991c4 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -130,3 +130,6 @@ 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') + -- cgit v1.2.1 From 182f10bfd1f75bc08ad5d5a5f352707793dce750 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:31:33 +0000 Subject: Quote spaces --- cliapp/runcmd.py | 12 +++++++++++- cliapp/runcmd_tests.py | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index 081d14b..5212222 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -203,5 +203,15 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): def shell_quote(s): '''Return a shell-quoted version of s.''' - return s + + safe = 'abc123' + + quoted = [] + for c in s: + if c in safe: + quoted.append(c) + else: + quoted.append("'%c'" % c) + + return ''.join(quoted) diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index 44991c4..be01694 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -133,3 +133,6 @@ class ShellQuoteTests(unittest.TestCase): 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(' '), "' '") + -- cgit v1.2.1 From fac1e9438367dc19f535dde4b9ee1e6450410496 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:32:05 +0000 Subject: Quote double-quote --- cliapp/runcmd_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index be01694..69f9309 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -136,3 +136,6 @@ class ShellQuoteTests(unittest.TestCase): def test_quotes_space(self): self.assertEqual(cliapp.shell_quote(' '), "' '") + def test_quotes_double_quote(self): + self.assertEqual(cliapp.shell_quote('"'), "'\"'") + -- cgit v1.2.1 From 2952ba6730f79e6f648811c232bdd2ae0311099d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:33:12 +0000 Subject: Quote single quote --- cliapp/runcmd.py | 2 ++ cliapp/runcmd_tests.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index 5212222..48f6404 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -210,6 +210,8 @@ def shell_quote(s): for c in s: if c in safe: quoted.append(c) + elif c == "'": + quoted.append('"\'"') else: quoted.append("'%c'" % c) diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py index 69f9309..a8a15dc 100644 --- a/cliapp/runcmd_tests.py +++ b/cliapp/runcmd_tests.py @@ -139,3 +139,6 @@ class ShellQuoteTests(unittest.TestCase): def test_quotes_double_quote(self): self.assertEqual(cliapp.shell_quote('"'), "'\"'") + def test_quotes_single_quote(self): + self.assertEqual(cliapp.shell_quote("'"), '"\'"') + -- cgit v1.2.1 From c492e2efbaadbd93f077b5dab7c4c8ac8babcec6 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:35:27 +0000 Subject: Allow some more safe characters --- cliapp/runcmd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index 48f6404..c8e0281 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -204,7 +204,11 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr): def shell_quote(s): '''Return a shell-quoted version of s.''' - safe = 'abc123' + lower_ascii = 'abcdefghijklmnopqrstuvwxyz' + upper_ascii = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + digits = '0123456789' + punctuation = '-_/=.,:' + safe = set(lower_ascii + upper_ascii + digits + punctuation) quoted = [] for c in s: -- cgit v1.2.1 From 5e3a3cfd1f325c3c2d346967ed6c2e381f89d546 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:43:28 +0000 Subject: Add cliapp.ssh_runcmd --- cliapp/__init__.py | 2 +- cliapp/runcmd.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 9ab5cc4..6d4040a 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -21,7 +21,7 @@ __version__ = '1.20121216' 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 +from runcmd import runcmd, runcmd_unchecked, shell_quote, ssh_runcmd from app import Application, AppException # The plugin system diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index c8e0281..10b75ef 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -221,3 +221,27 @@ def shell_quote(s): 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. + + 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. + + ''' + + local_argv = ['ssh', target, '--'] + map(shell_quote, argv) + return runcmd(local_argv, **kwargs) + -- cgit v1.2.1 From 523f008c0648174f8d046df41f1943661afeb1e2 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 10 Feb 2013 12:45:48 +0000 Subject: Update NEWS --- NEWS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS b/NEWS index cf7600c..db86fc4 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,12 @@ Version UNRELEASED 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. Bug fixes: -- cgit v1.2.1 From cf89ed0cb38d0304ff6f97fbd4892c42f88a2918 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 5 Mar 2013 11:31:05 +0000 Subject: Add log_error to cliapp.runcmd --- NEWS | 2 ++ cliapp/runcmd.py | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/NEWS b/NEWS index db86fc4..df63faf 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,8 @@ Version UNRELEASED 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: diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py index 10b75ef..c578059 100644 --- a/cliapp/runcmd.py +++ b/cliapp/runcmd.py @@ -40,19 +40,26 @@ def runcmd(argv, *args, **kwargs): ''' - if 'ignore_fail' in kwargs: - ignore_fail = kwargs['ignore_fail'] - del kwargs['ignore_fail'] - else: - ignore_fail = False + 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 ignore_fail: - logging.info(msg) + if opts['ignore_fail']: + if opts['log_error']: + logging.info(msg) else: - logging.error(msg) + if opts['log_error']: + logging.error(msg) raise cliapp.AppException(msg) return out -- cgit v1.2.1 From 2c198cb79729ebc8f44e0e326db71e4ef18cde3b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 13 Mar 2013 21:21:50 +0000 Subject: Prepare release version 1.20130313 --- NEWS | 2 +- cliapp/__init__.py | 2 +- debian/changelog | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index db86fc4..729fe41 100644 --- a/NEWS +++ b/NEWS @@ -1,7 +1,7 @@ NEWS for cliapp =============== -Version UNRELEASED +Version 1.20130313 ------------------ * Add `cliapp.Application.compute_setting_values` method. This allows diff --git a/cliapp/__init__.py b/cliapp/__init__.py index 6d4040a..ea30ac2 100644 --- a/cliapp/__init__.py +++ b/cliapp/__init__.py @@ -15,7 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -__version__ = '1.20121216' +__version__ = '1.20130313' from fmt import TextFormat diff --git a/debian/changelog b/debian/changelog index 0a2a142..eed671f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cliapp (1.20130313-1) unstable; urgency=low + + * New upstream version. + + -- Lars Wirzenius Wed, 13 Mar 2013 21:21:40 +0000 + python-cliapp (1.20121216-1) unstable; urgency=low * New upstream release. -- cgit v1.2.1