[Checkins] SVN: zc.publication/branches/dev/ checkpoint

Jim Fulton jim at zope.com
Tue Jun 16 07:17:55 EDT 2009


Log message for revision 101074:
  checkpoint
  

Changed:
  U   zc.publication/branches/dev/buildout.cfg
  U   zc.publication/branches/dev/setup.py
  U   zc.publication/branches/dev/src/zc/publication/README.txt
  U   zc.publication/branches/dev/src/zc/publication/__init__.py
  U   zc.publication/branches/dev/src/zc/publication/configure.zcml
  A   zc.publication/branches/dev/src/zc/publication/tests.py

-=-
Modified: zc.publication/branches/dev/buildout.cfg
===================================================================
--- zc.publication/branches/dev/buildout.cfg	2009-06-16 10:54:55 UTC (rev 101073)
+++ zc.publication/branches/dev/buildout.cfg	2009-06-16 11:17:55 UTC (rev 101074)
@@ -4,11 +4,14 @@
 
 [test]
 recipe = zc.recipe.testrunner
-eggs = zc.publication
+eggs = zc.publication [test]
 
 [py]
 recipe = zc.recipe.egg
 eggs = ${test:eggs}
+       PasteScript
+       hello-example
+
 interpreter = py
 
 [hello]

Modified: zc.publication/branches/dev/setup.py
===================================================================
--- zc.publication/branches/dev/setup.py	2009-06-16 10:54:55 UTC (rev 101073)
+++ zc.publication/branches/dev/setup.py	2009-06-16 11:17:55 UTC (rev 101074)
@@ -14,20 +14,31 @@
 name, version = 'zc.publication', '0'
 
 install_requires = [
+    'ZConfig',
     'setuptools',
+    'zope.app.component',
+    'zope.app.publication',
+    'zope.app.publisher',
     'zope.authentication',
+    'zope.component [zcml]',
+    'zope.configuration',
+    'zope.error',
+    'zope.interface',
+    'zope.location',
     'zope.principalregistry',
     'zope.publisher',
-    'zope.app.component',
-    'zope.app.publication',
-    'zope.error',
-    'ZConfig',
+    'zope.security',
     ]
-extras_require = dict(test=['zope.testing'])
+extras_require = dict(
+    test=['zope.testing', 'webtest', 'manuel ==1.0.0a2', 'PasteDeploy'],
+    )
 
 entry_points = """
 [zope.publisher.publication_factory]
 default = zc.publication:Publication
+
+[paste.app_factory]
+main = zc.publication:Application
 """
 
 from setuptools import setup

Modified: zc.publication/branches/dev/src/zc/publication/README.txt
===================================================================
--- zc.publication/branches/dev/src/zc/publication/README.txt	2009-06-16 10:54:55 UTC (rev 101073)
+++ zc.publication/branches/dev/src/zc/publication/README.txt	2009-06-16 11:17:55 UTC (rev 101074)
@@ -1,56 +1,376 @@
-Introduction
-============
+Overview
+========
 
-zc.publication.publcation is a publication object that extends
-``zope.app.publication.browser.BrowserPublication`` with the ability
-to provide an alternative root object, rather than one read from a
-ZODB database.
+The ``zc.publication`` package eases development of WSGI applications
+using the Zope Toolkit publishing frameworks.  Leveraging the Zope
+Toolkit publication frameworks, ``zc.publication`` provides:
 
-Let's look at an example::
+- object traversal from a root object,
+- mapping of requests to transactions,
+- a *optional* security protection system [#protection]_
+  (using sercurity proxies),
+- *optional* authentication during traversal to allow multiple
+   authentication domains,
+- events to notify applications of key pooints in the request lidecycle,
+- error handling with
+  - application-provided error views,
+  - application-pluggable error logging,
+  - request retry to handle conflicting transactions,
 
+With ``zc.publication``, these features can largely be ignored until
+they are needed.
 
-   import zope.publisher.browser
+Getting Started: Hello World
+============================
 
-   class Hello(zope.publisher.browser.BrowserPage):
+Let's start with among the simplest applications you can create, once
+that provides a single web resource.  We'll create a web resource that
+simply outputs the text, "Hello World!", using the module ``hello.py``::
 
-       def __init__(self, request):
-           self.request = request
+    def hello():
+        return """<html><body>
+        Hello World!
+        </body></html>
+        """
 
-       def __call__(self):
-           return """<html><body>
-           Hello world
-           </body></html>
-           """
+.. -> src
 
-We'll put the code above in a module, ``hello``.
+    >>> import sys
+    >>> sys.path.insert(0, '.')
+    >>> open('hello.py', 'w').write(src)
 
-We also create a paste configuration file::
+To turn this into a web application, we'll use ``zc.publication``.
+``zc.publication`` builds on ``zope.publisher`` and Paste Deployment.
 
+Paste Deployment is used to assemble a web server from WSGI
+components, including, at a minimum, a application component and a
+server.  See the Paste Deployment documentation for more information.
+
+We create a Paste Deployment configuration file, ``hello.ini``::
+
     [app:main]
-    use = egg:zope.publisher
-    publication = egg:zc.publication
-    root = hello:Hello
-    zcml = hello.zcml
+    use = egg:zc.publication
+    root = hello:lambda request: hello
+    loggers =
+      <logger>
+         level INFO
+         <logfile>
+           path STDOUT
+         </logfile>
+      </logger>
 
-Here, we:
+    [server:main]
+    use = egg:Paste#http
+    port = 8080
 
-- Use the ``zope.publisher`` WSGI application designed to work with paste.
+.. -> src
 
-- Use the ``zc.publication`` publication plugin for zope.publisher
+    >>> open('hello.ini', 'w').write(src)
+    >>> app = testapp('hello.ini')
 
-- We specify a callable that takes a request and returns an object. In
-  this case, we specify the Hello class.
 
-- We specify the name of a ZCML file to be read to establish
-  application configuration settings.
+The configuration file defines a web server using an application
+component and a server.  The ``server:main`` section defines the web
+server to use.  Here we use an HTTP server provides by the Paste egg,
+running on port 8080.
 
-With these settings, we can access the URL::
+The ``app.main`` section defines the application.  The use option
+specifies a Paste Deployment application factory. Here we use the
+factory provided by zc.publication.  The ``zc.publication``
+application factory accepts a number of options:
 
-   http://localhost/
+root
+   A module and expression, of the form ``module:expression`` that
+   specifies a factory that takes a request and returns the root object for
+   traversal
 
-and get the response::
+zcml
+   Zope Configuration Markup (ZCML) source.
 
+   If this option isn't specified, the configuration provided by
+   ``zc.publication`` is used.
+
+proxy
+   A module and expression, of the form ``module:expression`` a
+   security proxy function
+
+   If this option is ommitted, then no proxies are used.  To use the
+   standard Zope Toolkit security proxies, use
+   ``zope.security.checker.ProxyFactory``.
+
+In ``hello.ini``, we only used the root option.  We specified a
+root-object factory using a lambda that just returned the hello function.
+
+With this example, we can visit the URL::
+
+   http://localhost:8080/
+
+.. -> url strip
+
+and get::
+
    <html><body>
-           Hello world
-           </body></html>
+       Hello World!
+       </body></html>
 
+.. -> expected strip
+
+    >>> app.get(url, status=200).body.strip() == expected
+    True
+
+Views
+=====
+
+Another way to make resources available is using views.  Views display
+content objects.  Lets rewrite our hello resource as a view::
+
+    class Hello(object):
+
+        def __init__(self, context, request):
+            pass
+
+        def __call__(self):
+            return """<html><body>
+            Hello World!
+            </body></html>
+            """
+
+.. -> src
+
+    >>> open('hello.py', 'w').write(src)
+    >>> del sys.modules['hello']
+
+A view adapts a context, typically a content object of some sort, and
+a request and provides a call method to generate a page.
+
+We register views aith ZCML. Let's change our configuration file::
+
+    [app:main]
+    use = egg:zc.publication
+    zcml =
+       <configure xmlns="http://namespaces.zope.org/zope"
+                  xmlns:browser="http://namespaces.zope.org/browser"
+                  >
+         <include package="zc.publication" />
+
+         <adapter
+             for="zope.location.interfaces.IRoot
+                  zope.publisher.interfaces.browser.IBrowserRequest"
+             provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+             name="index.html"
+             factory="hello.Hello"
+             />
+       </configure>
+
+    [server:main]
+    use = egg:Paste#http
+    port = 8080
+
+.. -> src
+
+    >>> open('hello.ini', 'w').write(src)
+    >>> app = testapp('hello.ini')
+
+In this example, we didn't specify a root object.  If we don't specify
+a root, ``zc.publication`` uses an instance of ``zope.location.Location``
+that provides ``zope.location.interfaces.IRoot``.  This allows us to
+use views as top-level objects in the URL space.  In this example,
+we've registered a view named ``index.html``.  Because ``index.html``
+is the default view name, we can access it with either::
+
+   http://localhost:8080/
+
+.. -> url strio
+
+    >>> import logging
+    >>> logger = logging.getLogger()
+    >>> logger.setLevel(logging.INFO)
+    >>> logger.addHandler(logging.StreamHandler(sys.stdout))
+
+    >>> app.get(url, status=500).body
+
+    >>> app.get(url, status=200).body.strip() == expected
+    True
+
+or::
+
+   http://localhost:8080/index.html
+
+.. -> url strip
+
+    >>> app.get(url, status=200).body.strip() == expected
+    True
+
+
+Traversal
+=========
+
+In the initial example, we avoided traversal by providing an
+application with a single resource and no URL path to traverse.  Most
+applications will provide multiple resources.  To provide multiple
+resource requires use of the ``zope.publisher`` traversal interfaces.
+There are 2 interfaces to be aware of. The first is
+``zope.publisher.interfaces.IPublishTraverse``::
+
+    class IPublishTraverse(Interface):
+
+        def publishTraverse(request, name):
+            """Lookup a name
+
+            The 'request' argument is the publisher request object.  The
+            'name' argument is the name that is to be looked up; it must
+            be an ASCII string or Unicode object.
+
+            If a lookup is not possible, raise a NotFound error.
+            """
+
+This interface is used to traverse an object with a name.  It's used
+when a URL path isn't empty.  The publisher starts with the root
+object. If a path isn't empty, it adapts the root object to
+``IPublishTraverse`` and calls ``publishTraverse`` on the result of
+the adaptation with the first name in the path, which returns a new
+object. If there are additional names in the path, the process is
+continued with the new object.  When the path is exhausted, the last
+object returned by ``publishTraverse`` (or the root object, if the
+path is empty) is caled to produce a response.
+
+The second interface to be aware of is
+``zope.publisher.interfaces.browser.IBrowserPublisher``::
+
+    class IBrowserPublisher(IPublishTraverse):
+
+        def browserDefault(request):
+            """Provide the default object
+
+            The default object is expressed as a (possibly different)
+            object and/or additional traversal steps.
+
+            Returns an object and a sequence of names.  If the sequence of
+            names is not empty, then a traversal step is made for each name.
+            After the publisher gets to the end of the sequence, it will
+            call browserDefault on the last traversed object.
+
+            Normal usage is to return self for object and a default
+            view name.
+
+            The publisher calls this method at the end of each
+            traversal path. If a non-empty sequence of names is
+            returned, the publisher will traverse those names and call
+            browserDefault again at the end.
+
+            Note that if additional traversal steps are indicated (via a
+            nonempty sequence of names), then the publisher will try to adjust
+            the base href.
+            """
+
+``IBrowserPublisher`` is used for ``GET``, ``HEAD``, and ``POST``
+requests to identify a default resource for an object.  Whwn ll of the
+steps in a URL path have been traversed, the resulting object is
+adapted to this interface and browserDefault is called.  If no names
+are returned, then traversal is completed, and the object returned is
+published. If names are returned, they are added to the path and
+traversal continues from the retirned object. There are 2 use
+cases that this interface addresses:
+
+Traversing application objects that are not resources
+   If a URL path addresses an application object, you need a way to
+   select a resource to display the object.
+
+Controlling how relative URLs are interpreted
+   You often want to control how relative URLs are interpreted. For
+   example, when a URL addresses a container or an application object,
+   you want URLs to be reletive to the object.  Generally, in this
+   case, the URL should end with a slash.  If it doesn't, you'll want
+   the browser to be redirected to the URL with a slash added, or
+   you'll want to set the base href in the page returned.  If a URL
+   addresses a resource already, you'll generally want URLs to be
+   interpreted relative to the parent URL.
+
+``zc.publication`` registers some default adapters to handle common
+cases:
+
+- An adapter for all objects to ``IPublishTraverse`` provides
+  traversal by looking up named views.  This allowed traversal from
+  the root object to the ``index.html`` view the second example.
+
+- An adapter for all objects to ``IBrowserPublisher`` with an
+  ``browserDefault`` method that returns the adapted object and an
+  empty path.
+
+- An adapter for ``zope.location.interfaces.ILocation`` to
+  ``IBrowserPublisher`` with an ``browserDefault`` method that returns
+  the adapted object and a path with the default view name, which it
+  arranges to be ``index.html``.
+
+To support more complex traversal models, you'll need to provide these
+interfaces yourself, or provide or use other adapters to them.  For
+example, if you arrange your content objects in a containment hierarch
+using the zope.container framework, you can use the adapters provided
+by that framework to provide traversal of the containment hierarchy.
+
+Background
+==========
+
+The ``zope.publisher`` framework for creating web applications
+requires a publication object that customizes the publisher for a
+particular application framework. The ``zope.app.publication`` package (ZAPP)
+provides a publication object that provides a framework that provides
+capabilities used by most Zope applications, including:
+
+- object traversal from a root object,
+- mapping of requests to transactions,
+- a security protection system [#protection]_ (using sercurity proxies),
+- authentication during traversal to allow multiple authentication domains,
+- events to notify applications of key pooints in the request lidecycle,
+- error handling with
+  - application-provided error views,
+  - application-pluggable error logging,
+  - request retry to handle conflicting transactions,
+
+ZAPP provides a lot of power and flexibility, but at a
+cost.  It can be hard to provide all of the components needed to get
+started.
+
+The ZAPP framework was originally used to create a large application
+similar to the Zope 2 application.  For lack of a better name, lets
+call this application "Z3A".  Z3A included a large number of
+components configured with many Zope Configuration Markup (ZCML)
+files.  To ease high-level configuration of Z3A, an application
+configuration framework based on ZConfig was used to provide a
+high-level configuration.  Z3A provided an example of how to build
+applications using ZAPP.
+
+While most applications can benefit from most of the features that are
+provided by ZAPP, most applications are much simpler than Z3A.  Z3A
+provides a poor example of how to create most applications.  It's not
+at all clear how to create simpler applications. People often resorted
+to copying Z3A and building on it, carrying along components they
+didn't need or understand.
+
+Most of the choices made by ZAPP are generally applicable, but 2 are
+not [#traveral]_:
+
+- ZAPP assumes the root object for traversal is particular object in
+  a ZODB object database.
+
+- ZAPP assumes a security protection scheme based on a particular
+  implementation of security proxies.
+
+---------------------------------------------------------------------
+
+.. [#protection] A security protection system is responsible for
+   enforcing security restrictions. It is a part of a large security
+   architecture that includes authentication, authorization,
+   declaration, and protection.
+
+.. [#traversal] It can be argued that traversal isn't a generally
+   applicable choice. Certainly, an approach based on URL pattern
+   matching is a reasonable approach to traversal, however, traversal
+   is a useful approach in many applications, especially application
+   with data-driven URL hierarchies.
+
+
+.. cleanup
+
+    >>> _ = sys.path.pop(0)
+    >>> del sys.modules['hello']

Modified: zc.publication/branches/dev/src/zc/publication/__init__.py
===================================================================
--- zc.publication/branches/dev/src/zc/publication/__init__.py	2009-06-16 10:54:55 UTC (rev 101073)
+++ zc.publication/branches/dev/src/zc/publication/__init__.py	2009-06-16 11:17:55 UTC (rev 101074)
@@ -14,6 +14,12 @@
 
 import os
 import zope.app.publication.browser
+import zope.app.publication.traversers
+import zope.component
+import zope.configuration.xmlconfig
+import zope.interface
+import zope.location.interfaces
+import zope.publisher.interfaces.browser
 import zope.publisher.paste
 
 class Publication(zope.app.publication.browser.BrowserPublication):
@@ -26,28 +32,69 @@
 
         super(Publication, self).__init__(None)
 
-        root = options['root']
-        if root.startswith('egg:'):
+        root = options.get('root')
+        if root is None:
+            rootobj = zope.location.Location()
+            zope.interface.directlyProvides(
+                rootobj, zope.location.interfaces.IRoot)
+            root = lambda request: rootobj
+        elif root.startswith('egg:'):
             root = zope.publisher.paste.get_egg(root[4:], 'zc.publication.root')
-            base = os.getcwd()         # XXX
         else:
-            module_name, expr = root.split(':')
+            module_name, expr = root.split(':', 1)
             module = __import__(module_name, {}, {}, ['*'])
-            root = eval(expr, options, module.__dict__)
-            base = os.path.dirname(module.__file__)
+            root = eval(expr, module.__dict__, options)
 
-        self.getApplication = root
+        self.getApplication = lambda request: self.proxy(root(request))
 
-        zcml = options.get('zcml')
-        if zcml is not None:
-            zcml = os.path.join(base, zcml)
-            import zope.configuration.xmlconfig
-            zope.configuration.xmlconfig.xmlconfig(open(zcml))
+        zcml = options.get('zcml', '<include package="zc.publication" />')
+        zope.configuration.xmlconfig.string(zcml)
 
-        logconfig = options.get('logging')
+        logconfig = options.get('loggers')
         if logconfig:
             import ZConfig
             ZConfig.configureLoggers(logconfig)
         else:
             import logging
             logging.basicConfig()
+
+    # XXX zope.app.publication should have an overridable proxy method
+
+    def proxy(self, ob):
+        return ob
+
+    # XXX the following method is overridden to use
+    # self.proxy rather than ProxyFactory:
+
+    def getDefaultTraversal(self, request, ob):
+        if zope.publisher.interfaces.browser.IBrowserPublisher.providedBy(ob):
+            # ob is already proxied, so the result of calling a method will be
+            return ob.browserDefault(request)
+        else:
+            adapter = zope.component.queryMultiAdapter(
+                (ob, request),
+                zope.publisher.interfaces.browser.IBrowserPublisher,
+                )
+            if adapter is not None:
+                ob, path = adapter.browserDefault(request)
+                return self.proxy(ob), path
+            else:
+                # ob is already proxied
+                return ob, None
+
+    # XXX we should override handleException too, but it's rather complicated.
+    # *really* should release a new version of zope.app.publication.
+
+
+class Application(zope.publisher.paste.Application):
+
+    def __init__(self, global_config=None, **options):
+        zope.publisher.paste.Application.__init__(
+            self, global_config, 'egg:zc.publication', **options)
+
+class DefaultTraverser(
+    zope.app.publication.traversers.SimpleComponentTraverser,
+    ):
+
+    def browserDefault(self, request):
+        return self.context, ()

Modified: zc.publication/branches/dev/src/zc/publication/configure.zcml
===================================================================
--- zc.publication/branches/dev/src/zc/publication/configure.zcml	2009-06-16 10:54:55 UTC (rev 101073)
+++ zc.publication/branches/dev/src/zc/publication/configure.zcml	2009-06-16 11:17:55 UTC (rev 101074)
@@ -18,5 +18,28 @@
       provides="zope.authentication.interfaces.IAuthentication"
       component=".noauth"
       />
+
+  <adapter
+     provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+     for="*
+          zope.publisher.interfaces.browser.IBrowserRequest"
+     factory=".DefaultTraverser"
+     />
+
+  <class class=".DefaultTraverser">
+    <allow interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
+  </class>
+
+  <adapter
+     provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+     for="zope.location.interfaces.ILocation
+          zope.publisher.interfaces.browser.IBrowserRequest"
+     factory="zope.app.publication.traversers.SimpleComponentTraverser"
+     />
+
+  <class class="zope.app.publication.traversers.SimpleComponentTraverser">
+    <allow interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
+  </class>
+
   <browser:defaultView name="index.html" />
 </configure>

Added: zc.publication/branches/dev/src/zc/publication/tests.py
===================================================================
--- zc.publication/branches/dev/src/zc/publication/tests.py	                        (rev 0)
+++ zc.publication/branches/dev/src/zc/publication/tests.py	2009-06-16 11:17:55 UTC (rev 101074)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) Zope Foundation 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
+import manuel
+import manuel.doctest
+import manuel.testing
+import os
+import paste.deploy
+import re
+import textwrap
+import unittest
+import webtest
+
+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 testapp(name, *args, **kw):
+    name = os.path.abspath(name)
+    app = paste.deploy.loadapp('config://'+name)
+    return webtest.TestApp(app, *args, **kw)
+
+def setUp(test):
+    setupstack.setUpDirectory(test)
+    test.globs['testapp'] = testapp
+
+def test_suite():
+    return unittest.TestSuite((
+        manuel.testing.TestSuite(
+            assignment_manuel(),
+            'README.txt',
+            setUp=setUp, tearDown=setupstack.tearDown),
+        ))
+


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



More information about the Checkins mailing list