[Checkins] SVN: zc.ngi/branches/jim-dev/s checkpoint

Jim Fulton jim at zope.com
Wed Sep 2 07:04:48 EDT 2009


Log message for revision 103481:
  checkpoint
  

Changed:
  U   zc.ngi/branches/jim-dev/setup.py
  U   zc.ngi/branches/jim-dev/src/zc/ngi/blocking.py
  U   zc.ngi/branches/jim-dev/src/zc/ngi/blocking.txt
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/Makefile
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/_static/
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/_templates/
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/conf.py
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/contents.txt
  A   zc.ngi/branches/jim-dev/src/zc/ngi/doc/index.txt
  A   zc.ngi/branches/jim-dev/src/zc/ngi/generator.py
  U   zc.ngi/branches/jim-dev/src/zc/ngi/testing.py
  U   zc.ngi/branches/jim-dev/src/zc/ngi/tests.py

-=-
Modified: zc.ngi/branches/jim-dev/setup.py
===================================================================
--- zc.ngi/branches/jim-dev/setup.py	2009-09-02 11:01:34 UTC (rev 103480)
+++ zc.ngi/branches/jim-dev/setup.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -38,8 +38,6 @@
         '**********************\n'
         )
 
-open('documentation.txt', 'w').write(long_description)
-
 setup(
     name = name, version=version,
     author = "Jim Fulton",
@@ -56,7 +54,7 @@
     namespace_packages = ['zc'],
     install_requires = ['setuptools'],
     extras_require = dict(
-        test = ['zope.testing'],
+        test = ['zope.testing', 'manuel'],
         ),
     zip_safe = False,
     )

Modified: zc.ngi/branches/jim-dev/src/zc/ngi/blocking.py
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/blocking.py	2009-09-02 11:01:34 UTC (rev 103480)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/blocking.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -32,7 +32,69 @@
     """An attempt to connect timed out.
     """
 
-def connect(address, connect, timeout=None):
+class RequestConnection:
+
+    def __init__(self, connection, connector):
+        self.connection = connection
+        self.connector = connector
+
+    def write(self, data):
+        self.write = self.connection.write
+        self.write(data)
+
+    def writelines(self, data):
+        self.writelines = self.connection.writelines
+        self.writelines(data)
+
+    def close(self):
+        self.connection.close()
+        self.connector.closed = 'client'
+        self.connector.event.set()
+
+    def setHandler(self, handler):
+        self.handler = handler
+        self.handleInput = handler.handleInput
+        self.handle_exception = handler.handle_exception
+        self.connection.setHandler(self)
+
+    def handle_close(self, connection, reason):
+        self.connector.closed = reason
+        self.connector.event.set()
+
+class RequestConnector:
+
+    failed = closed = connection = None
+
+    def __init__(self, handler, event):
+        self.handler
+        self.event
+
+    def connected(self, connection):
+        self.connection = connection
+        connection = RequestConnection(connection, self)
+        self.handler.connected(connection)
+
+    def failed_connection(self, reason):
+        self.failed = reason
+        self.event.set()
+
+def request(address, connection_handler, connect=None, timeout=None):
+    if connect is None:
+        connect = zc.ngi.implementation.connect
+    event = threading.Event()
+    connector = RequestConnector(connection_handler, event)
+    event.wait(timeout)
+    if connector.closed is not None:
+        return connector.closed
+    if connector.failed is not None:
+        raise ConnectionFailed(connector.failed)
+    if connector.connection is None:
+        raise ConnectionTimeout
+    raise Timeout
+
+def connect(address, connect=None, timeout=None):
+    if connect is None:
+        connect = zc.ngi.implementation.connect
     return _connector().connect(address, connect, timeout)
 
 class _connector:
@@ -58,7 +120,8 @@
         self.event.set()
 
 def open(connection_or_address, connector=None, timeout=None):
-    if connector is None:
+    if connector is None and hasattr(connection_or_address, 'setHandler'):
+        # connection_or_address is a connection
         connection = connection_or_address
     else:
         connection = connect(connection_or_address, connector, timeout)

Modified: zc.ngi/branches/jim-dev/src/zc/ngi/blocking.txt
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/blocking.txt	2009-09-02 11:01:34 UTC (rev 103480)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/blocking.txt	2009-09-02 11:04:48 UTC (rev 103481)
@@ -3,7 +3,7 @@
 =======================
 
 The NGI normally uses an event-based networking model in which
-application code reactes to incoming data.  That model works well for
+application code reacts 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.
 

Added: zc.ngi/branches/jim-dev/src/zc/ngi/doc/Makefile
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/doc/Makefile	                        (rev 0)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/doc/Makefile	2009-09-02 11:04:48 UTC (rev 103481)
@@ -0,0 +1,88 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf _build/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
+	@echo
+	@echo "Build finished. The HTML pages are in _build/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in _build/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in _build/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in _build/qthelp, like this:"
+	@echo "# qcollectiongenerator _build/qthelp/zcngi.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile _build/qthelp/zcngi.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in _build/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
+	@echo
+	@echo "The overview file is in _build/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in _build/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in _build/doctest/output.txt."


Property changes on: zc.ngi/branches/jim-dev/src/zc/ngi/doc/Makefile
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.ngi/branches/jim-dev/src/zc/ngi/doc/conf.py
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/doc/conf.py	                        (rev 0)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/doc/conf.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+#
+# zc.ngi documentation build configuration file, created by
+# sphinx-quickstart on Sun Jul 26 08:56:15 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'contents'
+
+# General information about the project.
+project = u'zc.ngi'
+copyright = u'2009, Jim Fulton'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.1'
+# The full version, including alpha/beta/rc tags.
+release = '1.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'zcngidoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('contents', 'zcngi.tex', u'zc.ngi Documentation',
+   u'Jim Fulton', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True


Property changes on: zc.ngi/branches/jim-dev/src/zc/ngi/doc/conf.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: zc.ngi/branches/jim-dev/src/zc/ngi/doc/contents.txt
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/doc/contents.txt	                        (rev 0)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/doc/contents.txt	2009-09-02 11:04:48 UTC (rev 103481)
@@ -0,0 +1,20 @@
+.. zc.ngi documentation master file, created by
+   sphinx-quickstart on Sun Jul 26 08:56:15 2009.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to zc.ngi's documentation!
+==================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+


Property changes on: zc.ngi/branches/jim-dev/src/zc/ngi/doc/contents.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.ngi/branches/jim-dev/src/zc/ngi/doc/index.txt
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/doc/index.txt	                        (rev 0)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/doc/index.txt	2009-09-02 11:04:48 UTC (rev 103481)
@@ -0,0 +1,505 @@
+Introduction
+============
+
+Network programs are typically difficult to test because they require
+setting up network connections, clients, and servers.
+
+The Network Gateway Interface (NGI) seeks to improve this situation by
+separating application code from network code [#twisted]_.  NGI
+provides a layered architecture with plugable networking
+implementations. This allows application and network code to be tested
+independently and provides greater separation of concerns. A testing
+implementation supports testing application code without making
+network calls.
+
+NGI defines 2 groups of interfaces, application and implementation.
+Application interfaces are implemented by people writing applications
+using ngi. Implementation interfaces are written by back-end
+implementors.
+
+NGI is primary an asynchronous networking library.  Applications
+provide handlers that respond to network events.  The application
+interfaces definee these handlers:
+
+IConnectionHandler
+    Application component that handles TCP network input.
+
+IClientConnectHandler
+    Application component that handles successful or failed outgoing
+    TCP connections.
+
+IServer
+    Application callback to handle incoming connections.
+
+IUDPHandler
+    Application callback to handle incoming UDP messages.
+
+NGI also provides a synchronous API implemented on top of the
+asynchronous API.
+
+The implemention APIs provide (or mimic) low-level networking APIs:
+
+IImplementation
+    APIs for implementing and connecting to TCP servers and for
+    implementing and sending messages to UDP servers.
+
+IConnection
+    Network connection implementation.  This is the interface that
+    TCP applications interact with to actually get and send data.
+
+We'll look at these interfaces in more detail in the following sections.
+
+Connection Handlers
+===================
+
+The core application interface in NGI is IConnectionHandler.  It's an
+event-based API that's used to exchange data with a peer on the other
+side of a connection.  Let's look at a simple echo server that accepts
+input and sends it back after converting it to upper case::
+
+  class Echo:
+
+      def handle_input(self, conection, data):
+          connection.write(data.upper())
+
+      def handle_close(self, connection, reason):
+          print 'closed', reason
+
+      def handle_exception(self, connection, exception):
+          print 'oops', exception
+
+.. -> src
+
+    >>> exec(src)
+
+There are only 3 methods in the interface, 2 of which are optional.
+Each of the 3 methods takes a connection object, implementing
+``IConnection``.  Typically, connection handlers will call the write,
+writelines, or close methods from the handler's handle input method.
+The writelines [#writelines] method takes an iteraable object.
+
+The handler's handle_close and handle_exception methods are optional.
+The handle_exception method is only called if an iterator created from
+an iterable passed to writelines raises an exception.  If a call to
+handle_exception fails, an implementation will close the connection.
+
+The handle_close method is called when a connection is closed other
+than through the connection handler calling the connection's close
+method.  For many applications, this is uninteresting, which is why
+the method is optional.  Clients that maintain long-running
+conections, may try to create new connections when notified that a
+connection has closed.
+
+Testing connection handlers
+---------------------------
+
+Testing a connection handler is very easy.  Just call it's methods
+passing suitable arguments. The zc.ngi.testing module provides a
+connection implementation designed to make testing convenient.  For
+example, to test our Echo connection handler, we can use code like the
+following:
+
+    >>> import zc.ngi.testing
+    >>> connection = zc.ngi.testing.Connection()
+    >>> handler = Echo()
+    >>> handler.handle_input(connection, 'hello out there')
+    -> 'HELLO OUT THERE'
+
+Any data written to the connection, using it's write or writelines
+methods, is written to standard output preceeded by "-> ".
+
+    >>> handler.handle_close(connection, 'done')
+    closed done
+
+Imperative handlers using generators
+------------------------------------
+
+Let's look at a slightly more complicated example.  We'll implement
+simple word-count server connection handler that implements something
+akin to the Unix word-count command.  It takes a line of input
+containing a text length followed by length bytes of data.  After
+recieving the length bytes of data, it send back a line of data
+containing line, word, and character counts::
+
+  class WC:
+
+      input = ''
+      count = None
+
+      def handle_input(self, conection, data):
+          self.input += data
+
+          if self.count is None:
+              if '\n' not in self.input:
+                  return
+              count, self.input = self.input.split('\n', 1)
+              self.count = int(count)
+
+          if len(self.input) < self.count:
+              return
+
+          data = self.input[:self.count]
+          self.input = self.input[self.count:]
+          self.count = None
+          connection.write(
+              '%d %d %d\n' % (
+                 len(data.split('\n')), len(data.split()), len(data)
+                 ))
+
+.. -> src
+
+    >>> exec(src)
+
+    >>> handler = WC()
+    >>> connection = zc.ngi.testing.Connection()
+    >>> handler.handle_input(connection, '15')
+    >>> handler.handle_input(connection, '\nhello out\nthere')
+    -> '2 3 15\n'
+
+Here, we ommitted the optional handle_close and handle_exception
+methods.  The implementation is a bit complicated. We have to use
+instance variables to keep track of state between calls.  Note that we
+can't count on data coming in a line at a time or make any assumptions
+about the amount of data we'll recieve in a handle_input call.  The
+logic is complicated by the fact that we have two modes of collecting
+input. In the first mode, we're collecting a length. In the second
+mode, we're collecting input for analysis.
+
+Connection handlers can often be simplified by writing them as
+generators, using zc.ngi.generator.handler::
+
+    import zc.ngi.generator
+
+    @zc.ngi.generator.handler
+    def wc(connection):
+        input = ''
+        while 1:
+            while '\n' not in input:
+                input += (yield)
+            count, input = input.split('\n', 1)
+            count = int(count)
+            while len(input) < count:
+                input += (yield)
+            data = input[:count]
+            connection.write(
+                '%d %d %d\n' % (
+                   len(data.split('\n')), len(data.split()), len(data)
+                   ))
+            input = input[count:]
+
+.. -> src
+
+    >>> import sys
+    >>> if sys.version_info >= (2, 5):
+    ...     exec(src)
+    ... else:
+    ...     def wc(conection):
+    ...         connection.setHandler(WC())
+
+The generator takes a connection object and gets data via yield
+statements.  The yield statements can raise exceptions.  In
+particular, a GeneratorExit exception is raised when the connection is
+closed.  The yield statement will also (re)raise any exceptions raised
+by calling an iterator created from an iterable passed to writelines.
+
+A generator-based handler is instantiated by calling it with a
+connection object:
+
+    >>> handler = wc(connection)
+    >>> handler.handle_input(connection, '15')
+    >>> handler.handle_input(connection, '\nhello out\nthere')
+    -> '2 3 15\n'
+
+    >>> handler.handle_close(connection, 'done')
+
+Implementing servers
+====================
+
+Implementing servers is only slightly more involved that implementing
+connection handlers.  A server is just a callable that takes a
+connection.  It typically creates a connection handler and passes it
+to the connection's setHandler method.  We can create a server using
+the Echo conection handler::
+
+    def echo_server(connection):
+        connection.setHandler(Echo())
+
+.. -> src
+
+    >>> exec(src)
+
+Of course, it's simpler to just use a connection handler class as a
+server by calling setHandler in the constructor::
+The full echo server is::
+
+  class Echo:
+
+      def __init__(self, connection):
+          connection.setHandler(self)
+
+      def handle_input(self, connection, data):
+          connection.write(data.upper())
+
+      def handle_close(self, connection, reason):
+          print 'closed', reason
+
+      def handle_exception(self, connection, exception):
+          print 'oops', exception
+
+.. -> src
+
+    >>> exec(src)
+
+Note that handlers created from generators can be used as servers
+directly.
+
+To actually get connections, we have to register a server with a
+listener. NGI implentations provide listener functions that take a
+server and return listener objects.
+
+NGI implementations provide a listener method, that takes an address
+and a server.  When testing servers, we'll often use the
+``zc.ngi.testing.listener`` function:
+
+    >>> listener = zc.ngi.testing.listener('echo', Echo)
+
+Generally, the address will either be a host/port tuple or the name of
+a unix domain socket, although an implementation may define a custom
+address representation.  The ``zc.ngi.testing.listener`` function will
+take any hashable address object.
+
+We can connect to a testing listener using it's connect method:
+
+    >>> connection = listener.connect()
+
+The connection returned from listener.connect is not the connection
+passed to the server.  Instead, it's a test connection that we can use
+as if we're writing a client:
+
+    >>> connection.write('Hi\nthere.')
+    -> 'HI\nTHERE.'
+
+It is actually a peer of the connection passed to the server. Testing
+connections have peer attributes that you can use to get to the peer
+connection.
+
+    >>> connection.peer.peer is connection
+    True
+
+The test connection has a default handler that just prints data to
+standard output, but we can call setHandler on it to use a different
+handler:
+
+    >>> class Handler:
+    ...     def handle_input(self, connection, data):
+    ...         print 'got', `data`
+    >>> connection.setHandler(Handler())
+    >>> connection.write('take this')
+    got 'TAKE THIS'
+
+Listeners provide two methods for controlling servers.  The
+``connections`` method returns an iterator of open connections. The
+``close`` method is used to stop a server, immediately, or after current
+connections have been closed.  See the reference sections of the
+documentation for more information.
+
+Implementing clients
+====================
+
+Implementing clients is a little bit more involved than writing
+servers because in addition to handling connections, you have to
+initiate the connections in the first place.  This involves
+implementing client connect handlers.  You request a connection by
+calling an implementation's ``connect`` function, passing a connect
+handler.  The ``connected`` method is called if the connection suceeds
+and the ``failed_connect`` method is called if it fails.
+
+Let's implement a word-count client.  It will take a string and use a
+work-count server to get it's line, word, and character counts::
+
+  class WCClient:
+
+      def __init__(self, data):
+          self.data = data
+
+      def connected(self, connection):
+          connection.setHandler(LineReader())
+          connection.write(self.data)
+
+      def failed_connect(self, reason):
+          print 'failed', reason
+
+  class LineReader:
+
+      input = ''
+      def handle_input(self, connection, data):
+          self.input += data
+          if '\n' in self.input:
+             print 'LineReader got', self.input
+             connection.close()
+
+.. -> src
+
+    >>> exec(src)
+
+Testing client connect handlers
+-------------------------------
+
+We test client connect handlers the same way we test connection
+handlers and servers, by calling their methods:
+
+    >>> wcc = WCClient('Hello out\nthere')
+    >>> wcc.failed_connect('test')
+    failed test
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> wcc.connected(connection)
+    -> 'Hello out\nthere'
+
+In this example, the connect handler set the connection handler to an
+instance of LineReader and wrote the data to be analyzed to the
+connection.  We now want to send some test result data to the reader.  If
+we call the connection's write method, the data we pass will just be
+printed, as the data the connect handler passed to the connection
+write method was.  We want to play the role of the server. To do that,
+we need to get the test connection's peer and call it's write method:
+
+    >>> connection.peer.write('text from server\n')
+    LineReader got text from server
+    <BLANKLINE>
+    -> CLOSE
+
+Conbining connect handlers with connection handlers
+---------------------------------------------------
+
+A connect handler can be it's own connection handler:
+
+  class WCClient:
+
+      def __init__(self, data):
+          self.data = data
+
+      def connected(self, connection):
+          connection.setHandler(self)
+          connection.write(self.data)
+
+      def failed_connect(self, reason):
+          print 'failed', reason
+
+      input = ''
+      def handle_input(self, connection, data):
+          self.input += data
+          if '\n' in self.input:
+             print 'WCClient got', self.input
+             connection.close()
+
+.. -> src
+
+    >>> exec(src)
+
+    >>> wcc = WCClient('Line one\nline two')
+    >>> connection = zc.ngi.testing.Connection()
+    >>> wcc.connected(connection)
+    -> 'Line one\nline two'
+
+    >>> connection.peer.write('more text from server\n')
+    WCClient got more text from server
+    <BLANKLINE>
+    -> CLOSE
+
+and, of course, a generator can be used in the connected method:
+
+  class WCClientG:
+
+      def __init__(self, data):
+          self.data = data
+
+      @zc.ngi.generator.handler
+      def connected(self, connection):
+          connection.write(self.data)
+          input = ''
+          while '\n' not in input:
+              input += (yield)
+          print 'Got', input
+
+      def failed_connect(self, reason):
+          print 'failed', reason
+
+.. -> src
+
+    >>> if sys.version_info >= (2, 5):
+    ...     exec(src)
+    ...     wcc = WCClientG('first one\nsecond one')
+    ...     connection = zc.ngi.testing.Connection()
+    ...     _ = wcc.connected(connection)
+    ...     connection.peer.write('still more text from server\n')
+    -> 'first one\nsecond one'
+    Got still more text from server
+    <BLANKLINE>
+    -> CLOSE
+
+Conecting
+---------
+
+Implementations provide a ``connect`` method that takes an address and
+connect handler.  We'll often refer to the ``connect`` method as a
+"connector".  Applications that maintain long-running connections will
+often need to reconnect when conections are lost or retry cnectins
+when they fail.  In situations like this, we'll often pass a connect
+function to the application.
+
+When testing application connection logic, you'll typically create
+your own connector object.
+
+An important thing to note about making connections is that connector
+calls return immediately.  Connections are made and connection
+handlers are called in separate threads.  This means that you can have
+many outstading connect requests active at once.
+
+Blocking API
+============
+
+Event-based API's can be very convenient when implementing servers,
+and sometimes even when implementing clients.  In many cases though,
+simple clients can be problematic because, as mentioned in the
+previous section, calls to connectors are made in a separate thread. A
+call to an implementation's ``connect`` method returns immediately,
+before a connection is made and handled. A simple script that makes a
+single request to a server has to wait for a request to be completed
+before exiting.
+
+To support the common use case of a client that makes a single request
+(or finite number of requests) to a server, the ``zc.ngi.blocking``
+module provides a ``request`` function that makes a single request and
+blocks until the request has completed::
+
+    >>> import zc.ngi.blocking
+    >>> zc.ngi.blocking.request(zc.ngi.testing.connect, 'xxx', WCClient)
+
+The request function takes a connector, an address, and a connect
+handler. In the example above, we used the ``zc.ngi.testing``
+implementation's ``connect`` function as the connector.  The testing
+connector accepts any hashable object as an address.  By default,
+connections using the testing connector fail right away, as we saw
+above.
+
+
+
+connecting
+request
+threading
+udp
+----------------------
+
+Notes:
+
+- Maybe close should change to wait until data are sent
+- Maybe grow a close_now. Or some such. Or maybe grow a close_after_sent.
+- What about errors raised by handle_input?
+- Need to make sure we have tests of edge cases where there are errors
+  calling handler methods.
+- testing.listener doesn't use the address argument
+
+- Can we implement application connection retry logic wo threads?
+  Should we? Testing would be easier if the implementation provided
+  it. If conectors took a delay argument, then it would be easier to test.


Property changes on: zc.ngi/branches/jim-dev/src/zc/ngi/doc/index.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.ngi/branches/jim-dev/src/zc/ngi/generator.py
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/generator.py	                        (rev 0)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/generator.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -0,0 +1,55 @@
+##############################################################################
+#
+# Copyright Zope Foundation 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.
+#
+##############################################################################
+
+class handler(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args):
+        return ConnectionHandler(self.func(*args), args[-1])
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return self
+
+        return (lambda connection:
+                ConnectionHandler(self.func(inst, connection), connection)
+                )
+
+class ConnectionHandler(object):
+
+    def __init__(self, gen, connection):
+        try:
+            gen.next()
+        except StopIteration:
+            return
+
+        self.gen = gen
+        connection.setHandler(self)
+
+    def handle_input(self, connection, data):
+        try:
+            self.gen.send(data)
+        except StopIteration:
+            connection.close()
+
+    def handle_close(self, connection, reason):
+        try:
+            self.gen.throw(GeneratorExit, GeneratorExit(reason))
+        except (GeneratorExit, StopIteration):
+            pass
+
+    def handle_exception(self, connection, exception):
+        self.gen.throw(exception.__class__, exception)


Property changes on: zc.ngi/branches/jim-dev/src/zc/ngi/generator.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Modified: zc.ngi/branches/jim-dev/src/zc/ngi/testing.py
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/testing.py	2009-09-02 11:01:34 UTC (rev 103480)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/testing.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -171,21 +171,29 @@
 
 class listener:
 
-    def __init__(self, handler):
+    def __init__(self, addr, handler=None):
+        if handler is None:
+            handler = addr
         self._handler = handler
         self._close_handler = None
         self._connections = []
 
-    def connect(self, connection, handler=None):
+    def connect(self, connection=None, handler=None):
         if handler is not None:
             # connection is addr in this case and is ignored
             handler.connected(Connection(None, self._handler))
             return
         if self._handler is None:
             raise TypeError("Listener closed")
+        if connection is None:
+            connection = Connection()
+            peer = connection.peer
+        else:
+            peer = None
         self._connections.append(connection)
         connection.control = self
         self._handler(connection)
+        return peer
 
     connector = connect
 

Modified: zc.ngi/branches/jim-dev/src/zc/ngi/tests.py
===================================================================
--- zc.ngi/branches/jim-dev/src/zc/ngi/tests.py	2009-09-02 11:01:34 UTC (rev 103480)
+++ zc.ngi/branches/jim-dev/src/zc/ngi/tests.py	2009-09-02 11:04:48 UTC (rev 103481)
@@ -15,10 +15,13 @@
 
 $Id$
 """
+from zope.testing import doctest
+import manuel.capture
+import manuel.doctest
+import manuel.testing
 import threading, unittest
-from zope.testing import doctest
+import zc.ngi.async
 import zc.ngi.testing
-import zc.ngi.async
 import zc.ngi.wordcount
 
 def test_async_cannot_connect():
@@ -181,6 +184,10 @@
 
 def test_suite():
     return unittest.TestSuite([
+        manuel.testing.TestSuite(
+            manuel.doctest.Manuel() + manuel.capture.Manuel(),
+            'doc/index.txt',
+            ),
         doctest.DocFileSuite(
             'README.txt',
             'testing.test',



More information about the checkins mailing list