Skip to content

Article and example code about different micro web frameworks for Python

Notifications You must be signed in to change notification settings

sma/microwebframeworks

Repository files navigation

Microwebframeworks

Simon Willison erwähnt in Django Heresies 5 Mikro-Web-Rahmenwerke juno, newf, mnml, itty und djng, die ich mir im folgenden genauer anschauen möchte. Wer mag, ergänze doch bitte weitere Rahmenwerke.

Beachtenswert finde ich, dass github allgegenwärtig ist. Mir gefällt's.

Außerdem ist es wohl ein ungeschriebenes Gesetz, das der Name 4 Buchstaben haben muss. Das ist dann auch der Grund, warum ich "web.py" oder "Werkzeug" nicht erwähne. Die Anzahl der Buchstaben passt nicht ;) Vielleicht, wenn es "wkzg" hieße...

newf

Größe: 145 Zeilen. Imports: re und cgi. WSGI-basiert. Abhängigkeiten: Keine. Allerdings ist es ohne Template-Sprache (Jinja, Cheetah oder Mako werden erwähnt) und ORM (Autumn, Storm oder SQLObject werden erwähnt) kaum zu etwas zu gebrauchen. Für Sessions wird Beaker empfohlen.

Im Prinzip stellt newf ein Request- und ein Response-Objekt zur Verfügung und eine Application, der man eine Liste von Mappings von URLs auf View-Funktionen übergibt. Und das war's dann auch schon.

Hier ist ein Beispiel:

import newf

def hello(request, name="Welt"):
    return newf.Response("Hallo %s" % name)
    
application = newf.Application((
    (r'^/hello$', hello),
    (r'^/hello/(?P<name>.*)$', hello),
))

Sympatisch finde ich, dass SQLalchemy (was IMHO überbewertet wird) explizit nicht erwähnt wird. Autumn ist ein ORM vom Autor selbst. Leider ist Dokumentation Mangelware, sodass ich nur sagen kann, dass das Query-API ähnlich zu dem von Django aussieht und Modelle ebenfalls ähnlich definiert werden. Es gibt jedoch keine Manager, sondern man baut explizit Query-Objekte. Relationen sehen irgendwie unfertig aus.

Das aber nur nebenbei.

Das URL-Mapping sieht ein bisschen wie bei "web.py" aus und ansonsten bietet das Rahmenwerk eigentlich so wenig, dass ich's kaum als ein Rahmenwerk bezeichnen würde. Der größte Teil des Systems ist noch die 30-Zeilen lange Liste mit den Namen der verschiedenen HTTP-Codes -- IMHO total entbehrlich.

Ein Wiki

Als größeres Beispiel will ich einen Wiki bauen.

Die Klasse Wiki kapselt die notwendige Funktionalität. Über pages() bekommt man eine Liste aller Seiten. Über get_page() kann ich den Text einer Seite erfragen und mit set_page() wieder schreiben. Außerdem gibt es eine Methode, in der man eine Wiki-typische Formatierung implementieren könnte.

Quelltext

Hier ist die zugehörige Web-Anwendung.

Das explizite esc, quote und unquote nervt genau wie die das Fehlen von Templates. Ich hätte natürlich eines der vorgeschlagenen Template-Systeme nutzen können, doch ich würde mir selbst von einem Mikro-Web-Rahmenwerk wünschen, dass es Feature-Complete ist. Man beachte auch, dass ich explizit prüfen muss, ob wohl ein POST angekommen ist. Und der cgi.FieldStorage leckt durch und bricht IMHO die Abstraktion.

Templates

Eine kleine Template-Bibliothek, genannt "tmpl", kann man in 30 Zeilen schreiben. Man erbt von Tmpl und jedes mit "_" beginnende Attribut der Klasse ist ein Template. Ich ersetze Text in ${...}, der optional mit |e oder |u HTML-konform "escaped" bzw. wie eine URL formatiert werden kann, definiere Blöcke mit ${block ...}...${endblock}, erbe von anderen Templates mit ${extends ...} und kann einfache, nicht schachtelbare Schleifen mit ${for var in items}...${endfor} ausführen.

Quelltext

Hier nochmal die Web-Anwendung mit "echten" Templates

mnml

Größe 409 Zeilen. Imports re, sys und cgi. WSGI-basiert. Abhängigkeiten: Keine. Auch hier fehlt eine Template-Sprache oder ein ORM. Für Sessions könnte man wieder Beaker benutzen.

Ich rate mal, mnml steht für minimal. Dabei ist es der große Bruder von newf, aus dem es wohl entstanden ist. Die 30 Zeilen mit den HTTP-Codes findet man jedenfalls auch hier wieder. Neben einem HttpRequest- und HttpResponse-Objekt stellt mnml noch einen RequestHandler zur Verfügung. Zwei verschiedene WebApplication-Unterklassen erlauben URL-Mappings genau wie bei newf und Django oder wie bei Pylons und Rails. Der Code hätte dabei noch Optimierungspotential.

Hier ist ein Beispiel:

import mnml

class Hello(mnml.RequestHandler):
    def GET(self, name="World"):
        return mnml.HttpResponse("Hello, %s" % name)
        
application = mnml.TokenBasedApplication((
    ('/hello', Hello),
    ('/hello/:name', Hello),
))

Enttäuschend ist, dass TokenBasedApplication gar nicht funktioniert. Da fehlt ein groups = matches.groups() im Code. Das musste ich erst reparieren. Echte Vorteile gegenüber newf sind auch nicht auszumachen. Warum das jetzt 250 Zeile mehr Code braucht, ist mir unklar.

Dank der RequestHandler brauche ich jetzt keine explizite /save-URL mehr, sondern kann ein POST gegen /edit/:name machen. Doch sind diese GET und POST-Methoden schöner als einfache view-Funktionen?

Ich erkenne keine Verbesserung gegenüber newf.

Dennoch, hier ist der Wiki: Wiki mit Templates

itty

Größe 467 Zeilen. Imports cgi, mimetypes, os, re, sys und optional urlparse. WSGI-basiert. Abhängigkeiten: Keine. Template-Sprache oder ORM fehlen. Auch Sessions gehen nicht ohne externen Code. Neben der üblichen wsgiref-Anwendung ist itty auch mit Adaptern für die Google AppEngine, CherryPy, Flup, Paste und Twisted versehen. Kostet zwar ~60 Zeilen Code, scheint aber sinnvoll. Meine Freunde, die HTTP-Status-Codes sind übrigens auch wieder mit von der Partie.

Sinatra wird als Inspirationsquelle genannt, aber wird es erreicht? Sinatra behauptet von sich selbst zwar auch, minimal (AFAIK etwa 1500 Zeilen) zu sein, doch benötigt eine Reihe weiterer Gems. Dafür integriert es Templates (HAML) und bietet eine nette DSL. So etwas habe ich in Python bislang noch nicht gesehen.

So sieht mein Hallo-Welt-Beispiel mit itty aus:

from itty import get, run_itty

@get("/hello")
@get("/hello/(?P<name>.*)")
def hello(request, name="mundus"):
    return "Salve, %s!" % name

run_itty()

Der get-Decorator (es gibt auch welche für POST, PUT, DELETE) ist nett, braucht aber einen regulären Ausdruck. Die :name-Notation beginnt mir auch zu gefallen.

Positiv zu erwähnen ist, dass itty ein paar Beispiele mitbringt (nennenswerte Dokumentation gibt es keine), die zeigen, wie man statische Dateien einbindet oder einen error-Handler benutzt. Ansonsten ist auch itty nur ein dünner Request/Response-Wrapper ohne weitere Abstraktionen, genau wie newf oder mnml.

Mein Wiki-Beispiel ist schnell umgeschrieben. Ich kann die expliziten Response-Objekte einsparen. Auch ist request.POST offenbar ein normales dict und abstrahiert so vom cgi.FieldStorage. Für ein Redirect muss man raise statt return benutzen.

Zusammen mit meinen Templates gefällt mir itty recht gut, auch wenn sie 10 Zeilen länger ist als die Version, die einfach so HTML generiert.

Dennoch, hier ist der Wiki: Wiki mit Templates

juno

Mit 783 Zeilen ist juno der Bolide unter den Mikro-Web-Rahmenwerken. Imports cgi, re, os, sys, mimetype und urlparse. Juno will Mako oder Jinja2 als Template-Engine haben und konfiguiert diese automatisch. Juno braucht außerdem SQLalchemy und bindet diese Bibliothek als ORM ein. Optional kann flup benutzt werden, um statt WSGI lieber SCGI oder FCGI zu benutzen. Auch ein Adapter für die Google AppEngine ist vorhanden.

