Getting started with Cherrypy and Jinja2
Asked Answered
C

2

11

This is my first time delving into web development in python. My only other experience is PHP, and I never used a framework before, so I'm finding this very intimidating and confusing.

I'm interested in learning CherryPy/Jinja2 to make a ZFS Monitor for my NAS. I've read through the basics of the docs on CherryPy/Jinja2 but I find that the samples are disjointed and too simplistic, I don't really understand how to make these 2 things "come together" gracefully.

Some questions I have:

  1. Is there a simple tutorial shows how you make CherryPy and Jinja2 work together nicely? I'm either finding samples that are too simple, like the samples on CherryPy / Jinja2 docs, or way to complex. (example: https://github.com/jovanbrakus/cherrypy-example).

  2. Is there a standardized or "expected" way to create web applications for CherryPy? (example: What should my directory structure look like? Is there a way to declare static things; is it even necessary?)

  3. Does anyone have recommended literature for this or is the online documentation the best resource?

Cosetta answered 30/5, 2013 at 19:17 Comment(0)
S
34

Congratulations on choosing Python, I'm sure you'll learn to love it as have I.

Regarding CherryPy, I'm not an expert, but was also in the same boat as you a few days ago and I'd agree that the tutorials are a little disjointed in parts.

For integrating Jinja2, as in their doc page, the snippet of HTML should have been specified that it is the template file and as such saved in the path /templates/index.html. They also used variables that didn't match up in the template code sample and controller sample.

The below is instead a complete working sample of a simple hello world using CherryPy and Jinja2

/main.py:

import cherrypy
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))

class Root:
    @cherrypy.expose
    def index(self):
        tmpl = env.get_template('index.html')
        return tmpl.render(salutation='Hello', target='World')

cherrypy.config.update({'server.socket_host': '127.0.0.1',
                         'server.socket_port': 8080,
                        })

cherrypy.quickstart(Root())

/templates/index.html:

<h1>{{ salutation }} {{ target }}</h1>

Then in your shell/command prompt, serve the app using:

python main.py

And in your browser you should be able to see it at http://localhost:8080

That hopefully helps you to connect Jinja2 templating to your CherryPy app. CherryPy really is a lightweight and very flexible framework, where you can choose many different ways to structure your code and file structures.

Shiftless answered 31/5, 2013 at 7:41 Comment(3)
I had trouble figuring out a good project structure as well and this post and the links included helped clarify things a bit. groups.google.com/forum/?fromgroups#!topic/cherrypy-users/…Kronstadt
Nice! this intro is enough for me to get started for CherryPy and Mako.Kennykeno
Upvote, but please help, because my problem continues with Nginx #23359595.Disciplinarian
M
13

Application structure

First about standard directory structure of a project. There is none, as CherryPy doesn't mandate it, neither it tells you what data layer, form validation or template engine to use. It's all up to you and your requirements. And of course as this is a great flexibility as it causes some confusion to beginners. Here's how a close to real-word application directory structure may look like.

.           — Python virtual environment
└── website — cherryd to add this to sys.path, -P switch
    ├── application
    │   ├── controller.py — request routing, model use
    │   ├── model.py      — data access, domain logic
    │   ├── view          — template
    │   │   ├── layout
    │   │   ├── page
    │   │   └── part
    │   └── __init__.py — application bootstrap
    ├── public
    │   └── resource — static
    │       ├── css
    │       ├── image
    │       └── js
    ├── config.py — configuration, environments
    └── serve.py  — bootstrap call, cherryd to import this, -i switch

Then standing in the root of virtual environment you usually do the following to start CherryPy in development environment. cherryd is CherryPy's suggest way of running an application.

. bin/activate
cherryd -i serve -P website

Templating

Now let's look closer to the template directory and what it can look like.

.
├── layout
│   └── main.html
├── page
│   ├── index
│   │   └── index.html
│   ├── news
│   │   ├── list.html
│   │   └── show.html
│   ├── user
│   │   └── profile.html
│   └── error.html     
└── part
    └── menu.html

To harness nice Jinja2's feature of template inheritance, here are layouts which define structure of a page, the slots that can be filled in a particular page. You may have layout for a website and layout for email notifications. There's also a directory for a part, reusable snippet used across different pages. Now lets see the code that corresponds the structure above.

I've made the following also available as a runnable which is easier to navigate files, you can run and play with it. The paths start with . like in the first section's tree.

website/config.py

# -*- coding: utf-8 -*-


import os


path   = os.path.abspath(os.path.dirname(__file__))
config = {
  'global' : {
    'server.socket_host' : '127.0.0.1',
    'server.socket_port' : 8080,
    'server.thread_pool' : 8,

    'engine.autoreload.on' : False,

    'tools.trailing_slash.on' : False
  },
  '/resource' : {
    'tools.staticdir.on'  : True,
    'tools.staticdir.dir' : os.path.join(path, 'public', 'resource')
  }
}

website/serve.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-


from application import bootstrap


bootstrap()


# debugging purpose, e.g. run with PyDev debugger
if __name__ == '__main__':
  import cherrypy
  cherrypy.engine.signals.subscribe()
  cherrypy.engine.start()
  cherrypy.engine.block()

website/application/__init__.py

Notable part here is a CherryPy tool which helps to avoid boilerplate related with rendering templates. You just need return a dict from CherryPy page handler with data for the template. Following convention-over-configuration principle, the tool when not provided with template name will use classname/methodname.html e.g. user/profile.html. To override the default template you can use @cherrypy.tools.template(name = 'other/name'). Also note that the tool exposes a method automatically, so you don't need to append @cherrypy.expose on top

# -*- coding: utf-8 -*-


import os
import types

import cherrypy
import jinja2

import config


class TemplateTool(cherrypy.Tool):

  _engine = None
  '''Jinja environment instance'''


  def __init__(self):
    viewLoader   = jinja2.FileSystemLoader(os.path.join(config.path, 'application', 'view'))
    self._engine = jinja2.Environment(loader = viewLoader)

    cherrypy.Tool.__init__(self, 'before_handler', self.render)

  def __call__(self, *args, **kwargs):
    if args and isinstance(args[0], (types.FunctionType, types.MethodType)):
      # @template
      args[0].exposed = True
      return cherrypy.Tool.__call__(self, **kwargs)(args[0])
    else:
      # @template()
      def wrap(f):
        f.exposed = True
        return cherrypy.Tool.__call__(self, *args, **kwargs)(f)
      return wrap

  def render(self, name = None):
    cherrypy.request.config['template'] = name

    handler = cherrypy.serving.request.handler
    def wrap(*args, **kwargs):
      return self._render(handler, *args, **kwargs)
    cherrypy.serving.request.handler = wrap

  def _render(self, handler, *args, **kwargs):
    template = cherrypy.request.config['template']
    if not template:
      parts = []
      if hasattr(handler.callable, '__self__'):
        parts.append(handler.callable.__self__.__class__.__name__.lower())
      if hasattr(handler.callable, '__name__'):
        parts.append(handler.callable.__name__.lower())
      template = '/'.join(parts)

    data     = handler(*args, **kwargs) or {}
    renderer = self._engine.get_template('page/{0}.html'.format(template))

    return renderer.render(**data) if template and isinstance(data, dict) else data


def bootstrap():
  cherrypy.tools.template = TemplateTool()

  cherrypy.config.update(config.config)

  import controller

  cherrypy.config.update({'error_page.default': controller.errorPage})
  cherrypy.tree.mount(controller.Index(), '/', config.config)

website/application/controller.py

As you can see with use of the tool page handlers look rather clean and will be consistent with other tools, e.g. json_out.

# -*- coding: utf-8 -*-


import datetime

import cherrypy


class Index:

  news = None
  user = None


  def __init__(self):
    self.news = News()
    self.user = User()

  @cherrypy.tools.template
  def index(self):
    pass

  @cherrypy.expose
  def broken(self):
    raise RuntimeError('Pretend something has broken')


class User:

  @cherrypy.tools.template
  def profile(self):
    pass


class News:

  _list = [
    {'id': 0, 'date': datetime.datetime(2014, 11, 16), 'title': 'Bar', 'text': 'Lorem ipsum'},
    {'id': 1, 'date': datetime.datetime(2014, 11, 17), 'title': 'Foo', 'text': 'Ipsum lorem'}
  ]


  @cherrypy.tools.template
  def list(self):
    return {'list': self._list}

  @cherrypy.tools.template
  def show(self, id):
    return {'item': self._list[int(id)]}


def errorPage(status, message, **kwargs):
  return cherrypy.tools.template._engine.get_template('page/error.html').render()

In this demo app I used blueprint css file, to demonstrate how static resource handling works. Put it in website/application/public/resource/css/blueprint.css. The rest is less interesting, just Jinja2 templates for completeness.

website/application/view/layout/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv='content-type' content='text/html; charset=utf-8' />
    <title>CherryPy Application Demo</title>
    <link rel='stylesheet' media='screen' href='/resource/css/blueprint.css' />
  </head>
  <body>
    <div class='container'>
      <div class='header span-24'>
        {% include 'part/menu.html' %}
      </div>
      <div class='span-24'>{% block content %}{% endblock %}</div>      
    </div>
  </body>
</html>

website/application/view/page/index/index.html

{% extends 'layout/main.html' %}
{% block content %}
  <div class='span-18 last'>
      <p>Root page</p>      
  </div>
{% endblock %}

website/application/view/page/news/list.html

{% extends 'layout/main.html' %}
{% block content %}
  <div class='span-20 last prepend-top'>
    <h1>News</h1>
    <ul>
    {% for item in list %}
      <li><a href='/news/show/{{ item.id }}'>{{ item.title }}</a> ({{ item.date }})</li>
    {% endfor %}
    </ul>
  </div>
{% endblock %}

website/application/view/page/news/show.html

{% extends 'layout/main.html' %}
{% block content %}
  <div class='span-20 last prepend-top'>
    <h2>{{ item.title }}</h2>
    <div class='span-5 last'>{{ item.date }}</div>
    <div class='span-19 last'>{{ item.text }}</div>
  </div>
{% endblock %}

website/application/view/page/user/profile.html

{% extends 'layout/main.html' %}
{% block content %}
  <div class='span-18'>
    <table>
      <tr><td>First name:</td><td>John</td></tr>
      <tr><td>Last name:</td><td>Doe</td></tr>
    <table>
  </div>
{% endblock %}

website/application/view/page/error.html

It's a 404-page.

{% extends 'layout/main.html' %}
{% block content %}
  <h1>Error has happened</h1>
{% endblock %}

website/application/view/part/menu.html

<div class='span-4 prepend-top'>
  <h2><a href='/'>Website</a></h2>
</div>
<div class='span-20 prepend-top last'>
    <ul>
      <li><a href='/news/list'>News</a></li>
      <li><a href='/user/profile'>Profile</a></li>
      <li><a href='/broken'>Broken</a></li>
    </ul>
</div>

References

Code above goes closely with backend section of qooxdoo-website-skeleton. For full-blown Debain deployment of such application, cherrypy-webapp-skeleton may be useful.

Macrospore answered 17/11, 2014 at 13:14 Comment(2)
Using your template tool, would it be possible to override the template from within the function? For example, if I wanted the default template behavior in most cases, but if a certain set of conditions are met switch to another template path?Anklebone
@Justin Yes. Because the tool wraps the handler you need to change it a little so your handler's side effect isn't overridden. Just make data = handler(*args, **kwargs) or {} be on first line in TemplateTool._render and your code will work.Macrospore

© 2022 - 2024 — McMap. All rights reserved.