Magic

Pybald only has a few ‘magical’ behaviors. This documentation explains how they work.

Loading Controllers

Pybald uses a ‘controller registry’ for keeping track of which classes are to be used as controllers. This registry is just a list of all controllers that have been imported / loaded in the current project. To register a class to act as a controller, you can inherit from the pybald.core.controllers.Controller class. Inheriting from this class will automatically register the class as a controller.

>>> from pybald.core.controllers import Controller
>>> class PostCommentController(Controller):
...    pass
...
>>> print Controller.registry
[<class '__main__.PostCommentController'>]

While you can organize your controller classes any way you wish, the convention is to place one controller per module with a filename that matches the name of the contained controller. So, for example, a HomeController class might be located in a file named: ./app/controllers/home_controller.py. Additionally, for a controller to be registered, its class must be defined and/or its containing module imported. There are some convenience utilities for auto-importing entire paths which will be discussed later.

It’s also recommended that you follow the Python style guide (PEP8). Controller module (file) names are expected to be all lowercase, using underscores to separate words. Controller class names are expected to follow the CapWords convention.

On startup, when the pybald Router is first instantiated, but before the web application starts, the Router is passed a controller registry to use for associating with the route map.

Again taking cues from Ruby on Rails, the initial project is configured with a default route /{controller}/{action}/{id}. This default route means that just by defining a class, inheriting from BaseClass, a URL will become immediately available at the ‘lookup’ value without any further configuration or additional route creation.

In the above example /post_comment would become available as a project route and the router would instantiate a PostCommentController object to handle the request.

The @action decorator

Actions are any methods in Controllers with the exception of any methods whose name begins with an underscore. The standard pattern is to use the @action decorator to decorate each method you intend to use as an action. The @action decorator provides numerous convenience behaviors. A typical action will look like the following:

class HomeController(BaseController):
  '''A sample home controller'''

  @action
  def say_hello(self, req):
    return "hello world"

From the Router, each method is called with the WSGI call signature (environ, start_response). The action decorator uses the WSGI call to create a WebOb Request object which is passed to the method. Generally WebOb requests are easier and more convenient to deal with. The action decorator also provides three other convenience behaviors.

The first is that it assigns to the controller instance, data that is parsed/processed per request (such as session, user, or url variables.) The second is that it processes the name of the controller/action and uses that to define a default template for this call. Lastly, the action decorator adds some intelligent behaviors for the return value of the action. If the action returns a string, the string will be wrapped in a Response object and returned. If the action returns a Response object directly, the action will return that. If the action returns nothing (or None) then an attempt will be made to render the expected template for this action.

def action(method):
    '''
    Decorates methods that are WSGI apps to turn them into pybald-style actions.

    :param method: A method to turn into a pybald-style action.

    This decorator is usually used to take the method of a controller instance
    and add some syntactic sugar around it to allow the method to use WebOb
    Request and Response objects. It will work with any method that
    implements the WSGI spec.

    It allows actions to work with WebOb request / response objects and handles
    default behaviors, such as displaying the view when nothing is returned,
    or setting up a plain text Response if a string is returned. It also
    assigns instance variables from the ``pybald.extension`` environ variables
    that can be set from other parts of the WSGI pipeline.

    This decorator is optional but recommended for making working
    with requests and responses easier.
    '''
    # the default template name is the controller class + method name
    # the method name is pulled during decoration and stored for use
    # in template lookups
    template_name = method.__name__
    # special case where 'call' or 'index' use the base class name
    # for the template otherwise use the base name
    if template_name in ('index', '__call__'):
        template_name = ''

    @wraps(method)
    def action_wrapper(self, environ, start_response):
        req = Request(environ)
        # add any url variables as members of the controller
        for varname, value in req.urlvars.items():
            # Set the controller object to contain the url variables
            # parsed from the dispatcher / router
            setattr(self, varname, value)

        # add the pybald extension dict to the controller
        # object
        for key, value in req.environ.setdefault('pybald.extension', {}).items():
            setattr(self, key, value)

        # TODO: fixme this is a hack
        setattr(self, 'request', req)
        setattr(self, 'request_url', req.url)

        # set pre/post/view to a no-op if they don't exist
        pre = getattr(self, '_pre', noop_func)
        post = getattr(self, '_post', noop_func)

        # set the template_id for this request
        self.template_id = get_template_name(self, template_name)

        # The response is either the controllers _pre code, whatever
        # is returned from the controller
        # or the view. So pre has precedence over
        # the return which has precedence over the view
        resp = (pre(req) or
                 method(self, req) or
                 context.render(template=self.template_id,
                             data=self.__dict__ or {}))
        # if the response is currently a string
        # wrap it in a response object
        if isinstance(resp, str) or isinstance(resp, bytes):
            resp = Response(body=resp, charset="utf-8")
        # run the controllers post code
        post(req, resp)
        return resp(environ, start_response)
    return action_wrapper