CherryPy version 3.2 is the first major web application server to be made available on the Python 3 platform. It can be downloaded from http://cherrypy.org/. It is not a full-stack framework like the very popular Django, TurboGears, or Zope libraries. These frameworks provide extra support for data storage, templating, authentication, and other common web operations. Such features are not impossible in CherryPy, you're just responsible for finding or implementing them yourself.
CherryPy is a very powerful web server that uses a simple design for building web applications. Let's jump in head-first with a simple example that serves the HTML file we developed in the previous section:
import cherrypy class SimplePage: @cherrypy.expose def index(self): with open("html_document.html") as file: return file.read() cherrypy.quickstart(SimplePage())
If we run this program, we can visit http://localhost:8080/ in a web browser to see the web page in action. All we've done here is create a class to pass to the quickstart
function. That function starts a web server and serves pages from that class. Any methods of the class we created that have been marked as exposed
will be made available via HTTP at a URL with the same name as the method. Any method not explicitly marked as exposed can be used internally as a helper method, but cannot be accessed at any URL.
The method itself simply opens a file and returns the contents of that file. Ultimately, we've written a web application that serves a single HTML 5 web page.
Of course, having a site that serves only one web page is pretty boring. Let's look at an example that is just a touch more exciting:
import cherrypy template = """<!DOCTYPE html> <html> <body> {message} </body> </html>""" class AboutPage: @cherrypy.expose def index(self): return template.format(message=""" I'm not a very interesting person...""") @cherrypy.expose def contactPage(self): print(self) return template.format( message="I can't be contacted anywhere.") class MainPage: about = AboutPage() contact = contactPage @cherrypy.expose def index(self): return template.format(message=""" This is the main page. <a href="/about/">About Me</a> <a href="http:///contact/">Contact Me</a> <a href="/links/">Some Links</a> """) @cherrypy.expose def links(self): return template.format( message="No Links Yet") cherrypy.quickstart(MainPage())
This example shows three ways that pages can be added to a site. The obvious one is to add an exposed method, such as the links
method above, to the class. But we can add exposed objects in other ways too:
contactPage
aboutPage
app.some_page = AnExposedClass()
You've probably figured out already that the index
method is a special method. It doesn't map to the /index
URL; instead, it is the method called if no path is added after the ending slash.
We can also accept HTML form arguments. Let's create a real contact page:
import cherrypy
class ContactPage:
@cherrypy.expose
def index(self, message=None):
if message:
print("The user submitted:
{0}".format(
message))
return "Thank you!"
return """<form>
<textarea name="message"></textarea>
<input type="submit" />
</form>"""
cherrypy.quickstart(ContactPage())
This page displays a different result depending on the presence of a message
variable in the keyword arguments. If no such argument is supplied, the visitor is presented with a form to enter a message in. If the argument is supplied, the value of the message is printed to the console (normally we'd do something useful with the value, such as e-mailing it somewhere or storing it in a database or file for later retrieval). Then a thank you message is returned to the client.
So how did that message
parameter get set? Basically, any named inputs in a form (in this case, the message textarea)
are mapped to keyword arguments when the page is submitted. It's that simple!
As we discussed, CherryPy is just a web application server; it is not a web framework. It provides a complete web server and the basic features to map HTTP requests to code that must be executed when those requests are made. It also provides, with a bit of configuration, complete SSL support, the ability to set and retrieve cookies, caching support, HTTP authentication, and sessions. However, it is missing two key features that many other frameworks supply: templating and data storage.
Many websites use databases for data storage, but CherryPy does not supply this ability. Do we really need it to? We really just need database connectivity; it doesn't have to be built into the web framework. Indeed, why don't we just use SQLAlchemy, which we discussed earlier in the chapter? In fact, this is what the TurboGears framework uses for its database access.
This then, still leaves us to solve the templating problem, another framework feature that CherryPy lacks. Templating is the process of taking static strings or files, and replacing certain substrings in those files with new strings, based on some kind of context. The str.format
function we covered in Chapter 10 is a basic example of templating. It allows us to replace modifiers with variables passed into the function. Indeed, this was the template method we used in the example of a simple CherryPy application earlier.
Most template languages go beyond this ability to allow things like conditionals (including data in the template only if a certain condition is met, such as two variables being equal, or a user being logged in), and looping (including data in a template repeatedly, such as creating a table or unordered list containing multiple items from a Python list). Some even go so far as to allow arbitrary Python code to be executed within the template.
There are a myriad opinions on what a template language should be, which is why, for Python 2, there have been an immeasurable number of different template languages devised. This diversity hasn't spread to Python 3 yet, but one of the most powerful templating languages, Jinja2 is already available on the Python 3 platform. It can be downloaded from http://jinja.pocoo.org/.
As a sort of case study, let's take these three tools―CherryPy, SQLAlchemy, and Jinja―and create a quick and dirty blogging engine! We'll start with the SQLAlchemy models; these define the data that will be stored in the database:
import datetime import sqlalchemy as sqa from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Article(Base): __tablename__ = "article" rowid = sqa.Column(sqa.Integer, primary_key=True) title = sqa.Column(sqa.String) message = sqa.Column(sqa.String) pub_date = sqa.Column(sqa.DateTime) def __init__(self, title, message): self.title = title self.message = message self.pub_date=datetime.datetime.now() class Comment(Base): __tablename__ = "comment" rowid = sqa.Column(sqa.Integer, primary_key=True) article_id = sqa.Column(sqa.Integer, sqa.ForeignKey('article.rowid')) article = sqa.orm.relationship(Article, backref="comments") name = sqa.Column(sqa.String) message = sqa.Column(sqa.String) def __init__(self, article_id, name, message): self.article_id = article_id self.name = name self.message = message engine = sqa.create_engine('sqlite:///blog.db') Base.metadata.create_all(engine) Session = sqa.orm.sessionmaker(bind=engine)
We create two models with some fields. The two models are associated with a ForeignKey
relationship on the Comment
class.
The rowid
field is a special one; in SQLite, every model is automatically given a unique integer rowid
. We don't have to do anything to populate this number, it's simply available from the database. This wouldn't work with PostgreSQL or another engine; we'd have to set up a sequence or autoincrement
field instead.
We add an __init__
method to each class to make it easier to construct new instances. Then we associate the engine, create the tables, and create a Session
class to interact with the database later.
Now, we can set up Jinja to serve some templates from a folder for us:
import jinja2 templates = jinja2.Environment(loader=jinja2.FileSystemLoader( 'blog_templates'))
Well that was easy. This gives us a templates
variable that we can use to load templates based on filename from the given folder. Before we create the CherryPy app server, let's have a look at the templates. Let's scrutinize the simple template for adding a blog article first:
{% extends "base.html" %} {% block title %}New Entry{% endblock %} {% block content %} <form method="POST" action="/process_add/"> Title: <input name="title" type="text" size="40" /><br /> <textarea name="message" rows="10" cols="40"> </textarea><br /> <input type="submit" value="Publish" /> </form> {% endblock %}
This sort of resembles normal HTML, but all those {%
things are new. That's Jinja markup (it's also very similar to Django markup, if you have used or are interested in using Django's template system) for a template tag. Template tags are instructions to the templating system to do something special here. There are two types of template tags in use: extends
, and block
. The extends
tag essentially tells the template system to start with the base.html
, but replace any named blocks with the named blocks in this file. And that's what the block
and endblock
tags are; named blocks to override whatever is specified in the parent template, base.html
. This may be a bit clearer if we know what base.html
looks like:
<!DOCTYPE html>
<html>
<head><title>{% block title %}{% endblock %}</title></head>
<body>
<h1>My Blog</h1>
<ul>
<li><a href="http:///>Main</a></li>
<li><a href="/add/">Add Entry</a></li>
</ul>
<hr />
{% block content %}
{% endblock %}
<body>
<html>
This looks even more like a normal HTML page; it shows where the two named blocks should go in the context of a larger page.
Extending base.html
in other templates allows us to ignore the parts of every page that stay the same. Further, if we want to add a link to the menu or otherwise modify the overall site, we only have to do it in this single template file.
The other template, index.html
is substantially more complex:
{% extends "base.html" %} {% block title %}My Blog{% endblock %} {% block content %} {% for article in articles %} <h2>{{article.title}}</h2> <em>{{article.pub_date.strftime('%b %d %Y')}}</em> <p>{{article.message}}</p> <div style="margin-left: 6em"> <h3>Comments</h3> {% for comment in article.comments %} <em>{{comment.name}} wrote:</em> <p> {{comment.message}} </p> {% endfor %} {% include "comment_form.html" %} </div> <hr /> {% endfor %} {% endblock %}
It includes the same extends
and block
tags as the earlier template. In addition, it introduces us to the for
template tag, which loops over all the articles (or all the comments in an article) and renders slightly different HTML for each of them. It also renders a bunch of variables using the {{<variable_name>}}
syntax. The variable names are passed into the template from our CherryPy application or are assigned within the context, as is done inside the for
loops.
The rendering of the pub_date
variable on the article is particularly interesting, as the item is a datetime.datetime
object, and we can see that Jinja allows us to call the strftime
method on this object directly.
Finally, the include
tag allows us to render part of the template in a separate file, comment_form.html
, which looks like this:
<form method="POST" action="/process_comment/{{article.rowid}}/"> Name: <input name="name" type="text" size="30" /><br /> <textarea name="message" rows="5" cols="30"> </textarea><br /> <input type="submit" value="Comment" /> </form>
That's basic Jinja syntax in a nutshell; there's a lot more that can be done with it, of course, but these basics are enough to get you interested. They're also enough for our simple blog engine!
In the interest of understanding how web applications are designed, note that I didn't write those templates before I wrote the CherryPy application we're about to see. Instead, I developed iteratively, creating both the code and templates to add an article, followed by the code and templates to display an article, and finally, setting up the comment system. I grouped all the resulting templates together in the above section so we could focus on Jinja template syntax. Now, let's focus on CherryPy and how those templates are called!
First, here's our blog engine with the index method:
import cherrypy class Blog: @cherrypy.expose def index(self): session = Session() articles = session.query(Article).all() template = templates.get_template("index.html") content = template.render(articles=articles) session.close() return content cherrypy.quickstart(Blog())
Here's where we start to see our three puzzle pieces merging together. CherryPy, of course, is serving the page. Jinja is creating the page using our templates. And SQLAlchemy is giving Jinja the data it needs to display
First we construct a session
and use it to search for all the available articles. We then get a template by name; this comes from the templates
object we set up earlier in the module. Then we render the template, passing one keyword argument into it. Keyword arguments map to variables inside the template context; the template we defined earlier will loop over the articles we passed in this function. Then we return the rendered content to let CherryPy display it.
The code to display the form for adding a new article is even simpler; we just render the template, since it doesn't need any variables:
@cherrypy.expose def add(self): template = templates.get_template("add.html") return template.render()
You may have noticed in our templates that the forms for adding articles and comments have action attributes pointing to process_add
and process_comment
URLs. The process_add
URL simply constructs a new article from the form parameters (title and name), which come to us from CherryPy as keyword arguments. Then it raises an exception to redirect the client to the main view, which will display the new article:
@cherrypy.expose def process_add(self, title=None, message=None): session = Session() article = Article(title, message) session.add(article) session.commit() session.close() raise cherrypy.HTTPRedirect("/")
The process_comment
method is much the same, except it also accepts a positional argument. Positional arguments come between forward slash characters in the URL, so the following method signature would actually map to /process_comment/3/
if an article_id
of 3
is passed:
@cherrypy.expose def process_comment(self, article_id, name=None, message=None): session = Session() comment = Comment(article_id, name, message) session.add(comment) session.commit() session.close() raise cherrypy.HTTPRedirect("/")
And there we have it, a complete, simple blog engine with absolutely no authentication and which will fill up with spam in a matter of minutes. But it works! And we wrote it all using Python 3 objects.
18.188.152.136