Juno ist das erste Rahmenwerk, dass auf Template und ORM eine Antwort hat. Während es aus praktischen Überlegungen natürlich sinnvoll ist, vorhandene Rahmenwerke einzubinden, ist es aus der Sicht der Minimalität IMHO eine diskussionswürdige Entscheidung, Komponenten zu benutzen, die 10x größer sind als das eigene Rahmenwerk. Das würde ich dann nicht mehr mit dem Attribut "micro" versehen. Ich finde, der Trick ist ja, mit einer minimalen Anzahl an Code-Zeilen das Maximum an Funktion herauszuholen.

Positiv erwähnt werden muss die umfangreiche Dokumentation.

Hier ist das obligatorische Beispiel.

from juno import *

init({'use_templates': False, 'use_db': False})

@get(['/hello', '/hello/:name'])
def hello(web, name="qo'"):
    return "%s nuqneH" % name

run()

Wie bei itty gibt es einen get-Dekorator (und die Varianten für andere HTTP-Methoden). Und es gibt wieder Rails-style Pattern-Matching statt regulärer Ausdrücke. Das man zwei Routen in einer Liste zusammenfassen kann, ist eine gute Idee.

Man muss in init allerdings explizit Templates und Datenbank abschalten, wenn man das Programm laufen lassen will und wie ich, weder Jinja2 noch SQLalchemy installiert hat. Das finde ich ein bisschen unschön.

Für den Datenbank-Zugriff hat sich Juno ein etwas merkwürdiges Verfahren mit Factory-Funktionen ausgedacht. Damit wird von der etwas schwerfälligen Modellbeschreibung von SQLalchemy abstrahiert. Das leakt aber sofort wieder durch die Abstraktion, wenn es um Queries geht. Auch der Weg, wie man zusätzliche Methoden in seine "Klassen" bringt, ist IMHO hässlich.

Page = juno.Model('Page',
    title='string', 
    text='text', 
    last_updated='datetime',
    __repr__=lambda self: "#<Page %s>" % self.title
)
page = Page(title="Home", text="...", last_updated=datetime.now())
page.add()
page.save()

Hingegen gefällt mir wieder, wie ich mit zwei Zeilen meine Template-Engine in Juno einbauen kann:

config('get_template_handler', lambda path: T.get(path))
config('render_template_handler', lambda tmpl, **kwargs: T.render(tmpl, kwargs))

Die "lambdas" sind notwendig, weil ich erst nach dieser Definition meine Klasse T definiere. Andernfalls gäbe es einen Fehler. Ungewöhnlich finde ich, dass template und redirect zwei globale "Funktionen" sind, die in den Views funktionieren, obwohl diese nichts zurückgeben. Ansonsten sieht das (nicht ohne Zufall) fast genauso wie itty und all die anderen Rahmenwerke aus.

Hier noch mein Wiki.

djng

Größe ~160 Zeilen. Includes Django. Damit ist Djng eigentlich kein Mikro-Web-Rahmenwerk meiner Definition nach. Stattdessen soll es den Einsatz von Django einfacher machen. Willision definiert Mikro-Rahmenwerk allerdings so, dass man damit Anwendungen in nur einer Datei bauen kann. Das stimmt mit der Idee von Sinatra überein. Also liegt er wahrscheinlich nicht so falsch.

Djng ist übrigens ein einzige Rahmenwerk, das in mehr als einer Datei definiert ist.

Das klassische Beispiel:

import djng

def hello(request, name="Welt"):
    return djng.Response("Hallo, %s!" % name)

app = djng.Router(
    (r'^hello$', hello),
    (r'^hello/(+*)$', hello),
)

if __name__ == '__main__':
    djng.serve(app, '0.0.0.0', 8000)

Viel mehr lässt sich aber glaube ich noch nicht sagen, denn erstens ist alles undokumentiert und offenbar steht das Schreiben von Kommentaren im Quelltext seit neustem unter Strafe und zweitens alles noch nicht fertig und sehr im Fluss. Daher spare ich mir meinen Wiki.

Er sähe wahrscheinlich wieder ähnlich aus, diesmal dann aber mit Django-Templates und Django-ORM (wenn ich denn einen ORM bräuchte). Statt urls.py-Datei gibt es den djng.Router und statt settings.py gibt es... das habe ich nicht so ganz durchschaut, aber genau hier will Willision besser werden und "Services" benutzen, die man automatisch zusammenstecken kann.

