Commits

Thomas Lotze committed 4093fb8 Merge

merged wsig-only branch to default

  • Participants
  • Parent commits 9e40ad7, 41ff5aa

Comments (0)

Files changed (61)

 
 bin
 build
-buildout.cfg
 develop-eggs
 dist
-doc/ophelia.*
 eggs
 .installed.cfg
+local.cfg
 parts
     https://bitbucket.org/tlotze/ophelia/raw/tip/CHANGES.txt
 
 :Support the project:
-   .. image:: flattr-badge-large.png
+   .. image:: http://api.flattr.com/button/flattr-badge-large.png
        :alt: Flattr this
        :target:
           http://flattr.com/thing/515106/Ophelia-build-web-sites-from-templates-with-zero-code-repetition
-=======
-Changes
-=======
+======================
+Change log for Ophelia
+======================
 
 0.4 (unreleased)
 ================
 
+- Use xsendfile to deliver files from disk in the event that the requested
+  path couldn't be traversed using the templates. This makes Ophelia
+  independent of a front-end server for delivering on-disk documents. As a
+  small trade-off, having directory listings created dynamically is presently
+  not possible at all, other than explicitly by writing templates and scripts.
+
+- Dropped anything related to Apache and mod_python, continue WSGI-only.
+
+- Dropped support for Python 2.4 and 2.5. This means the wsgiref module is a
+  part of the Python standard library so the egg's ``wsgiref`` extra is gone.
+
+- Dropped the version restriction on zope.tal, updated tests. Page templates
+  no longer grow a trailing new-line character when rendering themselves.
+
+- Dropped ``ophelia.tool.feed`` since it was never properly designed, tested
+  nor even documented.
+
+- Dropped the ``ophelia-dump`` script which has never been really useful.
+
+- Simplified the ``ophelia-wsgiref`` example server so that it no longer reads
+  a particular section of its configuration file; have Ophelia's development
+  buildout create a separate configuration file for the included example site.
+
+- Added a paste application factory and some example configuration for running
+  Ophelia in a WSGI container configured by a paste "ini" file.
+
+- Allow configuration settings to be passed to the WSGI application upon
+  initialisation.
+
+- Added a debug mode for the WSGI application.
+
+- Declared the ``ophelia`` package a namespace to allow contribution packages
+  to be called ``ophelia.xxx``.
+
+- Deprecated the ``tools`` package as it was never designed for general use.
+  Need to keep them around for the sake of some known web sites using them.
+
+- Re-organised the small example site that comes with the source.
+
+- Re-organised tests, added programmatic tests for the WSGI interface.
+
 - Use Python's own ``doctest`` module for tests, got rid of using
   zope.testing's deprecated fork of it and thus the zope.testing dependency.
 
+- Applied most of the conventions of ``tl.pkg`` to the package source.
+
+- Made sure that HTTP response headers are of type ``str``.
+
 - Fixed the locale setting for date formatting introduced in 0.3.5.
 
+- Fixed the target location link inside the response body of WSGI redirects.
+
 
 0.3.5 (2012-02-18)
 ==================
-Copyright (c) 2005-2008 Thomas Lotze
+Copyright (c) 2005-2012 Thomas Lotze
 All Rights Reserved.
 
 This software is subject to the provisions of the Zope Public License,
-===================
-Overview of Ophelia
-===================
+===================================================================
+Ophelia – build a web site from templates with zero code repetition
+===================================================================
 
 Ophelia creates XHTML pages from templates written in TAL, the Zope Template
 Attribute Language. It is designed to reduce code repetition to zero.
 
-The package contains both a WSGI application running Ophelia as well as a
-request handler for mod_python, the Python module for the Apache2 web server.
+Ophelia is a WSGI application. The package includes a wsgiref-based server
+configured to run Ophelia as well as an application factory for use with
+paster.
+
+The package requires Python 2.6 or 2.7.
 
 Documentation files cited below can be found inside the package directory,
 along with a number of doctests for the modules.
 After you installed Ophelia and wrote some templates, how can you make it
 render web pages?
 
-Use Ophelia with Apache
-    The Python package contains a module ``ophelia.modpython`` that provides a
-    request handler for the mod_python Apache module.
-
 Use Ophelia as a WSGI application
     Ophelia defines an application class compliant with the WSGI standard,
     :PEP:`333`: ``ophelia.wsgi.Application``. You can either try it by running
     Ophelia's own wsgiref-based HTTP server or run it by any WSGI server you
     might care to use.
 
-    The wsgiref-based server is installed as the ``ophelia-wsgiref``
-    executable if Ophelia is installed as an egg with the "wsgiref" extra
-    enabled. Its script entry point is ``ophelia.wsgi.wsgiref_server``.
+Try the wsgiref-based server that comes with Ophelia
+    A rather simplistic and non-production-ready wsgiref-based server set up
+    to use the provided WSGI application is installed as the
+    ``ophelia-wsgiref`` executable. Its script entry point is
+    ``ophelia.wsgi.wsgiref_server``.
 
-Dump single pages to stdout
-    An executable which is always installed with the ophelia egg is
-    ``ophelia-dump``. This script has Ophelia render the response
-    corresponding to the path you specify, and prints it to ``sys.stdout``,
-    optionally with HTTP headers. The script's entry point is
-    ``ophelia.dump.dump``.
+    The script provides some usage instructions when called with the
+    ``--help`` option. It reads a configuration file; see CONFIGURATION.txt
+    for details.
 
-Both scripts provide some usage instructions when called with the ``--help``
-option. They read a configuration file; see CONFIGURATION.txt for details.
+Use paster to plug the application into a WSGI server
+    Ophelia provides a ``paste.app_factory#main`` entry point at
+    ``ophelia.wsgi.paste_app_factory``. This can be used to run Ophelia inside
+    any WSGI server that can read paste "ini" files. See CONFIGURATION.txt for
+    an example.
 
 
 What kind of sites is Ophelia good for?
 also import modules, define functions, access the file system, and generally
 do anything a Python program can do.
 
+Documents on disk
+-----------------
+
+Generally, a site will include documents that cannot be assembled from
+templates as described above. These are assets like images, javascript files
+and style sheets as well as pages that, e.g., may have been exported by some
+other system such as a source-code documentation generator.
+
+In order to mix such content into the URL space of an Ophelia-generated site,
+the template hierarchy must omit the relevant paths and a second directory
+hierarchy which directly corresponds to the URL-space needs to contain the
+documents to be delivered from disk. If Ophelia then finds that it cannot
+serve a request using the templates, it will fall back to the on-disk
+documents. Only if the latter do not include a file corresponding to the
+requested URL will a "404 Not found" error response be sent.
+
 
 How Ophelia behaves
 ===================
 segments which are not at the end of the path. If the URL is changed by these
 rules, Ophelia redirects the browser accordingly.
 
-The mod_python handler
-----------------------
-
-Apache2 processes a request in phases, each of which can be handled by modules
-such as mod_python. Ophelia provides a mod_python handler for the content
-generation phase. If a requested URL is configured to be handled by Ophelia,
-the handler tries to find appropriate templates in the file system, and build
-a page from them.
-
-Ophelia's mod_python handler never causes a File Not Found HTTP error.
-Instead, it passes control back to Apache and other modules if it finds it
-can't build a particular resource. Apache falls back to serving static content
-from disk in that case. Ophelia can thus be installed on top of a static site
-to handle just those requests for which templates exist in the template
-directory.
-
 
 Languages and APIs used in templates and scripts
 ================================================
 For WSGI, the web server gateway interface, see
 <http://www.python.org/dev/peps/pep-0333/>.
 
-For the mod_python API, see
-<http://www.modpython.org/live/current/doc-html/>.
-
 For the Ophelia API and predefined script and template variables, see API.txt.
 
 

base.cfg

-[buildout]
-develop = .
-versions = versions
-parts = ophelia apachesite test sphinx
-unzip = true
-allow-picked-versions = false
-
-[versions]
-Jinja2 = 2.5.5
-Pygments = 1.4
-RestrictedPython = 3.6.0
-docutils = 0.7
-pkginfo = 0.8
-pytz = 2011e
-repoze.sphinx.autointerface = 0.6.2
-setuptools = 0.6c11
-sphinx = 1.0.7
-tl.buildout-apache = 0.3
-tl.eggdeps = 0.4
-virtualenv = 1.6
-wsgiref = 0.1.2
-zc.recipe.cmmi = 1.3.4
-zope.browser = 1.3
-zope.component = 3.10.0
-zope.configuration = 3.7.4
-zope.contenttype = 3.5.3
-zope.event = 3.5.0-1
-zope.exceptions = 3.6.1
-zope.i18n = 3.7.4
-zope.i18nmessageid = 3.5.3
-zope.interface = 3.6.1
-zope.location = 3.9.0
-zope.pagetemplate = 3.5.2
-zope.proxy = 3.6.1
-zope.publisher = 3.12.6
-zope.schema = 3.8.0
-zope.security = 3.8.0
-zope.tal = 3.4.1
-zope.tales = 3.5.1
-zope.testrunner = 4.0.3
-zope.traversing = 3.14.0
-
-[ophelia]
-recipe = zc.recipe.egg
-eggs = ophelia [wsgiref]
-       tl.eggdeps
-interpreter = py
-arguments = config_file="${buildout:directory}/.installed.cfg",
-            section="ophelia"
-template-root = ${buildout:directory}/example/ophelia_pages
-site = http://${ophelia:host}:${ophelia:port}/
-
-[apache]
-recipe = tl.buildout_apache:httpd
-
-[mod-python]
-recipe = tl.buildout_apache:modpython
-httpd = apache
-
-[apachesite]
-recipe = tl.buildout_apache:root
-httpd = apache
-python = mod-python
-modules = authz_host dir autoindex
-htdocs = example/static
-eggs = ophelia
-extra-config =
-    PythonInterpreter main_interpreter
-    PythonOption template_root ${ophelia:template-root}
-    PythonOption site http://${apachesite:servername}/
-    PythonFixupHandler ophelia.modpython
-
-[test]
-recipe = zc.recipe.testrunner
-eggs = ophelia [test]
-defaults = ["-v", "-c", "-s", "ophelia"]
-
-[sphinx]
-recipe = zc.recipe.egg
-eggs = sphinx
-       repoze.sphinx.autointerface
-       pkginfo
-       ophelia
-scripts = sphinx-build
-arguments = argv=sys.argv+("-E -c doc/ -d build/sphinx/doctrees"
-                           " . build/sphinx/html/").split()
 ##############################################################################
 #
-# Copyright (c) 2006 Zope Foundation and Contributors.
+# Copyright (c) 2006 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
 is_jython = sys.platform.startswith('java')
 
 # parsing arguments
-parser = OptionParser(
-    'This is a custom version of the zc.buildout %prog script.  It is '
-    'intended to meet a temporary need if you encounter problems with '
-    'the zc.buildout 1.5 release.')
-parser.add_option("-v", "--version", dest="version", default='1.4.4',
-                          help='Use a specific zc.buildout version.  *This '
-                          'bootstrap script defaults to '
-                          '1.4.4, unlike usual buildpout bootstrap scripts.*')
+parser = OptionParser()
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
 parser.add_option("-d", "--distribute",
-                   action="store_true", dest="distribute", default=False,
+                   action="store_true", dest="distribute", default=True,
                    help="Use Disribute rather than Setuptools.")
 
 parser.add_option("-c", None, action="store", dest="config_file",
     def quote (c):
         return c
 
+cmd = 'from setuptools.command.easy_install import main; main()'
 ws  = pkg_resources.working_set
 
 if USE_DISTRIBUTE:
 else:
     requirement = 'setuptools'
 
-env = dict(os.environ,
-           PYTHONPATH=
-           ws.find(pkg_resources.Requirement.parse(requirement)).location
-           )
-
-cmd = [quote(sys.executable),
-       '-c',
-       quote('from setuptools.command.easy_install import main; main()'),
-       '-mqNxd',
-       quote(tmpeggs)]
-
-if 'bootstrap-testing-find-links' in os.environ:
-    cmd.extend(['-f', os.environ['bootstrap-testing-find-links']])
-
-cmd.append('zc.buildout' + VERSION)
-
 if is_jython:
     import subprocess
-    exitcode = subprocess.Popen(cmd, env=env).wait()
-else: # Windows prefers this, apparently; otherwise we would prefer subprocess
-    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
-assert exitcode == 0
+
+    assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
+           quote(tmpeggs), 'zc.buildout' + VERSION],
+           env=dict(os.environ,
+               PYTHONPATH=
+               ws.find(pkg_resources.Requirement.parse(requirement)).location
+               ),
+           ).wait() == 0
+
+else:
+    assert os.spawnle(
+        os.P_WAIT, sys.executable, quote (sys.executable),
+        '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION,
+        dict(os.environ,
+            PYTHONPATH=
+            ws.find(pkg_resources.Requirement.parse(requirement)).location
+            ),
+        ) == 0
 
 ws.add_entry(tmpeggs)
 ws.require('zc.buildout' + VERSION)
+[buildout]
+extends = versions/versions.cfg
+develop = .
+parts =
+    ophelia
+    test
+    doc
+    example-wsgiref-cfg
+    example-paster-server
+    example-paster-ini
+unzip = true
+
+[ophelia]
+recipe = zc.recipe.egg
+eggs = ophelia
+       tl.eggdeps
+interpreter = py
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ophelia [test]
+
+[doc]
+recipe = zc.recipe.egg
+eggs =
+    tl.pkg [doc]
+    ophelia
+    repoze.sphinx.autointerface
+scripts = doc
+
+[example-config]
+example_dir = ${buildout:directory}/example
+host = localhost
+port = 2080
+
+[example-wsgiref-cfg]
+recipe = collective.recipe.template
+input = ${:example_dir}/wsgiref.cfg.in
+output = ${buildout:parts-directory}/example/wsgiref.cfg
+<= example-config
+
+[example-paster-server]
+recipe = zc.recipe.egg
+eggs = ophelia
+       PasteDeploy
+       PasteScript
+scripts = paster
+
+[example-paster-ini]
+recipe = collective.recipe.template
+input = ${:example_dir}/paste.ini.in
+output = ${buildout:parts-directory}/example/paste.ini
+<= example-config

buildout.cfg.example

-[buildout]
-extends = base.cfg
-
-[apachesite]
-listen = 127.0.0.1:1080
-servername = localhost:1080
-
-[ophelia]
-host = 127.0.0.1
-port = 2080
-site = http://localhost:2080/
 .. include:: ../ABOUT.txt
-
-
-====
-
-
-.. toctree::
-
-    ../ROADMAP.txt
-    ../CHANGES.txt
-
-
-====
-
-
 .. literalinclude:: ../COPYRIGHT.txt
 .. literalinclude:: ../LICENSE.txt
 
+=============
+API reference
+=============
+
+.. autosummary::
+    :toctree: ./_api/
+
+    ophelia.request
+    ophelia.input
+    ophelia.pagetemplate
+    ophelia.util
+    ophelia.wsgi
+
+
+.. Local Variables:
+.. mode: rst
+.. End:
+.. include:: ../CHANGES.txt
-# -*- coding: utf-8 -*-
-#
-# documentation build configuration file
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# The contents of this file are pickled, so don't put values in the namespace
-# that aren't pickleable (module imports are okay, they're removed automatically).
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
+# coding: utf-8
+# Copyright (c) 2012 Thomas Lotze
+# See also LICENSE.txt
 
-import sys, os
-import datetime
-import pkginfo
+import os
+import os.path
+import tl.pkg.sphinxconf
 
-dist = pkginfo.Develop('..')
 
-# If your extensions are in another directory, add it 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('.'))
+_year_started = 2006
+_bitbucket_name = 'ophelia'
 
-# General configuration
-# ---------------------
+# XXX We need to hack around the assumption of a namespaced package made by
+# tl.pkg 0.1.
 
-# 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.autosummary', 'repoze.sphinx.autointerface']
+tl.pkg.sphinxconf.link_text_files_from_source = lambda project: None
 
-autosummary_generate = ['doc/modules.txt']
+tl.pkg.sphinxconf.set_defaults()
 
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['templates']
+for name in os.listdir(os.path.join('..', project)):
+    if not name.endswith('txt'):
+        continue
+    os.symlink(os.path.join('..', project, name),
+               '%s-%s' % (project, name))
 
-# The suffix of source filenames.
-source_suffix = '.txt'
+# XXX hack ends here
 
-# The encoding of source files.
-#source_encoding = 'utf-8'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = dist.name
-copyright = u'2006-%s %s' % (datetime.date.today().year, dist.author)
-
-# 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 = []
-for x in dist.version:
-    try:
-        version.append(str(int(x)))
-    except ValueError:
-        break
-version = '.'.join(version)
-# The full version, including alpha/beta/rc tags.
-release = dist.version
-
-# 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 = ['ABOUT', 'COPYRIGHT', 'LICENSE']
-
-# List of directories, relative to source directory, that shouldn't be searched
-# for source files.
-exclude_trees = ['build', 'parts', dist.name+'.egg-info', 'example', '.hg']
-
-# 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'
-
-# Options for HTML output
-# -----------------------
-
-# The style sheet to use for HTML and HTML Help pages. A file of that name
-# must exist either in Sphinx' static/ path, or in one of the custom paths
-# given in html_static_path.
-html_style = 'default.css'
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-#html_static_path = ['static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-html_sidebars = {
-    '**': ['project-links.html', 'localtoc.html', 'relations.html',
-           'sourcelink.html', 'searchbox.html'],
-}
-
-# 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, the reST sources are included in the HTML build as _sources/<name>.
-#html_copy_source = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = dist.name + '-doc'
-
-
-# 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, document class [howto/manual]).
-latex_documents = [
-  ('README', dist.name+'.tex', dist.name+ur' Documentation',
-   dist.author, 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_use_modindex = True
+extensions.append('repoze.sphinx.autointerface')
+==============================
+Ophelia documentation contents
+==============================
+
+.. toctree::
+    :maxdepth: 2
+
+    overview
+    ophelia-CONFIGURATION
+    ophelia-API
+
+    narrative
+    interfaces
+    api
+
+    about
+    roadmap
+    changes
+
+
+.. Local Variables:
+.. mode: rst
+.. End:

doc/interfaces.txt

 Interface definitions
 =====================
 
-:mod:`ophelia.interfaces` -- Public interfaces used in Ophelia
-==============================================================
+Public interfaces used in Ophelia
+=================================
 
 .. automodule:: ophelia.interfaces
     :synopsis: Public interfaces used in Ophelia.

doc/modules.txt

-=======
-Modules
-=======
-
-.. autosummary::
-    :toctree: ./
-
-    ophelia.request
-    ophelia.input
-    ophelia.pagetemplate
-    ophelia.util
-    ophelia.dump
-    ophelia.wsgi
-    ophelia.modpython
-    ophelia.tool.metadata
-    ophelia.tool.navigation
-
-
-.. Local Variables:
-.. mode: rst
-.. End:

doc/narrative.txt

+=======================
+Narrative documentation
+=======================
+
+.. toctree::
+    :maxdepth: 2
+
+    ophelia-request
+    ophelia-input
+    ophelia-pagetemplate
+    ophelia-util
+
+
+.. Local Variables:
+.. mode: rst
+.. End:
+.. include:: ../README.txt

doc/requirements.pip

+pkginfo
+sphinxcontrib-cheeseshop
+sphinxcontrib-issuetracker
+tl.pkg
+.. include:: ../ROADMAP.txt

doc/unittests.txt

-==========
-Unit tests
-==========
-
-.. toctree::
-    :maxdepth: 2
-    :glob:
-
-    ../ophelia/request
-    ../ophelia/input
-    ../ophelia/pagetemplate
-    ../ophelia/util
-
-
-.. Local Variables:
-.. mode: rst
-.. End:

example/documents/example.css

+body {
+     background-color: #fc3;
+     color: black;
+}
+
+.boxed {
+     border:1px solid red;
+}

example/documents/foo/baz.html

+<html>
+  <head>
+    <title>baz</title>
+  </head>
+  <body>
+    <p>
+      This page was not built by Ophelia.
+    </p>
+  </body>
+</html>

example/ophelia_pages/__init__

-site = __request__.site
-
-<?xml?>
-<!DOCTYPE html 
-          PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-  <head>
-    <link rel="stylesheet" type="text/css" href=""
-          tal:attributes="href string:${site}example.css"/>
-
-    <title tal:content="title" />
-  </head>
-
-  <body>
-    <h1 tal:content="title" />
-
-    <div tal:replace="structure innerslot" />
-  </body>
-</html>

example/ophelia_pages/foo/__init__

-<div class="boxed"
-     tal:content="structure innerslot" />

example/ophelia_pages/foo/bar.html

-import datetime
-
-title = "bar page"
-now = datetime.datetime.now()
-
-<?xml?>
-<p>
-  This is the bar page of the foo folder. The current date and time is
-  <span tal:replace="now" />.
-</p>

example/ophelia_pages/foo/index.html

-title = "index page"
-
-<?xml?>
-<p>
-  This is the index page of the foo folder.
-</p>

example/ophelia_pages/index.html

-title = "Ophelia example site"
-
-<?xml?>
-<p>
-  This is the index page of the
-  <a href="http://www.thomas-lotze.de/software/ophelia/">Ophelia</a>
-  example site located at
-  <a href="http://www.thomas-lotze.de/software/ophelia/example/">
-    http://www.thomas-lotze.de/software/ophelia/example/</a>.
-</p>
-
-<p>
-  Take a look at the <a href="foo/">foo</a>, <a href="foo/bar.html">foo/bar</a>,
-  and <a href="foo/baz.html">foo/baz</a> pages as well as the
-  <a href="asdf/">asdf</a> folder.
-</p>
-
-<p>
-  This hypothetical Apache2 configuration would publish the example site at
-  its proper URL, .../ophelia/ being the file system path to a copy of the
-  Ophelia source:
-</p>
-
-<pre>
-Alias /software/ophelia/example .../ophelia/example/static
-&lt;Location "/software/ophelia/example"&gt;
-    PythonInterpreter main_interpreter
-    PythonPath "['.../ophelia'] + sys.path"
-    PythonOption template_root .../ophelia/example/ophelia_pages
-    PythonOption site http://www.thomas-lotze.de/software/ophelia/example
-    PythonFixupHandler ophelia.modpython
-&lt;/Location&gt;
-</pre>

example/paste.ini.in

+[server:main]
+use = egg:paste#http
+host = ${host}
+port = ${port}
+
+[app:main]
+use = egg:ophelia
+set template_root = ${example_dir}/templates
+set document_root = ${example_dir}/documents
+set site = http://${host}:${port}/

example/static/asdf/README.txt

-This directory doesn't have an Ophelia index template. A directory index
-listing this README.txt file should be generated by Apache instead.

example/static/example.css

-body {
-     background-color: #fc3;
-     color: black;
-}
-
-.boxed {
-     border:1px solid red;
-}

example/static/foo/baz.html

-<html>
-  <head>
-    <title>baz</title>
-  </head>
-  <body>
-    <p>
-      This page was not built by Ophelia.
-    </p>
-  </body>
-</html>

example/templates/__init__

+site = __request__.site
+
+<?xml?>
+<!DOCTYPE html 
+          PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <link rel="stylesheet" type="text/css" href=""
+          tal:attributes="href string:${site}example.css"/>
+
+    <title tal:content="title" />
+  </head>
+
+  <body>
+    <h1 tal:content="title" />
+
+    <div tal:replace="structure innerslot" />
+  </body>
+</html>

example/templates/foo/__init__

+<div class="boxed"
+     tal:content="structure innerslot" />

example/templates/foo/bar.html

+import datetime
+
+title = "bar page"
+now = datetime.datetime.now()
+
+<?xml?>
+<p>
+  This is the bar page of the foo folder. The current date and time is
+  <span tal:replace="now" />.
+</p>

example/templates/foo/index.html

+title = "index page"
+
+<?xml?>
+<p>
+  This is the index page of the foo folder.
+</p>

example/templates/index.html

+title = "Ophelia example site"
+
+<?xml?>
+<p>
+  This is the index page of the
+  <a href="http://www.thomas-lotze.de/software/ophelia/">Ophelia</a>
+  example site located at
+  <a href="http://www.thomas-lotze.de/software/ophelia/example/">
+    http://www.thomas-lotze.de/software/ophelia/example/</a>.
+</p>
+
+<p>
+  Take a look at the <a href="foo/">foo</a>, <a href="foo/bar.html">foo/bar</a>,
+  and <a href="foo/baz.html">foo/baz</a> pages.
+</p>

example/wsgiref.cfg.in

+[DEFAULT]
+host = ${host}
+port = ${port}
+template_root = ${example_dir}/templates
+document_root = ${example_dir}/documents
+site = http://${host}:${port}/

index.txt

-Ophelia documentation contents
-==============================
-
-.. toctree::
-    :maxdepth: 2
-
-    README
-    ophelia/CONFIGURATION
-    ophelia/API
-
-    doc/interfaces
-    doc/modules
-    doc/unittests
-
-    doc/about
-
-
-Indices and tables
-==================
-
-- :ref:`genindex`
-- :ref:`modindex`
-- :ref:`search`
-
-
-.. Local Variables:
-.. mode: rst
-.. End:

ophelia/CONFIGURATION.txt

 Configuring Ophelia
 ===================
 
+Configuring Ophelia involves a number of settings variables. They can be
+passed as a dictionary to the Ophelia WSGI application,
+``ophelia.wsgi.Application``, upon instantiation. The package also comes with
+a console script that runs a wsgiref-based server and reads the same settings
+variables from an ini-style configuration file, as well as a paste application
+factory that lets Ophelia be plugged into any WSGI server that can handle a
+paste "ini" file.
 
-Configuration files
+Regardless of how the WSGI application is being served, all settings may also
+be overridden by accordingly named variables set in the WSGI environment of
+each individual request.
+
+The settings variables will end up in the environment namespace stored on the
+request object. It is possible to set any number of other variables that are
+not recognized by Ophelia using the same configuration mechanism. This allows
+configuring Python modules and scripts that belong to your site's content.
+
+The rest of this section contains a description of all variables as well as an
+example of a configuration file for the wsgiref server.
+
+
+Basic configuration
 ===================
 
-Ophelia comes with a number of clients that all exercise its request handler,
-but have different configuration mechanisms. The wsgiref-based HTTP server and
-the ``ophelia-dump`` script read their configuration from a section of an
-INI-style configuration file while the mod_python handler gets the same
-information from PythonOption variables set in the Apache configuration.
-Example configurations can be found below.
-
 Two variables must always be present:
 
 :template_root:
 
 In addition, the variables described below may be specified in order to
 influence the request handler's behaviour. All of them have sensible default
-values. They will end up in the environment namespace stored on the request
-object, along with any other variables that are not recognized by Ophelia.
-This allows configuring Python modules and scripts that belong to your site's
-content.
-
-While the above pertains to all three clients, the wsgiref server needs
-additional information:
-
-:host:
-    The network interface to bind to.
-
-:port:
-    The TCP port to listen at on that interface.
+values.
 
 For boolean variables such as redirect_index, the values "on", "true", or
 "yes" (case-insensitive) are taken to mean True, anything else means False.
 
+:debug:
+    Whether to include debug information such as formatted tracebacks in error
+    responses. This option is turned off by default.
+
+
+Delivery of documents straight from disk
+========================================
+
+Generally, a site includes documents that are not assembled from templates.
+They reside within a directory tree of their own whose root needs to be
+configured if needed. Also, if Ophelia sits behind a reverse proxy, documents
+from disk may be delivered more efficiently by using something like the
+X-Sendfile mechanism:
+
+:document_root:
+    Optional, the file system path to the on-disk documents root directory.
+
+:xsendfile:
+    One of "standard", "nginx" or "serve". Defaults to "serve" which means
+    on-disk documents are delivered directly by the Ophelia application. The
+    "standard" value causes an X-Sendfile header to be sent (such as, e.g.,
+    Apache understands) while the "nginx" value causes an X-Accel-Redirect
+    header for consumption by an nginx reverse proxy to be sent instead. In
+    the latter case, the nginx server needs to have the ``/-internal-/``
+    location configured as an "internal" alias of the document root directory.
+
 
 URL canonicalization and redirection
 ====================================
     response content.
 
 
-Example configuration for an Apache/mod_python installation
-===========================================================
-
-Assume that the root of your site is published at
-``<http://www.example.com/foo/bar/>``.
-
-Assume further that your site uses the following file system locations on a
-Unix system:
-
-:/var/example/templates/: for the tree of Ophelia templates
-:/var/example/python/:    for the Python packages
-:/var/example/static/:    for the static stuff
-
-To publish this site, add the following to your host's config::
-
-    Alias /foo/bar /var/example/static
-    <Location "/foo/bar">
-        PythonInterpreter main_interpreter
-        PythonPath "['/var/example/python'] + sys.path"
-        PythonOption template_root /var/example/templates
-        PythonOption site http://www.example.com/foo/bar/
-        PythonFixupHandler ophelia.modpython
-    </Location>
-
-This instructs Apache to let Ophelia handle any URI under /foo/bar/. Ophelia
-will build pages from templates where they exist, and Apache will serve files
-from your static content otherwise.
-
-It is possible to set the Ophelia handler only for directories or HTML
-documents by applying some path name heuristics and matching the location
-against a regular expression.
-
-Due to an interaction between Python's restricted mode and how mod_python
-creates multiple Python interpreters, Ophelia must run in mod_python's main
-Python interpreter. This means that if more than one Ophelia site is hosted by
-the same Apache process, they cannot be isolated from each other.
-
-
 Example configuration for the included WSGI server
 ==================================================
 
-Create a configuration file, say, opheliasite.cfg::
+First of all, note that the wsgiref-based server may be usable for development
+but certainly not for production. Therefore, its configuration mechanism is
+rather simplistic. Create a configuration file, say, ``wsgiref.cfg``,
+containing one section named "DEFAULT" that holds all the settings::
 
     [DEFAULT]
     host = 127.0.0.1
     port = 8080
-    template_root = /var/example/opheliasite
+    template_root = /var/example/templates
     site = http://localhost:8080/
 
 and run the ophelia-wsgiref script on that file::
 
-    ophelia-wsgiref -c opheliasite.cfg
+    $ ophelia-wsgiref wsgiref.cfg
 
-You may want to wrap this call in a run-control script.
+As the example shows, the wsgiref server needs two pieces of additional
+information:
 
-The same configuration can be used with the ophelia-dump script, except that
-it doesn't require the host and port to be set.
+:host:
+    The network interface to bind to.
+
+:port:
+    The TCP port to listen at on that interface.
+
+
+Example paste configuration
+===========================
+
+The simplest possible paste configuration will just run the paste HTTP server
+on the Ophelia WSGI application, without any middleware. Create a file, say,
+``paste.ini``::
+
+    [server:main]
+    use = egg:paste#http
+    host = 127.0.0.1
+    port = 8080
+
+    [app:main]
+    use = egg:ophelia
+    set template_root = /var/example/templates
+    set document_root = /var/example/documents
+    set site = http://localhost:8080/
+
+and run a paste server from it::
+
+    $ paster serve paste.ini
+    Starting server in PID 12345.
+    serving on http://127.0.0.1:8080
 
 
 .. Local Variables:

ophelia/__init__.py

-# Copyright (c) 2006-2007 Thomas Lotze
-# See also LICENSE.txt
-
-"""Ophelia builds a web site from TAL templates with zero code repetition.
-"""
+__import__('pkg_resources').declare_namespace(__name__)

ophelia/dump.py

-# Copyright (c) 2007-2008 Thomas Lotze
-# See also LICENSE.txt
-
-"""A script entry point for dumping a page built by Ophelia to stdout.
-"""
-
-import sys
-import optparse
-import ConfigParser
-
-import zope.exceptions.exceptionformatter
-
-import ophelia.request
-
-
-def dump(config_file="", section="DEFAULT"):
-    oparser = optparse.OptionParser("usage: %prog [options] path")
-    oparser.add_option("-c", dest="config_file", default=config_file)
-    oparser.add_option("-s", dest="section", default=section)
-    oparser.add_option("-v", dest="verbose",
-                       action="store_true", default=False,
-                       help="verbose, print response headers")
-    cmd_options, args = oparser.parse_args()
-
-    if len(args) != 1:
-        sys.stderr.write("Exactly one path must be requested.")
-        return 1
-    path = args[0]
-
-    config = ConfigParser.ConfigParser()
-    config.read(cmd_options.config_file)
-    env = dict((key.replace('-', '_'), value)
-               for key, value in config.items(cmd_options.section))
-    env.setdefault('wsgi.input', sys.stdin)
-
-    if path.startswith('/'):
-        path = path[1:]
-    if '?' in path:
-        path, env["QUERY_STRING"] = path.split('?', 1)
-
-    request = ophelia.request.Request(
-        path, env.pop("template_root"), env.pop("site"), **env)
-    try:
-        headers, body = request()
-    except:
-        msg = "".join(zope.exceptions.exceptionformatter.format_exception(
-            with_filenames=True, *sys.exc_info()))
-        sys.stderr.write(msg)
-        return 1
-
-    if cmd_options.verbose:
-        sys.stdout.write(
-            '\n'.join("%s: %s" % item for item in headers.items()) +
-            "\n\n")
-    sys.stdout.write(body)

ophelia/interfaces.py

-# Copyright (c) 2007-2008 Thomas Lotze
+# Copyright (c) 2007-2011 Thomas Lotze
 # See also LICENSE.txt
 
 """Public interfaces used in Ophelia.
     env = zope.interface.Attribute(
         """Compound environment namespace.
 
-        - site configuration options (PythonOption setting if running
-          mod_python, application configuration if using WSGI)
+        - site configuration options (WSGI application configuration)
         - the CGI or WSGI environment variables passed by the server
-        - if running mod_python, the Apache request object as apache_request
         - must contain the ``wsgi.input`` variable
         - may contain the ``ophelia.response_headers`` namespace of response
           headers already set by the server environment

ophelia/modpython.py

-# Copyright (c) 2006-2009 Thomas Lotze
-# See also LICENSE.txt
-
-try:
-    from mod_python import apache, util
-except:
-    # Allow the sphinx autosummary extension to import this module.
-    pass
-
-import sys
-import os.path
-import urlparse
-
-import zope.exceptions.exceptionformatter
-
-from ophelia.request import Request, NotFound, Redirect
-from ophelia.util import Namespace
-
-
-# fix-up request handler
-def fixuphandler(apache_request):
-    """Fix-up handler setting up the Ophelia content handler iff applicable.
-
-    This handler has the Ophelia request traverse the requested URL and
-    registers the generic content handler if and only if traversal is possible
-    and the requested resource can actually be served by Ophelia. This is to
-    prevent clobbering Apache's default generic handler chain if it's needed.
-
-    never raises a 404 but declines instead
-    may raise anything else
-
-    The intent is for templates to take precedence, falling back on any static
-    content gracefully.
-    """
-    env = Namespace(apache_request.get_options())
-
-    template_root = os.path.abspath(env.pop("template_root"))
-
-    # The site URL should be something we can safely urljoin path parts to.
-    site = env.pop("site")
-    if not site.endswith('/'):
-        site += '/'
-
-    # Determine the path to traverse by the requested URL to the site root
-    # URL. We want to catch requests to the site root specified without a
-    # trailing slash.
-    site_path = urlparse.urlparse(site)[2]
-    if not (apache_request.uri == site_path[:-1] or
-            apache_request.uri.startswith(site_path)):
-        return apache.DECLINED
-    path = apache_request.uri[len(site_path):]
-
-    # Apache already maps multiple HTTP headers to a comma-separated single
-    # header according to RfC 2068, section 4.2.
-    env.update(apache.build_cgi_env(apache_request))
-    env.setdefault('wsgi.input', InputStream(apache_request))
-    env.apache_request = apache_request
-
-    # Response headers may already have been set during earlier phases of
-    # Apache request processing.
-    env['ophelia.response_headers'] = Namespace(apache_request.headers_out)
-    request = Request(path, template_root, site, **env)
-    try:
-        request.traverse()
-    except NotFound:
-        return apache.DECLINED
-    except Redirect, e:
-        apache_request.handler = "mod_python"
-        apache_request.add_handler("PythonHandler",
-                                   "ophelia.modpython::redirect")
-        apache_request.__ophelia_location__ = e.uri
-    except:
-        report_exception(apache_request)
-    else:
-        apache_request.handler = "mod_python"
-        apache_request.add_handler("PythonHandler", "ophelia.modpython")
-        apache_request.__ophelia_request__ = request
-
-    return apache.OK
-
-
-# generic request handler
-def redirect(apache_request):
-    """Generic Apache request handler doing an Ophelia traversal's redirect.
-
-    Under certain circumstances, Apache writes to the request during the
-    fix-up phase so calling modpython.util.redirect() in the fix-up handler
-    may result in an IOError since headers have supposedly already been sent.
-    The generic handler gets a new chance to do redirection, so we defer it
-    until then, using this handler.
-    """
-    util.redirect(apache_request, apache_request.__ophelia_location__,
-                  permanent=True)
-
-
-def handler(apache_request):
-    """Generic Apache request handler serving pages from Ophelia's request.
-
-    This handler is called only after it is known that the requested resource
-    can actually be served by Ophelia.
-
-    may raise anything
-    """
-    request = apache_request.__ophelia_request__
-    try:
-        response_headers, content = request.build()
-    except Redirect, e:
-        util.redirect(apache_request, e.uri, permanent=True)
-    except:
-        report_exception(apache_request)
-
-    # deliver the page
-    apache_request.content_type = request.compiled_headers["Content-Type"]
-    apache_request.set_content_length(len(content))
-    apache_request.headers_out.update(response_headers)
-
-    if apache_request.header_only:
-        apache_request.write("")
-    else:
-        apache_request.write(content)
-
-    return apache.OK
-
-
-# helpers
-def report_exception(apache_request):
-    exc_type, exc_value, traceback_info = sys.exc_info()
-
-    if apache_request.get_config().get("PythonDebug") != "1":
-        raise exc_value
-
-    msg = zope.exceptions.exceptionformatter.format_exception(
-        exc_type, exc_value, traceback_info, with_filenames=True)
-
-    apache_request.status = apache.HTTP_INTERNAL_SERVER_ERROR
-    apache_request.content_type = "text/plain"
-    apache_request.write("".join(msg).encode("utf-8"))
-
-    for entry in msg:
-        for line in entry.splitlines():
-            apache_request.log_error(line, apache.APLOG_ERR)
-
-    raise apache.SERVER_RETURN(apache.DONE)
-
-
-class InputStream(object):
-    """Wrapper for the Apache request that implements the minimal API required
-    of a WSGI-compliant input stream.
-    """
-
-    def __init__(self, apache_request):
-        self.read = apache_request.read
-        self.readline = apache_request.readline
-        self.readlines = apache_request.readlines
-
-    def __iter__(self):
-        while True:
-            line = self.readline()
-            if not line:
-                break
-            yield line

ophelia/pagetemplate.txt

 overridden by names given later:
 
 >>> pt({"title": "A title"})
-u'<h1>A title</h1>\n'
+u'<h1>A title</h1>'
 
 >>> pt(title="The same title")
-u'<h1>The same title</h1>\n'
+u'<h1>The same title</h1>'
 
 >>> pt({"title": "A title"}, {"title": "A different title"})
-u'<h1>A different title</h1>\n'
+u'<h1>A different title</h1>'
 
 >>> pt({"title": "A title"}, title="Yet another title")
-u'<h1>Yet another title</h1>\n'
+u'<h1>Yet another title</h1>'
 
 If the template can't be compiled, it raises an exception as soon as
 compilation is attempted, which is on instantiation as well as on any update:
 
 >>> pt = PageTemplate("""<hr tal:attributes="class None" />""")
 >>> pt()
-u'<hr />\n'
+u'<hr />'
 
 That predefined variable "None" can be overridden (not that that would be a
 great idea):
 
 >>> pt({"None": "asdf"})
-u'<hr class="asdf" />\n'
+u'<hr class="asdf" />'
 
 
 .. Local Variables:

ophelia/tests.py

-# Copyright (c) 2007-2012 Thomas Lotze
-# See also LICENSE.txt
-
-import doctest
-import os
-import os.path
-import unittest
-
-
-flags = (doctest.ELLIPSIS |
-         doctest.NORMALIZE_WHITESPACE |
-         doctest.REPORT_NDIFF)
-
-
-def test_suite():
-    return unittest.TestSuite([
-        doctest.DocFileSuite(filename,
-                             package="ophelia",
-                             optionflags=flags,
-                             )
-        for filename in sorted(os.listdir(os.path.dirname(__file__)))
-        if filename.endswith(".txt")
-        ])

ophelia/tests/__init__.py

Empty file added.

ophelia/tests/fixtures/documents/folder/index.html

+<html><head></head><body><p>Folder index</p></body></html>

ophelia/tests/fixtures/documents/index.html

+<html><head></head><body><p>Root index</p></body></html>

ophelia/tests/fixtures/documents/smoke-document.html

+foo = 'bar'
+<?xml?>
+<html><head></head><body><p tal:content="foo" /></body></html>

ophelia/tests/fixtures/templates/raise.html

+raise Exception('message')
+<?xml?>

ophelia/tests/fixtures/templates/redirect.html

+from ophelia.request import Redirect
+raise Redirect(path='/smoke.html')
+<?xml?>

ophelia/tests/fixtures/templates/smoke.html

+foo = 'bar'
+<?xml?>
+<html><head></head><body><p tal:content="foo" /></body></html>

ophelia/tests/test_doctests.py

+# Copyright (c) 2007-2012 Thomas Lotze
+# See also LICENSE.txt
+
+import doctest
+import os
+import os.path
+import unittest
+
+
+flags = (doctest.ELLIPSIS |
+         doctest.NORMALIZE_WHITESPACE |
+         doctest.REPORT_NDIFF)
+
+
+def test_suite():
+    return unittest.TestSuite([
+        doctest.DocFileSuite(filename,
+                             package="ophelia",
+                             optionflags=flags,
+                             )
+        for filename in sorted(
+                os.listdir(os.path.dirname(os.path.dirname(__file__))))
+        if filename.endswith(".txt")
+        ])

ophelia/tests/test_wsgi.py

+# Copyright (c) 2012 Thomas Lotze
+# See also LICENSE.txt
+
+import logging
+import ophelia.wsgi
+import os.path
+import pkg_resources
+import webtest
+
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+
+FIXTURES = pkg_resources.resource_filename('ophelia', 'tests/fixtures')
+
+
+def fixture(*parts):
+    return os.path.join(FIXTURES, *parts)
+
+
+class BasicApplicationTest(unittest.TestCase):
+
+    def setUp(self):
+        self.app = webtest.TestApp(ophelia.wsgi.Application({
+                    'site': 'http://localhost/',
+                    'template_root': fixture('templates'),
+                    }))
+        logger = logging.getLogger('ophelia')
+        for handler in list(logger.handlers):
+            if isinstance(handler, logging.StreamHandler):
+                logger.removeHandler(handler)
+
+    def test_smoke(self):
+        r = self.app.get('/smoke.html', status=200)
+        self.assertEqual(
+            'text/html; charset=utf-8', r.headers['content-type'])
+        self.assertIn('<p>bar</p>', r.body)
+
+    def test_redirect(self):
+        r = self.app.get('/redirect.html', status=301)
+        self.assertEqual('http://localhost/smoke.html', r.headers['location'])
+        self.assertEqual('text/html', r.headers['content-type'])
+        self.assertIn('<a href="http://localhost/smoke.html">'
+                      'http://localhost/smoke.html</a>', r.body)
+
+    def test_debug_mode_is_off_by_default(self):
+        r = self.app.get('/raise.html', status=500)
+        self.assertNotIn('message', r.body)
+
+    def test_debug_mode_switched_on(self):
+        r = self.app.get(
+            '/raise.html', status=500, extra_environ={'debug': 'on'})
+        self.assertIn('message', r.body)
+
+
+class OnDiskDocumentsTest(unittest.TestCase):
+
+    def setUp(self):
+        self.app = webtest.TestApp(ophelia.wsgi.Application({
+                    'site': 'http://localhost/',
+                    'template_root': fixture('templates'),
+                    'document_root': fixture('documents'),
+                    }))
+
+    def test_smoke_document(self):
+        r = self.app.get('/smoke-document.html', status=200)
+        self.assertEqual('text/html', r.headers['content-type'])
+        self.assertIn('<p tal:content="foo" />', r.body)
+
+    def test_on_disk_default_index_name(self):
+        r = self.app.get('/', status=200)
+        self.assertIn('<p>Root index</p>', r.body)
+
+    def test_redirect_on_disk_directory_without_trailing_slash(self):
+        r = self.app.get('/folder', status=301)
+        self.assertEqual('http://localhost/folder/', r.headers['location'])
+
+    def test_no_redirect_on_disk_directory_with_index_name(self):
+        r = self.app.get('/folder/index.html', status=200)
+        self.assertIn('<p>Folder index</p>', r.body)
+
+    def test_redirect_on_disk_directory_with_index_name(self):
+        r = self.app.get('/folder/index.html',
+                         extra_environ={'redirect_index': 'on'},
+                         status=301)
+        self.assertEqual('http://localhost/folder/', r.headers['location'])
+
+    def test_on_disk_index_name_is_configurable(self):
+        r = self.app.get('/',
+                         extra_environ={'index_name': 'smoke-document.html'},
+                         status=200)
+        self.assertEqual('text/html', r.headers['content-type'])
+        self.assertIn('<p tal:content="foo" />', r.body)
+
+    def test_x_sendfile_header(self):
+        r = self.app.get('/smoke-document.html',
+                         extra_environ={'xsendfile': 'standard'},
+                         status=200)
+        self.assertEqual(fixture('documents', 'smoke-document.html'),
+                         r.headers['x-sendfile'])
+        self.assertEqual('', r.body)
+
+    def test_x_accel_redirect_header(self):
+        r = self.app.get('/smoke-document.html',
+                         extra_environ={'xsendfile': 'nginx'},
+                         status=200)
+        self.assertEqual('/-internal-/smoke-document.html',
+                         r.headers['x-accel-redirect'])
+        self.assertEqual('', r.body)

ophelia/tool/__init__.py

 # See also LICENSE.txt
 
 """Assorted tools for building a web site based on Ophelia.
+
+XXX deprecated in 0.4 (was never designed for use beyond a few specific sites)
+
 """

ophelia/tool/feed.py

-# Copyright (c) 2007 Thomas Lotze
-# See also LICENSE.txt
-
-import os.path
-import sys
-import shutil
-import datetime
-import pickle
-
-import feedparser
-
-import ophelia.request
-
-
-def filename(var_dir, key):
-    return os.path.join(var_dir, "feeds", key)
-
-
-def download(key, uri, delta, var_dir):
-    fn = filename(var_dir, key)
-    now = datetime.datetime.now()
-    if os.path.exists(fn):
-        try:
-            date, doc = pickle.load(open(fn))
-        except:
-            raise Exception("Can't read feed cache at " + fn)
-
-        if now - date < delta:
-            return
-
-    try:
-        doc = feedparser.parse(uri)
-    except:
-        raise Exception("Can't fetch or parse feed at " + uri)
-
-    try:
-        pickle.dump((now, doc), open(fn, "w"))
-    except:
-        raise Exception("Can't write feed cache at " + fn)
-
-
-class FeedLoader(object):
-
-    def __init__(self, date_format="%c"):
-        self.date_format = date_format
-
-    def __call__(self, key, count):
-        request = ophelia.request.get_request()
-        fn = filename(request.env.var_dir, key)
-        try:
-            date, doc = pickle.load(open(fn))
-            doc.date = date
-            doc.formatted_date = ophelia.util.strftime(self.date_format, date)
-            del doc.entries[count:]
-        except:
-            return None
-        else:
-            return doc
-# Copyright (c) 2007-2011 Thomas Lotze
+# Copyright (c) 2007-2012 Thomas Lotze
 # See also LICENSE.txt
 
 """A WSGI application running Ophelia, and an optional wsgiref server running
 this application.
 """
 
+import ConfigParser
+import logging
+import ophelia.request
+import ophelia.util
+import os.path
 import sys
-
+import wsgiref.simple_server
+import xsendfile
 import zope.exceptions.exceptionformatter
 
-import ophelia.request
+
+logger = logging.getLogger('ophelia')
+logger.addHandler(logging.StreamHandler())
+
+
+class Request(ophelia.request.Request):
+
+    @ophelia.request.push_request
+    def __call__(self):
+        try:
+            return super(Request, self).__call__()
+        except ophelia.request.NotFound:
+            env = self.env
+            document_root = env.get('document_root')
+            if not document_root:
+                raise
+
+            path = env['PATH_INFO']
+            parts = path.split('/')
+            index_name = env.get('index_name', 'index.html')
+
+            if (env.get('redirect_index') and parts[-1] == index_name):
+                raise ophelia.request.Redirect(path=path[:-len(index_name)])
+
+            fs_path = os.path.join(document_root, *parts)
+            if os.path.isdir(fs_path):
+                if not path.endswith('/'):
+                    raise ophelia.request.Redirect(path=path + '/')
+
+                path = '%s/%s' % (path.rstrip('/'), index_name)
+
+            raise ophelia.request.NotFound(path)
 
 
 class Application(object):
     """Ophelia's WSGI application.
     """
 
+    def __init__(self, options=None):
+        self.options = options or {}
+
     def __call__(self, env, start_response):
-        path = env["PATH_INFO"]
-        if path.startswith('/'):
-            path = path[1:]
-
+        env = ophelia.util.Namespace(self.options, **env)
+        path = env["PATH_INFO"].lstrip('/')
         context = env.get("ophelia.context", {})
 
-        request = ophelia.request.Request(
+        request = Request(
             path, env.pop("template_root"), env.pop("site"), **env)
 
         response_headers = {"Content-Type": "text/html"}
         exc_info = None
 
         try:
-            response_headers, body = request(**context)
+            try:
+                response_headers, body = request(**context)
+            except ophelia.request.NotFound, e:
+                env['PATH_INFO'] = e.args[0]
+                return self.sendfile(env, start_response)
         except ophelia.request.Redirect, e:
             status = "301 Moved permanently"
             text = ('The resource you were trying to access '
-                    'has moved permanently to <a href="%(uri)s">%(uri)s</a>')
+                    'has moved permanently to <a href="%(uri)s">%(uri)s</a>' %
+                    dict(uri=e.uri))
             response_headers["location"] = e.uri
-        except ophelia.request.NotFound, e:
-            status = "404 Not found"
-            text = "The resource you were trying to access could not be found."
         except Exception, e:
             status = "500 Internal server error"
             exc_info = sys.exc_info()
                 with_filenames=True, *exc_info))
             if isinstance(msg, unicode):
                 msg = msg.encode('utf-8')
-            text = "<pre>\n%s\n</pre>" % msg
-            self.report_exception(env, msg)
+            logger.error(msg)
+            if boolean(env.get('debug', False)):
+                text = '<pre>\n%s\n</pre>' % msg
+            else:
+                text = 'Something went wrong with the server software.'
         else:
             status = "200 OK"
 
         else:
             return []
 
-    def report_exception(self, env, msg):
-        sys.stderr.write(msg)
+    def sendfile(self, env, start_response):
+        xsendfile_app = xsendfile.XSendfileApplication(
+            env['document_root'], env.get('xsendfile', 'serve'))
+        return xsendfile_app(env, start_response)
 
     error_body = """\
         <html>
         """.replace(" ", "")
 
 
-def wsgiref_server(config_file="", section="DEFAULT"):
-    import optparse
-    import ConfigParser
-    import wsgiref.simple_server
+def boolean(value):
+    if isinstance(value, basestring):
+        return value.lower() in ("on", "true", "yes")
+    else:
+        return bool(value)
 
-    oparser = optparse.OptionParser()
-    oparser.add_option("-c", dest="config_file", default=config_file)
-    oparser.add_option("-s", dest="section", default=section)
-    cmd_options, args = oparser.parse_args()
+
+def paste_app_factory(global_conf, **local_conf):
+    options = global_conf.copy()
+    options.update(local_conf)
+    return Application(options)
+
+
+def wsgiref_server():
+    config_file = sys.argv[1]
 
     config = ConfigParser.ConfigParser()
-    config.read(cmd_options.config_file)
+    config.read(config_file)
     options = dict((key.replace('-', '_'), value)
-                   for key, value in config.items(cmd_options.section))
+                   for key, value in config.items('DEFAULT'))
 
-    app = Application()
-
-    def configured_app(environ, start_response):
-        environ.update(options)
-        return app(environ, start_response)
+    configured_app = Application(options)
 
     httpd = wsgiref.simple_server.make_server(
         options.pop("host"), int(options.pop("port")), configured_app)
 # Copyright (c) 2006-2012 Thomas Lotze
 # See also LICENSE.txt
 
+# This should be only one line. If it must be multi-line, indent the second
+# line onwards to keep the PKG-INFO file format intact.
 """Ophelia builds a web site from TAL templates with zero code repetition.
 """
 
+from setuptools import setup, find_packages
+import glob
 import os.path
-import glob
-from setuptools import setup, find_packages
+import sys
 
 
-project_path = lambda *names: os.path.join(os.path.dirname(__file__), *names)
+def project_path(*names):
+    return os.path.join(os.path.dirname(__file__), *names)
+
 
 longdesc = "\n\n".join((open(project_path("README.txt")).read(),
                         open(project_path("ABOUT.txt")).read()))
 
-root_files = glob.glob(project_path("*.txt"))
-data_files = [("", [name for name in root_files