Merge lp:~cboylan/boots/boots_pager into lp:boots

Proposed by Clark Boylan
Status: Merged
Merged at revision: not available
Proposed branch: lp:~cboylan/boots/boots_pager
Merge into: lp:boots
Diff against target: 201 lines (+97/-28)
2 files modified
boots/app/client_config.py (+39/-2)
boots/lib/ui/plain.py (+58/-26)
To merge this branch: bzr merge lp:~cboylan/boots/boots_pager
Reviewer Review Type Date Requested Status
Max Goodhart Approve
Review via email: mp+20320@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Clark Boylan (cboylan) wrote :

First implementation of pager integration. I half expect this to require more work, but I am looking for comments on what the desired behaviors should be and if I should be doing this with a threaded model, etc.

A decent first step though.

lp:~cboylan/boots/boots_pager updated
97. By Clark Boylan

generate tablized output in a generator for the PlainUI.

98. By Clark Boylan

Much better version of pager integration.

99. By Clark Boylan

More pager tweaks, with auto interpretation in PlainUI.

100. By Clark Boylan

Merged in chromakode's pager auto config work and better error checking for pager integration.

Revision history for this message
Max Goodhart (chromakode) wrote :

Nice work!

<chromakode> in a perfect world, we could spool output to the pager immediately
 but for now, this is a decent first implementation
<clarkb> indeed

review: Approve

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-02-27 05:40:10 +0000
3+++ boots/app/client_config.py 2010-03-01 08:32:17 +0000
4@@ -26,9 +26,31 @@
5 options/arguments."""
6
7 import optparse
8-import os
9+import os, os.path
10 import info
11
12+def find_executable(name):
13+ # Search algorithm adapted from os._execvpe
14+ head, tail = os.path.split(name)
15+ if head and os.path.exists(name):
16+ return name
17+ else:
18+ if 'PATH' in os.environ:
19+ envpath = os.environ['PATH']
20+ else:
21+ envpath = os.defpath
22+ PATH = envpath.split(os.pathsep)
23+ for dir in PATH:
24+ fullname = os.path.join(dir, name)
25+ if os.path.exists(fullname):
26+ return fullname
27+
28+def get_auto_pager():
29+ if find_executable("less"):
30+ return "less -XF"
31+ elif find_executable("more"):
32+ return "more"
33+
34 class ClientConfig(object):
35 """Class used to read client configuration from files and the cli."""
36
37@@ -41,7 +63,7 @@
38 self._defaults = {"command": None,
39 "database": None,
40 "script": None,
41- 'lingo': 'sql',
42+ "lingo": "sql",
43 "rcfile": info.RCFILE,
44 "host": "localhost",
45 "port": 9306,
46@@ -49,6 +71,8 @@
47 "password": False,
48 "prompt1": "> ",
49 "prompt2": "+ ",
50+ "pager": None,
51+ "pager_command": None,
52 "terminating_char": ";",
53 "history_length": 100,
54 "history_file": os.path.expanduser("~/.boots_history")}
55@@ -109,6 +133,11 @@
56 type = "string",
57 dest = "password",
58 help = "Connect using password. If none is given, query for password")
59+ self._cli_parser.add_option("--pager",
60+ action = "store",
61+ type = "string",
62+ dest = "pager",
63+ help = "Pipe query results to the specified pager.")
64 self._cli_parser.add_option("-t", "--terminatingchar",
65 action = "store",
66 type = "string",
67@@ -154,6 +183,14 @@
68
69 self._dict.update(from_file)
70 self._dict.update(from_cli)
71+ self._interpret_options()
72+
73+ def _interpret_options(self):
74+ # Interpret options
75+ if self["pager"] == "auto":
76+ self["pager_command"] = get_auto_pager()
77+ elif self["pager"]:
78+ self["pager_command"] = self["pager"]
79
80 def get_file_conf(self, filepath):
81 """Read a configuration from the specified file. Return a dict."""
82
83=== modified file 'boots/lib/ui/plain.py'
84--- boots/lib/ui/plain.py 2010-02-28 06:24:11 +0000
85+++ boots/lib/ui/plain.py 2010-03-01 08:32:17 +0000
86@@ -28,6 +28,8 @@
87 import os
88 import time
89 import readline
90+import subprocess
91+import threading
92
93 from boots.api.nodes.node import NodeGraph, SyncNode
94 from boots.lib.ui.components.help import HelpTopic
95@@ -59,6 +61,7 @@
96 self.prompt2 = console.config["prompt2"]
97 self.hist_file = console.config["history_file"]
98 self.lingo = console.config["lingo"]
99+ self.pager_command = console.config["pager_command"]
100 self.last_desc = None
101 self.buffer = []
102
103@@ -123,6 +126,25 @@
104 self.prompt1 = prompt1
105 self.prompt2 = prompt2
106
107+ def print_with_pager(self, text):
108+ try:
109+ pager = subprocess.Popen(self.pager_command.split(),
110+ shell=False,
111+ stdin=subprocess.PIPE)
112+ except:
113+ sys.stdout.write("Unable to run pager command \"{0}\". Pager disabled.\n"
114+ .format(self.pager_command))
115+ self.pager_command = None
116+ return False
117+
118+ try:
119+ pager.communicate(text)
120+ except IOError:
121+ # IOError is raised sometimes with large outputs
122+ pass
123+
124+ return True
125+
126 def present(self, result):
127 """Print the result provided as an argument.
128
129@@ -140,36 +162,46 @@
130 to the string 'NULL'. This utility function performs that
131 conversion."""
132 return value if value is not None else "NULL"
133-
134+
135+ def _gen_table():
136+ max_widths = map(max, [(len(column[0]), column[2]) for column in self.last_desc])
137+ dashes = map(lambda x: "-"*(x+2), max_widths)
138+ sep_line = "+" + "+".join(dashes) + "+\n"
139+ names = (column[0] for column in self.last_desc)
140+ yield sep_line
141+ yield padded(names, max_widths)
142+ yield sep_line
143+ for row in self.buffer:
144+ yield padded(map(show_NULL, row), max_widths)
145+ current_time = time.time()
146+ info_line = "{0} rows in set ({1:.2f} seconds).\n".format(len(self.buffer),
147+ current_time - result["begin_time"])
148+ yield sep_line
149+ yield info_line
150+
151 if type(result) is dict and "__server_execute_sql_query" in result:
152 self.last_desc = result["info"]
153 if result["result"]:
154 self.buffer.extend(result["result"])
155- else:
156- if self.buffer:
157- max_widths = map(max, [(len(column[0]), column[2]) for column in self.last_desc])
158- dashes = map(lambda x: "-"*(x+2), max_widths)
159- sep_line = "+" + "+".join(dashes) + "+\n"
160- current_time = time.time()
161- info_line = "{0} rows in set ({1:.2f} seconds).\n".format(len(self.buffer),
162- current_time - result["begin_time"])
163- names = (column[0] for column in self.last_desc)
164- sys.stdout.write(sep_line)
165- sys.stdout.write(padded(names, max_widths))
166- sys.stdout.write(sep_line)
167- for row in self.buffer:
168- sys.stdout.write(padded(map(show_NULL, row), max_widths))
169- sys.stdout.write(sep_line)
170- sys.stdout.write(info_line)
171- # Reset values for next result set.
172- self.buffer = []
173- else:
174- if isinstance(result, Exception):
175- sys.stdout.write("ERROR ")
176- sys.stdout.write(" :: ".join(map(str, result.args)))
177- elif result is not None:
178- sys.stdout.write(str(result))
179- sys.stdout.write("\n")
180+ elif self.buffer:
181+ printed = False
182+ if self.pager_command and self.console.driver.is_interactive:
183+ printed = self.print_with_pager("".join(_gen_table()))
184+
185+ # Fallback if no pager set, or paging fails.
186+ if not printed:
187+ for row in _gen_table():
188+ sys.stdout.write(row)
189+
190+ # Reset values for next result set.
191+ self.buffer = []
192+ elif isinstance(result, Exception):
193+ sys.stdout.write("ERROR ")
194+ sys.stdout.write(" :: ".join(map(str, result.args)))
195+ sys.stdout.write("\n")
196+ elif result is not None:
197+ sys.stdout.write(str(result))
198+ sys.stdout.write("\n")
199
200 @property
201 def is_interactive(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: