Merge lp:~nick-moffitt/charm-helpers/commandant into lp:~mew/charm-helpers/commandant

Proposed by Nick Moffitt
Status: Merged
Merged at revision: 25
Proposed branch: lp:~nick-moffitt/charm-helpers/commandant
Merge into: lp:~mew/charm-helpers/commandant
Diff against target: 218 lines (+123/-12)
4 files modified
charmhelpers/cli/__init__.py (+69/-2)
charmhelpers/cli/host.py (+1/-3)
tests/cli/test_command_decorators.py (+46/-0)
tests/cli/test_function_signature_analysis.py (+7/-7)
To merge this branch: bzr merge lp:~nick-moffitt/charm-helpers/commandant
Reviewer Review Type Date Requested Status
Matthew Wedgwood Pending
Review via email: mp+170642@code.launchpad.net

Description of the change

$ bin/chlp --help
usage: chlp [-h] [--format FMT | -y | -p | -r | -j | -t | -c]
            {mounts,service} ...

Perform common charm tasks

positional arguments:
  {mounts,service} Commands

optional arguments:
  -h, --help show this help message and exit
  --format FMT Select output format for returned data, where FMT is one
                    of: ['yaml', 'python', 'raw', 'json', 'tab', 'csv']
  -y, --yaml Output data in YAML format
  -p, --python Output data as a nicely-formatted python data structure
  -r, --raw Output data as raw string
  -j, --json Output data in JSON format
  -t, --tab Output data in excel-compatible tab-delimited format
  -c, --csv Output data as excel-compatible CSV

$ bin/chlp --format=tab mounts | cut -f 2 | sort -u
/dev/disk/by-uuid/5237fceb-23d0-412d-84d9-b8f8b3bf28af
/home/nick/.Private
binfmt_misc
cgroup
devpts
gvfsd-fuse
none
proc
rootfs
sysfs
tmpfs
udev
$ ./chlp -y mounts
- [/, rootfs]
- [/sys, sysfs]
- [/proc, proc]
- [/dev, udev]
- [/dev/pts, devpts]
- [/run, tmpfs]
- [/, /dev/disk/by-uuid/5237fceb-23d0-412d-84d9-b8f8b3bf28af]
- [/sys/fs/cgroup, none]
- [/sys/fs/fuse/connections, none]
- [/sys/kernel/debug, none]
- [/sys/kernel/security, none]
- [/run/lock, none]
- [/run/shm, none]
- [/run/user, none]
- [/sys/fs/cgroup/cpuset, cgroup]
- [/sys/fs/cgroup/cpu, cgroup]
- [/sys/fs/cgroup/cpuacct, cgroup]
- [/sys/fs/cgroup/memory, cgroup]
- [/sys/fs/cgroup/devices, cgroup]
- [/sys/fs/cgroup/freezer, cgroup]
- [/sys/fs/cgroup/blkio, cgroup]
- [/sys/fs/cgroup/perf_event, cgroup]
- [/sys/fs/cgroup/hugetlb, cgroup]
- [/proc/sys/fs/binfmt_misc, binfmt_misc]
- [/var/lib/schroot/mount/lp0-0b9ed0ed-191c-4195-a6ff-ae429d90f821, /dev/disk/by-uuid/5237fceb-23d0-412d-84d9-b8f8b3bf28af]
- [/var/lib/schroot/mount/lp0-efa0ce0d-8b91-445b-ad51-69336f1b59c2, /dev/disk/by-uuid/5237fceb-23d0-412d-84d9-b8f8b3bf28af]
- [/home/nick/Private, /home/nick/.Private]
- [/run/user/nick/gvfs, gvfsd-fuse]

To post a comment you must log in.
26. By Nick Moffitt

fix tests

27. By Nick Moffitt

