[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