第 15 章:Pylons 简介

虽然 Django 目前是 Python 最流行的 Web 框架,但它绝不是你的唯一选择。Django 从新闻机构快速实现内容管理解决方案的需求中发展而来,而 Pylons 则从在可能需要与现有数据库集成的环境中构建 Web 应用程序的需求中发展而来,这些应用程序并不完全适合“内容管理”领域中松散定义的应用程序类别。

Pylons 最大的优势在于它采用了一种最佳实践方法来构建其技术栈。在 Django 中,所有内容都是“内置”的,整个应用程序栈都是专门为单一世界观而设计的,该世界观定义了应用程序应该如何构建,而 Pylons 则采取了完全相反的方法。Pylons - 核心代码库位于“pylons”命名空间中 - 非常小巧。在 0.9.7 版本中,它大约有 5,500 行代码。相比之下,Django 大约有 125,000 行代码。

Pylons 通过广泛利用现有库来实现这种魔力,Pylons 社区与许多其他 Python 项目合作开发标准 API 以促进互操作性。

最终,选择 Django 或 Pylons 取决于你愿意做出哪些权衡。虽然 Django 非常容易学习,因为所有文档都集中在一个地方,并且所有与特定组件相关的文档始终在构建 Web 应用程序的上下文中进行讨论,但当你需要开始做一些超出 Django 设计范围的事情时,你会失去一些灵活性。

例如,在我最近参与的一个项目中,我需要与一个在 SQL Server 2000 中实现的非平凡数据库进行交互。对于 Django 来说,实现 SQL Server 后端非常困难。使用 Django 在 Windows 上开发 Web 应用程序的开发人员并不多,更不用说 SQL Server 了。虽然 Django ORM 是 Django 的一部分,但它也不是 Django 的核心重点。支持任意数据库根本不是 Django 的目标,而且这样做也是正确的。

Django 项目假设你会做一件合理的事情,即简单地使用 Postgresql 作为你的数据库后端。Web 开发人员有比为每个可能的数据库构建后端驱动程序更重要的事情要做。

另一方面,Pylons 利用了 SQLAlchemy。SQLAlchemy 可能是 Python 中最强大的数据库工具包。它只专注于数据库访问。SQL Server 后端已经为 CPython 以一种健壮的方式构建,为 Jython 后端实现额外的代码只花了 2 天时间 - 而且这是在没有看到 SQLAlchemy 内部代码的情况下完成的。

仅仅是这种体验就让我决定使用 Pylons。我不必依赖“Web 框架”人员来成为数据库专家。类似地,我不必依赖数据库专家来了解任何关于 Web 模板的信息。

简而言之,当你必须处理奇怪的东西时 - Pylons 是一个绝佳的选择 - 而且说实话 - 你几乎总是会遇到一些奇怪的东西需要处理。

给没有耐心的人的指南

安装 Pylons 的最佳方式是在 virtualenv 中。为 Jython 创建一个新的 virtualenv 并运行 easy_install

> easy_install "Pylons==0.9.7"

创建你的应用程序

> paster create --template=pylons RosterTool

# TODO: just use the defaults for everything.  No sqlalchemy

启动开发服务器

> paster serve --reload development.ini

打开浏览器并连接到 http://127.0.0.1:5000/

# TODO: 在这里包含截图

将一个静态文件放到 rostertool/public/welcome.html 中

<html>
    <body>Just a static file</body>
</html>

你现在应该可以通过访问以下地址加载静态内容

http://localhost:5000/welcome.html

添加一个控制器

RosterTool/roster > paster controller roster

Paste 会安装一个名为“controllers”的目录,并在其中安装一些文件,包括一个名为“roster.py”的模块。你可以打开它,你会看到一个名为“RosterController”的类,它只有一个方法“index”。Pylons 足够智能,可以自动将 URL 映射到控制器类名并调用一个方法。要调用 RosterController 的 index 方法,你只需要调用

http://localhost:5000/roster/index

恭喜你,你现在已经拥有了最基本的 Web 应用程序。它处理基本的 HTTP GET 请求,调用控制器上的一个方法,并返回一个响应。现在让我们详细介绍一下这些部分。

关于 Paste 的说明

当你设置你的玩具 Pylons 应用程序时,你可能想知道为什么 Pylons 似乎使用了一个名为“paster”的命令行工具,而不是像“pylons”这样明显的东西。Paster 实际上是 Pylons 使用的 Paste 工具集的一部分。

