Con Five verso Zope 3
Note: Return to tutorial view.
Introduzione
Questo testo è una libera traduzione di "Walking through Five to Zope 3" che potete trovare all'URL http://plone.org/documentation/tutorial/five-zope3-walkthrough
Autori
Il documento originale è stato scritto da Jean Francois (Jeff) Roche insieme a Russ Ferriday. Nasce dalla collaborazione cominciata al Plone Multimedia Sprint nel settembre 2005, subito dopo la Plone Conference di Vienna. Jeff stava effettuando il porting del suo famoso prodotto ZphotoSlides, implementando ATPhoto e ATPhotoAlbum per Plone. Russ ha dato una mano, ma Jeff ha fatto il grosso del lavoro, con l'aiuto di Gawel e di altri, ottenendo un gran risultato col pacchetto di ATPhoto. Ora stiamo lavorando insieme nuovamente per inglobare le caratteristiche del prodotto in ATContentTypes (ATCT) utilizzando Five, in attesa del possibile salto a Zope 3. Possiamo offrire una utile prospettiva perché Jeff ha sviluppato una certa esperienza sulle interfacce di Five, mentre Russ era un novizio all'inizio di questo lavoro comune. Ci auguriamo di poter condividere quel che abbiamo imparato con voi.
Presupposti
Avere qualche esperienza nello sviluppo di prodotti Plone.
Conoscere i principi della programmazione orientata agli oggetti.
Avere una conoscenza di base di UML.
Essere interessati ed avere una conoscenza di base dell'architettura a componenti di Zope 3.
I nostri obiettivi
Sempre più gente parla dell'architettura a componenti di Zope 3. Attualmente siamo in un momento di transizione in cui la gente deve mescolare concetti di Zope 2 e Zope 3. Non è facile cambiare il vostro modo di pensare andando verso i componenti di Zope 3, poiché questo si porta dietro molti (interessanti, realmente belli ed utili) nuovi concetti. Non c'è molta documentazione sull'uso dell'architettura a componenti di Zope 3 in Zope 2: proveremo a dare il nostro contributo nella speranza di aiutarvi a progredire verso Zope 3. Se quando avrete letto questo documento penserete che Zope 3 offra un modo piacevole di codificare/pensare allora avremo raggiunto il nostro obiettivo.
Il modello che implementeremo
Abbiamo lavorato all'implementazione mediante Five della funzionalità di archiviazione di ATPhoto, che permette di creare un archivio zip partendo da un albero di oggetti. Avevamo completato e testato un modulo di Archiviazione generico. E lo avevamo persino inglobato nel prodotto ATContentTypes. Ma, mentre documentavamo quel che avevamo fatto, ci siamo resi conto che avevamo mancato un'occasione per scomporre il nostro modello in componenti più utili e riutilizzabili. Così abbiamo ripreso in mano il nostro lavoro ed abbiamo deciso di isolare un meccanismo generico di ricorsione che potesse essere usato per il traversing e la manipolazione di un'ampia varietà di oggetti o di alberi di oggetti. Ciò ha fatto ritardare la nostra attività iniziale, ma pensiamo che, dati il valore educativo e la riusabilità del prodotto finale, ne sia valsa la pena. Questo tutorial descrive quel che abbiamo fatto. Oltre il valore immediato, potreste trovarlo un utile strumento per i vostri progetti. Sarà parte di ATContentTypes in Plone 2.5.
La separazione delle funzioni, la testabilità e la semplicità di manutenzione sono fattori importanti in termini di modellazione del software. Ma volevamo anche che la gente potesse usare il nostro lavoro: ciò implica essere in grado di comprenderne la logica! Così abbiamo realizzato tre classi semplici da definire che implementano il nostro modello:
un Operator che sa come operare sui nodi di un albero;
un Filtro che sa come filtrare gli elementi di un albero, in modo da prendere in considerazione solo gli elementi che ci interessano;
un TreeWalker, in grado di visitare i nodi di un albero, dati un punto di partenza, un'Operator e, se necessario, un Filtro.
Il comportamento del TreeWalker potrebbe non avere mai bisogno di essere cambiato, mentre l'idea dell'Operator e del Filtro è che saranno sostituiti da nuove classi, per realizzare nuove esigenze. Introduciamo ora il concetto di Interfaccia. Le interfacce sono parte del Python. Ma nel “normale” Python non hanno la stessa potenza fornita nella loro implementazione Java, C++, Delfi, etc.. Zope3 e Five hanno cambiato tutto con il pacchetto delle interfacce, che aggiunge il rigore alla loro definizione e al loro uso, rendendo funzionante il concetto di adattatore. Abbiamo dovuto definire le interfacce per il Filtro e l'Operator, come vedrete successivamente. Sebbene se non ne avessimo bisogno, abbiamo anche definito un'interfaccia per TreeWalker per imporre maggior rigore ora e maggiore flessibilità successivamente.
Nel diagramma UML riportato di seguito, puoi vedere le seguenti interfacce:
ItreeWalker
IOperator
IFilter
Esse definiscono i contratti di interfaccia per le classi che li implementano.
ITreeWalker è implementato in TreeWalker. E questo è lo stesso TreeWalker con cui farete la vostra operazione ricorsiva.
IOperator è fornito soltanto come interfaccia. Ne realizzerete un'implementazione per fare la vostra operazione. Ad esempio, abbiamo indicato l'ArchivingOperator realizzato per l'Archiver per cui abbiamo fatto il TreeWalker. Potete fornire qualsiasi operator vi sia utile in sostituzione di ArchivingOperator. IFilterFolder è un'interfaccia che potete re-implementare a piacere, se volete processare soltanto determinati tipi di oggetto mentre attraversate l'albero. Vi forniamo un'implementazione di default denominata FolderFilter che potrebbe essere tutto ciò di cui avrete bisogno. FolderFilter prende in considerazione tutte le cartelle ed elementi in quelle cartelle. Parleremo in seguito delle operazioni di filtraggio.
L' UML fornisce una descrizione molto generale di quel che abbiamo fatto. La sezione “Usare il Treewalker” ti mostra come usarlo, e successivamente ne approfondiremo l'implementazione.
Test, test e ancora test
Unit Testing
Lo sviluppo Test Driven, orientato ai test, ha dimostrato di essere una metodologia molto potente per rendere gli sviluppatori produttivi. Per questo motivo, scrivere test per ogni nuova funzionalità, per ogni modifica o correzione bug, da tempo viene fatto dalla maggior parte della Comunità di sviluppatori Zope/Plone: così tanto che se presentate del codice e desiderate che venga rilasciato, questo non accadrà senza i corrispondenti test!
Scrivere test richiede tempo, e devi prenderti il tempo di scrivere test. Una volta che li avrai scritti, quel tempo sarà ricompensato molte volte. Test ben progettati proveranno che le tue modifiche, correzioni di bug e rifattorizzazioni non hanno generato difetti nel tuo codice e ti eviteranno di perdere tempo a cercarli.
Il testing è magico, trasforma uno sviluppatore in un utente. Quando scrivi i tuoi test ti metti al posto dell'utente e cominci a immaginare come potrebbe usare il tuo codice. Questo può mostrarti quanto sia valido il tuo codice: dove sono le parti oscure, i metodi di difficile comprensione, le decomposizioni errate delle classi, etc.
Sai che uno dei punti di forza più grandi di Plone è la sua comunità. I test sono ancor più importanti per il codice condiviso perché:
- Desiderate condividere con altri le vostre funzionalità, il vostro modo di programmare, il vostro modo di pensare e mostrare il giusto modo di usare queste funzionalità.
- Molte persone diverse possono/potrebbero contribuire al vostro codice, se sono stati scritti molti test possono assicurarsi da soli che il codice che scrivono non rovina il vostro lavoro.
- Sono la tua garanzia quando il mantainer, o il responsabile del rilascio viene a brontolarti che hai rotto qualcosa con le tue ultime modifiche.
Quindi tutto questo serve alla verifica del tuo codice e a spiegare come funziona agli altri.
I test dovrebbero:
essere ripetibili;
funzionare senza il bisogno di intervento umano;
essere concisi;
raccontare una storia;
non verificare cose ovvie;
essere deterministici.
PloneTestCase
Quando crei test per Plone, non vuoi perdere tempo ad installare un portale Plone, o qualsiasi altro prodotto Zope/Plone. Perciò per ottenere risultati dei test più rapidi vengono usati framework per gli Unit Test capaci di creare suite di unit test automatizzati. Plone ha il suo: PloneTestCase.
Poichè Plone è basato su Zope, PloneTestCase è basato su ZopeTestCase (il framework per unit test di Zope - che è basata sul pacchetto di unit test del Python [e se desiderate conoscere tutta la storia, il pacchetto unit test del Python è basato su JUnit del Java e sul testing framework di Smalltalk). Questo framework è un aiuto enorme per far girare i vostri test velocemente, spesso e con risultati chiari.
Vocabolario
Test significa metodo di test
Unit test indica una classe che contiene tutti i metodi di test (se volete usare il test framework di Plone, questa classe dovrebbe ereditare da PloneTestCase).
Unit test suite indica una collezione di più unit test.
Descriveremo qui alcune delle funzionalità di base disponibili in PloneTestCase e che useremo molto nelle sezioni seguenti.
Prodotti installati
Questa è la lista dei prodotti di default installati. Se avete bisogno di prodotti supplementari dovrete installarli esplicitamente con il metodo che descriveremo successivamente.
Zope
ZCTextIndex
MailHost
PageTemplates
PythonScripts
ExternalMethod
GroupUserFolder
Five
CMF
CMFCore
CMFDefault
CMFCalendar
CMFTopic
DCWorkflow
CMFUid
CMFActionIcons
CMFQuickInstallerTool
CMFFormController
Plone - Archetypes
Archetypes
MimetypesRegistry
PortalTransfroms
ATContentTypes
ATReferenceBrowserWidget
CMFDynamicViewFTI
ExternalEditor
ExtendedPathIndex
ResourceRegistries
SecureMailHost
Kupu
- CMFPlone.
Con Plone 2.5 vengono installati anche altri prodotti importanti come CMFPlacefulWorkflow, PlonePAS…
Oggetti installati
Di seguito c'è una lista di oggetti che puoi usare quando installi PloneTestCare
self.portal : è un portale Plone pulito che si installa con tutti i portal_tool di cui avrete bisogno.
self.folder : quando attivi un PloneTestCase sei loggato in Plone come utente di default. Questa cartella (vuota) è la folder di default dell'utente. Poiché è importante che tu possa fare tutto quello che serve in questa folder, l'utente di default è il proprietario di questa cartella.
Metodi utili che puoi usare:
Questi sono i metodi che potete usare sull'oggetto self (all'interno dell'istanza di PloneTestCase):
addProduct(name): Usa quickinstaller per installare un prodotto all'interno del portale Plone (self.portal). Così se hai definito il tuo prodotto e il tuo Content Type, non dimenticarti di installarlo nel portale Plone prima di provare ad usarlo.
setRoles(roles, name=default_user) : Cambia i ruoli dell'utente corrente (i ruoli possono essere una stringa, un tuple o una lista). Veramente importante per controllare problemi di sicurezza. Potete anche cambiare i ruoli di altri utenti impostando il parametro name.
setGroup(groups, name=default_user) : Cambia i gruppi dell'utente corrente (i gruppi possono essere una stringa, un tuple o una lista). Potete anche cambiare i gruppi per altri utenti utilizzando il parametro name.
setPermissions(permissions, role) : Cambia i permessi sull'oggetto portale per il ruolo specificato. I permessi possono essere una stringa, un tuple o una lista. Il ruolo deve essere una stringa.
login(name) : A volte è più chiaro generare nuovi utenti con differenti ruoli o gruppi e dopo effettuare il login con questi utenti.
logout() : vuoi essere anonimo all'interno di Plone? Usa questo metodo.
Setup degli unit test
Il framework di testing manda in esecuzione tutti i metodi all'interno di qualunque classe se il nome del metodo comincia con la stringa test. Perciò testMethod1 (self) sarà automaticamente eseguito dal framework e non ti dovrai più preoccupare di invocarlo esplicitamente da qualche parte.
Potreste spesso voler ripetere la stessa fase di inizializzazione prima della chiamata al vostro metodo di test. Il framework ti offre un metodo efficace per farlo:
afterSetUp(self) : Dovresti mettere in questo metodo tutto il codice che vuoi mandare in esecuzione prima di eseguire ciascuno dei tuoi metodi di test.
Metodi di test di asserzione (basati su unittest del Python)
Nei test c'è un input conosciuto e un'uscita prevista. La correttezza di questa corrispondenza di ingresso e uscita è controllata da una asserzione. Il pacchetto di unit test del Python offre vari metodi per testare asserzioni:
failIf(expression) : il test fallisce se l' espressione è vera
failUnless(expression) : il test fallisce se l' espressione non è vera
failUnlessEqual(first, second) : Il test fallisce se i due oggetti non sono uguali secondo l' operatore
==.- failIfEqual(first, second) : il test fallisce se i due oggetti sono uguali secondo l'operatore
==. fail(msg) : Il test fallisce immediatamente, restituendo il messaggio specificato
failUnlessRaises(excClass, callableObj, args, *kwargs) : Il test fallisce a meno che un'eccezione di classe excClass non sia restituita da callableObj quando invocato con gli argomenti args e kwargs.
Fallimenti ed errori
Fallimenti ed errori sono due cose diverse!
Un fallimento accade quando un'asserzione fallisce (ti aspettavi il risultato opposto dall'asserzione del test).
Un errore accade quando succede qualcosa che non ti aspetti (eccezioni, errori nel codice…).
Creiamo un test! Si capisce meglio con un po' di pratica. Vedrai che è facile.
Il metodo usuale consiste nel creare una classe di test per ogni classe da testare ed un metodo (a volte di più) di test per ogni metodo importante nella vostra classe (i metodi getter e setter sono spesso esclusi dal test data la loro ovvietà).
In primo luogo scarica il PloneTestCase (http://plone.org/products/plonetestcase o da svn https://svn.plone.org/svn/collective/PloneTestCase/trunk/) ed estrailo nella cartella dei prodotti di Zope (supponiamo che avete installato Plone là dentro ;)).
Prendiamo la classe PloneTestCase con tutte le cose di cui abbiamo bisogno:
>>> from Products.PloneTestCase import PloneTestCase
Diciamo di voler testare parte del comportamento della classe Document di Plone (ATDocument). Generiamo una classe che usa questo PloneTestCase che abbiamo appena importato:
class TestATDocument(PloneTestCase):
"""
Un test case di base per Plone Document
"""
pass Questo è il nostro primo test di Plone. Facile no? Sono d'accordo che non testa molto :).
Testiamo quindi un paio di cose:
Quando modifico il titolo del mio documento, voglio che sia modificato correttamente (è vero che stiamo verificando funzionalità ovvioe, ma facciamo cose semplici).
Quando creo un documento, voglio che sia aggiunto all'interno del catalogo di Plone.
Come vedi in questi due test avremo bisogno di creare un documento, facciamolo una volta sola nel metodo di afterSetUp in modo che il nostro documento sia generato prima di ogni test.
Ricorda che il nome ogni metodo di test deve cominciare con la string test.
Generiamo il file testDocument.py:
class TestATDocument(PloneTestCase):
"""
Un test case meno basico per Plone Document
"""
def afterSetUp(self):
"""
Generiamo nella nostra cartella home il documento di cui abbiamo bisogno
"""
self.folder.invokeFactory('Document', id='doc')
# Ora abbiamo un documento con id "doc" dentro alla nostra cartella home
def testEditTitle(self):
"""
Vediamo se la modifica del titolo del documento va a buon fine
"""
self.folder.doc.setTitle('A wonderful document title')
self.assertEqual(self.folder.doc.Title, 'A wonderful document title')
# questo fallirà se il metodo setTitle non lavora correttamente!
def testDocumentInCatalog(self):
"""
Vediamo se il documento è presente nel catalogo
"""
# il catalogo è nel portale Plone
self.failUnless(self.portal.portal_catalog(getId='doc')) Eccolo qui. Se il test passa possiamo essere certi di poter cambiare il titolo di un documento correttamente e che una volta creato un documento è nel catalogo di Plone.
Ora è venuto il momento di includere il nostro nuovo testcase all'interno di un testsuite, e di eseguire i nostri test.
Per potere fare questo avrete bisogno di due file:
framework.py : Per poter lanciare il test dal Python avrai bisogno di impostare alcune PATH, questo file farà la maggior parte del lavoro per voi.
runalltests.py : Questo piccolo script Python si limita ad eseguire tutti i file nella directory corrente che iniziano con la parola “test”.
Copia questi file dalla cartella di PloneTestCase alla cartella in cui sono tutti i tuoi test case (spesso nella cartella “tests”).
Quindi, per eseguire la suite di test, dovremo decorare il nostro PloneTestCase. Per aggiungere un po' di difficoltà, voglio inoltre installare un prodotto all'interno del mio portale che non è presente tra quelli dichiarati. Diciamo che desidero utilizzare il Plone Language Tool (ammetto che non ne avremo bisogno per l'esecuzione della nostra prova):
# Prima, sopra a tutto, mandiamo in esecuzione il framework.py
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
# Installiamo il prodotto PloneLanguageTool in Zope
from Testing import ZopeTestCase
ZopeTestCase.installProduct('PloneLanguageTool')
# Inizializziamo il nostro Plone e installiamo di default il prodotto PloneLanguageToolal suo interno
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite(products=['PloneLanguageTool'])
# Ecco fatto, tutto installato. Possiamo mettere qui il nostro testcase...
class TestATDocument(PloneTestCase.PloneTestCase):
"""
Un test case meno semplice per Plone Document
"""
def afterSetUp(self):
"""
Generiamo nella nostra cartella home il documento di cui abbiamo bisogno
"""
self.folder.invokeFactory('Document', id='doc')
# Ora abbiamo un documento con id "doc" dentro alla nostra cartella home
def testEditTitle(self):
"""
Vediamo se la modifica del titolo del documento va a buon fine
"""
self.folder.doc.setTitle('A wonderful document title')
self.assertEqual(self.folder.doc.Title(), 'A wonderful document title')
# questo fallirà se il metodo setTitle non lavora correttamente!
def testDocumentInCatalog(self):
"""
Vediamo se il documento è presente nel catalogo
"""
# il catalogo è nel portale Plone
self.failUnless(self.portal.portal_catalog(getId='doc'))
# Ora dobbiamo mettere il nostro testcase in una test suite.
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestATDocument))
return suite
# e se vuoi essere in grado di eseguire la tua suite direttamente (python testDocument.py)
if __name__ == '__main__':
framework() Ora tutto è predisposto. L'ultima cosa da fare è dire dove è posizionato il vostro Zope nel sistema. Su sistemi Unix potete fare così:
export SOFTWARE_HOME=/usr/lib/zope2.9/lib/python
Ora avete due modi per eseguire il vostro test suite:
"python runalltests.py" : che cerca tutti i file con il nome che comincia con “test” ed esegue tutte le test suite definite.
“python testDocument.py" : che esegue la specifica test suite.
Mentre in esecuzione vedrai
Installazione dei prodotti Zope
Una volta eseguito, un singolo test (metodo) sarà rappresentato da:
"." : che significa che il test è stato eseguito correttamente
"F" : che indica che il tuo test è fallito (otterrete maggiori informazioni alla fine)
"E" : che significa che il tuo test ha un errore (otterrete maggiori informazioni alla fine)
Ogni volta che un test fallisce o ha un errore otterrete un traceback e informazioni più dettagliate sul fallimento/errore.
DocTest
Cosa vedi nella parola DocTest? Doc e Test. Quindi un doctest è documentazione e allo stesso tempo un test che verifica che il tuo codice funziona.
Molti sostengono che la gente dovrebbe leggere gli unit test, che dovrebbero essere abbastanza chiari da non dovere aggiungere ulteriori commenti. È corretto che i test dovrebbero essere chiari ma non sarei troppo rigido. Penso che spiegando il mio codice a più persone possibile otterrò un maggior numero di feedback.
Anche se consideriamo i test case come documentazione per lo sviluppatore, i DocTest sono considerati una via di mezzo fra documentazione e test case. Mai più documentazione sorpassata ed inutile! Doctest permette di rendere viva la documentazione, sempre al passo con l'implementazione corrente.
Un doctest è un file di testo, o di testo strutturato (che dovrebbe essere scritto all'interno della cartella “docs” dei tuoi pacchetti/prodotti). Quindi all'interno di questo file spiegherai il tuo codice ed allo stesso tempo potrai invocare codice Python. Per invocare codice Python basta scrivere:
>>>
Ciò rappresenta una chiamata all'interprete Python. Intorno ad esso potete disporre la vostra spiegazione. Se il vostro codice Python restituisce qualcosa, dovete fare esattamente lo stesso come se aveste denominato questo codice da una sessione dell' interprete del Python. Per esempio:
>>> print 'hello world' hello world
Il valore di ritorno deve essere scritto allo stesso livello di indentazione del >>>
Un problema è che un doctest rappresenta in sé più di un test. Vuoi mostrare molte cose all'interno del tuo doctest, ma il metodo afterSetUp viene eseguito soltanto una volta prima dell'esecuzione del doctest. Un doctest rappresenta una sessione Python. Così se scrivo:
>>> a = 'hello'
La mia variabile a sarà impostata a “hello” fino alla fine del documento. Non dimenticarlo mai, potrebbe comportare grossi problemi!
A proposito, tutto questo documento è un doctest per i prodotti di ATContentTypes. Può essere eseguito in quel contesto.
Vocabolario
Un “file” doctest rappresenterà un file di testo che include il nostro doctest.
Una “classe” doctest rappresenterà la classe unit test che definisce un doctest.
Una volta scritto, il file doctest dovrebbe essere collegato ad una classe test case e ad una test suite. Perciò vediamo come si prepara un doctest nella parte del test (questo dovrebbe andare all'interno di un file Python nella cartella dei test - con un nome file che comincia con test):
# Come prima usiamo il framework.py
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
# Installiamo plone come solito. Vogliamo testare cose attinenti a plone
# nel nostro doctest
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite()
# quindi abbiamo bisogno della doctestsuite di zope e di linkare il nostro file doctest
# con un test case funzionale
from Testing.ZopeTestCase import FunctionalDocFileSuite
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
# abbiamo un file doctest chiamato archive.txt che è posizionato in
# ATContentTypes nella cartella docs (lo ripeto, il file doctest
# dovrebbe sempre trovarsi nella cartella docs, non nella cartella dei test).
def test_suite():
import unittest
suite = unittest.TestSuite()
suite.addTest(FunctionalDocFileSuite('archive.txt',
package="Products.ATContentTTypes.docs",
test_class=FunctionalTestCase
)
)
if __name__ == '__main__':
framework() Come al solito puoi mandare in esecuzione direttamente questo file, o semplicemente eseguire python runalltests.py.
Ora immagina di voler predisporre alcune cose all'interno della tua classe testcase prima di eseguire il file doctest (archive.txt). È facile, genera la tua classe di test ereditando da FunctionalTestCase, definisci il metodo afterSetUp e modifica la test suite in modo da usare la tua classe. Facciamolo…
Fate attenzione, questo potrebbe confondere la gente che legge il vostro file doctest se non spiegate chiaramente che già avete generato i test nella classe doctest.
Manteniamolo semplice. Immaginiamo di dover realmente generare un documento all'interno della cartella home, ma non abbiamo bisogno di mostrarlo nel file doctest:
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite()
# ora ereditiamo FunctionalTestCase e definiamo il nostro metodo afterSetUp
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
class TestArchiveWithDocument(FunctionalTestCase):
"""
La nostra classe di test funzionale con un documento al suo interno
"""
def afterSetUp(self):
"""
Creazione di un documento nella cartella home che il doc test
archive possa usare
"""
self.folder.invokeFactory('Document', id='doc')
# Questo è tutto! Nessun metodo di test, il nostro unico metodo di test sarà
# il nostro file doctest.
# ora abbiamo bisogno di collegare la nostra classe di test funzionale
# al nostro file doctest all'interno di una test suite:
from Testing.ZopeTestCase import FunctionalDocFileSuite
def test_suite():
import unittest
suite = unittest.TestSuite()
suite.addTest(FunctionalDocFileSuite('archive.txt',
package="Products.ATContentTTypes.docs",
test_class=TestArchiveWithDocument
)
)
if __name__ == '__main__':
framework() Ora conoscete tutto sui test, non avrete più giustificazioni se non ne scrivete!
Usare TreeWalker
Partiamo
Per usare il TreeWalker nel tuo codice non devi fare molto! Questo è il punto. Sì, ci sono importazioni da maneggiare, come di consueto, ma a parte questo dovrai soltanto definire un Operatore e sarai operativo!
Per prima cosa guardiamo la cosa più semplice che tu possa fare, e partiamo da là. Visiteremo semplicemente un albero e non faremo altro. Per il nostro test abbiamo definito un NullOperator che non fa altro che soddisfare l'interfaccia di IOperator. Possiamo usare questa classe per cominciare.
Per caso, i passaggi che sto per usare come esempio sono presi da un file doctest chiamato treeWalker.txt presente nella cartella docs del prodotto ATContentTypes. Ecco perché vedrai il >>> a sinistra di ogni linea (se non sai cosa sono i doctests non dovresti preoccuparti per questa sintassi - immagina solo che il >>> è un prompt per una sessione Python interattiva seguito da comandi che potresti usare nella tua shell Python preferita, oppure torna alla sezione precedente per la storia completa). Per eseguire questo doctest dovresti usare il photoimagemerge branch con Archetypes 1.4 branch ed il CMFDynamicFTI trunk. Sei inoltre invitato a dare un'occhiata al file treeWalker.txt stesso. Il sorgente di questo tutorial di Plone.org è disponibile sotto forma di singolo file di testo strutturato, denominato treeOperations.txt, che si trova nella cartella docs ed è inoltre testato nella nostra test suite.
OK, andiamo avanti con il tutorial…
In primo luogo importiamo TreeWalker e NullOperator:
>>> from Products.ATContentTypes.adapters.treeWalker import TreeWalker
>>> from Products.ATContentTypes.adapters.operator import NullOperator Ora istanziamo NullOperator:
>>> nulloperator = NullOperator() Ora istanziamo un TreeWalker usando il nulloperator - dobbiamo sempre dire al TreeWalker cosa vogliamo che faccia ad ogni nodo che visita. In questo caso, niente:
>>> walker = TreeWalker(nulloperator)
Lasciamo che ispezioni l'albero iniziando da una cartella specifica:
>>> walker.walk(folder) Ecco fatto. I nodi sono stati visitati. Il metodo operate della classe NullOperator è stato invocato e non ci aspettiamo che accada niente.
Facciamo qualcosa di reale
Immagina di voler costruire una sintesi testuale o un albero che mostra il percorso di ogni oggetto (come il comando tree di UNIX) in una gerarchia di cartelle. E supponiamo che non vuoi occuparti dei dettagli dell'iterazione.
Possiamo farlo abbastanza rapidamente. Tutto quello che ci serve è di definire un operatore conforme all'interfaccia di IOperator e passarlo ad un nuovo tree walker. Qui c'è il codice per il nostro doctest:
>>> from zope.interface import implements
>>> from Products.ATContentTypes.interface.operator import IOperator
>>> class TracingOperator (object):
... implements(IOperator)
... def __init__(self):
... self.trace = []
... def getTrace(self):
... return self.trace
... def operate(self, context, path='', **kwargs):
... self.trace.append(path) Lo abbiamo denominato TracingOperator, perché restituisce una traccia dell'attraversamento della gerarchia. Abbiamo dovuto aggiungere due nuove importazioni: implements che ci fornisce la nuova rigidità di interfaccia accennato nell'introduzione e Ioperator, che è l'interfaccia che dobbiamo implementare.
Il costruttore inizializza una lista che memorizzerà i nodi che visitiamo. GetTrace () restituisce tale lista, e Operate() è il metodo, definito nell'interfaccia di IOperator, che viene invocato su ogni nodo dell'albero. In questo caso, Operate() si limita ad aggiungere al trace il percorso del nodo partendo dal nodo su cui è iniziata la visita dell'albero.
Citando direttamente dal DocTest:
Aggiungi qualche cartella::
>>> folder = self.folder
>>> folder.invokeFactory('Folder', 'f1')
'f1'
>>> folder.f1.invokeFactory('Folder', 'subf1_1')
'subf1_1'
>>> folder.f1.invokeFactory('Folder', 'subf1_2')
'subf1_2'
>>> folder.f1.subf1_2.invokeFactory('Folder', 'subsubf1_2_1')
'subsubf1_2_1' Aggiungiamo una selezione di documenti:
>>> folder.f1.invokeFactory('Document', 'd1')
'd1'
>>> folder.f1.d1.setText("A nice text")
>>> folder.f1.subf1_1.invokeFactory('Document', 'd2')
'd2'
>>> folder.f1.subf1_1.d2.setText("A nice text")
>>> folder.f1.subf1_1.invokeFactory('Document', 'd3')
'd3'
>>> folder.f1.subf1_1.d3.setText("A nice text")
>>> folder.f1.subf1_2.subsubf1_2_1.invokeFactory('Document', 'd4')
'd4'
Ed esegui l'operazione::
>>> tracingoperator = TracingOperator()
>>> tracingwalker = TreeWalker(tracingoperator)
>>> tracingwalker.walk(folder.f1)
>>> trace = tracingoperator.getTrace() Dopo aver istanziato il TracingOperator lo passiamo all'istanza del TreeWalker. Quindi effettuiamo la visita, a partire dalla cartella di esempio che abbiamo generato sopra. A proposito, “folder” è la cartella radice che ci fornisce il test framework.
Finalmente possiamo vedere la lista di articoli sul percorso che abbiamo visto:
>>> print trace
['f1', 'f1/subf1_1', 'f1/subf1_1/d2', 'f1/subf1_1/d3', 'f1/subf1_2',
'f1/subf1_2/subsubf1_2_1', 'f1/subf1_2/subsubf1_2_1/d4', 'f1/d1'] Questo è il tracciato della struttura originale della cartella. Ci sono un paio di cose da notare:
L' origine del percorso, “folder.f1” in questo caso viene visualizzato nei risultati. Ciò significa che “operate” è stato invocato anche su tale nodo.
Anche le cartelle intermedie sono stati visitate. Per esempio, “f1/subf1_2” è una cartella che contiene un'altra cartella.
Le cartelle sono visitate prima dei loro contenuti. Questa è un tipo di ricorsione chiamata “pre-order”, in quanto le cartelle sono visitate prima dei loro contenuti. Sembra che questo sia il caso più comune.
Approfondimento sul filtraggio
Il filtraggio è una parte importante nella visita di un albero. Migliora le prestazioni e può semplificare il codice nel tuo operatore. Volevamo potenza e flessibilità senza complessità. Inizialmente, filtrare era semplice e omogeneo - il filtro fornito di default, o il filtro passato al walker alla creazione, veniva applicato a tutti i nodi dell'albero. Abbiamo migliorato questo offrendo una terza opzione che potremmo denominare Filtraggio Eterogeneo, o forse Filtraggio Adattativo che applica automagicamente filtri differenti a nodi differenti di un albero.
Le opzioni di filtraggio sono:
Se una filterClass viene passata come parametro a TreeWalker (), quella classe viene applicata per filtrare il nodo di inizio e tutti i nodi secondari ed è responsabile della sicurezza di accesso al contenuto dell'albero (vedi successivamente).
Altrimenti, se una cartella implementa IFilterFolder, sarà adattata durante la ricorsione mediante quell'interfaccia ed il filtro configurato per quel tipo sarà applicato. Un livello di sicurezza è fornito dalla superclasse Filter.
Altrimenti, il filtro di default - FolderFilter - sarà applicato. FolderFilter restituisce tutti gli elementi in una cartella per elaborarli, in conformità con le osservazioni di sicurezza qui sotto.
Questo ci permette, configurando degli adattatori speciali per uno specifico tipo, di controllare il numero ed il tipo di elementi restituiti dal filtraggio. Un semplice esempio sarebbe un PhotoAlbum, dove se venisse definito un adattatore specifico, sarebbero restituiti solo oggetti foto e album secondari. Tutti gli altri documenti in una gerarchia di PhotoAlbum sarebbero ignorati durante l'attraversamento dell'albero. Ciò è sviluppato più dettagliatamente nella sezione Implementazione.
Usando “filterClass=FolderFilter” come parametro per TreeWalker (), abbiamo anche la possibilità di forzare facilmente l'elaborazione su tutto il contenuto accessibile (vedi Sicurezza, sotto), forzando l'applicazione del filtro meno stringente a tutti i nodi contenitore accessibili, senza eccezione.
Sicurezza
Esiste un problema di sicurezza che è importante considerare nella visita di un albero. In Zope possiamo invocare un metodo su un nodo se l'utente ha i permessi necessari su quel nodo. Ma quel permesso non ci impedisce durante la visita di accedere o manipolare contenuti a cui l'utente normalmente non avrebbe diritto d'accesso. Nel caso del nostro metodo Archive, non vogliamo che il nostro archivio compresso contenga elementi che l'utente normalmente non potrebbe vedere.
Dal punto di vista del programmatore, non vogliamo che il creatore di un filtro debba lottare con questioni di sicurezza di cui possiamo occuparci noi per suo conto.
Per occuparci di entrambe le questioni, abbiamo creato una gerarchia Filter che separa i due aspetti di selezione e di sicurezza del filtraggio in due metodi. L'utente normalmente implementerà la funzione di selezione in una nuova sottoclasse di Filter, lasciando che il nostro metodo listPermittedObjects (), fornito nella superclasse, si occupi dell'aspetto sicurezza. Senza entrare nel dettaglio, basta dire che implementare un adattatore per un filtro è facile quanto questo:
class PhotoAlbumFilter(Filter):
"""
"""
implements(IFilterFolder)
def filter(self):
return self.context.ObjectValues(['Image','PhotoAlbum','Folder']) E se vuoi escludere la nostra sicurezza completamente, hai sempre la possibilità di fornire la tua filterClass che ti permette di avere tutto il controllo e tutta la responsabilità che ti servono.
Implementazione
“Generalizzazione, generalizzazione ma non sovra-generalizzazione” è il punto principale. Desideriamo creare un'implementazione che possa essere usato da altri, i quali a loro volta provano a fare qualcosa che si avvicina alla nostra esigenza, ma che non sia esattamente la stessa. Attenzione però al fatto che troppa generalizzazione uccide l'intento. Il trucco sta nel trovare l'equilibrio. Diteci se pensate che con TreeWalker lo abbiamo fatto.
Precedentemente usavamo l'ereditarietà, mixin ed il meccanismo di ereditarietà multipla per generalizzare/specializzare il comportamento di un classe. Ciò significa organizzare la classe in una gerarchia, generalizzare con un metodo bottom-up e specializzare con un metodo top-down.
Nell'architettura a componenti di Zope 3, l'implementazione sarà diversa ed userà vari componenti:
Le interfacce
Gli adattatori
Gli eventi
La configurazione ZCML
Le viste browser
Questo vi aiuterà a separare le diverse funzionalità in componenti diversi.
Five
Homepage: http://codespeak.net/z3/five/
Five è un prodotto Zope creato per aiutare gli sviluppatori a spostarsi per gradi da Zope 2 all'architettura a componenti di Zope 3. Senza di esso, Zope 2 e Zope 3 sarebbero due mondi completamente separati e sarebbe impossible migrare fra loro. Five è incluso in Zope 2.8 (Five versione 1.2) e in Zope 2.9 (Five versione 1.3). E' realmente molto vicino allo stile di Zope 3 e permette di usare le interfacce, gli adattatori e gli eventi di Zope 3 all'interno di Zope 2 senza doverlo installare.
Five non è molto documentato, purtroppo, perché gli sviluppatori di Zope 2 non conoscono la potenza che Zope 3 potrebbe fornire loro, e quindi viene usato soltanto in pochi progetti. Stiamo cercando di superare questo problema ma non possiamo essere gli unici che ci provano, ecco perché ci auguriamo che questo tutorial possa aiutarvi a comprendere Five e Zope 3.
Interfacce
Questa sezione rende esplicita la differenza fra la interfacce di Zope 2 e Zope 3, e mostra la differenza fra le interfacce implementate nel file configure.zcml ed all'interno della classe. È il nostro primo sguardo al file di configurazione ZCML, quindi approfondiamo un po' l'argomento.
Interfacce
Le interfacce sono realmente utili per descrivere che cosa fa una classe. Le interfacce di Zope 2 e di Zope 3 entrambe documentano il “Cosa” senza parlare del “Come”, ma Zope 3 si spinge più avanti, usando le interfacce come componenti per adattare una classe, specializzando così il suo comportamento.
Zope2
Nel mondo di Zope 2, le interfacce erano intese solo a documentare la classe e dare una descrizione di metodi e attributi, in modo da poter dare un rapido sguardo ai diversi metodi di un classe e alla loro documentazione senza preoccuparsi dell'implementazione.
Le interfacce Zope 2 ereditano da questi pacchetti:
>>> from Interface import Interface
>>> from Interface import Attribute Perciò un'interfaccia è solo un classe con dei metodi, senza codice ed attributi, che eredita da Interface.
Per convenzione, un'interfaccia comincia sempre con la lettera I. Ecco un semplice esempio:
>>> class ITreeWalker (Interface):
... """
... Visita un albero
... """
...
... my_attribute = Attribute("""Un finto attributo solo come esempio""")
...
... def walk(context, path=''):
... """
... visita l'intero albero.
... """ Da notare che non utilizziamo il self nel metodo walk in un'interfaccia, poichè ci limitiamo a descrivere i metodi della classe. Nota inoltre come viene definito un attributo in un'interfaccia.
Diciamo che un classe implementa un'interfaccia non appena dispone degli stessi metodi ed attributi dell'interfaccia. Una classe può avere più metodi della relativa interfaccia ma non di meno! Non c'è problema se una classe implementa più di un'interfaccia.
Generiamo la classe che implementa l'interfaccia precedente in stile Zope 2:
>>> class SimpleTreeWalker:
... """
... Una classe molto sepplice che visiterà una gerarchia
... """
...
... __implements__ = (ITreeWalker)
...
... my_attribute = None
...
... def walk(self, context, path=''):
... """
... visita l'intero albero
... """
... pass Guarda la variabile __implements__ , in Zope 2 quello è il modo di dire che questa classe implementa le relative interfacce.
Da notare che Zope/Python non protesteranno se non implementerete correttamente tali interfacce (usando i metodi necessari, con i giusti parametri, etc.)
Perciò generare un test su questo è molto utile:
>>> ITreeWalker.isImplementedByInstancesOf(SimpleTreeWalker)
1
>>> from Interface.Verify import verifyClass
>>> verifyClass(ITreeWalker, SimpleTreeWalker)
1 Possiamo anche testare un'istanza della classe SimpleTreeWalker:
>>> simpleTreeWalkerObject = SimpleTreeWalker()
>>> ITreeWalker.isImplementedBy(simpleTreeWalkerObject)
1
>>> from Interface.Verify import verifyObject
>>> verifyObject(ITreeWalker, simpleTreeWalkerObject)
1 Così siamo felici di avere un'interfaccia e un classe che la implementa correttamente.
Zope3
Creare un'interfaccia in Zope 3 non è differente da Zope 2, cambiano solo le classi da cui si eredita.
Le interfacce di Zope 3 ereditano le informazioni da un pacchetto diverso:
>>> from zope.interface import Interface
>>> from zope.interface import Attribute Ora fissate bene in mente che tutte le cose che vengono da zope.XXX sono pronte per Zope 3.
Quindi possiamo usare la stessa interfaccia di prima:
>>> class ITreeWalker (Interface):
... """
... Visita un albero
... """
...
... my_attribute = Attribute("""Un semplice attributo di esempio""")
...
... def walk(context, path=''):
... """
... visita l'intero albero.
... """ Nello stile di Zope 3 abbiamo due diversi modi di definire che una classe implementa un'interfaccia:
Il primo modo può essere fatto dalla classe stessa in stile Z2.
Il secondo modo sarà molto differente. Non lo facciamo dalla classe come abbiamo fatto in Z2, ma lo facciamo dal file ZCML.
Il secondo metodo potrebbe essere più difficile del primo, benchè sia un buon modo di indicare il collegamento fra il cosa e il come fuori dal come stesso.
Dalla classe
Questo è il modo semplice per dichiarare che un classe implementa un'interfaccia Zope 3:
>>> from zope.interface import implements
>>> class SimpleTreeWalker:
... """
... Una semplice classe che visita una gerarchia
... """
...
... implements(ITreeWalker)
...
... my_attribute = None
...
... def walk(self, context, path=''):
... """
... visita l'intero albero
... """
... pass E questo è tutto, non dovrai più andare al tuo configure.zcml. La tua classe implementa l'interfaccia Zope 3.
Dal file ZCML
Digressione su ZCML
Dobbiamo parlare dello ZCML di Zope 3 . Zope 3 viene rilasciato con un nuovo meccanismo di configurazione e collegamento di componenti, usando XML. Questa è considerata come buona cosa da qualcuno e come nuova dolorosa cosa da altri, ad ogni modo non siamo qui per protestare, limitiamoci a spiegare.
Questo è un semplice esempio di configurazione mediante file ZCML:
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five">
</configure> xmlns definisce i diversi namespaces. (Definizione di W3C: “Un namespace XML è una collezione di nomi, identificati da un riferimento URI…, usati nei documenti XML come tipi di elemento e nomi di attributo”). Qui dichiariamo il namespace Zope 3 per la configurazione zcml di base ed il namespace Five per potere inserire il nostro codice Zope 2 nell'architettura a componenti di Zope 3.
Questo zcml risiede in configure.zcml ed è analizzato all'avvio di Zope da Five (in Zope 2).
Ora che possiamo giocare con ZCML torniamo alla nostra implementazione di interfaccia. Generiamo la classe che implementa l'interfaccia:
>>> class SimpleTreeWalker:
... """
... Una semplice classe che visiterà una gerarchia
... """
...
... my_attribute = None
...
... def walk(self, context, path=''):
... """
... visita l'intero albero
... """
... pass Da notare che questa classe è come la classe Z2 salvo che noi non parliamo più di __implements__! Il nuovo modo di dire che questa classe implementa l'interfaccia ITreeWalker è:
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five">
<five:implements
class="Products.ATContentTypes.adapters.treeWalker.TreeWalker"
interface="Products.ATContentTypes.interface.treeWalker.ITreeWalker"
/>
</configure> Adesso TreeWalker implementa l'interfaccia ItreeWalker. Nuovamente Zope/Python non protesteranno se non implementi correttamente la tua interfaccia, perciò un test è il benvenuto:
Testando dalla classe di SimpleTreeWalker:
Il test di implementazione dell'interfaccia Zope2 era:
ItreeWalker.isImplementedByInstanceOf(SimpleTreeWalker)
Il test di implementazione dell'interfaccia Zope3 è:
ITreeWalker.implementedBy(SimpleTreeWalker)
Testando dall'oggetto di SimpleTreeWalker (simpleTreeWalkerObject):
Il test di implementazione dell'interfaccia Zope2 era:
ItreeWalker.isImplementedBy()
Il test di implementazione dell'interfaccia Zope3 è:
ItreeWalker.providedBy(simpleTreeWalkerObject)
Uso:
>>> ITreeWalker.implementedBy(SimpleTreeWalker)
True
>>> from zope.interface.verify import verifyClass
>>> verifyClass(ITreeWalker, SimpleTreeWalker)
True Effettuiamo il test da un'istanza della classe SimpleTreeWalker:
>> simpleTreeWalkerObject = SimpleTreeWalker()
>> ITreeWalker.providedBy(simpleTreeWalkerObject)
True
>> from zope.interface.verify import verifyObject
>> verifyObject(ITreeWalker, simpleTreeWalkerObject)
True Siamo di nuovo felici, la nostra classe implementa un'interfaccia Zope 3!
La direttiva include di ZCML
Se devi definire molte direttive zcml all'interno del tuo configure.zcml, una buona pratica è di scindere il file usando la direttiva <include file="myfile.zcml"/>.
Esempio:
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five">
<include file="implements.zcml"/>
</configure> Posizione dei file delle Interfacce Zope2 e Zope3
Nel prodotto ATCT, le interfacce Zope 2 sono definite nel file interfaces.py. Tutte le interfacce Zope 3 sono nella cartella interface. Non c'è ancora una convenzione definitiva su dove mettere le interfacce (anche in Zope 3).
Zope 3 ti forzerà a scrivere le interfacce (che sono una buona pratica) poichè esse non avranno solo funzione di documentazione, sono anche il punto di partenza per ottenere l'adattatore.
Adattatori
Il problema principale con le classi Zope 2 è che diventano veramente complesse col tempo (anche se le tecniche di ereditarietà contribuiscono a ridurre il numero di funzionalità in un classe). Il nuovo modo di pensare di Zope 3 dice: mantieni la tua classe molto piccola ed aggiungi funzionalità alla tua classe per mezzo di adattatori. Così aggiungere funzionalità significa aggiungere un adattatore; un adattatore aggiunge una funzionalità ad un classe di base. Tutte le implementazioni specifiche di una funzionalità saranno nascoste dalle interfacce nell'architettura a componenti di Zope 3.
Perciò ricordiamoci che abbiamo 3 cose importanti da tenere presenti:
Le interfacce che definiscono le funzionalità
Gli adattatori che implementano le funzionalità
L' oggetto che può essere adattato
Puoi avere vari adattatori che implementano una stessa interfaccia. L'interfaccia dell'adattatore descrive soltanto le funzionalità e deve essere il più generale possibile. Diamo ora un'occhiata alle interfacce che definiscono una funzionalità. Prendiamo la funzionalità del filtro. In un oggetto folderish (una cartella) vuoi elencare soltanto del contenuto specifico (per esempio: in un PhotoAlbum vuoi elencare solo PhotoAlbum e immagini). Questo avrà un'interfaccia molto semplice:
>>> from zope.interface import Interface
>>> class IFilterFolder(Interface):
... """
... Filtra il contenuti di un oggetto folderish
... """
... def listObjects():
... """
... restituisce la lista di oggetti filtrati contenuti in una cartella
... """ Generiamo un adattatore filtro fittizio che implementa questa interfaccia e si limita a restituire tutti gli oggetti nella cartella:
>>> from zope.interface import implements
>>> class FolderFilter(object):
... """
... Filtro di esempio che non filtra nulla
... """
... implements(IFilterFolder)
...
... def __init__(self, context):
... """
... Inizializza il nostro adattatore
... """
... self.context = context
...
... def listObjects(self):
... """
... restituisce la lista di oggetti contenuti nella cartella
... """
... return self.context.objectValues() Come puoi vedere un adattatore eredita dalla classe Object, il tipo più semplice che potete immaginare. Si noti che un adattatore semplice prende l'oggetto da adattare (contesto) all'inizializzazione. La convenzione è di chiamarlo Context.
Generiamo un altro adattatore per un PhotoAlbum. Questo adattatore filtrerà gli oggetti Image e i PhotoAlbum contenuti:
>>> class PhotoAlbumFilter(object):
... """
... Filtro che restituisce solo oggetti Image e PhotoAlbum nella cartella
... """
... implements(IFilterFolder)
...
... def __init__(self, context):
... """
... Inizializziamo il nostro adattatore
... """
... self.context = context
...
... def listObjects(self):
... """
... restituisce la lista di oggetti Image e PhotoAlbum nella cartella
... """
... return [item for item in self.context.ObjectValues(['Image','Folder'])] In questo modo abbiamo due adattatori per un'interfaccia. Dovremmo ora aggiungere un po' di collante per fare in modo che FolderFilter adatti un oggetto Folder, e che PhotoAlbumFilter adatti un oggetto PhotoAlbum. Faremo questo nuovamente con un file ZCML: definiremo un adattatore che implementa un'interfaccia (e quindi una funzionalità) per un certo tipo di oggetto che implementa un'altra interfaccia (quanto appena detto diventerà più chiaro con l'esempio,… ci auguriamo :)):
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
>
<adapter
for="Products.ATContentTypes.interface.IATFolder"
provides="Products.ATContentTypes.interface.IFilterFolder"
factory="Products.ATContentTypes.adapters.folder.FolderFilter"
/>
<adapter
for="Products.ATContentTypes.interface.IPhotoAlbum"
provides="Products.ATContentTypes.interface.IFilterFolder"
factory="Products.ATContentTypes.adapters.image.PhotoAlbumFilter"
/>
</configure> Così ora è facile da capire, definiamo due adattatori che adatteranno due tipi di contenuto differenti.
Primo IATFolder
Vogliamo un adattatore che adatti ogni oggetto che implementa l'interfaccia Products.ATContentTypes.interface.IATFolder.
Un oggetto ATFolder la implementa e può essere adattato dal nostro adattatore:
for="Products.ATContentTypes.interface.IATFolder"
Qui diciamo che la funzionalità implementata dall'adattatore è descritta nell'interfaccia Products.ATContentTypes.interface.IFilterFolder:
provides="Products.ATContentTypes.interface.IFilterFolder"
Qui diciamo che l'adattatore che implementa IFilterFolder è in “Products.ATContentTypes.adapters.folder.FolderFilter”:
factory="Products.ATContentTypes.adapters.folder.FolderFilter"
Così, ricapitolando, questa direttiva significa che registreremo il nostro adattatore FolderFilter nel registro degli adattatori per qualsiasi oggetto che implementa IATFolder.
Secondo IPhotoAlbum
Vogliamo un adattatore che adatterà ogni oggetto che implementa l'interfaccia Products.ATContentTypes.interface.IPhotoAlbum. Adattiamo così fondamentalmente un “PhotoAlbum”:
for="Products.ATContentTypes.interface.IPhotoAlbum"
Qui diciamo che la funzionalità implementata dall'adattatore è descritta in questa interfaccia Products.ATContentTypes.interface.IFilterFolder:
provides="Products.ATContentTypes.interface.IFilterFolder"
Diciamo ora che l'adattatore che implementa IFilterFolder è in Products.ATContentTypes.adapters.image.PhotoAlbumFilter:
factory="Products.ATContentTypes.adapters.image.Phot
Sintetizzata in questo modo, questa direttiva registrerà il nostro adattatore PhotoAlbumFilter nel registro degli adattatori per qualsiasi oggetto che implementi IPhotoAlbum.
Ricordati il modello generico per la direttiva dell'adattatore:
<adapter
for="A"
provides="B"
implements="C"
/>
*for* (per) qualsiasi oggetto che implementi l'interfaccia A
noi *provide* (forniamo) una nuova funzionaalità definita dall'interfaccia B
*implements* (implementazione) della funzionalità è definita dalla sua *factory* nella classe C. Ci auguriamo che questo ora sia più chiaro!
Ed ora vediamo la magia del lookup dell'adattatore in azione. Come abbiamo detto prima, l'implementazione specifica di una funzionalità viene nascosta nell'architettura a componenti di Zope 3 dalle interfacce. Così, se desidero la lista filtrata degli oggetti di un qualsiasi contenuto folderish senza dovermi preoccupare se si tratta di un PhotoAlbum, una semplice cartella, una cartella Audio, mi limito ad adattare l'oggetto all'interfaccia. Invochiamola:
Se abbiamo una cartella “folder1”:
>>> self.folder.invokeFactory('Folder', 'folder1')
'folder1'
>>> folder1 = self.folder.folder1 Adattiamo la cartella all'interfaccia di IfilterFolder:
>>> from Products.ATContentTypes.interface import IFilterFolder
>>> adapter = IFilterFolder(folder1) E adesso otteniamo il FolderFilter:
>>> adapter.__class__
<class 'Products.ATContentTypes.adapters.folder.FolderFilter'> Questo adattatore usa la nostra cartella folder1 come contesto:
>>> adapter.context
<ATFolder at ...> Ora se abbiamo un PhotoAlbum folder1:
>>> from Products.ATContentTypes.interface import IPhotoAlbum
>>> from zope.interface import directlyProvides
>>> directlyProvides(folder1, IPhotoAlbum) In questo modo la cartella diventa in effetti un PhotoAlbum. Adattiamo il PhotoAlbum invocando l'interfaccia di IfilterFolder:
>>> adapter = IFilterFolder(folder1)
Ed otteniamo il PhotoAlbumFilter
Qui potresti chiedere: “Perchè otteniamo un PhotoAlbumFilter e non un FolderFilter, dato che folder1 implementa sia IATFolder che IPhotoAlbum?”. La risposta è semplice: il lookup dell'adattatore trova la prima interfaccia che si associa ad un adattatore.
Quindi stai attento alla priorità nella dichiarazione delle interfacce e quando abbiamo eseguito directlyProvides (folder1, IPhotoAlbum), abbiamo dato priorità assoluta all'interfaccia IphotoAlbum rispetto a tutte le altre. Così il lookup dell'adattatore in primo luogo prende IPhotoAlbum, cerca se c' è un adattatore che si associa a questa interfaccia (cercando per l'attributo “for” nelle direttive zcml), se c' è, lo usa, se non c'è adattatore per questa interfaccia prende la successiva (che dovrebbe essere IATFolder) ed effettua lo stesso lookup per l'adattatore che si abbina a questa:
>>> adapter.__class__
<class 'Products.ATContentTypes.adapters.image.PhotoAlbumFilter'> Non preoccuparti per l'implementazione dell'adattatore per il tuo oggetto, il lookup lo farà per te.
Ora puoi definire altri adattatori filtro per il tuo tipo di contenuto semplicemente implementando l'interfaccia IFilterFolder e registrando il tuo nuovo adattatore, come abbiamo fatto nel precedente esempio di ZCML. La successiva sezione entrerà nel dettaglio.
Vista browser
Le viste di Zope 2 sono definite nella classe, come i metodi invocati dalla vista; ciò non va bene, e la classe cresce rapidamente. Oltretutto, poichè la gente non vuole rendere la propria classe più grande, comincia a inserire logica all'interno delle loro ZPT, mescolando codice e interfaccia. Altri preferiscono scrivere script Python o usare metodi esterni (external method) (che sono assolutamente migliori delle direttiva python: inglobate in ZPT). Tutto questo dimostra che c' è molta differenza fra logica e presentazione.Il problema è stato risolto dalle viste di Zope 3.
A che cosa penseresti se dicessimo che una vista su un oggetto è semplicemente un adattatore? L'obiettivo è quello di far sparire tutto quanto riguarda le viste da qualche altra parte. E questo è ciò che faremo. Spostiamo tutte queste cose in una classe vista Browser e nella configurazione zcml, in modo che la classe di base non deve più preoccuparsi di come i consumatori vedono i suoi contenuti, e non deve più fornire i metodi per ottenere i contenuti da visualizzare.
Un classe vista browser è un multi-adattatore, intendendo con questo che adatta un oggetto e la request.
Definiamo una semplice vista Browser per il nostro Archiver. Vogliamo poter chiamare http://myhostname/plone/Afolder/zip ed ottenere una file .zip contenente tutti gli oggetti nella cartella.
Inoltre desideriamo potere chiamare http://myhostname/plone/Adocument/zip ed ottenere il contenuto compresso del documento.
Poichè non possiamo usare direttamente le viste di Zope 3, dobbiamo usare le viste di Five:
>>> from Products.Five import BrowserView
>>> from Products.ATContentTypes.interface.archive import IArchiver
Definiamo la nostra vista, ereditando da BrowserView::
>>> class ArchiveView(BrowserView):
... """
... Vista su un oggetto per ottenere il suo contenuti compresso
... """
... def getZipFile(self,**kwargs):
... """
... """
... adapted = IArchiver(self.context)
... self.request.RESPONSE.setHeader('Content-Type','application/zip')
... self.request.RESPONSE.addHeader("Content-Disposition","filename=%s.zip" % self.context.getId())
... self.request.RESPONSE.write(adapted.getRawArchive(**kwargs)) Nota che qui stiamo usando un multi-adattatore, possiamo usare self.context come l'oggetto adattato e il self.request come la richiesta adattata (capisci da questo che l'init è qualcosa del tipo __init__ (self, context, request)).
Dai uno sguardo a adapted = IArchiver (self.context): qui usiamo di nuovo la magia del lookup degli adattatori per ottenere un archiver dell'oggetto adattato dalla vista. Questo ci permette di archiviare un documento o una cartella chiamando sempre gli stessi metodi (definiti nell'interfaccia IArchiver) senza preoccuparci di come sia stato fatto! È semplicemente magia!
Ok, ora abbiamo la nostra classe BrowserView, dovremmo mettergli un po' di colla intorno ed attaccarla a degli oggetti… Indovinate? useremo ancora delle direttive ZCML per farlo.
Diciamo di voler essere in grado di ottenere un file compresso solo per le cartelle:
<configure
xmlns="http://namespaces.zope.org/five"
xmlns:browser="http://namespaces.zope.org/browser">
<browser:page
for=".interface.IATFolder"
name="zip"
class=".browser.archive.ArchiveView"
attribute="getZipFile"
permission="zope2.View"
/>
<traversable class=".content.folder.ATFolder"/>
</configure> In questo modo definiamo una vista browser, proveniente da Products.ATContentTypes.browser.archive.ArchiveView, per tutti gli oggetti che implementano l'interfaccia IATFolder per l'utente avente il permesso View sull'oggetto. Questa vista può essere richiamata come metodo zip sull'oggetto. Non dimenticate la direttiva traversable sulla classe che implementa l'interfaccia nell'opzione for della direttiva browser. Siamo obbligati a fare questo perché il publisher di Zope 2 non conosce le viste di Zope 3 (che ci permetteranno di richiamare http://myhostname/plone/Afolder/zip).
Ora diciamo di voler essere in grado di comprimere documenti e cartelle e di farlo nella stessa direttiva zcml… Qui abbiamo un problema perché l'attributo for="A" (come per l'adattatore) nella direttiva browser accetta solo un'interfaccia… Vedremo un concetto utile (di cui avrei dovuto parlare nella sezione Interfaccia…): le interfacce Marker.
Interfacce Marker
Dal momento che non vogliamo definire una vista browser per ogni oggetto che implementa una specifica interfaccia, realizzeremo un'interfaccia Marker generale: IArchivable, e marcheremo gli oggetti Document e Folder come IArchivable. Ciò genererà una specie di gerarchia. Marker vuol dire: gli oggetti Document e Folder sono archiviabili (Archivable).
La prima cosa è definire l'interfaccia Marker.
Un'interfaccia Marker non è nient'altro che un'interfaccia fittizia:
>>> from zope.interface import Interface
>>> class IArchivable(Interface):
... """
... interfaccia marker per oggetti che si possono archiviare
... """ Dopo averla ottenuta, mettiamole di nuovo un po' di colla intorno con le nostre amichevoli direttive ZCML:
<configure
xmlns="http://namespaces.zope.org/five">
<implements
class=".content.document.ATDocument"
interface=".interface.archive.IArchivable"
/> In questo modo ATDocument e ATFolder sono conosciute come IArchivable.Ora possiamo usare una sola direttiva di vista browser che punta alla nostra interfaccia Marker nuova di zecca:
<configure
xmlns="http://namespaces.zope.org/five"
xmlns:browser="http://namespaces.zope.org/browser">
<browser:page
for=".interface.IArchivable"
name="zip"
class=".browser.archive.ArchiveView"
attribute="getZipFile"
permission="zope2.View"
/> Adesso sia la cartella che il documento “possono essere visti” come un archivio zip, e poichè non abbiamo dimenticato la direttiva traversable possiamo richiamare: http://myhostname/plone/Adocument/zip e http://myhostname/plone/Afolder/zip. Tutto questo grazie alla nostra vista Browser, alla nostra interfaccia Marker, ai nostri adattatori, alle nostre interfacce ed alle nostre classi.
Eventi
Zope 3 introduce un modo pulito per definire un evento. Zope 2 non dava un buon modo per usare gli eventi (implementare un metodo con un nome specifico in una classe non è il modo migliore per maneggiare eventi). Scordiamoci Zope 2 e tuffiamoci direttamente in Zope 3.
In primo luogo spieghiamo che cosa intendiamo per evento:
Un evento è qualcosa che avviene; un'occorrenza e un punto arbitrario nel tempo.
In informatica, un evento indica che qualcosa è accaduto. La definizione Zope 3 di evento non è lontana dalla definizione che potreste conoscere se avete programmato una GUI. Un utente clicca sopra un pulsante, un evento viene stato trasmesso al programma, il programma cattura l'evento e fa quel che deve fare.
Zope 3 vuole mantenere la registrazione degli eventi e la gestione del singolo evento fuori dell'applicazione di base, cosa che (come per gli adattatori e le viste) aiuta molto a definire la tua applicazione senza dover definire direttamente all'interno della tua classe di base come usare gli eventi (come Zope 2 faceva con il metodo manageAfterAdd…).
Vediamo quali tipologie di eventi possiamo gestire in un sistema di gestione contenuti:
un oggetto è stato creato (zope.app.container.interfaces.IObjectAddedEvent)
un oggetto è stato modificato (zope.app.event.interfaces.IObjectModifiedEvent)
un oggetto è stato eliminato (zope.app.container.interfaces.IObjectRemovedEvent)
un oggetto è stato copiato (zope.app.event.interfaces.IObjectCopiedEvent)
un oggetto è stato spostato/rinominato (zope.app.container.interfaces.IObjectMovedEvent)
un oggetto contenitore è stato modificato (zope.app.container.interfaces.IContainerModifiedEvent)
il metadato di un oggetto è stato modificato (zope.app.event.interfaces.IObjectAnnotationsModifiedEvent)
... altri eventi molto specifici (mail, traversing, zope startup)
Dobbiamo registrare un evento su un determinato tipo di oggetto, così come registriamo un adattatore per uno specifico tipo di oggetto. In Zope 3, un oggetto che si registra per un evento è chiamato “subscriber di evento”; molto spesso un subscriber avrà un handler che consiste in una funzione di callback, la quale eseguirà specifiche operazioni sull'oggetto.
Tutti questi eventi sono chiaramente definiti tramite la loro interfaccia (come hai visto sopra), e queste interfacce saranno da noi utilizzate per registrarci a un evento e creare il nostro subscriber.
Prendiamo un evento semplice: dopo che un documento è stato creato vogliamo ricevere una mail che ce lo notifichi. Non ci preoccupiamo di dire alla cartella che lo contiene che il documento è stato creato.
Per prima cosa dobbiamo definire il codice dell'oggetto subscriber dell'evento. Puoi vedere come l'handler di un subscriber di evento prende due parametri,
il primo è l'oggetto per cui l'evento è stato inviato, il secondo è l'evento stesso (potremmo voler dare uno sguardo al punto da dove l'evento è stato inviato):
>>> from Products.CMFCore.utils import getToolByName >>> def sendMailUpponDocumentCreation(document, event): ... mailhost = getToolByName(document, 'MailHost') ... msg = "Un Document %s è stato creato in %s " % (document.getId(), document.absolute_url()) ... mto = 'somebody@somewhere.org' ... mfrom = 'plone@plone.org' ... msubject = 'Document Creato' ... mailhost.send(msg, mto, mfrom, msubject=subject)
Questo è tutto ciò che serve al subscriber di evento. Ora abbiamo bisogno di mettere del collante intorno al tipo di contenuto Document (usando come sempre la sua interfaccia – questo ci permette di usare anche qui le interfacce Marker!) e l'evento creato userà la nostra nuova funzione. Five 1.2 e Five 1.3 descrivono un evento in modo diverso; ipotizziamo di usare Five 1.2:
<configure
xmlns="http://namespaces.zope.org/zope"
>
<subscriber
for="Products.ATContentTypes.interface.IATDocument
zope.app.container.interfaces.IObjectAddedEvent"
factory="Products.ATContentTypes.content.document.sendMailUpponDocumentCreation"
/>
</configure> (in Five 1.3 sostituisci semplicemente l'attributo factory con l'attributo handler).
Come puoi vedere, un subscriber di evento è definito dalla direttiva subscriber (questa direttiva è presente nei namespace di Zope). Fai attenzione alle due interfacce nell'attributo for. La prima deve essere l'interfaccia che descrive l'oggetto. La seconda descrive l'evento (ed è una delle interfacce che abbiamo elencato nella lista di eventi precedente). Factory descrive la factory (spesso una funzione) usata per l'evento per l'oggetto che ci interessa.
Nuovamente la direttiva è chiara:
for="A
B"
factory="C"
che significa
per tutti gli oggetti che forniscono l'interfaccia A
Voglio eseguire C quando l'evento B viene catturato Da notare che possiamo avere più di un subscriber per una specifica interfaccia oggetto. Questo ci permette, ad esempio, di spedire una mail quando genero il mio documento, ma anche di spedire un messaggio all'oggetto contenitore del nuovo documento.
Ancora sulla compatibilità verso Zope 2
Come detto in precedenza, nello stile Zope 2 utilizzavamo metodi di classe (come manage_afterAdd, manage_beforeDelete, manage_afterClone, etc.) per gestire eventi. In questo modo avevamo bisogno di richiamare esplicitamente l'evento sulle classi da cui ereditavamo per propagare l'evento anche su di esse (facendo attenzione a propagare l'evento sulla classe padre solo se tale classe fosse capace di gestirlo).
Dato che ora possiamo usare gli eventi di Zope 3, manage_before e manage_after sono deprecati! Vediamo come possiamo effettuarne la migrazione.
Immaginiamo di essere in ATDocument, abbiamo un metodo manage_afterAdd nel vecchio stile Zope 2:
class ATDocument(ATCTContent, HistoryAwareMixin):
...
implements(ATCTContent, IATDocument, HistoryAwareMixin)
...
security.declarePrivate('manage_afterAdd')
def manage_afterAdd(self, item, container):
"""Corregge il testo quando creato da webdav
Indovina il mimetype corretto da id/dati
"""
ATCTContent.manage_afterAdd(self, item, container)
field = self.getField('text')
# hook per il validatore mxTidy / isTidyHtmlWithCleanup
tidyOutput = self.getTidyOutput(field)
if tidyOutput:
if hasattr(self, '_v_renamed'):
mimetype = field.getContentType(self)
del self._v_renamed
else:
mimetype = self.guessMimetypeOfText()
if mimetype:
field.set(self, tidyOutput, mimetype=mimetype) # set is ok
elif tidyOutput:
field.set(self, tidyOutput) # set is ok
... Da notare che ATDocument implementa l'interfaccia IATDocument. Perciò la utilizzeremo nello zcml più tardi.
La prima cosa da fare è creare la funzione factory. Chiamiamola afterDocumentCreation. Sostituiamo self con document:
def afterDocumentCreation(document, event):
"""Corregge il testo quando creato da webdav
Indovina il mimetype corretto da id/dati
"""
field = document.getField('text')
# hook per il validatore mxTidy / isTidyHtmlWithCleanup
tidyOutput = document.getTidyOutput(field)
if tidyOutput:
if hasattr(document, '_v_renamed'):
mimetype = field.getContentType(document)
del document._v_renamed
else:
mimetype = document.guessMimetypeOfText()
if mimetype:
field.set(document, tidyOutput, mimetype=mimetype) # set is ok
elif tidyOutput:
field.set(document, tidyOutput) # set is ok Da notare che non possiamo propagare l'evento alla classe ATCTContent da questa funzione perché non siamo più nella classe. Questo rappresenta un problema perché sappiamo che ATContent deve catturare l'evento e non è predisposto per gli eventi Zope 3. Ci sono due soluzioni possibili: predisponiamolo agli eventi Z3 o richiamiamo l'evento sia in stile Zope 3 che in stile Zope 2.
Implementiamo la seconda soluzione: dovremo lasciare dov'è la chiamata a manage_afterAdd nella nostra classe con una riga, la chiamata a ATContent.manage_afterAdd. Quindi manage_afterAdd diventa:
class ATDocument(ATCTContent, HistoryAwareMixin):
...
implements(ATCTContent, IATDocument, HistoryAwareMixin)
...
security.declarePrivate('manage_afterAdd')
def manage_afterAdd(self, item, container):
"""Corregge il testo quando creato da webdav
Indovina il mimetype corretto da id/dati
"""
ATCTContent.manage_afterAdd(self, item, container)
... Ora dobbiamo configurare ATDocument per essere contemporaneamente compatibile con gli eventi di Zope 3 e Zope 2. Five ci offre la possibilità di farlo con una sola direttiva: deprecatedManageAddDelete.
Questo è tutto. La prima direttiva è come quella dell'esempio precedente, colleghiamo l'evento agli oggetti che forniscono l'interfaccia IATDocument e specifichiamo la factory che abbiamo appena implementato usando il codice di taking manage_afterAdd. La seconda direttiva dice che sappiamo che ATDocument usa un metodo di evento in stile Zope 2 nella sua classe (manage_afterAdd) ma che abbiamo ancora bisogno di invocarlo.
Usare il Filtraggio
Dato che ora sai tutto degli adattatori Z3, potresti voler usare le nostre interfacce per definire nuovi filtri e visitare la tua gerarchia.
Per cominciare, dai un bello sguardo all'interessante interfaccia IFilterFolder che trovi in Products.ATContentTypes.interface.folder.IFilterFolder. Questa interfaccia dice quello che devi definire per creare un filtro corretto:
class IFilterFolder(Interface):
def listObjects():
"""
restituisce la lista degli oggetti filtrati senza controllare la sicurezza
"""
def listPermittedObjects():
"""
restituisce la lista degli oggetti filtrati e controllando la sicurezza
"""
def filter(context):
"""
filtra gli oggetti nel contesto da visitare con TreeWalker.
Questo metodo è privato e non dovrebbe essere richiamato dall'esterno.
Usa listObjects() piuttosto
""" Non aggiungerò commenti su questa interfaccia, poiché si descrive da sola.
Ora diamo uno sguardo a Products.ATContentTypes.adapters.folder; lì ci trovi la classe Filter:
class Filter(object):
def __init__(self, context):
"""
il context dovrebbe essere un oggetto folderish
"""
self.context = context
def listObjects(self):
"""
restituisce la lista degli oggetti filtrati senza controlli di sicurezza
"""
return self.filter()
def listPermittedObjects(self):
"""
restituisce la lista degli oggetti filtrati tenendo conto della sicurezza
"""
return LazyFilter(self.filter(), skip='')
def filter(self):
"""
filtra gli oggetti nel context che viene visitato da TreeWalker.
"""
raise NotImplementedError, 'This method should be overriden in a subclass'
def __call__(self):
return self.listPermittedObjects() Come puoi vedere, questa classe implementa quasi IFilterFolder. Per quasi intendo che c'è solo un metodo non chiaramente implementato, il metodo filter. Perciò l'unica cosa di cui hai bisogno per creare un nuovo filtro è quella di implementare questo metodo ed ereditare dalla classe Filter. Facciamolo!
Diciamo di voler ottenere tutti i documenti nella cartella che si trovano nello stato di workflow "published":
>>> from Products.ATContentTypes.adapters.folder import Filter
>>> from Products.ATContentTypes.interface.folder import IFilterFolder
>>> from Products.CMFCore.utils import getToolByName
>>> class FilterPublishedDocument(Filter):
... """
... prende solo i documenti pubblicati nella cartella
... """
... implements(IFilterFolder)
...
... def filter(self):
... workflow = getToolByName(self.context, 'portal_workflow')
... publishedDocuments = []
... for document in self.context.objectValues('Document'):
... if(workflow.getInfoFor(document,'review_state') == 'published'):
... publishedDocument.append(document)
...
... return publishedDocuments Questo è tutto per il filtraggio. Se vuoi usarlo nel treeWalker dovresti collegare questo filtro all'oggetto folderish che vuoi visitare (dovrebbe essere ATFolder), o dai semplicemente la classe FilterPublishedDocument come parametro all'inizializzazione del TreeWalker.
Per usare FilterPublishedDocument per ATFolder lo zcml dovrebbe essere come questo:
<adapter
for="Products.ATContentTypes.interface.IATFolder"
factory="Products.ATContentTypes.adapters.folder.FilterPublishedDocument"
provides="Products.ATContentTypes.interface.IFilterFolder"
/> Da notare che potremmo aver usato il catalogo nel filtraggio per filtrare gli oggetti in uno specifico stato (questo renderebbe tutto un po' più veloce ma meno semplice).
Finale
Revisori
Raphael Ritz – grazie per i tuoi commenti dettagliati e per aver evidenziato il problema di sicurezza in Filter, che ha stimolato la nostra derivazione da Filter e l'aggiunta di un metodo wrapper nella classe di base per poter applicare LazyFilter().
Martin Aspeli – grazie per la tua pronta revisione e incoraggiamento
Joel Burton – Ci aspettavamo niente di meno che una seria e incisiva revisione, Joel. Non siamo arrabbiati. Grazie!
Suggerimenti
Alec Mitchell – grazie per aver aiutato il merge dei nostri branch nel trunk di ATCT, per il tuo incoraggiamento, e per aver accettato il bellissimo compito di release manager di Plone.
Altri
La grande Comunità Plone che ha reso possibile tutto questo. - Grazie!
Riferimenti
Per avere ulteriori informazioni vi invitiamo a consultare:
Web Component Development with Zope 3
di Philipp von Weitershausen
http://worldcookery.com/
Zope 3 Developer's Handbook
di Stephan Richter
http://www.amazon.com/gp/product/0672326175/103-9651091-6407816
Literate Testing: Automated Testing with doctest
Jim Fulton, jim@zope.com
Tim Peters, tim@zope.com
PyCon 2004
http://www.python.org/pycon/dc2004/papers/4/
