From web2py to py4web

This chapter is dedicated to help users for porting old web2py applications to py4web.

Web2py and py4web share many similarities and some differences. For example they share the same database abstraction layer (pyDAL) which means pydal table definitions and queries are identical between the two frameworks. They also share the same template language with the minor caveat that web2py defaults to {{…}} delimiters while py4web defaults to [[…]] delimiters. They also share the same validators, part of pyDAL, and very similar helpers. The py4web ones are a lighter/faster/minimalist re-implementation but they serve the same purpose and support a very similar syntax. They both provide a Form object (equivalent to SQLFORM in web2py) and a Grid object (equivalent to SQLFORM.grid in web2py). They both provide a XML object that can sanitize HTML and URL helper to generate URL. They both can raise HTTP to return non-200 OK pages. They both provide an Auth object that can generate register/login/change password/lost password/edit profile forms. Both web2py and py4web track and log all errors.

Some of the main differences are the following:

  • web2py works with both Python 2.6+ and 3.6+, while py4web runs on Python 3.7+ only. So, if your old web2py application is still using Python 2, your first step involves migrating it to at least Python 3.7, better if the latest 3.9.

  • web2py apps consist of 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).

  • In web2py every app has a fixed folder structure. A function is an action if and only if it is defined in a controllers/*.py file. py4web is much less constraining. In py4web an app must have an entry point __init__.py and a static folder. Every other convention such as the location of templates, uploaded files, translation files, sessions, etc. is user specified.

  • In web2py the scaffolding app (the blue print 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.

  • In web2py every function in controllers/*.py is an action. In py4web a function is an action if it has the @action("...") decorator. That means that actions can be defined anywhere. The admin interface will help you locate where a particular action is defined.

  • In web2py the mapping between URLs and file/function names is automatic but it can be overwritten in “routes.py” (like in Django). In py4web the mapping is specified in the decorator as in @action(‘my_url_path’) (like in Bottle and Flask). Notice that if the path starts with “/” it is assumed to be an absolute path. If not, it is assumed to be relative and prepended by the “/{appname}/” prefix. Also, if the path ends with “/index”, the latter postfix is assumed to be optional.

  • In web2py the path extension matters and “http://.html” is expected to return HTML while “http://.json” is expected to return JSON, etc. In py4web there is no such convention. If the action returns a dict() and has a template, the dict() will be rendered by the template, else it will be rendered in JSON. More complex behavior can be accomplished using decorators.

  • In web2py there are many wrappers around each action and, for example, they could handle sessions, pluralization, database connections, and more whether the action needs it or not. This makes web2py performances hard to compare with other frameworks. In py4web everything is optional and features must be enabled and configured for each action using the @action.uses(...) decorator. The arguments of @action.uses(...) are called fixtures in analogy with the fixtures in a house. They add functionality by providing preprocessing and postprocessing to the action. For example @action.uses(session, T, db, flash) indicates that the action needs to use session, internationalization/pluralization (T), the database (db), and carry on state for flash messages upon redirection.

  • web2py uses its own request/response objects. py4web uses the request/response objects from the underlying Ombott library. While this may change in the future we are committed to keep them the interface with the web server, routing, partial requests, if modified since, and file streaming.

  • Both web2py and py4web use the same pyDAL therefore tables are defined using the same exact syntax, and so do queries. In web2py tables are re-defined at every HTTP request, when the entire models are executed. In py4web only the action is executed for every HTTP request, while the code defined outside of actions is only executed at startup. That makes py4web much faster, in particular when there are many tables. The downside of this approach is that the developer should be careful to never override pyDAL variables inside action or in any way that depends on the content of the request object, else the code is not thread safe. The only variables that can be changed at will are the following field attributes: readable, writable, requires, update, default. All the others are for practical purposes to be considered global and non thread safe. This is also the reason that makes using Lazy Tables with py4web useless and even dangerous.

  • Both web2py and pyweb have an Auth object which serve the same purpose. Both objects have the ability to generate forms pretty much in the same manner. The py4web ones 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 comes with the Rocket web server. py4web at the time of writing defaults to the Rocket3 web server, which is the same multi-threaded web server used by web2py stripped of all the Python2 logic and dependencies. Note that this may change in the future.

Simple conversion examples

“Hello world” example

web2py

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

–> py4web

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

“Redirect with variables” example

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={})

“Returning variables” example

web2py

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

–> py4web

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

“Returning args” example

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()

“Return calling methods” example

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"

“Setting up a counter” example

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'])

“View” example

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 and flash” example

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()

In the template you can access the flash object with

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

or using the more sophisticated

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

The latter requires utils.js from the scaffolding app to render the custom tag into a div with dismissal behavior.

Also notice that Flash is special: it is a singleton. So if you instantiate multiple Flash objects they share their data.

“grid” example

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()

“Accessing OS files” example

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” example

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())

Access with 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()

Access with http://.../auth/login. Notice that in web2py auth.user is the current logged-in user retrieved from session. In py4web instead auth.user is a fixture which serves the same purpose as @requires_login in web2py. In py4web only the user_id is stored in the session and it can be retrieved using auth.user_id. If you need more information about the user, you need to fetch the record from the database with auth.get_user() The latter returns all readable fields as a Python dictionary.

Also notice there is a big difference between:

@action.uses(auth)

and

@action.uses(auth.user)

In the first case the decorated action can access the auth object but auth.user_id may be None if the user is not logged in. In the second case we are requiring a valid logged in user and therefore auth.user_id is guaranteed to be a valid user id.

Also notice that if an action uses auth, then it automatically uses its session and its flash objects.