Advanced Topics
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Pyramid 2.1 (released 2026-03-11), checked on 2026-10-07. Every Python example was executed on Python 3.13 with Pyramid 2.1, WebTest 3.0.7, WebOb 1.8.11, zope.interface 8.6 and zope.component 7.1 (the last one installed separately; it is not a dependency of Pyramid). The running example is the Bookshelf Shelves scenario.
The earlier pages of this section teach the parts of Pyramid that nearly every application uses. This page collects the
features that are used rarely but that explain a lot when you meet them: invoking another view without an HTTP round
trip, asking the framework what it has been configured to do, taking control of HTTP caching headers, caching a value for
the lifetime of one request, the thread-local variables that sit behind get_current_request(), the component registry
that Pyramid is built on, and the corners of combining traversal with URL dispatch that the
traversal page only touches. Each topic stands alone, so read the one
you need.
| Topic | Use it when | Where it is introduced first |
|---|---|---|
Subrequests |
One view needs the response of another view of the same application |
|
The introspector |
A tool or an audit must list routes, views or permissions |
|
HTTP caching |
Browsers and proxies should reuse (or never reuse) a response |
|
|
An expensive value is needed from several places during one request |
|
Thread locals |
Legacy code, tests or scripts need "the current" request or registry |
|
The component registry |
You want utilities, adapters or the ZCA global API |
this page |
Hybrid applications |
A route must hand part of the URL to traversal |
Subrequests
A subrequest obtains a response from a view of the same application while another view is running. You build a new
request, hand it to request.invoke_subrequest(), and get a Response back, whatever the target view returned: a
view that uses a renderer or returns something a response adapter understands is turned into a real response, which is
the main advantage over importing the view function and calling it. Subrequests are slower than a plain function call
and less convenient when you only want the data, so reach for one only when you need the full pipeline of the target
view (its renderer, its permission check, its predicates, its caching headers).
The method takes the request to run and one keyword argument, use_tweens, which defaults to False:
use_tweens |
What happens |
|---|---|
|
The request goes straight to the router’s main handler. Tweens are skipped, so an exception raised by the target view is raised to the caller of |
|
The request enters the tween chain at the ingress end, as a real request would. The exception-view tween converts exceptions into responses, and every other tween runs too (a |
Either way the call also manages the thread-local stack so that get_current_request() inside the subrequest returns the
subrequest; attaches registry and invoke_subrequest to the request; applies request extensions added with
add_request_method; sends the NewRequest, ContextFound and NewResponse events; checks the permission of the target
view for the user implied by the subrequest; and runs response and finished callbacks. Build the subrequest with
Request.blank() instead of reusing the original request: reusing it is documented as a poor idea.
from pyramid.config import Configurator
from pyramid.request import Request
from webtest import TestApp
BOOKS = {"1": "Dune", "2": "Hyperion"}
def api_book(request):
book_id = request.matchdict["id"]
if book_id not in BOOKS:
raise LookupError(f"no book {book_id}")
return {"id": book_id, "title": BOOKS[book_id]}
def book_page(request):
sub = Request.blank("/api/books/" + request.matchdict["id"])
try:
data = request.invoke_subrequest(sub).json
except LookupError as exc: # no tweens: the exception reaches us
data = {"raised": str(exc)}
return {"page": data}
def book_page_tweens(request):
sub = Request.blank("/api/books/" + request.matchdict["id"])
response = request.invoke_subrequest(sub, use_tweens=True)
return {"status": response.status, "body": response.json}
def lookup_error_view(exc, request):
request.response.status_int = 404
return {"error": str(exc)}
with Configurator() as config:
config.add_route("api_book", "/api/books/{id}")
config.add_route("page", "/books/{id}")
config.add_route("page_tweens", "/books/{id}/tweens")
config.add_view(api_book, route_name="api_book", renderer="json")
config.add_view(book_page, route_name="page", renderer="json")
config.add_view(book_page_tweens, route_name="page_tweens", renderer="json")
config.add_exception_view(lookup_error_view, context=LookupError, renderer="json")
app = TestApp(config.make_wsgi_app())
print(app.get("/books/1").json)
print(app.get("/books/9").json)
print(app.get("/books/9/tweens").json)
print(app.get("/api/books/9", status=404).json)
The output is {'page': {'id': '1', 'title': 'Dune'}}, then {'page': {'raised': 'no book 9'}} for the first
subrequest on an unknown book (the LookupError came out of invoke_subrequest()), then
{'status': '404 Not Found', 'body': {'error': 'no book 9'}} once use_tweens=True let the exception view build a
response, and finally the same error body when a client calls the API route directly. The renderer of the target view
produced the JSON in every case, which is why .json works on the subrequest’s response.
Source: Invoking a Subrequest
Carrying the Identity Into a Subrequest
A blank request has an empty environment. It does not inherit the caller’s cookies, headers or authenticated user, and
the target view’s permission is checked against the subrequest. Copy whatever your security policy reads. This example
uses a policy that trusts the WSGI REMOTE_USER variable, which a web server sets after its own authentication (never
trust a client-supplied header for this):
from pyramid.security import Allowed, Denied
class RemoteUserPolicy:
def identity(self, request):
return request.environ.get("REMOTE_USER")
def authenticated_userid(self, request):
return self.identity(request)
def permits(self, request, context, permission):
if self.identity(request) == "ada":
return Allowed("ada may do everything in this demo")
return Denied("anonymous")
def shelf_data(request):
return {"shelf": "ada", "user": request.environ.get("REMOTE_USER")}
def dashboard(request):
sub = Request.blank("/api/shelf")
user = request.environ.get("REMOTE_USER")
if user:
sub.environ["REMOTE_USER"] = user # hand the identity over on purpose
return request.invoke_subrequest(sub, use_tweens=True)
with Configurator() as config:
config.set_security_policy(RemoteUserPolicy())
config.add_route("shelf", "/api/shelf")
config.add_route("dashboard", "/dashboard")
config.add_view(shelf_data, route_name="shelf", renderer="json", permission="view")
config.add_view(dashboard, route_name="dashboard")
secured = config.make_wsgi_app()
print(TestApp(secured, extra_environ={"REMOTE_USER": "ada"}).get("/dashboard").json)
print(TestApp(secured).get("/dashboard", status=403).status)
The first call prints {'shelf': 'ada', 'user': 'ada'} and the anonymous one prints 403 Forbidden: the denial
came from the subrequest and the calling view returned that response unchanged. Subrequests do not bypass security,
and they must not be fed paths taken from user input, because they can reach routes that the page was never meant to
expose.
Source: Subrequests with Tweens and Request.invoke_subrequest
Invoking an Exception View by Hand
The same chapter describes request.invoke_exception_view(exc_info=None, request=None, secure=True, reraise=False).
Inside an except block it runs the exception view registered for the exception and returns its response, or None
when no view matches. It is useful when you catch an exception yourself but still want the application’s standard error
page; most code should simply let the exception-view tween do it. The
errors page covers exception views in full.
The Introspector
The introspector, queried as request.registry.introspector, is the framework’s own record of its configuration; the
configuration page introduces it with a
route lookup. Its categories are described in the official chapter: routes, views, permissions, templates,
tweens, renderer factories, static views, traversers and others, and add-on packages may add their own. The
methods are get(category, discriminator), get_category(category), categories(), categorized() and related().
get_category returns a list of dictionaries with an introspectable and its related introspectables.
A practical use is an audit. The next function lists every route-bound view with its route pattern, its request method and its permission, so that a test can assert that no view was left public by accident:
def audit(registry):
rows = []
for entry in registry.introspector.get_category("views"):
view = entry["introspectable"]
if view["route_name"] is None:
continue
perms = [r.discriminator for r in entry["related"] if r.category_name == "permissions"]
routes = [r["pattern"] for r in entry["related"] if r.category_name == "routes"]
rows.append((routes[0], view["request_methods"], perms[0] if perms else None))
return sorted(rows, key=str)
with Configurator() as config:
config.add_route("books", "/books")
config.add_route("book", "/books/{id}")
config.add_route("health", "/health")
config.add_view(lambda r: {}, route_name="books", request_method="GET", renderer="json")
config.add_view(lambda r: {}, route_name="books", request_method="POST", permission="edit", renderer="json")
config.add_view(lambda r: {}, route_name="book", permission="view", renderer="json")
config.add_view(lambda r: {}, route_name="health", renderer="json")
audited = config.make_wsgi_app()
for row in audit(audited.registry):
print(row)
public = [row for row in audit(audited.registry) if row[2] is None]
assert [row[0] for row in public] == ["/books", "/health"]
It prints ('/books', 'GET', None), ('/books', 'POST', 'edit'), ('/books/{id}', None, 'view') and
('/health', None, None). The two rows without a permission are the public views, which is intended for GET /books
and /health here. Note that a default permission set with set_default_permission is not related to each view in the
introspector; to audit a deployment that uses one, also read the default permission category.
Every add_view introspectable carries the keys accept, attr, callable, containment, context, decorator,
derived_callable, exception_only, header, http_cache, mapper, match_param, name, order, path_info,
phash, predicates, request_methods, request_param, require_csrf, route_name and xhr in the 2.1 code I
inspected; the official chapter documents a shorter list, so treat the extra keys as informational rather than as a
stable API. Debug toolbars and reports use the same data.
Introspection records data for every configuration directive. A Configurator created with introspection=False throws every introspectable
away; the registry then still has an introspector object but it is empty:
with Configurator(introspection=False) as config:
config.add_route("books", "/books")
quiet = config.make_wsgi_app()
print(quiet.registry.introspector.categories(), quiet.registry.introspector.get_category("views"))
It prints [] None: any code, including your own audit, that relies on the introspector finds nothing (and
get_category returns None, so the audit function above would fail) when it is disabled. Keep it enabled unless memory or start-up time on a very large application is a measured problem. The debug toolbar
renders the introspector as a "Global" tab, and it is a development tool; it is never installed in a production
deployment (the logging and debugging page of this section covers it).
HTTP Caching and Pyramid
Pyramid does not cache anything on the server; it helps you write the headers that let browsers and shared caches do
it. The view argument is http_cache, which accepts the forms in the next table. A non-tuple value is equivalent to
calling response.cache_expires(value) in the view body; a tuple (seconds, options) is equivalent to
response.cache_expires(seconds, **options), where the options are anything the WebOb CacheControl object accepts.
http_cache value |
Meaning | Headers |
|---|---|---|
A non-zero integer |
Cache for that many seconds |
|
A |
Same, in a duration object |
|
|
Never cache |
|
|
Seconds plus extra directives |
|
|
Directives only, leave |
|
from datetime import timedelta
from pyramid.response import Response
def books_view(request):
response = Response("books")
if "private" in request.GET:
response.cache_control.prevent_auto = True # this answer must not be cached by http_cache
return response
def build(settings=None):
with Configurator(settings=settings) as config:
values = {
"seconds": 3600,
"timedelta": timedelta(days=1),
"never": 0,
"public": (3600, {"public": True}),
"cc_only": (None, {"public": True, "max_age": 60}),
}
for name, value in values.items():
config.add_route(name, "/" + name)
config.add_view(books_view, route_name=name, http_cache=value)
return TestApp(config.make_wsgi_app())
client = build()
for path in ("/seconds", "/timedelta", "/never", "/public", "/cc_only", "/seconds?private=1"):
res = client.get(path)
print(path, res.headers.get("Cache-Control"), "|", "Expires" in res.headers)
The output is, one line per request:
/seconds max-age=3600 | True
/timedelta max-age=86400 | True
/never max-age=0, must-revalidate, no-cache, no-store | True
/public max-age=3600, public | True
/cc_only max-age=60, public | False
/seconds?private=1 None | False
Source: Influencing HTTP Caching and Non-Predicate Arguments
The last line shows prevent_auto: the http_cache machinery leaves that response alone, so a single view can cache
the normal answer and refuse to cache a special one. Without prevent_auto the http_cache wrapper overwrites cache
headers that the view body set itself.
The pyramid.prevent_http_cache setting (environment variable PYRAMID_PREVENT_HTTP_CACHE, parsed like the other
boolean settings of the settings page) disables the
http_cache argument for the whole application, which is convenient in development and in staging where you do not want
a browser to hold stale pages:
off = build({"pyramid.prevent_http_cache": "true"})
print(off.get("/seconds").headers.get("Cache-Control"))
assert off.get("/seconds").headers.get("Cache-Control") is None
The result is None. The setting affects only responses shaped by http_cache; headers that a view sets in its own
body and the caching headers of static views and FileResponse are separate.
Decisions that matter more than the syntax:
-
Add
publiconly to content that is identical for every user. Amax-agealone lets shared caches (a CDN, a corporate proxy) store the response; anything that depends on a cookie or anAuthorizationheader should use0orno-store, andVaryshould name the request headers that change the body. -
http_cachesets freshness (how long a copy may be reused). Validators (ETag,Last-Modifiedwith conditional responses) let a client revalidate cheaply once a copy is stale; the request and response page shows them and the views page coversdecoratorandwrappernext tohttp_cache. -
A server-side cache of rendered fragments is a different problem, solved with libraries such as
dogpile.cache; the Flask page on caching compares the two layers in another framework.
Source: Influencing HTTP Caching and Non-Predicate Arguments
RequestLocalCache
pyramid.request.RequestLocalCache (added in Pyramid 2.0) is a store that keeps a value for the life of one request.
It wraps a function that takes the request: the first call computes the value, later calls during the same request return
the stored object, and a new request starts empty. It keeps a weak reference to each request and removes the value
through a finished callback as soon as the request ends, so nothing leaks between requests. It exists mainly for security
policies, which are asked about the identity several times per request (see the policy examples on the
security policy page), but any expensive per-request value
fits: the current shelves of the user, a parsed token, a feature-flag set.
from pyramid.request import RequestLocalCache
calls = []
@RequestLocalCache()
def current_shelves(request):
calls.append(request.path) # stands for a database query
return ["ada", "grace"]
def shelves_view(request):
first = current_shelves(request)
second = current_shelves(request)
return {"same_object": first is second, "total_calls": len(calls)}
with Configurator() as config:
config.add_route("shelves_cache", "/cache")
config.add_view(shelves_view, route_name="shelves_cache", renderer="json")
cache_app = TestApp(config.make_wsgi_app())
print(cache_app.get("/cache").json, cache_app.get("/cache").json)
Both calls in one request return the same object, so the function ran once per request: the output is
{'same_object': True, 'total_calls': 1} {'same_object': True, 'total_calls': 2}. The decorated function carries the
cache as its cache attribute, and the cache has four methods:
| Method | Behaviour |
|---|---|
|
Return the stored value, computing it with the creator (the decorated function, the constructor argument, or the one passed here) when it is missing. Raises |
|
Return the stored value or the default; never computes. |
|
Store a value, registering the cleanup callback only once per request. |
|
Forget the value and return the previous one (or |
cache = RequestLocalCache()
req = Request.blank("/x")
print(cache.get(req) is RequestLocalCache.NO_VALUE)
print(cache.get_or_create(req, lambda r: 42), cache.get(req))
print(cache.clear(req), cache.get(req) is cache.NO_VALUE)
It prints True, 42 42 and 42 True. To cache a method, bind the creator in the constructor so that the cache
only needs the request: self.identity_cache = RequestLocalCache(self.load_identity) and
return self.identity_cache.get_or_create(request), as the security page does.
How it relates to the alternatives: add_request_method(func, name, reify=True) also computes once per request but
stores the result as an attribute of the request object, so it is reachable as request.name and cannot be cleared;
RequestLocalCache lives outside the request, needs no registration, can be invalidated with clear (after a
logout inside the same request, for instance), and can be used from code that is handed a request but must not modify it.
A subrequest is a different request object and has its own entry.
Thread Locals
While a request is processed, Pyramid keeps two values in a thread-local stack: the registry and the request. The
router pushes them when the request starts and pops them when it ends, and a subrequest pushes a second pair on top, so
"current" always means the topmost entry. pyramid.threadlocal.get_current_request() and get_current_registry() read
it. The official chapter is blunt about them: they are kept for historical reasons, Pyramid internals use them, and
application code should almost never call them, because code that does cannot be tested or scripted without a request.
Pass the request (or data derived from it) as an argument instead.
import threading
from pyramid.threadlocal import get_current_request, get_current_registry, manager
seen = {}
def probe(request):
seen["same_request"] = get_current_request() is request
seen["same_registry"] = get_current_registry() is request.registry
seen["depth"] = len(manager.stack)
worker = {}
thread = threading.Thread(
target=lambda: worker.update(request=get_current_request(), registry=get_current_registry())
)
thread.start()
thread.join()
seen["thread_request"] = worker["request"]
seen["thread_sees_app_registry"] = worker["registry"] is request.registry
seen["inner"] = request.invoke_subrequest(Request.blank("/inner")).json
seen["after_sub"] = get_current_request() is request
return {"ok": True}
def inner(request):
return {"current_is_inner": get_current_request() is request, "depth": len(manager.stack)}
with Configurator() as config:
config.add_route("probe", "/probe")
config.add_route("inner", "/inner")
config.add_view(probe, route_name="probe", renderer="json")
config.add_view(inner, route_name="inner", renderer="json")
wsgi = config.make_wsgi_app()
TestApp(wsgi).get("/probe")
print(seen)
print(get_current_request(), get_current_registry() is wsgi.registry)
Source: Thread Locals and pyramid.threadlocal
The dictionary printed is {'same_request': True, 'same_registry': True, 'depth': 1, 'thread_request': None,
'thread_sees_app_registry': False, 'inner': {'current_is_inner': True, 'depth': 2}, 'after_sub': True}, followed by
None False for code running outside any request. Three lessons follow:
-
The stack is per thread. A thread or a thread-pool task started from a view sees the default values (no request and the process-wide global registry, not your application’s registry), so pass the request, or the data you need, to background work explicitly.
-
A subrequest pushes its own entry (
depth2) and pops it afterwards, so the outer request is current again. -
Outside a request,
get_current_request()isNoneandget_current_registry()is a global registry that is not your application’s.
Where "current" is legitimately useful is code that has no request parameter by design and is rarely changed: the
translator handed to a form library (see the Deform page of this section) and the facade of a deprecated API. In tests
and scripts the stack is managed for you. pyramid.testing.setUp() and tearDown() (or config.begin() and
config.end()) push and pop an entry around a unit test, and pyramid.scripting.prepare() (which pshell uses) is a context manager that builds a request, pushes it and returns the environment:
from pyramid import testing
from pyramid.scripting import prepare
with prepare(registry=wsgi.registry) as env:
print(sorted(env), get_current_request() is env["request"])
test_config = testing.setUp()
print(get_current_registry() is test_config.registry)
testing.tearDown()
print(get_current_request())
It prints ['closer', 'registry', 'request', 'root', 'root_factory'] True, True and None. The threadlocals
chapter says that scripts should expect get_current_request() to return None; with the context-manager form of
prepare() in 2.1, which I executed, the request it builds is current inside the with block. Do not rely on either
reading in portable code: take a request argument. The unit-testing chapter shows setUp and tearDown, and the
testing page of this section builds on them.
Source: Thread Locals and pyramid.threadlocal
For comparison, Flask solves the same need with its application and request context objects (Application and Request Lifecycle); Pyramid’s stack is smaller, less magical and explicitly discouraged in application code.
The Component Registry and the ZCA
Under the hood the application registry, config.registry while configuring and request.registry while serving, is a
Zope Component Architecture component registry: Pyramid’s Registry is a subclass of
zope.interface.registry.Components and also a dictionary, which is where the settings live. The ZCA stores
utilities (named or unnamed singletons looked up by interface), adapters (converters from one interface to another)
and subscribers (handlers called for a set of objects). Pyramid hides it deliberately: you need not know it to build
an application. It is a good tool for building a framework and an opaque one for building an application, in the
words of the official chapter, but it is there, and it is a clean way to register a service once and look it up anywhere
that has the request.
The registry has the full method API of the ZCA, so you never need the global functions of zope.component:
from zope.interface import Interface, implementer
from pyramid.exceptions import ConfigurationConflictError
class IBookCatalog(Interface):
def title_of(book_id):
"""Return the title for an id."""
@implementer(IBookCatalog)
class MemoryCatalog:
def __init__(self, books):
self.books = books
def title_of(self, book_id):
return self.books[book_id]
def register_catalog(config, catalog):
def register():
config.registry.registerUtility(catalog, IBookCatalog)
config.action(("catalog", IBookCatalog), register) # same discriminator means a conflict
def catalog_view(request):
catalog = request.registry.getUtility(IBookCatalog)
other = request.registry.queryUtility(IBookCatalog, name="other")
return {"title": catalog.title_of("1"), "no_other": other is None}
with Configurator() as config:
register_catalog(config, MemoryCatalog({"1": "Dune"}))
config.add_route("catalog", "/catalog")
config.add_view(catalog_view, route_name="catalog", renderer="json")
zapp = config.make_wsgi_app()
print(TestApp(zapp).get("/catalog").json)
try:
with Configurator() as config:
register_catalog(config, MemoryCatalog({}))
register_catalog(config, MemoryCatalog({}))
except ConfigurationConflictError as exc:
print("conflict:", str(exc).splitlines()[0])
The first print is {'title': 'Dune', 'no_other': True}; the second is conflict: Conflicting configuration actions.
Registering through config.action (with a discriminator) rather than calling registerUtility straight away puts the
registration into Pyramid’s two-phase configuration: two includes that register the same utility conflict at start-up
and the includer can override them, exactly as for routes and views (see the
configuration page). A direct call is simpler and never
conflicts, but whichever runs last silently wins.
Adapters and handlers use the same registry methods. registerAdapter(factory, required, provided) plus
queryAdapter(obj, interface) returns None when no adapter is registered (getAdapter raises instead), and
registerHandler(handler, required) plus subscribers(objects, None) calls every matching handler:
class IBookSummary(Interface):
pass
class Book:
def __init__(self, title):
self.title = title
@implementer(IBookSummary)
class BookSummary:
def __init__(self, book):
self.book = book
def text(self):
return f"[{self.book.title}]"
registry = zapp.registry
registry.registerAdapter(BookSummary, (Book,), IBookSummary)
print(registry.queryAdapter(Book("Dune"), IBookSummary).text(), registry.queryAdapter(object(), IBookSummary))
handled = []
registry.registerHandler(lambda book: handled.append(book.title), (Book,))
registry.subscribers((Book("Hyperion"),), None)
print(handled)
The output is [Dune] None and ['Hyperion']. Pyramid’s own events (NewRequest, ContextFound, NewResponse,
BeforeRender) are subscribers on this same registry; you normally register them with config.add_subscriber, which
adds predicate support and conflict handling on top.
The ZCA Global API and hook_zca
The functions of zope.component (getUtility, getAdapter, getSiteManager) consult the ZCA global registry by
default, and Pyramid creates a separate registry per application so that several applications can live in one process,
so those calls do not see your configuration. The documented fixes are to avoid the global API (what the previous
examples do), to call config.hook_zca(), or to hand the ZCA global registry to the Configurator. hook_zca()
imports zope.component, so install that package first; Pyramid does not depend on it.
from zope.component import getUtility, getSiteManager
from zope.interface.interfaces import ComponentLookupError
try:
getUtility(IBookCatalog)
except ComponentLookupError:
print("global API does not see the application registry")
hook = Configurator(registry=zapp.registry)
hook.hook_zca()
with prepare(registry=zapp.registry):
print(getUtility(IBookCatalog).title_of("1"), getSiteManager() is zapp.registry)
print(getSiteManager() is zapp.registry)
hook.unhook_zca()
try:
getUtility(IBookCatalog)
except ComponentLookupError:
print("hook removed")
The output is global API does not see the application registry, Dune True, False and hook removed. The hook
makes getSiteManager() return Pyramid’s current registry, so it works in threads that are processing a request (here
inside prepare) and falls back to the process-wide global elsewhere. unhook_zca() undoes it. The hook changes a
process-wide setting: if a Zope application shares the process, its calls start using the Pyramid registry, "inverting
the original problem". For that case the third option applies: create the application on the ZCA global registry itself
with from zope.component import getGlobalSiteManager, then config = Configurator(registry=getGlobalSiteManager())
followed by config.setup_registry(settings=settings) (the call that normally happens for a registry Pyramid creates).
Only do this when you need a single, process-wide registry: it gives up the ability to run two applications side by side
and makes tests share state.
Hybrid Applications: Edge Cases
The traversal page explains the four differences between pure traversal and a hybrid application and shows a route that
ends in *traverse. This section adds the official "Hello Traversal World" and the corners that surprise people.
Hello Traversal World
The smallest traversal application has no route at all. The URL space is the tree under the root factory, and a view
registered for context=Resource displays any node:
from pyramid.response import Response
class Resource(dict):
__name__ = None
__parent__ = None
def add(self, name, child=None):
child = Resource() if child is None else child
child.__name__, child.__parent__ = name, self
self[name] = child
return child
def get_root(request):
root = Resource()
ada = root.add("ada")
ada.add("dune")
return root
def show(context, request):
return Response(f"resource {context.__name__!r} with children {sorted(context)}")
with Configurator() as config:
config.set_root_factory(get_root)
config.add_view(show, context=Resource)
hello = TestApp(config.make_wsgi_app())
for path in ("/", "/ada", "/ada/dune"):
print(path, hello.get(path).text)
print(hello.get("/ada/nope", status=404).status)
The output is / resource None with children ['ada'], /ada resource 'ada' with children ['dune'],
/ada/dune resource 'dune' with children [] and 404 Not Found. A key that does not exist ends traversal with a view
name that no view answers, which becomes a 404. The official version of this example (in "Hello Traversal World") builds
a fixed tree of Resource dictionaries without __name__ and __parent__; the two attributes added here are what
request.resource_url needs to build links.
Source: Hello Traversal World
What a Matched Route Changes
The next application mounts the same tree under several routes, each showing one edge case. The trace view reports what
traversal produced:
def trace(context, request):
return {
"context": context.__name__,
"view_name": request.view_name,
"subpath": list(request.subpath),
"route": request.matched_route.name if request.matched_route else None,
}
def link(context, request):
return {
"plain": request.resource_url(context),
"routed": request.resource_url(context, route_name="api", route_kw={"version": "v2"}),
"slashed": request.resource_url(context, route_name="slashed", route_kw={"version": "v2"}),
"other_name": request.resource_path(context, route_name="files", route_remainder_name="rest"),
}
with Configurator(root_factory=get_root) as config:
config.add_route("api", "/api/{version}*traverse")
config.add_route("slashed", "/s/{version}/*traverse")
config.add_route("both", "/both/{shelf}/*traverse", traverse="/{shelf}/dune")
config.add_route("write", "/w/*traverse", request_method="POST")
config.add_route("global", "/g/*traverse", use_global_views=True)
config.add_route("files", "/files*rest")
config.add_route("static", "/static/*subpath")
config.add_view(trace, route_name="api", renderer="json")
config.add_view(trace, route_name="api", name="edit", renderer="json")
config.add_view(link, route_name="api", name="link", renderer="json")
config.add_view(trace, route_name="both", renderer="json")
config.add_view(trace, route_name="static", renderer="json")
config.add_view(trace, name="plain", renderer="json")
config.add_view(show, context=Resource)
hybrid = TestApp(config.make_wsgi_app())
for path in (
"/api/v1/ada/dune",
"/api/v1/ada/dune/edit/extra/more",
"/api/v1/ada/missing",
"/both/ada/anything",
"/g/ada/plain",
"/api/v1/plain",
"/plain",
"/static/css/site.css",
):
res = hybrid.get(path, expect_errors=True)
print(path, res.status_int, res.json if res.content_type == "application/json" else "")
print(hybrid.get("/api/v1/ada/link").json)
print(hybrid.get("/w/ada", expect_errors=True).status_int, hybrid.post("/w/ada", expect_errors=True).status_int)
The eight lines of the loop show these behaviours:
| Request | Result | Why |
|---|---|---|
|
200, context |
|
|
200, view name |
The path ran out of tree at |
|
404 |
View name |
|
200, context |
The route has both a |
|
200, context |
|
|
404 |
The |
|
200, no route |
No route matched, so this is plain traversal and the global view answers. |
|
200, subpath |
|
The two final lines print the link dictionary and 404 404. /w/ada has a route restricted to POST; a GET
does not match that route and falls through to pure traversal, which finds no w child in the tree (404). A POST
matches the route, but no view names it, so that is a 404 too: a route without a view for it is a dead end, not a
fall-through to other routes.
The generated links teach two further points. request.resource_url(context, route_name=..., route_kw=...) builds a
URL under the route; route_kw fills the other dynamic parts and route_remainder_name names the star argument when it
is not traverse. For the resource ada, the plain link has no route and is http://localhost/ada/; the routed link under
/api/{version}*traverse is http://localhost/api/v2/ada/ and the other_name path is /files/ada/, but the
pattern /s/{version}/*traverse that writes a slash before the star produces a double slash
(http://localhost/s/v2//ada/), because the traversal path already starts with one. Write the star directly after the last literal or marker, /api/{version}*traverse.
Finally, passing route_name makes resource_url ignore a __resource_url__ method on the resource, which is
documented as incompatible with the feature.
Pitfalls and Security Checklist
| Pitfall | What to do |
|---|---|
An exception from a subrequest escapes the exception view |
Pass |
A subrequest answers 403 although the user is logged in |
A blank request carries no identity. Copy the cookie, header or environ entry that your security policy reads. |
Subrequest URLs built from user input |
Never. A subrequest reaches any route of the application; use fixed paths. |
|
Know your tween list; use |
Audit code finds nothing |
The |
Private pages carry |
Use |
|
It removes every |
A value computed twice per request |
Wrap the loader in |
Background thread sees no request |
Thread locals are per thread; pass the request data explicitly. |
|
Take |
|
Use |
Two includes register the same utility |
Register through |
Hybrid view not found |
Give the view |
|
Remove the slash before the star in the route pattern. |
Summary
-
request.invoke_subrequest(Request.blank(path), use_tweens=...)runs another view through the full view pipeline; without tweens exceptions propagate, and the identity must be copied by hand. -
The introspector at
request.registry.introspectorlists routes, views, permissions and more; it is the base of audits and tools, andintrospection=Falseempties it. -
http_cachewritesExpiresandCache-Control;prevent_autoopts one response out, andpyramid.prevent_http_cacheswitches the feature off for the whole application. -
RequestLocalCachecaches a value for one request and releases it automatically. -
Thread locals hold the current request and registry per thread and per subrequest; keep them out of application code and pass the request instead.
-
The application registry is a ZCA component registry: use its methods (
registerUtility,registerAdapter,registerHandler) directly, register throughconfig.actionto get conflict detection, and treathook_zca()as a last resort. -
Hybrid routes limit view lookup to views named after the route, behave unpredictably when both a
*traversestar and atraverse=argument are given, and produce clean links only when the star follows its prefix without a slash.
The deployment of an application that uses these features (a WSGI server such as Waitress behind a reverse proxy, in a container) is described in the production pages of this section, and the Tornado page on WSGI interoperability shows how a WSGI application such as Pyramid can be served next to asynchronous handlers. For packaging see Docker and Kubernetes; for the other Python frameworks of this site see FastAPI and Django.