从 web2py 迁移到 py4web

本章旨在帮助用户将旧的 web2py 应用程序移植到 py4web。

Web2py 和 py4web 有很多相似之处,也有一些不同之处。例如,它们共享相同的数据库抽象层(pyDAL),这意味着两个框架之间的 pyDAL 的表定义和查询是相同的。它们还共享相同的模板语言,但有一点需要注意,web2py 默认为 {{...}} 分隔符,而 py4web 默认为 [[...]] 分隔符。它们还共享相同的验证器,pyDAL 的一部分,以及非常相似的 helpers 。py4web 是一个更轻/更快/极简主义的重新实现,但它们具有相同的目的,并支持非常相似的语法。py4web 也提供了一个 Form 对象(相当于 web2py 中的 SQLFORM)和一个 Grid 对象(相当于 web2py 的 SQLFORM.Grid )。它们均提供一个可清理 HTML 内容的 XML 对象,以及一个用于生成 URL 的 URL helper。它们均能通过抛出 HTTP 异常来返回非 200 OK 状态码的页面。它们还均提供一个 Auth 对象,该对象可生成注册、登录、修改密码、找回密码及编辑个人资料的表单。此外,web2py 和 py4web 这两个框架都会跟踪并记录所有错误。

主要区别如下:

  • web2py 同时适用于 Python 2.6+ 和 3.6+ ,而py4web 仅在 Python 3.7+ 上运行。因此,如果你的旧 web2py 应用程序仍在使用 Python 2,你的第一步是将其迁移到至少 Python 3.7 ,最好是最新的 3.9 。

  • web2py 应用程序由在每个 HTTP 请求时执行的文件集合组成(使用自定义导入程序,按预定顺序)。在 py4web 应用程序中,是由框架自动导入的常规 python 模块。顺便说一句,这使得使用标准 python 调试器成为可能(即使在最常用的 IDE 中)。

  • 在 web2py 中,每个应用程序都有一个固定的文件夹结构。当且仅当它在 controllers/*.py 文件中定义时,函数才是一个 action 。py4web 的约束要小得多。在py4web 中,应用程序必须有一个入口点 __init__.py 和一个 static 文件夹。其他所有约定,如模板、上传文件、翻译文件、会话等的位置,都由用户指定的。

  • 在 web2py 中,scaffolding 应用程序(创建新应用程序的蓝图)被称为 “welcome”。在 py4web 中,它被称为 “_scaffold ”。_scaffold 包含一个 “settings.py” 文件和一个 “common.py” 文件。后者提供了一个如何启用 Auth 并为特定应用程序配置所有选项的示例。_scaffold 还有一个 “model.py” 文件和一个 “controller.py” 文件,但与 web2py 不同,这些文件没有以任何特殊方式处理。它们的名称遵循一个约定(框架没有强制执行),并且与任何常规 python 模块一样,它们由 __init__.py 文件导入。

  • 在 web2py 中, controllers/*.py 中的每个函数都是一个 action 。在 py4web 中,如果一个函数有 @action("...") 装饰符,那么它就是一个动作。这意味着 @action("...") 可以在任何地方定义。管理界面将帮助您定位特定 action 的定义位置。

  • 在 web2py 中,URL 和文件/函数名之间的映射是自动的,但可以在 “routes.py” 中被覆盖(就像在 Django 中一样)。在 py4web 中,映射在装饰器 @action('my_url_path') 中被指定(就像在 Bottle 和 Flask 中一样)。请注意,如果路径以 “/” 开头,则假定为绝对路径。如果不是,则假定它是相对的,并且前缀为 “/{appname}/” 。此外,如果路径以 “/index” 结尾,则后缀(“/index”)被视为可选。

  • 在 web2py 中,路径扩展很重要,“http://*.html“ 预计将返回 HTML,同时 “http://*.json” 预计将返回 JSON 等。在 py4web 中没有这样的约定。如果 action 返回一个 dict() 并有一个模板,则 dict() 将由模板渲染后呈现,否则将以 JSON 呈现。使用装饰器可以实现更复杂的行为。

  • 在 web2py 中,每个 action 都有许多包装器,例如,无论 action 是否真的需要,它们都可以处理会话、多元化、数据库连接等。这使得 web2py 的性能很难与其他框架进行比较。在 py4web 中,一切都是可选的,必须使用 @action.uses(...) 装饰器为每个操作启用和配置功能。 @action.uses(...) 的参数被称为 fixtures 装置,类似于房子里的夹具装置。它们通过为 action 提供预处理和后处理来添加功能。例如, @action.uses(session, T, db, flash) 表示该 action 需要在重定向时使用会话、国际化/多元化(T)、数据库(db)和flash 消息的继续状态。

  • web2py 使用自己的 request/response 对象。py4web 使用底层Ombott库中的 request/response 对象。虽然这在未来可能会发生变化,但我们致力于保持它们与 web 服务器的接口、路由、部分请求和文件流(如果此后进行了修改)。

  • web2py 和 py4web 都使用相同的 pyDAL,因此表使用相同的语法定义,查询也是如此。在 web2py 中,当执行整个模型时,每个 HTTP 请求都会重新定义表。在 py4web 中,每个 HTTP 请求只执行 action 中的代码,而在 action 之外定义的代码只在启动时执行。这使得 py4web 更快,特别是在有很多表的情况下。这种方法的缺点是,开发人员应该小心,永远不要在 action 中或以任何依赖于请求对象内容的方式覆盖 pyDAL 变量,否则代码就不是线程安全的。唯一可以随意更改的变量是以下字段属性:readable 、 writable 、 requires 、 update 和 default 。出于实际目的,所有其他的都被认为是全局和非线程安全的。因此,在 py4web 中使用 懒惰表 毫无用处,甚至很危险。

  • web2py 和 py4web 都有一个用于相同目的的 Auth 对象。这两个对象都能够以几乎相同的方式生成表单。py4web 的 Auth 被定义为更加模块化和可扩展,并同时支持 Forms 和 API,但它缺少 auth.requires_* 装饰器和组成员资格/权限。这并不意味着该功能不可用。事实上,py4web 甚至更强大,这就是语法不同的原因。虽然 web2py Auth 对象试图做所有事情,但相应的 py4web 对象只负责建立用户的身份,而不是用户可以做什么。后者可以通过向用户附加标签来实现。因此,通过用用户所属组的标签标记用户并根据用户标签检查权限来分配组成员资格。Py4web 提供了一种机制,可以有效地为任何对象(包括但不限于用户)分配和检查标签。

  • Web2py 附带了 Rocket 网络服务器。在撰写本文时,py4web 默认使用 Rocket3 服务器,这是 web2py 使用的同一个多线程 web 服务器,去掉了所有 Python2 的逻辑和依赖关系。请注意,这可能会在未来发生变化。

简单的转换示例

“Hello world” 示例

web2py

# in controllers/default.py
def index():
   return "hello world"

--> py4web

# file imported by __init__.py
@action('index')
def index():
    return "hello world"

“带变量重定向” 的示例

web2py

request.get_vars.name
request.post_vars.name
request.env.name
raise HTTP(301)
redirect(url)
URL('c','f',args=[1,2],vars={})

--> py4web

request.query.get('name')
request.forms.get('name') or request.json.get('name')
request.environ.get('name')
raise HTTP(301)
redirect(url)
URL('c', 'f', 1, 2, vars={})

“返回变量” 的示例

web2py

def index():
   a = request.get_vars.a
   return locals()

--> py4web

@action("index")
def index():
   a = request.query.get('a')
   return locals()

“返回参数” 的示例

web2py

def index():
   a, b, c = request.args
   b, c = int(b), int(c)
   return locals()

--> py4web

@action("index/<a>/<b:int>/<c:int>")
def index(a,b,c):
   return locals()

“返回调用方法” 的示例

web2py

def index():
   if request.method == "GET":
      return "GET"
   if request.method == "POST":
      return "POST"
   raise HTTP(400)

--> py4web

@action("index", method="GET")
def index():
   return "GET"

@action("index", method="POST")
def index():
   return "POST"

“设置计数器” 的示例

web2py

def counter():
   session.counter = (session.counter or 0) + 1
   return str(session.counter)

--> py4web

def counter():
   session['counter'] = session.get('counter', 0) + 1
   return str(session['counter'])

“视图” 的示例

web2py

{{ extend 'layout.html' }}
<div>
{{ for k in range(1): }}
<span>{{= k }}<span>
{{ pass }}
</div>

--> py4web

[[ extend 'layout.html' ]]
<div>
[[ for k in range(1): ]]
<span>[[= k ]]<span>
[[ pass ]]
</div>

“Form 和 flash” 的示例

web2py

db.define_table('thing', Field('name'))

def index():
   form = SQLFORM(db.thing)
   form.process()
   if form.accepted:
      flash = 'Done!'
   rows = db(db.thing).select()
   return locals()

--> py4web

db.define_table('thing', Field('name'))

@action("index")
@action.uses(db, flash)
def index():
   form = Form(db.thing)
   if form.accepted:
      flash.set("Done!", "green")
   rows = db(db.thing).select()
   return locals()

在模板中,您可以通过以下方式访问 flash 对象

<div class="flash">[[=globals().get('flash','')]]</div>

或者使用更复杂的

<flash-alerts class="padded " data-alert="[[=globals().get( 'flash', '')]]"></flash-alerts>

后者需要 scaffolding 应用程序中的 utils.js 将自定义标签呈现为带有关闭功能的 div。

还要注意, Flash 很特别:它是一个单例。因此,如果实例化多个 Flash 对象,它们会共享数据。

“grid” 的示例

web2py

def index():
   grid = SQLFORM.grid(db.thing, editable=True)
   return locals()

--> py4web

@action("index")
@action.uses(db, flash)
def index():
   grid = Grid(db.thing)
   form.param.editable = True
   return locals()

“访问操作系统文件” 的示例

web2py

file_path = os.path.join(request.folder, 'file.csv')

--> py4web

from .settings import APP_FOLDER
file_path = os.path.join(APP_FOLDER, 'file.csv')

“auth” 的示例

web2py

auth = Auth()
auth.define_tables()

@requires_login()
def index():
   user_id = auth.user.id
   user_email = auth.user.email
   return locals()

def user():
    return dict(form=auth())

通过 http://.../user/login 访问。

--> py4web

auth = Auth(define_table=False)
auth.define_tables()
auth.enable(route='auth')

@action("index")
@action.uses(auth.user)
def index():
   user_id = auth.user_id
   user_email = auth.get_user().get('email')
   return locals()

通过 http://.../auth/login 进行访问。请注意,在 web2py 中, auth.user 是从会话中检索到的当前登录用户。在 py4web 中, auth.user 是一个夹具,其作用与 web2py 中的 @requires_login 相同。在 py4web 中,只有 user_id 存储在会话中,可以使用 auth.user_id 检索。如果你需要更多关于用户的信息,你需要使用 auth.get_user() 从数据库中获取记录,它将所有可读字段作为 Python 字典返回。

请注意,以下两者之间存在显著差异:

@action.uses(auth)

@action.uses(auth.user)

在第一种情况下,被修饰的 action 可以访问 auth 对象,但如果用户未登录,auth.user_id 可能为 None。在第二种情况下我们需要一个有效的登录用户,因此能保证 auth.user_id 是一个有效用户 id。

还要注意,如果一个操作(action)使用 auth,那么它会自动使用其 session 和 flash 对象。