SVN: r25422 - in trunk/quixote: . demo server

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Mon, 25 Oct 2004 17:04:30 -0400
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: nascheme
Date: 2004-10-25 17:04:08 -0400 (Mon, 25 Oct 2004)
New Revision: 25422

Added:
   trunk/quixote/directory.py
Modified:
   trunk/quixote/__init__.py
   trunk/quixote/demo/__init__.py
   trunk/quixote/demo/demo.cgi
   trunk/quixote/demo/demo_scgi.py
   trunk/quixote/demo/integer_ui.py
   trunk/quixote/demo/run_cgi.py
   trunk/quixote/demo/session.ptl
   trunk/quixote/errors.py
   trunk/quixote/logger.py
   trunk/quixote/mod_python_handler.py
   trunk/quixote/publish.py
   trunk/quixote/server/medusa_http.py
   trunk/quixote/server/twisted_http.py
   trunk/quixote/session.py
   trunk/quixote/util.py
Log:
Refactor the Publisher object.  The new design gives "namespaces" more
control over traversal.  The magic of automatic package imports is
gone.  By default, your namespaces must be 'Directory' instances.  The
_q_access hook is gone.  'namespace_stack' is also gone.


Modified: trunk/quixote/__init__.py
===================================================================
--- trunk/quixote/__init__.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/__init__.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -9,14 +9,9 @@
 
 __version__ = "2.0a1"
 
-__all__ = ['Publisher',
-           'get_publisher', 'get_request', 'get_session', 'get_user',
-           'get_path', 'enable_ptl', 'redirect']
-
-
 # These are frequently needed by Quixote applications, so make them easy
 # to get at.
-from quixote.publish import Publisher, \
+from quixote.publish import \
      get_publisher, get_request, get_response, get_path, redirect, \
      get_session, get_session_manager, get_user, get_field, get_cookie
 

Modified: trunk/quixote/demo/__init__.py
===================================================================
--- trunk/quixote/demo/__init__.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/__init__.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -4,38 +4,51 @@
               ("favicon.ico", "q_ico")]
 
 import sys
+import os
 from quixote import get_response
+from quixote.directory import Directory, Resolving
 from quixote.demo.pages import _q_index, _q_exception_handler, dumpreq
 from quixote.demo.integer_ui import IntegerUI
+from quixote.demo.session import SessionUI
+from quixote.demo import forms
 from quixote.errors import PublishError
 from quixote.util import StaticDirectory, StaticFile
 
-def simple():
-    # This function returns a plain text document, not HTML.
-    get_response().set_content_type("text/plain")
-    return "This is the Python function 'quixote.demo.simple'.\n"
+class DemoUI(Resolving, Directory):
 
-def error():
-    raise ValueError, "this is a Python exception"
+    _q_exports = ["", "simple", "error", "publish_error", "widgets",
+                  "form_demo", "dumpreq", "srcdir",
+                  ("favicon.ico", "q_ico")]
 
-def publish_error():
-    raise PublishError(public_msg="Publishing error raised by publish_error")
+    def _q_index(self):
+        return _q_index()
 
-def _q_lookup(component):
-    return IntegerUI(component)
+    def simple(self):
+        # This function returns a plain text document, not HTML.
+        get_response().set_content_type("text/plain")
+        return "This is the Python function 'quixote.demo.simple'.\n"
 
-def _q_resolve(component):
-    # _q_resolve() is a hook that can be used to import only
-    # when it's actually accessed.  This can be used to make
-    # start-up of your application faster, because it doesn't have
-    # to import every single module when it starts running.
-    if component == 'form_demo':
-        from quixote.demo.forms import form_demo
-        return form_demo
+    def error(self):
+        raise ValueError, "this is a Python exception"
 
-# Get current directory
-import os
-from quixote.demo import forms
-curdir = os.path.dirname(forms.__file__)
-srcdir = StaticDirectory(curdir, list_directory=True)
-q_ico = StaticFile(os.path.join(curdir, 'q.ico'))
+    def publish_error(self):
+        raise PublishError("Publishing error raised by publish_error")
+
+    def dumpreq(self):
+        return dumpreq()
+
+    def _q_lookup(self, component):
+        return IntegerUI(component)
+
+    def _q_resolve(self, component):
+        # _q_resolve() is a hook that can be used to import only
+        # when it's actually accessed.  This can be used to make
+        # start-up of your application faster, because it doesn't have
+        # to import every single module when it starts running.
+        if component == 'form_demo':
+            from quixote.demo.forms import form_demo
+            return form_demo
+
+    _curdir = os.path.dirname(forms.__file__)
+    srcdir = StaticDirectory(_curdir, list_directory=1)
+    q_ico = StaticFile(os.path.join(_curdir, 'q.ico'))

Modified: trunk/quixote/demo/demo.cgi
===================================================================
--- trunk/quixote/demo/demo.cgi	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/demo.cgi	2004-10-25 21:04:08 UTC (rev 25422)
@@ -8,8 +8,9 @@
 # Install the import hook that enables PTL modules.
 enable_ptl()
 
-# Create a Publisher instance 
-app = Publisher('quixote.demo', display_exceptions='plain')
+# Create a Publisher instance
+from quixote.demo import DemoUI
+app = Publisher(DemoUI(), display_exceptions='plain')
 
 # Enter the publishing main loop
 app.publish_cgi()

