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 | |
29 | from cStringIO import StringIO | |
30 | import sys as SYS | |
31 | ||
32 | import util as U | |
33 | ||
34 | ### There are a number of interesting things to do with output. | |
35 | ### | |
36 | ### The remote-service interface needs to prefix its output lines with `INFO' | |
37 | ### tokens so that they get parsed properly. | |
38 | ### | |
39 | ### The CGI interface needs to prefix its output with at least a | |
40 | ### `Content-Type' header. | |
41 | ||
42 | ###-------------------------------------------------------------------------- | |
43 | ### Utilities. | |
44 | ||
45 | def http_headers(**kw): | |
46 | """ | |
47 | Generate mostly-formatted HTTP headers. | |
48 | ||
49 | KW is a dictionary mapping HTTP header names to values. Each name is | |
50 | converted to external form by changing underscores `_' to hyphens `-', and | |
51 | capitalizing each constituent word. The values are converted to strings. | |
52 | If a value is a list, a header is produced for each element. Subsequent | |
53 | lines in values containing internal line breaks have a tab character | |
54 | prepended. | |
55 | """ | |
56 | def hack_header(k, v): | |
57 | return '%s: %s' % ('-'.join(i.title() for i in k.split('_')), | |
58 | str(v).replace('\n', '\n\t')) | |
59 | for k, v in kw.iteritems(): | |
60 | if isinstance(v, list): | |
61 | for i in v: yield hack_header(k, i) | |
62 | else: | |
63 | yield hack_header(k, v) | |
64 | ||
65 | ###-------------------------------------------------------------------------- | |
66 | ### Protocol. | |
67 | ||
68 | class BasicOutputDriver (object): | |
69 | """ | |
70 | A base class for output drivers, providing trivial implementations of most | |
71 | of the protocol. | |
72 | ||
73 | The main missing piece is the `_write' method, which should write its | |
74 | argument to the output with as little ceremony as possible. Any fancy | |
75 | formatting should be applied by overriding `write'. | |
76 | """ | |
77 | ||
78 | def __init__(me): | |
79 | """Trivial constructor.""" | |
80 | pass | |
81 | ||
82 | def writeln(me, msg): | |
83 | """Write MSG, as a complete line.""" | |
84 | me.write(str(msg) + '\n') | |
85 | ||
86 | def write(me, msg): | |
87 | """Write MSG to the output, with any necessary decoration.""" | |
88 | me._write(str(msg)) | |
89 | ||
90 | def close(me): | |
91 | """Wrap up when everything that needs saying has been said.""" | |
92 | pass | |
93 | ||
94 | def header(me, **kw): | |
95 | """Emit HTTP-style headers in a distinctive way.""" | |
96 | for h in http_headers(**kw): | |
97 | PRINT('[%s]' % h) | |
98 | ||
99 | class BasicLineOutputDriver (BasicOutputDriver): | |
100 | """ | |
101 | Mixin class for line-oriented output formatting. | |
102 | ||
103 | We override `write' to buffer partial lines; complete lines are passed to | |
104 | `_writeln' to be written, presumably through the low-level `_write' method. | |
105 | """ | |
106 | ||
107 | def __init__(me, *args, **kw): | |
108 | """Contructor.""" | |
109 | super(BasicLineOutputDriver, me).__init__(*args, **kw) | |
110 | me._buf = None | |
111 | ||
112 | def _flush(me): | |
113 | """Write any incomplete line accumulated so far, and clear the buffer.""" | |
114 | if me._buf: | |
115 | me._writeln(me._buf.getvalue()) | |
116 | me._buf = None | |
117 | ||
118 | def write(me, msg): | |
119 | """Write MSG, sending any complete lines to the `_writeln' method.""" | |
120 | ||
121 | if '\n' not in msg: | |
122 | ## If there's not a complete line here then we just accumulate the | |
123 | ## message into our buffer. | |
124 | ||
125 | if not me._buf: me._buf = StringIO() | |
126 | me._buf.write(msg) | |
127 | ||
128 | else: | |
129 | ## There's at least one complete line here. We take the final | |
130 | ## incomplete line off the end. | |
131 | ||
132 | lines = msg.split('\n') | |
133 | tail = lines.pop() | |
134 | ||
135 | ## If there's a partial line already buffered then add whatever new | |
136 | ## stuff we have and flush it out. | |
137 | if me._buf: | |
138 | me._buf.write(lines[0]) | |
139 | me._flush() | |
140 | ||
141 | ## Write out any other complete lines. | |
142 | for line in lines: | |
143 | me._writeln(line) | |
144 | ||
145 | ## If there's a proper partial line, then start a new buffer. | |
146 | if tail: | |
147 | me._buf = StringIO() | |
148 | me._buf.write(tail) | |
149 | ||
150 | def close(me): | |
151 | """If there's any partial line buffered, flush it out.""" | |
152 | me._flush() | |
153 | ||
154 | ###-------------------------------------------------------------------------- | |
155 | ### Implementations. | |
156 | ||
157 | class FileOutput (BasicOutputDriver): | |
158 | """Output driver for writing stuff to a file.""" | |
159 | def __init__(me, file = SYS.stdout, *args, **kw): | |
160 | """Constructor: send output to FILE (default is stdout).""" | |
161 | super(FileOutput, me).__init__(*args, **kw) | |
162 | me._file = file | |
163 | def _write(me, text): | |
164 | """Output protocol: write TEXT to the ouptut file.""" | |
165 | me._file.write(text) | |
166 | ||
167 | class RemoteOutput (FileOutput, BasicLineOutputDriver): | |
168 | """Output driver for decorating lines with `INFO' tags.""" | |
169 | def _writeln(me, line): | |
170 | """Line output protocol: write a complete line with an `INFO' tag.""" | |
171 | me._write('INFO %s\n' % line) | |
172 | ||
173 | ###-------------------------------------------------------------------------- | |
174 | ### Context. | |
175 | ||
176 | class DelegatingOutput (BasicOutputDriver): | |
177 | """Fake output driver which delegates to some other driver.""" | |
178 | ||
179 | def __init__(me, default = None): | |
180 | """Constructor: send output to DEFAULT.""" | |
181 | me._fluid = U.Fluid(target = default) | |
182 | ||
183 | @CTX.contextmanager | |
184 | def redirect_to(me, target): | |
185 | """Temporarily redirect output to TARGET, closing it when finished.""" | |
186 | try: | |
187 | with me._fluid.bind(target = target): | |
188 | yield | |
189 | finally: | |
190 | target.close() | |
191 | ||
192 | ## Delegating methods. | |
193 | def write(me, msg): me._fluid.target.write(msg) | |
194 | def writeln(me, msg): me._fluid.target.writeln(msg) | |
195 | def close(me): me._fluid.target.close() | |
196 | def header(me, **kw): me._fluid.target.header(**kw) | |
197 | ||
198 | ## Delegating properties. | |
199 | @property | |
200 | def headerp(me): return me._fluid.target.headerp | |
201 | ||
202 | ## The selected output driver. Set this with `output_to'. | |
203 | OUT = DelegatingOutput() | |
204 | ||
205 | def PRINT(msg = ''): | |
206 | """Write the MSG as a line to the current output.""" | |
207 | OUT.writeln(msg) | |
208 | ||
209 | ###----- That's all, folks -------------------------------------------------- |