Erfahrungsbericht - CherryPy und Cheetah

CherryPy-Logo Cheetah-Logo

Warum CherryPy und Cheetah?

Applikations-Software

Ich interessierte mich für Alternativen zu Zope und Plone, denn oft will ich nur eine kleine Webanwendung, mit einem Menü, ein paar Seiten und evt. noch ein paar kleine dynamische Elemente wie ein Formular zum Verschicken einer Email.

Für so etwas ist Plone absolut überdimensioniert. Zope wäre zwar kein schlechter Kandidat für diesen Fall, aber ich kenne mich mit Zope schon aus und wollte es mal mit anderen Mitteln versuchen.

Zuerst hatte ich mich in das Tutorial von Django eingearbeitet, aber mich interessiert die Arbeit mit Datenbankabstraktionsebenen (Objektrelationale Mapper) nicht. Die verbergen mir zu viel von dem was eine gute Datenbank kann. Außerdem kann ich SQL ziemlich gut und habe gelernt, damit meine Probleme zu lösen. Ich frage mich wirklich, warum das Tutorial von Django sich so sehr mit Datenbanken beschäftigt. Dann kommt noch dazu, dass beim Erstellen einer Django-Anwendung gleich auch noch eine Administrations-Website erstellt wird, mit der ich auch Zugriff auf Datenbanken habe??? Da ich dadurch ein eher negatives Gefühl von Django bekommen habe, habe ich mich kurz noch mit Pylons befasst.

Beim Installieren von Pylons wurde mir fast übel als die Abhängigkeiten installiert wurden. Aber unabhängig davon, dass ich jetzt "gefühlte" zehn Python-Pakete mehr auf meinem Rechner habe, bin ich einfach nicht damit warm geworden.

Dann bin ich noch kurz über Colubrid gestolpert. Aber wie bei Pylons, fehlte mir das Gefühl dafür. Außerdem fehlten die für mich wichtige Session-Verwaltung und die Benutzer-Authentifizierung. Das kann man sicher mit irgendeinem WSGI-Tool hinzufügen, aber bis ich damit klar gekommen wäre, wäre noch viel Wasser den Inn herunter gelaufen. ;-)

Dann wollte ich Webware ausprobieren. Aber denen sollte mal jemand erklären, dass man ein Tutorial für Anfänger gut sichtbar auf der Homepage verlinken muss, wenn sich jemand dafür interessieren soll.

Ich werde mich sicher noch mit Webware beschäftigen, da mir einige Ideen davon wirklich gut gefallen. Die Servlets zum Beispiel. Ich stehe einfach drauf, kleine, abgeschlossene Bausteine zu haben, die man zu einem Ganzen zusammensetzen kann (wie bei Legotechnik). :-)

Da ich zum Zeitpunkt meiner Recherche keine interessante Einführung in Webware gefunden habe, habe ich mir auch noch CherryPy angesehen. -- Und bin dann vorerst mal dabei geblieben.

Vorlagensprache

Da ich schon lange mit Zope und Plone arbeite, bin ich mit TAL/METAL "per du". Mir gefällt die Integration in die HTML-Tags und die Wiederverwendung von Code mit METAL-Makros. Aber ich schaffte es nicht, diese Sprache anderen näher zu bringen. Dann gibt es noch etwas was mich bei TAL/METAL stört. Sobald man es aus Zope raus nimmt, fehlt die Acquisition, die das Arbeiten damit unter Zope so schön einfach macht. Außerhalb von Zope muss man die METAL-Makros, die man einbinden möchte, mühevoll von außen (per Python) laden, kompilieren und an die TAL-Vorlage übergeben.

Ich suchte also nach einer Alternative, die mir so etwas bietet. Also eine Vorlagencode-Wiederverwendung oder eine Vererbung von Vorlagencode, mit der ich eine Vorlage als Hauptvorlage definieren und in Verbindung mit Untervorlagen daraus die fertigen HTML-Seiten generieren kann. Und das natürlich mit weniger Aufwand, als es vorher mit TAL/METAL war. Und die Vorlagensprache soll auch optisch nicht wie ein Klammerngewitter aussehen.

Ich spare mir jetzt die Aufzählung der Vorlagensprachen, die ich mir angesehen habe. Entweder sie waren zu einfach gestrickt oder zu kompliziert oder es gab keine Vererbung oder auch keine andere (für mich akzeptable) Möglichkeit, eine Hauptvorlage mit Untervorlagen zu verknüpfen. Wie auch immer. Ich bin bei Cheetah gelandet und hoffe damit keinen so falschen Griff gemacht zu haben.

Installation

Ich arbeite derzeit leider die meiste Zeit unter Windows XP. Viel lieber wäre mir Linux, aber ich kann meine Kunden nicht ignorieren... Das ist der Grund dafür, dass ich meine Versuche unter Windows durchgeführt habe und mich öfter mal auf Windows-Begebenheiten beziehe. CherryPy läuft unter Linux genau so gut und lässt sich unter Linux genau so einfach wie unter Windows installieren.

Installation unter Ubuntu-Linux

sudo aptitude install python-cherrypy
sudo aptitude install python-cheetah

Installation unter WindowsXP

Den Pfad anpassen

Der Python-Ordner und der Scripts-Ordner müssen in den Windows-Pfad, damit man anständig arbeiten kann. Das ist jetzt nur meine persönliche Meinung, aber wer einige Schritte nachvollziehen möchte die ich hier demonstriere, sollte es tun.

  • Start --> Einstellungen --> Systemsteuerung
  • System --> Reiter: Erweitert --> Button: Umgebungsvariablen
  • Systemvariablen: Variable Path auswählen und auf Bearbeiten klicken
  • Im Fenster "Systemvariable bearbeiten": Im Feld "Wert der Variablen":
    • Ganz hinten, den Pfad zum Python-Ordner anfügen. Getrennt vom vorherigen Pfad mit einem ";". Z.B.: ";C:\Python24".
    • Ganz hinten, den Pfad zum Python-Scripts-Ordner anfügen. Getrennt vom vorherigen Pfad mit einem ";". Z.B.: ";C:\Python24\Scripts".

easy_install installieren

easy_install macht uns die Sache ein bischen einfacher. Fast schon so einfach, wie die Installation von CherryPy und Cheetah unter Ubuntu. Mit easy_install kannst du fast alle Programme, die im Python Cheese Shop aufgelistet sind mit nur einer kleinen Anweisung installieren.

easy_install wird unter http://peak.telecommunity.com/DevCenter/EasyInstall erklärt.

Die Kurzfassung: Lade dir ez_setup.py herunter und führe es in Idle oder in der DOS-Konsole aus. (Damit du siehst, ob etwas schief läuft.) ez_setup.py installiert easy_install in den Python-Scripts-Ordner. Und, da vorher dieser Ordner zum Pfad hinzu gefügt wurde, kann man easy_install.exe jetzt von überall aus starten.

Installieren

In einer DOS-Konsole (Start --> Ausführen --> CMD):

easy_install CherryPy
easy_install Cheetah

Leider wird von Cheetah im Python-Scripts-Ordner nur eine Datei ohne Endung angelegt. Windows weigert sich, Dateien ohne Endung auszuführen. Deshalb muss man im Nachhinein noch die Datei cheetah in cheetah.py umbenennen, wenn man vernünftig damit arbeiten möchte.

Bei mir wird in diesem Fall aus der Datei C:\Python24\Scripts\cheetah die Datei C:\Python24\Scripts\cheetah.py. Ich habe mein Windows so eingestellt, dass es mir alle Dateiendungen anzeigt. Für all jene, die Windows so eingestellt haben, dass man die bekannten Dateiendungen nicht sieht: Mein Beileid. ;-)

Ob Cheetah prinzipiell funktioniert, lässt sich in einer DOS-Konsole feststellen.

cheetah

Dieser Befehl sollte so etwas hier zum Vorschein bringen:

C:\Dokumente und Einstellungen\Gerold>cheetah
         __  ____________  __
         \ \/            \/ /
          \/    *   *     \/    CHEETAH 2.0rc8 Command-Line Tool
           \      |       /
            \  ==----==  /      by Tavis Rudd <tavis@damnsimple.com>
             \__________/       and Mike Orr <iron@mso.oz.net>

USAGE:
------
  cheetah compile [options] [FILES ...]     : Compile template definitions
  cheetah fill [options] [FILES ...]        : Fill template definitions
  cheetah help                              : Print this help message
  cheetah options                           : Print options help message
  cheetah test [options]                    : Run Cheetah's regression tests
                                            : (same as for unittest)
  cheetah version                           : Print Cheetah version number

You may abbreviate the command to the first letter; e.g., 'h' == 'help'.
If FILES is a single "-", read standard input and write standard output.
Run "cheetah options" for the list of valid options.

*** USAGE ERROR ***: not enough command-line arguments

C:\Dokumente und Einstellungen\Gerold>

Das bedeute, dass Cheetah schon mal so weit funktioniert, dass man damit arbeiten kann.

Namemapper-Binärdatei installieren

Wenn man Python 2.4 einsetzt, und möchte, dass Cheetah schneller arbeitet, dann sollte man noch eine Binärdatei herunterladen und in den Cheetah-Ordner legen. Dieser Hinweis dazu findet sich in der Download-Seite von Cheetah:

IMPORTANT NOTE TO WINDOWS USERS: If you are using windows and Python 2.4, you should either use one of the .exe releases or grab _namemapper.pyd from the 0.9.17rc1 release and install it wherever your system puts Cheetah/NameMapper.py. This is a compiled copy of the C version of NameMapper (provided by Kevin Dangoor). Using it results in dramatic performance benefits.

Was so viel heißt wie: Wenn du Python 2.4 einsetzt, dann muss du dir die Datei _namemapper.pyd herunterladen und in den Cheetah-Ordner legen. Bei meiner Installation von Cheetah mit easy_install, wurde Cheetah in den Ordner C:\Python24\Lib\site-packages\cheetah-2.0rc8-py2.4.egg\Cheetah installiert. Und genau dort muss auch die Datei _namemapper.pyd hinein kopiert werden.

Wenn du Python 2.5 verwendest, dann kannst du die vorher genannte _namemapper.pyd nicht verwenden. Du brauchst eine eigens für Python 2.5 kompilierte Datei. Diese habe ich unter diesem URL gefunden: http://www.nabble.com/Re%3A-namemapper.pyd-and-python-2.5-p7902075.html Auf der Seite ganz unten --> _namemapper.pyd (16K) Download Attachment.

Auch wenn es sich hier so liest, als ob es ein riesiger Aufwand wäre, aber wenn Python installiert und easy_install eingerichtet ist, dann beschränkt sich die Installation eigentlich auf zwei kleine Befehle in der Kommandozeilenkonsole. :-)

CherryPy "Hallo Welt"

Für das Beispiel habe ich mir einen eigenen Ordner erstellt. Wo dieser Ordner erstellt wird, ist prinzipiell total egal. Aus Erfahrung, verzichte ich aber auf Leerzeichen und Sonderzeichen im Pfad zu diesem Ordner.

Bei mir läuft der Test im Ordner J:\Ablage\cptest. Eventuell nenne ich diesen Ordner im weiteren Verlauf dieses Berichtes "Testordner".

In diesem Ordner erstelle ich die Datei cptest.py. Sie soll meine Programmdatei sein.