Modified: trunk/quixote/demo/demo_scgi.py
===================================================================
--- trunk/quixote/demo/demo_scgi.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/demo_scgi.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -11,7 +11,9 @@
 
 
 from scgi.quixote_handler import QuixoteHandler, main
-from quixote import enable_ptl, Publisher
+from quixote import enable_ptl
+from quixote.publisher Publisher
+from quixote.demo import DemoUI
 
 class DemoPublisher(Publisher):
     def __init__(self, *args):
@@ -19,7 +21,7 @@
 
 class DemoHandler(QuixoteHandler):
     publisher_class = DemoPublisher
-    root_namespace = "quixote.demo"
+    root_directory = DemoUI()
     prefix = "/qdemo"
 
 

Modified: trunk/quixote/demo/integer_ui.py
===================================================================
--- trunk/quixote/demo/integer_ui.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/integer_ui.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -1,5 +1,6 @@
 import sys
 from quixote import get_response, redirect
+from quixote.directory import Directory
 from quixote.errors import TraversalError
 
 def fact(n):
@@ -9,9 +10,9 @@
         n -= 1
     return f
 
-class IntegerUI:
+class IntegerUI(Directory):
 
-    _q_exports = ["factorial", "prev", "next"]
+    _q_exports = ["", "factorial", "prev", "next"]
 
     def __init__(self, component):
         try:
