Quick Tour of Pyramid

Pyramid lets you start small and finish big. This Quick Tour of Pyramid is for those who want to evaluate Pyramid, whether you are new to Python web frameworks, or a pro in a hurry. For more detailed treatment of each topic, give the Quick Tutorial for Pyramid a try.

If you would prefer to cut and paste the example code in this tour you may browse the source code located in the Pyramid repository in the directory "docs/quick_tour" <https://github.com/Pylons/pyramid/>. If you have downloaded the source code, you will find the tour in the same location.

Installation

Once you have a standard Python environment setup, getting started with Pyramid is a breeze. Unfortunately "standard" is not so simple in Python. For this Quick Tour, it means Python, venv, pip, and Setuptools.

To save a little bit of typing and to be certain that we use the modules, scripts, and packages installed in our virtual environment, we'll set an environment variable, too.

As an example, for Python 3 on Linux:

# set an environment variable to where you want your virtual environment
$ export VENV=~/env
# create the virtual environment
$ python3 -m venv $VENV
# install pyramid
$ $VENV/bin/pip install pyramid
# or for a specific released version
$ $VENV/bin/pip install "pyramid==2.0"

For Windows:

# set an environment variable to where you want your virtual environment
c:\> set VENV=c:\env
# create the virtual environment
c:\> python -m venv %VENV%
# install pyramid
c:\> %VENV%\Scripts\pip install pyramid
# or for a specific released version
c:\> %VENV%\Scripts\pip install "pyramid==2.0"

As of version 2.0, Pyramid runs on Python 3 only. For simplicity, the remaining examples will show only Unix commands.

Hello World

Microframeworks have shown that learning starts best from a very small first step. Here's a tiny application in Pyramid:

 1from wsgiref.simple_server import make_server
 2from pyramid.config import Configurator
 3from pyramid.response import Response
 4
 5
 6def hello_world(request):
 7    return Response('Hello World!')
 8
 9
10if __name__ == '__main__':
11    with Configurator() as config:
12        config.add_route('hello', '/')
13        config.add_view(hello_world, route_name='hello')
14        app = config.make_wsgi_app()
15    server = make_server('0.0.0.0', 6543, app)
16    server.serve_forever()

This simple example is easy to run. Save this as app.py and run it:

$ $VENV/bin/python ./app.py

Next open http://localhost:6543/ in a browser, and you will see the Hello World! message.

New to Python web programming? If so, some lines in the module merit explanation:

  1. Lines 6-7. Implement the view code that generates the response.

  2. Line 10. if __name__ == '__main__': is Python's way of saying "Start here when running from the command line".

  3. Lines 11-13. Use Pyramid's configurator in a context manager to connect view code to a particular URL route.

  4. Lines 14-16. Publish a WSGI app using an HTTP server.

As shown in this example, the configurator plays a central role in Pyramid development. Building an application from loosely-coupled parts via Application Configuration is a central idea in Pyramid, one that we will revisit regularly in this Quick Tour.

Handling web requests and responses

Developing for the web means processing web requests. As this is a critical part of a web application, web developers need a robust, mature set of software for web requests.

Pyramid has always fit nicely into the existing world of Python web development (virtual environments, packaging, cookiecutters, one of the first to embrace Python 3, etc.). Pyramid turned to the well-regarded WebOb Python library for request and response handling. In our example above, Pyramid hands hello_world a request that is based on WebOb.

Let's see some features of requests and responses in action:

def hello_world(request):
    # Some parameters from a request such as /?name=lisa
    url = request.url
    name = request.params.get('name', 'No Name Provided')

    body = 'URL %s with name: %s' % (url, name)
    return Response(
        content_type="text/plain",
        body=body
    )

In this Pyramid view, we get the URL being visited from request.url. Also if you visited http://localhost:6543/?name=alice in a browser, the name is included in the body of the response:

URL http://localhost:6543/?name=alice with name: alice

Finally we set the response's content type, and return the Response.

Views

For the examples above, the hello_world function is a "view". In Pyramid views are the primary way to accept web requests and return responses.

So far our examples place everything in one file:

  • the view function

  • its registration with the configurator

  • the route to map it to an URL

  • the WSGI application launcher

Let's move the views out to their own views.py module and change the app.py to scan that module, looking for decorators that set up the views.

First our revised app.py:

 1from wsgiref.simple_server import make_server
 2from pyramid.config import Configurator
 3
 4if __name__ == '__main__':
 5    with Configurator() as config:
 6        config.add_route('home', '/')
 7        config.add_route('hello', '/howdy')
 8        config.add_route('redirect', '/goto')
 9        config.add_route('exception', '/problem')
10        config.scan('views')
11        app = config.make_wsgi_app()
12    server = make_server('0.0.0.0', 6543, app)
13    server.serve_forever()

We added some more routes, but we also removed the view code. Our views and their registrations (via decorators) are now in a module views.py, which is scanned via config.scan('views').

We now have a views.py module that is focused on handling requests and responses:

 1from html import escape
 2
 3from pyramid.httpexceptions import HTTPFound
 4from pyramid.response import Response
 5from pyramid.view import view_config
 6
 7
 8# First view, available at http://localhost:6543/
 9@view_config(route_name='home')
10def home_view(request):
11    return Response('<p>Visit <a href="/howdy?name=lisa">hello</a></p>')
12
13
14# /howdy?name=alice which links to the next view
15@view_config(route_name='hello')
16def hello_view(request):
17    name = request.params.get('name', 'No Name')
18    body = '<p>Hi %s, this <a href="/goto">redirects</a></p>'
19    # Python html.escape to prevent Cross-Site Scripting (XSS) [CWE 79]
20    return Response(body % escape(name))
21
22
23# /goto which issues HTTP redirect to the last view
24@view_config(route_name='redirect')
25def redirect_view(request):
26    return HTTPFound(location="/problem")
27
28
29# /problem which causes a site error
30@view_config(route_name='exception')
31def exception_view(request):
32    raise Exception()

We have four views, each leading to the other. If you start at http://localhost:6543/, you get a response with a link to the next view. The hello_view (available at the URL /howdy) has a link to the redirect_view, which issues a redirect to the final view.

Earlier we saw config.add_view as one way to configure a view. This section introduces @view_config. Pyramid's configuration supports imperative configuration, such as the config.add_view in the previous example. You can also use declarative configuration in which a Python decorator is placed on the line above the view. Both approaches result in the same final configuration, thus usually it is simply a matter of taste.

Routing

Writing web applications usually means sophisticated URL design. We just saw some Pyramid machinery for requests and views. Let's look at features that help with routing.

Above we saw the basics of routing URLs to views in Pyramid:

  • Your project's "setup" code registers a route name to be used when matching part of the URL.

  • Elsewhere a view is configured to be called for that route name.

注釈

Why do this twice? Other Python web frameworks let you create a route and associate it with a view in one step. As illustrated in Routes need relative ordering, multiple routes might match the same URL pattern. Rather than provide ways to help guess, Pyramid lets you be explicit in ordering. Pyramid also gives facilities to avoid the problem.

What if we want part of the URL to be available as data in my view? We can use this route declaration, for example:

6        config.add_route('hello', '/howdy/{first}/{last}')

With this, URLs such as /howdy/amy/smith will assign amy to first and smith to last. We can then use this data in our view:

5@view_config(route_name='hello')
6def hello_world(request):
7    body = '<h1>Hi %(first)s %(last)s!</h1>' % request.matchdict
8    return Response(body)

request.matchdict contains values from the URL that match the "replacement patterns" (the curly braces) in the route declaration. This information can then be used in your view.

Templating

Ouch. We have been making our own Response and filling the response body with HTML. You usually won't embed an HTML string directly in Python, but instead you will use a templating language.

Pyramid doesn't mandate a particular database system, form library, and so on. It encourages replaceability. This applies equally to templating, which is fortunate: developers have strong views about template languages. That said, the Pylons Project officially supports bindings for Chameleon, Jinja2, and Mako. In this step let's use Chameleon.

Let's add pyramid_chameleon, a Pyramid add-on which enables Chameleon as a renderer in our Pyramid application:

