chiark / gitweb /
Found in crybaby's working tree.
[chopwood] / output.py
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 import os as OS
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."""
81     me.warnings = []
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
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
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)
202   def warn(me, msg): me._fluid.target.warn(msg)
203
204   ## Delegating properties.
205   @property
206   def headerp(me): return me._fluid.target.headerp
207   @property
208   def warnings(me): return me._fluid.target.warnings
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 --------------------------------------------------