Merge lp:~daniel-nichter/boots/vertical-output into lp:boots
- vertical-output
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~daniel-nichter/boots/vertical-output |
Merge into: | lp:boots |
Diff against target: |
291 lines (+152/-60) 3 files modified
boots/app/client_config.py (+6/-0) boots/lib/ui/plain.py (+70/-56) tests/boots/lib/ui/ui_tests.py (+76/-4) |
To merge this branch: | bzr merge lp:~daniel-nichter/boots/vertical-output |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes | Approve | ||
Review via email: mp+25407@code.launchpad.net |
Commit message
Description of the change
Adds --vertical-output option to enable mysql cli \G-like output:
> show master status;
=======
File: mysql-bin.000001
Position: 364
Binlog_Do_DB:
Binlog_Ignore_DB:
1 row in set (0.00s server | +0.00s working)
Daniel Nichter (daniel-nichter) wrote : | # |
> Awesomeness! Daniel, is it possible to include a simple test case for this
> new functionality?
Yes, I will write tests...
- 165. By Daniel Nichter
-
Move padded(), show_NULL() and _gen_output() out of present() for testing; prepend _ to first two. Add ui tests.
Daniel Nichter (daniel-nichter) wrote : | # |
Added some basic ui tests. My understanding of boots internals is not complete so if the _gen_output() tests in particular seem odd please correct/enlighten me. Thanks.
Unmerged revisions
- 165. By Daniel Nichter
-
Move padded(), show_NULL() and _gen_output() out of present() for testing; prepend _ to first two. Add ui tests.
- 164. By Daniel Nichter
-
Add --vertical-output.
Preview Diff
1 | === modified file 'boots/app/client_config.py' | |||
2 | --- boots/app/client_config.py 2010-04-06 04:56:01 +0000 | |||
3 | +++ boots/app/client_config.py 2010-06-02 04:01:24 +0000 | |||
4 | @@ -107,6 +107,7 @@ | |||
5 | 107 | "pager": None, | 107 | "pager": None, |
6 | 108 | "pager_command": None, | 108 | "pager_command": None, |
7 | 109 | "terminating_char": ";", | 109 | "terminating_char": ";", |
8 | 110 | "vertical_output": False, | ||
9 | 110 | "history_length": 100, | 111 | "history_length": 100, |
10 | 111 | "history_file": os.path.expanduser("~/.boots_history")} | 112 | "history_file": os.path.expanduser("~/.boots_history")} |
11 | 112 | self._dict = self._defaults.copy() | 113 | self._dict = self._defaults.copy() |
12 | @@ -195,6 +196,11 @@ | |||
13 | 195 | type = "string", | 196 | type = "string", |
14 | 196 | dest = "terminating_char", | 197 | dest = "terminating_char", |
15 | 197 | help = _("specify the SQL statement terminating character (default is ';')")) | 198 | help = _("specify the SQL statement terminating character (default is ';')")) |
16 | 199 | misc_group.add_option("--vertical-output", | ||
17 | 200 | action = "store_true", | ||
18 | 201 | dest = "vertical_output", | ||
19 | 202 | default = False, | ||
20 | 203 | help = _("print vertical output (like \G)")) | ||
21 | 198 | misc_group.add_option("-F", "--historyfile", | 204 | misc_group.add_option("-F", "--historyfile", |
22 | 199 | action = "store", | 205 | action = "store", |
23 | 200 | type = "string", | 206 | type = "string", |
24 | 201 | 207 | ||
25 | === modified file 'boots/lib/ui/plain.py' | |||
26 | --- boots/lib/ui/plain.py 2010-03-29 23:47:14 +0000 | |||
27 | +++ boots/lib/ui/plain.py 2010-06-02 04:01:24 +0000 | |||
28 | @@ -68,6 +68,7 @@ | |||
29 | 68 | self.prompt2 = console.config["prompt2"] | 68 | self.prompt2 = console.config["prompt2"] |
30 | 69 | self.hist_file = console.config["history_file"] | 69 | self.hist_file = console.config["history_file"] |
31 | 70 | self.pager_command = console.config["pager_command"] | 70 | self.pager_command = console.config["pager_command"] |
32 | 71 | self.vertical_output = console.config["vertical_output"] | ||
33 | 71 | self.last_desc = None | 72 | self.last_desc = None |
34 | 72 | self.buffer = [] | 73 | self.buffer = [] |
35 | 73 | 74 | ||
36 | @@ -166,69 +167,16 @@ | |||
37 | 166 | def present(self, result): | 167 | def present(self, result): |
38 | 167 | """Print the result provided as an argument. | 168 | """Print the result provided as an argument. |
39 | 168 | 169 | ||
41 | 169 | If the result is a packet buffer the results until the last packet is | 170 | If the result is a packet, buffer the results until the last packet is |
42 | 170 | received. At that point print all of the buffered result rows in table | 171 | received. At that point print all of the buffered result rows in table |
43 | 171 | format. Non packet results are presented in their string representation | 172 | format. Non packet results are presented in their string representation |
44 | 172 | as received.""" | 173 | as received.""" |
45 | 173 | def padded(fields, widths): | ||
46 | 174 | """Utility function used to convert rows from tuples to table rows. | ||
47 | 175 | Results are returned as strings.""" | ||
48 | 176 | padded_fields = [] | ||
49 | 177 | for index, field in enumerate(fields): | ||
50 | 178 | padded_fields.append(field.ljust(widths[index])) | ||
51 | 179 | return u"| {0} |".format(u" | ".join(padded_fields)) | ||
52 | 180 | |||
53 | 181 | def show_NULL(value): | ||
54 | 182 | """There is a 'bug' in the dbapi that does not convert NULL objects | ||
55 | 183 | to the string 'NULL'. This utility function performs that | ||
56 | 184 | conversion.""" | ||
57 | 185 | return value if value is not None else "NULL" | ||
58 | 186 | |||
59 | 187 | def _gen_table(info): | ||
60 | 188 | with DurationCounter() as table_elapsed: | ||
61 | 189 | if self.buffer: | ||
62 | 190 | # If a column can contain the value NULL, we leave at least 4 | ||
63 | 191 | # characters, since display_size does not account for NULL. | ||
64 | 192 | max_widths = map(max, [(len(column.name), | ||
65 | 193 | column.display_size, | ||
66 | 194 | 4 if column.null_ok else 0) | ||
67 | 195 | for column in info["description"]]) | ||
68 | 196 | dashes = map(lambda x: "-"*(x+2), max_widths) | ||
69 | 197 | sep_line = "+" + "+".join(dashes) + "+" | ||
70 | 198 | names = (column[0] for column in info["description"]) | ||
71 | 199 | yield sep_line | ||
72 | 200 | yield padded(names, max_widths) | ||
73 | 201 | yield sep_line | ||
74 | 202 | for row in self.buffer: | ||
75 | 203 | yield padded(map(show_NULL, row), max_widths) | ||
76 | 204 | |||
77 | 205 | yield sep_line | ||
78 | 206 | |||
79 | 207 | if self.console.driver.is_interactive: | ||
80 | 208 | if info["description"] is not None: | ||
81 | 209 | set_count = n_("{count} row in set", | ||
82 | 210 | "{count} rows in set", | ||
83 | 211 | info["row_count"]) | ||
84 | 212 | else: | ||
85 | 213 | set_count = n_("{count} row affected", | ||
86 | 214 | "{count} rows affected", | ||
87 | 215 | info["row_count"]) | ||
88 | 216 | |||
89 | 217 | nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"] | ||
90 | 218 | timings = [_("{0:.2f}s server").format(info["server_elapsed"]), | ||
91 | 219 | _("+{0:.2f}s working").format(nodes_elapsed)] | ||
92 | 220 | |||
93 | 221 | if self.console.config["debug"]: | ||
94 | 222 | timings.append(_("{0:.2f}s table").format(table_elapsed.duration)) | ||
95 | 223 | |||
96 | 224 | yield "{0} ({1})".format(set_count.format(count=info["row_count"]), | ||
97 | 225 | " | ".join(timings)) | ||
98 | 226 | 174 | ||
99 | 227 | if type(result) is Rows: | 175 | if type(result) is Rows: |
100 | 228 | self.buffer.extend(result) | 176 | self.buffer.extend(result) |
101 | 229 | elif isinstance(result, ResultInfo): | 177 | elif isinstance(result, ResultInfo): |
102 | 230 | printed = False | 178 | printed = False |
104 | 231 | output = _gen_table(result.value) | 179 | output = self._gen_output(result.value) |
105 | 232 | if self.pager_command and self.console.driver.is_interactive: | 180 | if self.pager_command and self.console.driver.is_interactive: |
106 | 233 | printed = self.print_with_pager("\n".join(output).encode("utf-8")) | 181 | printed = self.print_with_pager("\n".join(output).encode("utf-8")) |
107 | 234 | 182 | ||
108 | @@ -237,7 +185,7 @@ | |||
109 | 237 | for line in output: | 185 | for line in output: |
110 | 238 | sys.stdout.write(line.encode("utf-8")) | 186 | sys.stdout.write(line.encode("utf-8")) |
111 | 239 | sys.stdout.write("\n") | 187 | sys.stdout.write("\n") |
113 | 240 | 188 | ||
114 | 241 | # Reset values for next result set. | 189 | # Reset values for next result set. |
115 | 242 | self.buffer = [] | 190 | self.buffer = [] |
116 | 243 | elif isinstance(result, Status): | 191 | elif isinstance(result, Status): |
117 | @@ -258,6 +206,72 @@ | |||
118 | 258 | else: | 206 | else: |
119 | 259 | return True | 207 | return True |
120 | 260 | 208 | ||
121 | 209 | def _padded(self, fields, widths): | ||
122 | 210 | """Utility function used to convert rows from tuples to table rows. | ||
123 | 211 | Results are returned as strings.""" | ||
124 | 212 | padded_fields = [] | ||
125 | 213 | for index, field in enumerate(fields): | ||
126 | 214 | padded_fields.append(field.ljust(widths[index])) | ||
127 | 215 | return u"| {0} |".format(u" | ".join(padded_fields)) | ||
128 | 216 | |||
129 | 217 | def _show_NULL(self, value): | ||
130 | 218 | """There is a 'bug' in the dbapi that does not convert NULL objects | ||
131 | 219 | to the string 'NULL'. This utility function performs that | ||
132 | 220 | conversion.""" | ||
133 | 221 | return value if value is not None else "NULL" | ||
134 | 222 | |||
135 | 223 | def _gen_output(self, info): | ||
136 | 224 | with DurationCounter() as table_elapsed: | ||
137 | 225 | if self.buffer: | ||
138 | 226 | if self.vertical_output is False: | ||
139 | 227 | # If a column can contain the value NULL, we leave at least 4 | ||
140 | 228 | # characters, since display_size does not account for NULL. | ||
141 | 229 | max_widths = map(max, [(len(column.name), | ||
142 | 230 | column.display_size, | ||
143 | 231 | 4 if column.null_ok else 0) | ||
144 | 232 | for column in info["description"]]) | ||
145 | 233 | dashes = map(lambda x: "-"*(x+2), max_widths) | ||
146 | 234 | sep_line = "+" + "+".join(dashes) + "+" | ||
147 | 235 | names = (column[0] for column in info["description"]) | ||
148 | 236 | yield sep_line | ||
149 | 237 | yield self._padded(names, max_widths) | ||
150 | 238 | yield sep_line | ||
151 | 239 | for row in self.buffer: | ||
152 | 240 | yield self._padded(map(self._show_NULL, row), max_widths) | ||
153 | 241 | |||
154 | 242 | yield sep_line | ||
155 | 243 | else: | ||
156 | 244 | max_width = max([ | ||
157 | 245 | len(column.name) for column in info["description"]]) | ||
158 | 246 | for row_number, row in enumerate(self.buffer, start=1): | ||
159 | 247 | row_header = " Row {0} ".format(row_number) | ||
160 | 248 | yield row_header.center(78, '=') | ||
161 | 249 | for i, column in enumerate(info["description"]): | ||
162 | 250 | line = "{0}: {1}".format( | ||
163 | 251 | column.name.rjust(max_width), row[i]) | ||
164 | 252 | yield line | ||
165 | 253 | yield "" | ||
166 | 254 | |||
167 | 255 | if self.console.driver.is_interactive: | ||
168 | 256 | if info["description"] is not None: | ||
169 | 257 | set_count = n_("{count} row in set", | ||
170 | 258 | "{count} rows in set", | ||
171 | 259 | info["row_count"]) | ||
172 | 260 | else: | ||
173 | 261 | set_count = n_("{count} row affected", | ||
174 | 262 | "{count} rows affected", | ||
175 | 263 | info["row_count"]) | ||
176 | 264 | |||
177 | 265 | nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"] | ||
178 | 266 | timings = [_("{0:.2f}s server").format(info["server_elapsed"]), | ||
179 | 267 | _("+{0:.2f}s working").format(nodes_elapsed)] | ||
180 | 268 | |||
181 | 269 | if self.console.config["debug"]: | ||
182 | 270 | timings.append(_("{0:.2f}s table").format(table_elapsed.duration)) | ||
183 | 271 | |||
184 | 272 | yield "{0} ({1})".format(set_count.format(count=info["row_count"]), | ||
185 | 273 | " | ".join(timings)) | ||
186 | 274 | |||
187 | 261 | @property | 275 | @property |
188 | 262 | def is_interactive(self): | 276 | def is_interactive(self): |
189 | 263 | if hasattr(os, "isatty"): | 277 | if hasattr(os, "isatty"): |
190 | 264 | 278 | ||
191 | === modified file 'tests/boots/lib/ui/ui_tests.py' | |||
192 | --- tests/boots/lib/ui/ui_tests.py 2010-02-27 05:40:10 +0000 | |||
193 | +++ tests/boots/lib/ui/ui_tests.py 2010-06-02 04:01:24 +0000 | |||
194 | @@ -24,6 +24,10 @@ | |||
195 | 24 | import unittest | 24 | import unittest |
196 | 25 | import boots_unit_test | 25 | import boots_unit_test |
197 | 26 | from boots.lib.ui import generic, plain | 26 | from boots.lib.ui import generic, plain |
198 | 27 | from boots.lib import console | ||
199 | 28 | from boots.app import client_config | ||
200 | 29 | from boots.api import api | ||
201 | 30 | import StringIO | ||
202 | 27 | 31 | ||
203 | 28 | class TestGeneric(boots_unit_test.BootsBaseTester): | 32 | class TestGeneric(boots_unit_test.BootsBaseTester): |
204 | 29 | def setUp(self): | 33 | def setUp(self): |
205 | @@ -33,14 +37,82 @@ | |||
206 | 33 | pass | 37 | pass |
207 | 34 | 38 | ||
208 | 35 | def test_example(self): | 39 | def test_example(self): |
210 | 36 | self.assert_(False) | 40 | # TODO: test me! |
211 | 41 | pass | ||
212 | 37 | 42 | ||
213 | 38 | class TestPlain(boots_unit_test.BootsBaseTester): | 43 | class TestPlain(boots_unit_test.BootsBaseTester): |
214 | 39 | def setUp(self): | 44 | def setUp(self): |
216 | 40 | pass | 45 | self.config = client_config.ClientConfig() |
217 | 46 | self.console = console.Console(self.config) | ||
218 | 47 | self.plain_ui = plain.PlainUI(self.console) | ||
219 | 41 | 48 | ||
220 | 42 | def tearDown(self): | 49 | def tearDown(self): |
221 | 43 | pass | 50 | pass |
222 | 44 | 51 | ||
225 | 45 | def test_example(self): | 52 | def test_show_NULL(self): |
226 | 46 | self.assert_(False) | 53 | """_show_NULL() should return whatever it's given unless |
227 | 54 | it's given nothing/None, in which case it should return | ||
228 | 55 | the string 'NULL'.""" | ||
229 | 56 | self.assertEqual(self.plain_ui._show_NULL(None), "NULL") | ||
230 | 57 | self.assertEqual(self.plain_ui._show_NULL(0), 0) | ||
231 | 58 | self.assertEqual(self.plain_ui._show_NULL("0"), "0") | ||
232 | 59 | self.assertEqual(self.plain_ui._show_NULL(1), 1) | ||
233 | 60 | self.assertEqual(self.plain_ui._show_NULL("1"), "1") | ||
234 | 61 | self.assertEqual(self.plain_ui._show_NULL("hello, world!"), "hello, world!") | ||
235 | 62 | |||
236 | 63 | def test_print_notificatilon(self): | ||
237 | 64 | """By default print_notification() writes a message to STDOUT | ||
238 | 65 | like the one below. For testing, we pass a third arg, a StringIO | ||
239 | 66 | to capture the output.""" | ||
240 | 67 | output = StringIO.StringIO() | ||
241 | 68 | self.plain_ui.print_notification("title", "here's my message", output) | ||
242 | 69 | notif = output.getvalue() | ||
243 | 70 | output.close() | ||
244 | 71 | self.assertEqual(notif, "title :: here's my message\n") | ||
245 | 72 | |||
246 | 73 | def test_gen_output(self): | ||
247 | 74 | """_gen_output() outputs rows in either default table/horizontal | ||
248 | 75 | format or optional vertical (\G) format. It returns a generator | ||
249 | 76 | that yields output lines.""" | ||
250 | 77 | |||
251 | 78 | # Add a row so _gen_output() has something to output. | ||
252 | 79 | row_desc = api.RowDescription( | ||
253 | 80 | (('Databases', 15, 3, 256, 256, 0, 0),)) | ||
254 | 81 | rows = api.Rows( (('db1',),), row_desc) | ||
255 | 82 | self.plain_ui.present(rows) | ||
256 | 83 | |||
257 | 84 | # Result info for the row. | ||
258 | 85 | info = api.ResultInfo({ | ||
259 | 86 | "description": row_desc, | ||
260 | 87 | "row_count": 1, | ||
261 | 88 | "begin_time": 1.0, | ||
262 | 89 | "end_time": 3.0, | ||
263 | 90 | "server_elapsed": 1.0, | ||
264 | 91 | }) | ||
265 | 92 | |||
266 | 93 | # Test default table/horizontal output. | ||
267 | 94 | res = self.plain_ui._gen_output(info.value) | ||
268 | 95 | lines = [] | ||
269 | 96 | for line in res: | ||
270 | 97 | lines.append(line) | ||
271 | 98 | self.assertEqual(lines, [ | ||
272 | 99 | "+-----------+", | ||
273 | 100 | "| Databases |", | ||
274 | 101 | "+-----------+", | ||
275 | 102 | "| db1 |", | ||
276 | 103 | "+-----------+", | ||
277 | 104 | "1 row in set (1.00s server | +1.00s working)", | ||
278 | 105 | ]) | ||
279 | 106 | |||
280 | 107 | # Test optional veritcal output. | ||
281 | 108 | self.plain_ui.vertical_output = True | ||
282 | 109 | res = self.plain_ui._gen_output(info.value) | ||
283 | 110 | lines = [] | ||
284 | 111 | for line in res: | ||
285 | 112 | lines.append(line) | ||
286 | 113 | self.assertEqual(lines, [ | ||
287 | 114 | "=================================== Row 1 ====================================", | ||
288 | 115 | "Databases: db1", | ||
289 | 116 | "", | ||
290 | 117 | "1 row in set (1.00s server | +1.00s working)", | ||
291 | 118 | ]) |
Awesomeness! Daniel, is it possible to include a simple test case for this new functionality?
Unit testing is pretty light right now. However, from a glance, it looks like adding a test method to the TestPlain class in /tests/ lib/ui/ ui_tests. py is the right way to go...
Cheers!
jay