@@ -28,30 +29,30 @@
 
     def _q_index(self):
         return """\
-<html>
-<head><title>The Number %d</title></head>
-<body>
-You have selected the integer %d.<p>
+        <html>
+        <head><title>The Number %d</title></head>
+        <body>
+        You have selected the integer %d.<p>
 
-You can compute its <a href="factorial">factorial</a> (%d!)<p>
+        You can compute its <a href="factorial">factorial</a> (%d!)<p>
 
-Or, you can visit the web page for the
-<a href="../%d/">previous</a> or
-<a href="../%d/">next</a> integer.<p>
+        Or, you can visit the web page for the
+        <a href="../%d/">previous</a> or
+        <a href="../%d/">next</a> integer.<p>
 
-Or, you can use redirects to visit the
-<a href="prev">previous</a> or
-<a href="next">next</a> integer.  This makes
-it a bit easier to generate this HTML code, but
-it's less efficient -- your browser has to go through
-two request/response cycles.  And someone still
-has to generate the URLs for the previous/next
-pages -- only now it's done in the <code>prev()</code>
-and <code>next()</code> methods for this integer.<p>
+        Or, you can use redirects to visit the
+        <a href="prev">previous</a> or
+        <a href="next">next</a> integer.  This makes
+        it a bit easier to generate this HTML code, but
+        it's less efficient -- your browser has to go through
+        two request/response cycles.  And someone still
+        has to generate the URLs for the previous/next
+        pages -- only now it's done in the <code>prev()</code>
+        and <code>next()</code> methods for this integer.<p>
 
-</body>
-</html>
-""" % (self.n, self.n, self.n, self.n-1, self.n+1)
+        </body>
+        </html>
+        """ % (self.n, self.n, self.n, self.n-1, self.n+1)
 
     def prev(self):
         return redirect("../%d/" % (self.n-1))

Modified: trunk/quixote/demo/run_cgi.py
===================================================================
--- trunk/quixote/demo/run_cgi.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/run_cgi.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -16,7 +16,8 @@
 
 import sys
 import new
-from quixote import enable_ptl, ptl_compile, Publisher
+from quixote import enable_ptl, ptl_compile
+from quixote.publisher import Publisher
 
 enable_ptl()
 filename = sys.argv[1]

Modified: trunk/quixote/demo/session.ptl
===================================================================
--- trunk/quixote/demo/session.ptl	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/demo/session.ptl	2004-10-25 21:04:08 UTC (rev 25422)
@@ -6,147 +6,149 @@
 __revision__ = "$Id$"
 
 from quixote import get_session_manager, get_session, get_request, get_field
+from quixote.directory import Directory
 from quixote.errors import QueryError
 
-_q_exports = ['login', 'logout']
-
-
 # Typical stuff for any Quixote app.
 
 def page_header [html] (title):
     '''\
-<html>
-<head><title>%s</title></head>
-<body>
-<h1>%s</h1>
-''' % (title, title)
+    <html>
+    <head><title>%s</title></head>
+    <body>
+    <h1>%s</h1>
+    ''' % (title, title)
 
 def page_footer [html] ():
     '''\
-</body>
-</html>
-'''
+    </body>
+    </html>
+    '''
 
 
 # We include the login form on two separate pages, so it's been factored
 # out to a separate template.
 
 def login_form [html] ():
+    '''\
+    <form method="POST" action="login">
+      <input name="name" width=30>
+      <input type="submit">
+    </form>
     '''
-<form method="POST" action="login">
-  <input name="name" width=30>
-  <input type="submit">
-</form>
-'''
 
 
-def _q_index [html] ():
-    page_header("Quixote Session Management Demo")
+class SessionUI(Directory):
 
-    session = get_session()
+    _q_exports = ['', 'login', 'logout']
 
-    # All Quixote sessions have the ability to track the user's identity
-    # in session.user.  In this simple application, session.user is just
-    # a string which the user enters directly into this form.  In the
-    # real world, you would of course use a more sophisticated form of
-    # authentication (eg. enter a password over an SSL connection), and
-    # session.user might be an object with information about the user
-    # (their email address, password hash, preferences, etc.).
+    def _q_index [html] (self):
+        page_header("Quixote Session Management Demo")
 
-    if session.user is None:
+        session = get_session()
+
+        # All Quixote sessions have the ability to track the user's identity
+        # in session.user.  In this simple application, session.user is just
+        # a string which the user enters directly into this form.  In the
+        # real world, you would of course use a more sophisticated form of
+        # authentication (eg. enter a password over an SSL connection), and
+        # session.user might be an object with information about the user
+        # (their email address, password hash, preferences, etc.).
+
+        if session.user is None:
+            '''
+            <p>You haven\'t introduced yourself yet.<br>
+            Please tell me your name:
+            '''
+            login_form()
+        else:
+            '<p>Hello, %s.  Good to see you again.</p>\n' % session.user
+
         '''
-<p>You haven\'t introduced yourself yet.<br>
-Please tell me your name:
-'''
-        login_form()
-    else:
-        '<p>Hello, %s.  Good to see you again.</p>\n' % session.user
+        You can now:
+        <ul>
+        '''
+        if session.user:
+            '  <li><a href="login">become someone else</a> (login again)\n'
+        '  <li><a href="logout">leave this session</a> (logout)\n'
+        '</ul>\n'
 
-    '''
-You can now:
-<ul>
-'''
-    if session.user:
-        '  <li><a href="login">become someone else</a> (login again)\n'
-    '  <li><a href="logout">leave this session</a> (logout)\n'
-    '</ul>\n'
+        # The other piece of information we track here is the number of
+        # requests made in each session; report that information for the
+        # current session here.
+        """\
+        <p>Your session is <code>%s</code><br>
+        You have made %d request(s) (including this one) in this session.</p>
+        """ % (repr(session), session.num_requests)
 
-    # The other piece of information we track here is the number of
-    # requests made in each session; report that information for the
-    # current session here.
-    """\
-<p>Your session is <code>%s</code><br>
-You have made %d request(s) (including this one) in this session.</p>
-""" % (repr(session), session.num_requests)
+        # The session manager is the collection of all sessions managed by
+        # the current publisher, ie. in this process.  Poking around in the
+        # session manager is not something you do often, but it's really
+        # handy for debugging/site administration.
+        mgr = get_session_manager()
+        session_ids = mgr.keys()
+        '''
+        <p>The current session manager is <code>%s</code><br>
+        It has %d session(s) in it right now:</p>
+        <table border=1>
+          <tr><th>session id</th><th>user</th><th>num requests</th></tr>
+        ''' % (repr(mgr), len(session_ids))
+        for sess_id in session_ids:
+            sess = mgr[sess_id]
+            ('  <tr><td>%s</td><td>%s</td><td>%d</td>\n'
+             % (sess.id,
+                sess.user and sess.user or "<i>none</i>",
+                sess.num_requests))
+        '<table>\n'
 
-    # The session manager is the collection of all sessions managed by
-    # the current publisher, ie. in this process.  Poking around in the
-    # session manager is not something you do often, but it's really
-    # handy for debugging/site administration.
-    mgr = get_session_manager()
-    session_ids = mgr.keys()
-    '''
-<p>The current session manager is <code>%s</code><br>
-It has %d session(s) in it right now:</p>
-<table border=1>
-  <tr><th>session id</th><th>user</th><th>num requests</th></tr>
-''' % (repr(mgr), len(session_ids))
-    for sess_id in session_ids:
-        sess = mgr[sess_id]
-        ('  <tr><td>%s</td><td>%s</td><td>%d</td>\n'
-         % (sess.id,
-            sess.user and sess.user or "<i>none</i>",
-            sess.num_requests))
-    '<table>\n'
+        page_footer()
 
-    page_footer()
 
+    # The login() template has two purposes: to display a page with just a
+    # login form, and to process the login form submitted either from the
+    # index page or from login() itself.  This is a fairly common idiom in
+    # Quixote (just as it's a fairly common idiom with CGI scripts -- it's
+    # just cleaner with Quixote).
 
-# The login() template has two purposes: to display a page with just a
-# login form, and to process the login form submitted either from the
-# index page or from login() itself.  This is a fairly common idiom in
-# Quixote (just as it's a fairly common idiom with CGI scripts -- it's
-# just cleaner with Quixote).
+    def login [html] (self):
+        page_header("Quixote Session Demo: Login")
+        request = get_request()
+        session = get_session()
 
-def login [html] ():
-    page_header("Quixote Session Demo: Login")
-    request = get_request()
-    session = get_session()
+        # We seem to be processing the login form.
+        if request.form:
+            user = get_field("name")
+            if not user:
+                raise QueryError("no user name supplied")
 
-    # We seem to be processing the login form.
-    if request.form:
-        user = get_field("name")
-        if not user:
-            raise QueryError("no user name supplied")
+            session.user = user
 
-        session.user = user
+            '<p>Welcome, %s!  Thank you for logging in.</p>\n' % user
+            '<a href="./">back to start</a>\n'
 
-        '<p>Welcome, %s!  Thank you for logging in.</p>\n' % user
-        '<a href="./">back to start</a>\n'
+        # No form data to process, so generate the login form instead.  When
+        # the user submits it, we'll return to this template and take the
+        # above branch.
+        else:
+            '<p>Please enter your name here:</p>\n'
+            login_form()
 
-    # No form data to process, so generate the login form instead.  When
-    # the user submits it, we'll return to this template and take the
-    # above branch.
-    else:
-        '<p>Please enter your name here:</p>\n'
-        login_form()
+        page_footer()
 
-    page_footer()
 
+    # logout() just expires the current session, ie. removes it from the
+    # session manager and instructs the client to forget about the session
+    # cookie.  The only code necessary is the call to
+    # SessionManager.expire_session() -- the rest is just user interface.
 
-# logout() just expires the current session, ie. removes it from the
-# session manager and instructs the client to forget about the session
-# cookie.  The only code necessary is the call to
-# SessionManager.expire_session() -- the rest is just user interface.
+    def logout [html] (self):
+        page_header("Quixote Session Demo: Logout")
+        session = get_session()
+        if session.user:
+            '<p>Goodbye, %s.  See you around.</p>\n' % session.user
 
-def logout [html] ():
-    page_header("Quixote Session Demo: Logout")
-    session = get_session()
-    if session.user:
-        '<p>Goodbye, %s.  See you around.</p>\n' % session.user
+        get_session_manager().expire_session()
 
-    get_session_manager().expire_session()
-
-    '<p>Your session has been expired.</p>\n'
-    '<p><a href="./">start over</a></p>\n'
-    page_footer()
+        '<p>Your session has been expired.</p>\n'
+        '<p><a href="./">start over</a></p>\n'
+        page_footer()

Added: trunk/quixote/directory.py
===================================================================
--- trunk/quixote/directory.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/directory.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -0,0 +1,106 @@
+"""$HeadURL$
+$Id$
+
+Logic for traversing directory objects and generating output.
+"""
+from quixote.html import htmltext
+from quixote.errors import TraversalError, TrailingSlashError
+
+class Directory(object):
+    """
+    Instance attributes: none
+    """
+
+    # A list containing strings or 2-tuples of strings that map external
+    # names to internal names.  Note that the empty string will be
+    # implicitly mapped to '_q_index'.
+    _q_exports = []
+
+    def _q_translate(self, component):
+        """(component : string) -> string | None
+
+        Translate a path component into a Python identifier.  Returning
+        None signifies that the component does not exist.
+        """
+        if component in self._q_exports:
+            if component == '':
+                return '_q_index' # implicit mapping
+            else:
+                return component
+        else:
+            # check for an explicit external to internal mapping
+            for value in self._q_exports:
+                if isinstance(value, tuple):
+                    if value[0] == component:
+                        return value[1]
+            else:
+                return None
+
+    def _q_lookup(self, component):
+        """(component : string) -> object
+
+        Lookup a path component and return the corresponding object (usually
+        a Directory, a method or a string).  Returning None signals that the
+        component does not exist.
+        """
+        return None
+
+    def _q_traverse(self, path):
+        """(path: [string]) -> object
+
+        Traverse a path and return the result.
+        """
+        assert len(path) > 0
+        component = path[0]
+        path = path[1:]
+        name = self._q_translate(component)
+        if name is not None:
+            obj = getattr(self, name)
+        else:
+            obj = self._q_lookup(component)
+        if obj is None:
+            raise TraversalError('directory %r has no component %r' %
+                                 (self, component))
+        if path:
+            if not isinstance(obj, Directory):
+                raise TraversalError('%r is not a Directory instance' % obj)
+            return obj._q_traverse(path)
+        else:
+            if callable(obj):
+                return obj()
+            elif isinstance(obj, Directory) and obj._q_translate(''):
+                raise TrailingSlashError(
+                    '%r is not callable (missing trailing slash?)' % obj)
+            else:
+                return obj
+
+
+class AccessControlled(object):
+    """
+    A mix-in class that calls the _q_access() method before traversing
+    into the directory.
+    """
+    def _q_access(self):
+        pass
+
+    def _q_traverse(self, path):
+        self._q_access()
+        return super(AccessControlled, self)._q_traverse(path)
+
+
+class Resolving(object):
+    """
+    A mix-in class that provides the _q_resolve() method.  _q_resolve()
+    is called if a component name appears in the _q_exports list but is
+    not an instance attribute.  _q_resolve is expected to return the
+    component object.
+    """
+    def _q_resolve(self, name):
+        return None
+
+    def _q_translate(self, component):
+        name = super(Resolving, self)._q_translate(component)
+        if name is not None and not hasattr(self, name):
+            obj = self._q_resolve(name)
+            setattr(self, name, obj)
+        return name


Property changes on: trunk/quixote/directory.py
___________________________________________________________________
Name: svn:keywords
   + HeadURL Id

Modified: trunk/quixote/errors.py
===================================================================
--- trunk/quixote/errors.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/errors.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -84,6 +84,11 @@
             msg = msg + ": " + self.private_msg
         return msg
 
+class TrailingSlashError (TraversalError):
+    """A TraversalError that most likely could be avoided by appending a
+    slash to the path.
+    """
+
 class RequestError(PublishError):
     """
     Raised when Quixote is unable to parse an HTTP request (or its CGI
@@ -146,12 +151,10 @@
         return msg
 
 
-def default_exception_handler(exc):
+def format_publish_error(exc):
     """(exc : PublishError) -> string
 
-    Format a PublishError exception as a web page.  This is the default
-    handler called if no '_q_exception_handler' function was found while
-    traversing the path.
+    Format a PublishError exception as a web page.
     """
     return htmltext("""\
     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"

Modified: trunk/quixote/logger.py
===================================================================
--- trunk/quixote/logger.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/logger.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -9,8 +9,9 @@
 
 class DefaultLogger:
     """
-    This is the default logger object used by the Quixote publisher.  You may
-    provide your own object if you wish to have different behavior.
+    This is the default logger object used by the Quixote publisher.  It
+    controls access log and error log behavior.  You may provide your own
+    object if you wish to have different behavior.
 
     Instance attributes:
 

Modified: trunk/quixote/mod_python_handler.py
===================================================================
--- trunk/quixote/mod_python_handler.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/mod_python_handler.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -8,7 +8,8 @@
 
 import sys
 from mod_python import apache
-from quixote import Publisher, enable_ptl
+from quixote import enable_ptl
+from quixote.publish import Publisher
 from quixote.config import Config
 
 class ErrorLog:

Modified: trunk/quixote/publish.py
===================================================================
--- trunk/quixote/publish.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/publish.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -16,7 +16,9 @@
 except ImportError:
     pass
 
-from quixote import errors
+from quixote.directory import Directory
+from quixote.errors import PublishError, TrailingSlashError, \
+     format_publish_error
 from quixote.html import htmltext
 from quixote import util
 from quixote.config import Config
@@ -62,8 +64,8 @@
 class Publisher:
     """
     The core of Quixote and of any Quixote application.  This class is
-    responsible for converting each HTTP request into a search of
-    Python's package namespace and, ultimately, a call of a Python
+    responsible for converting each HTTP request into a traversal of the
+    application's directory tree and, ultimately, a call of a Python
     function/method/callable object.
 
     Each invocation of a driver script should have one Publisher
@@ -74,10 +76,11 @@
     script process.
 
     Instance attributes:
-      root_namespace : module | instance | class
-        the Python namespace that will be searched for objects to
-        fulfill each HTTP request
+      root_directory : Directory
+        the root directory that will be searched for objects to fulfill
+        each request
       logger : DefaultLogger
+        controls access log and error log behavior
       exit_now : boolean
         used for internal state management.  If true, the loop in
         publish_cgi() will terminate at the end of the current request.
@@ -87,10 +90,9 @@
         from the quixote.config module are used.
       _request : HTTPRequest
         the HTTP request currently being processed.
-      namespace_stack : [ module | instance | class ]
     """
 
-    def __init__(self, root_namespace, logger=None, config=None, **kwargs):
+    def __init__(self, root_directory, logger=None, config=None, **kwargs):
         global _publisher
         if config is None:
             self.config = Config(**kwargs)
@@ -110,19 +112,11 @@
             raise RuntimeError, "only one instance of Publisher allowed"
         _publisher = self
 
-        if isinstance(root_namespace, str):
-            self.root_namespace = _get_module(root_namespace)
-        else:
-            # Should probably check that root_namespace is really a
-            # namespace, ie. a module, class, or instance -- but it's
-            # tricky to know if something is really a class or instance
-            # (because of ExtensionClass), and who knows what other
-            # namespaces are lurking out there in the world?
-            self.root_namespace = root_namespace
+        if not isinstance(root_directory, Directory):
+            raise TypeError('Directory instance expected, got %r' %
+                            root_directory)
+        self.root_directory = root_directory
 
-        # for PublishError exception handling
-        self.namespace_stack = [self.root_namespace]
-
         self.exit_now = False
         self._request = None
 
@@ -160,53 +154,23 @@
         SessionPublisher to handle session details."""
         pass
 
+    def format_publish_error(self, exc):
+        return format_publish_error(exc)
+
     def finish_interrupted_request(self, exc):
         """
         Called at the end of an interrupted request.  Requests are
         interrupted by raising a PublishError exception.  This method
         should return a string object which will be used as the result of
         the request.
-
-        This method searches for the nearest namespace with a
-        _q_exception_handler attribute.  That attribute is expected to be
-        a function and is called with the request and exception instance
-        as arguments and should return the error page (e.g. a string).  If
-        the handler doesn't want to handle a particular error it can
-        re-raise it and the next nearest handler will be found.  If no
-        _q_exception_handler is found, the default Quixote handler is
-        used.
         """
-
-        # Throw away the existing response object and start a new one
-        # for the error document we're going to create here.
-        request = get_request()
-        request.response = HTTPResponse()
-
-        # set response status code so every custom doesn't have to do it
-        request.response.set_status(exc.status_code)
-
+        print 'Publisher finish_interrupted_request'
         if not self.config.display_exceptions and exc.private_msg:
             exc.private_msg = None # hide it
+        request = get_request()
+        request.response = HTTPResponse(status=exc.status_code)
+        return self.format_publish_error(exc)
 
-        # walk up stack and find handler for the exception
-        stack = self.namespace_stack[:]
-        while 1:
-            handler = None
-            while stack:
-                object = stack.pop()
-                if hasattr(object, "_q_exception_handler"):
-                    handler = object._q_exception_handler
-                    break
-            if handler is None:
-                handler = errors.default_exception_handler
-
-            try:
-                return handler(exc)
-            except errors.PublishError:
-                assert handler is not errors.default_exception_handler
-                continue # exception was re-raised or another exception occured
-
-
     def finish_failed_request(self):
         """
         Called at the end of an failed request.  Any exception (other
@@ -287,10 +251,7 @@
         return error_file.getvalue()
 
 
-    def get_namespace_stack(self):
-        """get_namespace_stack() ->  [ module | instance | class ]
-        """
-        return self.namespace_stack
+    _SLASH_PAT = re.compile("//*")
 
     def try_publish(self, request, path):
         """try_publish(request : HTTPRequest, path : string) -> string
@@ -299,46 +260,16 @@
         traverse_url() to get a callable object.  The object is called and
         the output is returned.  Exceptions are handled by the caller.
         """
-
         self.start_request()
-
-        # Initialize the publisher's namespace_stack
-        self.namespace_stack = []
-
-        # Traverse package to a (hopefully-) callable object
-        object = _traverse_url(self.root_namespace, path, request,
-                               self.config.fix_trailing_slash,
-                               self.namespace_stack)
-
-        # None means no output -- traverse_url() just issued a redirect.
-        if object is None:
-            return None
-
-        # Anything else must be either a string...
-        if isstring(object):
-            output = object
-
-        # ...or a callable.
-        elif callable(object):
-            try:
-                output = object()
-            except SystemExit:
-                output = "SystemExit exception caught, shutting down"
-                self.log(output)
-                self.exit_now = True
-
-            if output is None:
-                raise RuntimeError, 'callable %s returned None' % repr(object)
-
-        # Uh-oh: 'object' is neither a string nor a callable.
-        else:
-            raise RuntimeError(
-                "object is neither callable nor a string: %s" % repr(object))
-
-
+        # split path into components
+        if '//' in path:
+            path = self._SLASH_PAT.sub("/", path)
+        if path[:1] != '/':
+            raise TrailingSlashError("path does not start with /")
+        path = path[1:].split('/')
+        output = self.root_directory._q_traverse(path)
         # The callable ran OK, commit any changes to the session
         self.finish_successful_request()
-
         return output
 
     _GZIP_HEADER = ("\037\213" # magic
@@ -390,9 +321,18 @@
         try:
             self.parse_request(request)
             output = self.try_publish(request, env.get('PATH_INFO', ''))
-        except errors.PublishError, exc:
-            # Exit the publishing loop and return a result right away.
-            output = self.finish_interrupted_request(exc)
+        except PublishError, exc:
+            if (self.config.fix_trailing_slash and
+                isinstance(exc, TrailingSlashError) and
+                not request.form):
+                # This is for the convenience of users who type in paths.
+                # Repair the path and redirect.  This should not happen for
+                # URLs within the site.
+                redirect(request.get_path() + "/", permanent=True)
+                output = None
+            else:
+                # Exit the publishing loop and return a result right away.
+                output = self.finish_interrupted_request(exc)
         except:
             # Some other exception, generate error messages to the logs, etc.
             output = self.finish_failed_request()
@@ -454,9 +394,9 @@
 
 class SessionPublisher(Publisher):
 
-    def __init__(self, root_namespace, session_mgr=None, **kwargs):
+    def __init__(self, root_directory, session_mgr=None, **kwargs):
         from quixote.session import SessionManager
-        Publisher.__init__(self, root_namespace, **kwargs)
+        Publisher.__init__(self, root_directory, **kwargs)
         if session_mgr is None:
             self.session_mgr = SessionManager()
         else:
@@ -479,6 +419,7 @@
         self.session_mgr.commit_changes(session)
 
     def finish_interrupted_request(self, exc):
+        print 'SessionPublisher finish_interrupted_request'
         output = Publisher.finish_interrupted_request(self, exc)
 
         # commit the current transaction so that any changes to the
@@ -508,222 +449,6 @@
 
 # class SessionPublisher
 
-_slash_pat = re.compile("//*")
-
-def _traverse_url(root_namespace, path, request, fix_trailing_slash,
-                  namespace_stack):
-    """traverse_url(root_namespace : any, path : string,
-                    request : HTTPRequest, fix_trailing_slash : bool,
-                    namespace_stack : list) -> (object : any)
-
-    Perform traversal based on the provided path, starting at the root
-    object.  It returns the script name and path info values for
-    the arrived-at object, along with the object itself and
-    a list of the namespaces traversed to get there.
-
-    It's expected that the final object is something callable like a
-    function or a method; intermediate objects along the way will
-    usually be packages or modules.
-
-    To prevent crackers from writing URLs that traverse private
-    objects, every package, module, or object along the way must have
-    a _q_exports attribute containing a list of publicly visible
-    names.  Not having a _q_exports attribute is an error, though
-    having _q_exports be an empty list is OK.  If a component of the path
-    isn't in _q_exports, that also produces an error.
-
-    Modifies the namespace_stack as it traverses the url, so that
-    any exceptions encountered along the way can be handled by the
-    nearest handler.
-    """
-
-    # If someone accesses a Quixote driver script without a trailing
-    # slash, we'll wind up here with an empty path.  This won't
-    # work; relative references in the page generated by the root
-    # namespace's _q_index() will be off.  Fix it by redirecting the
-    # user to the right URL; when the client follows the redirect,
-    # we'll wind up here again with path == '/'.
-    if (not path and fix_trailing_slash):
-        redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=True)
-        return None
-
-    # replace repeated slashes with a single slash
-    if path.find("//") != -1:
-        path = _slash_pat.sub("/", path)
-
-    # split path apart; /foo/bar/baz  -> ['foo', 'bar', 'baz']
-    #                   /foo/bar/     -> ['foo', 'bar', '']
-    path_components = path[1:].split('/')
-
-    # Traverse starting at the root
-    object = root_namespace
-    namespace_stack.append(object)
-
-    # Loop over the components of the path
-    for component in path_components:
-        if component == "":
-            # "/q/foo/" == "/q/foo/_q_index"
-            component = "_q_index"
-        object = _get_component(object, component, path, request,
-                               namespace_stack)
-
-    if not (isstring(object) or callable(object)):
-        # We went through all the components of the path and ended up at
-        # something which isn't callable, like a module or an instance
-        # without a __call__ method.
-        if path[-1] != '/':
-            if not request.form and fix_trailing_slash:
-                # This is for the convenience of users who type in paths.
-                # Repair the path and redirect.  This should not happen for
-                # URLs within the site.
-                redirect(request.get_path() + "/", permanent=True)
-                return None
-
-            else:
-                # Automatic redirects disabled or there is form data.  If
-                # there is form data then the programmer is using the
-                # wrong path.  A redirect won't work if the form data came
-                # from a POST anyhow.
-                raise errors.TraversalError(
-                    "object is neither callable nor string "
-                    "(missing trailing slash?)",
-                    private_msg=repr(object),
-                    path=path)
-        else:
-            raise errors.TraversalError(
-                "object is neither callable nor string",
-                private_msg=repr(object),
-                path=path)
-
-    return object
-
-
-def _lookup_export(name, exports):
-    """Search an exports list for a name.  Returns the internal name for
-    'name' or return None if 'name' is not in 'exports'.
-
-    Each element of the export list can be either a string or a 2-tuple
-    of strings that maps an external name into internal name.  The
-    mapping is useful when the desired external name is not a valid
-    Python identifier.
-    """
-    for value in exports:
-        if value == name:
-            internal_name = name
-            break
-        elif isinstance(value, tuple):
-            if value[0] == name:
-                internal_name = value[1] # internal name is different
-                break
-    else:
-        if name == '_q_index':
-            internal_name = name # _q_index does not need to be in exports list
-        else:
-            internal_name = None # not found in exports
-    return internal_name
-
-
-def _get_component(container, component, path, request, namespace_stack):
-    """Get one component of a path from a namespace.
-    """
-    # First security check: if the container doesn't even have an
-    # _q_exports list, fail now: all Quixote-traversable namespaces
-    # (modules, packages, instances) must have an export list!
-    if not hasattr(container, '_q_exports'):
-        raise errors.TraversalError(
-                    private_msg="%r has no _q_exports list" % container)
-
-    # Second security check: call _q_access function if it's present.
-    if hasattr(container, '_q_access'):
-        # will raise AccessError if access failed
-        container._q_access()
-
-    # Third security check: make sure the current name component
-    # is in the export list or is '_q_index'.  If neither
-    # condition is true, check for a _q_lookup() and call it.
-    # '_q_lookup()' translates an arbitrary string into an object
-    # that we continue traversing.  (This is very handy; it lets
-    # you put user-space objects into your URL-space, eliminating
-    # the need for digging ID strings out of a query, or checking
-    # PATHINFO after Quixote's done with it.  But it is a
-    # compromise to security: it opens up the traversal algorithm
-    # to arbitrary names not listed in _q_exports!)  If
-    # _q_lookup() doesn't exist or is None, a TraversalError is
-    # raised.
-
-    # Check if component is in _q_exports.  The elements in
-    # _q_exports can be strings or 2-tuples mapping external names
-    # to internal names.
-    if component in container._q_exports or component == '_q_index':
-        internal_name = component
-    else:
-        # check for an explicit external to internal mapping
-        for value in container._q_exports:
-            if isinstance(value, tuple):
-                if value[0] == component:
-                    internal_name = value[1]
-                    break
-        else:
-            internal_name = None
-
-    if internal_name is None:
-        # Component is not in exports list.
-        object = None
-        if hasattr(container, "_q_lookup"):
-            object = container._q_lookup(component)
-        elif hasattr(container, "_q_getname"):
-            warnings.warn("_q_getname() on %s used; should "
-                          "be replaced by _q_lookup()" % type(container))
-            object = container._q_getname(component)
-        if object is None:
-            raise errors.TraversalError(
-                private_msg="object %r has no attribute %r" % (
-                                                    container,
-                                                    component))
-
-    # From here on, you can assume that the internal_name is not None
-    elif hasattr(container, internal_name):
-        # attribute is in _q_exports and exists
-        object = getattr(container, internal_name)
-
-    elif internal_name == '_q_index':
-        if hasattr(container, "_q_lookup"):
-            object = container._q_lookup("")
-        else:
-            raise errors.AccessError(
-                private_msg=("_q_index not found in %r" % container))
-
-    elif hasattr(container, "_q_resolve"):
-        object = container._q_resolve(internal_name)
-        if object is None:
-            raise RuntimeError, ("component listed in _q_exports, "
-                                 "but not returned by _q_resolve(%r)"
-                                 % internal_name)
-        else:
-            # Set the object, so _q_resolve won't need to be called again.
-            setattr(container, internal_name, object)
-
-    elif type(container) is types.ModuleType:
-        # try importing it as a sub-module.  If we get an ImportError
-        # here we don't catch it.  It means that something that
-        # doesn't exist was exported or an exception was raised from
-        # deeper in the code.
-        mod_name = container.__name__ + '.' + internal_name
-        object = _get_module(mod_name)
-
-    else:
-        # a non-existent attribute is in _q_exports,
-        # and the container is not a module.  Give up.
-        raise errors.TraversalError(
-                private_msg=("%r in _q_exports list, "
-                             "but not found in %r" % (component,
-                                                      container)))
-
-    namespace_stack.append(object)
-    return object
-
-
-
 # Publisher singleton, only one of these per process.
 _publisher = None
 
@@ -769,7 +494,3 @@
         return None
     else:
         return session.user
-
-
-def isstring(x):
-    return isinstance(x, (basestring, htmltext))

Modified: trunk/quixote/server/medusa_http.py
===================================================================
--- trunk/quixote/server/medusa_http.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/server/medusa_http.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -126,6 +126,7 @@
 def main():
     from quixote import enable_ptl
     enable_ptl()
+    from quixote.demo import DemoUI
 
     if len(sys.argv) == 2:
         port = int(sys.argv[1])
@@ -133,7 +134,7 @@
         port = 8080
     print 'Now serving the Quixote demo on port %d' % port
     server = http_server.http_server('', port)
-    publisher = Publisher('quixote.demo', display_exceptions='plain')
+    publisher = Publisher(DemoUI(), display_exceptions='plain')
     dh = QuixoteHandler(publisher, 'Quixote/demo', server)
     server.install_handler(dh)
     asyncore.loop()

Modified: trunk/quixote/server/twisted_http.py
===================================================================
--- trunk/quixote/server/twisted_http.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/server/twisted_http.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -239,7 +239,7 @@
         return p
 
 
-def Server(namespace, http_port, **kwargs):
+def Server(root_directory, http_port, **kwargs):
     from twisted.internet import reactor
     from quixote.publish import Publisher
 
@@ -255,7 +255,7 @@
     ##        ctx.use_privatekey_file('/path/to/pem/encoded/ssl_key_file')
     ##        return ctx
 
-    publisher = Publisher(namespace, **kwargs)
+    publisher = Publisher(root_directory, **kwargs)
     qf = QuixoteFactory(publisher)
 
     reactor.listenTCP(http_port, qf)
@@ -264,8 +264,8 @@
     return reactor
 
 
-def run(namespace, port, **kwargs):
-    app = Server(namespace, port, **kwargs)
+def run(root_directory, port, **kwargs):
+    app = Server(root_directory, port, **kwargs)
     app.run()
 
 

Modified: trunk/quixote/session.py
===================================================================
--- trunk/quixote/session.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/session.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -337,8 +337,7 @@
         self._set_cookie(session_id)
 
     def revoke_session_cookie(self):
-        """revoke_session_cookie()
-
+        """
         Remove the session cookie from the remote user's session by
         resetting the value and maximum age in the response object.  Also
         remove the cookie from the request so that further processing of
@@ -349,8 +348,7 @@
             del get_request().cookies[cookie_name]
 
     def expire_session(self):
-        """expire_session()
-
+        """
         Expire the current session, ie. revoke the session cookie from
         the client and remove the session object from the session
         manager and from the current request.

Modified: trunk/quixote/util.py
===================================================================
--- trunk/quixote/util.py	2004-10-25 20:40:06 UTC (rev 25421)
+++ trunk/quixote/util.py	2004-10-25 21:04:08 UTC (rev 25422)
@@ -9,7 +9,7 @@
   StaticFile            : Wraps a file from a filesystem as a
                           Quixote resource.
   StaticDirectory       : Wraps a directory containing static files as
-                          a Quixote namespace.
+                          a Quixote directory.
 
 StaticFile and StaticDirectory were contributed by Hamish Lawson.
 See doc/static-files.txt for examples of their use.
@@ -26,6 +26,7 @@
 from rfc822 import formatdate
 import quixote
 from quixote import errors
+from quixote.directory import Directory
 from quixote.html import htmltext, TemplateIO
 from quixote.http_response import Stream
 
@@ -189,13 +190,13 @@
         return FileStream(open(self.path, 'rb'), stat.st_size)
 
 
-class StaticDirectory:
+class StaticDirectory(Directory):
 
     """
-    Wrap a filesystem directory containing static files as a Quixote namespace.
+    Wrap a filesystem directory containing static files as a Quixote directory.
     """
 
-    _q_exports = []
+    _q_exports = ['']
 
     FILE_CLASS = StaticFile