Kann man ein Webrahmenwerk, wie das für Java geschriebene Wicket auch in Python bauen? Dieser Frage möchte ich nachgehen. Hier ist ein einfaches Beispiel, welches eine Webseite anzeigt, auf der "Hello World" ausgegeben wird:
import web
class HelloWorld(web.Page):
def create(self):
self.add(web.Label("message", "Hello World"))
html = """<span web:id="message">Message goes here</span>"""
web.start(HelloWorld)
Eine Webseite definiere ich durch eine Unterklasse von web.Page
. In der Methode create
füge ich der Seite ein web.Label
namens message
hinzu. In dem HTML der Seite ersetzt dann das Label den mit web:id="message"
markierten Text durch den Wert seines Modells - in meinem Fall ein einfacher String. Mit web.start
lasse ich das ganze dann als Webanwendung laufen.
web.Page
und web.Label
erben von web.Component
, welches die Basisklasse für alle Komponenten ist. Komponenten bilden eine Hierarchie, in der parent
die übergeordnete Komponente bezeichnet und children
die untergeordneten Komponenten sind. Die Methode create
existiert, um in Unterklassen überschrieben zu werden:
class Component(object):
def __init__(self, name=None, model=None):
self.parent, self.children, self.name = None, [], name
self.model = model if isinstance(model, Model) else Model(model)
self.create()
def create(self):
pass
def add(self, child):
self.children.append(child); child.parent = self; return self
Um eine Webseite anzuzeigen, muss ich das HTML-Dokument anpassen. Dies geschieht in einer apply
-Methode, die ich rekursiv für jede Komponente beginnend mit einer Seite aufrufe.
class Component...
def apply(self, html):
self.apply_children(html)
def apply_children(self, html):
for child in self.children:
child.apply(html)
class Label(Component):
def apply(self, html):
self.element(html).replace_content(self.model.object())
Bei Page
beginnt es mit der Methode render
:
class Page(Component):
def render(self):
html = Html(self.html)
self.apply(html)
return html.render()
Mit Html
erzeuge ich aus dem String ein veränderbares Dokumenten-Objekt-Modell. Es versteht render
, um sich selbst wieder zu einem String zu machen. Die Aufgabe, den String zu parsen könnte ich einer fertigen Bibliothek überlassen, doch da ich im Hinterkopf habe, dass ganze später als Beispiel für einen eigenen Python-Interpreter zu benutzen, werde ich den Parser schnell selbst bauen:
import re
TAG = re.compile(r'<([-:\w]+)([^>]*?)(/?)>|</([-:\w]+)>|([^<]*)')
ATTR = re.compile(r'([-:\w]+)\s*=\s*"([^"]*)"')
def Html(s):
root = current = Root()
for m in TAG.finditer(s):
if m.group(1):
attrs = dict(n.groups() for n in ATTR.finditer(m.group(2)))
current = current.add(Element(m.group(1), attrs, bool(m.group(3))))
elif m.group(4):
if current.name != m.group(4): raise ValueError
current = current.parent
elif m.group(5):
current.add(Text(m.group(5)))
return root
class Root(object):
def __init__(self):
self.parent, self.children = None, []
def add(self, child):
self.children.append(child); child.parent = self; return child
def render(self):
return self.render_children()
def render_children(self):
return "".join(child.render() for child in self.children)
class Element(Root):
def __init__(self, name, attrs, single):
super(Element, self).__init__()
self.name, self.attrs, self.single = name, attrs, single
self.web_id = attrs.pop("web:id", None)
def render(self):
if self.single: return "<%s%s/>" % (self.name, self.render_attrs())
return "<%s%s>%s</%s>" % (
self.name, self.render_attrs(), self.render_children(), self.name)
def render_attrs(self):
return "".join(' %s="%s"' % item for item in self.attrs.items())
class Text(object):
def __init__(self, text):
self.parent, self.text = None, text
def render(self):
return self.text
Der Parser erfordert korrektes XHTML, erkennt nur Attribute mit "
und weiß nichts von Entities (was nicht so schwer nachzurüsten wäre), erzeugt aber ansonsten eine Hierarchie von Element
- und Text
-Objekten, die unter einem Root
-Objekt hängen.
Eine Komponente muss jetzt ein passendes Element finden. Die Namen der Komponenten müssen nur eindeutig innerhalb ihres Containers sein, daher ist die Suche etwas aufwendiger:
class Component...
def element(self, html):
return self.parent.element(html).find_by_web_id(self.name)
class Page...
def element(self, html):
return html
class Root...
def find_by_web_id(self, web_id):
return self.find_children_by_web_id(web_id)
def find_children_by_web_id(self, web_id):
for child in self.children:
element = child.find_by_web_id(web_id)
if element:
return element
return None
class Element...
def find_by_web_id(self, web_id):
if self.web_id == web_id:
return self
return self.find_children_by_web_id(web_id)
class Text...
def find_by_web_id(self, web_id):
return None
Wurde das passende Element gefunden, kann es modifiziert werden:
class Element...
def replace_content(self, obj):
html = self.html(obj)
for child in self.children:
child.parent = None
self.children = html.children
for child in self.children:
child.parent = self
def html(self, obj):
return obj if isinstance(obj, Root) else Html(str(obj))
Der Wert eines Label
wird als web.Model
abstrahiert, das eine Methode object
hat, die den Wert zurückgibt. Um nicht jedes Mal explizit ein Modell zu erzeugen, macht dies eine web.Component
implizit.
class Model(object):
def __init__(self, obj):
self.obj = obj
def object(self):
return self.obj
Modelle helfen, auch auf komplexe Daten zuzugreifen:
class PropertyModel(Model):
def __init__(self, obj, path):
self.obj, self.path = obj, path.split(".")
def object(self):
return reduce(lambda obj, name: getattr(obj, name), self.path, self.obj)
Der letzte Schritt ist, die Webseite als Teil einer WSGI-Anwendung darzustellen:
import threading
context = threading.local()
from wsgiref.simple_server import make_server
def start(page):
def app(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
if 'page' not in context.__dict__:
context.page = page()
return [context.page.render()]
make_server('', 8000, app).serve_forever()
Stefan