[Checkins] SVN: bobo/trunk/ bobo is back

Jim Fulton jim at zope.com
Tue May 26 07:06:35 EDT 2009


Log message for revision 100392:
  bobo is back

Changed:
  A   bobo/trunk/
  A   bobo/trunk/bobo/
  A   bobo/trunk/bobo/README.txt
  A   bobo/trunk/bobo/ez_setup.py
  A   bobo/trunk/bobo/setup.py
  A   bobo/trunk/bobo/src/
  A   bobo/trunk/bobo/src/bobo.py
  A   bobo/trunk/bobo/src/boboserver.py
  A   bobo/trunk/bobodoctestumentation/
  A   bobo/trunk/bobodoctestumentation/setup.py
  A   bobo/trunk/bobodoctestumentation/src/
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/Makefile
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/__init__.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/_static/
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/_templates/
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobo.svg
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.html
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/boboserver.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/conf.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/contents.txt
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/decorator.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/edit.html
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/examples.txt
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.ini
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/htpasswd
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/index.txt
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/main.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/make.bat
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/more.txt
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/reference.txt
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.test
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/tests.py
  A   bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/who.ini
  A   bobo/trunk/buildout.cfg
  A   bobo/trunk/doc

-=-
Added: bobo/trunk/bobo/README.txt
===================================================================
--- bobo/trunk/bobo/README.txt	                        (rev 0)
+++ bobo/trunk/bobo/README.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,20 @@
+Bobo
+====
+
+Bobo is a light-weight framework for creating WSGI web applications.
+
+It's goal is to be easy to use and remember. You don't have to be a genius.
+
+It addresses 2 problems:
+
+- Mapping URLs to objects
+
+- Calling objects to generate HTTP responses
+
+Bobo doesn't have a templateing language, a database integration layer,
+or a number of other features that are better provided by WSGI
+middle-ware or application-specific libraries.
+
+Bobo builds on other frameworks, most notably WSGI and WebOb.
+
+To learn more. visit: http://bobo.digicool.com


Property changes on: bobo/trunk/bobo/README.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobo/ez_setup.py
===================================================================
--- bobo/trunk/bobo/ez_setup.py	                        (rev 0)
+++ bobo/trunk/bobo/ez_setup.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+    'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+    'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+    'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+    'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+    'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+    'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+    'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+    'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+    'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+    'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+    'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+    'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+    'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+    'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+    'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+    'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+    'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+    'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+    'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+    def do_download():
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+    try:
+        import pkg_resources
+    except ImportError:
+        return do_download()       
+    try:
+        pkg_resources.require("setuptools>="+version); return
+    except pkg_resources.VersionConflict, e:
+        if was_imported:
+            print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first, using 'easy_install -U setuptools'."
+            "\n\n(Currently using %r)"
+            ) % (version, e.args[0])
+            sys.exit(2)
+        else:
+            del pkg_resources, sys.modules['pkg_resources']    # reload ok
+            return do_download()
+    except pkg_resources.DistributionNotFound:
+        return do_download()
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+
+


Property changes on: bobo/trunk/bobo/ez_setup.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobo/setup.py
===================================================================
--- bobo/trunk/bobo/setup.py	                        (rev 0)
+++ bobo/trunk/bobo/setup.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# Copyright 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.
+#
+##############################################################################
+name = 'bobo'
+version = '0'
+
+long_description = """
+Bobo is a light-weight framework for creating WSGI web applications.
+
+It's goal is to be easy to use and remember. You don't have to be a genius.
+
+It addresses 2 problems:
+
+- Mapping URLs to objects
+
+- Calling objects to generate HTTP responses
+
+Bobo doesn't have a templateing language, a database integration layer,
+or a number of other features that are better provided by WSGI
+middle-ware or application-specific libraries.
+
+Bobo builds on other frameworks, most notably WSGI and WebOb.
+
+To learn more. visit: http://bobo.digicool.com
+"""
+
+entry_points = """
+[console_scripts]
+bobo = boboserver:server
+
+[paste.app_factory]
+main = bobo:Application
+
+[paste.filter_app_factory]
+reload = boboserver:Reload
+debug = boboserver:Debug
+"""
+
+from ez_setup import use_setuptools
+use_setuptools()
+from setuptools import setup
+
+import sys
+
+if sys.version_info >= (2, 5):
+    install_requires = ['WebOb']
+else:
+    install_requires = ['WebOb', 'PasteDeploy', 'Paste']
+
+setup(
+    name = name,
+    version = version,
+    author = "Jim Fulton",
+    author_email = "jim at zope.com",
+    description = "Web application framework for the impatient",
+    license = "ZPL 2.1",
+    keywords = "WSGI",
+    url='http://www.python.org/pypi/'+name,
+    long_description=long_description,
+
+    py_modules = ['bobo', 'boboserver'],
+    package_dir = {'':'src'},
+    install_requires = install_requires,
+    entry_points = entry_points,
+    tests_require = ['bobodoctestumentation', 'webtest', 'zope.testing'],
+    test_suite = 'bobodoctestumentation.tests.test_suite',
+    )


Property changes on: bobo/trunk/bobo/setup.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobo/src/bobo.py
===================================================================
--- bobo/trunk/bobo/src/bobo.py	                        (rev 0)
+++ bobo/trunk/bobo/src/bobo.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,1287 @@
+##############################################################################
+#
+# Copyright 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.
+#
+##############################################################################
+"""Create WSGI-based web applications.
+"""
+
+# Public names:
+__all__ = (
+    'Application',
+    'early',
+    'late',
+    'NotFound',
+    'order',
+    'post',
+    'preroute',
+    'query',
+    'redirect',
+    'reroute',
+    'resource',
+    'resources',
+    'scan_class',
+    'subroute',
+    )
+
+__metaclass__ = type
+
+import re
+import sys
+import webob
+
+bbbbad_errors = KeyboardInterrupt, SystemExit, MemoryError
+
+_default_content_type = 'text/html; charset=UTF-8'
+
+_json_content_type = re.compile('application/json;?').match
+
+class Application:
+    """Create a WSGI application.
+
+    The DEFAULT argument, if given, is a dictionary of options.
+    Keyword options override options given in the DEFAULT options.
+
+    Option values are strings, typically read from ConfigParser files.
+
+    The values used by bobo, ``bobo_resources``, ``bobo_configure``
+    and ``bobo_errors``, can have comments.  Lines within these
+    values are truncated at the first '#' characters.
+
+    The one required option is bobo_resources:
+
+    bobo_resources
+       Specifies resources to be used.
+
+       This option can be used to:
+
+       - Specify modules to be scanned for resources.
+       - Include specific resources, rather than all resources in given modules.
+       - Override the order of resources given in modules.
+       - Override routes used for resources given in modules.
+
+       Resources are specified on separate lines.  Resources take one
+       of 4 forms:
+
+       module_name
+          Use the resources from the given module.
+
+       resource
+          Use the named resource.
+
+          The resource is of the form: modulename:expression.  The
+          object is obtained by evaluating the expression in the named
+          module.
+
+       route -> resource
+          The given route, possibly with placeholders, is
+          handled by the given resource.
+
+          The resource is of the form: modulename:expression.
+
+          The object named by the resource must meet one of the following
+          conditions:
+
+          - It was created using one of the bobo decorators:
+            ``resource``, ``post``, ``query``, or ``subroute``.
+
+          - It has ``bobo_reroute`` method that takes the given route
+            and returns a new resource. (The bobo decorators provide this.)
+
+          - It is a class, in which case it is treated as a subroute.
+
+          Newlines may be included between the"->" and the resource, allowing
+          the specification to be split over multiple lines.
+
+       route +> resource
+          The given route, which may not have placeholder, is added as
+          a prefix of the given resource's route.
+
+          The resource is of the form: modulename:expression, or just
+          modulename.
+
+          Newlines may be included between the"+>" and the resource, allowing
+          the specification to be split over multiple lines.
+
+    Bobo also used the following options:
+
+    bobo_configuration
+       Specify one or more (whitespace-delimited) callables to be
+       called with the configuration data passed to the application.
+
+       Each callable is of the form: module_name:global_name
+
+    bobo_errors
+       Specify an object to be used for generating error responses.
+       The value must be a module name or an object name of the form:
+       ``modulename:expression``.  The object must have the
+       callable attributes:
+
+       not_found(request, method)
+          Generate a response when a resource can't be found.
+
+          This should return a 404 response.
+
+       method_not_allowed(request, method, methods)
+          Generate a response when the resource found doesn't allow the
+          request method.
+
+          This should return a 405 response and set the ``Allowed`` response
+          header to the list of allowed headers.
+
+       missing_form_variable(request, method, name)
+          Generate a response when a form variable is missing.
+
+          The proper response in this situation isn't obvious.
+
+       exception(request, method, ex_info)
+          Generate a response for the exception information given by
+          exc_info.  This method is optional.  Bobo's default behavior
+          is to simply re-raise the exception.
+
+    """
+
+    def __init__(self, DEFAULT=None, **config):
+        if DEFAULT:
+            DEFAULT = dict(DEFAULT)
+            DEFAULT.update(config)
+            config = DEFAULT
+
+        self.config = config
+
+        for name in filter(None, _uncomment(config, 'bobo_configure').split()):
+            _get_global(name)(config)
+
+        bobo_errors = _uncomment(config, 'bobo_errors')
+        if bobo_errors:
+            if ':' in bobo_errors:
+                bobo_errors = _get_global(bobo_errors)
+            else:
+                bobo_errors = _import(bobo_errors)
+            self.not_found = bobo_errors.not_found
+            self.method_not_allowed = bobo_errors.method_not_allowed
+            self.missing_form_variable = bobo_errors.missing_form_variable
+            try:
+                self.exception = bobo_errors.exception
+            except AttributeError:
+                pass
+
+        bobo_resources = _uncomment(config, 'bobo_resources', True)
+        if bobo_resources:
+            self.handlers = _route_config(bobo_resources)
+        else:
+            raise ValueError("Missing bobo_resources option.")
+
+    def bobo_response(self, request, path, method):
+        try:
+            for handler in self.handlers:
+                response = handler(request, path, method)
+                if response is not None:
+                    return response
+            return self.not_found(request, method)
+        except BoboException, exc:
+            return self.build_response(request, method, exc)
+        except MethodNotAllowed, v:
+            return self.method_not_allowed(request, method, v.allowed)
+        except MissingFormVariable, v:
+            return self.missing_form_variable(request, method, v.name)
+        except NotFound, v:
+            return self.not_found(request, method)
+        except bbbbad_errors:
+            raise
+        except Exception:
+            if not hasattr(self, 'exception'):
+                raise
+            return self.exception(request, method, sys.exc_info())
+
+    def __call__(self, environ, start_response):
+        """Handle a WSGI application request.
+        """
+        request = webob.Request(environ)
+        if request.charset is None:
+            # Maybe middleware can be more tricky?
+            request.charset = 'utf8'
+
+        return self.bobo_response(request, request.path_info, request.method
+                                  )(environ, start_response)
+
+    def build_response(self, request, method, data):
+        """Build a response object from raw data.
+
+        This method is used by bobo when an application returns data rather
+        than a response object.  It can be overridden by subclasses to support
+        alternative request implementations. (For example, some implementations
+        may have response objects on a request that influence how a response is
+        generated.)
+
+        The data object has several attributes:
+
+        status
+            Integer HTTP status code
+
+        body
+            Raw body data as returned from an application
+
+        content_type
+            The desired content type
+
+        headers
+            A list of header name/value pairs.
+        """
+
+        content_type = data.content_type
+        response = webob.Response(status=data.status,
+                                  headerlist=data.headers,
+                                  content_type=content_type)
+
+        if method == 'HEAD':
+            return response
+
+        body = data.body
+        if isinstance(body, str):
+            response.body = body
+        elif _json_content_type(content_type):
+            try:
+                import json
+            except ImportError:
+                import simplejson
+                sys.modules['json'] = simplejson
+                json = simplejson
+            response.body = json.dumps(body)
+        elif isinstance(body, unicode):
+            response.unicode_body = body
+        else:
+            raise TypeError('bad response', body, content_type)
+
+        return response
+
+    def not_found(self, request, method):
+        return _err_response(
+            404, method, "Not Found", "Could not find: "+request.path_info)
+
+    def missing_form_variable(self, request, method, name):
+        return _err_response(
+            403, method,
+            "Missing parameter", 'Missing form variable %s' % name)
+
+    def method_not_allowed(self, request, method, methods):
+        return _err_response(
+            405, method,
+            "Method Not Allowed", "Invalid request method: %s" % method,
+            [('Allow', ', '.join(sorted(methods)))])
+
+def _err_response(status, method, title, message, headers=()):
+    response = webob.Response(status=status, headerlist=headers or [])
+    response.content_type = 'text/html; charset=UTF-8'
+    if method != 'HEAD':
+        response.unicode_body = _html_template % (title, message)
+    return response
+
+_html_template = u"""<html>
+<head><title>%s</title></head>
+<body>%s</body>
+</html>
+"""
+
+def redirect(url, status=302, body=None,
+             content_type="text/html; charset=UTF-8"):
+    """Generate a response to redirect to a URL.
+
+    The optional ``status`` argument can be used to supply a status other than
+    302.  The optional ``body`` argument can be used to specify a response
+    body. If not specified, a default body is generated based on the URL given
+    in the ``url`` argument.
+    """
+    if body is None:
+        body = u'See %s' % url
+    response = webob.Response(status=status, headerlist=[('Location', url)])
+    response.content_type = content_type
+    response.unicode_body = body
+    return response
+
+class BoboException(Exception):
+
+    def __init__(self, status, body,
+                 content_type='text/html; charset=UTF-8', headers=None):
+        self.status = status
+        self.body = body
+        self.content_type = content_type
+        self.headers = headers or []
+
+def _scan_module(module_name):
+    module = _import(module_name)
+    bobo_response = getattr(module, 'bobo_response', None)
+    if bobo_response is not None:
+        yield bobo_response
+        return
+
+    resources = []
+    for resource in module.__dict__.itervalues():
+        bobo_response = getattr(resource, 'bobo_response', None)
+        if bobo_response is None:
+            continue
+        # Check for unbound handler and skip
+        if getattr(bobo_response, 'im_self', bobo_response) is None:
+            continue
+
+        order = getattr(resource, 'bobo_order', 0) or _late_base
+        resources.append((order, resource, bobo_response))
+
+    resources.sort()
+    by_route = {}
+    for order, resource, bobo_response in resources:
+        route = getattr(resource, 'bobo_route', None)
+        if route is not None:
+            methods = getattr(resource, 'bobo_methods', 0)
+            if methods != 0:
+                by_methods = by_route.get(route)
+                if not by_methods:
+                    by_methods = by_route[route] = {}
+                    yield _make_br_function_by_methods(route, by_methods)
+                if methods is None:
+                    methods = (methods, )
+                for method in methods:
+                    if method not in by_methods:
+                        by_methods[method] = bobo_response
+                continue
+        yield bobo_response
+
+def _make_br_function_by_methods(route, by_method):
+
+    route_data = _compile_route(route)
+
+    def bobo_response(request, path, method):
+        handler = by_method.get(method)
+        if handler is None:
+            handler = by_method.get(None)
+        if handler is None:
+            data = route_data(request, path)
+            if data is not None:
+                raise MethodNotAllowed(by_method)
+            return None
+
+        return handler(request, path, method)
+
+    return bobo_response
+
+def _uncomment(config, name, split=False):
+    str = config.get(name, '')
+    result = filter(None, (
+        line.split('#', 1)[0].strip()
+        for line in str.strip().split('\n')
+        ))
+    if split:
+        return result
+    return '\n'.join(result)
+
+class _MultiResource(list):
+    def bobo_response(self, request, path, method):
+        for resource in self:
+            r = resource(request, path, method)
+            if r is not None:
+                return r
+
+def resources(resources):
+    """Create a resource from multiple resources
+
+    A new resource is returned that works by searching the given resources in
+    the order they're given.
+    """
+    handlers = _MultiResource()
+    for resource in resources:
+        if isinstance(resource, basestring):
+            if ':' in resource:
+                resource = _get_global(resource)
+            else:
+                resource = _MultiResource(_scan_module(resource))
+        elif getattr(resource, 'bobo_response', None) is None:
+            resource = _MultiResource(_scan_module(resource.__name__))
+
+        handlers.append(resource.bobo_response)
+
+    return handlers
+
+def reroute(route, resource):
+    """Create a new resource from a re-routable resource.
+
+    The resource can be a string, in which case it should be a global
+    name, of the form ``module:expression``.
+    """
+    if isinstance(resource, basestring):
+        resource = _get_global(resource)
+
+    try:
+        bobo_reroute = resource.bobo_reroute
+    except AttributeError:
+        import types
+        if isinstance(resource, (type, types.ClassType)):
+            return Subroute(route, resource)
+        raise TypeError("Expected a reroutable")
+    return bobo_reroute(route)
+
+def preroute(route, resource):
+    """Create a new resource by adding a route prefix
+
+    The given route is used as a subroute that is matched before
+    matching the given resource's route.
+
+    The resource can be a string, in which case it should be a global
+    name, of the form ``module:expression``, or a module name.  If a
+    module name is given, and the module doesn't have a
+    bobo_response function, then a resource is computed that tries
+    each of the resources found in the module in order.
+    """
+    if isinstance(resource, basestring):
+        if ':' in resource:
+            resource = _get_global(resource)
+        else:
+            resource = _MultiResource(_scan_module(resource))
+    elif getattr(resource, 'bobo_response', None) is None:
+        resource = _MultiResource(_scan_module(resource.__name__))
+
+    return Subroute(route, lambda request: resource)
+
+_resource_re = re.compile('\s*([\S]+)\s*([-+]>)\s*(\S+)?\s*$').match
+def _route_config(lines):
+    resources = []
+    lines.reverse()
+    while lines:
+        route = lines.pop()
+        m = _resource_re(route)
+        if m is None:
+            sep = resource = None
+        else:
+            route, sep, resource = m.groups()
+
+        if not resource:
+            if not sep:
+                # route is the resource.
+                if ':' in route:
+                    resources.append(_get_global(route).bobo_response)
+                else:
+                    resources.extend(_scan_module(route))
+                continue
+            else:
+                # line continuation
+                resource = lines.pop()
+
+        if sep == '->':
+            resource = reroute(route, resource)
+        else:
+            resource = preroute(route, resource)
+
+        resources.append(resource.bobo_response)
+
+    return resources
+
+def _get_global(attr):
+    if ':' in attr:
+        mod, attr = attr.split(':', 1)
+    elif not mod:
+        raise ValueError("No ':' in global name", attr)
+    mod = _import(mod)
+    return eval(attr, mod.__dict__)
+
+def _import(module_name):
+    return __import__(module_name, {}, {}, ['*'])
+
+_order = 0
+def order():
+    """Return an integer that can be used to order a resource.
+
+    The function returns a larger integer each time it is called.  A
+    resource can use this to set it's ``bobo_order`` attribute.
+    """
+    global _order
+    _order += 1
+    return _order
+
+_late_base = 1<<99
+def late():
+    """Return an order used for resources that should be searched late.
+
+    The function returns a larger integer each time it is called.  The
+    value is larger than values returned by the order or early
+    functions.
+    """
+    return order() + _late_base
+
+_early_base = -_late_base
+def early():
+    """Return an order used for resources that should be searched early.
+
+    The function returns a larger integer each time it is called.  The
+    value is smaller than values returned by the order or late
+    functions.
+    """
+    return order() + _early_base
+
+_ext_re = re.compile('/(\w+)').search
+class _Handler:
+
+    partial = False
+
+    def __init__(self, route, handler,
+                 method=None, params=None, check=None, content_type=None,
+                 order_=None):
+        if route is None:
+            route = '/'+handler.__name__
+            ext = _ext_re(content_type)
+            if ext:
+                route += '.'+ext.group(1)
+        self.bobo_route = route
+        if isinstance(method, basestring):
+            method = (method, )
+        self.bobo_methods = method
+
+        self.handler = handler
+        self.bobo_original = getattr(handler, 'bobo_original', handler)
+        bobo_sub_find = getattr(handler, 'bobo_response', None)
+        if bobo_sub_find is not None:
+            self.bobo_sub_find = bobo_sub_find
+
+        self.content_type = content_type
+        self.params = params
+        self.check = check
+        if order_ is None:
+            order_ = order()
+        self.bobo_order = order_
+
+    @property
+    def bobo_handle(self):
+        func = original = self.bobo_original
+        if self.params:
+            func = _make_caller(func, self.params)
+        func = _make_bobo_handle(func, original, self.check, self.content_type)
+        self.__dict__['bobo_handle'] = func
+        return func
+
+    @property
+    def match(self):
+        route_data = _compile_route(self.bobo_route, self.partial)
+        methods = self.bobo_methods
+        if methods is None:
+            return route_data
+
+        def match(request, path, method):
+            data = route_data(request, path)
+            if data is not None:
+                if method not in methods:
+                    raise MethodNotAllowed(methods)
+                return data
+
+        self.__dict__['match'] = match
+        return match
+
+    def bobo_response(self, *args):
+        request, path, method = args[-3:]
+        route_data = self.match(request, path, method)
+        if route_data is None:
+            return self.bobo_sub_find(*args)
+
+        return self.bobo_handle(*args[:-2], **route_data)
+
+    def bobo_sub_find(self, *args):
+        pass
+
+    def __call__(self, *args, **kw):
+        return self.bobo_original(*args, **kw)
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return _UnboundHandler(self, class_)
+        return _BoundHandler(self, inst, class_)
+
+    @property
+    def func_code(self):
+        return self.bobo_original.func_code
+
+    @property
+    def func_defaults(self):
+        return self.bobo_original.func_defaults
+
+    @property
+    def __name__(self):
+        return self.bobo_original.__name__
+
+    def bobo_reroute(self, route):
+        return self.__class__(route, self.bobo_original, self.bobo_methods,
+                              self.params, self.check, self.content_type)
+
+class _UnboundHandler:
+
+    im_self = None
+
+    def __init__(self, handler, class_):
+        self.im_func = handler
+        self.im_class = class_
+
+    def __get__(self, inst, class_):
+        self._check_args(args)
+        if inst is None:
+            return self
+        return _BoundHandler(self.im_func, inst, self.im_class_)
+
+    def __repr__(self):
+        return "<unbound resource %s.%s>" % (
+            self.im_class.__name__,
+            self.im_func.__name__,
+            )
+
+    def _check_args(self, args):
+        if not args or not isinstance(args[0], self.im_class):
+            raise TypeError("Need %s initial argument"
+                            % self.im_class.__name__)
+
+    def __call__(self, *args, **kw):
+        self._check_args(args)
+        return self.im_func(*args, **kw)
+
+class _BoundHandler:
+
+    def __init__(self, handler, inst, class_):
+        if not isinstance(inst, class_):
+            raise TypeError("Can't bind", inst, class_)
+        self.im_func = handler
+        self.im_self = inst
+        self.im_class = class_
+
+    def __repr__(self):
+        return "<bound resource %s.%s of %r>" % (
+            self.im_class.__name__,
+            self.im_func.__name__,
+            self.im_self,
+            )
+
+    def bobo_response(self, *args):
+        return self.im_func.bobo_response(self.im_self, *args)
+
+    def __call__(self, *args, **kw):
+        return self.im_func(self.im_self, *args, **kw)
+
+def _handler(route, func=None, **kw):
+    if func is None:
+        if route is None or isinstance(route, basestring):
+            return lambda f: _handler(route, f, **kw)
+        func = route
+        route = None
+    elif route is not None:
+        assert isinstance(route, basestring)
+        if route and not route.startswith('/'):
+            raise ValueError("Non-empty routes must start with '/'.", route)
+
+    return _Handler(route, func, **kw)
+
+def resource(route=None, method=('GET', 'POST', 'HEAD'),
+             content_type=_default_content_type, check=None, order=None):
+    """Create a resource
+
+    This function is used as a decorator to define a resource. It can be applied
+    to any kind of callable, not just a function.
+
+    Arguments:
+
+    route
+        The route to match against a request URL to determine
+        if the decorated callable should be used to satisfy a
+        request.
+
+        if omitted, a route will be computed using the decorated
+        callable's name with the content_type subtype used as an extension.
+
+    method
+        The HTTP request method or methods that can be used. This can be either
+        a string giving a single method name, or a sequence of strings.
+
+    content_type
+        The content_type for the response.
+
+        The content type is ignored if the callable returns a response object.
+
+    check
+        A check function.
+
+        If provided, the check function (or callable) will be called
+        before the decorated callable.  The check function is passed
+        an instance, a request, and the decorated callable.  If the
+        resource is a method, then first argument is the instance the
+        method was called on, otherwise it is None.  If the check
+        function returns a response, the response will be used instead
+        of calling the decorated callable.
+
+    order
+        The order controls how resources are searched when matching
+        URLs.  Normally, resources are searched in order of
+        evaluation.  Passing the result of calling ``bobo.early`` or
+        ``bobo.late`` can cause resources to be searched early or late.
+
+    The function may be used as a decorator directly without calling
+    it. For example::
+
+       @bobo.resource
+       def example(request):
+           ...
+
+    is equivalent to::
+
+       @bobo.resource()
+       def example(request):
+           ...
+
+    The callable must take a request object as the first argument.  If the
+    route has placeholders, then the callable must accept named parameters
+    corresponding to the placeholders.  The named parameters must have defaults
+    for any optional placeholders.
+
+    Unlike the post and query decorators, this decorator doesn't introspect the
+    callable it's applied to.
+    """
+    return _handler(route, method=method, check=check,
+                    content_type=content_type, order_=order)
+
+def post(route=None, method=['POST', 'PUT'],
+         content_type=_default_content_type, check=None, order=None):
+    """Create a resource that passes POST data as arguments
+
+    This function is used as a function decorator to define a resource.
+
+    Arguments:
+
+    route
+        The route to match against a request URL to determine
+        if the decorated callable should be used to satisfy a
+        request.
+
+        if omitted, a route will be computed using the decorated
+        callable's name with the content_type subtype used as an extension.
+
+    method
+        The HTTP request method or methods that can be used. This can
+        be either a string giving a single method name, or a sequence
+        of strings.
+
+        The method argument defaults to the string ``'POST'``.
+
+    content_type
+        The content_type for the response.
+
+        The content type is ignored if the callable returns a response object.
+
+    check
+        A check function.
+
+        If provided, the check function (or callable) will be called
+        before the decorated function.  The check function is passed
+        an instance, a request, and the decorated function.  If the
+        resource is a method, then first argument is the instance the
+        method was called on, otherwise it is None.  If the check
+        function returns a response, the response will be used instead
+        of calling the decorated function.
+
+    order
+        The order controls how resources are searched when matching
+        URLs.  Normally, resources are searched in order of
+        evaluation.  Passing the result of calling ``bobo.early`` or
+        ``bobo.late`` can cause resources to be searched early or late.
+
+    The function may be used as a decorator directly without calling
+    it. For example::
+
+       @bobo.post
+       def example():
+           ...
+
+    is equivalent to::
+
+       @bobo.post()
+       def example():
+           ...
+
+    The callable the decorator is applied to is analyzed to determine it's
+    signature.  When the callable is called, the request, route data and
+    request form data are used to satisfy any named arguments in the callable's
+    signature.  For example, in the case of::
+
+       @bobo.post('/:a')
+       def example(bobo_request, a, b, c=None):
+           ...
+
+    when handling a request for: ``http://localhost/x``, with a post
+    body of ``b=1``, the request is passed to the ``bobo_request``
+    argument. the route data value ``'x'`` is passed to the argument
+    ``a``, and the form data ``1`` is passed for ``b``.
+
+    Standard function metadata attributes ``func_code`` and ``func_defaults``
+    are used to determine the signature and required arguments. The method
+    attribute, ``im_func`` is used to determine if the callable is a method, in
+    which case the first argument found in the signature is ignored.
+    """
+    return _handler(route, method=method, params='POST', check=check,
+                    content_type=content_type, order_=order)
+
+def query(route=None, method=('GET', 'POST', 'HEAD'),
+          content_type=_default_content_type, check=None, order=None):
+    """Create a resource that passes form data as arguments
+
+    Create a decorator that, when applied to a callable, creates a
+    resource.
+
+    Arguments:
+
+    route
+        The route to match against a request URL to determine if the decorated
+        callable should be used to satisfy a request.
+
+        if omitted, a route will be computed using the decorated
+        callable's name with the content_type subtype used as an extension.
+
+    method
+        The HTTP request method or methods that can be used. This can
+        be either a string giving a single method name, or a sequence
+        of strings.
+
+        The method argument defaults to the tuple ``('GET', 'HEAD', 'POST')``.
+
+    content_type
+        The content_type for the response.
+
+        The content type is ignored if the callable returns a response object.
+
+    check
+        A check function.
+
+        If provided, the check function (or callable) will be called
+        before the decorated function.  The check function is passed
+        an instance, a request, and the decorated function.  If the
+        resource is a method, then first argument is the instance the
+        method was called on, otherwise it is None.  If the check
+        function returns a response, the response will be used instead
+        of calling the decorated function.
+
+    order
+        The order controls how resources are searched when matching
+        URLs.  Normally, resources are searched in order of
+        evaluation.  Passing the result of calling ``bobo.early`` or
+        ``bobo.late`` can cause resources to be searched early or late.
+
+    The function may be used as a decorator directly without calling
+    it. For example::
+
+       @bobo.query
+       def example():
+           ...
+
+    is equivalent to::
+
+       @bobo.query()
+       def example():
+           ...
+
+    The callable the decorator is applied to is analyzed to determine it's
+    signature.  When the callable is called, the request, route data and
+    request form data are used to satisfy any named arguments in the callable's
+    signature.  For example, in the case of::
+
+       @bobo.query('/:a')
+       def example(bobo_request, a, b, c=None):
+           ...
+
+    when handling a request for: ``http://localhost/x?b=1``,
+    the request is passed to the ``bobo_request`` argument. the route
+    data value ``'x'`` is passed to the argument ``a``, and the form
+    data ``1`` is passed for ``b``.
+
+    Standard function metadata attributes ``func_code`` and
+    ``func_defaults`` are used to determine the signature and required
+    arguments. The method attribute, ``im_func`` is used to determine
+    if the callable is a method, in which case the first argument found
+    in the signature is ignored.
+    """
+    return _handler(route, method=method, params='params', check=check,
+                    content_type=content_type, order_=order)
+
+route_re = re.compile(r'(/:[a-zA-Z]\w*\??)(\.[^/]+)?')
+def _compile_route(route, partial=False):
+    assert route.startswith('/') or not route
+    pat = route_re.split(route)
+    pat.reverse()
+    rpat = []
+    prefix = pat.pop()
+    if prefix:
+        rpat.append(re.escape(prefix))
+    while pat:
+        name = pat.pop()[2:]
+        optional = name.endswith('?')
+        if optional:
+            name = name[:-1]
+        name = '/(?P<%s>[^/]*)' % name
+        ext = pat.pop()
+        if ext:
+            name += re.escape(ext)
+        if optional:
+            name = '(%s)?' % name
+        rpat.append(name)
+        s = pat.pop()
+        if s:
+            rpat.append(re.escape(s))
+
+    if partial:
+        match = re.compile(''.join(rpat)).match
+        def route_data(request, path, method=None):
+            m = match(path)
+            if m is None:
+                return m
+            path = path[len(m.group(0)):]
+            if path and not path.startswith('/'):
+                path = '/'+path
+            return (dict(item for item in m.groupdict().iteritems()
+                         if item[1] is not None),
+                    path,
+                    )
+    else:
+        match = re.compile(''.join(rpat)+'$').match
+        def route_data(request, path, method=None):
+            m = match(path)
+            if m is None:
+                return m
+            return dict(item for item in m.groupdict().iteritems()
+                        if item[1] is not None)
+
+    return route_data
+
+def _make_bobo_handle(func, original, check, content_type):
+
+    def handle(*args, **route):
+        if check is not None:
+            if len(args) == 1:
+                result = check(None, args[0], original)
+            else:
+                result = check(args[0], args[1], original)
+            if result is not None:
+                return result
+        result = func(*args, **route)
+        if hasattr(result, '__call__'):
+            return result
+
+        raise BoboException(200, result, content_type)
+
+    return handle
+
+def _make_caller(obj, paramsattr):
+    wrapperCount = 0
+    unwrapped = obj
+
+    for i in range(10):
+        bases = getattr(unwrapped, '__bases__', None)
+        if bases is not None:
+            raise TypeError("mapply() can not call class constructors")
+
+        im_func = getattr(unwrapped, 'im_func', None)
+        if im_func is not None:
+            unwrapped = im_func
+            wrapperCount += 1
+        elif getattr(unwrapped, 'func_code', None) is not None:
+            break
+        else:
+            unwrapped = getattr(unwrapped, '__call__' , None)
+            if unwrapped is None:
+                raise TypeError("mapply() can not call %s" % repr(obj))
+    else:
+        raise TypeError("couldn't find callable metadata, mapply() error on %s"
+                        % repr(obj))
+
+    code = unwrapped.func_code
+    defaults = unwrapped.func_defaults
+    names = code.co_varnames[wrapperCount:code.co_argcount]
+    nargs = len(names)
+    nrequired = len(names)
+    if defaults:
+        nrequired -= len(defaults)
+
+    # XXX maybe handle f(..., **kw)?
+
+    def bobo_apply(*pargs, **route):
+        request = pargs[-1]
+        pargs = pargs[:-1] # () or (self, )
+        params = getattr(request, paramsattr)
+        kw = {}
+        for index in range(len(pargs), nargs):
+            name = names[index]
+            if name == 'bobo_request':
+                kw[name] = request
+                continue
+
+            v = route.get(name)
+            if v is None:
+                v = params.getall(name)
+                if not v:
+                    if index < nrequired:
+                        raise MissingFormVariable(name)
+                    continue
+                if len(v) == 1:
+                    v = v[0]
+
+            kw[name] = v
+
+        return obj(*pargs, **kw)
+
+    return bobo_apply
+
+class Subroute(_Handler):
+
+    partial = True
+
+    def __init__(self, route, handler):
+        _Handler.__init__(self, route, handler)
+
+    def bobo_response(self, *args):
+        request, path, method = args[-3:]
+        route_data = self.match(request, path)
+        if route_data is None:
+            return self.bobo_sub_find(*args)
+
+        route_data, path = route_data
+        resource = self.bobo_original(*args[:-2], **route_data)
+        if resource is not None:
+            return resource.bobo_response(request, path, method)
+
+    def bobo_reroute(self, route):
+        return self.__class__(route, self.bobo_original)
+
+def _subroute(route, ob, scan):
+    if scan:
+        scan_class(ob)
+        return _subroute_class(route, ob)
+
+    import types
+    if isinstance(ob, (type, types.ClassType)):
+        return _subroute_class(route, ob)
+    return Subroute(route, ob)
+
+def subroute(route=None, scan=False, order=None):
+    """Create a resource that matches a URL in multiple steps
+
+    If called with a route or without any arguments, subroute returns
+    an object that should then be called with a resource factory.  The
+    resource factory will be called with a request and route data and
+    should return a resource object.  For example::
+
+       @subroute('/:employee_id', scan=True)
+       class EmployeeView:
+           def __init__(self, request, employee_id):
+               ...
+
+    If no route is supplied, the ``__name__`` attribute of the callable
+    is used.
+
+    The resource factory may return None to indicate that a resource can't be
+    found on the subroute.
+
+    The scan argument, if given, should be given as a keyword
+    parameter. It defaults to False.  If True, then the callable
+    should be a class and a ``bobo_response`` instance method will be
+    added to the class that calls resources found by scanning the
+    class and its base classes.  Passing a True ``scan``
+    argument is equivalent to calling ``scan_class``::
+
+       @subroute('/:employee_id')
+       @scan_class
+       class EmployeeView:
+           def __init__(self, request, employee_id):
+
+    ``subroute`` can be passed a callable directly, as in::
+
+       @subroute
+       class Employees:
+           def __init__(self, request):
+               ...
+           def bobo_response(self, request, path, method):
+               ...
+
+    Which is equivalent to calling ``subroute`` without the callable
+    and then passing the callable to the route::
+
+       @subroute()
+       class Employees:
+           def __init__(self, request):
+               ...
+           def bobo_response(self, request, path, method):
+               ...
+
+    Note that in the example above, the scan argument isn't passed and
+    defaults to False, so the class has to provide it's own
+    ``bobo_response`` method (or otherwise arrange that instances have one).
+
+    The optional ``order`` parameter controls how resources are
+    searched when matching URLs.  Normally, resources are searched in
+    order of evaluation.  Passing the result of calling ``bobo.early``
+    or ``bobo.late`` can cause resources to be searched early or late.
+    It is usually a good idea to use ``bobo.late`` for subroutes that
+    match any URL.
+    """
+
+    if route is None:
+        return lambda ob: _subroute('/'+ob.__name__, ob, scan)
+    if isinstance(route, basestring):
+        return lambda ob: _subroute(route, ob, scan)
+    return _subroute('/'+route.__name__, route, scan)
+
+class _subroute_class_method(object):
+    def __init__(self, class_, class_func, inst_func):
+        self.class_ = class_
+        self.class_func = class_func
+        self.inst_func = inst_func
+
+    def __get__(self, inst, class_):
+        if inst is None:
+            return self.class_func.__get__(class_, type(class_))
+        inst_func = self.inst_func
+        if inst_func is None:
+            try:
+                return super(self.class_, inst).bobo_response
+            except TypeError:
+                raise AttributeError(
+                    "%s instance has no attribute 'bobo_response'"
+                    % inst.__class__.__name__)
+        return inst_func.__get__(inst, class_)
+
+def _subroute_class(route, ob):
+    matchers = ob.__dict__.get('bobo_subroute_matchers', None)
+    if matchers is None:
+        matchers = ob.bobo_subroute_matchers = []
+    matchers.append(_compile_route(route, True))
+
+    br_orig = getattr(ob, 'bobo_response', None)
+    if br_orig is not None:
+        if br_orig.im_self is not None:
+            # we found another class method.
+            if len(matchers) > 1:
+                # stacked matchers, so we're done
+                return ob
+            if (('bobo_response' in ob.__dict__)
+                or not hasattr(ob, '__mro__')):
+                del ob.bobo_subroute_matchers
+                raise TypeError("bobo_response class method already defined")
+            # ok, it's inherited, we'll use super if necessary
+            br_orig = None
+
+    def bobo_response(self, request, path, method):
+        for matcher in matchers:
+            route_data = matcher(route, path)
+            if route_data:
+                route_data, path = route_data
+                resource = ob(request, **route_data)
+                if resource is not None:
+                    return resource.bobo_response(request, path, method)
+
+    ob.bobo_response = _subroute_class_method(ob, bobo_response, br_orig)
+    return ob
+
+def scan_class(class_):
+    """Create an instance bobo_response method for a class
+
+    Scan a class (including its base classes) for resources and generate
+    a bobo_response method of the class that calls them.
+    """
+
+    try:
+        mro = class_.__mro__
+    except AttributeError:
+        mro = type('C', (object, class_), {}).__mro__
+
+    resources = {}
+    for c in reversed(mro):
+        for name, resource in c.__dict__.iteritems():
+            br = getattr(resource, 'bobo_response', None)
+            if br is None:
+                continue
+            order = getattr(resource, 'bobo_order', 0) or _late_base
+            resources[name] = order, resource
+
+    by_route = {}
+    handlers = []
+    for (order, (name, resource)) in sorted(
+        (order, (name, resource))
+        for (name, (order, resource)) in resources.iteritems()
+        ):
+        route = getattr(resource, 'bobo_route', None)
+        if route is not None:
+            methods = getattr(resource, 'bobo_methods', 0)
+            if methods != 0:
+                by_methods = by_route.get(route)
+                if not by_methods:
+                    by_methods = by_route[route] = {}
+                    handlers.append(
+                        _make_br_method_by_methods(route, by_methods))
+                if methods is None:
+                    methods = (methods, )
+                for method in methods:
+                    if method not in by_methods:
+                        by_methods[method] = name
+                continue
+
+        handlers.append(_make_br_method_for_name(name))
+
+    def bobo_response(self, request, path, method):
+        for handler in handlers:
+            found = handler(self, request, path, method)
+            if found is not None:
+                return found
+
+    old = class_.__dict__.get('bobo_response')
+    if isinstance(old, _subroute_class_method):
+        old.inst_func = bobo_response
+    else:
+        class_.bobo_response = bobo_response
+
+    return class_
+
+def _make_br_method_for_name(name):
+    return (lambda self, request, path, method:
+            getattr(self, name).bobo_response(request, path, method)
+            )
+
+def _make_br_method_by_methods(route, methods):
+    route_data = _compile_route(route)
+
+    def bobo_response(self, request, path, method):
+        name = methods.get(method)
+        if name is None:
+            name = methods.get(None)
+        if name is None:
+            data = route_data(request, path)
+            if data is not None:
+                raise MethodNotAllowed(methods)
+            return None
+
+        return getattr(self, name).bobo_response(request, path, method)
+
+    return bobo_response
+
+class MissingFormVariable(Exception):
+    def __init__(self, name):
+        self.name = name
+
+    def __str__(self):
+        return self.name
+
+class MethodNotAllowed(Exception):
+    def __init__(self, allowed):
+        self.allowed = sorted(allowed)
+
+    def __str__(self):
+        return "Allowed: %s" % repr(self.allowed)[1:-1]
+
+class NotFound(Exception):
+    """A resource cannot be found.
+
+    This exception can be raised by application code.
+    """


Property changes on: bobo/trunk/bobo/src/bobo.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobo/src/boboserver.py
===================================================================
--- bobo/trunk/bobo/src/boboserver.py	                        (rev 0)
+++ bobo/trunk/bobo/src/boboserver.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,256 @@
+##############################################################################
+#
+# Copyright 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.
+#
+##############################################################################
+"""Create WSGI-based web applications.
+"""
+
+__all__ = (
+    'Debug',
+    'Reload',
+    'server',
+    'static',
+    )
+
+__metaclass__ = type
+
+import bobo
+import optparse
+import os
+import mimetypes
+import pdb
+import re
+import sys
+import traceback
+import types
+import webob
+
+if sys.version_info < (2, 5):
+    # can't use wsgiref, use paste
+    import paste.httpserver
+
+    def run_server(app, port):
+        paste.httpserver.server_runner(app, {}, port=port)
+else:
+    import wsgiref.simple_server
+
+    def run_server(app, port):
+        wsgiref.simple_server.make_server('', port, app).serve_forever()
+
+
+class Directory:
+
+    def __init__(self, root, path=None):
+        self.root = os.path.abspath(root)+os.path.sep
+        self.path = path or root
+
+    @bobo.query('')
+    def base(self, bobo_request):
+        return bobo.redirect(bobo_request.url+'/')
+
+    @bobo.query('/')
+    def index(self):
+        links = []
+        for name in os.listdir(self.path):
+            if os.path.isdir(os.path.join(self.path, name)):
+                name += '/'
+            links.append('<a href="%s">%s</a>' % (name, name))
+        return """<html>
+        <head><title>%s</title></head>
+        <body>
+          %s
+        </body>
+        </html>
+        """ % (self.path[len(self.root):], '<br>\n  '.join(links))
+
+    @bobo.subroute('/:name')
+    def traverse(self, request, name):
+        path = os.path.abspath(os.path.join(self.path, name))
+        if not path.startswith(self.root):
+            raise bobo.NotFound
+        if os.path.isdir(path):
+            return Directory(self.root, path)
+        else:
+            return File(path)
+
+bobo.scan_class(Directory)
+
+class File:
+    def __init__(self, path):
+        self.path = path
+
+    @bobo.query('')
+    def base(self, bobo_request):
+        response = webob.Response()
+        content_type = mimetypes.guess_type(self.path)[0]
+        if content_type is not None:
+            response.content_type = content_type
+        try:
+            response.body = open(self.path).read()
+        except IOError:
+            raise bobo.NotFound
+
+        return response
+
+bobo.scan_class(File)
+
+def static(route, directory):
+    """Create a resource that serves static files from a directory
+    """
+    return bobo.preroute(route, Directory(directory))
+
+class Reload:
+    """Module-reload middleware
+
+    This middleware can *only* be used with bobo applications.  It
+    monitors a list of modules given by a ``modules`` keyword
+    parameter and configuration option.  When a module changes, it
+    reloads the module and reinitializes the bobo application.
+
+    The Reload class implements the `Paste Deployment
+    filter_app_factory protocol
+    <http://pythonpaste.org/deploy/#paste-filter-app-factory>`_ and is
+    exported as a ``paste.filter_app_factory`` entry point named ``reload``.
+    """
+
+    def __init__(self, app, default, modules):
+        if not isinstance(app, bobo.Application):
+            raise TypeError("Reload can only be used with bobo applications")
+        self.app = app
+
+        self.mtimes = mtimes = {}
+        for name in modules.split():
+            module = sys.modules[name]
+            mtimes[name] = (module.__file__, os.stat(module.__file__).st_mtime)
+
+    def __call__(self, environ, start_response):
+        for name, (path, mtime) in self.mtimes.iteritems():
+            if os.stat(path).st_mtime != mtime:
+                print 'Reloading', name
+                execfile(path, sys.modules[name].__dict__)
+                self.app.__init__(self.app.config)
+                self.mtimes[name] = path, os.stat(path).st_mtime
+
+        return self.app(environ, start_response)
+
+class Debug:
+    """Post-mortem debugging middleware
+
+    This middleware catches uncaught exceptions and runs the
+    ``pdb.post_mortem`` debugging function, helping you to debug
+    exceptions raised by your application.
+
+    The Debug class implements the `Paste Deployment
+    filter_app_factory protocol
+    <http://pythonpaste.org/deploy/#paste-filter-app-factory>`_ and is
+    exported as a ``paste.filter_app_factory`` entry point named ``debug``.
+    """
+
+    def __init__(self, app, default=None):
+        self.app = app
+
+    def __call__(self, environ, start_response):
+        try:
+            return self.app(environ, start_response)
+        except:
+            traceback.print_exception(*sys.exc_info())
+            pdb.post_mortem(sys.exc_info()[2])
+            raise
+
+_mod_re = re.compile(
+    "(^|>) *(\w[a-zA-Z_.]*)(:|$)"
+    ).search
+
+def server(args=None, Application=bobo.Application):
+    """Bobo development server
+
+    The server function implements the bobo development server.
+
+    It is exported as a ``console_script`` entry point named ``bobo``.
+
+    An alternate application can be passed in to run the server with a
+    different application implementation as long as application passed
+    in subclasses bobo.Application.
+    """
+
+    if args is None:
+        import logging; logging.basicConfig()
+        args = sys.argv[1:]
+
+    usage = "%prog [options] name=value ..."
+    if sys.version_info >= (2, 5):
+        usage = 'Usage: ' + usage
+    parser = optparse.OptionParser(usage)
+    parser.add_option(
+        '--port', '-p', type='int', dest='port', default=8080,
+        help="Specify the port to listen on.")
+    parser.add_option(
+        '--file', '-f', dest='file', action='append',
+        help="Specify a source file to publish.")
+    parser.add_option(
+        '--resource', '-r', dest='resource', action='append',
+        help=("Specify a resource, such as a module or module global,"
+              " to publish."))
+    parser.add_option(
+        '--debug', '-D', action='store_true', dest='debug',
+        help="Run the post mortem debugger for uncaught exceptions.")
+    parser.add_option(
+        '-c', '--configure', dest='configure',
+        help="Specify the bobo_configure option.")
+    parser.add_option(
+        '-s', '--static', dest='static', action='append',
+        help=("Specify a route and directory (route=directory)"
+              " to serve statically"))
+
+    def error(message):
+        sys.stderr.write("Error:\n%s\n\n" % message)
+        parser.parse_args(['-h'])
+
+    options, pos = parser.parse_args(args)
+
+    resources = options.resource or []
+    mname = 'bobo__main__'
+    for path in options.file or ():
+        module = types.ModuleType(mname)
+        module.__file__ = path
+        execfile(module.__file__, module.__dict__)
+        sys.modules[module.__name__] = module
+        resources.append(module.__name__)
+        mname += '_'
+
+    for s in options.static or ():
+        route, path = s.split('=', 1)
+        resources.append("boboserver:static(%r,%r)" % (route, path))
+
+    if not resources:
+        error("No resources were specified.")
+
+    if [a for a in pos if '=' not in a]:
+        error("Positional arguments must be of the form name=value.")
+    app_options = dict(a.split('=', 1) for a in pos)
+
+    module_names = [m.group(2)
+                    for m in map(_mod_re, resources)
+                    if m is not None]
+
+    if options.configure:
+        if (':' not in options.configure) and module_names:
+            options.configure = module_names[0]+':'+options.configure
+        app_options['bobo_configure'] = options.configure
+
+    app = Application(app_options, bobo_resources='\n'.join(resources))
+    app = Reload(app, None, ' '.join(module_names))
+    if options.debug:
+        app = Debug(app)
+
+    print "Serving %s on port %s..." % (resources, options.port)
+    run_server(app, options.port)


