Einblick in web.py

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Freitag 4. Januar 2008, 16:16

Ich habe mir eben web.py angeschaut.

Mir gefällt, wie simpel ein einfaches "Hallo Welt" ist. Mir gefällt auch, dass es hinreichend komplett mit einem eigenen Server, einer Template-Lösung einer simplen Datenbank-Abstraktion daher kommt. Eine Session-Verwaltung fehlt allerdings bereits.

Es gibt zwar (ein sogar deutschsprachiges) Tutorial, aber ansonsten ist die Dokumentation außerordentlich schlecht. Nicht nur, dass es kaum irgendwelche Informationen auf der Webseite gibt, der Quelltext ist schwer verdaulich, weil kaum kommentiert und mit äußerst kurzen Variablen oder Funktionsnamen gesegnet. Ich erwarte als Doku außerdem nicht nur das API - das könnte ich im Quelltext nachlesen - sondern die Art und Weise, wie man das Ganze einsetzen soll.

Dennoch, vielleicht als Hilfe für andere, hier mein simples Beispiel in Tutorial-Form.

Das obligatorisches "Hallo Welt" zeigt, wie URLs auf Controller abgebildet werden und wie die Antwort per PRINT erzeugt wird. Die letzte Zeile startet einen Server auf Port 8080.

Code: Alles auswählen

import web

urls = ('/', 'index')

class index:
    def GET():
        print "Hallo, Welt"

web.run(urls, globals())
Ich habe einen Wiki gebaut, der Sqlite als Datenbank nutzt. Die Datenbank "code.db" habe ich so angelegt (gibt es überhaupt einen Unterschied zwischen `string` und `text`? Egal...):

Code: Alles auswählen

create table pages (name string primary key, title string, content text);
insert into pages (name, title, content) values ('index', 'The Index', 'This is the index');
Diese Zeile verbindet web.py mit meiner Datenbank:

Code: Alles auswählen

web.config.db_parameters = dict(dbn='sqlite', db='code.db')
Meine URLs sehen so aus:

Code: Alles auswählen

urls = (
    '/', 'index',
    '/(\w+)', 'page',
    '/(\w+)/edit', 'edit',
    '/(\w+)/save', 'save',
)
Zugriff auf das web.py-Template-System habe ich mit diesem Objekt, dem ich sage, wo sich meine Templates befinden - nämlich in einem Unterverzeichnis `templates`:

Code: Alles auswählen

render = web.template.render('templates/', cache=False)
Auf "/" erfolgt ein Redirect auf die "index"-Seite:

Code: Alles auswählen

class index:
    def GET(self):
        web.redirect('/index')
Wikiseiten zeigt dieser Controller an:

Code: Alles auswählen

class page:
    def GET(self, name):
        p = get_page(name)
        if p:
            print render.base(p.title, render.view(p))
        else:
            web.redirect('/%s/edit' % name)
Dazu einige Erklärungen.

Eine Seite laden macht die folgende Funktion. Hier bin ich das erste Mal ob der mangelhaften Dokumentation verzweifelt. Vielleicht gibt es einen besseren Weg, ein Objekt (web.py nutzt hier spezielle dicts) aus der Datenbank zu laden.

Code: Alles auswählen

def get_page(name):
    for p in web.select('pages', where="name=$name", vars=locals(), limit=1):
        return p
    return None
Mir hätte ja ein `where=("name=?", name)` als Syntax besser gefallen. Vielleicht baue ich mir ja mein eigenes sma(rt)web.py, da mache ich das dann so ;)

Meine Templates bestehen aus einem gemeinsamen Basis-Template und je einem View-spezifischen. Ich habe mir das so aus einem Beispiel abgeschaut, Vererbung muss man explizit im Controller machen. Nun, ja. "render" hat jetzt für jede Datei ein magisches Attribut, darin eine Funktion, der ich die im Template definierten Parameter übergebe. Oh, man beachte das ":" vor body, was definiert, dass dieser Wert nicht automatisch escaped werden soll - das ansonsten automatisch < und & escaped wird, finde ich sehr gut.

Code: Alles auswählen

def with (title, body)    "base.html"
<html>
<head>
    <title>$title</title>
    <style type="text/css" media="screen">
        pre { margin: 1em; background: #eee; padding: .5em; }
    </style>
</head>
<body>
    <h1>Wiki</h1>
    <div>$:body</div>
    <h6>© 2007 by me</h6>
</body>

Code: Alles auswählen

$def with (page)    "view.html"
<h2>$page.title</h2>
<div>$page.content</div>
<p><a href="/$page.name/edit">Edit this page</a></p>
Analog der Controller, der die Webeite zum Editieren einer Wikiseite baut:

Code: Alles auswählen

class edit:
    def GET(self, name):
        p = get_page(name)
        if not p:
            p = web.Storage(name=name, title=name, content="")
        print render.base(p.title, render.edit(p))

Code: Alles auswählen

$def with (page)    "edit.html"
<h2>Edit $page.title</h2>
<form method="post" action="/$page.name/save">
    <input type="text" name="title" value="$page.title"/>
    <br/>
    <textarea cols="60" rows="20" name="content">$page.content</textarea>
    <br/>
    <input type="submit" value="Save"> or <a href="/$page.name">Cancel</a>
</form>
Ich glaube, man kann auch irgendwie Formulare generieren lassen, aber außer das es "form.py" gibt kann ich da nix sagen. Ist aber in meinem Fall auch so einfach genug. Doch es wäre schon schön, wenn Validierung, Userverwaltung, usw. auch mit nur 2 Zeilen hinzugefügt werden könnten.

So weit, so gut. Nicht so schick der - wegen des kruden DB-Zugriffs - der verbleibende Controller, der eine Seite speichert:

Code: Alles auswählen

class save:
    def POST(self, name):
        i = web.input()
        if get_page(name):
            web.update('pages', 
                title=i.title, content=i.content, 
                where='name=$name', vars=locals())
        else:
            web.insert('pages',
                seqname=False, 
                name=name, title=i.title, content=i.content)
        web.redirect("/%s" % name)
Man beachte, diesmal wird "POST" statt "GET" als Methode benutzt. Mit "web.input" kommt man an die Request-Daten (ieek) und in meiner Verzweiflung, zwischen einem Update und eine Save unterscheiden zu müssen, mache ich auch noch ein "get_page". Da web.py davon ausgeht, dass eine Tabelle eine "id"-Spalte mit Autoinkrement hat - meine jedoch nicht die Erwartungen von web.py erfüllt - muss ich diesen Automatismus mit "seqname=False" abschalten (wieso False, wieso nicht None?!).

Ok, aber ihr habt jetzt meinen Wiki gesehen, er funktioniert und wenn man noch dies macht, ist die Entwicklung auch nicht ganz so schmerzhaft (die Debug-Seite stammt übrigens von Django ;):

Code: Alles auswählen

web.webapi.internalerror = web.debugerror
web.run(urls, globals(), web.reloader)
Für den Produktivbetrieb sollte man wohl das "cache=False" in "render" entfernen.

Ergo: Leichtgewichtig, vielleicht zu leicht, denn alles was jetzt schnell geht, könnte sich bei umfangreicheren Projekten rächen. Der DB-Zugriff ist IMHO die schwächste Komponente von web.py. Das Template-Rahmenwerk scheint interessant, ist aber noch nicht fertig. Die Debug-Seite könnte mehr eigene Identität vertragen und es wäre nett, wenn man - wie bei Werkzeug - eine kleine Kommandozeile hätte. Formularverarbeitung braucht Dokumentation genau wie der URL-Dispatch und was und wie das mit der Middleware geht.

Stefan
Antworten