Und hier das erste Hallowelt-Programm:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import cherrypy


class Root(object):

    def index(self):
        return "Hallo Welt"
    index.exposed = True


def main():
    cherrypy.quickstart(Root())


if __name__ == "__main__":
    main()

So sieht die Ausgabe in der Konsole aus, wenn man dieses Programm startet:

J:\Ablage\cptest>cptest.py
[29/May/2007:20:16:49] HTTP Serving HTTP on http://0.0.0.0:8080/
CherryPy Checker:
The Application mounted at '' has an empty config.

Das Programm ist jetzt über den URL http://localhost:8080/ erreichbar und sollte im Browser den Text Hallo Welt anzeigen.

Wenn man jetzt im Programm etwas verändert und es neu speichert, dann startet sich das Programm in der Konsole automatisch neu. Das ist während der Entwicklungsphase ziemlich praktisch und kann später abgeschaltet werden.

Mit <Strg+C> in der Konsole, stoppt man das Programm.

Einstellungen

CherryPy erwartet Einstellungen entweder als Dictionary, als offenes Dateiobjekt oder als Dateiname. Ich habe mich für die eigenständige INI-Datei entschieden. Den Dateinamen übergebe ich als Text.

Dafür erstelle ich die Datei cptest.ini (noch ohne Inhalt) und erweitere das Programm:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self):
        return "Hallo Welt"
    index.exposed = True


def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)


if __name__ == "__main__":
    main()

Vorsicht! Das funktioniert nicht im Idle, da dieser __file__ falsch interpretiert.

Statische Dateien ausliefern

Nichts ist lästiger, als wenn das Programm von sich aus nicht sofort statische Dateien ausliefern kann. Damit meine ich z.B. Bilder oder Stylesheets. Damit wird das Testen erschwert und zweitens ist es einfach nur lästig.

Auch CherryPy ist so ein Kandidat. Aber zum Glück kann man das ziemlich schnell beheben. Man kann einen oder mehrere Ordner einstellen, in den man statische Dateien geben kann. Weiters kann man für jeden Ordner einstellen, welche Dateien (oder Dateiendungen) wie statische Dateien behandelt werden sollen.

Ich erstelle mir also im Testordner die Ordner images und css, wie ich es von anderen Projekten gewohnt bin.

Damit die Dateien ausgeliefert werden, die in diesen Ordnern liegen, schreibe ich folgendes in die INI-Datei:

[/]
tools.staticdir.root = "J:/Ablage/cptest"

[/css]
tools.staticdir.on = True
tools.staticdir.dir = "css"

[/images]
tools.staticdir.on = True
tools.staticdir.dir = "images"

Zum Testen hole ich mir das CherryPy-Logo und speichere es in den images-Ordner.

http://www.cherrypy.org/chrome/common/cplogo.jpg

Mal testen, ob das Bild von CherryPy korrekt ausgeliefert wird... :-) http://localhost:8080/images/cplogo.jpg

Erster Kontakt mit Cheetah

Wie man im HalloWelt-Beispiel sehen kann, wird von CherryPy Text ausgeliefert. Wenn man sich den Header der ausgelieferten Seite ansieht, dann erkennt man, dass die Seite automatisch als "text/html" gekennzeichnet wird.

Mit wget (http://de.wikipedia.org/wiki/Wget) kann man sich den Header ansehen:

J:\Dokumente und Einstellungen\Gerold>wget -S --spider http://localhost:8080/
--23:05:42--  http://localhost:8080/
           => `index.html'
Resolving localhost... 127.0.0.1
Connecting to localhost|127.0.0.1|:8080... connected.
HTTP request sent, awaiting response...
  HTTP/1.1 200 OK
  Date: Tue, 29 May 2007 21:05:42 GMT
  Content-Length: 488
  Content-Type: text/html
  Server: CherryPy/3.0.1
  Connection: Keep-Alive
Length: 488 [text/html]
200 OK

J:\Dokumente und Einstellungen\Gerold>

Andere Content-Typen auszuliefern ist auch kein Problem, aber darum kümmern wir uns später.

Cheetah ist eine Vorlagensprache mit der man Text generieren kann. Jede Art von Text. Für uns ist wichtig, dass man damit auch HTML generieren kann.

So könnte eine XHTML-Seite ohne dynamische Elemente aussehen:

<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Ein Titel</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  HTML-Seite ohne dynamische Elemente.

</body>
</html>

Und so sieht die gleiche Seite mit dynamischen Elementen mit Cheetah aus:

#attr title = "Ein anderer Titel"
#attr content = "HTML-Seite mit dynamischen Elementen."
<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>$title</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  $content

</body>
</html>

Oben habe ich mit #attr die Standardwerte für die zu ersetzenden Felder angegeben. Und im XHTML-Code selber habe ich $title und $content als Platzhalter eingesetzt. Wird diese Vorlage nur gerendert und angezeigt, ohne diese Platzhalter vom Programm aus zu ersetzen, dann werden die mit #attr definierten Standardwerte eingesetzt. Die Platzhalter $title und $content können aber auch vom Programm ersetzt werden.

Man kann eine Cheetah-Vorlage auch ohne CherryPy testen und erstellen. Dazu muss man in der Konsole in den Ordner wechseln, in dem auch die Cheetah-Vorlage zu finden ist.

Damit wird aus der Vorlage eine HTML-Seite erstellt:

cheetah fill <vorlagenname>

Ich speichere die Vorlage unter dem Namen "index.tmpl" ab, da das meine Vorlage für die Haupteite wird.

Und so wird die HTML-Seite index.html erstellt:

cheetah fill index.tmpl

Da die Felder $title und $content nicht ersetzt wurden, wurden dafür intern die Standardwerte aus den #attr-Zeilen verwendet.

Um $title und $content von der Kommandozeile aus zu ersetzen, muss man diese als Umgebungsvariablen festlegen:

set title=Ich bin der neue Titel
set content=Ich bin der neue Body
cheetah fill --env index.tmpl

Damit wurde die HTML-Seite neu erstellt und die Felder ersetzt. Das kann man verwenden, wenn man ein paar statische HTML-Seiten erstellen möchte. Z.B. zum Erstellen von Hilfeseiten für Programme.

Und das ist die soeben erstellte, neue HTML-Datei:

<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Ich bin der neue Titel</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  Ich bin der neue Body

</body>
</html>

Cheetah und CherryPy

Ich möchte dieses Spiel mit der Vorlage fortsetzen und diese heran ziehen, wenn der URL des Programms (ohne weitere Seitenangabe) aufgerufen wird. Also bei dem URL http://localhost:8080/. Dazu muss ich das Programm nur geringfügig ändern.

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self):
        filename = os.path.join(APPDIR, "index.tmpl")
        template = Template(file = filename)

        template.title = "Die Indexseite (CherryPy)"
        template.content = (
            "Dieser Text befindet sich auf der Indexseite.\n"
            "Dieser Text wurde von CherryPy ersetzt."
        )

        return str(template)
    index.exposed = True


def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)


if __name__ == "__main__":
    main()

Wenn man jetzt den URL http://localhost:8080/ aufruft, dann bekommt man das hier zu sehen:

<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Die Indexseite (CherryPy)</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  Dieser Text befindet sich auf der Indexseite.
Dieser Text wurde von CherryPy ersetzt.

</body>
</html>

Wie man sieht, wurden die Platzhalter $title und $content ersetzt.

Cheetah Vorlagenvererbung

Cheetah kann Teile einer Vorlage mit einer anderen Vorlage überschreiben. Das ist einer der Gründe, weshalb ich Cheetah ausgewählt habe. Damit kann man das Layout der gesamten Website in einer Vorlage festlegen. Die Inhalte der einzelnen Seiten kommen aus vielen kleinen Vorlagen. Da das Layout schon in der Hauptvorlage festgelegt wird, sehen die Vorlagen für die Inhalte klein und überschaubar aus.

Man kann mit der Anweisung #extends eine Hauptvorlage erweitern. In der Hauptvorlage werden Blöcke mit #block <name> und #end block <name> definiert. In der Seitenvorlage werden solche Blöcke ebenfalls definiert. Die Blöcke der Vorlage überschreiben die Blöcke der Hauptvorlage.

Dann lege ich einfach mal mit einem Beispiel los. Als Hauptvorlage erstelle ich die Datei "hauptvorlage.tmpl". Diese sieht bei mir so aus:

hauptvorlage.tmpl:

#attr title = "Hallo Welt"
<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Hallo Welt - $title</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  ## Header BEGIN
  <h1 align="center">$title</h1>
  <hr />
  ## Header END

  #block main
  <p>
    Dieser Text sollte im Normalfall von der importierenden
    Vorlage ersetzt werden. Wenn das nicht der Fall ist, dann
    wurde etwas vergessen.
  </p>
  #end block main

  ## Footer BEGIN
  <hr />
  <p align="center">
    <a href="http://validator.w3.org/check?uri=referer"><img
      src="http://www.w3.org/Icons/valid-xhtml10"
      border="0" alt="Valid XHTML 1.0 Transitional"
      height="31" width="88" /></a>
  </p>
  ## Footer END

</body>
</html>

Ich habe einen Standardwert für den Platzhalter $title festgelegt. Dieser Titel wird in der Vorlage zwei mal verwendet. Einmal im Title-Tag und einmal als Überschrift.

Das Neue in dieser Vorlage ist der Block mit dem Namen "main". Dieser ist in dieser Vorlage mit einem Text vorbelegt. Wird dieser nicht von einer anderen Vorlage überschrieben, dann bleibt er so drinnen wie er ist.

#block main
<p>
  Dieser Text sollte im Normalfall von der importierenden
  Vorlage ersetzt werden. Wenn das nicht der Fall ist, dann
  wurde etwas vergessen.
</p>
#end block main

Jetzt wirds spaßig. :-) Jetzt ändere ich die Vorlage "index.tmpl" so um, dass diese die Hauptvorlage erweitert.

index.tmpl:

#extends hauptvorlage
#attr title = "Die Indexseite"
#attr content = "Dummytext, der auf der Indexseite zu finden ist"

#block main
  <p>
    $content
  </p>
  <p>
    Dieser Text kommt von der Vorlage "index.tmpl". Dieser Text
    wird nicht von CherryPy ersetzt.
  </p>
#end block main

...nichts mehr von Seitenlayout zu sehen. Das wird alles von der Hauptvorlage geholt. #extends hauptvorlage kümmert sich darum, dass zuerst die Hauptvorlage geladen wird. Dann wird mit #attr title = "Die Indexseite" der Standardwert für den Platzhalter $title überschrieben. (Dieser wurde in der Hauptvorlage auch definiert.) $content wird ebenfalls vorbelegt und kann später von CherryPy ersetzt werden.

Und jetzt kommt das interessanteste. Der Block "main" wird auch komplett überschrieben.

Zuerst wird also alles aus der Hauptvorlage geholt. Und dann werden Teile der Hauptvorlage überschrieben. Genial einfach, nicht wahr? :-)

Kompilieren

Das funktioniert, weil man Cheetah-Vorlagen in Python-Module kompilieren (umwandeln) kann. Das muss man auch tun, denn Vorlagenvererbung funktioniert nur mit Python-Modulen. Das ist aber nicht so schlimm, denn man kann sich später im eigenen Programm selber darum kümmern und die Basisvorlagen automatisch kompilieren lassen, falls sich diese geändert haben oder noch nicht kompiliert wurden. (wenn man möchte) Man kann Cheetah ins Programm importieren und damit die py-Dateien erstellen.

Aber jetzt zurück zum normalen händischen Kompilieren. Mit folgendem Befehl auf der Kommandozeile, werden alle TMPL-Dateien des Ordners und aller Unterordner kompiliert:

cheetah compile -R

Das sieht bei mir so aus:

J:\Ablage\cptest>cheetah compile -R
Drilling down recursively from current directory.
Compiling hauptvorlage.tmpl -> hauptvorlage.py
Compiling index.tmpl -> index.py

J:\Ablage\cptest>

Aus "hauptvorlage.tmpl" wird "hauptvorlage.py". Richtige Python-Dateien, die man importieren und sogar ausführen kann. :-)

Zum Testen können wir die Vorlagen mit cheetah fill -R in HTML-Dateien umwandeln. Nach dem Ausführen dieses Befehls haben wir im Testordner jeweils zwei TMPL-Dateien, zwei PY-Dateien und zwei HTML-Dateien. Wenn sich irgendetwas an den Vorlagen ändert, dann muss man danach die Vorlagen neu kompilieren, damit Cheetah die neuen Python-Dateien importieren kann.

Hier sollte ich noch dazu sagen, dass nur die Vorlagen, die von anderen Vorlagen importiert werden, kompiliert werden müssen. Das wäre in unserem Fall nur die Datei "hauptvorlage.tmpl". Man muss also nicht befürchten, dass bei jeder kleinen Änderung alles neu kompiliert werden muss.

Es empfiehlt sich, eine CMD-Datei anzulegen, mit der die Vorlagen neu kompiliert werden. Das Füllen ist nicht notwendig, das habe ich nur zum Testen gemacht. Gefüllt werden die Vorlagen später wieder von CherryPy. Die HTML-Dateien kann man getrost wieder löschen. Wenn man nicht alle Vorlagen rekursiv (-R) kompilieren möchte, dann muss man den Befehl cheetah compile für die zu kompilierenden Vorlagen einzeln ausführen. Dabei sollte man aber auch darauf achten, dass man zuerst die Vorlagen kompiliert, die von anderen Vorlagen importiert werden.

vorlagen_kompilieren.cmd:

cheetah compile -R
pause

Oder unter Linux -> vorlagen_kompilieren.sh:

#!/bin/sh
cheetah compile -R
echo 'Enter zum Beenden.'
read

Wenn man jetzt http://localhost:8080/ aufruft, dann sollte das hier dabei raus kommen:

<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Hallo Welt - Die Indexseite (CherryPy)</title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-15" />
</head>
<body>

  <h1 align="center">Die Indexseite (CherryPy)</h1>
  <hr />

  <p>
    Dieser Text befindet sich auf der Indexseite.
Dieser Text wurde von CherryPy ersetzt.
  </p>
  <p>
    Dieser Text kommt von der Vorlage "index.tmpl". Dieser Text
    wird nicht von CherryPy ersetzt.
  </p>

  <hr />
  <p align="center">
    <a href="http://validator.w3.org/check?uri=referer"><img
      src="http://www.w3.org/Icons/valid-xhtml10"
      border="0" alt="Valid XHTML 1.0 Transitional"
      height="31" width="88" /></a>

  </p>

</body>
</html>

Was wird von CherryPy bei welchem URL ausgeliefert?

CherryPy kann nicht irgendeine zufällig ausgewählte Funktion heran ziehen, um eine Seite auszuliefern. CherryPy muss bescheid wissen, welche Funktion zu welchem URL gehört. Ändert man nichts an den Einstellungen, dann arbeitet CherryPy mit seinem eingebauten "Default Dispatcher", der sich darum kümmert, dass ein URL einer Funktion zugewiesen wird.

CherryPy arbeitet so, wenn man nichts an den Einstellungen ändert, dass man ein Objekt (eine Klasse) zum Root-Objekt (Wurzelobjekt) erklären kann. Der URL http://localhost:8080/ ruft dieses Wurzelobjekt auf. Wird kein Seitenname bzw. Funktionsname über den URL übergeben, dann wird die Methode "index", der Wurzelklasse aufgerufen. Das ist das Verhalten, wie es in allen Beispielen, die ich bis jetzt gezeigt habe, üblich war.

In den oben gezeigten Beispielen gibt es also eine Root-Klasse, die ich beim Starten der Anwendung übergeben habe. Hier die entsprechende Zeile:

cherrypy.quickstart(Root(), config = INI_FILENAME)

In dieser Root-Klasse gibt es die Methode "index", welche immer dann ausgeführt wird, wenn kein spezieller Funktionsname mit dem URL übergeben wurde. Dieser URL http://localhost:8080/ zeigt auf das Root-Objekt (/) der Anwendung.

Man kann aber auch einen Funktionsnamen über den URL übergeben. Z.B. zeigt der URL http://localhost:8080/myfunction auf die Methode myfunction, innerhalb der Root-Klasse. Falls es in der Root-Klasse eine Methode mit diesem Namen gibt, dann wird diese Methode aufgerufen.

Hier das veränderte Beispiel mit der neuen Methode:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self):
        filename = os.path.join(APPDIR, "index.tmpl")
        template = Template(file = filename)

        template.title = "Die Indexseite (CherryPy)"
        template.content = (
            "Dieser Text befindet sich auf der Indexseite.\n"
            "Dieser Text wurde von CherryPy ersetzt."
        )

        return str(template)
    index.exposed = True


    def myfunction(self):
        return "Es wurde die neue Funktion aufgerufen"
    myfunction.exposed = True


def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)


if __name__ == "__main__":
    main()

Ruft man jetzt den URL http://localhost:8080/myfunction auf, dann sollte man den Text "Es wurde die neue Funktion aufgerufen" zurück bekommen. Wie man sieht, ist die Zuweisung von URLs an Funktionen innerhalb des Root-Objekts ziemlich einfach. Jede neue Methode, die mit funktionsname.exposed = True für die Veröffentlichung gekennzeichnet wurde, ist über einen URL erreichbar.

Man kann aber auch mehrere Klassen für verschiedene Bereiche der Website erstellen -- so eine Art virtuelle Ordnerstruktur innerhalb des Programms.

Ich zeige es mal vor:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self):
        filename = os.path.join(APPDIR, "index.tmpl")
        template = Template(file = filename)
        template.title = "Die Indexseite (CherryPy)"
        template.content = "Dieser Text wurde von CherryPy ersetzt."
        return str(template)
    index.exposed = True


    def myfunction(self):
        return "Es wurde die neue Methode aufgerufen"
    myfunction.exposed = True


class MySubdir(object):

    def index(self):
        return 'Indexseite des "virtuellen" Ordners "mysubdir".'
    index.exposed = True


    def afunction(self):
        return "Es wurde die Methode 'afunction' der MySubdir-Klasse aufgerufen."
    afunction.exposed = True


# Anwendung zusammensetzen
root = Root()
root.mysubdir = MySubdir()


def main():
    cherrypy.quickstart(root, config = INI_FILENAME)


if __name__ == "__main__":
    main()

Das Programm enthält jetzt nicht mehr nur eine Klasse für die Wurzel, sondern auch eine Klasse für einen "virtuellen Ordner" mit dem Namen "mysubdir".

Die Klasse "MySubdir" unterscheidet sich vom Aufbau nicht von der Root-Klasse. Erst beim Zusammensetzen der Anwendung wird klar welche Aufgabe die MySubdir-Klasse hat. Sie ist für den URL http://localhost:8080/mysubdir/ zuständig.

Die Methode "afunction" wird mit dem URL http://localhost:8080/mysubdir/afunction erreicht.

Bilder und andere statische Dateien

Statische Dateien, die zu einem virtuellen Ordner gehören, können ohne weiteres in einen realen Ordner gelegt und für die Auslieferung markiert werden. Ich habe es gerne, wenn mich das Verhalten eines Programmes nicht überrascht. Deshalb nenne ich reale Ordner, die zu einem virtuellen Ordner gehören auch gleich.

Der virtuelle Ordner "mysubdir" bekommt jetzt einen realen Bruder im Dateisystem. Ich erstelle den neuen Ordner "mysubdir" ("J:\Ablage\cptest\mysubdir) und lege dort ein Bild hinein. Nehmen wir an, dass das Bild in einem globalen Bilderordner ("images") nichts zu suchen hat, da es nur in Verbindung mit diesem Ordner gebraucht wird.

Als Beispiel, lege ich das Python-Logo in den neuen Ordner.

Python-Logo

Wenn ich diesen Ordner in der INI-Datei einfach nur als "staticdir" ausweisen würde, dann würden alle Dateien, die sich darin befinden, ohne vorherige Prüfung, ausgeliefert werden. Das ist mir persönlich zu unsicher. Wie leicht kann es passieren, dass ich dort eine Python-Datei ablege die ein Passwort enthält.

Um auch wirklich nur Bilder von diesem Ordner ausliefern zu lassen, kann ich die Auslieferung mit einer "Regular Expression" auf Bilder einschränken. Der entsprechende Eintrag in der INI-Datei sieht bei mir so aus:

tools.staticdir.match = "(?i)^.+\.gif$|^.+\.jpg$"

Und da ich es für ungefährlich halte, wenn GIFs oder JPGs, die in den realen Ordnern liegen, ausgeliefert werden, habe ich den Root-Ordner (inklusive aller seiner Unterordner) entsprechend markiert.

Meine INI-Datei sieht jetzt so aus:

[/]
tools.staticdir.root = "J:/Ablage/cptest"
tools.staticdir.on = True
tools.staticdir.dir = "."
tools.staticdir.match = "(?i)^.+\.gif$|^.+\.jpg$|^.+\.png$"

[/css]
tools.staticdir.on = True
tools.staticdir.dir = "css"
tools.staticdir.match = "(?i)^.+\.css$"

[/images]
tools.staticdir.on = True
tools.staticdir.dir = "images"
tools.staticdir.match = "(?i)^.+\.gif$|^.+\.jpg$|^.+\.png$"

Vom css-Ordner werden jetzt nur noch CSS-Dateien ausgeliefert. Vom images-Ordner und von allen anderen Ordnern unterhalb von "J:/Ablage/cptest" werden Bilder ausgeliefert. Nur GIFs, JPGs oder PNGs kommen durch. Andere Dateien werden nicht ausgeliefert. Der Abschnitt [/images] ist jetzt eigentlich nicht mehr nötig. Ich lasse ihn nur drinnen, um auf den besonderen Status des images-Ordners hin zu weisen.

Parameter vom Client zum Server übermitteln (GET und POST)

Es gibt zwei gängige Methoden, mit mit denen man Daten über das HTTP-Protokoll vom Client (Browser) zum Server (CherryPy) übermitteln kann. Das sind GET und POST. Es gibt auch noch andere Methoden, aber die sind für uns nicht wichtig.

GET ist die Methode, Parameter per URL zu übermitteln. Ein typischer URL mit Parameter sieht z.B. so aus http://localhost:8080/funktionsname?parameter1=Hallo&parameter2=Welt.

POST übermittelt die Parameter im Hintergrund, im Header der Seitenanforderung, also nicht direkt sichtbar.

Man kann sich das so vorstellen: Der Browser erstellt sozusagen einen Text, gleich wie wir es machen, wenn wir eine Textdatei schreiben. In diesem Text steht in der ersten Zeile, welche Seite der Browser vom Server abrufen möchte. Dann steht noch die Methode (GET oder POST) und die Protokollversion (HTTP/1.0 oder HTTP/1.1) in dieser ersten Zeile.

Unter die erste Zeile kommen dann noch zusätzliche Daten, die der Browser an den Server übermitteln muss/kann/soll. Z.B. die Cookie-Daten oder die Kennung des Browsers.

Der Unterschied zwischen GET und POST ist der, dass GET die Parameter in der ersten Zeile, also als Teil des URLs übermittelt und POST die Parameter unterhalb der ersten Zeile als weiteren Teil des Headers.

Mit POST können normalerweise mehr Daten als mit GET an den Server geschickt werden, da nicht jeder Server einen sehr langen URL unterstützt. Siehe auch: http://tools.ietf.org/html/rfc2616#section-3.2.1

Mehr Informationen über HTTP findest du im Wikipedia.

Parameter per GET übergeben (über den URL)

Und hier gleich das Beispiel:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self, vorname = ""):
        if vorname:
            template = Template("Dein Vorname ist $vorname.")
            template.vorname = vorname
            return str(template)
        else:
            return \
                """
                Es wurde kein Vorname übergeben.
                <a href="/?vorname=Thomas">Mit Vorname
                sieht das so aus...</a>
                """
    index.exposed = True

def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)

if __name__ == "__main__":
    main()

Die Index-Methode hat eine zusätzliche Parameterdefinition (vorname = "") bekommen. Wir bekommen also den Vornamen als Parameter übergeben.

Wenn der Vorname übergeben wurde, dann wird ein kurzer Text angezeigt. Zum Üben habe ich hier auch wieder Cheetah verwendet. Die Vorlage besteht hier nur aus dem Text "Dein Vorname ist $vorname.". Der Vorname wird eine Zeile darunter als Attribut an die Vorlage übergeben und mit return str(template) wird die Vorlage gerendert und an den Browser übermittelt.

Wenn der Vorname nicht übergeben wurde, dann wird ein kurzer Text mit einem Link (=Anker-Tag) übermittelt. Wie man sieht, schreibe ich nicht den vollständigen URL http://localhost:8080/?vorname=Thomas hin, sondern lasse den URL mit einem Slash (/) beginnen. Das hat den Vorteil, dass unser Programm auch dann noch funktioniert, wenn wir damit auf einen anderen Server übersiedeln. Wenn http:// weg gelassen wird, dann ergänzt der Browser den URL automatisch mit der Root-Adresse der aktuell angezeigten Seite. Möchte man die aktuelle Seite noch einmal aufrufen, dann genügt es auch, wenn man einen Punkt "." statt des Pfades angibt. Das werde ich im POST-Beispiel demonstrieren.

Parameter per POST übergeben (über den HTTP-Header)

Dazu braucht man ein HTML-Formular. In diesem HTML-Formular kann man die Methode übergeben, die zum Übermitteln der Formulardaten vewendet werden soll.

Und hier wieder das Beispiel:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def index(self, vorname = ""):
        if vorname:
            template = Template("Dein Vorname ist $vorname.")
            template.vorname = vorname
            return str(template)
        else:
            return \
                """
                Es wurde kein Vorname übergeben.
                <form method="POST" action="">
                  <input type="text" name="vorname" value="Thomas">
                  <input type="submit" value="Submit">
                </form>
                """
    index.exposed = True

def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)

if __name__ == "__main__":
    main()

Der Unterschied zum vorherigen Beispiel liegt diesmal nicht auf der Serverseite, sondern auf der Clientseite, also im Code, der an den Browser übermittelt wird.

Zuerst kommt der Text "Es wurde kein Vorname übergeben.". Dann die Definition eines einfachen HTML-Formulares. Im FORM-Tag legt das Attribut "method" die Methode der Übermittlung fest. Das ACTION-Attribut gibt die Zieladresse an, an die die Formulardaten übermittelt werden. Das INPUT-Tag vom Typ "text" zeigt im Browser ein kleines Textfeld an. Das INPUT-Tag vom Typ "submit" stellt einen Button dar. Wenn man auf diesen Button klickt, dann werden die Daten des Formulars an die im ACTION-Attribut festgelegte Adresse geschickt.

CherryPy nimmt die Daten an und verarbeitet sie. Dabei wird intern kein Unterschied zwischen GET und POST gemacht. Möchte man aber trotzdem darauf reagieren, dann kann man die Übermittlungsmethode mit cherrypy.request.method abfragen.

Kleiner Tipp zwischendurch - Request Attribute anzeigen

Es ist oft wichtig, schnell herauszufinden, welche Daten uns CherryPy über das Request-Objekt zur Verfügung stellt.

def show_request():
    """
    Zeigt die Attributes des Requests als Text an.
    """

    cherrypy.response.headers["Content-Type"] = "text/plain"
    for key, value in cherrypy.request.__dict__.items():
        yield "%s: %s\n\n" % (key, repr(value).replace("\\n", "\n"))

Diese Funktion gibt die Repräsentationen der einzelnen Request-Attribute zurück. Wenn man mal weiß, welche Attribute es gibt, dann ist es einfacher damit umzugehen. Erklärungen zum Request-Objekt findet man hier: http://www.cherrypy.org/wiki/RequestObject

Cheetah Vorlagen von CherryPy automatisch ausliefern lassen

Jetzt wird es spannend. Ich möchte unser Testbeispiel so herrichten, dass Cheetah-Vorlagen, also Dateien mit der Endung ".tmpl" automatisch von CherryPy ausgeliefert werden, auch wenn diese nicht in CherryPy mit einer eigenen Funktion bedacht sind. Es soll genügen, eine Cheetah-Vorlage im Dateisystem zu erstellen.

Dafür, denke ich, eignet sich die "default"-Methode von CherryPy. Es ist nämlich so, dass vom "Default Dispatcher" zuerst versucht wird, eine zugewiesene Methode für einen URL zu finden. Findet er nichts, dann schaut er nach, ob es eine Methode mit dem Namen "default" gibt. Diese führt der "Default Dispatcher" dann aus.

Die an diese Methode übergebenen Argumente geben Auskunft über den aufgerufenen Pfad und die daran übergebenen, benannten Parameter. Jetzt muss ich nur noch prüfen, ob es eine TMPL-Datei am gewünschten Ort gibt und schon kann es los gehen.

Vorlagen ohne Caching ausliefern

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class Root(object):

    def default(self, *args, **kwargs):
        """
        Standard-Handler für Objekte, die keiner anderen Handler-Funktion
        zugewiesen werden konnten.

        TMPL-Dateien werden hier auch ohne zugewiesener Funktion abgearbeitet
        und ausgeliefert.
        """

        filename = os.path.join(APPDIR, *args)

        if os.path.isdir(filename):
            # Handelt es sich um die index.tmpl?
            filename = os.path.join(filename, "index.tmpl")
            if os.path.isfile(filename):
                # Fehlenden Slash anhängen
                request = cherrypy.request
                path_info = request.path_info
                if not path_info.endswith('/'):
                    new_url = cherrypy.url(path_info + '/', request.query_string)
                    raise cherrypy.HTTPRedirect(new_url)
            else:
                raise cherrypy.NotFound()
        else:
            # Benannte Vorlage
            if filename.lower().endswith(".tmpl"):
                if not os.path.isfile(filename):
                    raise cherrypy.NotFound()
            else:
                raise cherrypy.NotFound()

        # Vorlage rendern und zurück geben
        template = Template(file = filename)
        template.request = cherrypy.request
        template.response = cherrypy.response
        return str(template)

    default.exposed = True


def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)

if __name__ == "__main__":
    main()

Dieses Beispiel hat einen Nachteil. Die Vorlagen werden nicht gecached, also zwischengespeichert. Und es wird auch nicht vom in Cheetah eingebauten Caching-System profitiert. Aber dafür ist es nicht so kompliziert wie das nächste Beispiel mit Caching und Threads.

Aber zuerst erkläre ich mal, was im oben gezeigten Beispiel so alles passiert. Es gibt nur die Funktion "default". Diese wird immer dann ausgeführt, wenn keine andere Handler-Funktion für den URL zuständig ist.

Als *args wird eine Liste mit dem Pfad und von **kwargs werden die übergebenen Parameter an die Funktion übergeben. Aus args und dem Anwendungspfad setze ich einen möglichen Dateipfad zusammen.

Dann wird geprüft ob es sich beim Dateipfad um einen Ordner handelt. Wenn ja, dann wird nachgesehen, ob es im Ordner eine "index.tmpl"-Datei gibt. Wenn ja, dann wird noch geprüft, ob der URL mit einem Slash (/) aufhört. Das ist wichtig um mit relativen Pfaden versehene Bilder aus der HTML-Seite heraus anzeigen zu können. Endet der Pfad nicht auf einen Slash, dann wird mit cherrypy.HTTPRedirect zu einem URL mit Slash umgeleitet. Danach wird die Vorlage geladen, der Request und der Response angehängt -- vielleicht braucht man diese Objekte irgendwann mal direkt in der Vorlage. Danach wird die Vorlage gerendert und ausgeliefert

So ähnlich passiert es auch, wenn kein Ordner übergeben wurde, sondern ein Dateinamen, der auf ".tmpl" endet. Es fällt nur die Prüfung auf den Slash weg und da man den Dateinamen schon kennt, wird hier auch nicht lange gezögert. Die Vorlage wird geladen, der Request und der Response angehängt und ausgeliefert. Viel mehr passiert da nicht mehr.

Was haben wir damit geschaffen? So etwas wie wir es evt. schon von PHP her kennen. Wir müssen jetzt nur noch TMPL-Dateien, also Cheetah-Vorlagen in eine reale Ordnerstruktur legen. Diese werden ohne zusätzlichen Aufwand, direkt vom Programm erkannt, gerendert und ausgeliefert. Wir müssen jetzt nur mehr dann Hand anlegen, wenn etwas mehr Dynamik in den Vorlagen gefordert wird. Aber auch dann muss man an der "default"-Funktion nichts ändern. Wie mehr Dynamik in die Cheetah-Vorlagen kommt, erkläre ich etwas später. Aber mach dich schon darauf gefasst. Cheetah wird dich mit seinen Möglichkeiten überraschen. :-)

Vorlagen zwischenspeichern und nur bei Bedarf neu kompilieren lassen

Den Text dieses Kapitels habe ich ausgelagert. Denn das dort beschriebene Beispiel ist nur für Fortgeschrittene geeignet. Und oben gezeigter Code funktioniert wunderbar. Das Caching ist wirklich nur für sehr stark frequendierte Websites wichtig. --> ...zum ausgelagerten Kapitel

Abschließende Worte

Das war's für's Erste. Viel Spaß beim Nachmachen. Mehr Hilfe gibt es auf der CherryPy-Website und auf der Cheetah-Website.

Hier findest du den Erfahrungsbericht, wie man CherryPy mit mod_fastcgi in den Apachen einbindet. Als Beispielserver habe ich dafür Debian Etch verwendet.

Den Quellcode der Beispiele findest du ganz unten zum Herunterladen.

Stichworte

Template Web Internet WSGI

Attachments

Die Dateien wurden mit 7-Zip gepackt.