$VENV/bin/pip install pyramid_chameleon

With the package installed, we can include the template bindings into our configuration in app.py:

6        config.add_route('hello', '/howdy/{name}')
7        config.include('pyramid_chameleon')
8        config.scan('views')

Now lets change our views.py file:

1from pyramid.view import view_config
2
3
4@view_config(route_name='hello', renderer='hello_world.pt')
5def hello_world(request):
6    return dict(name=request.matchdict['name'])

Ahh, that looks better. We have a view that is focused on Python code. Our @view_config decorator specifies a renderer that points to our template file. Our view then simply returns data which is then supplied to our template hello_world.pt:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Quick Glance</title>
</head>
<body>
<h1>Hello ${name}</h1>
</body>
</html>

Since our view returned dict(name=request.matchdict['name']), we can use name as a variable in our template via ${name}.

Templating with Jinja2

We just said Pyramid doesn't prefer one templating language over another. Time to prove it. Jinja2 is a popular templating system, modeled after Django's templates. Let's add pyramid_jinja2, a Pyramid add-on which enables Jinja2 as a renderer in our Pyramid applications:

$VENV/bin/pip install pyramid_jinja2

With the package installed, we can include the template bindings into our configuration:

6        config.add_route('hello', '/howdy/{name}')
7        config.include('pyramid_jinja2')
8        config.scan('views')

The only change in our view is to point the renderer at the .jinja2 file:

4@view_config(route_name='hello', renderer='hello_world.jinja2')
5def hello_world(request):
6    return dict(name=request.matchdict['name'])

Our Jinja2 template is very similar to our previous template:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Hello World</title>
</head>
<body>
<h1>Hello {{ name }}!</h1>
</body>
</html>

Pyramid's templating add-ons register a new kind of renderer into your application. The renderer registration maps to different kinds of filename extensions. In this case, changing the extension from .pt to .jinja2 passed the view response through the pyramid_jinja2 renderer.

Static assets

Of course the Web is more than just markup. You need static assets: CSS, JS, and images. Let's point our web app at a directory from which Pyramid will serve some static assets. First let's make another call to the configurator in app.py:

6        config.add_route('hello', '/howdy/{name}')
7        config.add_static_view(name='static', path='static')
8        config.include('pyramid_jinja2')

This tells our WSGI application to map requests under http://localhost:6543/static/ to files and directories inside a static directory alongside our Python module.

Next make a directory named static, and place app.css inside:

body {
    margin: 2em;
    font-family: sans-serif;
}

All we need to do now is point to it in the <head> of our Jinja2 template, hello_world.jinja2:

4    <title>Hello World</title>
5    <link rel="stylesheet" href="/static/app.css"/>
6</head>

This link presumes that our CSS is at a URL starting with /static/. What if the site is later moved under /somesite/static/? Or perhaps a web developer changes the arrangement on disk? Pyramid provides a helper to allow flexibility on URL generation:

4    <title>Hello World</title>
5    <link rel="stylesheet" href="{{ request.static_url('__main__:static/app.css') }}"/>
6</head>

By using request.static_url to generate the full URL to the static assets, you ensure that you stay in sync with the configuration and gain refactoring flexibility later.

Returning JSON

Modern web apps are more than rendered HTML. Dynamic pages now use JavaScript to update the UI in the browser by requesting server data as JSON. Pyramid supports this with a JSON renderer:

 9@view_config(route_name='hello_json', renderer='json')
10def hello_json(request):
11    return [1, 2, 3]

This wires up a view that returns some data through the JSON renderer, which calls Python's JSON support to serialize the data into JSON, and sets the appropriate HTTP headers.

We also need to add a route to app.py so that our app will know how to respond to a request for hello.json.

6        config.add_route('hello', '/howdy/{name}')
7        config.add_route('hello_json', 'hello.json')
8        config.add_static_view(name='static', path='static')

View classes

So far our views have been simple, free-standing functions. Many times your views are related. They may have different ways to look at or work on the same data, or they may be a REST API that handles multiple operations. Grouping these together as a view class makes sense and achieves the following goals.

  • Group views

  • Centralize some repetitive defaults

  • Share some state and helpers

The following shows a "Hello World" example with three operations: view a form, save a change, or press the delete button in our views.py:

 7# One route, at /howdy/amy, so don't repeat on each @view_config
 8@view_defaults(route_name='hello')
 9class HelloWorldViews:
10    def __init__(self, request):
11        self.request = request
12        # Our templates can now say {{ view.name }}
13        self.name = request.matchdict['name']
14
15    # Retrieving /howdy/amy the first time
16    @view_config(renderer='hello.jinja2')
17    def hello_view(self):
18        return dict()
19
20    # Posting to /howdy/amy via the "Edit" submit button
21    @view_config(request_param='form.edit', renderer='edit.jinja2')
22    def edit_view(self):
23        print('Edited')
24        return dict()
25
26    # Posting to /howdy/amy via the "Delete" submit button
27    @view_config(request_param='form.delete', renderer='delete.jinja2')
28    def delete_view(self):
29        print('Deleted')
30        return dict()

As you can see, the three views are logically grouped together. Specifically:

  • The first view is returned when you go to /howdy/amy. This URL is mapped to the hello route that we centrally set using the optional @view_defaults.

  • The second view is returned when the form data contains a field with form.edit, such as clicking on <input type="submit" name="form.edit" value="Save">. This rule is specified in the @view_config for that view.

  • The third view is returned when clicking on a button such as <input type="submit" name="form.delete" value="Delete">.

Only one route is needed, stated in one place atop the view class. Also, the assignment of name is done in the __init__ function. Our templates can then use {{ view.name }}.

Pyramid view classes, combined with built-in and custom predicates, have much more to offer:

  • All the same view configuration parameters as function views

  • One route leading to multiple views, based on information in the request or data such as request_param, request_method, accept, header, xhr, containment, and custom predicates.

Quick project startup with cookiecutters

So far we have done all of our Quick Tour as a single Python file. No Python packages, no structure. Most Pyramid projects, though, aren't developed this way.

To ease the process of getting started, the Pylons Project provides a cookiecutter that generates a sample Pyramid project from project templates. This cookiecutter will install Pyramid and its dependencies as well.

First you'll need to install cookiecutter.

$VENV/bin/pip install cookiecutter

Let's use the cookiecutter pyramid-cookiecutter-starter to create a starter Pyramid project in the current directory, entering values at the prompts as shown below for the following command.

$VENV/bin/cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master

If prompted for the first item, accept the default yes by hitting return.

You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: hello_world
repo_name [hello_world]: hello_world
Select template_language:
1 - jinja2
2 - chameleon
3 - mako
Choose from 1, 2, 3 [1]: 1
Select backend:
1 - none
2 - sqlalchemy
3 - zodb
Choose from 1, 2, 3 [1]: 1

We then run through the following commands.

# Change directory into your newly created project.
cd hello_world
# Create a new virtual environment...
python3 -m venv env
# ...where we upgrade packaging tools...
env/bin/pip install --upgrade pip setuptools
# ...and into which we install our project and its testing requirements.
env/bin/pip install -e ".[testing]"
# Reset our environment variable for a new virtual environment.
export VENV=~/hello_world/env

We are moving in the direction of a full-featured Pyramid project, with a proper setup for Python standards (packaging) and Pyramid configuration. This includes a new way of running your application:

$VENV/bin/pserve development.ini

Let's look at pserve and configuration in more depth.

Application running with pserve

Prior to the cookiecutter, our project mixed a number of operational details into our code. Why should my main code care which HTTP server I want and what port number to run on?

pserve is Pyramid's application runner, separating operational details from your code. When you install Pyramid, a small command program called pserve is written to your bin directory. This program is an executable Python module. It's very small, getting most of its brains via import.

You can run pserve with --help to see some of its options. Doing so reveals that you can ask pserve to watch your development files and reload the server when they change:

$VENV/bin/pserve development.ini --reload

The pserve command has a number of other options and operations. Most of the work, though, comes from your project's wiring, as expressed in the configuration file you supply to pserve. Let's take a look at this configuration file.

参考

See also: What Is This pserve Thing

Configuration with .ini files

Earlier in Quick Tour we first met Pyramid's configuration system. At that point we did all configuration in Python code. For example, the port number chosen for our HTTP server was right there in Python code. Our cookiecutter has moved this decision and more into the development.ini file:

###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###

[app:main]
use = egg:hello_world

pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
    pyramid_debugtoolbar

# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1

###
# wsgi server configuration
###

[server:main]
use = egg:waitress#main
listen = localhost:6543

###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###

[loggers]
keys = root, hello_world

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = INFO
handlers = console

[logger_hello_world]
level = DEBUG
handlers =
qualname = hello_world

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

Let's take a quick high-level look. First the .ini file is divided into sections:

  • [app:main] configures our WSGI app

  • [server:main] holds our WSGI server settings

  • Various sections afterwards configure our Python logging system

We have a few decisions made for us in this configuration:

  1. WSGI app: What package has our WSGI application in it? use = egg:hello_world in the app section tells the configuration what application to load.

  2. Easier development by automatic template reloading: In development mode, you shouldn't have to restart the server when editing a Jinja2 template. pyramid.reload_templates = true sets this policy, which might be different in production.

  3. Choice of web server: use = egg:waitress#main tells pserve to use the waitress server.

  4. Interfaces: listen = localhost:6543 tells waitress to listen on all interfaces on port 6543 for both IPv4 and IPv6.

Additionally the development.ini generated by this cookiecutter wired up Python's standard logging. We'll now see in the console, for example, a log on every request that comes in, as well as traceback information.

Easier development with debugtoolbar

As we introduce the basics, we also want to show how to be productive in development and debugging. For example, we just discussed template reloading and earlier we showed --reload for application reloading.

pyramid_debugtoolbar is a popular Pyramid add-on which makes several tools available in your browser. Adding it to your project illustrates several points about configuration.

Our cookiecutter pyramid-cookiecutter-starter already configured our package to include the add-on pyramid_debugtoolbar in its setup.py:

11requires = [
12    'plaster_pastedeploy',
13    'pyramid',
14    'pyramid_jinja2',
15    'pyramid_debugtoolbar',
16    'waitress',
17]

It was installed when you previously ran:

$VENV/bin/pip install -e ".[testing]"

The pyramid_debugtoolbar package is a Pyramid add-on, which means we need to include its configuration into our web application. The cookiecutter already took care of this for us in its development.ini using the pyramid.includes facility:

14pyramid.includes =
15    pyramid_debugtoolbar

You'll now see a Pyramid logo on the right side of your browser window, which when clicked opens a new window that provides introspective access to debugging information. Even better, if your web application generates an error, you will see a nice traceback on the screen. When you want to disable this toolbar, there's no need to change code: you can remove it from pyramid.includes in the relevant .ini configuration file.

Unit and functional tests and pytest

Yikes! We got this far and we haven't yet discussed tests. This is particularly egregious, as Pyramid has had a deep commitment to full test coverage since before its release.

Our pyramid-cookiecutter-starter cookiecutter generated conftest.py, test_functional.py, and test_views.py modules inside the tests package with two unit tests and two functional tests in it. It also configured setup.py with test requirements: pytest as the test runner, WebTest for running view tests, and the pytest-cov tool which yells at us for code that isn't tested:

19tests_require = [
20    'WebTest',
21    'pytest',
22    'pytest-cov',
23]
43    extras_require={
44        'testing': tests_require,
45    },

We already installed the test requirements when we ran the command $VENV/bin/pip install -e ".[testing]". We can now run all our tests:

$VENV/bin/pytest --cov --cov-report=term-missing

This yields the following output.

=========================== test session starts ===========================
platform darwin -- Python 3.9.0, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /<somepath>/hello_world, configfile: pytest.ini, testpaths: hello_world, tests
plugins: cov-2.10.1
collected 4 items

tests/test_functional.py ..                                          [ 50%]
tests/test_views.py ..                                               [100%]

---------- coverage: platform darwin, python 3.9.0-final-0 -----------
Name                            Stmts   Miss  Cover   Missing
-------------------------------------------------------------
hello_world/__init__.py             7      0   100%
hello_world/routes.py               3      0   100%
hello_world/views/__init__.py       0      0   100%
hello_world/views/default.py        4      0   100%
hello_world/views/notfound.py       5      0   100%
-------------------------------------------------------------
TOTAL                              19      0   100%

======================== 4 passed in 0.65 seconds =========================

Our tests passed, and its coverage is complete. What did our tests look like?

1def test_root(testapp):
2    res = testapp.get('/', status=200)
3    assert b'Pyramid' in res.body
4
5def test_notfound(testapp):
6    res = testapp.get('/badurl', status=404)
7    assert res.status_code == 404
 1from hello_world.views.default import my_view
 2from hello_world.views.notfound import notfound_view
 3
 4
 5def test_my_view(app_request):
 6    info = my_view(app_request)
 7    assert app_request.response.status_int == 200
 8    assert info['project'] == 'hello_world'
 9
10def test_notfound_view(app_request):
11    info = notfound_view(app_request)
12    assert app_request.response.status_int == 404
13    assert info == {}

Pyramid supplies helpers for test writing, which we use in the test setup and teardown. Our view tests import the view, make a dummy request, and sees if the view returns what we expected. Our functional tests verify that the response body from a request to the web root contains what we expected and that the expected response code for making a request to /badurl results in 404.

Logging

It's important to know what is going on inside our web application. In development we might need to collect some output. In production we might need to detect situations when other people use the site. We need logging.

Fortunately Pyramid uses the normal Python approach to logging. The development.ini file for your project has a number of lines that configure the logging for you to some reasonable defaults. You then see messages sent by Pyramid (for example, when a new request comes in).

Maybe you would like to log messages in your code? In your Python module, import and set up the logging in your views/default.py:

3import logging
4log = logging.getLogger(__name__)

You can now, in your code, log messages:

7def my_view(request):
8    log.debug('Some Message')

This will log Some Message at a DEBUG log level to the application-configured logger in your development.ini. What controls that? These emphasized sections in the configuration file:

34[loggers]
35keys = root, hello_world
36
37[handlers]
38keys = console
39
40[formatters]
41keys = generic
42
43[logger_root]
44level = INFO
45handlers = console
46
47[logger_hello_world]
48level = DEBUG
49handlers =
50qualname = hello_world

Our application, a package named hello_world, is set up as a logger and configured to log messages at a DEBUG or higher level. When you visit http://localhost:6543, your console will now show:

2016-12-25 03:03:57,059 DEBUG [hello_world.views:8][waitress] Some Message

参考

See also: Quick Tutorial Logging and Logging.

Sessions

When people use your web application, they frequently perform a task that requires semi-permanent data to be saved. For example, a shopping cart. This is called a session.

Pyramid has basic built-in support for sessions. Third party packages such as pyramid_redis_sessions provide richer session support. Or you can create your own custom sessioning engine. Let's take a look at the built-in sessioning support. In our __init__.py we first import the kind of sessioning we want:

1from pyramid.config import Configurator
2from pyramid.session import SignedCookieSessionFactory

警告

As noted in the session docs, this example implementation is not intended for use in settings with security implications.

Now make a "factory" and pass it to the configurator's session_factory argument:

 9        config.include('.routes')
10        my_session_factory = SignedCookieSessionFactory('itsaseekreet')
11        config.set_session_factory(my_session_factory)
12        config.scan()

Pyramid's request object now has a session attribute that we can use in our view code in views/default.py:

 7def my_view(request):
 8    log.debug('Some Message')
 9    session = request.session
10    if 'counter' in session:
11        session['counter'] += 1
12    else:
13        session['counter'] = 0
14    return {'project': 'hello_world'}

We need to update our Jinja2 template templates/mytemplate.jinja2 to show counter increment in the session:

4<div class="content">
5  <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
6  <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
7  <p>Counter: {{ request.session.counter }}</p>
8</div>

Databases

