chiark / gitweb /
service.py: Yet more unqualified names needing qualification.
[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
29from cStringIO import StringIO
30import sys as SYS
31
32import 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
45def 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
68class 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
99class 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
157class 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
167class 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
176class 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'.
203OUT = DelegatingOutput()
204
205def PRINT(msg = ''):
206 """Write the MSG as a line to the current output."""
207 OUT.writeln(msg)
208
209###----- That's all, folks --------------------------------------------------