[Checkins] SVN: zc.ngi/trunk/ Added a (hysterically poorly named) writelines method to allow

Jim Fulton jim at zope.com
Sun Jan 7 09:21:49 EST 2007


Log message for revision 71766:
  Added a (hysterically poorly named) writelines method to allow
  iteratibles to be passed to connections for output.
  
  Added a new ConnectionHandler method to allow an implementation to
  report errors to an application asynchronously. The main use case for
  this is in handling errors from iterators passed to writelines.
  
  Added a blocking I/O API to simplify writing some kinds of
  applications.
  
  Finished transition to rule that setHandler can only be called from
  callbacks by eliminating some unnecessary locking. (The rules for
  allowable callbacks need to be fleshed out a little bit more to deal
  with the special case of connections created without a callback, for
  example using blocking.connect.
  

Changed:
  U   zc.ngi/trunk/src/zc/ngi/README.txt
  U   zc.ngi/trunk/src/zc/ngi/async.py
  U   zc.ngi/trunk/src/zc/ngi/async.txt
  A   zc.ngi/trunk/src/zc/ngi/blocking.py
  A   zc.ngi/trunk/src/zc/ngi/blocking.txt
  U   zc.ngi/trunk/src/zc/ngi/interfaces.py
  D   zc.ngi/trunk/src/zc/ngi/notes.txt
  U   zc.ngi/trunk/src/zc/ngi/testing.py
  U   zc.ngi/trunk/src/zc/ngi/tests.py
  U   zc.ngi/trunk/src/zc/ngi/wordcount.py
  U   zc.ngi/trunk/todo.txt

-=-
Modified: zc.ngi/trunk/src/zc/ngi/README.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/README.txt	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/README.txt	2007-01-07 14:21:48 UTC (rev 71766)
@@ -103,10 +103,13 @@
     ...     def handle_close(self, connection, reason):
     ...         print 'closed:', reason
     ...         if self.strings:
-    ...             print 'closed prematurely'    
+    ...             print 'closed prematurely'
+    ...
+    ...     def handle_exception(self, connection, exception):
+    ...         print 'exception:', exception.__class__.__name__, exception
 
 
-The client implements the IClientConnectHandler and IInputHandler
+The client implements the IClientConnectHandler and IConnectionHandler
 interfaces.  More complex clients might implement these interfaces with
 separate classes.
 
@@ -178,8 +181,32 @@
 
     >>> bool(connection2)
     False
-    
 
+Passing iterables to connections
+================================
+
+The writelines method of IConnection accepts iterables of strings.
+
+    >>> def greet():
+    ...     yield 'hello\n'
+    ...     yield 'world\n'
+
+    >>> zc.ngi.testing.Connection().writelines(greet())
+    -> 'hello\n'
+    -> 'world\n'
+
+If there is an error in your iterator, or if the iterator returns
+a non-string value, an exception will be reported using
+handle_exception:
+
+    >>> def bad():
+    ...     yield 2
+    >>> connection = zc.ngi.testing.Connection()
+    >>> connection.setHandler(zc.ngi.testing.PrintingHandler(connection))
+    >>> connection.writelines(bad())
+    -> EXCEPTION TypeError Got a non-string result from iterable
+
+
 Implementing network servers
 ============================
 

Modified: zc.ngi/trunk/src/zc/ngi/async.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/async.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -77,8 +77,8 @@
         self.__connected = True
         self.__closed = None
         self.__handler = None
+        self.__exception = None
         self.__output = []
-        self.__handler_lock = threading.Lock()
         dispatcher.__init__(self, sock, addr)
         self.logger = logger
 
@@ -89,22 +89,31 @@
         if self.__handler is not None:
             raise TypeError("Handler already set")
 
-        self.__handler_lock.acquire()
-        try:
-            self.__handler = handler
-            if self.__closed:
-                handler.handle_close(seld, self.__closed)
-        finally:
-            self.__handler_lock.release()
+        self.__handler = handler
+        if self.__exception:
+            exception = self.__exception
+            self.__exception = None
+            handler.handle_exception(self, exception)
+        if self.__closed:
+            handler.handle_close(self, self.__closed)
         
     def write(self, data):
         if __debug__:
             self.logger.debug('write %r', data)
+        assert isinstance(data, str) or (data is zc.ngi.END_OF_DATA)
         self.__output.append(data)
         notify_select()
+        
+    def writelines(self, data):
+        if __debug__:
+            self.logger.debug('writelines %r', data)
+        assert not isinstance(data, str), "writelines does not accept strings"
+        self.__output.append(iter(data))
+        notify_select()
 
     def close(self):
         self.__connected = False
+        self.__output[:] = []
         dispatcher.close(self)
         if self.control is not None:
             self.control.closed(self)
@@ -143,35 +152,60 @@
         while self.__output:
             output = self.__output
             v = output[0]
-            if v == zc.ngi.END_OF_DATA:
+            if v is zc.ngi.END_OF_DATA:
                 self.close()
                 return
             
+            if not isinstance(v, str):
+                # Must be an iterator
+                try:
+                    v = v.next()
+                except StopIteration:
+                    # all done
+                    output.pop(0)
+                    continue
+                
+                if __debug__ and not isinstance(v, str):
+                    exc = TypeError("iterable output returned a non-string", v)
+                    self.__report_exception(exc)
+                    raise exc
+
+                output.insert(0, v)
+
+            if not v:
+                output.pop(0)
+                continue
+
             try:
                 n = self.send(v)
             except socket.error, err:
                 if err[0] in expected_socket_write_errors:
                     return # we couldn't write anything
                 raise
+            except Exception, v:
+                self.__report_exception(v)
+                raise
             
             if n == len(v):
                 output.pop(0)
             else:
                 output[0] = v[n:]
                 return # can't send any more
+
+    def __report_exception(self, exception):
+        if self.__handler is not None:
+            self.__handler.handle_exception(self, exception)
+        else:
+            self.__exception = exception
             
     def handle_close(self, reason='end of input'):
         if __debug__:
             self.logger.debug('close %r', reason)
-        self.__handler_lock.acquire()
-        try:
-            if self.__handler is not None:
-                self.__handler.handle_close(self, reason)
-            else:
-                self.__closed = reason
-            self.close()
-        finally:
-            self.__handler_lock.release()
+        if self.__handler is not None:
+            self.__handler.handle_close(self, reason)
+        else:
+            self.__closed = reason
+        self.close()
 
     def handle_expt(self):
         self.handle_close('socket error')

Modified: zc.ngi/trunk/src/zc/ngi/async.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.txt	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/async.txt	2007-01-07 14:21:48 UTC (rev 71766)
@@ -32,7 +32,7 @@
     >>> import zc.ngi.async
     >>> port = zc.ngi.wordcount.start_server_process(zc.ngi.async.listener)
 
-We passed the module and name of the listener to be used.
+We passed the listener to be used.
 
 Now, we'll start a number of threads that connect to the server and
 check word counts of some sample documents.  If all goes well, we
@@ -47,4 +47,56 @@
     >>> _ = [thread.start() for thread in threads]
     >>> _ = [thread.join() for thread in threads]
     
+Iterable input
+==============
+
+We can pass data to the server using an iterator.  To illustrate this,
+we'll use the blocking interface:
+
+    >>> import zc.ngi.blocking
+    >>> output, input = zc.ngi.blocking.open(addr, zc.ngi.async.connector,
+    ...                                      timeout=1.0)
+    >>> def hello(name):
+    ...     yield "hello\n"
+    ...     yield name
+    ...     yield "\0"
+    
+    >>> output.writelines(hello('world'), timeout=1.0)
+    >>> input.readline(timeout=1.0)
+    '1 2 11\n'
+
+.. Error handling:
+
+   If we pass a non-iterable to writelines, we'll get an immediate
+   error.  To demonstrate this we'll violate out output file and
+   access it's _connection attribute so that we can bypass the check
+   in the blocking writelines method:
+
+    >>> output._connection.writelines(2)
+    Traceback (most recent call last):
+    ...
+    TypeError: iteration over non-sequence
+
+    >>> output._connection.writelines('foo')
+    Traceback (most recent call last):
+    ...
+    AssertionError: writelines does not accept strings
+
+   If we pass an iterable that returns a non-string, we'll get a type
+   error when we try to read because handle_exception is caused ion
+   the input handler.
+
+    >>> output.writelines([2], timeout=0.1)
+    Traceback (most recent call last):
+    ...
+    Timeout
+
+    >>> input.readline()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('iterable output returned a non-string', 2)
+
+
+.. stop the server
+
     >>> zc.ngi.wordcount.stop_server_process(zc.ngi.async.connector, addr)

Added: zc.ngi/trunk/src/zc/ngi/blocking.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/blocking.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/blocking.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -0,0 +1,307 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""File-like network interface
+
+$Id$
+"""
+
+import threading, time
+
+import zc.ngi
+
+class ConnectionFailed(Exception):
+    """A Connection attempt failed
+    """
+
+class Timeout(Exception):
+    """An operation timed out.
+    """
+
+class ConnectionTimeout(Timeout, ConnectionFailed):
+    """An attempt to connect timed out.
+    """
+
+def connect(address, connector, timeout=None):
+    return _connector().connect(address, connector, timeout)
+    
+class _connector:
+
+    failed = connection = None
+
+    def connect(self, address, connector, timeout):
+        event = self.event = threading.Event()
+        connector(address, self)
+        event.wait(timeout)
+        if self.failed is not None:
+            raise ConnectionFailed(self.failed)
+        if self.connection is not None:
+            return self.connection
+        raise ConnectionTimeout()
+    
+    def connected(self, connection):
+        self.connection = connection
+        self.event.set()
+
+    def failed_connect(reason):
+        self.failed = reason
+        self.event.set()
+
+def open(connection_or_address, connector=None, timeout=None):
+    if connector is None:
+        connection = connection_or_address
+    else:
+        connection = connect(connection_or_address, connector, timeout) 
+    return OutputFile(connection), InputFile(connection)
+
+class _BaseFile:
+
+    def __init__(self, connection):
+        self._connection = connection
+        self._position = 0
+        self._closed = False
+
+    def seek(self, offset, whence=0):
+        position = self._position
+        if whence == 0:
+            position = offset
+        elif whence == 1:
+            position += offset
+        elif whence == 2:
+            position -= offset
+        else:
+            raise IOError("Invalid whence argument", whence)
+        if position < 0:
+            raise IOError("Invalid offset", offset)
+        self._position = position
+
+    def tell(self):
+        return self._position
+
+    def _check_open(self):
+        if self._closed:
+            raise ValueError("I/O operation on closed file")
+
+class OutputFile(_BaseFile):
+
+    def invalid_method(*args, **kw):
+        raise IOError("Invalid operation on output file")
+
+    read = readline = readlines = invalid_method
+
+    def flush(self):
+        pass
+
+    def close(self):
+        if not self._closed:
+            self._connection.write(zc.ngi.END_OF_DATA)
+        self._closed = True
+            
+    def write(self, data):
+        self._check_open()
+        assert isinstance(data, str)
+        self._position += len(data)
+        self._connection.write(data)
+            
+    def writelines(self, data, timeout=None, nonblocking=False):
+        self._check_open()
+        if nonblocking:
+            self._connection.writelines(iter(data))
+            return
+
+        event = threading.Event()
+        self._connection.writelines(
+            _writelines_iterator(data, self, event.set))
+        # wait for iteration to finish
+        event.wait(timeout)
+        if not event.isSet():
+            raise Timeout()
+        
+    
+
+class _writelines_iterator:
+
+    def __init__(self, base, file, notify):
+        self._base = iter(base)
+        self._file = file
+        self._notify = notify
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        try:
+            data = self._base.next()
+            self._file._position += 1
+            return data
+        except StopIteration:
+            self._notify()
+            raise
+
+class InputFile(_BaseFile):
+
+    def __init__(self, connection):
+        _BaseFile.__init__(self, connection)
+        self._condition = threading.Condition()
+        self._data = ''
+        self._exception = None
+        connection.setHandler(self)
+
+    def invalid_method(*args, **kw):
+        raise IOError("Invalid operation on output file")
+
+    flush = write = writelines = invalid_method
+
+    def handle_input(self, connection, data):
+        condition = self._condition
+        condition.acquire()
+        self._data += data
+        condition.notifyAll()
+        condition.release()
+        
+    def handle_close(self, connection, reason):
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._closed = True
+            condition.notifyAll()
+        finally:
+            condition.release()
+
+    def handle_exception(self, connection, exception):
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._exception = exception
+            condition.notifyAll()
+        finally:
+            condition.release()
+
+    def close(self):
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._closed = True
+            self._connection.close()
+            condition.notifyAll()
+        finally:
+            condition.release()
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        s = self.readline()
+        if s:
+            return s
+        raise StopIteration
+
+    def read(self, size=None, timeout=None):
+        deadline = None
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._check_exception()
+            while 1:
+                data = self._data
+                if size is not None and size <= len(data):
+                    data, self._data = data[:size], data[size:]
+                    break
+                elif self._closed:
+                    if data:
+                        self._data = ''
+                    break
+
+                timeout, deadline = self._wait(timeout, deadline)
+
+            self._position += len(data)
+            return data
+        finally:
+            condition.release()
+
+    def readline(self, size=None, timeout=None):
+        deadline = None
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._check_exception()
+            while 1:
+                data = self._data
+                l = data.find('\n')
+                if l >= 0:
+                    l += 1
+                    if size is not None and size < l:
+                        l = size
+                    data, self._data = data[:l], data[l:]
+                    break
+                elif size is not None and size <= len(data):
+                    data, self._data = data[:size], data[size:]
+                    break
+                elif self._closed:
+                    if data:
+                        self._data = ''
+                    break
+                timeout, deadline = self._wait(timeout, deadline)
+
+            self._position += len(data)
+            return data
+
+        finally:
+            condition.release()
+
+    def readlines(self, sizehint=None, timeout=None):
+        deadline = None
+        condition = self._condition
+        condition.acquire()
+        try:
+            self._check_exception()
+            while 1:
+                data = self._data
+                if sizehint is not None and sizehint <= len(data):
+                    l = data.rfind('\n')
+                    if l >= 0:
+                        l += 1
+                        data, self._data = data[:l], data[l:]
+                        return data.splitlines(True)
+                elif self._closed:
+                    if data:
+                        self._data = ''
+                    return data.splitlines()
+                timeout, deadline = self._wait(timeout, deadline)
+        finally:
+            condition.release()
+
+    def _check_exception(self):
+        if self._exception is not None:
+            exception = self._exception
+            self._exception = None
+            raise exception
+
+    def _wait(self, timeout, deadline):
+        if timeout is not None:
+            if deadline is None:
+                if timeout <= 0:
+                    raise Timeout()
+                deadline = time.time() + timeout
+            else:
+                timeout = deadline - time.time()
+                if timeout <= 0:
+                    raise Timeout()
+            self._condition.wait(timeout)
+        else:
+            self._condition.wait()
+
+        self._check_exception()
+        
+        return timeout, deadline
+        


Property changes on: zc.ngi/trunk/src/zc/ngi/blocking.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/blocking.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/blocking.txt	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/blocking.txt	2007-01-07 14:21:48 UTC (rev 71766)
@@ -0,0 +1,156 @@
+=======================
+Blocking network access
+=======================
+
+The NGI normally uses an event-based networking model in which
+application code reactes to incoming data.  That model works well for
+some applications, especially server applications, but can be a bit of
+a bother for simpler applications, especially client applications.
+
+The zc.ngi.blocking module provides a simple blocking network model.
+The open function can be used to create a pair of file-like objects
+that can be used for writing output and reading input.  To illustrate
+this, we'll use the wordcount server.  We'll use the peer function to
+create a testing connector that connects to the server directory
+without using a network:
+
+    >>> import zc.ngi.wordcount
+    >>> import zc.ngi.testing
+    >>> connector = zc.ngi.testing.peer(('localhost', 42),
+    ...                                 zc.ngi.wordcount.Server)
+    
+The open function is called with an address and a connector:
+    
+    >>> import zc.ngi.blocking
+    >>> output, input = zc.ngi.blocking.open(('localhost', 42), connector)
+    
+The output file lets us send output to the server:
+
+    >>> output.write("Hello\n")
+    >>> output.write("world\n")
+    >>> output.write("\0")
+
+The wordcount server accepts a sequence of text from the
+client. Delimited by null characters.  For each input text, it
+generates a line of summary statistics:
+
+    >>> input.readline()
+    '2 2 12\n'
+
+We can use the writelines method to send data using an iterator:
+
+    >>> def hello(name):
+    ...     yield "hello\n"
+    ...     yield name
+    ...     yield "\0"
+
+    >>> output.writelines(hello("everyone"))
+    >>> output.writelines(hello("bob"))
+
+ To close the connection to the server, we'll send a close command,
+ which is a documenty consisting of the letter "C":
+
+    >>> output.write("C\0")
+
+This causes the server to close the connection after it has sent it's
+data.
+
+We can use the read function to read either a fixed number of bytes
+from the server:
+
+    >>> input.read(5)
+    '1 2 1'
+
+Or to read the remaining data:
+
+    >>> input.read()
+    '4\n1 2 9\n'
+
+If read is called without a size, it won't return until the server has
+closed the connection.  
+
+In this example, we've been careful to only read as much data as the
+server produces.  For example, we called read without passing a length
+only after sending a quit command to the server.  When using the
+blocking library, care is needed to avoid a deadlock, in which both
+sides of a connection are waiting for input.
+
+The blocking open and input methods accept an optional timeout
+argument.  The timeout argument accepts a floating-point time-out
+value, in seconds.  If a connection or input operation times out, a
+Timeout exception is raised:
+
+    >>> output, input = zc.ngi.blocking.open(('localhost', 42), connector)
+    >>> import time
+    >>> then = time.time()
+    >>> input.read(5, timeout=0.5)
+    Traceback (most recent call last):
+    ...
+    Timeout
+
+    >>> 0.5 < (time.time() - then) < 1
+    True
+
+The readline and readlines functions accept a timeout as well:
+
+    >>> then = time.time()
+    >>> input.readline(timeout=0.5)
+    Traceback (most recent call last):
+    ...
+    Timeout
+
+    >>> 0.5 < (time.time() - then) < 1
+    True
+
+    >>> then = time.time()
+    >>> input.readlines(timeout=0.5)
+    Traceback (most recent call last):
+    ...
+    Timeout
+
+    >>> 0.5 < (time.time() - then) < 1
+    True
+
+Timeouts can also be specified when connecting. To illustrate this,
+we'll pass a do-nothing connector:
+
+    >>> then = time.time()
+    >>> zc.ngi.blocking.open(None, (lambda *args: None), timeout=0.5)
+    Traceback (most recent call last):
+    ...
+    ConnectionTimeout
+
+    >>> 0.5 < (time.time() - then) < 1
+    True
+
+Low-level connection management
+===============================
+
+When we used open above, we passed an address and a connector, and the
+oprn function created a connection and created file-like objects for
+output and input.  The connect function can be used to create a
+connection without a file-like object:
+
+    >>> connection = zc.ngi.blocking.connect(('localhost', 42), connector)
+
+The if the open function is called without a connector, the the first
+object must be a connection object and output and input objects for
+that connection will be returned:
+
+    >>> output, input = zc.ngi.blocking.open(connection)
+    >>> output.write("Hello\n")
+    >>> output.write("world\n")
+    >>> output.write("\0")
+    >>> input.readline()
+    '2 2 12\n'
+
+Like the open function, the connect function accepts a timeout:
+
+    >>> then = time.time()
+    >>> zc.ngi.blocking.connect(None, (lambda *args: None), timeout=0.5)
+    Traceback (most recent call last):
+    ...
+    ConnectionTimeout
+
+    >>> 0.5 < (time.time() - then) < 1
+    True


Property changes on: zc.ngi/trunk/src/zc/ngi/blocking.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: zc.ngi/trunk/src/zc/ngi/interfaces.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/interfaces.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/interfaces.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -34,15 +34,15 @@
   simultaneous calls to the same application handler.  (Note that this
   requirement does not extend across multiple implementations.
   Theoretically, different implementations could call handlers at the
-  same time.
+  same time.)
 
 - All handler calls that are associated with a connection include the
   connection as a parameter,  This allows a single handler object to
   respond to events from multiple connections.
 
 Applications may be multi-threaded.  This means that implementations
-must be thread safe.  This means that calls into the implementation
-could be made at any time.
+must be thread safe.  This means that, unless otherwise stated, calls
+into the implementation could be made at any time.
 
 $Id$
 """
@@ -51,6 +51,8 @@
 
 class IConnection(Interface):
     """Network connections
+
+    This is an implementation interface.
   
     Network connections support communication over a network
     connection, or any connection having separate input and output
@@ -73,22 +75,35 @@
         """
 
     def write(data):
-        """Write output data to a connection.
+        """Output a string to the connection.
         
         The write call is non-blocking.
         """
 
+    def writelines(data):
+        """Output an iterable of strings to the connection.
+        
+        The writelines call is non-blocking. Note, that the data may
+        not have been consumed when the method returns.        
+        """
+
     def close():
         """Close the connection
         """
 
 class IServerConnection(IConnection):
-
+    """Server connection
+    
+    This is an implementation interface.
+    """
+    
     control = Attribute("An IServerControl")
 
-class IInputHandler(Interface):
-    """Objects that can handle connection input-data events
+class IConnectionHandler(Interface):
+    """Application objects that can handle connection input-data events
 
+    This is an application interface.
+
     The methods defined be this interface will never be called
     simultaneously from separate threads, so implementation of the
     methods needn't be concerned with thread safety with respect to
@@ -121,8 +136,19 @@
         called.      
         """
 
+    def handle_exception(connection, exception):
+        """Recieve a report of an exception encountered by a connection
+
+        This method is used to recieve exceptions from an NGI
+        implementation.  Typically, this will be due to an error
+        encounted processing data passed to the connection write or
+        writelines methods.
+        """
+
 class IConnector(Interface):
     """Create a connection to a server
+    
+    This is an implementation interface.
     """
 
     def __call__(address, handler):
@@ -136,6 +162,8 @@
 
 class IClientConnectHandler(Interface):
     """Receive notifications of connection results
+
+    This is an application interface.
     """
 
     def connected(connection):
@@ -152,6 +180,8 @@
 
 class IListener(Interface):
     """Listed for incoming connections
+    
+    This is an implementation interface.
     """
 
     def __call__(address, handler):
@@ -164,6 +194,8 @@
 
 class IServer(Interface):
     """Handle server connections
+
+    This is an application interface.
     """
 
     def __call__(connection):
@@ -172,6 +204,8 @@
 
 class IServerControl(Interface):
     """Server information and close control
+    
+    This is an implementation interface.
     """
 
     def connections():
@@ -190,3 +224,145 @@
         handler will be called when all of the existing connections
         have been closed.
         """
+
+class IBlocking(Interface):
+    """Top-level blocking interface provided by the blocking module
+    """
+
+    def connect(address, connector, timeout=None):
+        """Connect to the given address using the given connector
+
+        A timout value may be given as a floating point number of
+        seconds.
+
+        If connection suceeds, an IConnection is returned, otherwise
+        an exception is raised.
+        """
+
+    def open(connection_or_address, connector=None, timeout=None):
+        """Get output and input files for a connection or address
+
+        The first argument is either a connection or an address.
+        If (and only if) it is an address, then a connector must be
+        provided as the second argument and a connection is gotten by
+        calling the connect function with the given address,
+        connector, and timeout.
+
+        A pair of file-like objects is returned. The first is an
+        output file-like object, an IBlockingOutput, for sending
+        output to the connection.  The second file-like object is an
+        input file-like object, an IBlockingInput, for reading data
+        from the connection.
+        """
+
+class IBlockingPositionable(Interface):
+    """File-like objects with file positions.
+
+    To mimic file objects, working seek and tell methods are provided
+    that report and manipulate pseudo file positions.  The file
+    position starts at zero and is advanced by reading or writing
+    data. It can be adjusted (pointlessly) by the seek method.
+    """
+
+    def tell():
+        """Return the current file position.
+        """
+
+    def seek(offset, whence=0):
+        """Reset the file position
+
+        If whence is 0, then the file position is set to the offset.
+
+        If whence is 1, the position is increased by the offset.
+
+        If whence is 2, the position is decreased by the offset.
+
+        An exception is raised if the position is set to a negative
+        value. 
+        """
+
+    def close():
+        """Close the connection.
+        """
+
+class IBlockingOutput(IBlockingPositionable):
+    """A file-like object for sending output to a connection.
+    """
+
+    def flush():
+        """Do nothing.
+        """
+
+    def write(data):
+        """Write a string to the connection.
+
+        The function will return immediately.  The data may be queued.
+        """
+
+    def writelines(iterable, timeout=0, nonblocking=False):
+        """Write an iterable of strings to the connection.
+
+        By default, the call will block until the data from the
+        iterable has been consumed.  If a true value is passed to the
+        non-blocking keyword argument, then the function will return
+        immediately. The iterable will be consumed at some later time.
+
+        In (the default) blocking mode, a timeout may be provided to
+        limit the time that the call will block.  If the timeout
+        expires, a zc.ngi.blocking.Timeout excation will be raised.
+        """
+
+class IBlockingInput(IBlockingPositionable):
+    """A file-like object for reading input from a connection.
+    """
+
+    def read(size=None, timeout=None):
+        """Read data
+
+        If a size is specified, then that many characters are read,
+        blocking of necessary.  If no size is specified (or if size is
+        None), then all remaining input data are read.
+
+        A timeout may be specified as a floating point number of
+        seconds to wait.  A zc.ngi.blocking.Timeout exception will be
+        raised if the data cannot be read in the number of seconds given.
+        """
+
+    def readline(size=None, timeout=None):
+        """Read a line of data
+
+        If a size is specified, then the lesser of that many
+        characters or a single line of data are read, blocking of
+        necessary.  If no size is specified (or if size is None), then
+        a single line are read.
+
+        A timeout may be specified as a floating point number of
+        seconds to wait.  A zc.ngi.blocking.Timeout exception will be
+        raised if the data cannot be read in the number of seconds given.
+        """
+
+    def readlines(sizehint=None, timeout=None):
+        """Read multiple lines of data
+
+        If a sizehint is specified, then one or more lines of data are
+        returned whose total length is less than or equal to the size
+        hint, blocking if necessary. If no sizehint is specified (or
+        if sizehint is None), then the remainder of input, split into
+        lines, is returned.
+
+        A timeout may be specified as a floating point number of
+        seconds to wait.  A zc.ngi.blocking.Timeout exception will be
+        raised if the data cannot be read in the number of seconds given.
+        """
+
+        
+
+    def __iter__():
+        """Return the input object
+        """
+
+    def next():
+        """Return a line of input
+
+        Raises StopIteration if there is no more input.
+        """

Deleted: zc.ngi/trunk/src/zc/ngi/notes.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/notes.txt	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/notes.txt	2007-01-07 14:21:48 UTC (rev 71766)
@@ -1,11 +0,0 @@
-=====
-Notes
-=====
-
-Timeout
-=======
-
-It's tempting to try to add a timeout feature, however, this adds
-quite a bit of complication, or at least time calls to get this right.
-I think it makes more sense for applications to use threads to create
-alarms. 

Modified: zc.ngi/trunk/src/zc/ngi/testing.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/testing.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/testing.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -37,6 +37,9 @@
         else:
             print '-> CLOSE'
 
+    def handle_exception(self, connection, exception):
+        print '-> EXCEPTION', exception.__class__.__name__, exception
+
 class Connection:
 
     control = None
@@ -45,6 +48,7 @@
         self.handler = None
         self.closed = False
         self.input = ''
+        self.exception = None
         if peer is None:
             peer = Connection(self)
             handler(peer)
@@ -64,6 +68,10 @@
         
     def setHandler(self, handler):
         self.handler = handler
+        if self.exception:
+            exception = self.exception
+            self.exception = None
+            handler.handle_exception(self, exception)
         if self.input:
             handler.handle_input(self, self.input)
             self.input = ''
@@ -89,8 +97,29 @@
     def write(self, data):
         if data is zc.ngi.END_OF_DATA:
             return self.close()
-        self.peer.test_input(data)
 
+        if isinstance(data, str):
+            self.peer.test_input(data)
+        else:
+            raise TypeError("write argument must be a string")
+
+    def writelines(self, data):
+        assert not (isinstance(data, str) or (data is zc.ngi.END_OF_DATA))
+        data = iter(data)
+        try:
+            for d in data:
+                if not isinstance(d, str):
+                    raise TypeError("Got a non-string result from iterable")
+                self.write(d)
+        except Exception, v:
+            self._exception(v)
+
+    def _exception(self, exception):
+        if self.handler is None:
+            self.exception = exception
+        else:
+            self.handler.handle_exception(self, exception)
+
 class TextPrintingHandler(PrintingHandler):
 
     def handle_input(self, connection, data):

Modified: zc.ngi/trunk/src/zc/ngi/tests.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/tests.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/tests.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -108,6 +108,7 @@
             'README.txt',
             'message.txt',
             'adapters.txt',
+            'blocking.txt',
             ),
         doctest.DocFileSuite(
             'async.txt',

Modified: zc.ngi/trunk/src/zc/ngi/wordcount.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/wordcount.py	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/src/zc/ngi/wordcount.py	2007-01-07 14:21:48 UTC (rev 71766)
@@ -56,6 +56,9 @@
                 connection.write(zc.ngi.END_OF_DATA)
                 connection.control.close(lambda c: 1)
                 return
+            elif data == 'C':
+                connection.write(zc.ngi.END_OF_DATA)
+                return
             else:
                 cc = len(data)
                 lc = len(data.split('\n'))-1

Modified: zc.ngi/trunk/todo.txt
===================================================================
--- zc.ngi/trunk/todo.txt	2007-01-07 14:01:52 UTC (rev 71765)
+++ zc.ngi/trunk/todo.txt	2007-01-07 14:21:48 UTC (rev 71766)
@@ -1,2 +1,9 @@
-- Need to change the async implementation to allow setHandler to be
-  called multiple times, but only from the main thread.
+- Test for blocking where there is some i/o before timeout.
+
+- Test for blocking where there is some i/o before success.
+
+- Test blocking timout on writelines and non-blocking option
+
+- Test handle_exception for blocking
+
+- Test we fail early when passing non-iterable to writelines



More information about the Checkins mailing list