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

Subscribers

People subscribed via source and target branches

to status/vote changes: