i first thought on focusing on the socket module, because it's the part that<br>bothers me most, but since people have expressed their thoughts on completely<br>revamping the IO stack, perhaps we should be open to adopting new ideas,
<br>mainly from the java/.NET world (keeping the momentum from the previous post).<br><br>there is an inevitable issue of performance here, since it basically splits <br>what used to be &quot;file&quot; or &quot;socket&quot; into many layers... each adding additional 
<br>overhead, so many parts should be lowered to C.<br><br>if we look at java/.NET for guidance, they have come up with two concepts:<br>* stream - an arbitrary, usually sequential, byte data source<br>* readers and writers - the way data is encoded into/decoded from the stream.
<br>we'll use the term &quot;codec&quot; for these readers and writers in general.<br><br>so &quot;stream&quot; is the &quot;where&quot; and &quot;codec&quot; is the &quot;how&quot;, and the concept of <br>codecs is not limited to ASCII vs UTF-8. it can grow into fully-fledged 
<br>protocols.<br><br><br><br><br>- - - - - - -<br>Streams<br>- - - - - - -<br><br>streams provide an interface to data sources, like memory, files, pipes, or<br>sockets. the basic interface of all of these is<br><br>class Stream:
<br>&nbsp;&nbsp;&nbsp; def close(self)<br>&nbsp;&nbsp;&nbsp; def read(self, count)<br>&nbsp;&nbsp;&nbsp; def readall(self)<br>&nbsp;&nbsp;&nbsp; def write(self, data)<br><br>and unlike today's files and sockets, when you read from a broken socket or<br>past the end of the file, you get EOFError.
<br><br>read(x) guarantees to return x bytes, or EOFError otherwise (and also restoing<br>the stream position). on the other hand, readall() makes no such guarantee: it<br>reads all the data up to EOF, and if you readall() from EOF, you get &quot;&quot;.
<br><br>perhaps readall() should return all *available* data, not necessarily up to <br>EOF. for files, this is equivalent, but for sockets, readall would return all <br>the data that sits in the network stack. this could be a nice way to do 
<br>non-blocking IO.<br><br>and if we do that already, perhaps we should introduce async operations as a<br>built-in feature? .NET does (BeginRead, EndRead, etc.)<br>&nbsp;&nbsp; &nbsp;def async_read(self, count, callback)<br>&nbsp;&nbsp; &nbsp;def async_write(self, data, callback)
<br><br>i'm not sure about these two, but it does seem like a good path to follow.<br><br>-----<br><br>another issue is the current class hierarchy: fileno, seek, and readline are<br>meaningless in many situations, yet they are considered the part of the file-
<br>protocol (take a look at StringIO implementing isatty!). <br><br>these methods, which may be meaningless for several types of streams, must <br>not be part of the base Stream class.<br><br>for example, only FileStream and MemoryStream are seekable, so why have seek
<br>as part of the base Stream class?<br><br>-----<br><br>streams that don't rely on an operating-system resource, would derive directly<br>from Stream. as examples for such streams, we can condier <br><br>class MemoryStream(Stream):
<br>&nbsp;&nbsp;&nbsp; # like today's StringIO<br>&nbsp;&nbsp;&nbsp; # allows seeking<br><br>class RandomStream(Stream):<br>&nbsp;&nbsp;&nbsp; # provider of random data<br><br>-----<br><br>on the other hand, streams that rely on operating-system resources, like files 
<br>or sockets, would derive from<br><br>class OSStream(Stream):<br>&nbsp;&nbsp;&nbsp; def isatty(self)<br>&nbsp;&nbsp;&nbsp; def fileno(self) # for select()<br>&nbsp;&nbsp;&nbsp; def dup(self)<br><br>and there are several examples for this kind:<br><br>FileStream is the entity that works with files, instead of the file/open
<br>class of today. since files provide random-access (seek/tell), this kind of <br>stream is &quot;seekable&quot; and &quot;tellable&quot;.<br><br>class FileStream(OSStream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, filename, mode = &quot;r&quot;)
<br>&nbsp;&nbsp;&nbsp; def seek(self, pos, offset = None)<br>&nbsp;&nbsp;&nbsp; def tell(self)<br>&nbsp;&nbsp;&nbsp; def set_size(self, size)<br>&nbsp;&nbsp;&nbsp; def get_size(self)<br><br>although i prefer properties instead<br>&nbsp;&nbsp;&nbsp; position = property(tell, seek)<br>&nbsp;&nbsp;&nbsp; size = property(get_size, set_size)
<br><br>PipeStream represents a stream over a (simplex) pipe:<br><br>class PipeStream(OSStream):<br>&nbsp;&nbsp;&nbsp; def get_mode(self) # read or write<br><br>DuplexPipeStream is an abstraction layer that uses two simplex pipes<br>as a full-duplex stream:
<br><br>class DuplexPipeStream(OSStream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, incoming, outgoing):<br><br>&nbsp;&nbsp;&nbsp; @classmethod<br>&nbsp;&nbsp;&nbsp; def open(cls):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; incoming, outgoing = os.pipe()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return cls(incoming, outgoing)<br>
<br>NetworkStreams provide a stream over a socket. unlike files, sockets may<br>get quite complicated (options, accept, bind), so we keep the distinction:<br>* sockets as the underlying &quot;physical resource&quot;<br>* NetworkStreams wrap them with a nice stream interface. for example, while
<br>socket.recv(x) may return less than x bytes, networkstream.read(x) returns x<br>bytes.<br><br>we must keep this distinction because streams are *data sources*, and there's<br>no way to represent things like bind or accept in a data source. only client 
<br>(connected) sockets would be wrappable by NetworkStream. server sockets don't<br>provide data and hence have nothing to do with streams.<br><br>class NetworkStream(OSStream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, sock)<br><br><br>
<br><br>- - - - - - - - -<br>Special Streams<br>- - - - - - - - -<br><br>it will also be useful to have a way to duplicate a stream, like the unix tee<br>command does<br><br>class TeeStream(Stream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, src_stream, dst_stream)
<br><br>f1 = FileStream(&quot;c:\\blah&quot;)<br>f2 = FileStream(&quot;c:\\yaddah&quot;)<br>f1 = TeeStream(f1, f2)<br><br>f1.write(&quot;hello&quot;) <br><br>will write &quot;hello&quot; to f2 as well. that's useful for monitoring/debugging,
<br>like echoing everything from a NetworkStream to a file, so you could debug<br>it easily.<br><br>-----<br><br>buffering is always *explicit* and implemented at the interpreter level, <br>rather than by libc, so it is consistent between all platforms and streams. 
<br>all streams, by nature, and *non-buffered* (write the data as soon as <br>possible). buffering wraps an underlying stream, making it explicit<br><br>class BufferedStream(Stream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream, bufsize)
<br>&nbsp;&nbsp;&nbsp; def flush(self)<br>&nbsp;&nbsp; &nbsp;<br>(BufferedStream appears in .NET)<br><br>class LineBufferedStream(BufferedStream):<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream, flush_on = b&quot;\n&quot;)<br>&nbsp;&nbsp; &nbsp;<br>f = LineBufferedStream(FileStream(&quot;c:\\blah&quot;))
<br><br>where flush_on specifies the byte (or sequence of bytes?) to flush upon<br>writing. by default it would be on newline.<br><br><br><br><br><br>- - - - - - -<br>Codecs<br>- - - - - - -<br><br>as was said earlier, formatting defines how the data (or arbitrary objects) are
<br>to be encoded into and decoded from a stream. <br><br>class StreamCodec:<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream)<br>&nbsp;&nbsp;&nbsp; def write(self, ...)<br>&nbsp;&nbsp;&nbsp; def read(self, ...)<br><br>for example, in order to serialize binary records into a file, you would use 
<br><br>class StructCodec(StreamCodec):<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream, format):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Codec.__init__(self, stream)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.format = format<br>&nbsp;&nbsp;&nbsp; def write(self, *args):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.stream.write(struct.pack
(self.format, *args))<br>&nbsp;&nbsp;&nbsp; def read(self):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; size = struct.calcsize(self.format)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; data = self.stream.read(size)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return struct.unpack(self.format, data)<br><br>(similar to BinaryReader/BinaryWriter in .NET)
<br><br>and for working with text, you would have<br><br>class TextCodec(StreamCodec):<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream, textcodec = &quot;utf-8&quot;):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Codec.__init__(self, stream)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.textcodec = textcodec
<br>&nbsp;&nbsp;&nbsp; def write(self, data):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.stream.write(data.encode(self.textcodec))<br>&nbsp;&nbsp;&nbsp; def read(self, length):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return self.stream.read(length).decode(self.textcodec)<br><br>&nbsp;&nbsp;&nbsp; def __iter__(self) # iter by lines
<br>&nbsp;&nbsp;&nbsp; def readline(self) # read the next line<br>&nbsp;&nbsp;&nbsp; def writeline(self, data) # write a line<br><br>as you can see, only the TextCodec adds the readline/writeline methods, as <br>they are meaningless to most binary formats. the stream itself has no notion 
<br>of a line. <br><br>&lt;big drum roll&gt; no more newline issues! &lt;/big drum roll&gt;<br><br>the TextCodec will do the translation for you. all newlines are \n in python,<br>and are written to the underlying stream in a way that would please the 
<br>underlying platform.<br><br>so the &quot;rb&quot; and &quot;wb&quot; file modes will deminish, and instead you would wrap the<br>FileStream with a TextCodec. it's explicit, so you won't be able to corrupt<br>data accidentally.
<br><br>-----<br><br>it's worth to note that in .NET (and perhaps java as well), they splitted <br>TextCodec into two parts, the TextReader and TextWriter classes, which you <br>initialize over a stream:<br><br>f = new FileStream(&quot;c:\\blah&quot;);
<br>sr = new StreamReader(f, Encoding.UTF8);<br>sw = new StreamWriter(f, Encoding.UTF8);<br>sw.Write(&quot;hello&quot;);<br>f.Position = 0;<br>sr.read(5);<br><br>but why separate the two? it could only cause problems, as you may initialize 
<br>them with different encodings, which leads to no good. under the guidelines <br>of this suggestion, it would be implemented this way:<br><br>f = TextCodec(FileStream(&quot;c:\\blah&quot;), &quot;utf-8&quot;)<br><br>which can of course be refactored to a function:
<br><br>def textfile(filename, mode = &quot;r&quot;, codec = &quot;utf-8&quot;):<br>&nbsp;&nbsp;&nbsp; return TextCodec(FileStream(filename, mode), codec)<br><br>for line in textfile(&quot;c:\\blah&quot;):<br>&nbsp;&nbsp;&nbsp; print line<br><br>unlike today's file objects, FileStream objects don't know about lines, so 
<br>you can't iterate through a file directly. it's quite logical if you think <br>about it, as there's no meaning to iterating over a binary file by lines.<br>it's a feature of text files.<br><br>-----<br><br>many times, especially in network protocols, you need framing for transfering 
<br>frames/packets/messages over a stream. so a very useful FramingCodec can be <br>introduced:<br><br>class FramingCodec(Codec):<br>&nbsp;&nbsp;&nbsp; def write(self, data):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.stream.write(struct.pack(&quot;&lt;L&quot;, len(data)))
<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.stream.write(data)<br>&nbsp;&nbsp;&nbsp; def read(self):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; length, = struct.unpack(&quot;&lt;L&quot;, self.stream.read(4))<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return self.stream.read(length)<br><br>once you set up such a connection, you are free of socket hassle:
<br><br>conn = FramingCodec(NetworkStream(TcpClientSocket(&quot;host&quot;, 1234)))<br>conn.write(&quot;hello&quot;)<br>reply = conn.read() <br><br>and it can be extended by subclassing, for instance, to allow serializing 
<br>streams: you can write objects directly to the stream and get them on the <br>other side with ease:<br><br>class SeralizingCodec(FramingCodec):<br>&nbsp;&nbsp;&nbsp; def write(self, obj):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FramingCodec.write(self, pickle.dumps
(obj))<br>&nbsp;&nbsp;&nbsp; def read(self):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return pickle.loads(FramingCodec.read(self))<br><br>conn = SeralizingCodec(NetworkStream(TcpClientSocket(&quot;host&quot;, 1234)))<br>conn.send([1,2,{3:4}])<br>person = conn.recv()
<br>print person.first_name<br><br>and it can serve as the basis for RPC protocols or as a simple way to transfer<br>arbitrary objects (for example, database query results from a server, etc.)<br><br>and since the codecs don't care what the underlying stream is, it can be a 
<br>FileStream as well, serializing objects to disk.<br><br>-----<br><br>many protocols can also be represented as codecs. textual protocols, like <br>HTTP or SMTP, can be easily implemented that way:<br><br>class HttpClientCodec( *TextCodec* ):
<br>&nbsp;&nbsp;&nbsp; def __init__(self, stream):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TextCodec.__init__(self, stream, textcodec = &quot;ascii&quot;)<br>&nbsp;&nbsp; &nbsp;<br>&nbsp;&nbsp;&nbsp; def write(self, request, params, data = &quot;&quot;):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.writeline(&quot;%s %s&quot; % (request, params))
<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.writeline()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if data:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.writeline(data)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;<br>&nbsp;&nbsp;&nbsp; def read(self):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return response, header, data<br>&nbsp;&nbsp; &nbsp;<br>&nbsp;&nbsp;&nbsp; def do_get(filename):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
self.write(&quot;GET&quot;, filename)<br><br>&nbsp;&nbsp;&nbsp; def do_post(filename, data):<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.write(&quot;POST&quot;, filename, data)<br><br>class HttpServerCodec(TextCodec):<br>&nbsp;&nbsp;&nbsp; ....<br><br>and then an http-server becomes rather simple:
<br><br># client<br>conn = HttpClientCodec(NetworkStream(TcpClientSocket(&quot;host&quot;, 8080)))<br>conn.do_get(&quot;/index.html&quot;)<br>response, header, data = conn.recv()<br>if response == &quot;200&quot;:<br>&nbsp;&nbsp;&nbsp; print data
<br><br># server<br>s = TcpServerSocket((&quot;&quot;, 8080))<br>client_sock = s.accept()<br>conn = HttpServerCodec(NetworkStream(client_sock))<br>request, params, data = conn.read()<br><br>if request == &quot;GET&quot;:<br>
&nbsp;&nbsp;&nbsp; ...<br><br>you can write something like urllib in no-time.<br><br>-----<br><br>it's worth to note that codecs are &quot;stackable&quot;, so you can chain them, thus<br>creating more complex codecs, for instance:<br><br>
https_conn = HttpClientCodec(SslCodec(NetworkStream(...)))<br><br>and other crazy stuff can follow: imaging doing SSL authentication over pipes,<br>between two processes. why only sockets? yeah, it's crazy, but why not?<br>
<br><br><br><br><br>- - - - - - -<br>Summary<br>- - - - - - -<br><br>to conclude this long post, streams are generic data providers (random, files,<br>sockets, in-memory), and codecs provide an abstraction layer over streams, 
<br>allowing sophisticated use cases (text, binary records, framing, and even <br>full protocols). <br><br>i've implemented some of these ideas in RPyC ( <a href="http://rpyc.wikispaces.com">http://rpyc.wikispaces.com</a>
 ),<br>in the Stream and Channel modules (i needed a uniform way of working with <br>pipes and sockets). of course i didn't go rewriting the whole io stack there, <br>but it shows real-life usage of this model.<br><br><br>
<br>-tomer<br>