Resumeé

Die 4 Systeme (außer Djng) unterscheiden sich nur marginal. Alle kapseln WSGI-Anwendungen und stellen Request- und bis auf bei juno auch Response-Objekte zur Verfügung. Beim Routing setzen einige auf eine einfache Liste, andere auf Dekoratoren.

Spannender ist vielleicht, was sie nicht bieten -- und was ich mir eigentlich wünschen würde: Kein Rahmenwerk traut sich, yet-another-template-engine zu bauen, auch wenn sie alle yet-another-web-framework sind. Mein Beispiel zeigt jedoch, dass das auch in wenigen Zeilen nicht weiter schwer ist. Keiner kümmert sich um Formulare und deren einfache Verarbeitung. Die Konventionen von Rails, anhand von Namen wie page[author][][name] automatisch komplexe Datenstrukturen zu bauen, die dann zufällig genau kompatibel zum ORM sind, um daraus einfach Objekte zu erzeugen, finde ich gelungen. Das hin- und hergequote von URLs ist auch lästig und hier sollte es Abstraktionen geben. Um nochmals Rails zu erwähnen: Dort werden automatisch Hilfsfunktionen für URLs generiert. Unterstützung für REST wäre heutzutage auch hilfreich.

Nachtrag:

Resort

Größe unbekannt. Imports re und cgi. WSGI-basiert. Kein ORM, aber Unterstützung für das Erzeugen von HTML. Sessions sind integraler Bestandteil, Beaker ist nicht nötig. Größter Nachteil: Das Rahmenwerk existiert nicht. Resort ist ein Synonym für Seaside und meine Idee, wie etwas wie Seaside in Python aussehen könnte.

So sähe das Hallo-Welt-Beispiel aus:

class Hello(resort.Component):
    def __init__(self, name="World"):
        self.name = name
    
    def render(self, html):
        html.text("Hello, ", self.name)

if __name__ == '__main__':
    resort.route("/hello", lambda: Hello())
    resort.route("/hello/:name", lambda name: Hello(name))
    resort.run()

Eine Komponente kann sich mittels render darstellen. HTML erzeugte ich mit passenden Methoden des html-Objekts. Resort kennt eigentlich keine benannten URLs, sondern generiert alles automatisch. Einsprungpunkte müssen daher explizit definiert werden. Das lambda sorgt dafür, dass Komponenten erst dann erzeugt werden, wenn sie auch benötigt werden -- dafür dann aber auch jedes Mal wieder, wenn die URL aufgerufen wird. Das könnte auch eine dumme Idee sein.

Das Wiki-Beispiel benötigt keine Template, allerdings ein etwas anderes Wiki-Objekt, welches nicht einfach nur Strings sondern Page-Objekte zurückgibt, die wissen, aus welchem Wiki sie stammen und sich selbst speichern können. Die Index-Komponente bildet den Einsprungpunkt und verlinkt auf alle Seiten.

class Index(resort.Component):
    def __init__(self, wiki):
        self.wiki = wiki

    def render(self, html):
        html.heading("All Pages")
        with html.unorderd_list():
            for page in self.wiki.pages():
                html.list_item(html.anchor(call=html.bind(self.view, page), text=page))
    
    def view(self, name):
        self.goto(View(self.wiki.get_page(name)))

Über goto kann ich die aktuell angezeigte Komponente austauschen. Ein View stellt dann die übergebene Seite dar. Mit bind kann ich zu einer Funktion eine neue Funktion definieren, die die übergebenen Argumente als eigene Argumente bekommt. So definiere ich für jeden Seitennamen eine Funktion, die view passend aufruft. Diese Funktion wiederum wird vom Rahmenwerk aufgerufen, wenn der Benutzer auf einen Link klickt.

Die View-Komponente kennt schließlich noch eine Edit-Komponente, mit der man eine Seite bearbeiten kann und die sich wie ein Dialog über der Seite öffnet und das Formular mit dem Eingabefeld für den Text anzeigt. Quelltext.

Allen Code (und diesen Text) findet man übrigens hier: http://github.com/sma/microwebframeworks/tree/master.

Stefan

About

Article and example code about different micro web frameworks for Python

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages