==================
The request object
==================


Ophelia's request object is both used for processing the request as the HTTP
server needs to obtain a response, and accessed by user code to use the
current traversal state and influence how the request will be further
processed. Accordingly, a request object provides two interfaces, IRequestAPI
meant to be exposed to user code, and IRequestTraversal specifying the
traversal mechanism:

    >>> from ophelia.interfaces import IRequestAPI, IRequestTraversal
    >>> from ophelia.request import Request

    >>> from zope.interface.verify import verifyClass
    >>> verifyClass(IRequestAPI, Request)
    True
    >>> verifyClass(IRequestTraversal, Request)
    True


Initial state of a request
==========================

A request is three positional parameters upon instantiation: the path to
traverse from the site root URI to the requested resource URI, the file system
path to the directory holding the input files, and the URI of the site root.
Additionally, it accepts any number of keyword parameters that make up the
namespace of environment variables.

These parameters will be stored on the request object. They are not meant to
be changed during traversal. However, the template root path is normalized by
os.path.abspath, and the site root URI is appended a '/' if it doesn't end
with one yet:

    >>> from ophelia.util import Namespace

    >>> request = Request("example/somepage.html",
    ...                   "/tmp//nonexistent/",
    ...                   "http://localhost:1080",
    ...                   QUERY_STRING="")

    >>> request.path
    'example/somepage.html'

    >>> request.template_root
    '/tmp/nonexistent'

    >>> request.site
    'http://localhost:1080/'

    >>> request.env
    {'QUERY_STRING': ''}

Also, the traversal state and rendering context have been initialized. This
comprises two sets of attributes, those meant for common user code and those
that are probably only useful in a few special cases.

Among the former are the namespaces of variables visible to scripts and TALES
expressions, macros, and response headers, as well as the attributes for the
inner slot that will be built up as the templates are rendered, and the
encoded body text after its is complete:

    >>> request.context
    {'__request__': <ophelia.request.Request object at ...>}

    >>> request.macros
    {}

    >>> request.response_headers
    {'Content-Type':
     "python:'text/html; charset=' + __request__.response_encoding"}

    >>> request.innerslot

    >>> request.content

The latter include the beginning of the URI and the file system path as far as
it has been traversed, the tail of path segments yet to traverse, and the
stack of file contexts collected:

    >>> request.current
    'http://localhost:1080/'

    >>> request.dir_path
    '/tmp/nonexistent'

    >>> request.tail
    ['example', 'somepage.html']

    >>> request.stack
    []

Finally, the request has attributes reflecting configuration options. The
simpler ones are the response encoding, index file name, and index URI
redirection flag:

    >>> request.response_encoding
    'utf-8'

    >>> request.index_name
    'index.html'

    >>> request.redirect_index
    False

Also, the input file splitter has been set up, storing the input encodings:

    >>> request.splitter
    <ophelia.input.Splitter object at ...>

    >>> request.splitter.script_encoding
    'ascii'

    >>> request.splitter.template_encoding
    'ascii'

As our above example request wasn't given any special configuration, these
attributes all have default values. They can be overridden by the caller
passing configuration variables in the environment:

    >>> request = Request("example/somepage.html",
    ...                   "/tmp//nonexistent/",
    ...                   "http://localhost:1080",
    ...                   QUERY_STRING="",
    ...                   response_encoding="utf-16",
    ...                   index_name="default.html",
    ...                   redirect_index=True,
    ...                   script_encoding="latin-9",
    ...                   template_encoding="utf-8")

    >>> request.response_encoding
    'utf-16'

    >>> request.index_name
    'default.html'

    >>> request.redirect_index
    True

    >>> request.splitter.script_encoding
    'latin-9'

    >>> request.splitter.template_encoding
    'utf-8'


TALES expression evaluation
===========================

Building response body and headers both involve evaluating TALES expressions.
For the body, this is done through rendering page templates while header
expressions are evaluated directly by the request handler.

The TALES namespace
-------------------

The first step in both cases is to build the namespace to evaluate the
expressions in. The request provides a method that takes a namespace of input
file specific variables and returns a namespace that includes additional
variables defined by the TALES engine, an accessor for the inner template
slot, the namespace for METAL macros, and the current script context variables
(which initially is exactly one name, "__request__"):

    >>> tales_ns = request.tales_namespace({})
    >>> sorted(tales_ns.items())
    [('__request__', <ophelia.request.Request object at ...>),
     ('innerslot', None),
     ('macros', {}),
     ('modules', <zope.tales.expressions.SimpleModuleImporter object at ...>)]

    >>> tales_ns = request.tales_namespace({"__file__": "/tmp/asdf"})
    >>> sorted(tales_ns.items())
    [('__file__', '/tmp/asdf'),
     ('__request__', <ophelia.request.Request object at ...>),
     ('innerslot', None),
     ('macros', {}),
     ('modules', <zope.tales.expressions.SimpleModuleImporter object at ...>)]

After a Python script has been run in the context namespace, it has the name
"__builtins__" defined. This name will not be passed on to TALES expressions
(mainly because it messes up exception traceback supplements involving a
namespace listing):

    >>> exec 'title = u"A heading"' in request.context
    >>> sorted(request.context)
    ['__builtins__', '__request__', 'title']

    >>> tales_ns = request.tales_namespace({})
    >>> sorted(tales_ns.items())
    [('__request__', <ophelia.request.Request object at ...>),
     ('innerslot', None),
     ('macros', {}),
     ('modules', <zope.tales.expressions.SimpleModuleImporter object at ...>),
     ('title', u'A heading')]

Note that TALES expressions actually see some additional variables beside
those from our prepared namespace:

    >>> from zope.tales.engine import Engine as TALESEngine
    >>> tales_ns = {}
    >>> contexts = TALESEngine.getContext(tales_ns).evaluate("CONTEXTS")
    >>> sorted(contexts.items())
    [('default', <object object at ...>),
     ('loop', {}),
     ('nothing', None),
     ('repeat', {})]

Building response headers
-------------------------

Response headers are TALES expressions stored in the mapping that is the
response_headers request attribute. The namespace these expressions will be
evaluated in is the same namespace that has been modified by scripts and
already used for TALES expressions while rendering the templates.

The Request class has a method that builds response headers from the TALES
expressions registered. The Content-Type header is always there, making use of
the response encoding configured for the request:

    >>> request.response_headers
    {'Content-Type':
     "python:'text/html; charset=' + __request__.response_encoding"}

    >>> request.response_encoding = 'ascii'
    >>> request.build_headers()
    >>> request.compiled_headers
    {'Content-Type': 'text/html; charset=ascii'}

    >>> request.response_encoding = 'utf-8'
    >>> request.build_headers()
    >>> request.compiled_headers
    {'Content-Type': 'text/html; charset=utf-8'}

Further headers can be added during traversal. They are evaluated after the
response body is finished to allow for, e.g., the Content-Length header to be
calculated from the body text:

    >>> request.response_headers["Content-Length"] = \
    ...     "python:str(len(__request__.content))"
    >>> request.content = "asdf"
    >>> request.build_headers()
    >>> sorted(request.compiled_headers)
    ['Content-Length', 'Content-Type']
    >>> request.compiled_headers["Content-Length"]
    '4'