Paste 用于构建 Web 应用程序框架 - 而不是 Web 应用程序 - 而是像 Pylons 这样的 Web 应用程序框架。每次你使用“paster”时,实际上都是调用 Paste。每次你访问 HTTTP 请求和响应对象时 - 那就是 WebOb - Paste 的 HTTP 包装代码的后代。Pylons 广泛使用 Paste 进行配置管理、测试、使用 WebOb 进行基本的 HTTP 处理。你最好至少浏览一下 Paste 文档,看看 Paste 中有哪些可用功能。

Pylons MVC

Pylons,就像 Django 以及任何合理的 Web 框架(或 GUI 工具包)一样,使用模型-视图-控制器设计模式。

在 Pylons 中,这映射到

组件 实现
模型 SQLAlchemy(或你喜欢的任何其他数据库工具包)
视图 Mako(或你喜欢的任何模板语言)
控制器 纯 Python 代码

重申一下 - Pylons 的宗旨是让你 - 应用程序开发人员决定你愿意做出的特定权衡。如果使用更类似于 Django 的模板语言更适合你的 Web 设计师,那么就切换到 Jinja2。如果你不想处理 SQLAlchemy - 你可以使用 SQLObject 或原始 SQL,如果你愿意的话。

Pylons 提供了一些工具来帮助你以合理的方式将这些部分连接在一起。

Routes 是一个将 URL 映射到类的库。这是你每次 Web 服务器被访问时调度方法的基本机制。Routes 提供了与 Django 的 URL 调度程序类似的功能。

Webhelpers 是 Pylons 的事实标准库。它包含 Web 中常用的函数,例如向用户显示状态消息、日期转换函数、HTML 标签生成、分页函数、文本处理 - 不胜枚举。

Pylons 还提供基础设施,以便你可以操作特定于 Web 应用程序的事物,包括

  • WSGI 中间件,可以在你的现有代码库中以最小的侵入性方式为你的应用程序添加功能。
  • 一个强大的测试框架,包括一个惊人的好调试器,你可以通过 Web 使用它。
  • 帮助程序,以启用 RESTful API 开发,以便你可以将你的应用程序公开为一个编程接口。

现在让我们将曲棍球阵容包装在一个 Web 应用程序中。我们将针对几个功能

  • 表单处理和验证,以便通过 Web 添加新球员
  • 登录和身份验证,以确保不是任何人都可以编辑我们的列表
  • 添加一个 JSON/REST API,以便我们可以从其他工具修改数据

在这个过程中,我们将使用命令行和 Web 中的交互式调试器来直接观察和交互运行应用程序的状态。

深入了解 Java 内存模型

关于重新加载的说明 - 有时候,如果您使用 Jython 在 Pylons 上进行开发,Java 会抛出类似这样的 OutOfMemory 错误

java.lang.OutOfMemoryError: PermGen space
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

Java 在称为永久代堆空间的东西中跟踪类定义。当 HTTP 线程重新启动并且您的类被重新加载时,这对 Pylons 来说是一个问题。旧的类定义不会消失 - 它们永远不会被垃圾回收。由于 Jython 在幕后动态创建 Java 类,因此每次您的开发服务器重新启动时,您都可能将数百个新类加载到 JVM 中。

重复此操作几次,很快您的 JVM 就会耗尽 permgen 空间,然后它就会崩溃并死亡。

要修改 permgen 堆大小,您需要使用一些扩展的命令行选项来指示 Java。要将堆设置为 128M,您需要使用“-XX:MaxPermSize=128M”。

要使 Jython 默认获得此行为,您需要编辑 JYTHON_HOME/bin/jython(或 jython.bat)中的 Jython 启动脚本,方法是编辑读取以下内容的行

set _JAVA_OPTS=

变为

set _JAVA_OPTS=-XX:MaxPermSize=128M

在生产环境中,您不会在运行时生成新的类定义,因此这应该不是问题,但在开发过程中可能会非常令人沮丧。

调用 Pylons shell

是的,我将立即开始进行测试,因为它将为您提供一种以交互方式探索 Pylons 应用程序的方法。

Pylons 为您提供了一个类似于 Django 的交互式 shell。您可以使用以下命令启动它。

RosterTool > jython setup.py egg_info
RosterTool > paster shell test.ini

这将产生一个不错的交互式 shell,您可以立即开始使用它。现在让我们看看我们玩具应用程序中的那些请求和响应对象。

RosterTool > paster shell test.ini

Pylons Interactive Shell
Jython 2.5.0 (Release_2_5_0:6476, Jun 16 2009, 13:33:26)
[OpenJDK Server VM (Sun Microsystems Inc.)]

All objects from rostertool.lib.base are available
Additional Objects:
mapper     -  Routes mapper object
wsgiapp    -  This project's WSGI App instance
app        -  paste.fixture wrapped around wsgiapp