Begin testing the command-wrapping decorators. Fixed the optional argument syntax to include dashes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/cli/__init__.py'
--- charmhelpers/cli/__init__.py 2013-06-19 23:00:23 +0000
+++ charmhelpers/cli/__init__.py 2013-06-27 15:47:32 +0000
@@ -1,15 +1,82 @@
1import inspect1import inspect
2import itertools2import itertools
3import argparse3import argparse
4import sys
5
6class OutputFormatter(object):
7 def __init__(self, outfile=sys.stdout):
8 self.formats = { 'raw': self.raw,
9 'python': self.pprint,
10 'json': self.json,
11 'yaml': self.yaml,
12 'csv': self.csv,
13 'tab': self.tab,
14 }
15 self.outfile = outfile
16
17 @property
18 def supported_formats(self):
19 return self.formats.keys()
20
21 def raw(self, output):
22 """Output data as raw string"""
23 try:
24 self.outfile.writelines(output)
25 except TypeError:
26 self.outfile.write(str(output))
27
28 def pprint(self, output):
29 """Output data as a nicely-formatted python data structure"""
30 import pprint
31 pprint.pprint(output, stream=self.outfile)
32
33 def json(self, output):
34 """Output data in JSON format"""
35 import json
36 json.dump(output, self.outfile)
37
38 def yaml(self, output):
39 """Output data in YAML format"""
40 import yaml
41 yaml.safe_dump(output, self.outfile)
42
43 def csv(self, output):
44 """Output data as excel-compatible CSV"""
45 import csv
46 csvwriter = csv.writer(self.outfile)
47 csvwriter.writerows(output)
48
49 def tab(self, output):
50 """Output data in excel-compatible tab-delimited format"""
51 import csv
52 csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
53 csvwriter.writerows(output)
54
55 def format_output(self, output, fmt='raw'):
56 fmtfunc = self.formats[fmt]
57 fmtfunc(output)
458
559
6class CommandLine(object):60class CommandLine(object):
7 argument_parser = None61 argument_parser = None
8 subparsers = None62 subparsers = None
63 formatter = None
964
10 def __init__(self):65 def __init__(self):
11 if not self.argument_parser:66 if not self.argument_parser:
12 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')67 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
68 if not self.formatter:
69 self.formatter = OutputFormatter()
70 formatgroup = self.argument_parser.add_mutually_exclusive_group()
71 choices = self.formatter.supported_formats
72 formatgroup.add_argument("--format", metavar='FMT',
73 help="Select output format for returned data, "
74 "where FMT is one of: {}".format(choices),
75 choices=choices, default='raw')
76 for fmt, fmtfunc in self.formatter.formats.iteritems():
77 formatgroup.add_argument("-{}".format(fmt[0]),
78 "--{}".format(fmt), action='store_const', const=fmt,
79 dest='format', help=fmtfunc.__doc__)
13 if not self.subparsers:80 if not self.subparsers:
14 self.subparsers = self.argument_parser.add_subparsers(help='Commands')81 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
1582
@@ -55,7 +122,7 @@
55 vargs = getattr(arguments, argspec.vargs)122 vargs = getattr(arguments, argspec.vargs)
56 for arg in argspec.args:123 for arg in argspec.args:
57 kwargs[arg] = getattr(arguments, arg)124 kwargs[arg] = getattr(arguments, arg)
58 arguments.func(*vargs, **kwargs)125 self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format)
59126
60127
61cmdline = CommandLine()128cmdline = CommandLine()
@@ -72,7 +139,7 @@
72 positional_args = argspec.args[:len(argspec.defaults)]139 positional_args = argspec.args[:len(argspec.defaults)]
73 keyword_names = argspec.args[-len(argspec.defaults):]140 keyword_names = argspec.args[-len(argspec.defaults):]
74 for arg, default in itertools.izip(keyword_names, argspec.defaults):141 for arg, default in itertools.izip(keyword_names, argspec.defaults):
75 yield (arg,), {'default': default}142 yield ('--{}'.format(arg),), {'default': default}
76 else:143 else:
77 positional_args = argspec.args144 positional_args = argspec.args
78145
79146
=== modified file 'charmhelpers/cli/host.py'
--- charmhelpers/cli/host.py 2013-06-19 23:00:23 +0000
+++ charmhelpers/cli/host.py 2013-06-27 15:47:32 +0000
@@ -5,9 +5,7 @@
5@cmdline.subcommand()5@cmdline.subcommand()
6def mounts():6def mounts():
7 "List mounts"7 "List mounts"
8 for mount in host.mounts():8 return host.mounts()
9 print mount
10
119
12@cmdline.subcommand_builder('service', description="Control system services")10@cmdline.subcommand_builder('service', description="Control system services")
13def service(subparser):11def service(subparser):
1412
=== renamed directory 'tests/commandant' => 'tests/cli'
=== added file 'tests/cli/test_command_decorators.py'
--- tests/cli/test_command_decorators.py 1970-01-01 00:00:00 +0000
+++ tests/cli/test_command_decorators.py 2013-06-27 15:47:32 +0000
@@ -0,0 +1,46 @@
1"""Tests for the commandant code that analyzes a function signature to
2determine the parameters to argparse."""
3
4from testtools import TestCase, matchers
5from mock import patch
6
7from charmhelpers import cli
8
9@patch('sys.exit')
10class SubCommandTest(TestCase):
11 """Test creation of subcommands"""
12
13 def test_subcommand_wrapper(self, mock_sys_exit):
14 """Test function name detection"""
15 cmdline = cli.CommandLine()
16 @cmdline.subcommand()
17 def payload():
18 "A function that does work."
19 pass
20 args = cmdline.argument_parser.parse_args(['payload'])
21 self.assertEqual(args.func, payload)
22 self.assertEqual(mock_sys_exit.mock_calls, [])
23
24 def test_bogus_arguments(self, mock_sys_exit):
25 """Test function name detection"""
26 cmdline = cli.CommandLine()
27 @cmdline.subcommand()
28 def payload():
29 "A function that does work."
30 pass
31 self.assertRaises(TypeError, cmdline.argument_parser.parse_args,
32 ['deliberately bad input'])
33 mock_sys_exit.assert_called_once_with(2)
34
35 def test_cmdline_options(self, mock_sys_exit):
36 """Test detection of positional arguments and optional parameters."""
37 cmdline = cli.CommandLine()
38 @cmdline.subcommand()
39 def payload(x, y=None):
40 "A function that does work."
41 return x
42 args = cmdline.argument_parser.parse_args(['payload', 'positional', '--y=optional'])
43 self.assertEqual(args.func, payload)
44 self.assertEqual(args.x, 'positional')
45 self.assertEqual(args.y, 'optional')
46 self.assertEqual(mock_sys_exit.mock_calls, [])
047
=== modified file 'tests/cli/test_function_signature_analysis.py'
--- tests/commandant/test_function_signature_analysis.py 2013-06-06 16:58:35 +0000
+++ tests/cli/test_function_signature_analysis.py 2013-06-27 15:47:32 +0000
@@ -3,35 +3,35 @@
33
4from testtools import TestCase, matchers4from testtools import TestCase, matchers
55
6from charmhelpers.commandant import signature6from charmhelpers import cli
77
8class FunctionSignatureTest(TestCase):8class FunctionSignatureTest(TestCase):
9 """Test a variety of function signatures."""9 """Test a variety of function signatures."""
1010
11 def test_positional_arguments(self):11 def test_positional_arguments(self):
12 """Finite number of order-dependent required arguments."""12 """Finite number of order-dependent required arguments."""
13 argparams = signature.describe_arguments(lambda x, y, z: False)13 argparams = cli.describe_arguments(lambda x, y, z: False)
14 self.assertEqual(tuple(argparams),14 self.assertEqual(tuple(argparams),
15 ((('x',), {}), (('y',), {}), (('z',), {})))15 ((('x',), {}), (('y',), {}), (('z',), {})))
1616
17 def test_keyword_arguments(self):17 def test_keyword_arguments(self):
18 """Function has optional parameters with default values."""18 """Function has optional parameters with default values."""
19 argparams = tuple(signature.describe_arguments(19 argparams = tuple(cli.describe_arguments(
20 lambda x, y=3, z="bar": False))20 lambda x, y=3, z="bar": False))
21 self.assertIn((('y',), {'default': 3}), argparams)21 self.assertIn((('--y',), {'default': 3}), argparams)
22 self.assertIn((('z',), {'default': 'bar'}), argparams)22 self.assertIn((('--z',), {'default': 'bar'}), argparams)
2323
24 def test_varargs(self):24 def test_varargs(self):
25 """Function has a splat-operator parameter to catch an arbitrary number25 """Function has a splat-operator parameter to catch an arbitrary number
26 of positional parameters."""26 of positional parameters."""
27 argparams = tuple(signature.describe_arguments(27 argparams = tuple(cli.describe_arguments(
28 lambda x, y=3, *z: False))28 lambda x, y=3, *z: False))
29 self.assertIn((('z',), {'nargs': '*'}), argparams)29 self.assertIn((('z',), {'nargs': '*'}), argparams)
3030
31 def test_keyword_splat_missing(self):31 def test_keyword_splat_missing(self):
32 """Double-splat arguments can't be represented in the current version32 """Double-splat arguments can't be represented in the current version
33 of commandant."""33 of commandant."""
34 args = signature.describe_arguments(34 args = cli.describe_arguments(
35 lambda x, y=3, *z, **missing: False)35 lambda x, y=3, *z, **missing: False)
36 for opts, _ in args:36 for opts, _ in args:
37 # opts should be ('varname',) at this point37 # opts should be ('varname',) at this point

Subscribers

People subscribed via source and target branches

to all changes: