[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