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 instantiated with three positional parameters: 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:

>>> input_stream = "mock input stream"
>>> request = Request("example/somepage.html",
...                   "/tmp//nonexistent/",
...                   "http://localhost:1080",
...                   QUERY_STRING="",
...                   HTTP_HOST="localhost:1080",
...                   **{"wsgi.input": input_stream})
>>> request.path
'example/somepage.html'
>>> request.template_root
'/tmp/nonexistent'
>>> request.site
'http://localhost:1080/'
>>> request.env
{'QUERY_STRING': '',
 'wsgi.input': 'mock input stream',
 'HTTP_HOST': 'localhost:1080'}

Two attributes are derived from the environment variables: a file-like object from which to read the request body and a namespace of HTTP headers:

>>> request.input
'mock input stream'
>>> request.headers
{'HOST': 'localhost:1080'}

Traversal state and rendering context have been initialized by now. 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 first set of attributes are the namespaces of variables visible to scripts and TALES expressions, of macros and of response headers, as well as the inner slot that will be built up as the templates are rendered, and the encoded body text after it 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

As an aside, if response headers have been set earlier by the server environment, they may be passed to the request through the environment to pre-fill the response_headers namespace:

>>> Request("/somepage.html", "/tmp//nonexistent/", "http://localhost:1080",
...         **{"wsgi.input": input_stream,
...            "ophelia.response_headers": {"Content-Type": "text/plain",
...                                         "X-Foo": "bar"}}
... ).response_headers
{'Content-Type':
 "python:'text/html; charset=' + __request__.response_encoding",
 'X-Foo': 'string:bar'}

The second set of attributes includes the URI and file system path as far as they have 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

Another configuration attribute that belongs to the traversal part of the interface is the immediate-result switch which defaults to False:

>>> request.immediate_result
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="",
...                   HTTP_HOST="localhost:1080",
...                   response_encoding="utf-16",
...                   index_name="default.html",
...                   redirect_index=True,
...                   script_encoding="latin-9",
...                   template_encoding="utf-8",
...                   immediate_result=True,
...                   **{"wsgi.input": "mock input stream"})
>>> 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'
>>> request.immediate_result
True

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 ...>)]

The namespace passed to tales_namespace() will override the variables defined in the request’s own context:

>>> request.context.__file__ = "/tmp/foo"
>>> request.tales_namespace().__file__
'/tmp/foo'
>>> request.tales_namespace({"__file__": "/tmp/bar"}).__file__
'/tmp/bar'

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__', '__file__', '__request__', 'title']
>>> tales_ns = request.tales_namespace({})
>>> sorted(tales_ns.items())
[('__file__', '/tmp/foo'),
 ('__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'