从 web2py 迁移到 py4web

This chapter is dedicated to helping users port old web2py applications to 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 apps consist of a collection of files which are executed at every HTTP request (using a custom importer, in a predetermined order). In py4web apps are regular python modules that are imported automatically by the frameworks. By the way, this makes possible the use of standard python debuggers (even inside the most used IDEs).

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

  • In web2py the scaffolding app (the blueprint for creating new apps) is called “welcome”. In py4web it is called “_scaffold”. _scaffold contains a “settings.py” file and a “common.py”. The latter provides an example of how to enable Auth and configure all the options for the specific app. _scaffold has also a “model.py” file and a “controller.py” file but, unlike web2py, those files are not treated in any special manner. Their names follow a convention (not enforced by the framework) and they are imported by the __init__.py file as for any regular python module.

  • 在 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 中使用 懒惰表 毫无用处,甚至很危险。

  • Both web2py and py4web have an Auth object which serves the same purpose. Both objects have the ability to generate forms pretty much in the same manner. The py4web one is defined to be more modular and extensible and support both Forms and APIs, but it lacks the auth.requires_* decorators and group membership/permissions. This does not mean that the feature is not available. In fact py4web is even more powerful and that is why the syntax is different. While the web2py Auth objects tries to do everything, the corresponding py4web object is only in charge of establishing the identity of a user, not what the user can do. The latter can be achieved by attaching Tags to users. So group membership is assigned by labeling users with the Tags of the groups they belong to and checking permissions based on the user tags. Py4web provides a mechanism for assigning and checking tags efficiently to any object, including but not limited to, users.

  • 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 对象。