>>> resp = app.get('/roster/index')
>>> resp
<Response 200 OK 'Hello World'>
>>> resp.req
<Request at 0x43 GET http://localhost/roster/index>

Pylons 允许您实际对应用程序运行请求并使用生成的响应进行操作。即使对于像 HTTP 请求和响应这样“简单”的东西,Pylons 也使用一个库来提供便利方法和属性,以使您的开发生活更轻松。在这种情况下,它是 WebOb - Paste 的旧 HTTP 包装代码的派生版本。

请求和响应对象都有框架提供的数十个属性和方法。如果您花时间浏览 WebOb 的文档,您几乎肯定会从中受益。

以下四个属性是您必须了解才能理解请求对象的含义。最好的方法是在 shell 中尝试使用请求对象。

request.GET

GET 是 URL 中传递的变量的特殊字典。Pylons 会自动将多次出现的 URL 参数转换为离散的键值对。

>>> resp = app.get('/roster/index?foo=bar&x=42&x=50')
>>> resp.req.GET
UnicodeMultiDict([('foo', u'bar'), ('x', u'42'), ('x', u'50')])
>>> req.GET['x']
u'50'
>>> req.GET.getall('x')
[u'42', u'50']

请注意,您可以根据从字典中获取值的方式来获取最后一个值或值列表。如果您不注意,这可能会导致细微的错误。

request.POST
POST 类似于 GET,但适当的是,它只返回在 HTTP POST 提交期间发送的变量。
request.params
Pylons 将所有 GET 和 POST 数据合并到一个 MultiValueDict 中。在几乎所有情况下,这都是您真正想要使用的属性,以获取用户发送到服务器的数据。
request.headers
此字典提供客户端发送到服务器的所有 HTTP 标头。

上下文变量和应用程序全局变量

大多数 Web 框架提供一个请求范围的变量来充当值的集合。Pylons 也不例外 - 每当您使用 paste 创建一个新的控制器时,它会自动导入一个属性“c”,它是上下文变量。

这是我发现 Pylons 令人沮丧的一个方面。“c”属性是在您指示 paste 为您构建一个新的控制器时作为导入生成的。“c”值不是您控制器的属性 - Pylons 具有特殊的全局线程安全变量 - 这只是其中之一。您可以在上下文中存储希望在请求持续时间内存在的变量。这些值在请求/响应周期完成后不会持久存在,因此不要将其与会话变量混淆。

另一个你会经常用到的全局变量是 pylons.session。在这里,你可以存储需要在多个请求/响应周期中持续存在的变量。你可以将这个变量视为一个特殊的字典 - 只需使用标准的 Jython 字典语法,Pylons 会处理剩下的事情。

路由

路由很像 Django 的 URL 分发器。它提供了一种机制,让你可以将 URL 映射到控制器类和要调用的方法。

通常,我发现路由在 URL 匹配表达能力方面做了一些权衡,以换取更简单的推理,即哪些 URL 被定向到特定的控制器和方法。路由不支持正则表达式,只支持简单的变量替换。

一个典型的路由看起来像这样

map.connect('/{mycontroller}/{someaction}/{var1}/{var2}')

上面的路由将找到名为“Mycontroller”的控制器(注意类的大小写),并调用该对象上的“someaction”方法。变量 var1 和 var2 将作为参数传入。

map 对象的 connect() 方法还将接受可选参数,以填充没有足够 URL 编码数据的 URL 的默认值,这些数据不足以正确调用具有最小所需参数数量的方法。首页就是一个例子 - 让我们尝试将首页连接到 Roster.index 方法。

编辑 rostertool/config/routing.py,以便在 #CUSTOM_ROUTES_HERE 之后有 3 行,内容如下

map.connect('/', controller='roster', action='index')
map.connect('/{action}/{id}/', controller='roster')
map.connect('/add_player/', controller='roster', action='add_player')

虽然这看起来应该可以工作,但你可以尝试运行“paster serve”,它不会工作。

Pylons 总是尝试在搜索要调用的控制器和方法之前提供静态内容。你需要进入 RosterTool/rostertool/public 并删除 paster 在你第一次创建应用程序时安装的“index.html”文件。

在你的浏览器中再次加载 http://127.0.0.1:5000/ - 默认的 index.html 应该消失了,你现在应该看到你的欢迎页面。

控制器和模板

利用我们在第 12 章中定义的 Table 模型,让我们创建曲棍球阵容,但这次使用 postgresql 数据库。我假设你有一个允许你创建新数据库的 postgresql 安装。

