Skip to content

Instantly share code, notes, and snippets.

@sma
Created May 3, 2009 17:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sma/106070 to your computer and use it in GitHub Desktop.
Save sma/106070 to your computer and use it in GitHub Desktop.
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)
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)
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(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())
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)
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))
class Text(object):
def __init__(self, text):
self.parent, self.text = None, text
def render(self):
return self.text
def find_by_web_id(self, web_id):
return None
# ------------------------------------------------------------------------------------
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
def apply(self, html):
self.apply_children(html)
def apply_children(self, html):
for child in self.children:
child.apply(html)
def element(self, html):
return self.parent.element(html).find_by_web_id(self.name)
class Label(Component):
def apply(self, html):
self.element(html).replace_content(self.model.object())
class Page(Component):
def render(self):
html = Html(self.html)
self.apply(html)
return html.render()
def element(self, html):
return html
# ------------------------------------------------------------------------------------
class Model(object):
def __init__(self, obj):
self.obj = obj
def object(self):
return self.obj
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)
# ------------------------------------------------------------------------------------
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()

Wicket in Python

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment