Merge lp:~daniel-nichter/boots/vertical-output into lp:boots

Proposed by Daniel Nichter
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
Reviewer Review Type Date Requested Status
Jay Pipes Approve
Review via email: mp+25407@code.launchpad.net

Description of the change

Adds --vertical-output option to enable mysql cli \G-like output:

> show master status;
=================================== Row 1 ====================================
            File: mysql-bin.000001
        Position: 364
    Binlog_Do_DB:
Binlog_Ignore_DB:

1 row in set (0.00s server | +0.00s working)

To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) wrote :

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

review: Approve
Revision history for this message
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.

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'boots/app/client_config.py'
--- boots/app/client_config.py 2010-04-06 04:56:01 +0000
+++ boots/app/client_config.py 2010-06-02 04:01:24 +0000
@@ -107,6 +107,7 @@
107 "pager": None,107 "pager": None,
108 "pager_command": None,108 "pager_command": None,
109 "terminating_char": ";",109 "terminating_char": ";",
110 "vertical_output": False,
110 "history_length": 100,111 "history_length": 100,
111 "history_file": os.path.expanduser("~/.boots_history")}112 "history_file": os.path.expanduser("~/.boots_history")}
112 self._dict = self._defaults.copy()113 self._dict = self._defaults.copy()
@@ -195,6 +196,11 @@
195 type = "string",196 type = "string",
196 dest = "terminating_char",197 dest = "terminating_char",
197 help = _("specify the SQL statement terminating character (default is ';')"))198 help = _("specify the SQL statement terminating character (default is ';')"))
199 misc_group.add_option("--vertical-output",
200 action = "store_true",
201 dest = "vertical_output",
202 default = False,
203 help = _("print vertical output (like \G)"))
198 misc_group.add_option("-F", "--historyfile",204 misc_group.add_option("-F", "--historyfile",
199 action = "store",205 action = "store",
200 type = "string",206 type = "string",
201207
=== modified file 'boots/lib/ui/plain.py'
--- boots/lib/ui/plain.py 2010-03-29 23:47:14 +0000
+++ boots/lib/ui/plain.py 2010-06-02 04:01:24 +0000
@@ -68,6 +68,7 @@
68 self.prompt2 = console.config["prompt2"]68 self.prompt2 = console.config["prompt2"]
69 self.hist_file = console.config["history_file"]69 self.hist_file = console.config["history_file"]
70 self.pager_command = console.config["pager_command"]70 self.pager_command = console.config["pager_command"]
71 self.vertical_output = console.config["vertical_output"]
71 self.last_desc = None72 self.last_desc = None
72 self.buffer = []73 self.buffer = []
73 74
@@ -166,69 +167,16 @@
166 def present(self, result):167 def present(self, result):
167 """Print the result provided as an argument.168 """Print the result provided as an argument.
168169
169 If the result is a packet buffer the results until the last packet is170 If the result is a packet, buffer the results until the last packet is
170 received. At that point print all of the buffered result rows in table171 received. At that point print all of the buffered result rows in table
171 format. Non packet results are presented in their string representation172 format. Non packet results are presented in their string representation
172 as received."""173 as received."""
173 def padded(fields, widths):
174 """Utility function used to convert rows from tuples to table rows.
175 Results are returned as strings."""
176 padded_fields = []
177 for index, field in enumerate(fields):
178 padded_fields.append(field.ljust(widths[index]))
179 return u"| {0} |".format(u" | ".join(padded_fields))
180
181 def show_NULL(value):
182 """There is a 'bug' in the dbapi that does not convert NULL objects
183 to the string 'NULL'. This utility function performs that
184 conversion."""
185 return value if value is not None else "NULL"
186
187 def _gen_table(info):
188 with DurationCounter() as table_elapsed:
189 if self.buffer:
190 # If a column can contain the value NULL, we leave at least 4
191 # characters, since display_size does not account for NULL.
192 max_widths = map(max, [(len(column.name),
193 column.display_size,
194 4 if column.null_ok else 0)
195 for column in info["description"]])
196 dashes = map(lambda x: "-"*(x+2), max_widths)
197 sep_line = "+" + "+".join(dashes) + "+"
198 names = (column[0] for column in info["description"])
199 yield sep_line
200 yield padded(names, max_widths)
201 yield sep_line
202 for row in self.buffer:
203 yield padded(map(show_NULL, row), max_widths)
204
205 yield sep_line
206
207 if self.console.driver.is_interactive:
208 if info["description"] is not None:
209 set_count = n_("{count} row in set",
210 "{count} rows in set",
211 info["row_count"])
212 else:
213 set_count = n_("{count} row affected",
214 "{count} rows affected",
215 info["row_count"])
216
217 nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"]
218 timings = [_("{0:.2f}s server").format(info["server_elapsed"]),
219 _("+{0:.2f}s working").format(nodes_elapsed)]
220
221 if self.console.config["debug"]:
222 timings.append(_("{0:.2f}s table").format(table_elapsed.duration))
223
224 yield "{0} ({1})".format(set_count.format(count=info["row_count"]),
225 " | ".join(timings))
226174
227 if type(result) is Rows:175 if type(result) is Rows:
228 self.buffer.extend(result)176 self.buffer.extend(result)
229 elif isinstance(result, ResultInfo):177 elif isinstance(result, ResultInfo):
230 printed = False178 printed = False
231 output = _gen_table(result.value)179 output = self._gen_output(result.value)
232 if self.pager_command and self.console.driver.is_interactive:180 if self.pager_command and self.console.driver.is_interactive:
233 printed = self.print_with_pager("\n".join(output).encode("utf-8"))181 printed = self.print_with_pager("\n".join(output).encode("utf-8"))
234 182
@@ -237,7 +185,7 @@
237 for line in output:185 for line in output:
238 sys.stdout.write(line.encode("utf-8"))186 sys.stdout.write(line.encode("utf-8"))
239 sys.stdout.write("\n")187 sys.stdout.write("\n")
240 188
241 # Reset values for next result set.189 # Reset values for next result set.
242 self.buffer = []190 self.buffer = []
243 elif isinstance(result, Status):191 elif isinstance(result, Status):
@@ -258,6 +206,72 @@
258 else:206 else:
259 return True207 return True
260208
209 def _padded(self, fields, widths):
210 """Utility function used to convert rows from tuples to table rows.
211 Results are returned as strings."""
212 padded_fields = []
213 for index, field in enumerate(fields):
214 padded_fields.append(field.ljust(widths[index]))
215 return u"| {0} |".format(u" | ".join(padded_fields))
216
217 def _show_NULL(self, value):
218 """There is a 'bug' in the dbapi that does not convert NULL objects
219 to the string 'NULL'. This utility function performs that
220 conversion."""
221 return value if value is not None else "NULL"
222
223 def _gen_output(self, info):
224 with DurationCounter() as table_elapsed:
225 if self.buffer:
226 if self.vertical_output is False:
227 # If a column can contain the value NULL, we leave at least 4
228 # characters, since display_size does not account for NULL.
229 max_widths = map(max, [(len(column.name),
230 column.display_size,
231 4 if column.null_ok else 0)
232 for column in info["description"]])
233 dashes = map(lambda x: "-"*(x+2), max_widths)
234 sep_line = "+" + "+".join(dashes) + "+"
235 names = (column[0] for column in info["description"])
236 yield sep_line
237 yield self._padded(names, max_widths)
238 yield sep_line
239 for row in self.buffer:
240 yield self._padded(map(self._show_NULL, row), max_widths)
241
242 yield sep_line
243 else:
244 max_width = max([
245 len(column.name) for column in info["description"]])
246 for row_number, row in enumerate(self.buffer, start=1):
247 row_header = " Row {0} ".format(row_number)
248 yield row_header.center(78, '=')
249 for i, column in enumerate(info["description"]):
250 line = "{0}: {1}".format(
251 column.name.rjust(max_width), row[i])
252 yield line
253 yield ""
254
255 if self.console.driver.is_interactive:
256 if info["description"] is not None:
257 set_count = n_("{count} row in set",
258 "{count} rows in set",
259 info["row_count"])
260 else:
261 set_count = n_("{count} row affected",
262 "{count} rows affected",
263 info["row_count"])
264
265 nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"]
266 timings = [_("{0:.2f}s server").format(info["server_elapsed"]),
267 _("+{0:.2f}s working").format(nodes_elapsed)]
268
269 if self.console.config["debug"]:
270 timings.append(_("{0:.2f}s table").format(table_elapsed.duration))
271
272 yield "{0} ({1})".format(set_count.format(count=info["row_count"]),
273 " | ".join(timings))
274
261 @property275 @property
262 def is_interactive(self):276 def is_interactive(self):
263 if hasattr(os, "isatty"):277 if hasattr(os, "isatty"):
264278
=== modified file 'tests/boots/lib/ui/ui_tests.py'
--- tests/boots/lib/ui/ui_tests.py 2010-02-27 05:40:10 +0000
+++ tests/boots/lib/ui/ui_tests.py 2010-06-02 04:01:24 +0000
@@ -24,6 +24,10 @@
24import unittest24import unittest
25import boots_unit_test25import boots_unit_test
26from boots.lib.ui import generic, plain26from boots.lib.ui import generic, plain
27from boots.lib import console
28from boots.app import client_config
29from boots.api import api
30import StringIO
2731
28class TestGeneric(boots_unit_test.BootsBaseTester):32class TestGeneric(boots_unit_test.BootsBaseTester):
29 def setUp(self):33 def setUp(self):
@@ -33,14 +37,82 @@
33 pass37 pass
3438
35 def test_example(self):39 def test_example(self):
36 self.assert_(False)40 # TODO: test me!
41 pass
3742
38class TestPlain(boots_unit_test.BootsBaseTester):43class TestPlain(boots_unit_test.BootsBaseTester):
39 def setUp(self):44 def setUp(self):
40 pass45 self.config = client_config.ClientConfig()
46 self.console = console.Console(self.config)
47 self.plain_ui = plain.PlainUI(self.console)
4148
42 def tearDown(self):49 def tearDown(self):
43 pass50 pass
4451
45 def test_example(self):52 def test_show_NULL(self):
46 self.assert_(False)53 """_show_NULL() should return whatever it's given unless
54 it's given nothing/None, in which case it should return
55 the string 'NULL'."""
56 self.assertEqual(self.plain_ui._show_NULL(None), "NULL")
57 self.assertEqual(self.plain_ui._show_NULL(0), 0)
58 self.assertEqual(self.plain_ui._show_NULL("0"), "0")
59 self.assertEqual(self.plain_ui._show_NULL(1), 1)
60 self.assertEqual(self.plain_ui._show_NULL("1"), "1")
61 self.assertEqual(self.plain_ui._show_NULL("hello, world!"), "hello, world!")
62
63 def test_print_notificatilon(self):
64 """By default print_notification() writes a message to STDOUT
65 like the one below. For testing, we pass a third arg, a StringIO
66 to capture the output."""
67 output = StringIO.StringIO()
68 self.plain_ui.print_notification("title", "here's my message", output)
69 notif = output.getvalue()
70 output.close()
71 self.assertEqual(notif, "title :: here's my message\n")
72
73 def test_gen_output(self):
74 """_gen_output() outputs rows in either default table/horizontal
75 format or optional vertical (\G) format. It returns a generator
76 that yields output lines."""
77
78 # Add a row so _gen_output() has something to output.
79 row_desc = api.RowDescription(
80 (('Databases', 15, 3, 256, 256, 0, 0),))
81 rows = api.Rows( (('db1',),), row_desc)
82 self.plain_ui.present(rows)
83
84 # Result info for the row.
85 info = api.ResultInfo({
86 "description": row_desc,
87 "row_count": 1,
88 "begin_time": 1.0,
89 "end_time": 3.0,
90 "server_elapsed": 1.0,
91 })
92
93 # Test default table/horizontal output.
94 res = self.plain_ui._gen_output(info.value)
95 lines = []
96 for line in res:
97 lines.append(line)
98 self.assertEqual(lines, [
99 "+-----------+",
100 "| Databases |",
101 "+-----------+",
102 "| db1 |",
103 "+-----------+",
104 "1 row in set (1.00s server | +1.00s working)",
105 ])
106
107 # Test optional veritcal output.
108 self.plain_ui.vertical_output = True
109 res = self.plain_ui._gen_output(info.value)
110 lines = []
111 for line in res:
112 lines.append(line)
113 self.assertEqual(lines, [
114 "=================================== Row 1 ====================================",
115 "Databases: db1",
116 "",
117 "1 row in set (1.00s server | +1.00s working)",
118 ])

Subscribers

People subscribed via source and target branches

to status/vote changes: