Commit | Line | Data |
---|---|---|
a2916c06 MW |
1 | ### -*-python-*- |
2 | ### | |
3 | ### Output machinery | |
4 | ### | |
5 | ### (c) 2013 Mark Wooding | |
6 | ### | |
7 | ||
8 | ###----- Licensing notice --------------------------------------------------- | |
9 | ### | |
10 | ### This file is part of Chopwood: a password-changing service. | |
11 | ### | |
12 | ### Chopwood is free software; you can redistribute it and/or modify | |
13 | ### it under the terms of the GNU Affero General Public License as | |
14 | ### published by the Free Software Foundation; either version 3 of the | |
15 | ### License, or (at your option) any later version. | |
16 | ### | |
17 | ### Chopwood is distributed in the hope that it will be useful, | |
18 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
19 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
20 | ### GNU Affero General Public License for more details. | |
21 | ### | |
22 | ### You should have received a copy of the GNU Affero General Public | |
23 | ### License along with Chopwood; if not, see | |
24 | ### <http://www.gnu.org/licenses/>. | |
25 | ||
26 | from __future__ import with_statement | |
27 | ||
28 | import contextlib as CTX | |
5b7c6334 | 29 | import os as OS |
a2916c06 MW |
30 | from cStringIO import StringIO |
31 | import sys as SYS | |
32 | ||
33 | import util as U | |
34 | ||
35 | ### There are a number of interesting things to do with output. | |
36 | ### | |
37 | ### The remote-service interface needs to prefix its output lines with `INFO' | |
38 | ### tokens so that they get parsed properly. | |
39 | ### | |
40 | ### The CGI interface needs to prefix its output with at least a | |
41 | ### `Content-Type' header. | |
42 | ||
43 | ###-------------------------------------------------------------------------- | |
44 | ### Utilities. | |
45 | ||
46 | def http_headers(**kw): | |
47 | """ | |
48 | Generate mostly-formatted HTTP headers. | |
49 | ||
50 | KW is a dictionary mapping HTTP header names to values. Each name is | |
51 | converted to external form by changing underscores `_' to hyphens `-', and | |
52 | capitalizing each constituent word. The values are converted to strings. | |
53 | If a value is a list, a header is produced for each element. Subsequent | |
54 | lines in values containing internal line breaks have a tab character | |
55 | prepended. | |
56 | """ | |
57 | def hack_header(k, v): | |
58 | return '%s: %s' % ('-'.join(i.title() for i in k.split('_')), | |
59 | str(v).replace('\n', '\n\t')) | |
60 | for k, v in kw.iteritems(): | |
61 | if isinstance(v, list): | |
62 | for i in v: yield hack_header(k, i) | |
63 | else: | |
64 | yield hack_header(k, v) | |
65 | ||
66 | ###-------------------------------------------------------------------------- | |
67 | ### Protocol. | |
68 | ||
69 | class BasicOutputDriver (object): | |
70 | """ | |
71 | A base class for output drivers, providing trivial implementations of most | |
72 | of the protocol. | |
73 | ||
74 | The main missing piece is the `_write' method, which should write its | |
75 | argument to the output with as little ceremony as possible. Any fancy | |
76 | formatting should be applied by overriding `write'. | |
77 | """ | |
78 | ||
79 | def __init__(me): | |
80 | """Trivial constructor.""" | |
5b7c6334 | 81 | me.warnings = [] |
a2916c06 MW |
82 | |
83 | def writeln(me, msg): | |
84 | """Write MSG, as a complete line.""" | |
85 | me.write(str(msg) + '\n') | |
86 | ||
87 | def write(me, msg): | |
88 | """Write MSG to the output, with any necessary decoration.""" | |
89 | me._write(str(msg)) | |
90 | ||
5b7c6334 MW |
91 | def warn(me, msg): |
92 | """Write MSG as a warning message.""" | |
93 | SYS.stderr.write('%s: %s\n' % (OS.path.basename(SYS.argv[0]), msg)) | |
94 | ||
a2916c06 MW |
95 | def close(me): |
96 | """Wrap up when everything that needs saying has been said.""" | |
97 | pass | |
98 | ||
99 | def header(me, **kw): | |
100 | """Emit HTTP-style headers in a distinctive way.""" | |
101 | for h in http_headers(**kw): | |
102 | PRINT('[%s]' % h) | |
103 | ||
104 | class BasicLineOutputDriver (BasicOutputDriver): | |
105 | """ | |
106 | Mixin class for line-oriented output formatting. | |
107 | ||
108 | We override `write' to buffer partial lines; complete lines are passed to | |
109 | `_writeln' to be written, presumably through the low-level `_write' method. | |
110 | """ | |
111 | ||
112 | def __init__(me, *args, **kw): | |
113 | """Contructor.""" | |
114 | super(BasicLineOutputDriver, me).__init__(*args, **kw) | |
115 | me._buf = None | |
116 | ||
117 | def _flush(me): | |
118 | """Write any incomplete line accumulated so far, and clear the buffer.""" | |
119 | if me._buf: | |
120 | me._writeln(me._buf.getvalue()) | |
121 | me._buf = None | |
122 | ||
123 | def write(me, msg): | |
124 | """Write MSG, sending any complete lines to the `_writeln' method.""" | |
125 | ||
126 | if '\n' not in msg: | |
127 | ## If there's not a complete line here then we just accumulate the | |
128 | ## message into our buffer. | |
129 | ||
130 | if not me._buf: me._buf = StringIO() | |
131 | me._buf.write(msg) | |
132 | ||
133 | else: | |
134 | ## There's at least one complete line here. We take the final | |
135 | ## incomplete line off the end. | |
136 | ||
137 | lines = msg.split('\n') | |
138 | tail = lines.pop() | |
139 | ||
140 | ## If there's a partial line already buffered then add whatever new | |
141 | ## stuff we have and flush it out. | |
142 | if me._buf: | |
143 | me._buf.write(lines[0]) | |
144 | me._flush() | |
145 | ||
146 | ## Write out any other complete lines. | |
147 | for line in lines: | |
148 | me._writeln(line) | |
149 | ||
150 | ## If there's a proper partial line, then start a new buffer. | |
151 | if tail: | |
152 | me._buf = StringIO() | |
153 | me._buf.write(tail) | |
154 | ||
155 | def close(me): | |
156 | """If there's any partial line buffered, flush it out.""" | |
157 | me._flush() | |
158 | ||
159 | ###-------------------------------------------------------------------------- | |
160 | ### Implementations. | |
161 | ||
162 | class FileOutput (BasicOutputDriver): | |
163 | """Output driver for writing stuff to a file.""" | |
164 | def __init__(me, file = SYS.stdout, *args, **kw): | |
165 | """Constructor: send output to FILE (default is stdout).""" | |
166 | super(FileOutput, me).__init__(*args, **kw) | |
167 | me._file = file | |
168 | def _write(me, text): | |
169 | """Output protocol: write TEXT to the ouptut file.""" | |
170 | me._file.write(text) | |
171 | ||
172 | class RemoteOutput (FileOutput, BasicLineOutputDriver): | |
173 | """Output driver for decorating lines with `INFO' tags.""" | |
174 | def _writeln(me, line): | |
175 | """Line output protocol: write a complete line with an `INFO' tag.""" | |
176 | me._write('INFO %s\n' % line) | |
177 | ||
178 | ###-------------------------------------------------------------------------- | |
179 | ### Context. | |
180 | ||
181 | class DelegatingOutput (BasicOutputDriver): | |
182 | """Fake output driver which delegates to some other driver.""" | |
183 | ||
184 | def __init__(me, default = None): | |
185 | """Constructor: send output to DEFAULT.""" | |
186 | me._fluid = U.Fluid(target = default) | |
187 | ||
188 | @CTX.contextmanager | |
189 | def redirect_to(me, target): | |
190 | """Temporarily redirect output to TARGET, closing it when finished.""" | |
191 | try: | |
192 | with me._fluid.bind(target = target): | |
193 | yield | |
194 | finally: | |
195 | target.close() | |
196 | ||
197 | ## Delegating methods. | |
198 | def write(me, msg): me._fluid.target.write(msg) | |
199 | def writeln(me, msg): me._fluid.target.writeln(msg) | |
200 | def close(me): me._fluid.target.close() | |
201 | def header(me, **kw): me._fluid.target.header(**kw) | |
5b7c6334 | 202 | def warn(me, msg): me._fluid.target.warn(msg) |
a2916c06 MW |
203 | |
204 | ## Delegating properties. | |
205 | @property | |
206 | def headerp(me): return me._fluid.target.headerp | |
5b7c6334 MW |
207 | @property |
208 | def warnings(me): return me._fluid.target.warnings | |
a2916c06 MW |
209 | |
210 | ## The selected output driver. Set this with `output_to'. | |
211 | OUT = DelegatingOutput() | |
212 | ||
213 | def PRINT(msg = ''): | |
214 | """Write the MSG as a line to the current output.""" | |
215 | OUT.writeln(msg) | |
216 | ||
217 | ###----- That's all, folks -------------------------------------------------- |