Quixote:以 Python 为中心的 Web 应用程序框架
Quixote 是一个为 Python 程序员设计的 Web 应用程序框架。它最初由 Andrew Kuchling、Neil Schemenauer 和我(Greg Ward)在 MEMS Exchange 开发,目的是为了简化我们的实际工作——创建一个由网络驱动的半导体制造站点网络。为了开发我们的主网站 (www.mems-exchange.org),我们需要专注于构建这样一个网络所需的复杂业务逻辑,并在后端和用户界面之间划清界限。我们还希望尽可能多地使用 Python,因为我们认为 Python 是这种复杂且快速变化的应用程序领域中最合适的语言。
Quixote 需要 Python 2.0 或更高版本、对 Python 的良好理解以及实现 CGI 协议的 Web 服务器。(尽管您的应用程序在使用 FastCGI 或 SCGI 等机制时会更愉快,这些机制允许长时间运行的 Web 进程。)
Quixote 是由需要开发动态网站的 Python 程序员编写和使用的,同时尽可能多地利用他们现有的 Python 知识。特别是,Quixote 不是很适应常见的“Web 设计师”和“Web 开发人员”之间的区分。如果您组织的 Web 设计师热衷于尝试真正的编程语言,那么 Quixote 可能会为他们提供一个很好的 Python 入门;但是任何不理解“导入模块”或“调用函数”意味着什么的人,都无法在 Quixote 上取得多大进展。同样,任何希望使用专用的 WYSIWYG HTML 编辑器来创建网页的人都会被排除在外。
顺便说一句,这与大多数其他 Web 应用程序框架所采取的立场完全相反,而这正是我们不喜欢大多数其他 Web 应用程序框架的原因。根据我们有限的经验,它们都发明了一种 HTML 模板语言,该语言在 HTML 中嵌入某种编程语言,通常会设置一些刻意的限制,以防止幼稚的用户搬起石头砸自己的脚。对于想要权力和灵活性并且完全有能力将枪口从自己脚上移开的程序员来说,这通常最终会变得痛苦和令人沮丧。
具体来说,Quixote 的模板语言 PTL(Python 模板语言)颠倒了通常的模型,它使 Python 代码更容易生成长文本字符串(例如 HTML 文档),而不是将 Python 代码嵌入到类似 HTML 的模板语言中。我们将在稍后更详细地介绍 PTL。
如果符合以下条件,Quixote 可能是适合您的工具:
您需要开发具有复杂编程需求的动态网站,无论是在后端还是在演示/用户界面方面;
您更关心提供良好的内容并确保网站背后的逻辑正确,而不是花哨的设计技巧;
您不想学习(并与)另一种 HTML 模板语言作斗争;和/或
您希望使用您已经了解的关于 Python 的所有知识(模块、包、函数、类等等)来开发网站
Quixote 构建于四个核心原则之上:
发布函数结果:Quixote 的主要工作是使用 URL 查找 Python 可调用对象(例如,函数或方法)并将其结果放在 Web 上。
URL 是用户界面的一部分,源代码和 URL 空间的组织应大致对应。
将 HTML 嵌入 Python 比将 Python 嵌入 HTML 更简洁、更容易。
没有魔法:当 Quixote 无法弄清楚该做什么时,它拒绝猜测程序员的意图,而是选择引发异常。
开发 Quixote 应用程序的常用方法是编写一组类来实现系统的基本逻辑——通常称为域类、域对象、业务逻辑等等。理想情况下,您的域类应该与您将要实现的用户界面类型无关。然后,您将 Web 界面实现为一组单独的 PTL 模块。这两部分代码之间的关系应该是完全单向的:Web 界面将导入并严重依赖域类,但域类将完全不知道 Web 界面。
作为一个真实的例子,考虑 SPLAT!,这是一个我编写的简单错误跟踪工具,作为 Quixote 示例应用程序(也是因为我们需要一个简单的错误跟踪器)。SPLAT!(以错误被压扁的声音命名)由一个 Python 包 `splat` 和一个名为 `splat.web` 的子包组成。域类,SPLAT! 对错误的理解、用户的理解、错误的存储方式,都在名为 `splat.bug`、`splat.user`、`splat.database` 等模块中。
SPLAT! 的 Web 界面在 `splat.web` 包中实现,具有以下模块:
splat.web.util (splat/web/util.ptl) splat.web.index (splat/web/index.ptl) splat.web.bug_ui (splat/web/bug_ui.ptl) splat.web.prefs (splat/web/prefs.ptl)
让我们看一下 `splat.web` 包中的代码。Quixote 使用的每个命名空间(包或模块)都必须提供两个特殊的标识符:`_q_index()` 和 `_q_exports`。我们稍后会介绍 `_q_exports`。现在,让我们专注于 `_q_index()`;对于 `splat.web` 包,它通过 `splat/web/__init__.py` 中的导入提供:
from splat.web.index import _q_index
这符合在 `__init__.py` 文件中放置尽可能少的代码的推荐做法。任何需要由包本身(而不是该包中的模块)提供的函数或类,都应简单地在包的 `__init__.py` 中导入。
Quixote 的 `_q_index()` 等同于 `index.html`,但 `_q_index()` 不是包含目录默认网页的文件,而是一个 Python 可调用对象(例如,函数、方法或 PTL 模板),它返回命名空间的默认网页。实际上,传统的基于文件系统的 Web 服务器(例如 Apache)与 Quixote 以 Python 为中心的构建网站的方式之间存在许多有用的类比。
文件系统(例如 Apache) | Quixote |
---|---|
目录 | Python 命名空间(模块、包,...) |
文件 | Python 可调用对象(函数、方法,...) |
index.html | _q_index() |
文件存在,可读 | 可调用对象存在,在 _q_exports 中 |
让我们考虑一个简单的 SPLAT! 的 `_q_index()`,它被编写为一个 Python 函数:
from quixote.html import html_quote from splat.web.util import get_bug_database def _q_index (request): result = ["""\ <html> <head><title>SPLAT! Bug Index</title></head> <body> <table> <tr> <th>bug id</th> <th>description</th> </tr> """] bug_db = get_bug_database() for bug in bug_db.get_all_bugs(): if bug.status != bug.ST_RESOLVED: result.append("""\ <tr> <td>%s</td> <td>%s</td> </tr> """ % (bug, html_quote(bug.description)) result.append("""\ </table> </body> </html> """) return "".join(result)
我们将网页构建为字符串列表,这些字符串在末尾连接在一起。(这比重复的字符串连接边框效率高得多。实际上,result += ... 循环可能符合 Python 中的反模式,因为它具有二次运行时间。)
对于这个简单的示例,将 `_q_index()` 编写为 Python 函数并不是太不方便,但是有一种更好的方法:PTL。PTL 只是 Python,它使用不同的方式来指定函数返回值。实际上,上面的函数很容易重写为 PTL 模板:
template _q_index (request): """\ <html> <head><title>SPLAT! Bug Index</title></head> <body> <table> <tr> <th>bug id</th> <th>description</th> </tr> """ bug_db = get_bug_database() for bug in bug_db.get_all_bugs(): if bug.status != bug.ST_RESOLVED: """\ <tr> <td>%s</td> <td>%s</td> </tr> """ % (bug, html_quote(bug.description)) """\ </table> </body> </html> """
在这个阶段,差异并不显着:PTL 版本隐式地累积和返回 HTML 文档,而不是显式地执行此操作。PTL 的工作原理是累积模板中运行的每个语句的结果;每个非 None 结果都存储在 TemplateIO 的实例中(Quixote 提供的一个类)。当模板返回时,它实际上返回 TemplateIO 对象的 `str()`。这会将所有累积的语句结果转换为字符串(再次使用 `str()`),并返回这些字符串的连接。
当您意识到可以像重构 Python 函数一样重构 PTL 模板时,PTL 开始变得有趣。例如,我们可以将我们的 `_q_index()` 模板分解如下:
template header (title): """\ <html> <head><title>SPLAT! - %s</title></head> <body> """ % html_quote(title) template footer (): """\ </table> </body> </html> """ template bug_row (bug): """\ <tr> <td>%s</td> <td>%s</td> </tr> """ % (bug, html_quote(bug.description) template _q_index (request, bug): header("Bug Index") """\ <table> <tr> <th>bug id</th> <th>description</th> </tr> """ bug_db = get_bug_database() for bug in bug_db.get_all_bugs(): if bug.status != bug.ST_RESOLVED: bug_row(bug) "</table>\" footer()
现在我们有了可重用的 `header()` 和 `footer()` 模板,并且我们简化了 `_q_index()` 的主循环。任何程序员都认识到过程抽象的价值;不幸的是,大多数 Web 模板语言都没有。
为我们的根命名空间编写 `_q_index()` 函数意味着,当用户请求与此应用程序对应的基本 URL 时,Quixote 可以生成响应。例如,您可以进行设置,使 `/bugs/` 成为您站点上 SPLAT! 的基本 URL,即 `/bugs/` 对应于 `splat.web` 包。当您的服务器收到对 `/bugs/` 的请求时,Quixote 将调用 `splat.web._q_index()`——由于 `splat/web/__init__.py` 中的导入,实际上是 `splat.web.index._q_index()`——并返回生成的 HTML 页面。但是,只要您可以在 Python(或 PTL)中实现某些功能,就可以使用 Quixote 将其与 URL 关联并将其放在 Web 上。
正如上表所示,Quixote 可以发布任何 Python 函数或 PTL 模板的结果,只要您将它们列在 `_q_exports` 中。例如,我可能想借用 GUI 编程中的约定,并向 SPLAT! 添加一个“关于”页面。在 URL 空间中放置此页面的显而易见的位置是 `/bugs/about`,因此我需要在 `splat.web` 包中添加一个可调用对象 `about()`。一种方法(尽管不一定是推荐的做法)是在 `splat/web/__init__.py` 中定义一个 Python 函数:
import splat # for __version__ from splat.web.util import header, footer [...] def about (request): text = '''\ <p>This bug database is brought to you by:</p> <p align="center"><font size="+3">SPLAT! %s</font></p> <p>For more information, please visit the <a href="http://www.mems-exchange.org/software/splat/"> SPLAT! web page</a>.</p> ''' % splat.__version__ return header("About") + text + footer()
这演示了 Python 和 PTL 代码如何干净地结合在一起。我可以导入上面显示的 `header()` 和 `footer()` 模板(顺便说一句,它们实际上位于 `splat.web.util` 中),并像调用 Python 函数一样调用它们。
但是,`about()` 函数实际上还不能工作。Quixote 信任任何碰巧以 URL 命名的随机 Python 函数都应该发布到 Web 上,这将是危险的。因此,您必须明确声明 `about()` 旨在导出到世界,方法是在此命名空间的 `_q_exports` 列表中列出它,该列表也位于 `splat/web/__init__.py` 中:
_q_exports = ['about']
仅仅编写 Python 函数和 PTL 模板,并让 Quixote 通过 Web 发布其结果,您就可以取得很大的进展。但是,使 URL 成为用户界面的一部分意味着,SPLAT! 发布单个错误的明显方法是通过类似 `/bugs/0001`、`/bugs/0134` 等 URL。Quixote 有一个很好的钩子,可让您处理像这样的任意 URL。
对象发布只是一个花哨的术语(无耻地从 Zope 窃取),意思是您可以使用 Quixote 在任意对象周围包装 Web 界面。与 Quixote 一样,技巧是将 URL 映射到 Python 代码。但是现在,您不需要提供一个命名为与 URL 组件匹配的 Python 函数,而是提供一个 Python 函数 `_q_getname()`,Quixote 将其用作后备。例如,如果 Quixote 正在处理 URL `/bugs/0124` 的请求,它将无法在 `splat.web` 包中找到名为 `0124` 的函数。在放弃并引发异常(这将转换为 HTTP 404 错误)之前,Quixote 会在该包中查找函数 `_q_getname()`。如果找到,Quixote 将调用您的 `_q_getname()`,并将字符串“0124”——当前正在检查的 URL 组件——传递给它。
不要将 `_q_getname()` 视为像 `_q_index()` 或 `about()` 一样。Quixote 仅在 URL 遍历的末尾调用这些函数:例如,在处理 URL `/bugs/about` 时,`bugs` 组件对应于 Python 命名空间 `splat.web`,因此 Quixote 不必调用任何东西。只有当它查看终端组件 `about` 时,它才会调用 Python 函数——上面定义的 `splat.web.about()` 函数。同样,在处理 URL `/bugs/` 时,Quixote 仅调用 `_q_index()`,因为 URL 的终端组件(最后一个斜杠后的部分)是空字符串。
但是,可以在 URL 的任何位置调用 `_q_getname()`。例如,SPLAT! 实际上将每个错误的 URL 实现为命名空间(例如,`/bugs/0134/` 调用 `_q_index()` 方法,`/bugs/0134/edit` 调用 `edit()` 方法等)。在这种情况下,错误 ID 不是 URL 的终端组件,但 Quixote 以相同的方式通过 `_q_getname()` 处理它。但是,对于本文,错误 ID 将是终端 URL 组件,我们仅处理类似 `/bugs/0134` 的 URL。执行此操作的最简单方法是编写 `_q_getname()` 函数。再次假设此代码位于 `splat/web/__init__.py` 中,它返回请求的错误的 HTML 页面:
from quixote.errors import TraversalError from splat.web.util import get_bug_database [...] def _q_getname (request, name): try: bug_id = int(name) except ValueError: raise TraversalError("invalid bug ID: %r (not an integer)" % name) bug_db = get_bug_database() bug = bug_db.get_bug(bug_id) if bug is None: raise TraversalError("no such bug: %r" % bug_id) return header("Bug %s" % bug) + format_bug_page(bug) + footer()
(我省略了 `format_bug_page()` 的实现。)此函数的大部分内容都与获取任意用户输入(以 URL 组件的形式)以及从错误数据库中获取错误对象或引发适当的异常有关。(Quixote 异常通常对应于 HTTP 错误代码;`TraversalException` 变为 404 “未找到”错误。应用程序唯一需要在 `_q_getname()` 函数内部引发 `TraversalException` 的时候,是因为所有其他 URL 解释都由 Quixote 在内部处理。)
使用 `_q_getname()` 为对象而不是单个页面发布命名空间更有趣,但这超出了本文的范围。现在我们已经对使用 Quixote 编程有了很好的感觉,让我们看一下从您的 Web 服务器到 Quixote 应用程序代码的必要繁文缛节。
每个 Quixote 应用程序都需要一些胶水来将 Web 服务器连接到应用程序;这种胶水的性质取决于连接的性质。将 Web 服务器连接到 Quixote 应用程序的最简单方法是 CGI,在这种情况下,您需要为您的应用程序提供 CGI 驱动程序脚本。SPLAT! 的 CGI 驱动程序脚本(顺便说一句,也适用于 FastCGI)看起来像这样:
1.#!/usr/bin/python 2. 3. from quixote import enable_ptl, Publisher 4. from splat.config import OptionParser, get_config 5. 6. enable_ptl() 7. config = get_config() 8. config.read_file("/www/conf/splat.conf") 9. pub = Publisher("splat.web", config=config) 10. pub.setup_logs() 11. pub.publish_cgi()
第 6 行中对 `enable_ptl()` 的调用安装了一个导入钩子,使 Python 的 `import` 语句将 PTL 模块视为与 Python 模块相同。它只需要在每个 Python 进程中完成一次,因此驱动程序脚本是执行此操作的明显位置。
第 7 行和第 8 行创建了一个标准的 SPLAT! 配置对象,并通过读取本地配置文件对其进行自定义。(实际上,它比这更复杂,因为 SPLAT! 有几个需要读取相同配置文件的辅助命令行脚本。)大多数 Quixote 应用程序都希望执行类似的操作,以便自定义 Quixote 的行为。特别是,Quixote 的默认设置优先考虑安全性和性能,而不是易于调试,因此对于开发新应用程序,通过读取本地配置文件来覆盖它们很有用。Quixote 提供的演示程序有一个执行此操作的简单示例。
第 9 行是我们最终确定 SPLAT! 的 Web 界面由 `splat.web` 包实现的位置。每个 Quixote 应用程序都以 `Publisher` 类的实例为中心,这是 Quixote 的所有 URL 解释完成的地方。因为此对象需要知道您的应用程序的根命名空间,所以它作为参数传递给 `Publisher` 构造函数,如所示。
每个 Quixote 应用程序最多可以有三个日志文件:错误日志、调试日志和访问日志。这些日志文件的名称通过传递给 `Publisher` 构造函数的配置对象指定(将它们放在您的本地配置文件中是一件好事),但是您需要调用 `setup_logs()`,如第 10 行所示,以确保日志文件实际上已打开并写入。Quixote 处理的每个 HTTP 请求都会记录在访问日志中;写入 `sys.stdout` 的每个字符串都写入调试日志;写入 `sys.stderr` 的每个字符串都写入错误日志。这意味着为调试 Quixote 应用程序进行检测就像添加 `print` 语句一样简单。
最后,在第 11 行中,我们将控制权传递给 Quixote。如果此驱动程序脚本用作 CGI 脚本,则整个过程将为每个 HTTP 请求重复;如果它作为 FastCGI 脚本处理,则 `process_cgi()` 方法将在 Web 服务器保持脚本运行时处理请求。
此时,您可以将驱动程序脚本安装到 Web 服务器配置为查找 CGI 脚本的任何位置,例如,对于标准的 Debian Apache 包,您将其放在 `/usr/lib/cgi-bin` 中。现在您可以通过类似 `/cgi-bin/splat.cgi/` 的 URL 访问 SPLAT!,这将起作用,但相当难看,并且暴露了很多实现细节。如果您使用启用了重写引擎的 Apache,则可以轻松添加一条规则,将 `/bugs/` 重写为 `/cgi-bin/splat.cgi/`,这样最终用户永远不必看到那个难看且信息过多的 URL。有关更多信息,请参阅 Quixote 源代码发行版中的 `doc/web-server.txt`。
Quixote 可从 www.mems-exchange.org/software/quixote 获取。您可以下载最新的源代码发行版(截至撰写本文时的当前版本为 0.5)、浏览文档、加入 quixote-users@mems-exchange.org 邮件列表或浏览邮件列表存档。
安装说明可以在网站上找到,也包含在源代码发行版的 `doc/INSTALL` 中。
Quixote 演示程序包含在源代码发行版中,是一个比 SPLAT! 更简单的示例应用程序。Quixote 演示程序的文档非常详细地介绍了代码,并在此过程中解释了 Quixote 的大多数重要原则。
您还会找到我在此处未介绍的一些有趣的 Quixote 功能的文档,特别是 Quixote 的会话管理界面及其 HTML 表单/小部件库。会话管理使您可以通过会话 cookie 维护关于您网站各个用户的服务器端信息,这对于动态网站具有各种有用的应用。Quixote 的表单/小部件库使构建和处理复杂的 Web 表单(仍然是与 Web 用户交互的唯一可移植、可靠的方式)变得更加容易。与 Quixote 的其余部分一样,它围绕常见的 Web 编程任务包装了一个面向对象的 Python 接口。
Quixote 最初的编写是因为我们对使用 Python 编写 Web 应用程序的可用选项不满意。唯一接近我们想要的工具是 Zope,但事实证明 Zope 比我们需要的更大、更复杂。Zope 从一开始就内置了“Web 设计师”与“Web 开发人员”的区别,并且非常努力地使网站大部分可以通过 Web 本身进行编辑。这是一个有趣的想法,但它给 Zope 增加了巨大的复杂性。作为非常乐于使用文本编辑器和文件系统的程序员,我们感到被冷落了。因此,在创建 Quixote 时,我们无耻地窃取了 Zope 最好的想法(将 URL 映射到 Python 对象),并将整个事情都面向 Python 程序员。最明显的例子是,Zope 将 URL 映射到对象数据库中的任意对象,而 Quixote 将它们映射到 Python 包、模块和函数——Python 程序员只需使用文本编辑器即可轻松创建和操作的对象。结果是一个 Web 应用程序框架,它使动态网页的创建变得如此容易,几乎感觉像作弊。
电子邮件:gward@python.net