>>> from sqlalchemy import create_engine
>>> from sqlalchemy.schema import Sequence
>>> db = create_engine('postgresql+zxjdbc://myuser:mypass@localhost:5432/mydb')
>>> connection = db.connect()
>>> metadata = MetaData()
>>> player = Table('player', metadata,
...     Column('id', Integer, primary_key=True),
...     Column('first', String(50)),
...     Column('last', String(50)),
...     Column('position', String(30)))
>>> metadata.create_all(engine)

现在让我们将数据连接到控制器,显示一些数据并让基本的表单处理工作起来。我们将为 sqlalchemy 模型创建一个基本的 CRUD(创建、读取、更新、删除)接口。由于空间限制,这个 HTML 将非常基础,但你会了解到事物是如何组合在一起的。

Paste 不仅仅生成控制器的存根 - 它还会在 rostertool/tests/functional/ 中以 test_roster.py 的形式生成一个空的函数测试用例。我们很快就会访问测试。

控制器实际上是 Pylons 中发生动作的地方。在这里,你的应用程序将从数据库中获取数据,并将其准备为模板以将其呈现为 HTML。让我们将所有球员的列表放在网站的首页。我们将实现一个模板来呈现所有球员的列表。然后,我们将实现控制器中的一个方法来覆盖 Roster 的 index() 方法,使用 SQLAlchemy 从磁盘加载记录并将它们发送到模板。

在此过程中,我们将接触到模板继承,这样你就可以看到如何在 Mako 中通过子类化模板来节省按键次数。

首先,让我们在 rostertool/templates 目录中创建两个模板,base.html 和 list_players.html。

base.html

<html>
    <body>
        <div class="header">
            ${self.header()}
        </div>

        ${self.body()}
    </body>
</html>

<%def name="header()">
    <h1>${c.page_title}</h1>
    <% messages = h.flash.pop_messages() %>
    % if messages:
    <ul id="flash-messages">
        % for message in messages:
        <li>${message}</li>
        % endfor
    </ul>
    % endif
</%def>

list_players.html

<%inherit file="base.html" />
<table border="1">
    <tr>
        <th>Position</th><th>Last name</th><th>First name</th><th>Edit</th>
    </tr>
    % for player in c.players:
        ${makerow(player)}
    % endfor
</table>

<h2>Add a new player</h2>
${h.form(h.url_for(controller='roster', action='add_player'), method='POST')}
    ${h.text('first', 'First Name')} <br />
    ${h.text('last', 'Last Name')} <br />
    ${h.text('position', 'Position')} <br />
    ${h.submit('add_player', "Add Player")}
${h.end_form()}

<%def name="makerow(row)">
<tr>
    <td>${row.position}</td>\
    <td>${row.last}</td>\
    <td>${row.first}</td>\
    <td><a href="${h.url_for(controller='roster', action='edit_player', id=row.id)}">Edit</a></td>\
</tr>
</%def>

这里有很多事情要做。基本模板让 Mako 定义了一组所有页面都可以重用的样板 HTML。每个部分都用 <%def name=”block()”> 部分定义,并且这些块在子类化模板中被重载。实际上 - Mako 让你的页面模板看起来像具有可以渲染页面子部分的方法的对象。

list_players.html 模板包含的内容会立即替换到基本模板的 self.body() 方法中。我们主体的第一部分使用了我们的魔法上下文变量“c”。在这里 - 我们正在遍历数据库中的每个球员,并将它们渲染成一个表格作为一行。请注意,我们可以使用 Mako 方法语法来创建一个名为“makerow”的方法,并在我们的模板中直接调用它。

#XX: Mako 旁注 Mako 提供了一套丰富的模板函数。我只打算使用 Mako 最基本的部分 - 继承、变量替换和循环迭代来使玩具应用程序正常工作。我强烈建议你深入研究 Mako 文档,以发现功能并更好地了解如何使用模板库。 ##

接下来,我们添加一个小的表单来创建新的玩家。这里的技巧是看到表单是由辅助函数以编程方式生成的。Pylons 自动将 YOURPROJECT/lib/helpers(在本例中为 rostertool.lib.helpers)导入为模板中的“h”变量。helpers 模块通常从 Pylons 的部分或依赖库导入函数,以允许从应用程序中的任何位置访问这些功能。虽然这看起来像是违反了“关注点分离”——看看模板,看看它给我们带来了什么?我们从需要调用的特定控制器和方法中完全解耦了 URL。模板使用一个特殊的路由函数“url_for”来计算将为特定控制器和方法映射的 URL。list_players.html 文件的最后一部分包含用于显示警报消息的代码。

现在让我们看看我们的 rostertool.lib.helpers 模块

from routes import url_for
from webhelpers.html.tags import *
from webhelpers.pylonslib import Flash as _Flash

# Send alert messages back to the user
flash = _Flash()

