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
1=== modified file 'charmhelpers/cli/__init__.py'
2--- charmhelpers/cli/__init__.py 2013-06-19 23:00:23 +0000
3+++ charmhelpers/cli/__init__.py 2013-06-27 15:47:32 +0000
4@@ -1,15 +1,82 @@
5 import inspect
6 import itertools
7 import argparse
8+import sys
9+
10+class OutputFormatter(object):
11+ def __init__(self, outfile=sys.stdout):
12+ self.formats = { 'raw': self.raw,
13+ 'python': self.pprint,
14+ 'json': self.json,
15+ 'yaml': self.yaml,
16+ 'csv': self.csv,
17+ 'tab': self.tab,
18+ }
19+ self.outfile = outfile
20+
21+ @property
22+ def supported_formats(self):
23+ return self.formats.keys()
24+
25+ def raw(self, output):
26+ """Output data as raw string"""
27+ try:
28+ self.outfile.writelines(output)
29+ except TypeError:
30+ self.outfile.write(str(output))
31+
32+ def pprint(self, output):
33+ """Output data as a nicely-formatted python data structure"""
34+ import pprint
35+ pprint.pprint(output, stream=self.outfile)
36+
37+ def json(self, output):
38+ """Output data in JSON format"""
39+ import json
40+ json.dump(output, self.outfile)
41+
42+ def yaml(self, output):
43+ """Output data in YAML format"""
44+ import yaml
45+ yaml.safe_dump(output, self.outfile)
46+
47+ def csv(self, output):
48+ """Output data as excel-compatible CSV"""
49+ import csv
50+ csvwriter = csv.writer(self.outfile)
51+ csvwriter.writerows(output)
52+
53+ def tab(self, output):
54+ """Output data in excel-compatible tab-delimited format"""
55+ import csv
56+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
57+ csvwriter.writerows(output)
58+
59+ def format_output(self, output, fmt='raw'):
60+ fmtfunc = self.formats[fmt]
61+ fmtfunc(output)
62
63
64 class CommandLine(object):
65 argument_parser = None
66 subparsers = None
67+ formatter = None
68
69 def __init__(self):
70 if not self.argument_parser:
71 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
72+ if not self.formatter:
73+ self.formatter = OutputFormatter()
74+ formatgroup = self.argument_parser.add_mutually_exclusive_group()
75+ choices = self.formatter.supported_formats
76+ formatgroup.add_argument("--format", metavar='FMT',
77+ help="Select output format for returned data, "
78+ "where FMT is one of: {}".format(choices),
79+ choices=choices, default='raw')
80+ for fmt, fmtfunc in self.formatter.formats.iteritems():
81+ formatgroup.add_argument("-{}".format(fmt[0]),
82+ "--{}".format(fmt), action='store_const', const=fmt,
83+ dest='format', help=fmtfunc.__doc__)
84 if not self.subparsers:
85 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
86
87@@ -55,7 +122,7 @@
88 vargs = getattr(arguments, argspec.vargs)
89 for arg in argspec.args:
90 kwargs[arg] = getattr(arguments, arg)
91- arguments.func(*vargs, **kwargs)
92+ self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format)
93
94
95 cmdline = CommandLine()
96@@ -72,7 +139,7 @@
97 positional_args = argspec.args[:len(argspec.defaults)]
98 keyword_names = argspec.args[-len(argspec.defaults):]
99 for arg, default in itertools.izip(keyword_names, argspec.defaults):
100- yield (arg,), {'default': default}
101+ yield ('--{}'.format(arg),), {'default': default}
102 else:
103 positional_args = argspec.args
104
105
106=== modified file 'charmhelpers/cli/host.py'
107--- charmhelpers/cli/host.py 2013-06-19 23:00:23 +0000
108+++ charmhelpers/cli/host.py 2013-06-27 15:47:32 +0000
109@@ -5,9 +5,7 @@
110 @cmdline.subcommand()
111 def mounts():
112 "List mounts"
113- for mount in host.mounts():
114- print mount
115-
116+ return host.mounts()
117
118 @cmdline.subcommand_builder('service', description="Control system services")
119 def service(subparser):
120
121=== renamed directory 'tests/commandant' => 'tests/cli'
122=== added file 'tests/cli/test_command_decorators.py'
123--- tests/cli/test_command_decorators.py 1970-01-01 00:00:00 +0000
124+++ tests/cli/test_command_decorators.py 2013-06-27 15:47:32 +0000
125@@ -0,0 +1,46 @@
126+"""Tests for the commandant code that analyzes a function signature to
127+determine the parameters to argparse."""
128+
129+from testtools import TestCase, matchers
130+from mock import patch
131+
132+from charmhelpers import cli
133+
134+@patch('sys.exit')
135+class SubCommandTest(TestCase):
136+ """Test creation of subcommands"""
137+
138+ def test_subcommand_wrapper(self, mock_sys_exit):
139+ """Test function name detection"""
140+ cmdline = cli.CommandLine()
141+ @cmdline.subcommand()
142+ def payload():
143+ "A function that does work."
144+ pass
145+ args = cmdline.argument_parser.parse_args(['payload'])
146+ self.assertEqual(args.func, payload)
147+ self.assertEqual(mock_sys_exit.mock_calls, [])
148+
149+ def test_bogus_arguments(self, mock_sys_exit):
150+ """Test function name detection"""
151+ cmdline = cli.CommandLine()
152+ @cmdline.subcommand()
153+ def payload():
154+ "A function that does work."
155+ pass
156+ self.assertRaises(TypeError, cmdline.argument_parser.parse_args,
157+ ['deliberately bad input'])
158+ mock_sys_exit.assert_called_once_with(2)
159+
160+ def test_cmdline_options(self, mock_sys_exit):
161+ """Test detection of positional arguments and optional parameters."""
162+ cmdline = cli.CommandLine()
163+ @cmdline.subcommand()
164+ def payload(x, y=None):
165+ "A function that does work."
166+ return x
167+ args = cmdline.argument_parser.parse_args(['payload', 'positional', '--y=optional'])
168+ self.assertEqual(args.func, payload)
169+ self.assertEqual(args.x, 'positional')
170+ self.assertEqual(args.y, 'optional')
171+ self.assertEqual(mock_sys_exit.mock_calls, [])
172
173=== modified file 'tests/cli/test_function_signature_analysis.py'
174--- tests/commandant/test_function_signature_analysis.py 2013-06-06 16:58:35 +0000
175+++ tests/cli/test_function_signature_analysis.py 2013-06-27 15:47:32 +0000
176@@ -3,35 +3,35 @@
177
178 from testtools import TestCase, matchers
179
180-from charmhelpers.commandant import signature
181+from charmhelpers import cli
182
183 class FunctionSignatureTest(TestCase):
184 """Test a variety of function signatures."""
185
186 def test_positional_arguments(self):
187 """Finite number of order-dependent required arguments."""
188- argparams = signature.describe_arguments(lambda x, y, z: False)
189+ argparams = cli.describe_arguments(lambda x, y, z: False)
190 self.assertEqual(tuple(argparams),
191 ((('x',), {}), (('y',), {}), (('z',), {})))
192
193 def test_keyword_arguments(self):
194 """Function has optional parameters with default values."""
195- argparams = tuple(signature.describe_arguments(
196+ argparams = tuple(cli.describe_arguments(
197 lambda x, y=3, z="bar": False))
198- self.assertIn((('y',), {'default': 3}), argparams)
199- self.assertIn((('z',), {'default': 'bar'}), argparams)
200+ self.assertIn((('--y',), {'default': 3}), argparams)
201+ self.assertIn((('--z',), {'default': 'bar'}), argparams)
202
203 def test_varargs(self):
204 """Function has a splat-operator parameter to catch an arbitrary number
205 of positional parameters."""
206- argparams = tuple(signature.describe_arguments(
207+ argparams = tuple(cli.describe_arguments(
208 lambda x, y=3, *z: False))
209 self.assertIn((('z',), {'nargs': '*'}), argparams)
210
211 def test_keyword_splat_missing(self):
212 """Double-splat arguments can't be represented in the current version
213 of commandant."""
214- args = signature.describe_arguments(
215+ args = cli.describe_arguments(
216 lambda x, y=3, *z, **missing: False)
217 for opts, _ in args:
218 # opts should be ('varname',) at this point

Subscribers

People subscribed via source and target branches

to all changes: