"""
View utilities and base classes for Tet applications.
This module provides view-related utilities including:
- :class:`view_config` - Extended Pyramid view configuration decorator
- :class:`expose` - Decorator for exposing controller methods as views
- :class:`BaseController` - Base class for traversal-based controllers
- :class:`ServiceViews` - Base class for service-based view classes
Example
-------
Using the expose decorator with controllers::
from tet.view import BaseController, expose
class UserController(BaseController):
@expose(renderer="json")
def index(self):
return {"users": []}
@expose(renderer="json")
def profile(self):
return {"user": "john"}
Using ServiceViews for dependency injection::
from tet.view import ServiceViews
from pyramid.view import view_config
class UserViews(ServiceViews):
@view_config(route_name="users", renderer="json")
def list_users(self):
# self.request and self.context are available
return {"users": []}
"""
from inspect import isclass
from pyramid.request import Request
from pyramid.view import *
from pyramid.view import view_config as _pyramid_view_config
from pyramid_di import RequestScopedBaseService
[docs]
class view_config(_pyramid_view_config):
"""Extended Pyramid view_config decorator."""
def __init__(self, **settings):
super(view_config, self).__init__(**settings)
[docs]
class expose(object):
"""
Decorator for exposing controller methods as views.
Use on methods of :class:`BaseController` subclasses. The method name
becomes the view name (``index`` becomes the default view).
"""
venusian = venusian
def __init__(self, **settings):
self.__dict__.update(settings)
def __call__(self, wrapped):
settings = self.__dict__.copy()
def callback(context, name, ob):
config = context.config.with_package(info.module)
name = attr_name
if name == "index":
name = ""
def view_wrapper(request):
# TODO: should we stack the request?
return getattr(request.context, attr_name)()
config.add_view(view=view_wrapper, name=name, context=ob, **settings)
info = self.venusian.attach(wrapped, callback, category="pyramid")
if info.scope != "class":
# if the decorator was attached to a method in a class, or
# otherwise executed at class scope, we need to set an
# 'attr' into the settings if one isn't already in there
raise ValueError("expose can be only applied to instance methods!")
attr_name = wrapped.__name__
settings["_info"] = info.codeinfo # fbo "action_method"
return wrapped
[docs]
class BaseController(object):
"""
Base class for traversal-based controllers.
Supports nested controllers as class attributes and custom lookup
via ``_lookup`` method.
"""
def __getitem__(self, name):
"""Look up child controller by 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("Child not found: %s" % name)
[docs]
class ServiceViews(RequestScopedBaseService):
"""
Base class for view classes with dependency injection support.
Provides ``self.request`` and ``self.context`` attributes.
"""
def __init__(self, request: Request):
super().__init__(request=request)
self.context = getattr(request, "context", None)