Views and Controllers
Views are the heart of any Pyramid application: callables that turn a request
(and, in traversal, a context) into a response. Tet does not replace Pyramid’s
view machinery – it builds directly on top of it. The tet.view module
re-exports the entire pyramid.view public API and then layers a few
conveniences on top:
An
tet.view.view_configsubclass of Pyramid’s ownview_config, so you can import everything view-related from one place.An
tet.view.exposedecorator that registers a controller method as a traversal view using venusian, the same scanning mechanism Pyramid itself uses.A
tet.view.BaseControllerclass for building traversal-based, nestable controllers.A
tet.view.ServiceViewsbase class that ties view classes into thepyramid_didependency-injection system, giving youself.requestandself.contextfor free.
Everything below is grounded in the actual code in
src/tet/view/__init__.py. If you already know Pyramid views, you can keep
using them unchanged – Tet’s additions are opt-in.
A note on imports
Because tet.view does from pyramid.view import *, names such as
view_defaults, forbidden_view_config, notfound_view_config and
render_view_to_response are available straight from tet.view. The one
deliberate shadow is view_config: importing it from tet.view gives you
Tet’s subclass rather than Pyramid’s. They are behaviour-compatible today, so
mixing imports is harmless, but importing from one place keeps your code tidy.
# One import line for the common cases.
from tet.view import view_config, view_defaults, ServiceViews
The extended view_config decorator
tet.view.view_config is a thin subclass of
pyramid.view.view_config. Its constructor simply forwards every keyword
argument to the Pyramid base class:
class view_config(_pyramid_view_config):
"""Extended Pyramid view_config decorator."""
def __init__(self, **settings):
super().__init__(**settings)
That means it accepts exactly the predicates and settings you already know:
route_name, renderer, request_method, permission,
context, name, match_param, and so on. The subclass exists so that
Tet owns the symbol and can extend it in the future without forcing you to
change imports.
A plain function view looks exactly like it does in Pyramid:
from tet.view import view_config
@view_config(route_name="home", renderer="templates/home.tk")
def home_view(request):
return {"title": "Welcome to Tet"}
As always, the decorator only records configuration; it does nothing until a
config.scan() picks it up via venusian. Make sure the package containing
your views is scanned during application start-up.
Service-aware view classes with ServiceViews
The most idiomatic way to write views in Tet is the class-based style backed
by dependency injection. tet.view.ServiceViews subclasses
pyramid_di.RequestScopedBaseService, so a fresh instance is created per
request and participates in the DI container. Its constructor takes the
request and also resolves the current context:
class ServiceViews(RequestScopedBaseService):
def __init__(self, request: Request):
super().__init__(request=request)
self.context = getattr(request, "context", None)
In practice you subclass it, decorate methods with view_config, and access
self.request and self.context inside each method. Because the class is
a request-scoped service, you can also declare injected dependencies the same
way you would in any other pyramid_di service.
from tet.view import ServiceViews, view_config
from pyramid_di import autowired
class UserViews(ServiceViews):
# A request-scoped service injected by pyramid_di.
user_service = autowired(UserService)
@view_config(route_name="users", renderer="json")
def list_users(self):
# self.request and self.context are available.
return {"users": self.user_service.all()}
@view_config(
route_name="user",
renderer="json",
request_method="GET",
)
def show_user(self):
user_id = self.request.matchdict["id"]
return {"user": self.user_service.get(user_id)}
When you decorate methods of a class with view_config, Pyramid records the
method name as the view’s attr. On scan, Pyramid instantiates the class
with the request and calls the named method, which is why self.request is
populated. Combine that with view_defaults to share settings across every
method of the class:
from tet.view import view_config, view_defaults, ServiceViews
@view_defaults(route_name="account", renderer="json")
class AccountViews(ServiceViews):
@view_config(request_method="GET")
def read(self):
return {"id": self.context.id}
@view_config(request_method="POST")
def update(self):
self.context.update(self.request.json_body)
return {"status": "ok"}
Why use ServiceViews over plain functions? You get a natural place to keep
per-request collaborators (database sessions, domain services, the current
user) without threading them through every function call, and you get the
pyramid_di lifecycle for free.
Traversal controllers with BaseController
For traversal-based applications, tet.view.BaseController lets you
model your URL space as a tree of controller objects. A controller is both a
context (it appears as request.context) and a container: indexing into it
with controller[name] resolves the next path segment.
BaseController implements __getitem__ with two lookup strategies, in
order:
If the controller defines a
_lookup(name)method, it is tried first. Returning a value resolves the segment; raisingKeyErrorfalls through.Otherwise, if a class attribute with that name is itself a
BaseControllersubclass, it is instantiated with the current request and wired up with__parent__and__name__so Pyramid’s location machinery works.
If neither succeeds, a KeyError is raised, which Pyramid turns into a 404.
def __getitem__(self, name):
if hasattr(self, "_lookup"):
try:
return self._lookup(name)
except KeyError:
pass
child_controller = getattr(self, name, None)
if isclass(child_controller) and issubclass(child_controller, BaseController):
child = child_controller(self.request)
child.__parent__ = self
child.__name__ = name
return child
raise KeyError(f"Child not found: {name}")
A realistic controller tree mixes static child controllers (declared as nested
classes) with dynamic lookups (implemented in _lookup):
from tet.view import BaseController, expose
class UserController(BaseController):
def __init__(self, request):
self.request = request
@expose(renderer="json")
def index(self):
return {"user": self.__name__}
class UsersController(BaseController):
def __init__(self, request):
self.request = request
@expose(renderer="json")
def index(self):
# Reached at /users/ -- "index" maps to the empty view name.
return {"users": ["alice", "bob"]}
def _lookup(self, name):
# Reached at /users/<name>/ for any name.
child = UserController(self.request)
child.__parent__ = self
child.__name__ = name
return child
class RootController(BaseController):
def __init__(self, request):
self.request = request
# Static child: /users/ resolves to UsersController.
users = UsersController
Wire the root controller up as your root factory and Tet/Pyramid will traverse through the tree segment by segment, calling the exposed method on whichever controller is the final context.
Exposing controller methods with expose
Inside a BaseController, decorate the methods you want to publish with
tet.view.expose. The decorator stores its keyword settings and uses
venusian to defer registration until config.scan() runs – the same
deferred-registration pattern Pyramid uses everywhere. On scan it calls
config.add_view(...) for the method.
Two behaviours are worth remembering:
The method name becomes the view name, with one special case: a method named
indexis registered with the empty name (""), so it answers the default view for that context.profilewould answer.../profile.exposeis class-scope only. It inspects the venusian scope and raisesValueError("expose can be only applied to instance methods!")if attached anywhere other than a method defined at class scope.
from tet.view import BaseController, expose
class ArticleController(BaseController):
def __init__(self, request):
self.request = request
@expose(renderer="json")
def index(self):
# Default view for this context: /articles/<id>/
return {"article": self.__name__}
@expose(renderer="templates/comments.tk", request_method="GET")
def comments(self):
# Named view: /articles/<id>/comments
return {"comments": []}
Under the hood expose registers a small wrapper that re-dispatches to the
context, getattr(request.context, attr_name)(), and passes the context
class as the context predicate so the view only matches that controller.
Any extra keywords you give to expose (renderer, request_method,
permission, …) flow straight into add_view, just like view_config.
Choosing between the styles
Reach for plain
view_configfunctions for small, route-mapped endpoints.Reach for
ServiceViewswhen you want route-mapped views with injected dependencies and shared per-request state – this is the recommended default for most Tet applications.Reach for
BaseControllerplusexposewhen your application is traversal-oriented and you want the URL hierarchy to mirror an object tree.
All three coexist in the same application; pick per feature, not per project.
Registering and scanning
None of these decorators take effect until the configurator scans them. A minimal setup looks like:
from pyramid.config import Configurator
def main(global_config, **settings):
with Configurator(settings=settings) as config:
config.include("pyramid_di") # required for ServiceViews
config.add_route("users", "/users")
config.add_route("user", "/users/{id}")
config.scan("myapp.views") # picks up view_config / expose
return config.make_wsgi_app()
For traversal controllers, also set your RootController as the root factory
(see SQLAlchemy Integration for the SQLAlchemy-backed variant) and scan the module
that defines them so expose registrations are collected.
See also
Viewlets – composing reusable view fragments within a page.
Configuration – including Tet modules and wiring the configurator.
SQLAlchemy Integration – root factories for traversal-based controllers.
JSON Handling – the JSON renderer used by the examples above.
The upstream Pyramid views documentation.