chiark / gitweb /
agpl.py: Python 2.5 compatibility.
[chopwood] / output.py
CommitLineData
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
26from __future__ import with_statement
27
28import contextlib as CTX
5b7c6334 29import os as OS
a2916c06
MW
30from cStringIO import StringIO
31import sys as SYS
32
33import 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
46def 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
69class 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
104class 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
162class 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
172class 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
181class 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'.
211OUT = DelegatingOutput()
212
213def PRINT(msg = ''):
214 """Write the MSG as a line to the current output."""
215 OUT.writeln(msg)
216
217###----- That's all, folks --------------------------------------------------