Web applications mean data. Data means databases. Frequently SQL databases. SQL databases frequently mean an "ORM" (object-relational mapper.) In Python, ORM usually leads to the mega-quality SQLAlchemy, a Python package that greatly eases working with databases.

Pyramid and SQLAlchemy are great friends. That friendship includes a cookiecutter!

cd ~
env/bin/cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master

If prompted for the first item, accept the default yes by hitting return.

You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: sqla_demo
repo_name [sqla_demo]: sqla_demo
Select template_language:
1 - jinja2
2 - chameleon
3 - mako
Choose from 1, 2, 3 [1]: 1
Select backend:
1 - none
2 - sqlalchemy
3 - zodb
Choose from 1, 2, 3 [1]: 2

We then run through the following commands as before.

# Change directory into your newly created project.
cd sqla_demo
# Create a new virtual environment...
python3 -m venv env
# ...where we upgrade packaging tools...
env/bin/pip install --upgrade pip setuptools
# ...and into which we install our project and its testing requirements.
env/bin/pip install -e ".[testing]"
# Reset our environment variable for a new virtual environment.
export VENV=~/sqla_demo/env

We now have a working sample SQLAlchemy application with all dependencies installed. The sample project provides a method to generate a database migration from existing models and upgrade the database schema using Alembic. Let's generate the first revision.

$VENV/bin/alembic -c development.ini revision --autogenerate -m "init"

Now let's upgrade the database schema.

$VENV/bin/alembic -c development.ini upgrade head

The sample project also provides a console script to load data into the SQLite database. Let's run it, then start the application:

$VENV/bin/initialize_sqla_demo_db development.ini
$VENV/bin/pserve development.ini

The ORM eases the mapping of database structures into a programming language. SQLAlchemy uses "models" for this mapping. The cookiecutter generated a sample model:

11class MyModel(Base):
12    __tablename__ = 'models'
13    id = Column(Integer, primary_key=True)
14    name = Column(Text)
15    value = Column(Integer)

View code, which mediates the logic between web requests and the rest of the system, can then easily get at the data thanks to SQLAlchemy:

 8@view_config(route_name='home', renderer='sqla_demo:templates/mytemplate.jinja2')
 9def my_view(request):
10    try:
11        query = request.dbsession.query(models.MyModel)
12        one = query.filter(models.MyModel.name == 'one').one()
13    except SQLAlchemyError:
14        return Response(db_err_msg, content_type='text/plain', status=500)
15    return {'one': one, 'project': 'sqla_demo'}

Forms

Developers have lots of opinions about web forms, thus there are many form libraries for Python. Pyramid doesn't directly bundle a form library, but Deform is a popular choice for forms, along with its related Colander schema system.

As an example, imagine we want a form that edits a wiki page. The form should have two fields on it, one of them a required title and the other a rich text editor for the body. With Deform we can express this as a Colander schema:

class WikiPage(colander.MappingSchema):
    title = colander.SchemaNode(colander.String())
    body = colander.SchemaNode(
        colander.String(),
        widget=deform.widget.RichTextWidget()
    )

With this in place, we can render the HTML for a form, perhaps with form data from an existing page:

form = self.wiki_form.render()

We'd like to handle form submission, validation, and saving:

# Get the form data that was posted
controls = self.request.POST.items()
try:
    # Validate and either raise a validation error
    # or return deserialized data from widgets
    appstruct = wiki_form.validate(controls)
except deform.ValidationFailure as e:
    # Bail out and render form with errors
    return dict(title=title, page=page, form=e.render())

# Change the content and redirect to the view
page['title'] = appstruct['title']
page['body'] = appstruct['body']

Deform and Colander provide a very flexible combination for forms, widgets, schemas, and validation. Recent versions of Deform also include a retail mode for gaining Deform features on custom forms.

Deform uses attractive CSS from Twitter Bootstrap and more powerful select, checkbox, and date and time widgets.

参考

See also: Quick Tutorial Forms, Deform, and Colander.

Conclusion

This Quick Tour covered a little about a lot. We introduced a long list of concepts in Pyramid, many of which are expanded on more fully in the Pyramid developer docs.