Property changes on: bobo/trunk/bobo/src/boboserver.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/setup.py
===================================================================
--- bobo/trunk/bobodoctestumentation/setup.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/setup.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,38 @@
+##############################################################################
+#
+# Copyright 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.
+#
+##############################################################################
+name = 'bobodoctestumentation'
+version = '0'
+
+long_description = """\
+The bobo documentation and tests are broken out into a separate project
+to keep the bobo distribution as small as possible.
+"""
+
+from setuptools import setup
+
+setup(
+    name = name,
+    version = version,
+    author = "Jim Fulton",
+    author_email = "jim at zope.com",
+    description = "Bobo tests and documentation",
+    license = "ZPL 2.1",
+    url='http://www.python.org/pypi/'+name,
+    long_description=long_description,
+
+    packages = ['bobodoctestumentation'],
+    package_dir = {'':'src'},
+    package_data = {'bobodoctestumentation': ['*.txt', '*.test']},
+    install_requires = ['manuel ==1.0.0a2', 'simplejson'],
+    )


Property changes on: bobo/trunk/bobodoctestumentation/setup.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/Makefile
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/Makefile	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/Makefile	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,88 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = ../../../bin/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/bobo.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile _build/qthelp/bobo.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: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/Makefile
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/__init__.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/__init__.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/__init__.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1 @@
+#


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/__init__.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobo.svg
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobo.svg	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobo.svg	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   inkscape:export-filename="/Users/jim/p/bobo/dev/bobodoctestumentation/src/bobodoctestumentation/bobo.png"
+   inkscape:export-xdpi="18.312744"
+   inkscape:export-ydpi="18.312744"
+   sodipodi:docname="bobo.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.6519321"
+     inkscape:cx="637.53202"
+     inkscape:cy="741.7666"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1680"
+     inkscape:window-height="1002"
+     inkscape:window-x="0"
+     inkscape:window-y="22" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#211f4d;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       d="M 538.69353,73.757553 C 510.36293,158.49963 533.16615,207.59773 599.28009,259.46853 C 682.73411,320.89274 682.06667,385.62621 662.50085,464.93598"
+       id="path2383"
+       sodipodi:nodetypes="ccc"
+       inkscape:export-filename="/Users/jim/p/bobo/dev/bobodoctestumentation/src/bobodoctestumentation/bobo.png"
+       inkscape:export-xdpi="18.312744"
+       inkscape:export-ydpi="18.312744" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#211f4d;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       d="M 616.40238,233.12655 C 415.76921,462.88133 265.86922,422.62166 98.782434,460.98468"
+       id="path2385"
+       sodipodi:nodetypes="cc"
+       inkscape:export-filename="/Users/jim/p/bobo/dev/bobodoctestumentation/src/bobodoctestumentation/bobo.png"
+       inkscape:export-xdpi="18.312744"
+       inkscape:export-ydpi="18.312744" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#211f4d;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 178.28798,547.43362 C 276.31451,527.56142 393.42638,496.31351 419.67499,394.29224 C 350.43749,274.25921 382.96203,232.06727 416.20332,214.68716 C 474.22527,190.20839 515.8921,248.44765 489.96087,281.85922 C 472.14423,294.5101 455.54034,295.03352 441.2282,272.63952"
+       id="path2387"
+       sodipodi:nodetypes="ccccc"
+       inkscape:export-filename="/Users/jim/p/bobo/dev/bobodoctestumentation/src/bobodoctestumentation/bobo.png"
+       inkscape:export-xdpi="18.312744"
+       inkscape:export-ydpi="18.312744" />
+    <g
+       id="g3221"
+       transform="matrix(1.215887,0,0,1.215887,-130.66318,-53.360583)">
+      <path
+         sodipodi:open="true"
+         transform="matrix(-0.1575875,0.4518636,0.581641,0.1597469,259.47541,-128.26184)"
+         sodipodi:end="7.2278659"
+         sodipodi:start="2.5678671"
+         d="M 432.59507,803.01246 A 61.24511,44.122822 0 1 1 519.92365,814.8173"
+         sodipodi:ry="44.122822"
+         sodipodi:rx="61.24511"
+         sodipodi:cy="779.06415"
+         sodipodi:cx="484.03391"
+         id="path3183"
+         style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#211f4d;stroke-width:13.70801735;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         sodipodi:nodetypes="cc"
+         id="path3185"
+         d="M 655.80516,196.94293 C 646.01826,203.00446 639.0412,211.04151 652.21393,238.76826"
+         style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:7.87183571;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobo.svg
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.html
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.html	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.html	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,59 @@
+<html>
+  <head>
+    <title>Bobocalc</title>
+
+    <style type="text/css">
+      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
+      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
+    </style>
+
+    <script
+       type="text/javascript"
+       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
+       djConfig="parseOnLoad: true, isDebug: true, debugAtAllCosts: true"
+       ></script>
+
+    <script type="text/javascript">
+      dojo.require("dojo.parser");
+      dojo.require("dijit.form.Button");
+      dojo.require("dijit.form.ValidationTextBox");
+
+      bobocalc = function () {
+          function op(url) {
+              dojo.xhrGet({
+                  url: url, handleAs: 'json',
+                  content: {
+                      value: dojo.byId('value').textContent,
+                      input: dijit.byId('input').value
+                  },
+                  load: function(data) {
+                      dojo.byId('value').textContent = data.value;
+                      dojo.byId('input').value = '';
+                  }
+              });
+          }
+          return {
+              add: function () { op('add.json'); },
+              sub: function () { op('sub.json'); },
+              clear: function () { dojo.byId('value').textContent = 0; }
+          };
+      }();
+    </script>
+
+  </head>
+  <body class="tundra">
+    <h1><em>Bobocalc</em></h1>
+
+    Value: <span id="value">0</span>
+    <form>
+      <label for="input">Input:</label>
+      <input
+         type="text" id="input" name="input"
+         dojoType="dijit.form.ValidationTextBox" regExp="[0-9]+"
+         />
+      <button dojoType="dijit.form.Button" onClick="bobocalc.clear">C</button>
+      <button dojoType="dijit.form.Button" onClick="bobocalc.add">+</button>
+      <button dojoType="dijit.form.Button" onClick="bobocalc.sub">-</button>
+    </form>
+  </body>
+</html>


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.html
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,16 @@
+import bobo, os
+
+ at bobo.query('/')
+def html():
+    return open(os.path.join(os.path.dirname(__file__),
+                             'bobocalc.html')).read()
+
+ at bobo.query(content_type='application/json')
+def add(value, input):
+    value = int(value)+int(input)
+    return dict(value=value)
+
+ at bobo.query(content_type='application/json')
+def sub(value, input):
+    value = int(value)-int(input)
+    return dict(value=value)


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,43 @@
+Sample ajax application
+-----------------------
+
+    >>> import bobo, os, webtest
+    >>> os.mkdir('docs')
+    >>> app = webtest.TestApp(bobo.Application(
+    ...   bobo_resources='bobodoctestumentation.bobocalc',
+    ...   ))
+
+    >>> print app.get('/') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html>
+      <head>
+        <title>Bobocalc</title>
+        ...
+      </head>
+      <body class="tundra">
+        <h1><em>Bobocalc</em></h1>
+        Value: <span id="value">0</span>
+        <form>
+          <label for="input">Input:</label>
+          <input
+             type="text" id="input" name="input"
+             dojoType="dijit.form.ValidationTextBox" regExp="[0-9]+"
+             />
+          <button dojoType="dijit.form.Button"
+              onClick="bobocalc.clear">C</button>
+          <button dojoType="dijit.form.Button" onClick="bobocalc.add">+</button>
+          <button dojoType="dijit.form.Button" onClick="bobocalc.sub">-</button>
+        </form>
+      </body>
+    </html>
+
+    >>> print app.get('/add.json?value=0&input=42')
+    Response: 200 OK
+    Content-Type: application/json
+    {"value": 42}
+
+    >>> print app.get('/sub.json?value=42&input=42')
+    Response: 200 OK
+    Content-Type: application/json
+    {"value": 0}


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/bobocalc.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/boboserver.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/boboserver.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/boboserver.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,310 @@
+Bobo server and middleware tests
+================================
+
+These are fairly whote box tests, in that we mock up the environmeny
+of the components being tested pretty extensively.
+
+We'll start by creating a source file:
+
+    >>> open('my.py', 'w').write('''
+    ... import bobo
+    ... @bobo.query('/:me')
+    ... def hi(me, who='world'):
+    ...     return "Hi %s, I'm %s" % (who, me)
+    ... ''')
+
+For Python 2.5 and later, we'll mock the
+wsgiref.simple_server.make_server used by the bobo server. For earlier
+versions, mock paste.httpserver.server_runner:
+
+    >>> import sys
+    >>> if sys.version_info >= (2, 5):
+    ...     class Server:
+    ...         def __init__(self, host, port, app):
+    ...             self.host, self.port, self.app = host, port, app
+    ...         def serve_forever(self):
+    ...             global served_app
+    ...             served_app = self.app
+    ...             print 'serve_forever', repr(self.host), self.port
+    ...
+    ...     import wsgiref.simple_server
+    ...     make_server = wsgiref.simple_server.make_server
+    ...     wsgiref.simple_server.make_server = Server
+    ...     def restore_server():
+    ...         wsgiref.simple_server.make_server = make_server
+    ... else:
+    ...     def faux_server_runner(app, _, port):
+    ...         global served_app
+    ...         served_app = app
+    ...         print 'serve_forever', repr(''), port
+    ...
+    ...     import paste.httpserver
+    ...     server_runner = paste.httpserver.server_runner
+    ...     paste.httpserver.server_runner = faux_server_runner
+    ...     def restore_server():
+    ...         paste.httpserver.server_runner = server_runner
+
+Now, let't run the server. We'll run it without arguments and make
+sure we het some help:
+
+    >>> stderr = sys.stderr
+    >>> sys.stderr = sys.stdout
+
+    >>> import boboserver
+
+>>> import sys
+>>> sys.argv[0] = 'test'
+>>> try: boboserver.server([])
+... except SystemExit: pass
+... else: print '???'
+Error:
+No resources were specified.
+<BLANKLINE>
+Usage: test [options] name=value ...
+<BLANKLINE>
+Options:
+  -h, --help            show this help message and exit
+  -p PORT, --port=PORT  Specify the port to listen on.
+  -f FILE, --file=FILE  Specify a source file to publish.
+  -r RESOURCE, --resource=RESOURCE
+                        Specify a resource, such as a module or module global,
+                        to publish.
+  -D, --debug           Run the post mortem debugger for uncaught exceptions.
+  -c CONFIGURE, --configure=CONFIGURE
+                        Specify the bobo_configure option.
+  -s STATIC, --static=STATIC
+                        Specify a route and directory (route=directory) to
+                        serve statically
+
+And run it with a source file:
+
+    >>> boboserver.server(['-fmy.py'])
+    Serving ['bobo__main__'] on port 8080...
+    serve_forever '' 8080
+
+    >>> import webob, pprint
+    >>> def start_response(status, headers):
+    ...     print status
+    ...     pprint.pprint(headers)
+    ...     print '-----------------'
+
+    >>> def req(*args, **kw):
+    ...     print served_app(webob.Request.blank(*args, **kw).environ,
+    ...                      start_response)
+
+    >>> req('/foo')
+    200 OK
+    [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '17')]
+    -----------------
+    ["Hi world, I'm foo"]
+
+If we change the source, it will be reloaded:
+
+    >>> import time
+    >>> time.sleep(1.1)
+
+    >>> open('my.py', 'w').write('''
+    ... import bobo
+    ... @bobo.query('/:me')
+    ... def hi(me, who='world'):
+    ...     return "Hi %s, I'm %s!" % (who, me)
+    ... ''')
+
+    >>> req('/foo')
+    Reloading bobo__main__
+    200 OK
+    [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '18')]
+    -----------------
+    ["Hi world, I'm foo!"]
+
+Let's publish a module with an error:
+
+    >>> open('foo.py', 'w').write('''
+    ... import bobo
+    ... @bobo.query('/x')
+    ... def x():
+    ...     return "x", y
+    ... ''')
+    >>> sys.path.insert(0, '.')
+
+    >>> import pdb, traceback
+    >>> post_mortem = pdb.post_mortem
+    >>> def faux_post_mortem(tb):
+    ...     print 'post_mortem:'
+    ...     traceback.print_tb(tb, 1)
+    >>> pdb.post_mortem = faux_post_mortem
+
+    >>> boboserver.server(['-rfoo', '-fmy.py', '-p80', '-D'])
+    Serving ['foo', 'bobo__main__'] on port 80...
+    serve_forever '' 80
+
+    >>> try: req('/x')
+    ... except Exception, v: print 'raised', v
+    ... else: print '???'
+    ... # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    NameError: global name 'y' is not defined
+    post_mortem:
+    ...
+        return self.app(environ, start_response)
+    raised global name 'y' is not defined
+
+    >>> time.sleep(1.1)
+
+    >>> open('my.py', 'w').write('''
+    ... import bobo
+    ... @bobo.query('/:me')
+    ... def hi(me):
+    ...     return "Hi you, I'm %s!" % (who, me)
+    ... ''')
+
+    >>> open('foo.py', 'w').write('''
+    ... import bobo
+    ... @bobo.query('/x')
+    ... def x():
+    ...     return "x"
+    ... ''')
+
+    >>> req('/x')
+    Reloading foo
+    Reloading bobo__main__
+    200 OK
+    [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '1')]
+    -----------------
+    ['x']
+
+The --static option is handy for publishing static files. There are
+middleware components that are better for serving static data in
+production, but the --static option is useful when just getting
+started.
+
+    >>> import os, webtest
+    >>> os.mkdir('docs')
+    >>> os.mkdir(os.path.join('docs', 'subdir'))
+    >>> open(os.path.join('docs', 'doc1.txt'), 'w').write('doc1 text')
+    >>> open(os.path.join('docs', 'subdir', 'doc2.html'), 'w').write(
+    ...    'doc2 text')
+
+    >>> boboserver.server(['-s/resources=docs'])
+    Serving ["boboserver:static('/resources','docs')"] on port 8080...
+    serve_forever '' 8080
+
+    >>> app = webtest.TestApp(served_app)
+    >>> app.get('/resources') # doctest: +NORMALIZE_WHITESPACE
+    <302 Found text/html
+       location: http://localhost/resources/ body='See http:...ces/'/31>
+
+    >>> print app.get('/resources/', status=200).body
+    <html>
+            <head><title></title></head>
+            <body>
+              <a href="doc1.txt">doc1.txt</a><br>
+      <a href="subdir/">subdir/</a>
+            </body>
+            </html>
+    <BLANKLINE>
+
+    >>> app.get('/resources/subdir') # doctest: +NORMALIZE_WHITESPACE
+    <302 Found text/html
+      location: http://localhost/resources/subdir/ body='See http:...dir/'/38>
+
+    >>> print app.get('/resources/subdir/', status=200).body
+    <html>
+            <head><title>subdir</title></head>
+            <body>
+              <a href="doc2.html">doc2.html</a>
+            </body>
+            </html>
+    <BLANKLINE>
+
+    >>> app.get('/resources/doc1.txt')
+    <200 OK text/plain body='doc1 text'>
+
+    >>> app.get('/resources/subdir/doc2.html')
+    <200 OK text/html body='doc2 text'>
+
+    >>> print app.get('/resources/doc2.html', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources/doc2.html</body>
+    </html>
+
+    >>> print app.get('/resources//etc/passwd', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources//etc/passwd</body>
+    </html>
+
+    >>> print app.get('/resources/../../', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources/../../</body>
+    </html>
+
+Cleanup:
+
+    >>> restore_server()
+    >>> sys.stderr = stderr
+    >>> pdb.post_mortem = post_mortem
+
+Real server tests to make sure we can actually run the server. :)
+-----------------------------------------------------------------
+
+    >>> open('my.py', 'w').write('''
+    ... import bobo, os
+    ...
+    ... @bobo.query(method=None, content_type='text/plain')
+    ... def method(bobo_request):
+    ...     return "You made a %s request." % bobo_request.method
+    ...
+    ... @bobo.query
+    ... def exit():
+    ...     os._exit(0) # wsgiref catches all exceptions :(
+    ... ''')
+
+    >>> import bobodoctestumentation.tests
+    >>> port = bobodoctestumentation.tests.get_port()
+
+Whimper. I hate using processes in tests.
+
+    >>> import subprocess
+    >>> open('serve.py', 'w').write('''
+    ... import sys
+    ... sys.path[:] = %r
+    ... import boboserver
+    ... boboserver.server()
+    ... ''' % sys.path)
+
+    >>> proc = subprocess.Popen(
+    ...     [sys.executable, 'serve.py', '-p%s' % port, '-fmy.py'],
+    ...     stderr=subprocess.STDOUT, stdout=open('log', 'w'))
+
+    >>> import urllib2, time
+    >>> deadline = time.time()+30
+    >>> while 1:
+    ...     try:
+    ...         print urllib2.urlopen(
+    ...             'http://localhost:%s/method.plain' % port).read()
+    ...         break
+    ...     except urllib2.URLError:
+    ...         if time.time() > deadline:
+    ...             print 'Timed out!'
+    ...             break
+    ...         time.sleep(.1)
+    You made a GET request.
+
+urllib2 doesn't do PUT :(
+
+    >>> import httplib
+    >>> conn = httplib.HTTPConnection('localhost', port)
+    >>> conn.request('PUT', '/method.plain')
+    >>> print conn.getresponse().read()
+    You made a PUT request.
+
+    >>> conn.close()
+
+    >>> try: urllib2.urlopen('http://localhost:%s/exit.html' % port)
+    ... except Exception: pass
+    ... else: print 'expected 500'
+


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/boboserver.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/conf.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/conf.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/conf.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+#
+# bobo documentation build configuration file, created by
+# sphinx-quickstart on Sun Apr 19 07:39:53 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'bobo'
+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 = '0'
+# The full version, including alpha/beta/rc tags.
+release = '0'
+
+# 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.
+dc_dark = '#4C46B0'
+dc_darker = '#211F4D'
+dc_medium = '#B5B1FC'
+html_theme_options = dict(
+    stickysidebar = True,
+    sidebarbgcolor = dc_dark,
+    sidebarlinkcolor = '#fff',
+    headbgcolor = dc_medium,
+    headtextcolor = dc_darker,
+    relbarbgcolor = dc_darker,
+    footerbgcolor = dc_darker,
+    )
+
+# 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
+html_logo = "bobo.png"
+
+# 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 = False
+
+# 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 = 'bobodoc'
+
+
+# -- 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 = [
+  ('index', 'bobo.tex', u'bobo 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 = 'bobo-big.png'
+
+# 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: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/conf.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/contents.txt
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/contents.txt	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/contents.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,14 @@
+.. bobo documentation master file, created by
+   sphinx-quickstart on Sun Apr 19 07:39:53 2009.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Table of Contents
+=================
+
+.. toctree::
+
+   index
+   more
+   reference
+   examples


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/contents.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/decorator.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/decorator.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/decorator.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,839 @@
+Decorator module tests
+======================
+
+Route compilation
+-----------------
+
+Internal function _route_compile compiles route strings to matching
+fuctions that return None when there is no match and a dictionary of
+rout data when there is. Although it is internal, it provides a nice
+place to test the route handling. We'll use a helper function to try
+it.
+
+    >>> import bobo, pprint, webob
+    >>> def test(route, url, partial=False):
+    ...     match = bobo._compile_route(route, partial)
+    ...     request = webob.Request.blank(url)
+    ...     d = match(request, request.path_info)
+    ...     if d is None:
+    ...        return
+    ...     if partial:
+    ...        d, path = d
+    ...        print repr(path)
+    ...     if not d or len(d) == 1:
+    ...        print repr(d)
+    ...     else:
+    ...        pprint.pprint(d, width=1)
+    ...     if not isinstance(partial, basestring):
+    ...        return
+    ...     match = bobo._compile_route(partial)
+    ...     d = match(request, path)
+    ...     if d is None:
+    ...        return
+    ...     if not d or len(d) == 1:
+    ...        print repr(d)
+    ...     else:
+    ...        pprint.pprint(d, width=1)
+
+We give a route and a url and an optional partial march flag.
+Non-partial routes must match the entire URL path. Partial matches can
+have a trailing string.  Note that a non-empty bobo_path_info always starts
+with a '/'.
+
+If the partial flag is a string, then we treat it as a subroute and,
+if the initial route matches, we try to match against the subroute.
+
+Some things to note:
+
+- A leading '/' is added to non-empty routes that lack one.
+
+- If path segment is optional, the optional part includes the
+  preceeding '/' and the extension.
+
+- An exception: an optional segment with an extension only matches at
+  the end of a url if either the segment is present or the url has a
+  trailing slash.
+
+    >>> test('/', '')
+    >>> test('/', '/')
+    {}
+    >>> test('', '/')
+    >>> test('', '/', partial=True)
+    '/'
+    {}
+    >>> test('/', '/foo')
+    >>> test('/', '/foo', partial=True)
+    '/foo'
+    {}
+    >>> test('/foo', '/foo')
+    {}
+    >>> test('/foo/', '/foo')
+    >>> test('/foo/', '/foo/')
+    {}
+    >>> test('/foo/', '/foo/bar')
+    >>> test('/foo/', '/foo/bar', '/bar')
+    '/bar'
+    {}
+    {}
+    >>> test('/:x', '')
+    >>> test('/:x', '/')
+    {'x': ''}
+    >>> test('/:x', '/a')
+    {'x': 'a'}
+    >>> test('/:x', '/aa')
+    {'x': 'aa'}
+    >>> test('/:xx', '')
+    >>> test('/:xx', '/')
+    {'xx': ''}
+    >>> test('/:xx', '/a')
+    {'xx': 'a'}
+    >>> test('/:xx', '/aa')
+    {'xx': 'aa'}
+    >>> test('/zzz/:xx', '/zzz')
+    >>> test('/zzz/:xx', '/zzz/')
+    {'xx': ''}
+    >>> test('/zzz/:xx', '/zzz/a')
+    {'xx': 'a'}
+    >>> test('/zzz/:xx', '/zzz/aa')
+    {'xx': 'aa'}
+    >>> test('/:xx/:y', '/a')
+    >>> test('/:xx/:y', '/a/')
+    {'xx': 'a',
+     'y': ''}
+    >>> test('/:xx/:y', '/a/b')
+    {'xx': 'a',
+     'y': 'b'}
+    >>> test('/zzz/:xx/www/:y', '/a/b')
+    >>> test('/zzz/:xx/www/:y', '/zzz/aa/www/bb')
+    {'xx': 'aa',
+     'y': 'bb'}
+    >>> test('/zzz/:xx/www/:y.html', '/zzz/aa/www/bb.html')
+    {'xx': 'aa',
+     'y': 'bb'}
+    >>> test('/zzz/:xx/www/:yy.html', '/zzz/aa/www/b.html')
+    {'xx': 'aa',
+     'yy': 'b'}
+    >>> test('/zzz/:xx?/www/:yy.html', '/zzz/aa/www/b.html')
+    {'xx': 'aa',
+     'yy': 'b'}
+    >>> test('/zzz/:xx?/www/:yy.html', '/zzz/www/b.html')
+    {'yy': 'b'}
+    >>> test('/zzz/:xx/www/:yy?.html', '/zzz/qq/www')
+    {'xx': 'qq'}
+    >>> test('/zzz/:xx?/www/:yy?.html', '/zzz/www')
+    {}
+    >>> test('/zzz/:xx?/:yy?.html', '/zzz')
+    {}
+    >>> test('/zzz/:xx?/:yy?.html', '/zzz/')
+    {'xx': ''}
+    >>> test('/zzz/:xx?/:yy?', '/zzz')
+    {}
+    >>> test('/zzz/:xx?/www/:yy?.html', '/zzz/www/')
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www')
+    {'xx': 'aaa'}
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www/')
+    {'xx': 'aaa',
+     'yy': ''}
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www/bbb/ccc')
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www/bbb/ccc', '/ccc')
+    '/ccc'
+    {'xx': 'aaa',
+     'yy': 'bbb'}
+    {}
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www/bbb/ccc', '/:foo')
+    '/ccc'
+    {'xx': 'aaa',
+     'yy': 'bbb'}
+    {'foo': 'ccc'}
+    >>> test('/zzz/:xx/www/:yy?', '/zzz/aaa/www/bbb/ccc', '/:foo/:bar?')
+    '/ccc'
+    {'xx': 'aaa',
+     'yy': 'bbb'}
+    {'foo': 'ccc'}
+    >>> test('/zzz/:xx/www/:yy?.html', '/zzz/aaa/www/bbb/ccc')
+    >>> test('/zzz/:xx/www/:yy?.html', '/zzz/aaa/www/bbb/ccc', '/ccc')
+    '/bbb/ccc'
+    {'xx': 'aaa'}
+    >>> test('/zzz/:xx/www/:yy?.html', '/zzz/aaa/www/ccc', '/ccc')
+    '/ccc'
+    {'xx': 'aaa'}
+    {}
+    >>> test('/zzz/:xx/www/:yy?.html', '/zzz/aaa/www/ccc', '/:ccc')
+    '/ccc'
+    {'xx': 'aaa'}
+    {'ccc': 'ccc'}
+
+resource
+--------
+
+The resource decorator defines a resource function and gives it a
+route.  It also provides automation of response creation.
+
+    >>> @bobo.resource('/foo', method='GET')
+    ... @bobo.resource('/:name', content_type='text/plain; charset=Latin-1')
+    ... def hi(request, name=None):
+    ...     print 'request:'
+    ...     print str(request).replace('\r', '')
+    ...     print '-----'
+    ...     return 'Hi %s.' % name
+
+As we can see, we can stack resources.  We can supply a content type.
+
+We use resources by calling the result of calling bobo_response:
+
+    >>> def print_response(response):
+    ...     print (response.status + '\n'
+    ...            + '\n'.join('%s: %s' % (name, value)
+    ...                        for (name, value) in sorted(response.headerlist))
+    ...            + '\n\n'
+    ...            + response.body)
+
+    >>> import webob, StringIO
+    >>> def call_resource(resource, url, input=None, env=None, **kw):
+    ...     env = env or {}
+    ...     if input:
+    ...         env['wsgi.input'] = StringIO.StringIO(input)
+    ...         env['CONTENT_LENGTH'] = str(len(input))
+    ...         env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+    ...         env['REQUEST_METHOD'] = 'POST'
+    ...     request = webob.Request.blank(url, env, **kw)
+    ...     try:
+    ...         found = resource.bobo_response(
+    ...             request, request.path_info, request.method)
+    ...         if found is not None:
+    ...             print_response(found)
+    ...     except bobo.BoboException, v:
+    ...         print v.__class__.__name__+':'
+    ...         pprint.pprint(v.__dict__, width=1)
+
+    >>> call_resource(hi, '/foo')
+    request:
+    GET /foo
+    Host: localhost:80
+    <BLANKLINE>
+    <BLANKLINE>
+    -----
+    BoboException:
+    {'body': 'Hi None.',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+When a callable doesn't returns a non-response, a BoboException is
+raised.  It gets caught by the bobo Application and used to build a
+response.
+
+Note that we matched the first resource because resources are checked in
+order.
+
+    >>> call_resource(hi, '/bar')
+    request:
+    GET /bar
+    Host: localhost:80
+    <BLANKLINE>
+    <BLANKLINE>
+    -----
+    BoboException:
+    {'body': 'Hi bar.',
+     'content_type': 'text/plain; charset=Latin-1',
+     'headers': [],
+     'status': 200}
+
+Here, we matched the second caller and passed the name.
+
+    >>> call_resource(hi, '/foo', method='HEAD')
+    Traceback (most recent call last):
+    ...
+    MethodNotAllowed: Allowed: 'GET'
+
+This time, we matched the first resource, but the method was invalid,
+so a MethodNotAllowed exception was raised.
+
+Notice that we didn't match the first route even though the URL
+matched the route pattern. This is because the request method didn't
+match.
+
+Calling the resource calls the underliting function:
+
+    >>> hi(None)
+    request:
+    None
+    -----
+    'Hi None.'
+
+    >>> hi(None, 'bob')
+    request:
+    None
+    -----
+    'Hi bob.'
+
+It is invalid to specify a non-empty route without a leading /:
+
+    >>> @bobo.resource(':name', content_type='text/plain; charset=Latin-1')
+    ... def hi(request, name=None):
+    ...     pass
+    Traceback (most recent call last):
+    ...
+    ValueError: ("Non-empty routes must start with '/'.", ':name')
+
+If the content type is application/json and a resource has a
+non-response, non-string response, the response will be automatically
+encoded as json:
+
+    >>> @bobo.resource('/:type?', content_type='application/json')
+    ... def uni(request, type=None):
+    ...     val = u'\uaaaa'
+    ...     if type=='array':
+    ...         return [val]
+    ...     elif type == 'ob':
+    ...         return {'val': val}
+    ...     return '"\\uaaaa"'
+
+    >>> call_resource(uni, '/')
+    BoboException:
+    {'body': '"\\uaaaa"',
+     'content_type': 'application/json',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(uni, '/array')
+    BoboException:
+    {'body': [u'\uaaaa'],
+     'content_type': 'application/json',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(uni, '/ob')
+    BoboException:
+    {'body': {'val': u'\uaaaa'},
+     'content_type': 'application/json',
+     'headers': [],
+     'status': 200}
+
+If a callable returns a response, then we don't get a bobo exception:
+
+    >>> @bobo.resource('/:name', content_type='text/plain; charset=Latin-1')
+    ... def hi(request, name=None):
+    ...     print 'request:'
+    ...     print str(request).replace('\r', '')
+    ...     print '-----'
+    ...     return webob.Response('Hi %s.' % name)
+
+    >>> call_resource(hi, '/bar')
+    request:
+    GET /bar
+    Host: localhost:80
+    <BLANKLINE>
+    <BLANKLINE>
+    -----
+    200 OK
+    Content-Length: 7
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    Hi bar.
+
+query and post
+--------------
+
+The query and post decorators provide extra convenience by marshaling
+request query and/or post data as function parameters.
+
+    >>> @bobo.query('/:bobo_request/:x?')
+    ... def foo(bobo_request, x, y, z=None):
+    ...     return "%s %s %s %s" % (type(bobo_request).__name__, x, y, z)
+
+    >>> call_resource(foo, '/a/b?bobo_request=1&x=2&y=3')
+    BoboException:
+    {'body': 'Request b 3 None',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(foo, '/a?bobo_request=1&x=2&y=3')
+    BoboException:
+    {'body': 'Request 2 3 None',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+Note that the name bobo_request is reserved. It is also set to the
+request, even if the name is in the form data or in the route data.
+Also, route data takes precedence over form data.
+
+The query decorators will also use form data:
+
+    >>> call_resource(foo, '/a?bobo_request=1&x=2&y=3', 'y=4&z=5')
+    BoboException:
+    {'body': "Request 2 ['3', '4'] 5",
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+If parameters are ommitted, we'll get a MissingFormVariable error:
+
+    >>> call_resource(foo, '/a/b')
+    Traceback (most recent call last):
+    ...
+    MissingFormVariable: y
+
+The post decorator will *only* use form data:
+
+    >>> @bobo.post('/:bobo_request/:x?')
+    ... def foo(bobo_request, x, y, z=None):
+    ...     return "%s %s %s %s" % (type(bobo_request).__name__, x, y, z)
+
+    >>> call_resource(foo, '/a?bobo_request=1&x=2&y=3', 'y=4&z=5&x=6')
+    BoboException:
+    {'body': 'Request 6 4 5',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(foo, '/a/b?bobo_request=1&x=2&y=3', 'y=4&z=5&x=6')
+    BoboException:
+    {'body': 'Request b 4 5',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(foo, '/a/b', 'z=1')
+    Traceback (most recent call last):
+    ...
+    MissingFormVariable: y
+
+Resources as methods
+--------------------
+
+If a resource is defined in a class, it handles binding correctly.
+
+    >>> class C:
+    ...     def __init__(self, request=None):
+    ...         self.x = 99
+    ...
+    ...     @bobo.resource('/a/:y')
+    ...     def m1(self, request, y):
+    ...         return "%s %s %s" % (request.__class__.__name__, self.x, y)
+    ...
+    ...     @bobo.query('/b/:y')
+    ...     def m2(self, y, z):
+    ...         return "%s %s %s" % (self.x, y, z)
+
+    >>> C.m1
+    <unbound resource C.m1>
+
+    >>> C.m1(None, 1)
+    Traceback (most recent call last):
+    ...
+    TypeError: Need C initial argument
+
+    >>> C.m1(C(), None, 1)
+    'NoneType 99 1'
+
+    >>> C().m1 # doctest: +ELLIPSIS
+    <bound resource C.m1 of <__builtin__.C instance at ...>>
+
+    >>> C().m1(None, 1)
+    'NoneType 99 1'
+
+    >>> call_resource(C().m1, '/b/b')
+
+    >>> call_resource(C().m1, '/a/b')
+    BoboException:
+    {'body': 'Request 99 b',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> C.m2
+    <unbound resource C.m2>
+
+    >>> C.m2(None, 1, 2)
+    Traceback (most recent call last):
+    ...
+    TypeError: Need C initial argument
+
+    >>> C.m2(C(), 1, 2)
+    '99 1 2'
+
+    >>> C().m2 # doctest: +ELLIPSIS
+    <bound resource C.m2 of <__builtin__.C instance at ...>>
+
+    >>> C().m2(1, 2)
+    '99 1 2'
+
+    >>> call_resource(C().m2, '/a/b')
+
+    >>> call_resource(C().m2, '/b/b?z=3')
+    BoboException:
+    {'body': '99 b 3',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+subroute
+--------
+
+The subroute decorator is used with a factory that returns a resource.
+
+    >>> @bobo.resource('/foo')
+    ... def hi(request, name=None):
+    ...     print 'request:'
+    ...     print str(request).replace('\r', '')
+    ...     print '-----'
+    ...     return 'Hi %s.' % name
+
+    >>> def sub1(request, first):
+    ...     print 'sub1', first
+    ...     return hi
+    >>> sub1 = bobo.subroute('/:first')(sub1)
+
+    >>> call_resource(sub1, '/x/y/z')
+    sub1 x
+
+    >>> call_resource(sub1, '/x/foo')
+    sub1 x
+    request:
+    GET /x/foo
+    Host: localhost:80
+    <BLANKLINE>
+    <BLANKLINE>
+    -----
+    BoboException:
+    {'body': 'Hi None.',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+If we try to use subroute with C, we lose because C doesn't have a
+bobo_response method.
+
+    >>> bobo.subroute('/x')(C) is C
+    True
+    >>> call_resource(C, '/x/a/b')
+    Traceback (most recent call last):
+    ...
+    AttributeError: C instance has no attribute 'bobo_response'
+
+C doesn't have a bobo_response method. We can use scan_class to
+give it one:
+
+    >>> bobo.scan_class(C) is C
+    True
+    >>> call_resource(C, '/x/a/b')
+    BoboException:
+    {'body': 'Request 99 b',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+We can get the same effect using the scan keyword argument to
+subroute.
+
+    >>> class B:
+    ...     @bobo.resource('/a/:y')
+    ...     def m1(self, request, y):
+    ...         return "%s %s %s" % (request.__class__.__name__, self.x, y)
+
+    >>> class C(B):
+    ...     def __init__(self, request, x):
+    ...         self.x = x
+    ...
+    ...     @bobo.query('/b/:y')
+    ...     def m2(self, y, z):
+    ...         return "%s %s %s" % (self.x, y, z)
+
+    >>> bobo.subroute('/:x', scan=True)(C) is C
+    True
+
+    >>> call_resource(C, '/pre/a/b')
+    BoboException:
+    {'body': 'Request pre b',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(C, '/pro/b/c?z=1')
+    BoboException:
+    {'body': 'pro c 1',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+Scanning classes with duplicate routes
+--------------------------------------
+
+    >>> class B:
+    ...     @bobo.resource('/:y', 'GET')
+    ...     def gety(self, request, y):
+    ...         return "B.gety %s %s %s" % (request.method, self.x, y)
+    ...     @bobo.resource('', 'GET')
+    ...     def get(self, request, y=None):
+    ...         return "B.get %s %s %s" % (request.method, self.x, y)
+
+    >>> class C(B):
+    ...     def __init__(self, request, x):
+    ...         self.x = x
+    ...
+    ...     @bobo.resource('/:y', 'POST')
+    ...     def posty(self, request, y):
+    ...         return "C.posty %s %s %s" % (request.method, self.x, y)
+    ...     @bobo.resource('', 'POST')
+    ...     def post(self, request, y=None):
+    ...         return "C.post %s %s %s" % (request.method, self.x, y)
+
+    >>> bobo.subroute('/:x', True)(C) is C
+    True
+
+    >>> call_resource(C, '/pre')
+    BoboException:
+    {'body': 'B.get GET pre None',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(C, '/pre', method='POST')
+    BoboException:
+    {'body': 'C.post POST pre None',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(C, '/pre', method='HEAD')
+    Traceback (most recent call last):
+    ...
+    MethodNotAllowed: Allowed: 'GET', 'POST'
+
+    >>> call_resource(C, '/pre/a')
+    BoboException:
+    {'body': 'B.gety GET pre a',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(C, '/pre/a', method='POST')
+    BoboException:
+    {'body': 'C.posty POST pre a',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(C, '/pre/a', method='HEAD')
+    Traceback (most recent call last):
+    ...
+    MethodNotAllowed: Allowed: 'GET', 'POST'
+
+subroutes don't screw up other uses of a class
+----------------------------------------------
+
+    >>> class D(C):
+    ...     pass
+
+    >>> request = webob.Request.blank('/')
+    >>> d = D(request, 'zzz')
+    >>> d.gety(request, 22)
+    'B.gety GET zzz 22'
+
+check option
+------------
+
+The query, post, and resource decorators have a check option that can
+be used to express preconditions on resources.  The check option takes
+a function that will be called with a request prior to calling a
+resource. If the check function returns None, then the function will be
+called as usual.  Otherwise, the returned value from the check
+function is used as the response.
+
+The check function takes 3 positional arguments:
+
+- an instance, if the resource is a method, or None,
+- the request, and
+- the decorated callable.
+
+    >>> def authenticated(inst, request, func):
+    ...     if not request.remote_user:
+    ...         response = webob.Response(status=401)
+    ...         message = u'unauthenticated '+func.__name__
+    ...         message += ' '+inst.__class__.__name__
+    ...         response.unicode_body = message
+    ...         return response
+
+    >>> @bobo.query(check=authenticated)
+    ... def hi(self=None):
+    ...     return 'Hi! '+self.__class__.__name__
+
+    >>> class C:
+    ...     hi = hi
+    >>> c = C()
+
+    >>> call_resource(hi, '/')
+    >>> call_resource(c.hi, '/')
+
+    >>> call_resource(hi, '/hi.html')
+    401 Unauthorized
+    Content-Length: 27
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi NoneType
+
+    >>> call_resource(c.hi, '/hi.html')
+    401 Unauthorized
+    Content-Length: 20
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi C
+
+    >>> call_resource(hi, '/hi.html', env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi! NoneType',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(c.hi, '/hi.html', env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi! C',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> @bobo.post(check=authenticated)
+    ... def hi(self=None):
+    ...     return 'Hi! '+self.__class__.__name__
+
+    >>> call_resource(hi, '/')
+    >>> call_resource(c.hi, '/')
+    >>> call_resource(hi, '/hi.html', input='x=1')
+    401 Unauthorized
+    Content-Length: 27
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi NoneType
+
+    >>> call_resource(c.hi, '/hi.html', input='x=1')
+    401 Unauthorized
+    Content-Length: 20
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi C
+
+    >>> call_resource(hi, '/hi.html', input='x=1', env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi! NoneType',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(c.hi, '/hi.html', input='x=1',
+    ...               env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi! C',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> @bobo.resource(check=authenticated)
+    ... def hi(request):
+    ...     return 'Hi! '+request.url
+
+    >>> class C:
+    ...     @bobo.resource(check=authenticated)
+    ...     def hi(self, request):
+    ...         return 'Hi C! '+request.url
+    >>> c = C()
+
+    >>> call_resource(hi, '/')
+    >>> call_resource(c.hi, '/')
+
+    >>> call_resource(hi, '/hi.html')
+    401 Unauthorized
+    Content-Length: 27
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi NoneType
+
+    >>> call_resource(c.hi, '/hi.html')
+    401 Unauthorized
+    Content-Length: 20
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    unauthenticated hi C
+
+    >>> call_resource(hi, '/hi.html', env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi! http://localhost/hi.html',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+    >>> call_resource(c.hi, '/hi.html', env=dict(REMOTE_USER='jim'))
+    BoboException:
+    {'body': 'Hi C! http://localhost/hi.html',
+     'content_type': 'text/html; charset=UTF-8',
+     'headers': [],
+     'status': 200}
+
+subroute class-manipulation edge cases
+--------------------------------------
+
+Subroute on class that already has class method:
+
+    >>> class C:
+    ...     @classmethod
+    ...     def bobo_response(self, request, path, method):
+    ...         return webob.Response('C')
+
+    >>> bobo.subroute(C)
+    Traceback (most recent call last):
+    ...
+    TypeError: bobo_response class method already defined
+
+Subroute on class with inherited class method:
+
+(Also subroute on class that has instance method)
+
+    >>> class S(C):
+    ...     def __init__(self, request):
+    ...         pass
+    ...     def bobo_response(self, request, path, method):
+    ...         return webob.Response('s')
+
+    >>> S = bobo.subroute(S)
+    >>> call_resource(S, '/S')
+    200 OK
+    Content-Length: 1
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    s
+
+Subroute on class that adds method in __init__:
+
+    >>> class I:
+    ...     def __init__(self, request):
+    ...         self.bobo_response = (lambda request, path, method:
+    ...                               webob.Response('i'))
+
+    >>> I = bobo.subroute(I)
+    >>> call_resource(I, '/I')
+    200 OK
+    Content-Length: 1
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+    i
+
+subroute factory can return None
+--------------------------------
+
+    >>> @bobo.subroute
+    ... def traverse(request):
+    ...     print 'traverse'
+
+    >>> call_resource(traverse, '/traverse/x')
+    traverse
+
+    >>> class Traverse(object):
+    ...     def __new__(class_, request):
+    ...         print 'Traverse'
+    >>> Traverse = bobo.subroute(Traverse)
+
+    >>> call_resource(Traverse, '/Traverse/x')
+    Traverse


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/decorator.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/edit.html
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/edit.html	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/edit.html	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,51 @@
+<html>
+  <head>
+    <title>%(action)s %(name)s</title>
+
+    <style type="text/css">
+      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
+      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
+    </style>
+
+    <script
+       type="text/javascript"
+       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
+       djConfig="parseOnLoad: true"
+       ></script>
+
+    <script type="text/javascript">
+      dojo.require("dojo.parser");
+      dojo.require("dijit.Editor");
+      dojo.require("dijit._editor.plugins.LinkDialog")
+      dojo.require("dijit._editor.plugins.FontChoice")
+
+      function update_body() {
+          dojo.byId('page_body').value = dijit.byId('editor').getValue();
+      }
+
+      dojo.addOnLoad(update_body);
+    </script>
+
+
+  </head>
+  <body class="tundra">
+    <h1>%(action)s %(name)s</h1>
+
+    <div dojoType="dijit.Editor"
+         id="editor"
+         onChange="update_body"
+         extraPlugins="['insertHorizontalRule', 'createLink',
+                        'insertImage', 'unlink', 
+                        {name:'dijit._editor.plugins.FontChoice',
+                         command:'fontName', generic:true}
+                         ]"
+         >
+      %(body)s
+    </div>
+
+    <form method="POST">
+      <input type="hidden" name="body" id="page_body">
+      <input type="submit" value="Save">
+    </form>
+  </body>
+</html>


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/edit.html
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/examples.txt
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/examples.txt	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/examples.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,328 @@
+Examples
+========
+
+File-system-based wiki
+----------------------
+
+In this section, we present a wiki implementation that stores wiki
+documents in a file-system directory:
+
+.. literalinclude:: fswiki.py
+   :language: python
+   :linenos:
+
+We need to know the name of the directory to store the files in.  On
+line 3, we define a configuration function, ``config``.
+
+To run this with the bobo server, we'll use the command line::
+
+   bobo -ffswiki.py -cconfig directory=wikidocs
+
+This tells bobo to:
+
+- run the file ``fswiki.py``
+- pass configuration information to it's config function on start up, and
+- pass the configuration directory setting of ``'wikidocs'``.
+
+On line 11, we define an ``index`` method to handle ``/`` that lists
+the documents in the wiki.
+
+On line 22, we define a post resource, ``save``, for a post to a named document
+that saves the body submitted and redirects to the same URL.
+
+On line 27, we define a query, ``get``, for the named document that
+displays it if it exists, otherwise, it displays a creation page.
+Also, if the ``edit`` form variable is present, an editing interface
+is presented.  By default, queries will accept POST requests, however,
+because the ``save`` function comes first, it is used for POST
+requests before the get function.
+
+Both the editing and creation interfaces use an edit template, which
+is just a Python string read from a file that provides a
+form. In this case, we use Dojo to provide an HTML editor for the
+body:
+
+.. literalinclude:: edit.html
+   :language: html
+
+.. _wikia:
+
+File-based wiki with authentication and (minimal) authorization
+---------------------------------------------------------------
+
+Traditionally, wikis allowed anonymous edits.  Sometimes though, you
+want to require log in to make changes.  In this example, we extend the
+file-based wiki to require authentication to make changes.
+
+Bobo doesn't provide any authentication support itself.  To provide
+authentication support for bobo applications, you'll typically use
+either an application library, or WSGI middleware.  Middleware is
+attractive because there are a number of middleware authentication
+implementations available and because authentication is generally
+something you want to apply in blanket fashion to an entire
+application.
+
+In this example, we'll use the repoze.who authentication middleware
+component, in part because it integrates well using PasteDeploy.
+
+.. literalinclude:: fswikia.py
+   :language: python
+   :linenos:
+
+We've added 2 new pages, ``login.html`` and ``logout.html``, to our
+application, starting on line 11.
+
+The login page illustrates 2 common properties of authentication
+middleware:
+
+1. The authentication user id is provided in the ``REMOTE_USER``
+   environment variable and made available in the ``remote_user``
+   request attribute.
+
+2. We signal to middleware that it should ask for credentials by
+   returning a response with a 401 status.
+
+The login method uses remote_user to check whether a user is
+authenticated. If they are, it redirects them back to the URL from
+which they were sent to the login page. Otherwise, a 401 response is
+returned, which triggers repoze.who to present a log in form.
+
+The log out form redirects the user back to the page they came from
+after deleting the authentication cookie.  The authentication cookie
+is configured in the repoze.who configuration file, ``who.ini``.
+
+We're going to want most pages to have links to the login and logout
+pages, and to display the logged in user, as appropriate. We provided
+some helper functions starting on line 23 for getting log in and log out
+URLs and for rendering a part of a page that either displays a log in
+link or the logged-in user and a log out link.
+
+The ``index`` function is modified to add the user info and log in or log out
+links.
+
+The ``save`` function illustrates a feature of the ``query``, ``post``, and
+``resource`` decorators that's especially useful for adding
+authorization checks.  The ``save`` function can't be used at all unless a
+user is authenticated.  We can pass a check function to the decorator
+that can compute a response if calling the underlying function isn't
+appropriate.  In this case, we use an ``authenticated`` function that
+returns a redirect response if a user isn't authenticated.
+
+The ``save`` method is modified to check whether the user is
+authenticated and to redirect to the login page if they're not.
+
+The ``get`` function is modified to:
+
+- Display user information and log-in/log-out links
+- Present a not-found page with a log-in link if the page doesn't
+  exist and the user isn't logged in.
+
+Some notes about this example:
+
+- The example implements a very simple *authorization* model.  A user
+  can add or edit content if they're logged in.  Otherwise they can't.
+
+- All the application knows about a user is their id.  The
+  authentication plug-in passes their log in name as their id.  A more
+  sophisticated plug-in would pass a less descriptive identifier and it
+  would be up to the application to look up descriptive information
+  from a user database based on this information.
+
+.. _wikiapaste:
+
+Assembling and running the example with Paste Deployment and Paste Script
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To use WSGI middleware, we'll use `Paste Deployment
+<http://pythonpaste.org/deploy/>`_ to configure the middleware and our
+application and to knit them together.  Here's the configuration file:
+
+.. literalinclude:: fswikia.ini
+   :language: ini
+
+The configuration defines 5 WSGI components, in 5 sections:
+
+``server:main``
+    This section configures a simple HTTP server running on port 8080.
+
+``app:main``
+    This section configures our application.  The options:
+
+    ``use``
+        The ``use`` option instructs Paste Deployment to run the bobo
+        main application.
+
+    ``bobo_resources``
+        The ``bobo_resources`` option tells bobo to run the application
+        in the module ``bobodoctestumentation.fswikia``.
+
+    ``bobo_configure``
+        The ``bobo_configure`` option tells bobo to call the config
+        function with the configuration options.
+
+    ``directory``
+        The ``directory`` option is used by the application to
+        determine where to store wiki pages.
+
+    ``filter-with``
+        The ``filter-with`` option tells Paste Deployment to apply the
+        reload middleware, defined by the ``filter:reload`` section to
+        the application.
+
+``filter:reload``
+    The ``filter:reload`` section defines a middleware component that
+    reloads given modules when their sources change.  It's provided by
+    the bobo egg under the name ``reload``, as indicated by the
+    ``use`` option.
+
+    The ``filter-with`` option is used to apply yet another filter,
+    ``who`` to the reload middleware.
+
+``filter:who``
+    The ``filter:who`` section configures a repose.who authentication
+    middleware component.  It uses the ``config_file`` option to
+    specify a repoze.who configuration file, ``who.ini``:
+
+    .. literalinclude:: who.ini
+       :language: ini
+
+   See the `repoze.who documentation <http://static.repoze.org/whodocs/>`_ for
+   details of configuring repoze.who.
+
+
+    The ``filter-with`` option is used again here to apply a final
+    middleware component, ``debug``.
+
+``filter:debug``
+    The ``filter:debug`` section defines a post-mortem debugging
+    middleware component that allows us to debug exceptions raised by
+    the application, or by the other 2 middleware components.
+
+In this example, we apply 3 middleware components to the bobo
+application. When a request comes in:
+
+    1. The server calls the debug component.
+
+    2. The debug component calls the who component.  If an
+       exception is raised, the ``pdb.post_mortem`` debugger is
+       invoked.
+
+    3. The who component checks for credentials and sets
+       ``REMOTE_USER`` in the request environment if they are present.
+       It then calls the reload component.  If the response from the
+       reload component has a 401 status, it presents a log in form.
+
+    4. The reload component checks to see if any of it's configured
+       module sources have changed. If so, it reloads the modules and
+       reinitializes it's application. (The reload component knows how
+       to reinitialize bobo applications and can only be used with
+       bobo application objects.)
+
+       The reload component calls the bobo application.
+
+The configuration above is intended to support development.  A
+production configuration would omit the ``reload`` and ``debug``
+components::
+
+  [app:main]
+  use = egg:bobo
+  bobo_resources = bobodoctestumentation.fswikia
+  bobo_configure = config
+  directory = wikidocs
+  filter-with = who
+
+  [filter:who]
+  use = egg:repoze.who#config
+  config_file = who.ini
+
+  [server:main]
+  use = egg:Paste#http
+  port = 8080
+
+To run the application in the foreground, we'll use::
+
+   paster serve fswikia.ini
+
+For this to work, the ``paster`` script must be installed in such a
+way that PasteScript, repoze.who, bobo, the wiki application
+module, and all their dependencies are all importable.  This can be done
+either by installing all of the necessary packages into a (real or
+`virtual <http://pypi.python.org/pypi/virtualenv>`_) Python, or using
+`zc.buildout <http://www.buildout.org/>`_.
+
+To run this example, I used a buildout that defined a ``paste`` part::
+
+  [paste]
+  recipe = zc.recipe.egg
+  eggs = PasteScript
+         repoze.who
+         bobodoctestumentation
+
+The bobodoctestumentation package is a package that includes the
+examples used in this documentation and depends on bobo.  Because the
+configuration files are in the ``bobodoctestumentation`` source
+directory, I actually ran the application this way::
+
+   cd bobodoctestumentation/src/bobodoctestumentation
+   ../../../bin/paster serve fswikia.ini
+
+Ajax calculator
+---------------
+
+This example shows how the ``application/json`` content type can be
+used in ajax [#ajax]_ applications.  We implement a small (silly) ajax
+calculator application:
+
+.. literalinclude:: bobocalc.py
+   :language: python
+   :linenos:
+
+The ``html`` method returns the application page:
+
+.. literalinclude:: bobocalc.html
+   :language: html
+   :linenos:
+
+This page presents a value, and input field and clear (C), add (+) and
+subtract (-) buttons.  When the user selects the add or subtract
+buttons, an ajax request is made to the server. The ajax request
+passes the input and current value as form data to the ``add`` or
+``sub`` resources on the server.
+
+The ``add`` and ``sub`` methods in ``bobocalc.py`` simply convert
+their arguments to integers and compute a new value which they return
+in a dictionary. Because we used the ``application/json`` content
+type, the dictionaries returned are marshaled as JSON.
+
+Static resources
+----------------
+
+We provide a resource that serves a static file-system directory.
+This is useful for serving static resources such as javascript source
+and CSS.
+
+.. literalinclude:: static.py
+   :language: python
+   :linenos:
+
+This example illustrates:
+
+traversal
+  The ``Directory.traverse`` method enables directories to be
+  traversed with a name to get to sub-directories or files.
+
+non-decorator syntax
+  We call :func:`scan_class` on the ``Directory`` and ``File`` classes
+  rather than using ``scan_class`` as a decorator so we can use the
+  application with Python 2.4 and Python 2.5.
+
+use of the :class:`bobo.NotFound` exception
+  Rather than construct a not-found ourselves, we simply raise
+  bobo.NotFound, and let bobo generate the response for us.
+
+
+----------------------------------------------------------------
+
+.. [#ajax] This isn't strictly "Ajax", because there's no XML
+   involved. The requests we're making are asynchronous and pass data
+   as form data and generally expect response data to be formatted as JSON.


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/examples.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,44 @@
+import bobo, os
+
+def config(config):
+    global top
+    top = config['directory']
+    if not os.path.exists(top):
+        os.mkdir(top)
+
+edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')
+
+ at bobo.query('/')
+def index():
+    return """<html><head><title>Bobo Wiki</title></head><body>
+    Documents
+    <hr />
+    %(docs)s
+    </body></html>
+    """ % dict(
+        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
+                           for name in sorted(os.listdir(top)))
+        )
+
+ at bobo.post('/:name')
+def save(bobo_request, name, body):
+    open(os.path.join(top, name), 'w').write(body)
+    return bobo.redirect(bobo_request.path_url, 303)
+
+ at bobo.query('/:name')
+def get(name, edit=None):
+    path = os.path.join(top, name)
+    if os.path.exists(path):
+        body = open(path).read()
+        if edit:
+            return open(edit_html).read() % dict(
+                name=name, body=body, action='Edit')
+
+        return '''<html><head><title>%(name)s</title></head><body>
+        %(name)s (<a href="%(name)s?edit=1">edit</a>)
+        <hr />%(body)s</body></html>
+        ''' % dict(name=name, body=body)
+
+    return open(edit_html).read() % dict(
+        name=name, body='', action='Create')
+


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,61 @@
+File-system wiki
+----------------
+
+    >>> import bobo, os, webtest
+    >>> os.mkdir('docs')
+    >>> app = webtest.TestApp(bobo.Application(
+    ...   bobo_resources='bobodoctestumentation.fswiki',
+    ...   bobo_configure='bobodoctestumentation.fswiki:config',
+    ...   directory='docs',
+    ...   ))
+
+    >>> print app.get('/')
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>Bobo Wiki</title></head><body>
+        Documents
+        <hr />
+        </body></html>
+
+    >>> print app.get('/front') # doctest: +ELLIPSIS
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html>
+    ...
+      <body class="tundra">
+        <h1>Create front</h1>
+        ...
+        <form method="POST">
+          <input type="hidden" name="body" id="page_body">
+          <input type="submit" value="Save">
+        </form>
+      </body>
+    </html>
+
+    >>> print app.post('/front', 'body=sometext')
+    Response: 303 See Other
+    Content-Type: text/html; charset=UTF-8
+    Location: http://localhost/front
+    See http://localhost/front
+
+    >>> print app.get('/front')
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>front</title></head><body>
+            front (<a href="front?edit=1">edit</a>)
+            <hr />sometext</body></html>
+
+    >>> print app.get('/')
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>Bobo Wiki</title></head><body>
+        Documents
+        <hr />
+        <a href="front">front</a>
+        </body></html>
+
+    >>> os.listdir('docs')
+    ['front']
+
+    >>> open(os.path.join('docs', 'front')).read()
+    'sometext'


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswiki.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.ini
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.ini	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.ini	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,23 @@
+[app:main]
+use = egg:bobo
+bobo_module = bobodoctestumentation.fswikia
+bobo_configure = config
+directory = wikidocs
+filter-with = reload
+
+[filter:reload]
+use = egg:bobo#reload
+modules = bobodoctestumentation.fswikia
+filter-with = who
+
+[filter:who]
+use = egg:repoze.who#config
+config_file = who.ini
+filter-with = debug
+
+[filter:debug]
+use = egg:bobo#debug
+
+[server:main]
+use = egg:Paste#http
+port = 8080


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.ini
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,92 @@
+import bobo, os, webob
+
+def config(config):
+    global top
+    top = config['directory']
+    if not os.path.exists(top):
+        os.mkdir(top)
+
+edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')
+
+ at bobo.query('/login.html')
+def login(bobo_request, where=None):
+    if bobo_request.remote_user:
+        return bobo.redirect(where or bobo_request.relative_url('.'))
+    return webob.Response(status=401)
+
+ at bobo.query('/logout.html')
+def logout(bobo_request, where=None):
+    response = bobo.redirect(where or bobo_request.relative_url('.'))
+    response.delete_cookie('wiki')
+    return response
+
+def login_url(request):
+    return request.application_url+'/login.html?where='+request.url
+
+def logout_url(request):
+    return request.application_url+'/logout.html?where='+request.url
+
+def who(request):
+    user = request.remote_user
+    if user:
+        return '''
+        <div style="float:right">Hello: %s
+        <a href="%s">log out</a></div>
+        ''' % (user, logout_url(request))
+    else:
+        return '''
+        <div style="float:right"><a href="%s">log in</a></div>
+        ''' % login_url(request)
+
+ at bobo.query('/')
+def index(bobo_request):
+    return """<html><head><title>Bobo Wiki</title></head><body>
+    <div style="float:left">Documents</div>%(who)s
+    <hr style="clear:both" />
+    %(docs)s
+    </body></html>
+    """ % dict(
+        who=who(bobo_request),
+        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
+                           for name in sorted(os.listdir(top))),
+        )
+
+def authenticated(self, request, func):
+    if not request.remote_user:
+        return bobo.redirect(login_url(request))
+
+ at bobo.post('/:name', check=authenticated)
+def save(bobo_request, name, body):
+    open(os.path.join(top, name), 'w').write(body.encode('UTF-8'))
+    return bobo.redirect(bobo_request.path_url, 303)
+
+ at bobo.query('/:name')
+def get(bobo_request, name, edit=None):
+    user = bobo_request.remote_user
+
+    path = os.path.join(top, name)
+    if os.path.exists(path):
+        body = open(path).read().decode('UTF-8')
+        if edit:
+            return open(edit_html).read() % dict(
+                name=name, body=body, action='Edit')
+
+        if user:
+            edit = ' (<a href="%s?edit=1">edit</a>)' % name
+        else:
+            edit = ''
+
+        return '''<html><head><title>%(name)s</title></head><body>
+        <div style="float:left">%(name)s%(edit)s</div>%(who)s
+        <hr style="clear:both" />%(body)s</body></html>
+        ''' % dict(name=name, body=body, edit=edit, who=who(bobo_request))
+
+    if user:
+        return open(edit_html).read() % dict(
+            name=name, body='', action='Create')
+
+    return '''<html><head><title>Not found: %(name)s</title></head><body>
+        <h1>%(name)s doesn not exist.</h1>
+        <a href="%(login)s">Log in</a> to create it.
+        </body></html>
+        ''' % dict(name=name, login=login_url(bobo_request))


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,123 @@
+File-system wiki with authentication and (minimal) authorization
+----------------------------------------------------------------
+
+    >>> import bobo, os, webtest
+    >>> os.mkdir('docs')
+    >>> boboapp = bobo.Application(
+    ...   bobo_resources='bobodoctestumentation.fswikia',
+    ...   bobo_configure='bobodoctestumentation.fswikia:config',
+    ...   directory='docs',
+    ...   )
+    >>> app = webtest.TestApp(boboapp)
+
+    >>> print app.get('/') # doctest: +NORMALIZE_WHITESPACE
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>Bobo Wiki</title></head><body>
+      <div style="float:left">Documents</div>
+      <div style="float:right"><a
+               href="http://localhost/login.html?where=http://localhost/">log
+                                                               in</a></div>
+        <hr style="clear:both" />
+        </body></html>
+
+    >>> print app.get('/front') # doctest: +NORMALIZE_WHITESPACE
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>Not found: front</title></head><body>
+    <h1>front doesn not exist.</h1>
+    <a href="http://localhost/login.html?where=http://localhost/front">Log
+        in</a> to create it.
+            </body></html>
+
+    >>> print app.post('/front', 'body=sometext')
+    Response: 302 Found
+    Content-Type: text/html; charset=UTF-8
+    Location: http://localhost/login.html?where=http://localhost/front
+    See http://localhost/login.html?where=http://localhost/front
+
+    >>> print app.get('http://localhost/login.html'
+    ...               '?where=http://localhost/front', status=401)
+    Response: 401 Unauthorized
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+
+    >>> app = webtest.TestApp(boboapp, extra_environ=dict(REMOTE_USER='bobo'))
+
+    >>> print app.get('http://localhost/login.html'
+    ...               '?where=http://localhost/front')
+    Response: 302 Found
+    Content-Type: text/html; charset=UTF-8
+    Location: http://localhost/front
+    See http://localhost/front
+
+    >>> print app.get('/front') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html>
+      <head>
+        <title>Create front</title>
+      ...
+      <body class="tundra">
+        <h1>Create front</h1>
+        ...
+        <form method="POST">
+          <input type="hidden" name="body" id="page_body">
+          <input type="submit" value="Save">
+        </form>
+      </body>
+    </html>
+
+    >>> print app.post('/front', 'body=sometext')
+    Response: 303 See Other
+    Content-Type: text/html; charset=UTF-8
+    Location: http://localhost/front
+    See http://localhost/front
+
+    >>> print app.get('/front') # doctest: +NORMALIZE_WHITESPACE
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>front</title></head><body>
+      <div style="float:left">front (<a href="front?edit=1">edit</a>)</div>
+      <div style="float:right">Hello: bobo
+      <a href="http://localhost/logout.html?where=http://localhost/front">log
+           out</a></div>
+      <hr style="clear:both" />sometext</body></html>
+
+    >>> print app.get('/') # doctest: +NORMALIZE_WHITESPACE
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>Bobo Wiki</title></head><body>
+        <div style="float:left">Documents</div>
+            <div style="float:right">Hello: bobo
+            <a href="http://localhost/logout.html?where=http://localhost/">log
+                      out</a></div>
+        <hr style="clear:both" />
+        <a href="front">front</a>
+        </body></html>
+
+    >>> print app.get('http://localhost/logout.html?'
+    ...               'where=http://localhost/front')
+    ... # doctest: +ELLIPSIS
+    Response: 302 Found
+    Content-Type: text/html; charset=UTF-8
+    Location: http://localhost/front
+    Set-Cookie: wiki=; expires="..."; Max-Age=0; Path=/
+    See http://localhost/front
+
+    >>> app = webtest.TestApp(boboapp)
+    >>> print app.get('/front') # doctest: +NORMALIZE_WHITESPACE
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    <html><head><title>front</title></head><body>
+      <div style="float:left">front</div>
+      <div style="float:right"><a
+         href="http://localhost/login.html?where=http://localhost/front">log
+                                                             in</a></div>
+      <hr style="clear:both" />sometext</body></html>
+
+    >>> os.listdir('docs')
+    ['front']
+
+    >>> open(os.path.join('docs', 'front')).read()
+    'sometext'


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/fswikia.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/htpasswd
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/htpasswd	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/htpasswd	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1 @@
+jim:X6Htwq7jlZPPQ


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/htpasswd
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/index.txt
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/index.txt	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/index.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,731 @@
+Introduction
+============
+
+Bobo is a light-weight framework for creating `WSGI
+<http://wsgi.org>`_ web applications.
+
+It's goal is to be easy to learn and remember.
+
+It provides 2 features:
+
+- Mapping URLs to objects
+
+- Calling objects to generate HTTP responses
+
+It doesn't have a templateing language, a database integration layer,
+or a number of other features that can be provided by WSGI middle-ware
+or application-specific libraries.
+
+Bobo builds on other frameworks, most notably WSGI and `WebOb
+<http://pythonpaste.org/webob/>`_.
+
+.. _installation:
+
+Installation
+============
+
+Bobo can be installed in the usual ways, including using the `setup.py
+install command
+<http://docs.python.org/install/index.html#the-new-standard-distutils>`_.
+You can, of course, use `Easy Install
+<http://peak.telecommunity.com/DevCenter/EasyInstall>`_, `Buildout
+<http://www.buildout.org>`_, or `pip <http://pip.openplans.org/>`_.
+
+To use the setup.py install command, download and unpack the `source
+distribution <http://pypi.python.org/bobo>`_ and run the setup
+script::
+
+  python setup.py install
+
+Bobo works with Python 2.4, 2.5, and 2.6.  Python 3.0 support is planned.
+Of course, when using Python 2.4 and 2.5, class decorator syntax can't
+be used. You can still use the decorators by calling them with a class
+after a class is created.
+
+Getting Started
+===============
+
+Let's create a minimal web application, "hello world". We'll put it in
+a file named "hello.py"::
+
+    import bobo
+
+    @bobo.query
+    def hello():
+        return "Hello world!"
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+
+    >>> import webob, webtest, bobo
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+This application creates a single web resource, "/hello.html", that simply
+outputs the text "Hello world".
+
+Bobo decorators, like ``bobo.query`` used in the example above
+control how URLs are mapped to objects. They also control how
+functions are called and returned values converted to web responses.
+If a function returns a string, it's assumed to be HTML and used to
+construct a response.  You can control the content type used by
+passing a content_type keyword argument to the decorator.
+
+Let's try out our application.  Assuming that bobo's installed, you
+can run the application on port 8080 using [#bobooptions]_::
+
+    bobo -f hello.py
+
+This will start a web server running on localhost port 8080.  If you
+visit::
+
+    http://localhost:8080/hello.html
+
+.. -> url strip
+
+you'll get the greeting::
+
+    Hello world!
+
+.. -> expected_body strip
+
+    >>> app.get(url, status=200).body == expected_body
+    True
+
+The URL we used to access the application was determined by the name
+of the resource function and the content type used by the decorator,
+which defaults to "text/html; charset=UTF-8". Let's change the
+application so we can use a URL like::
+
+    http://localhost:8080/
+
+.. -> url strip
+
+We'll do this by providing a URL path::
+
+    @bobo.query('/')
+    def hello():
+        return "Hello world!"
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+    >>> app.get(url, status=200).body == expected_body
+    True
+
+Here, we passed a path to the ``query`` decorator.  We used a '/'
+string, which makes a URL like the one above work. (We also omitted
+the import for brevity.)
+
+We don't need to restart the server to see our changes.  The bobo
+development server automatically reloads the file if it changes.
+
+As its name suggests, the ``query`` decorator is meant to work with
+resources that return information, possibly using form data.  Let's
+modify the application to allow the name of the person to greet to be
+given as form data::
+
+    @bobo.query('/')
+    def hello(name="world"):
+        return "Hello %s!" % name
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+If a function accepts named arguments, then data will be
+supplied from form data.  If we visit::
+
+    http://localhost:8080/?name=Sally
+
+.. -> url strip
+
+We'll get the output::
+
+   Hello Sally!
+
+.. -> expected_body strip
+
+    >>> app.get(url, status=200).body == expected_body
+    True
+
+The ``query`` decorator will accept ``GET``, ``POST`` and ``HEAD``
+requests. It's appropriate when server data aren't modified.  To
+accept form data and modify data on a server, you should use the ``post``
+decorator. The ``post`` decorator works like the ``query`` decorator
+accept that it only allows ``POST`` and ``PUT`` requests and won't pass data
+provided in a query string as function arguments.
+
+::
+
+    @bobo.post('/')
+    def hello(name="world"):
+        return "Hello %s!" % name
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+    >>> app.post('/', 'name=Bob', status=200)
+    <200 OK text/html body='Hello Bob!'>
+    >>> app.put('/', 'name=Bob', status=200)
+    <200 OK text/html body='Hello Bob!'>
+
+    >>> response = app.get(url, status=405)
+    >>> response.headers['Allow']
+    'POST, PUT'
+    >>> print response.body,
+    <html>
+    <head><title>Method Not Allowed</title></head>
+    <body>Invalid request method: GET</body>
+    </html>
+
+The ``query`` and ``post`` decorators are convenient when you want to just get
+user input passed as function arguments.  If you want a bit more
+control, you can also get the request object by defining a
+``bobo_request`` parameter::
+
+    @bobo.query('/')
+    def hello(bobo_request, name="world"):
+        return "Hello %s! (method=%s)" % (name, bobo_request.method)
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+    >>> app.get('/', status=200).body
+    'Hello world! (method=GET)'
+
+The request object gives full access to all of the form data, as well
+as other information, such as cookies and input headers.
+
+The ``query`` and ``post`` decorators introspect the function they're
+applied to. This means they can't be used with callable objects that
+don't provide function meta data.  There's a low-level decorator,
+``resource`` that does no introspection and can be used with any
+callable::
+
+    @bobo.resource('/')
+    def hello(request):
+        name = request.params.get('name', 'world!')
+        return "Hello %s!" % name
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+    >>> app.get('/?name=Bob', status=200).body
+    'Hello Bob!'
+
+The ``resource`` decorator always passes the request object as the first
+positional argument to the callable it's given.
+
+Automatic response generation
+==============================
+
+The :func:`resource`, :func:`post`, and :func:`query` decorators
+provide automatic response generation when the value returned by an
+application isn't a :term:`response` object.  The generation
+of the response is controlled by the content type given to the
+``content_type`` decorator parameter.
+
+If an application returns a string, then a response is
+constructed using the string with the content type.
+
+If an application doesn't return a response or a string, then the
+handling depends on whether or not the content type is
+``'application/json``. For ``'application/json``, the returned value
+is marshalled to JSON using the ``json`` (or ``simplejson``) module, if
+present.  If the module isn't importable, or if marshaling fails, then
+an exception will be raised.
+
+If an application returns a unicode string and the content type
+isn't ``'application/json'``, the string is encoded using the
+character set given in the content_type, or using the UTF-8
+encoding, if the content type doesn't include a charset
+parameter.
+
+If an application returns a non-response non-string result and
+the content type isn't ``'application/json'``, then an
+exception is raised.
+
+If an application wants greater control over a response, it will
+generally want to construct a `webob.Response
+<http://pythonpaste.org/webob/reference.html#id2>`_ object and return
+that.
+
+.. _routes:
+
+Routes
+======
+
+We saw earlier that we could control the URLs used to access resources
+by passing a path to a decorator. The path we pass can specify a
+multi-level URL and can have placeholders, which allow us to pass data
+to the resource as part of the URL.
+
+Here, we modify the hello application to let us pass the name of the
+greeter in the URL::
+
+    @bobo.query('/greeters/:myname')
+    def hello(name="world", myname='Bobo'):
+        return "Hello %s! My name is %s." % (name, myname)
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+Now, to access the resource, we use a URL like::
+
+    http://localhost:8080/greeters/myapp?name=Sally
+
+.. -> url strip
+
+for which we get the output::
+
+    Hello Sally! My name is myapp.
+
+.. -> expected_body strip
+
+    >>> app.get(url).body == expected_body
+    True
+
+We call these paths :term:`routes` because they use a syntax inspired
+loosely by the `Ruby on Rails Routing
+<http://api.rubyonrails.org/classes/ActionController/Routing.html>`_
+system.
+
+You can have any number of placeholders or constant URL paths in a
+route.  The values associated with the placeholders will be made
+available as function arguments.
+
+If a placeholder is followed by a question mark, then the route
+segment is optional.  If we change the hello example::
+
+    @bobo.query('/greeters/:myname?')
+    def hello(name="world", myname='Bobo'):
+        return "Hello %s! My name is %s." % (name, myname)
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+we can use the URL::
+
+    http://localhost:8080/greeters?name=Sally
+
+.. -> url strip
+
+for which we get the output::
+
+    Hello Sally! My name is Bobo.
+
+.. -> expected_body strip
+
+    >>> app.get(url).body == expected_body
+    True
+
+Note, however, if we use the URL::
+
+    http://localhost:8080/greeters/?name=Sally
+
+.. -> url strip
+
+we get the output::
+
+    Hello Sally! My name is .
+
+.. -> expected_body strip
+
+    >>> app.get(url).body == expected_body
+    True
+
+Placeholders must be legal Python identifiers.  A placeholder may be
+followed by an extension.  For example, we could use::
+
+    @bobo.query('/greeters/:myname.html')
+    def hello(name="world", myname='Bobo'):
+        return "Hello %s! My name is %s." % (name, myname)
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+Here, we've said that the name must have an ".html" suffix.  To access
+the function, we use a URL like::
+
+    http://localhost:8080/greeters/myapp.html?name=Sally
+
+.. -> url strip
+
+And get::
+
+    Hello Sally! My name is myapp.
+
+.. -> expected_body strip
+
+    >>> app.get(url).body == expected_body
+    True
+
+If the placeholder is optional::
+
+    @bobo.query('/greeters/:myname?.html')
+    def hello(name="world", myname='Bobo'):
+        return "Hello %s! My name is %s." % (name, myname)
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+Then we can use a URL like::
+
+    http://localhost:8080/greeters?name=Sally
+
+.. -> url strip
+
+    >>> app.get(url, status=200).body
+    'Hello Sally! My name is Bobo.'
+
+or::
+
+    http://localhost:8080/greeters/jim.html?name=Sally
+
+.. -> url strip
+
+    >>> app.get(url, status=200).body
+    'Hello Sally! My name is jim.'
+
+
+Subroutes
+---------
+
+Sometimes, you want to split URL matching into multiple steps.  You
+might do this to provide cleaner abstractions in your application, or
+to support more flexible resource organization.  You can use the
+subroute decorator to do this.  The subroute decorator decorates a
+callable object that returns a resource.  The subroute uses the given
+route to match the beginning of the request path.  The resource
+returned by the callable is matched against the remainder of the
+path. Let's look at an example::
+
+    import bobo
+
+    database = {
+       '1': dict(
+            name='Bob',
+            documents = {
+              'hi.html': "Hi. I'm Bob.",
+              'hobbies': {
+                'cooking.html': "I like to cook.",
+                'sports.html': "I like to ski.",
+                },
+              },
+            ),
+    }
+
+    @bobo.subroute('/employees/:employee_id', scan=True)
+    class Employees:
+
+        def __init__(self, request, employee_id):
+            self.employee_id = employee_id
+            self.data = database[employee_id]
+
+        @bobo.resource('')
+        def base(self, request):
+            return bobo.redirect(request.url+'/')
+
+        @bobo.query('/')
+        @bobo.query('/summary.html')
+        def summary(self):
+            return """
+            id: %s
+            name: %s
+            See my <a href="documents">documents</a>.
+            """ % (self.employee_id, self.data['name'])
+
+        @bobo.query('/details.html')
+        def details(self):
+            "Show employee details"
+            # ...
+
+        @bobo.post('/update.html')
+        def add(self, name, phone, fav_color):
+            "Update employee data"
+            # ...
+
+        @bobo.subroute
+        def documents(self, request):
+            return Folder(self.data['documents'])
+
+.. -> src
+
+    >>> import sys
+    >>> if sys.version_info < (2, 6):
+    ...     src = src.replace("\n at bobo.subroute", "\n#bobo.subroute")
+    ...     src += ("\nEmployees = bobo.subroute("
+    ...             "'/employees/:employee_id', scan=True)(Employees)")
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+With this example, if we visit::
+
+    http://localhost:8080/employees/1/summary.html
+
+.. -> url strip
+
+    >>> print app.get(url).body
+    <BLANKLINE>
+            id: 1
+            name: Bob
+            See my <a href="documents">documents</a>.
+    <BLANKLINE>
+
+We'll get the summary for a user.  The URL will be matched in 2
+steps. First, the path ``/employees/1`` will match the subroute.  The
+class is called with the request and employee id. Then the routes
+defined for the individual methods are searched.  The remainder of the
+path, ``/summary.html``, matches the route for the summary
+method. (Note that we provided two decorators for the summary method,
+which allows us to get to it two ways.)  The methods were scanned for
+routes because we used the ``scan`` keyword argument.
+
+The ``base`` method has a route that is an empty string. This is a special
+case that handles an empty path after matching a subroute.  The base
+method will be called for a URL like::
+
+    http://localhost:8080/employees/1
+
+.. -> url strip
+
+which would redirect to::
+
+    http://localhost:8080/employees/1/
+
+.. -> expected_location strip
+
+    >>> response = app.get(url, status=302)
+    >>> response.headers['location'] == expected_location
+    True
+
+The ``documents`` method defines another subroute. Because we left off the
+route path, the method name is used.  This returns a Folder
+instance. Let's look at the Folder class::
+
+    @bobo.scan_class
+    class Folder:
+
+        def __init__(self, data):
+            self.data = data
+
+        @bobo.query('')
+        def base(self, bobo_request):
+            return bobo.redirect(request.url+'/')
+
+        @bobo.query('/')
+        def index(self):
+            return '\n'.join('<a href="%s/">%s<a><br>' % (k, k)
+                             for k in self.data)
+
+        @bobo.subroute('/:item_id')
+        def subitem(self, request, item_id):
+            item = self.data[item_id]
+            if isinstance(item, dict):
+               return Folder(item)
+            else:
+               return Document(item)
+
+    @bobo.scan_class
+    class Document:
+
+        def __init__(self, text):
+            self.text = text
+
+        @bobo.query('')
+        def get(self):
+            return self.text
+
+.. -> src
+
+    >>> if sys.version_info < (2, 6):
+    ...     src = src.replace("\n at bobo.scan_class", "")
+    ...     src += "\nbobo.scan_class(Folder)\nbobo.scan_class(Document)"
+
+    >>> update_module('helloapp', src)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+
+The ``Folder`` and ``Document`` classes use the ``scan_class``
+decorator. The ``scan_class`` class decorator scans a class to make
+routes defined for it's methods available.  Using the ``scan_class``
+decorator is equivalent to using the ``scan`` keyword with
+``subroute`` decorator [#whyscan]_.  Now consider a URL::
+
+    http://localhost:8080/employees/1/documents/hobbies/sports.html
+
+.. -> url strip
+
+which outputs::
+
+    I like to ski.
+
+.. -> expected_body strip
+
+    >>> app.get(url).body == expected_body
+    True
+
+The URL is matched in multiple steps:
+
+1. The path ``/employees/1`` matches the ``Employees`` class.
+
+2. The path ``/documents`` matches the ``documents`` method, which returns
+   a ``Folder`` using the employee documents dictionary.
+
+3. The path ``/hobbies`` matches the ``subitem`` method of the ``Folder``
+   class, which returns the ``hobbies`` dictionary from the documents folder.
+
+4. The path ``/sports.html`` also matches the ``subitem`` ``Folder`` method,
+   which returns a ``Document`` using the text for the ``sports.html`` key.
+
+5, The empty path matches the ``get`` method of the ``Document`` class.
+
+Of course, the employee document tree can be arbitrarily deep.
+
+The ``subroute`` decorator can be applied to any callable object that
+takes a request and route data and returns a resource.
+
+Methods and REST
+----------------
+
+When we define a resource, we can also specify the HTTP methods it will
+handle.  The ``resource`` and ``query`` decorators will handle GET, HEAD and
+POST methods by default. The ``post`` decorator handles POST and PUT methods.
+You can specify one or more methods when using the ``resource``,
+``query``, and ``post`` decorators::
+
+    @bobo.resource(method='GET')
+    def hello(who='world'):
+        return "Hello %s!" % who
+
+    @bobo.resource(method=['GET', 'HEAD'])
+    def hello2(who='world'):
+        return "Hello %s!" % who
+
+If multiple resources (resource, query, or post) in a module or class
+have the same route strings, the resource used will be selected based
+on both the route and the methods allowed. (If multiple resources match
+a request, the first one defined will be used [#order]_.)
+
+::
+
+    @bobo.subroute('/employees/:employeeid')
+    class Employee:
+
+        def __init__(self, request, employee_id):
+            self.request = request
+            self.id = employee_id
+
+        @bobo.bobo.resource('', 'PUT')
+        def put(self, request):
+            "Save employee data"
+
+        @bobo.post('')
+        def new_employee(self):
+            "Add an employee"
+
+        @bobo.query('', 'GET')
+        def get(self, request):
+            "Get employee data"
+
+        @bobo.bobo.resource('/resume', 'PUT')
+        def save_resume(self, request):
+            "Save employee data"
+
+        @bobo.bobo.query('/resume')
+        def resume(self):
+            "Save employee data"
+
+The ability to provide handlers for specific methods provides support
+for the `REST architectural style
+<http://en.wikipedia.org/wiki/Representational_State_Transfer>`_.
+.. _configuration:
+
+Beyond the bobo development server
+==================================
+
+The bobo server makes it easy to get started.  Just run it with a
+source file and off you go.  When you're ready to deploy your
+application, you'll want to put your source code in an importable
+Python module (or package). Bobo publishes modules, not source
+files. The bobo server provides the convenience of converting a source
+file to a module.
+
+The bobo command-line server is convenient for getting
+started, but production applications will usually be configured with
+selected servers and middleware using `Paste Deployment
+<http://pythonpaste.org/deploy/>`. Bobo includes a Paste Deployment
+application implementation.  To use bobo with Paste Deployment, simply
+define an application section using the bobo egg::
+
+    [app:main]
+    use = egg:bobo
+    bobo_resources = hellowapp
+    bobo_configure = helloapp:configure
+    employees_database = /home/databases/employees.db
+
+    [server:main]
+    use = egg:Paste#http
+    host = localhost
+    port = 8080
+
+In this example, we're using the HTTP `server that is built into Paste
+<http://pythonpaste.org/modules/httpserver.html>`_.
+
+The application section (``app:main``) contains bobo options, as well
+as application-specific options.  In this example, we used the
+``bobo_resources`` option to specify that we want to use resources
+found in the hellowapp module, and the ``bobo_configure`` option to
+specify a configuration handler to be called with configuration data.
+
+You can put application-specific options in the application section,
+which can be used by configuration handlers.  You can provide one or
+more configuration handlers using the bobo_configure option.  Each
+configuration handler is specified as a module name and global name
+[#globalexpr]_ separated by a colon.
+
+Configuration handlers are called with a mapping object containing
+options from the application section and from the DEFAULT section, if
+present, with application options taking precedence.
+
+To start the server, you'll run the paster script installed with
+PasteScript and specify the name of your configuration file::
+
+    paster serve app.ini
+
+You'll need to install `Paste Script
+<http://pypi.python.org/pypi/PasteScript>`_ to use bobo with Paste Deployment.
+
+See :ref:`wikiapaste` for a complete example.
+
+.. [#bobooptions] You can use the ``-p`` option to control the port
+   used. To find out more about the bobo server, use the ``-h`` option
+   or see :ref:`boboserver`.
+
+.. [#whyscan] You might be wondering why we require the scan keyword in
+   the subroute decorator to scan methods for resources.  The reason
+   is that scan_class is somewhat invasive. It adds a instance method
+   to the class, which may override an existing method. This should
+   not be done implicitly.
+
+.. [#order] More precisely, the resource with the lowest :term:`order`
+   will be used.  By default, a resources order is determined by the
+   order of definition.  You can override the order by passing an
+   ``order`` keyword argument to a decorator. See :ref:`order`.
+
+.. [#globalexpr] The name can be any Python expression that doesn't
+   contain spaces. It will be evaluated using the module globals.


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/index.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/main.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/main.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/main.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,338 @@
+bobo main (default) application factory
+=======================================
+
+The bobo.main module provides a Paste Deployment application factory
+that published module globals that have bobo_response methods. We
+call these objects resources.
+
+By default, the main application generated by the factory scans the
+given modules for resources.  Let's create some resources in some test modules:
+
+    >>> import bobo, webob, sys
+    >>> class Resource:
+    ...     def __init__(self, path):
+    ...         self.path = path
+    ...         self.bobo_order = bobo.order()
+    ...     def bobo_response(self, request, path, method):
+    ...         print 'trying', self.path
+    ...         if path == self.path:
+    ...             return self(request)
+    ...     def __call__(self, request, where='here'):
+    ...         return webob.Response("%s %s!" % (self.path, where))
+    ...     def bobo_reroute(self, route):
+    ...         return Resource(route)
+
+
+    >>> import bobo.testmodule1, bobo.testmodule2
+
+    >>> for i in range(5):
+    ...     setattr(bobo.testmodule1, 'v%sx' % i, Resource('/mod1/v%s' % i))
+    ...     setattr(bobo.testmodule2, 'v%sx' % i, Resource('/mod2/v%s' % i))
+
+Now, we'll create an application:
+
+    >>> import webtest, sys
+    >>> stdout = sys.stdout
+    >>> def makeapp(*args, **kw):
+    ...     app = bobo.Application(*args, **kw)
+    ...     def work_around_webtest_duping_stdout(*appargs):
+    ...         sys.stdout = stdout
+    ...         return app(*appargs)
+    ...     return webtest.TestApp(work_around_webtest_duping_stdout)
+
+    >>> app = makeapp(bobo_resources='bobo.testmodule1\nbobo.testmodule2')
+
+We can make requests of the application for our test paths:
+
+    >>> print app.get('/mod1/v0', status=200).body
+    trying /mod1/v0
+    /mod1/v0 here!
+
+    >>> print app.get('/mod2/v3', status=200).body
+    trying /mod1/v0
+    trying /mod1/v1
+    trying /mod1/v2
+    trying /mod1/v3
+    trying /mod1/v4
+    trying /mod2/v0
+    trying /mod2/v1
+    trying /mod2/v2
+    trying /mod2/v3
+    /mod2/v3 here!
+
+If make requests for a path we don't have a resource for, we'll get a
+non-found response:
+
+    >>> print app.get('/mod2', status=404).body
+    trying /mod1/v0
+    trying /mod1/v1
+    trying /mod1/v2
+    trying /mod1/v3
+    trying /mod1/v4
+    trying /mod2/v0
+    trying /mod2/v1
+    trying /mod2/v2
+    trying /mod2/v3
+    trying /mod2/v4
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /mod2</body>
+    </html>
+    <BLANKLINE>
+
+Route configuration
+-------------------
+
+Routes can be defined as part of the configuration:
+
+    >>> app = makeapp(bobo_resources="""
+    ...     /foo/bar -> bobo.testmodule1:v2x
+    ...     /:where/x ->
+    ...          bobo.testmodule1:v3x
+    ...     """)
+
+Now, with the new app, the old routes don't work, but the new ones do:
+
+    >>> print app.get('/mod2/v3', status=404).body
+    trying /foo/bar
+    trying /:where/x
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /mod2/v3</body>
+    </html>
+    <BLANKLINE>
+
+    >>> print app.get('/foo/bar', status=200).body
+    trying /foo/bar
+    /foo/bar here!
+
+    >>> print app.get('/there/x', status=404).body
+    trying /foo/bar
+    trying /:where/x
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /there/x</body>
+    </html>
+    <BLANKLINE>
+
+Note that in the last example, we didn't find the resource because our
+test resource doesn't interpret route placeholders.
+
+We can have resources that implement subroutes.  That is, they are
+called to produce resources.
+
+    >>> class SubRoute:
+    ...     def __init__(self, request, **route_info):
+    ...         self.route_info = route_info
+    ...
+    ...     def bobo_response(self, request, path, method):
+    ...         if path == '/x/y/z':
+    ...             return self(request)
+    ...
+    ...     def __call__(self, request):
+    ...         return webob.Response(str(self.route_info))
+
+    >>> bobo.testmodule2.SubRoute = SubRoute
+
+    >>> app = makeapp(bobo_resources="""
+    ...     /:where ->
+    ...          bobo.testmodule2:SubRoute
+    ...     """)
+
+    >>> print app.get('/there/x', status=404).body
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /there/x</body>
+    </html>
+    <BLANKLINE>
+
+    >>> print app.get('/there/x/y/z', status=200).body
+    {'where': 'there'}
+
+Note classes that define bobo_response instance methods aren't
+found via module scanning:
+
+    >>> app = makeapp(bobo_resources='bobo.testmodule2')
+    >>> print app.get('/xxx', status=404).body
+    trying /mod2/v0
+    trying /mod2/v1
+    trying /mod2/v2
+    trying /mod2/v3
+    trying /mod2/v4
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /xxx</body>
+    </html>
+    <BLANKLINE>
+
+If the class had been found, calling it's unbound bobo_response
+method would have caused an error. OTOH, classes with bobo_response
+class methods will be found:
+
+    >>> class C:
+    ...     @classmethod
+    ...     def bobo_response(self, request, path, method):
+    ...         print 'trying C'
+
+    >>> bobo.testmodule2.C = C
+    >>> app = makeapp(bobo_resources='bobo.testmodule2')
+    >>> print app.get('/xxx', status=404).body
+    trying /mod2/v0
+    trying /mod2/v1
+    trying /mod2/v2
+    trying /mod2/v3
+    trying /mod2/v4
+    trying C
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /xxx</body>
+    </html>
+    <BLANKLINE>
+
+Configuration handlers
+----------------------
+
+    >>> import pprint
+    >>> def config_handler(d):
+    ...     print 'handler1'
+    ...     pprint.pprint(d, width=1)
+
+    >>> bobo.testmodule1.config_handler = config_handler
+
+    >>> import pprint
+    >>> def config_handler(d):
+    ...     print 'handler2'
+    ...     pprint.pprint(d, width=1)
+
+    >>> bobo.testmodule2.config_handler = config_handler
+
+    >>> app = makeapp(
+    ...     dict(x=1, y=2),
+    ...     bobo_resources='bobo.testmodule1',
+    ...     bobo_configure=
+    ...      'bobo.testmodule2:config_handler bobo.testmodule2:config_handler',
+    ...     x=3) # doctest: +NORMALIZE_WHITESPACE
+    handler2
+    {'bobo_configure':
+     'bobo.testmodule2:config_handler bobo.testmodule2:config_handler',
+     'bobo_resources': 'bobo.testmodule1',
+     'x': 3,
+     'y': 2}
+    handler2
+    {'bobo_configure':
+     'bobo.testmodule2:config_handler bobo.testmodule2:config_handler',
+     'bobo_resources': 'bobo.testmodule1',
+     'x': 3,
+     'y': 2}
+
+Scanning modules
+----------------
+
+When scanning a module, resources with the same routs are combined.
+
+    >>> bobo.testmodule1.__dict__.clear()
+
+    >>> @bobo.resource('/:y', 'GET')
+    ... def gety(request, y):
+    ...     return "B.gety %s %s" % (request.method, y)
+    >>> @bobo.resource('', 'GET')
+    ... def get(request):
+    ...     return "B.get %s" % (request.method)
+    >>> @bobo.resource('/:y', 'POST')
+    ... def posty(request, y):
+    ...     return "C.posty %s %s" % (request.method, y)
+    >>> @bobo.resource('', 'POST')
+    ... def post(request):
+    ...     return "C.post %s" % (request.method)
+
+    >>> bobo.testmodule1.get = get
+    >>> bobo.testmodule1.gety = gety
+    >>> bobo.testmodule1.post = post
+    >>> bobo.testmodule1.posty = posty
+
+    >>> app = makeapp(bobo_resources='bobo.testmodule1')
+
+    >>> print app.get('', status=200).body
+    B.get GET
+
+    >>> print app.post('', '', status=200).body
+    C.post POST
+
+    >>> print app.get('', extra_environ=dict(REQUEST_METHOD='HEAD'),
+    ...               status=405)
+    Response: 405 Method Not Allowed
+    Allow: GET, POST
+    Content-Type: text/html; charset=UTF-8
+    <BLANKLINE>
+
+    >>> print app.get('/a', status=200).body
+    B.gety GET a
+
+    >>> print app.post('/a', '', status=200).body
+    C.posty POST a
+
+    >>> print app.delete('/a', status=405)
+    Response: 405 Method Not Allowed
+    Allow: GET, POST
+    Content-Type: text/html; charset=UTF-8
+    <html>
+    <head><title>Method Not Allowed</title></head>
+    <body>Invalid request method: DELETE</body>
+    </html>
+
+
+redirect
+--------
+
+    >>> response = bobo.redirect('http://www.python.org/')
+    >>> response # doctest: +ELLIPSIS
+    <Response at ... 302 Found>
+
+    >>> print response
+    302 Found
+    Location: http://www.python.org/
+    content-type: text/html; charset=UTF-8
+    Content-Length: 26
+    <BLANKLINE>
+    See http://www.python.org/
+
+    >>> response = bobo.redirect('http://www.python.org/', 301)
+    >>> response # doctest: +ELLIPSIS
+    <Response at ... 301 Moved Permanently>
+
+Ordering
+--------
+
+    >>> l1 = bobo.late()
+    >>> l2 = bobo.late()
+    >>> o1 = bobo.order()
+    >>> o2 = bobo.order()
+    >>> e1 = bobo.early()
+    >>> e2 = bobo.early()
+    >>> e1 < e2 < o1 < o2 < l1 < l2
+    True
+
+    >>> bobo.testmodule1.__dict__.clear()
+
+    >>> @bobo.query('/o', order=l1)
+    ... def f1():
+    ...     return 'f1'
+    >>> bobo.testmodule1.f1 = f1
+
+    >>> @bobo.query('/o')
+    ... def f2():
+    ...     return 'f2'
+    >>> bobo.testmodule1.f2 = f2
+
+    >>> app = makeapp(bobo_resources='bobo.testmodule1')
+    >>> app.get('/o').body
+    'f2'
+
+    >>> @bobo.query('/o', order=bobo.early())
+    ... def f3():
+    ...     return 'f3'
+    >>> bobo.testmodule1.f3 = f3
+
+    >>> app = makeapp(bobo_resources='bobo.testmodule1')
+    >>> app.get('/o').body
+    'f3'


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/main.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/make.bat
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/make.bat	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/make.bat	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,112 @@
+ at ECHO OFF
+
+REM Command file for Sphinx documentation
+
+set SPHINXBUILD=sphinx-build
+set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	: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 over 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
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (_build\*) do rmdir /q /s %%i
+	del /q /s _build\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html
+	echo.
+	echo.Build finished. The HTML pages are in _build/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% _build/dirhtml
+	echo.
+	echo.Build finished. The HTML pages are in _build/dirhtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "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.
+	goto end
+)
+
+if "%1" == "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\bobo.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile _build\qthelp\bobo.ghc
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex
+	echo.
+	echo.Build finished; the LaTeX files are in _build/latex.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes
+	echo.
+	echo.The overview file is in _build/changes.
+	goto end
+)
+
+if "%1" == "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.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% _build/doctest
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in _build/doctest/output.txt.
+	goto end
+)
+
+:end


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/make.bat
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/more.txt
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/more.txt	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/more.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,772 @@
+Additional topics
+=================
+
+Check functions
+---------------
+
+When using the ``query``, ``post``, and ``resource`` decorators, you
+can define a check function.  Before calling the decorated function,
+the check function is called.  If the check function returns a
+response, the check function's response is used rather than calling
+the decorated function.  A common use of check functions is for
+authorization::
+
+   import bobo, webob
+
+   data = {'x': 'some text'}
+
+   def authenticated(inst, request, func):
+       if not request.remote_user:
+           return webob.Response(status=401)
+
+   @bobo.post('/:name', check=authenticated)
+   def update(name, body):
+       data[name] = body
+       return 'Updated'
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> import bobo, webtest
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='helloapp'))
+    >>> _ = app.post('/foo', 'body=sometext', status=401)
+    >>> import helloapp
+    >>> helloapp.data
+    {'x': 'some text'}
+
+    >>> app.post('/foo', 'body=sometext',status=200,
+    ...     extra_environ=dict(REMOTE_USER='jim')).body
+    'Updated'
+
+    >>> import pprint
+    >>> pprint.pprint(helloapp.data, width=1)
+    {'foo': u'sometext',
+     'x': 'some text'}
+
+In this example, we use a very simple authorization model.  We can
+update data if the user is authenticated.  Check functions take 3
+positional arguments:
+
+- an instance
+- a request
+- the decorated function (or callable)
+
+If a resource is a method, the first argument passed to the check
+function will be the instance the method is applied to. Otherwise, it
+will be None.
+
+Decorated objects can be used directly
+--------------------------------------
+
+Functions or callables decorated by the ``query``, ``post``,
+``resource`` and ``subroute`` decorators can be called as if they were
+undecorated. For example, with::
+
+    @bobo.query('/:name', check=authenticated)
+    def get(name):
+        return data[name]
+
+.. -> src
+
+    >>> update_module('helloapp', src)
+    >>> get = helloapp.get
+
+We can call the get function directly:
+
+    >>> get('x')
+    'some text'
+
+Similarly, classes decorated with the subroute decorator can be used
+normally. The subroute decorator simply adds a ``bobo_response`` class
+method that allows the class to be used as a :term:`resource`.
+
+.. _configuredroutes:
+
+Configured routes
+-----------------
+
+For simplicity, you normally specify routes in your application code.
+For example, in::
+
+    @bobo.query('/greeters/:myname')
+    def hello(name="world", myname='Bobo'):
+        return "Hello %s! My name is %s." % (name, myname)
+
+You specify 2 things:
+
+1. Which URLs should be handled by the hello function.
+
+2. How to call the function.
+
+In most cases, being able to specify this information one place is
+convenient.
+
+Sometimes, however, you may want to separate routes from your
+implementation to:
+
+- Manage the routes in one place,
+
+- Omit some routes defined in the implementation,
+
+- Change the routes or search order from what's given in the
+  implementation.
+
+Bobo provides a way to explicitly configure the routes as part of
+configuration. When you specify resources, you can control the order
+resources are searched and override the routes used.
+
+The ``bobo_response`` takes a number of resources separated by
+newlines. Resources take one of 4 forms:
+
+modulename
+    Use all of the resources found in the module.
+
+modulename:expression
+    Use the given :term:resource.  The resource is specified using a
+    module name and an expression (typically just a global name)
+    that's executed in the module's global scope.
+
+route -> modulename:expression
+    Use the given object with the given route. The object is specified
+    using a module name and an expression (typically just a global
+    name) that's executed in the module's global scope.
+
+    The object must have a ``bobo_route`` method, as objects created
+    using one of the ``query``, ``post``, ``resource`` or ``subroute``
+    decorators do, or the object must be a class with a constructor
+    that takes a request and route data and returns a resource.
+
+route +> modulename:expression
+    Use a :term:`resource`, but add the given route as a prefix of the
+    resources route. The resource is given by a module name and
+    expression.
+
+    The given route may not have placeholders.
+
+Resources are separated by newlines.  The string ``->``, or ``+>`` at
+the end of a line acts as a line continuation character.
+
+To show how this works, we'll look at an example.  We'll create a
+2 modules with some resources in them. First, people::
+
+    import bobo
+
+    @bobo.subroute('/employee/:id', scan=True)
+    class Employee:
+        def __init__(self, request, id):
+            self.id = id
+
+        @bobo.query('/')
+        def hi(self):
+            return "Hi, I'm employee %s" % self.id
+
+    @bobo.query('/:name')
+    def hi(name):
+        return "Hi, I'm %s" % name
+
+.. -> src
+
+    >>> import sys
+    >>> if sys.version_info < (2, 6):
+    ...     src = src.replace("\n at bobo.subroute", "\n#bobo.subroute")
+    ...     src += ("\nEmployee = bobo.subroute("
+    ...             "'/employee/:id', scan=True)(Employee)")
+
+    >>> update_module('people', src)
+
+Then docs::
+
+    import bobo
+
+    documents = {
+        'bobs': {
+        'hi.html': "Hi. I'm Bob.",
+        'hobbies': {
+          'cooking.html': "I like to cook.",
+          'sports.html': "I like to ski.",
+          },
+        },
+    }
+
+    @bobo.subroute('/docs', scan=True)
+    class Folder:
+
+        def __init__(self, request, data=None):
+            if data is None:
+                data = documents
+            self.data = data
+
+        @bobo.query('')
+        def base(self, bobo_request):
+            return bobo.redirect(request.url+'/')
+
+        @bobo.query('/')
+        def index(self):
+            return '\n'.join('<a href="%s/">%s<a><br>' % (k, k)
+                             for k in self.data)
+
+        @bobo.subroute('/:item_id')
+        def subitem(self, request, item_id):
+            item = self.data[item_id]
+            if isinstance(item, dict):
+               return Folder(request, item)
+            else:
+               return Document(item)
+
+    @bobo.scan_class
+    class Document:
+
+        def __init__(self, text):
+            self.text = text
+
+        @bobo.query('')
+        def get(self):
+            return self.text
+
+.. -> src
+
+    >>> import sys
+    >>> if sys.version_info < (2, 6):
+    ...     src = src.replace("\n at bobo.subroute", "\n#bobo.subroute")
+    ...     src += "\nFolder = bobo.subroute('/docs', scan=True)(Folder)"
+    ...     src = src.replace("\n at bobo.scan_class", "")
+    ...     src += "\nbobo.scan_class(Document)"
+
+
+
+    >>> update_module('docs', src)
+
+We use the ``bobo_resources`` option to control the URLs we access these
+with::
+
+    [app:main]
+    use = egg:bobo
+    bobo_resources =
+        # Same routes
+        people:Employee # 1
+        docs            # 2
+
+        # new routes
+        /long/winded/path/:name/lets/get/on/with/it -> # 3
+           people:hi                                   # 3 also
+        /us/:id -> people:Employee  # 4
+
+        # prefixes
+        /folks +> people # 5
+        /ho +> people:hi # 6
+
+.. -> ini
+
+    >>> import ConfigParser, StringIO
+    >>> parser = ConfigParser.ConfigParser()
+    >>> parser.readfp(StringIO.StringIO(ini))
+    >>> app = webtest.TestApp(bobo.Application(dict(parser.items('app:main'))))
+
+This example shows a number of things:
+
+- We can use blank lines and comments.  Route configurations can get
+  involved, so comments are useful. In the example, comments are used
+  to assign numbers to the individual routes so we can refer to them.
+
+- We have several form of resource:
+
+  1. Use an existing resource with its original route.
+
+     If we use a URL like::
+
+       http://localhost:8080/employee/1/
+
+     .. -> url1 strip
+
+     We'll get output::
+
+       Hi, I'm employee 1
+
+     .. -> expected1 strip
+
+         >>> app.get(url1, status=200).body == expected1
+         True
+
+  2. Use the resources from a module with their original routes.
+
+     If we use a URL like::
+
+       http://localhost:8080/docs/bobs/hi.html
+
+     .. -> url2 strip
+
+     We'll get output::
+
+       Hi. I'm Bob.
+
+     .. -> expected2 strip
+
+         >>> app.get(url2, status=200).body == expected2
+         True
+
+  3. Define a new route for an existing resource.
+
+
+     If we use a URL like::
+
+        http://localhost:8080/long/winded/path/bobo/lets/get/on/with/it
+
+     .. -> url3 strip
+
+     We'll get output::
+
+       Hi, I'm bobo
+
+     .. -> expected3 strip
+
+         >>> app.get(url3, status=200).body == expected3
+         True
+
+  4. Define a new route for an existing subroute.
+
+     If we use a URL like::
+
+        http://localhost:8080/us/1/
+
+     .. -> url4 strip
+
+     We'll get output::
+
+       Hi, I'm employee 1
+
+     .. -> expected4 strip
+
+         >>> app.get(url4, status=200).body == expected4
+         True
+
+  5. Use all of the routes from a module with a prefix added.
+
+     If we use a URL like::
+
+        http://localhost:8080/folks/employee/1/
+
+     .. -> url5 strip
+
+     We'll get output::
+
+       Hi, I'm employee 1
+
+     .. -> expected5 strip
+
+         >>> app.get(url5, status=200).body == expected5
+         True
+
+  6. Use an existing route adding a prefix.
+
+     If we use a URL like::
+
+        http://localhost:8080/ho/silly
+
+     .. -> url6 strip
+
+     We'll get output::
+
+       Hi, I'm silly
+
+     .. -> expected6 strip
+
+         >> app.get(url6, status=200).body == expected6
+         True
+
+Configuring routes in python
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To configure routes in Python, you can use the ``bobo.resources``
+function::
+
+    import bobo
+
+    myroutes = bobo.resources((
+        # Same routes
+        'people:Employee', # 1
+        'docs',            # 2
+
+        # new routes
+        bobo.reroute(
+          '/long/winded/path/:name/lets/get/on/with/it', # 3
+          'people:hi'),                                  # 3 also
+        bobo.reroute('/us/:id', 'people:Employee'),  # 4
+
+        # prefixes
+        bobo.preroute('/folks', 'people'), # 5
+        bobo.preroute('/ho', 'people:hi'), # 6
+    ))
+
+.. -> src
+
+    >>> update_module('routemod', src)
+    >>> update_module('routemod', """
+    ... @bobo.query
+    ... def xxx():
+    ...     return 'xxx'
+    ... """)
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='routemod'))
+
+    >>> app.get(url1, status=200).body == expected1
+    True
+    >>> app.get(url2, status=200).body == expected2
+    True
+    >>> app.get(url3, status=200).body == expected3
+    True
+    >>> app.get(url4, status=200).body == expected4
+    True
+    >>> app.get(url5, status=200).body == expected5
+    True
+    >>> app.get(url6, status=200).body == expected6
+    True
+    >>> app.get('http://localhost:8080/xxx.html', status=200).body == 'xxx'
+    True
+
+The ``resources`` function takes an iterable of resources, where the
+resources can be resource objects, or strings naming resource objects
+or modules.
+
+The ``reroute`` function takes a route and an existing resource and
+returns a new resource with the given route.  The resource must have a
+``bobo_route`` method, as resources created using one of the
+``query``, ``post``, ``resource`` or ``subroute`` decorators do, or
+the resource must be a class with a constructor that takes a request
+and route data and returns a resource.
+
+The ``preroute`` function takes a route and a resource and returns a
+new resource that uses the given route as a subroute to get to the
+resource.
+
+The example above is almost equivalent to the earlier
+example.  If the module containing the code above is given to the
+bobo_resources option, then the resources defined by the call will be
+used.  It is slightly different from the earlier example, because if
+the module defines any other resources, they'll be used as well.
+
+Resource modules
+~~~~~~~~~~~~~~~~
+
+Rather than defining a resource in a module, we can make a module a
+resource by defining a ``bobo_response`` module attribute::
+
+    import bobo, docs, people
+
+    bobo_response = bobo.resources((
+        # Same routes
+        people.Employee, # 1
+        docs,            # 2
+
+        # new routes
+        bobo.reroute(
+          '/long/winded/path/:name/lets/get/on/with/it', # 3
+          people.hi),                                    # 3 also
+        bobo.reroute('/us/:id', people.Employee),  # 4
+
+        # prefixes
+        bobo.preroute('/folks', people), # 5
+        bobo.preroute('/ho', people.hi), # 6
+
+    )).bobo_response
+
+.. -> src
+
+    >>> update_module('routemod2', src)
+    >>> update_module('routemod2', """
+    ... @bobo.query
+    ... def xxx():
+    ...     return 'xxx'
+    ... """)
+
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='routemod2'))
+
+    >>> app.get(url1, status=200).body == expected1
+    True
+    >>> app.get(url2, status=200).body == expected2
+    True
+    >>> app.get(url3, status=200).body == expected3
+    True
+    >>> app.get(url4, status=200).body == expected4
+    True
+    >>> app.get(url5, status=200).body == expected5
+    True
+    >>> app.get(url6, status=200).body == expected6
+    True
+    >>> _ = app.get('http://localhost:8080/xxx', status=404)
+
+Here, rather than adding a new resource to the module, we've copied the
+``bobo_response`` method from a new resource to the module, making
+the module a resource.  When bobo scans a module, it first checks
+whether the module has a ``bobo_response`` attribute. If it does,
+then bobo uses the module as a resource and doesn't scan the module for
+resources.  This way, we control precisely which resources will be used,
+given the module.
+
+This example also illustrates that, rather than passing strings to the
+``resources``, ``reroute`` and ``preroute`` functions, we can pass
+objects directly.
+
+Error response generation
+-------------------------
+
+There are three cases for which bobo has to generate error responses:
+
+1. When a resource can't be found, bobo generates a "404 Not Found"
+   response.
+2. When a resource can be found but it doesn't allow the request
+   method, bobo generates a "405 Method Not Allowed" response.
+3. When a ``query`` or ``post`` decorated function requires a
+   parameter and the parameter is isn't in the given form data, bobo
+   generates a "405 Forbidden" response with a body that indicates the
+   missing parameter.
+
+For each of these responses, bobo generates a small HTML body.
+
+Applications can take over generating error responses by specifying a
+``bobo_errors`` option that specified an object or a module defining 3
+callable attributes:
+
+not_found(request, method)
+   Generate a response when a resource can't be found.
+
+   This should return a 404 response.
+
+method_not_allowed(request, method, methods)
+   Generate a response when the resource found doesn't allow the
+   request method.
+
+   This should return a 405 response and set the ``Allowed`` response
+   header to the list of allowed headers.
+
+missing_form_variable(request, method, name)
+   Generate a response when a form variable is missing.
+
+   The proper response in this situation isn't obvious.
+
+The value given for the ``bobo_errors`` option is either a module
+name, or an object name of the form: "module_name:expression".
+
+Let's look at an example. First, an ``errorsample`` module::
+
+    import bobo, webob
+
+    @bobo.query(method='GET')
+    def hi(who):
+        return 'Hi %s' % who
+
+    def not_found(request, method):
+        return webob.Response("not found", status=404)
+
+    def method_not_allowed(request, method, methods):
+        return webob.Response(
+            "bad method "+method, status=405,
+            headerlist=[
+                ('Allow', ', '.join(methods)),
+                ('Content-Type', 'text/plain'),
+                ])
+
+    def missing_form_variable(request, method, name):
+        return webob.Response("Missing "+name)
+
+
+.. -> src
+
+    >>> update_module('errorsample', src)
+
+Then a configuration file::
+
+    [app:main]
+    use = egg:bobo
+    bobo_resources = errorsample
+    bobo_errors = errorsample
+
+.. -> ini
+
+    >>> parser = ConfigParser.ConfigParser()
+    >>> parser.readfp(StringIO.StringIO(ini))
+    >>> app = webtest.TestApp(bobo.Application(dict(parser.items('app:main'))))
+
+If we use the URL::
+
+   http://localhost:8080/hi.html?who=you
+
+.. -> url1 strip
+
+We'll get the response::
+
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    Hi you
+
+.. -> expected1 strip
+
+    >>> str(app.get(url1)).strip() == expected1
+    True
+
+But if we use::
+
+   http://localhost:8080/ho
+
+.. -> url2 strip
+
+We'll get::
+
+    Response: 404 Not Found
+    Content-Type: text/html; charset=UTF-8
+    not found
+
+.. -> expected2 strip
+
+    >>> str(app.get(url2, status=404)).strip() == expected2
+    True
+
+If we use::
+
+   http://localhost:8080/hi.html
+
+.. -> url3 strip
+
+We'll get::
+
+    Response: 200 OK
+    Content-Type: text/html; charset=UTF-8
+    Missing who
+
+.. -> expected3 strip
+
+    >>> str(app.get(url3)).strip() == expected3
+    True
+
+If we make a POST to the same URL, we'll get::
+
+    Response: 405 Method Not Allowed
+    Allow: GET
+    Content-Type: text/plain
+    bad method POST
+
+.. -> expected4 strip
+
+    >>> str(app.post(url3, 'who=me', status=405)).strip() == expected4
+    True
+
+
+We can use an object with methods rather than module-level functions
+to generate error responses. Here we define an ``errorsample2`` module
+that defines an class with methods for generating error responses::
+
+   import bobo, webob
+
+   class Errors:
+
+       def not_found(self, request, method):
+           return webob.Response("not found", status=404)
+
+       def method_not_allowed(self, request, method, methods):
+           return webob.Response(
+               "bad method "+method, status=405,
+               headerlist=[
+                   ('Allow', ', '.join(methods)),
+                   ('Content-Type', 'text/plain'),
+                   ])
+
+       def missing_form_variable(self, request, method, name):
+           return webob.Response("Missing "+name)
+
+.. -> src
+
+   >>> update_module('errorsample2', src)
+
+In the configuration file, we specify an object, rather than a module::
+
+    [app:main]
+    use = egg:bobo
+    bobo_resources = errorsample
+    bobo_errors = errorsample2:Errors()
+
+.. -> ini2
+
+Note that in this example, rather than just using a global name, we
+use an expression to specify the errors object.
+
+.. check
+
+    >>> parser = ConfigParser.ConfigParser()
+    >>> parser.readfp(StringIO.StringIO(ini2))
+    >>> app = webtest.TestApp(bobo.Application(dict(parser.items('app:main'))))
+
+    >>> str(app.get(url1)).strip() == expected1
+    True
+    >>> str(app.get(url2, status=404)).strip() == expected2
+    True
+    >>> str(app.get(url3)).strip() == expected3
+    True
+    >>> str(app.post(url3, 'who=me', status=405)).strip() == expected4
+    True
+
+Uncaught exceptions
+~~~~~~~~~~~~~~~~~~~
+
+Normally, bobo let's uncaught exceptions propagate to calling
+middleware or servers.  If you want to provide custom handling of
+uncaught exceptions, you can include an ``exceptions`` method in
+object you give to ``bobo_errors``.
+
+::
+
+   import bobo, webob
+
+   class Errors:
+
+       def not_found(self, request, method):
+           return webob.Response("not found", status=404)
+
+       def method_not_allowed(self, request, method, methods):
+           return webob.Response(
+               "bad method "+method, status=405,
+               headerlist=[
+                   ('Allow', ', '.join(methods)),
+                   ('Content-Type', 'text/plain'),
+                   ])
+
+       def missing_form_variable(self, request, method, name):
+           return webob.Response("Missing "+name)
+
+       def exception(self, request, method, exc_info):
+           return webob.Response("Dang! %s" % exc_info[1], status=500)
+
+.. -> src
+
+    >>> update_module('errorsample2', src)
+
+    >>> update_module('badapp',
+    ...   'import bobo\n\n at bobo.resource\ndef bad(x, y):\n  pass\n')
+    >>> app = webtest.TestApp(bobo.Application(bobo_resources='badapp'))
+    >>> app.get('/bad.html')
+    Traceback (most recent call last):
+    ...
+    TypeError: bad() takes exactly 2 arguments (1 given)
+
+    >>> app = webtest.TestApp(bobo.Application(
+    ...    bobo_resources='badapp', bobo_errors='errorsample2:Errors()'))
+    >>> app.get('/bad.html', status=500).body
+    'Dang! bad() takes exactly 2 arguments (1 given)'
+
+.. _order:
+
+Ordering Resources
+------------------
+
+When looking for resources (or sub-resources) that match a request,
+resources are tried in order, where the default order is the order of
+definition. The order can be overridden by passing an order using the
+``order`` keyword argument to the bobo decorators [#customorder]_.
+The results of calling the functions ``bobo.early()`` and
+``bobo.late()`` are typically the only values that are useful to pass.
+It is usually a good idea to use ``bobo.late()`` for subroutes that
+match any path, so that more specific routes are tried earlier.  If
+multiple resources that use ``bobo.late()`` (or ``bobo.early()``)
+match a path, the first one defined will be used.
+
+.. [#customorder] Advanced applications may provide their own
+   :term:`resource` implementations.  Custom resource implementations
+   must implement the resource interface and will provide an order
+   using the ``bobo_order`` attribute.  See :ref:`resourceinterface`.


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/more.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/reference.txt
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/reference.txt	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/reference.txt	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,259 @@
+Reference
+=========
+
+:mod:`bobo` module documentation
+----------------------------------------------
+
+.. automodule:: bobo
+   :members:
+
+:mod:`boboserver` module documentation
+----------------------------------------------
+
+.. automodule:: boboserver
+   :members:
+
+.. _boboserver:
+
+The bobo server
+---------------
+
+.. program:: bobo
+
+The bobo server is a script that runs a development web server with a
+given source file or modules, and configuration options. The usage is:
+
+  bobo [options] [name=value ...]
+
+Command-line arguments are either options, or
+configuration options of the form optionname=value.
+
+Options:
+
+-f SOURCE, --file SOURCE
+   Specify a source file name to be published. It'll be converted to a module
+   named ``bobo__main__`` and will have its __file__ set to the original
+   file name.
+
+-r RESOURCE, --resource RESOURCE
+   Specify a resource, such as a module or global, to publish.
+
+-D, --debug
+   Provide post-mortem debugging.  If an uncaught exception is raised,
+   use ``pdb.post_mortem`` to debug it.
+
+-p PORT, --port PORT
+   Specify the port to listen on.
+
+-c GLOBALNAME, --configure=GLOBALNAME
+   Specify the name of a global to call with configuration data.  This is
+   shorthand for ``bobo_configure=globalname``.  This is normally a
+   name of the form ``modulename:expression``, however, if you supply
+   just an expression, the module of the first resource will be used.
+   For example, with a command like::
+
+     bobo -f my.py -c config
+
+   The ``config`` function in ``my.py`` will be used.
+
+-s ROUTE=PATH, --static ROUTE=PATH
+   Publish static files in the directory given by PATH at the route
+   given by ROUTE.
+
+   While there are middleware components that are better at publishing
+   static resources for production use, this option makes it easier to
+   get started during development.
+
+After the options, you can give configuration options as name=value
+pairs. These will be passed as configuration options to bobo and to
+any configuration handler you specify.
+
+Example::
+
+  bobo -f fswiki.py -c config directory=docs
+
+In this example, we run the application in the source file fswiki.py.
+We pass configuration data to the application's ``config`` function.
+The options include the setting of ``'doc'`` for the directory option.
+
+.. _routedetails:
+
+Advanced: resource interfaces
+-----------------------------
+
+Most applications will use the bobo-provided decorators to implement
+resources. These decorators create objects that provide certain
+methods and attributes.  Applications can implement these methods and
+attributes themselves to provide specialized behaviors.  For example,
+an application can implement bobo_response to provide a specialized
+object-look-up mechanism that doesn't use routes.
+
+The most important method is ``bobo_response``.  When bobo scans a
+module or class for resources, it looks for objects with this method.
+When handling a request, it calls this method on each of the objects
+found until a value is returned. See :ref:`resourceinterface` for more
+details.
+
+The optional methods, ``bobo_methods``, ``bobo_order`` and
+``bobo_response`` are used when scanning a module or class. Resources
+found in a module or class are ordered within the module or class
+based on values of their ``bobo_order`` attribute.  (If a resource
+doesn't have a ``bobo_order`` attribute, a value is used that is
+between those returned by ``bobo.order()`` and ``bobo.late()``.
+
+The ``bobo_route`` attribute is used to group resources within a
+module or class that have the same route.  Resources with the same
+route are treated as a single resource.  The route is matched and then
+a the first resource that accepts the request method is used.
+
+The optional :meth:`bobo_reroute` method is used by the bobo
+:func:`bobo.reroute` function to compute a new resource from an
+existing resource and a new route.
+
+.. _resourceinterface:
+
+IResource
+~~~~~~~~~
+
+.. class:: IResource
+
+   IResource is documented here to define an API that can be provided by
+   application-defined resources.  Bobo doesn't actually define an
+   IResource object.
+
+   .. method:: bobo_response(request, path, method)
+
+      Find an object to publish at the given path.
+
+      If an object is found, call it and return the result.
+
+      If no object can be found, return None.
+
+      If a resource matches a path but doesn't accept the request
+      method, a 405, method not allowed, response should be returned.
+
+      If the return value isn't a response, it should be converted to
+      a response.
+
+   .. attribute:: bobo_methods
+
+      This optional attribute specifies the HTTP request methods
+      supported by objects found by the resource.  See :ref:`routedetails`.
+      If present, it muse be a sequence of method names, or None.  If
+      it is None, then all methods are accepted.
+
+   .. attribute:: bobo_order
+
+      This optional attribute defines the precedence order for a
+      resource.  See :ref:`routedetails`.  If present, it must be an
+      integer. Resources with lower values for ``bobo_order`` are used
+      before resources with higher values.  If the attribute isn't
+      present, a very high value is assumed.
+
+      Typically, :func:`order` is called to get a value for bobo_order
+      when a resource is defined.
+
+   .. attribute:: bobo_route
+
+      This optional attribute defines the *complete* route for a resource.  See
+      :ref:`routedetails`.  If present, it must be an string.
+
+   .. method:: bobo_reroute(route)
+
+      Return a new resource for the given route.
+
+Advanced: subclassing bobo.Application
+--------------------------------------
+
+The bobo WSGI application, :class:`bobo.Application` can be subclassed
+to handle alternate request implementations. This is to allow
+applications written for frameworks using request implementations other
+than Webob to be used with bobo.  A subclass should override the
+:meth:`__call__` and :meth:`build_response` methods.
+
+The :meth:`__call__` method should:
+
+- Create a request.
+- Call ``self.bobo_response(request, path, method)`` to get a
+  response.
+- Return the result of calling the response with the ``environ`` and
+  ``start_response`` arguments passed to :meth:`__call__`.
+
+The :meth:`__call__` should look like::
+
+   def __call__(self, environ, start_response):
+        """Handle a WSGI application request.
+        """
+        request = ...
+
+        return self.bobo_response(request, request.path_info, request.method
+                                  )(environ, start_response)
+
+The request should implement as much of the `WebOb request API
+<http://pythonpaste.org/webob/reference.html#id1>`_ as practical. It
+must implement the attributes used by bobo: ``path_info``, ``method``,
+``params``, and ``POST``.
+
+The :meth:`build_response` method is used to build a response when an
+application function returns a value that isn't a response.  See the
+:class:`bobo.Application` for more information on this method.
+
+New application implementations will also want to provide matching
+development servers.  The :func:`boboserver.server` entry point accepts an
+alternate application object, making implementation of alternate
+development servers trivial.
+
+Glossary
+========
+
+.. glossary::
+
+   :sorted:
+
+   order
+       The order in which a resource is searched relative to other
+       resources.
+
+   response
+       An object that represents a web response.  This is usually a
+       `Webob response <http://pythonpaste.org/webob/#response>`_, but
+       it may be any callable object that implements the `WSGI
+       application interface
+       <http://www.python.org/dev/peps/pep-0333/#the-application-framework-side>`_.
+
+       Applications will typically return strings that are converted
+       to responses by bobo, or will construct and return Webob
+       response objects.
+
+   request
+       An object that contains information about a web request.  This
+       is a `Webob request object
+       <http://pythonpaste.org/webob/#request>`_.
+       See the Webob documentation to get details of its interface.
+
+   route
+       A URL pattern expressed as a path with placeholders, as in::
+
+          /greeters/:name/:page?.html
+
+       Routes are inspired by the `Ruby on Rails Routing
+       <http://api.rubyonrails.org/classes/ActionController/Routing.html>`_
+       system.
+
+       Placeholders are Python identifiers preceded by ":/".  If a
+       placeholder is followed by a question mark, it is optional.  A
+       placeholder may be followed by an extension.  When a route
+       matches a URL, the URL text corresponding to the placeholders
+       is passed to the application as keyword parameters.
+
+   route data
+       Values for placeholders resulting from matching a URL against a
+       route.  For example, matching the URL: ``http://localhost/a/b``
+       against the route ``/:x/:y/:z?`` results in the route data
+       ``{'x': 'a', 'y': 'b'}``.
+
+   resource
+       An object that has a bobo_response method. See :ref:`routedetails`.
+
+   routes
+       See route.


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/reference.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,57 @@
+import bobo, mimetypes, os, webob
+
+class Directory:
+
+    def __init__(self, root, path=None):
+        self.root = os.path.abspath(root)+os.path.sep
+        self.path = path or root
+
+    @bobo.query('')
+    def base(self, bobo_request):
+        return bobo.redirect(bobo_request.url+'/')
+
+    @bobo.query('/')
+    def index(self):
+        links = []
+        for name in os.listdir(self.path):
+            if os.path.isdir(os.path.join(self.path, name)):
+                name += '/'
+            links.append('<a href="%s">%s</a>' % (name, name))
+        return """<html>
+        <head><title>%s</title></head>
+        <body>
+          %s
+        </body>
+        </html>
+        """ % (self.path[len(self.root):], '<br>\n  '.join(links))
+
+    @bobo.subroute('/:name')
+    def traverse(self, request, name):
+        path = os.path.abspath(os.path.join(self.path, name))
+        if not path.startswith(self.root):
+            raise bobo.NotFound
+        if os.path.isdir(path):
+            return Directory(self.root, path)
+        else:
+            return File(path)
+
+bobo.scan_class(Directory)
+
+class File:
+    def __init__(self, path):
+        self.path = path
+
+    @bobo.query('')
+    def base(self, bobo_request):
+        response = webob.Response()
+        content_type = mimetypes.guess_type(self.path)[0]
+        if content_type is not None:
+            response.content_type = content_type
+        try:
+            response.body = open(self.path).read()
+        except IOError:
+            raise bobo.NotFound
+
+        return response
+
+bobo.scan_class(File)


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.test
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.test	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.test	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,66 @@
+Static resource
+---------------
+
+    >>> import bobo, os, webtest
+    >>> os.mkdir('docs')
+    >>> os.mkdir(os.path.join('docs', 'subdir'))
+    >>> open(os.path.join('docs', 'doc1.txt'), 'w').write('doc1 text')
+    >>> open(os.path.join('docs', 'subdir', 'doc2.html'), 'w').write(
+    ...    'doc2 text')
+
+    >>> app = webtest.TestApp(bobo.Application(
+    ...   bobo_resources=
+    ...     '/resources +> bobodoctestumentation.static:Directory("docs")',
+    ...   ))
+
+    >>> app.get('/resources') # doctest: +NORMALIZE_WHITESPACE
+    <302 Found text/html
+       location: http://localhost/resources/ body='See http:...ces/'/31>
+
+    >>> print app.get('/resources/', status=200).body
+    <html>
+            <head><title></title></head>
+            <body>
+              <a href="doc1.txt">doc1.txt</a><br>
+      <a href="subdir/">subdir/</a>
+            </body>
+            </html>
+    <BLANKLINE>
+
+    >>> app.get('/resources/subdir') # doctest: +NORMALIZE_WHITESPACE
+    <302 Found text/html
+      location: http://localhost/resources/subdir/ body='See http:...dir/'/38>
+
+    >>> print app.get('/resources/subdir/', status=200).body
+    <html>
+            <head><title>subdir</title></head>
+            <body>
+              <a href="doc2.html">doc2.html</a>
+            </body>
+            </html>
+    <BLANKLINE>
+
+    >>> app.get('/resources/doc1.txt')
+    <200 OK text/plain body='doc1 text'>
+
+    >>> app.get('/resources/subdir/doc2.html')
+    <200 OK text/html body='doc2 text'>
+
+    >>> print app.get('/resources/doc2.html', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources/doc2.html</body>
+    </html>
+
+    >>> print app.get('/resources//etc/passwd', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources//etc/passwd</body>
+    </html>
+
+    >>> print app.get('/resources/../../', status=404).body,
+    <html>
+    <head><title>Not Found</title></head>
+    <body>Could not find: /resources/../../</body>
+    </html>
+


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/static.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/tests.py
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/tests.py	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/tests.py	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,116 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+
+from zope.testing import doctest, setupstack, renormalizing
+import bobo
+import manuel
+import manuel.doctest
+import manuel.testing
+import pprint
+import re
+import sys
+import textwrap
+import types
+import unittest
+import webob
+
+def assignment_manuel():
+    assignment_re = re.compile(
+        r"[^\n]*::(?P<value>(\n| [^\n]*\n)+?)"
+        " *\.\. -> (?P<name>\w+)(?P<strip> +strip)? *\n")
+
+    m = manuel.Manuel()
+
+    @m.parser
+    def parse(document):
+        for region in document.find_regions(assignment_re):
+            data = region.start_match.groupdict()
+            data['value'] = textwrap.dedent(data['value'].expandtabs())
+            if data.get('strip'):
+                data['value'] = data['value'].strip()
+            source = "%(name)s = %(value)r\n" % data
+            example = doctest.Example(source, '', lineno=region.lineno-1)
+            document.replace_region(region, example)
+
+    m2 = manuel.doctest.Manuel()
+    m2.extend(m)
+
+    return m2
+
+def setUp(test):
+    setupstack.setUpDirectory(test)
+
+    for i in ('1', '2'):
+        name = 'testmodule'+i
+        module = types.ModuleType('bobo.'+name)
+        setattr(bobo, name, module)
+        sys.modules[module.__name__] = module
+        setupstack.register(test, delattr, bobo, name)
+        setupstack.register(test, sys.modules.__delitem__, module.__name__)
+
+def setup_intro(test):
+    setupstack.setUpDirectory(test)
+
+    def update_module(name, src):
+        if name not in sys.modules:
+            sys.modules[name] = types.ModuleType(name)
+            setupstack.register(test, sys.modules.__delitem__, name)
+        module = sys.modules[name]
+        exec src in module.__dict__
+
+    test.globs['update_module'] = update_module
+
+
+# XXX This should move to zope.testing
+import random, socket
+def get_port():
+    """Return a port that is not in use.
+
+    Checks if a port is in use by trying to connect to it.  Assumes it
+    is not in use if connect raises an exception.
+
+    Raises RuntimeError after 10 tries.
+    """
+    for i in range(10):
+        port = random.randrange(20000, 30000)
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            try:
+                s.connect(('localhost', port))
+            except socket.error:
+                # Perhaps we should check value of error too.
+                return port
+        finally:
+            s.close()
+    raise RuntimeError("Can't find port")
+
+def test_suite():
+    return unittest.TestSuite((
+        manuel.testing.TestSuite(
+            assignment_manuel(),
+            'index.txt', 'more.txt',
+            setUp=setup_intro),
+        doctest.DocFileSuite(
+            'main.test', 'decorator.test',
+            'fswiki.test', 'fswikia.test', 'bobocalc.test', 'static.test',
+            setUp=setUp, tearDown=setupstack.tearDown),
+        doctest.DocFileSuite(
+            'boboserver.test',
+            setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown,
+            checker=renormalizing.RENormalizing([
+                (re.compile('usage:'), 'Usage:'),
+                (re.compile('options:'), 'Options:'),
+                ])
+            ),
+        ))


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/tests.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/who.ini
===================================================================
--- bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/who.ini	                        (rev 0)
+++ bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/who.ini	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,31 @@
+[plugin:form]
+use = repoze.who.plugins.form:make_plugin
+login_form_qs = __do_login
+rememberer_name = auth_tkt
+
+[plugin:auth_tkt]
+use = repoze.who.plugins.auth_tkt:make_plugin
+secret = s33kr1t
+cookie_name = wiki
+secure = False
+include_ip = False
+
+[plugin:htpasswd]
+use = repoze.who.plugins.htpasswd:make_plugin
+filename = htpasswd
+check_fn = repoze.who.plugins.htpasswd:crypt_check
+
+[general]
+request_classifier = repoze.who.classifiers:default_request_classifier
+challenge_decider = repoze.who.classifiers:default_challenge_decider
+remote_user_key = REMOTE_USER
+
+[identifiers]
+plugins = form;browser auth_tkt
+
+[authenticators]
+plugins = htpasswd
+
+[challengers]
+plugins = form;browser
+


Property changes on: bobo/trunk/bobodoctestumentation/src/bobodoctestumentation/who.ini
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/buildout.cfg
===================================================================
--- bobo/trunk/buildout.cfg	                        (rev 0)
+++ bobo/trunk/buildout.cfg	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1,57 @@
+[buildout]
+develop = bobo bobodoctestumentation
+parts = test sphinx bobo paste
+versions = versions
+
+[versions]
+manuel = 1.0.0a2
+
+[bobo]
+recipe = zc.recipe.egg
+eggs = bobo
+       webtest
+interpreter = py
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = bobodoctestumentation
+       webtest
+       bobo
+
+[test2.4]
+recipe = zc.recipe.testrunner
+eggs = bobodoctestumentation
+       webtest
+       PasteDeploy
+       Paste
+       bobo
+
+python = python2.4
+
+[test2.5]
+recipe = zc.recipe.testrunner
+eggs = bobodoctestumentation
+       webtest
+       bobo
+python = python2.5
+
+[test2.6]
+recipe = zc.recipe.testrunner
+eggs = bobodoctestumentation
+       webtest
+       bobo
+python = python2.6
+
+[sphinx]
+recipe = zc.recipe.egg
+eggs = sphinx
+       Pygments
+       manuel
+       bobo
+python = python2.6
+
+[paste]
+recipe = zc.recipe.egg
+eggs = PasteScript
+       repoze.who
+       bobodoctestumentation


Property changes on: bobo/trunk/buildout.cfg
___________________________________________________________________
Added: svn:eol-style
   + native

Added: bobo/trunk/doc
===================================================================
--- bobo/trunk/doc	                        (rev 0)
+++ bobo/trunk/doc	2009-05-26 11:06:35 UTC (rev 100392)
@@ -0,0 +1 @@
+link ./bobodoctestumentation/src/bobodoctestumentation
\ No newline at end of file


Property changes on: bobo/trunk/doc
___________________________________________________________________
Added: svn:special
   + *



More information about the Checkins mailing list