chiark / gitweb /
service.py: Add missing `_describe' method for CommandRemoteService.
[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 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 --------------------------------------------------