在这里,我们从路由中导入 url_for 函数来执行我们的 URL 反转计算。我们从主要的 html.tags 辅助模块导入 HTML 标签生成器,并导入 Flash 为我们的页面提供警报消息。在接下来的几页中,我们将更详细地介绍控制器代码时,我将向您展示如何使用闪存消息。

现在,使用 paste 创建一个控制器(如果您在本章开头不耐烦,您已经完成了此操作)

$ cd ROSTERTOOL/rostertool
$ paster controller roster

RosterContoller 应该获得一个非常短的方法,该方法读取

def index(self):
    session = Session()
    c.page_title = 'Player List'
    c.players = session.query(Player).all()
    return render('list_players.html')

这段代码非常简单,我们只是使用 SQLAlchemy 会话从磁盘加载所有 Player 对象,并将其分配给特殊的上下文变量“c”。然后指示 Pylons 渲染 list_player.html 文件。现在让我们看看那个文件

上下文应该是您将要传递给应用程序其他部分的值的默认位置。请注意,Pylons 会自动将 URL 值绑定到上下文,因此虽然您可以从 self.form_result 中获取表单值,但您也可以从上下文中获取原始 URL 值。

您现在应该能够运行调试 Web 服务器,并且您可以访问首页以加载一个空的玩家列表。像本章开头一样启动您的调试 Web 服务器,然后转到 http://127.0.0.1:5000/ 以查看页面加载您的玩家列表(当前为空列表)。

现在我们需要进入核心部分,在这里我们可以开始创建、编辑和删除玩家。我们将确保输入至少经过最小程度的验证,错误会显示给用户,并且警报消息会正确填充。

首先,我们需要一个页面,该页面只显示单个玩家,并提供用于编辑和删除的按钮。

<%inherit file="base.html" />

<h2>Edit player</h2>
${h.form(h.url_for(controller='roster', action='save_player', id=c.player.id), method='POST')}
    ${h.hidden('id', c.player.id)} <br />
    ${h.text('first', c.player.first)} <br />
    ${h.text('last', c.player.last)} <br />
    ${h.text('position', c.player.position)} <br />
    ${h.submit('save_player', "Save Player")}
${h.end_form()}

${h.form(h.url_for(controller='roster', action='delete_player', id=c.player.id), method='POST')}
    ${h.hidden('id', c.player.id)} <br />
    ${h.hidden('first', c.player.first)} <br />
    ${h.hidden('last', c.player.last)} <br />
    ${h.hidden('position', c.player.position)} <br />
    ${h.submit('delete_player', "Delete Player")}
${h.end_form()}

此模板假设上下文中分配了一个“player”值,并且毫不奇怪——它将是我们第一次在第 12 章中看到的 Player 对象的完整实例。辅助函数允许我们使用 webhelper 标签生成函数来定义 HTML 表单。这意味着您不必担心转义字符或记住 HTML 属性的特定细节。helper.tag 函数默认情况下会做明智的事情。

我已经将编辑和删除表单设置为指向不同的 URL。您可能希望“保留”URL,但为每个操作提供单独的 URL 具有优势——尤其是对于调试。您可以通过读取日志文件来轻松查看 Web 服务器上命中了哪些 URL。如果 URL 相同,但行为由某个表单值决定,则看到相同类型的行为——这要难得多。它在您的控制器中也更难设置,因为您需要在每个方法级别调度行为。为什么不为不同的行为提供单独的方法——当他们需要在将来调试您的代码时,每个人都会感谢您。

在我们创建用于创建、编辑和删除的控制器方法之前,我们将创建一个 formencode 模式来提供基本验证。同样,Pylons 不会提供验证行为——它只是利用另一个库来做到这一点。在 rostertool/controllers/roster.py 中

class PlayerForm(formencode.Schema):
    # You need the next line to drop the submit button values
    allow_extra_fields=True

    first = formencode.validators.String(not_empty=True)
    last = formencode.validators.String(not_empty=True)
    position = formencode.validators.String(not_empty=True)

这只是对我们的输入提供基本的字符串验证。请注意,这不会提供任何关于 HTML 表单外观的提示——或者它是否完全是 HTML。FormEncode 可以验证任意 Python 字典并返回有关它们的错误。

我将向您展示 add 方法和 edit_player 方法。您应该尝试实现 save_player 和 delete_player 方法,以确保您理解这里发生的事情。

from pylons.decorators import validate
from rostertool.model import Session, Player

@validate(schema=PlayerForm(), form='index', post_only=False, on_get=True)
def add_player(self):
    first = self.form_result['first']
    last = self.form_result['last']
    position = self.form_result['position']
    session = Session()
    if session.query(Player).filter_by(first=first, last=last).count() > 0:
        h.flash("Player already exists!")
        return h.redirect_to(controller='roster')
    player = Player(first, last, position)
    session.add(player)
    session.commit()
    return h.redirect_to(controller='roster', action='index')

def edit_player(self, id):
    session = Session()
    player = session.query(Player).filter_by(id=id).one()
    c.player = player
    return render('edit_player.html')

这里有几点需要注意。edit_player 由 Routes 直接传入 'id' 属性。在 edit_player 方法中 - 'player' 被分配给上下文,但上下文从未明确地传递给模板渲染器。Pylons 将自动获取绑定到上下文的属性并将它们写入模板并渲染 HTML 输出。

在 add_player 方法中,我使用 validate 装饰器来根据 PlayerForm 强制执行输入。如果发生错误,装饰器的 form 属性将用于加载针对当前控制器的操作。在本例中 - 'index' - 因此首页加载。

如果您已经完成了第 12 章,那么 SQLAlchemy 代码应该很熟悉。add_player 方法的最后一行是重定向,以防止在浏览器中点击重新加载时出现问题。一旦所有数据操作完成 - 服务器将客户端重定向到结果页面。如果用户在结果页面上点击重新加载 - 不会发生任何数据变动。

以下是您需要实现以使事情正常工作的剩余方法的签名

  • save_player(self)
  • delete_player(self)

如果您遇到困难,您始终可以参考书籍网站上的工作示例代码。

添加 JSON API

将 JSON 集成到 Pylons 中非常简单。步骤与为普通 HTML 视图添加控制器方法大致相同。您调用 paste,paste 然后生成您的控制器存根和测试存根,您添加一些路由将控制器连接到 URL,然后您只需填写控制器代码。

$ cd ROSTERTOOL_HOME/rostertool
$ paster controller api

Pylons 提供了一个特殊的 @jsonify 装饰器,它将自动将 Python 原语类型转换为 JSON 对象。但是它 *不会* 将 POST 数据转换为对象 - 这是您的责任。在播放器列表中添加一个简单的读取接口只需要在您的 ApiController 中添加一个方法

@jsonify
def players(self):
    session = Session()
    players = [{'first': p.first,
                'last': p.last,
                'position': p.position,
                'id': p.id} for p in session.query(Player).all()]
    return players

添加一个钩子,以便人们可以将 JSON 格式的数据 POST 到您的服务器以创建新播放器,这几乎同样容易

import simplejson as json

@jsonify
def add_player(self):
    obj = json.loads(request.body)
    schema = PlayerForm()
    try:
        form_result = schema.to_python(obj)
    except formencode.Invalid, error:
        response.content_type = 'text/plain'
        return 'Invalid: '+unicode(error)
    else:
        session = Session()
        first, last, position = obj['first'], obj['last'], obj['position']
        if session.query(Player).filter_by(last=last, first=first,
                position=position).count() == 0:
            session.add(Player(first, last, position))
            session.commit()
            return {'result': 'OK'}
        else:
            return {'result':'fail', 'msg': 'Player already exists'}

单元测试、功能测试和日志记录

我最喜欢的 Pylons 功能之一是它丰富的测试和调试功能。它甚至设法利用社交网络,将其颠倒过来,并将其变成一个调试功能。我们很快就会谈到这一点。

了解如何在 pylons 中测试代码的第一步是熟悉 nose 测试框架。nose 通过让您摆脱困境来简化测试。没有类需要子类化,只需开始编写以“test”开头的函数,nose 就会运行它们。编写一个以“Test”为前缀的类,nose 会将其视为一组测试,运行每个以“test”开头的函数。对于每个测试方法,nose 都会在执行您的测试之前执行 setup() 方法,并且 nose 会在您的测试用例之后执行 teardown() 方法。

最棒的是,nose 会自动查找任何看起来像测试的东西并运行它。您不需要在树中组织复杂的测试用例链。计算机将为您完成此操作。

让我们看一下您的第一个测试用例 - 我们将只对模型进行检测,在本例中 - SQLAlchemy。由于模型层不依赖于 Pylons - 这实际上 - 只是一个 SQLAlchemy 的测试。

在 ROSTERTOOL_HOME/rostertool/tests 中,创建一个名为“test_models.py”的模块,内容如下

from rostertool.model import Player, Session, engine

class TestModels(object):

    def setup(self):
        self.cleanup()

    def teardown(self):
        self.cleanup()

    def cleanup(self):
        session = Session()
        for player in session.query(Player):
            session.delete(player)
        session.commit()

    def test_create_player(self):
        session = Session()
        player1 = Player('Josh', 'Juneau', 'forward')
        player2 = Player('Jim', 'Baker', 'forward')
        session.add(player1)
        session.add(player2)

        # But 2 are in the session, but not in the database
        assert 2 == session.query(Player).count()
        assert 0 == engine.execute("select count(id) from player").fetchone()[0]
        session.commit()

        # Check that 2 records are all in the database
        assert 2 == session.query(Player).count()
        assert 2 == engine.execute("select count(id) from player").fetchone()[0]

在我们运行测试之前,我们需要稍微编辑一下模型模块,以便模型知道从 Pylon 的配置文件中查找连接 URL。在您的 test.ini 中,添加一行设置 sqlalchemy.url 设置以指向 [app:main] 部分中的数据库。

您应该有一行看起来像这样

[app:main]
use = config:development.ini
sqlalchemy.url = postgresql+zxjdbc://username:password@localhost:5432/mydb

现在编辑模型文件,以便 create_engine 调用使用该配置。这就像从 pylons 导入 config 并执行字典查找一样简单。您想要的两行是

from pylons import config
engine = create_engine(config['sqlalchemy.url'])

就是这样。您的模型现在将从 Pylons 中查找您的数据库连接字符串。更棒的是 - nose 也知道如何使用该配置。

从命令行,您现在可以从 ROSTERTOOL_HOME 运行测试

ROSTERTOOL_HOME $ nosetests rostertool/tests/test_models.py
.
----------------------------------------------------------------------
Ran 1 test in 0.502s

完美!要捕获 stdout 并获取详细输出,您可以选择使用“-sv”选项。Nose 有自己的活跃开发者社区。您可以使用一些插件来进行覆盖率分析和性能分析。使用“nosetests –help”查看所有可用选项的完整列表。

由于 Pylons 的本质及其病态的解耦设计,编写小的单元测试来测试每一小段代码非常容易。您可以随意组装您的测试。只想有一堆测试函数?太棒了!如果您需要进行设置和拆卸,并且编写测试类是有意义的 - 那么就去做吧。

使用 nose 进行测试是一种乐趣 - 您不必被迫适应任何特定结构,关于您的测试必须去哪里才能执行。您可以以最适合您自己的方式组织您的测试。

这涵盖了基本的单元测试,但假设我们想测试我们曲棍球阵容的 JSON 接口。我们真的希望能够在 URL 上调用 GET 和 POST,以确保 URL 路由按预期工作。我们希望确保内容类型正确设置为“application/x-json”。换句话说 - 我们想要一个合适的函数测试 - 一个不像单元测试那样细粒度的测试。

当我们运行粘贴 shell 时,之前对“app”对象的了解应该让您大致了解需要什么。在 Pylons 中,您可以使用 TestController 来检测您的应用程序代码。幸运的是,Pylons 已经在您的 /tests 目录中为您创建了一个。只需导入它,对其进行子类化,您就可以像在 shell 中一样开始使用“app”对象。

现在让我们详细看一下功能测试。这是一个示例,您可以将其保存到 rostertool/tests/functional/test_api.py 中

from rostertool.tests import *
import simplejson as json
from rostertool.model.models import Session, Player

class TestApiController(TestController):
    # Note that we're using subclasses of unittest.TestCase so we need
    # to be careful with setup/teardown camelcasing unlike nose's
    # default behavior

    def setUp(self):
        session = Session()
        for player in session.query(Player):
            session.delete(player)
        session.commit()

    def test_add_player(self):
        data = json.dumps({'first': 'Victor',
            'last': 'Ng',
            'position': 'Goalie'})
        # Note that the content-type is set in the headers to make
        # sure that paste.test doesn't URL encode our data
        response = self.app.post(url(controller='api', action='add_player'),
            params=data,
            headers={'content-type': 'application/x-json'})
        obj = json.loads(response.body)
        assert obj['result'] == 'OK'

        # Do it again and fail
        response = self.app.post(url(controller='api', action='add_player'),
            params=data,
            headers={'content-type': 'application/x-json'})
        obj = json.loads(response.body)
        assert obj['result'] <> 'OK'

当您使用 TestController 作为您的超类时,您可能会很容易错过一个小的细节。首先,TestController 是标准 python 单元测试库中 unittest.TestCase 的后代。Nose 不会在 TestCase 子类上运行“setup”和“teardown”方法。相反,您必须使用 TestCase 使用的驼峰命名法。

通读测试用例应该向您展示您可以接触到的细节程度。所有您的标头都已公开,响应内容已公开 - 事实上,HTTP 响应完全公开为一个对象,供您检查和验证。

所以太好了,现在我们可以运行小的单元测试,更大的功能测试 - 让我们看一下通过网络提供的调试工具。

考虑当大多数 Web 应用程序堆栈发生错误时会发生什么。也许您会得到一个堆栈跟踪,也许不会。如果您幸运的话,您可以像 Django 一样看到每个堆栈帧中的局部变量。但是通常,如果您想在错误发生时与实时应用程序交互,您就会很不幸。

最终,您可能会找到触发错误的堆栈跟踪部分,但共享该信息的唯一方法是通过邮件列表或对源代码控制进行正式修补。让我们看一下一个例子。

我们将以开发模式启动我们的应用程序。我们还将在控制器中故意破坏一些代码以查看堆栈跟踪。但首先,我们需要将一些数据放入我们的应用程序中。运行

添加一个 sqlalchemy.url 配置行,就像您在 test.ini 配置中所做的那样,让我们以开发模式启动应用程序。我们将让服务器运行,以便文件系统上的任何代码更改都会自动检测到,并且代码会重新加载

$ paster serve development.ini --reload

我们将添加一个名为“John Doe”的单个球员作为中锋,并保存记录

# TODO: insert screenshot of the add user interface

现在让我们故意破坏一些代码以触发调试器。修改 RosterController 的 index 方法并编辑加载球员列表的调用。我们将使用 Web 会话而不是数据库会话来尝试加载 Player 对象。

def index(self):
    db_session = Session()
    c.page_title = 'Player List'
    c.players = session.query(Player).all()
    return render('list_players.html')

加载 http://127.0.0.1:5000/ 以查看错误页面。您应该看到类似这样的内容

# XXX: insert screen capture of the error page listing
'AttributeError: Session object hsa no attribute 'not_a_method'

Pylons 向您抛回了大量信息。在屏幕顶部,您将看到 4 个选项卡:Traceback、Extra Data、Template 和 Source - Pylons 默认情况下会将您置于 Traceback 选项卡中。如果您查看错误,您将看到源文件中发生错误的确切行号。Pylons Traceback 选项卡的特别之处在于,这实际上是一个完全交互式的会话。

您可以选择“+”号展开每个堆栈帧,并显示该帧上的文本输入以及一些局部变量。该文本输入是您服务器进程的接口。您可以在其中输入几乎任何 Python 命令,按回车键,您将获得实时结果。从这里我们可以看到,我们应该使用“db_session”而不是“session”变量。

Inspecting the application stack

这非常棒。如果您点击“查看”链接,您甚至可以跳转到导致错误的 Jython 模块的完整源代码列表。在撰写本文时,Pylons 中的一个错误是,有时超链接格式错误。因此,虽然回溯会正确列出发生错误的行号,但源代码列表可能会跳转到错误的行。

Pylons 开发人员还在搜索引擎中嵌入了一个接口,用于查看您的错误是否已被报告过。如果您向下滚动到回溯页面的底部,您将看到另一个选项卡控件,其中包含“搜索邮件列表”选项。在这里,Pylons 会自动提取异常消息,并为您提供一个接口,以便您可以直接搜索与您的特定 Pylons 安装相关的所有邮件列表。

如果您在邮件列表中找不到您的错误,您可以转到下一个选项卡“发布回溯”,并将您的堆栈跟踪提交到 PylonsHQ.com 上的 Web 服务,以便您可以尝试与其他协作者在线调试您的问题。结合单元测试、功能测试以及 Web 调试器为您提供的众多调试选项,Pylons 使调试体验尽可能轻松。

部署到 Servlet 容器

将您的 Pylons 应用程序部署到 Servlet 容器非常简单。只需使用 easy_install 从 PyPI 安装 snakefight,您就可以开始构建 WAR 文件。

$ easy_install snakefight
...snakefight will download and install here ...
$ jython setup.py bdist_war --paste-config test.ini

默认情况下,snakefight 会将您的 Jython 安装的完整实例捆绑到 WAR 文件中。它不包含您的应用程序依赖的任何 JAR 文件。对于我们的小示例,这只是 postgresql JDBC 驱动程序。您可以使用 –include-jars 选项并提供一个以逗号分隔的 JAR 文件列表。

$ jython setup.py bdist_war \
    --include-jars=postgresql-8.3-604.jdbc4.jar \
    --paste-config=test.ini

最终的 WAR 文件将位于 dist 目录下。它将包含您的 postgreql JDBC 驱动程序、Jython 的完整安装(包括 site-packages 中的任何内容)以及您的应用程序。您的 WAR 文件应该能够毫无问题地部署到任何符合标准的 Servlet 容器中。

结论

我们只触及了 Pylons 可能性的皮毛,但我希望您已经领略了 Pylons 的可能性。Pylons 使用大量包,因此您需要花费更多时间来克服最初的学习曲线,但回报是能够选择最适合解决您特定问题的库。