Alberi

Un albero è la rappresentazione di una struttura gerarchica e viene indicato col nome albero se è visibile un’unica radice, mentre viene definito foresta se si è in presenza di più radici.

In Genropy come già sapete ci si appoggia quasi sempre alle Bag e se abbiamo una Bag possiamo visualizzarla come un albero.

Il codice è estremamente banale: definiamo una bag, inseriamo dei dati con un opportuno path gerarchico e mettiamo quindi questa bag come data al path world_data. Se definiamo poi un tree che ha come storepath world_data otteniamo nella pagina la visualizzazione gerarchica della bag e quindi dei nostri dati.

Anche questo esempio è semplicissimo. Vediamo solo che abbiamo spostato il caricamento dell’albero in un metodo separato e che oltre all’albero primario ne creiamo altri che attingono allo stesso store ma a path differenti.

In questo esempio nella costruzione della bag aggiungiamo anche degli attributi ad ogni nodo per dare un nome (inglese ed italiano) al nodo stesso.

Nella GUI mettiamo una filteringSelect che ci consente di selezionare la lingua voluta (inizializzata ad “en”) e un dataFormula che trasforma la lingua in un nome di attributo (“name_en” oppure “name_it”). Da notare che facciamo in modo di far scattare la formula al caricamento della pagina aggiungendo “_onStart=True”.

Nella definizione dell’albero aggiungiamo il parametro labelAttribute il cui valore sarà “name_en” o “name_it” a seconda della lingua scelta.

Al variare della lingua l’albero si ridisegna in modo automatico usando la lingua voluta.

Vediamo ora l’uso di un Tree per visualizzare il contenuto di una directory del server.

Ci proponiamo di avere un albero che ci consenta di navigare tra le directories ed a fianco dell’albero vogliamo vedere il contenuto dei file con estensione .py.

Per prima cosa notiamo che viene importato il Directory Resolver. Ricordiamo che un resolver è un elemento che messo in una bag è in grado di eseguire una chiamata per popolare la bag stessa. Spesso succede che il resolver si chiami ricorsivamente.

Ad esempio nel caso del Directory resolver, quando ne istanziamo uno per un certo path, alla sua risoluzione viene esaminato il contenuto di quel path e creato un elemento di bag vuoto (ovvero con i soli attributi) nel caso di file, mentre viene creato un elemento che contiene un nuovo directory resolver per ogni directory trovata a quel livello. Quindi mano mano che interroghiamo il resolver questo va ad interrogare il file system e a costruire la bag.

Questo è quello che accadrebbe se lavorassimo solo in python. In realtà la bag (risolta al primo livello) viene passata al client, il quale trasforma ogni resolver python che riceve in una sua versione javascript in grado di fare una chiamata RPC per popolare il livello successivo. Tutto il meccanismo è automatico e completamente trasparente allo sviluppatore. Basta infatti mettere nella bag un resolver python ed il sistema di serializzazione e deserializzazione compie tutto il lavoro necessario.

Veniamo ora all’esame del codice:

def main(self,root,**kwargs):
    bc=root.borderContainer(datapath='diskviewer')
    self.treePane(bc.contentPane(region='left',splitter=True,
                                 width='250px',padding='4px'))
    self.topPane(bc.contentPane(region='top',
                                height='30px',background='#666'))
    center=bc.contentPane(region='center')
    center.codemirror(value='^.content',config_mode='python',
                      config_lineNumbers=True,config_indentUnit=4,
                      config_keyMap='softTab', height='100%')

Nella chiamata main viene creato un borderContainer cui viene assegnato un datapath “diskviewer” in cui metteremo tutti i dati del nostro esempio. Vengono poi effettuate le chiamate a “treePane” e “topPane” per riempire i contenuti della region “left” e region “top”. Infine viene definito un contentPane region “center” destinato ad accogliere il widget codemiror opportunamente configurato per mostrare un file python.

Vediamo treePane:

def treePane(self,pane):
  resolver= DirectoryResolver('/genropy/gitrepos/genropy')
  pane.data('.root.genropy',resolver())
  pane.tree(storepath='.root',hideValues=True, selectedLabelClass='selectedTreeNode',
                selected_abs_path='.abs_path',selected_file_ext='.file_ext',
                labelAttribute='nodecaption')
  pane.dataRpc('.content',self.getContent, filepath='^.abs_path',
                 file_ext='=.file_ext',_delay=500,
                 _if="file_ext=='py'",_else='return "Only python files here..."')

Per prima cosa creiamo un’istanza di DirectoryResolver passando il path di origine. In questo caso abbiamo usato “/genropy/gitrepos/genropy”. Se avessimo messo “/” avremmo potuto esaminare tutto il disco.

Mettiamo poi il resolver al path “.root.genropy” con l’istruzione data. Ricordiamo che ciò che viene messo in questa modalità è passato al client contestualmente alla chiamata “main”, questo significa che appena creata la pagina sul client, nello store al path “diskviewer.root.genropy” avremo il contenuto del primo livello della directory radice. Va notato che in realtà settiamo il resolver seguito da “()”. Chiamare un resolver ha l’effetto di risolverlo. Quello che facciamo quindi con l’istruzione data in questo caso è di chiamare il resolver che accederà al filesystem e creerà una Bag con gli elementi trovati in radice. Ricordiamo che per ogni sottodirectory verrà creato un nuovo DirectoryResolver.

Definiamo poi il nostro tree passando come parametro storepath proprio l’indirizzo cui abbiamo messo il nostro resolver risolto. Il widget Tree ammette molti parametri che verranno esaustivamente spiegati nella pagina della Widgetpedia dedicata ai Tree. Per ora notiamo l’attributo selectedLabelClass che definisce la classe di css da applicare alla label correntemente selezionata, l’attributo labelAttribute che specifica quale attributo del nodo deve essere usato come label nel tree e gli attributi selected_abs_path e selected_file_ext che hanno l’effetto di ricopiare ai path specificati gli attributi desiderati.

Vediamo qui l’uso di un nameSpace implicito introdotto dal prefisso 'selected_': ad esempio selected_abs_path='.abs_path' chiede al tree di mettere al path '.abs_path' l’attributo 'abs_path' del nodo selezionato.

Notiamo ancora l’attributo hideValues=True che rappresenta gli elementi terminali come “foglie” dato che il loro valore è sempre nullo. Senza questo attributo verrebbero rappresentati come elementi apribili di contenuto nullo.

Notiamo anche l’attributo autoCollapse=True che fa richiudere automaticamente un branch dell’albero quando viene aperto un branch dello stesso livello.

Abbiamo infine un dataRpc che scatta al variare di '.abs_path', ovvero del nodo di albero correntemente selezionato ed esegue la chiamata del metodo remoto getContent. Notiamo in questa dataRpc l’uso del parametro _delay per ritardare di mezzo secondo l’esecuzione della chiamata. Con questo accorgimento se l’utente clicca velocemente elementi diversi, viene eseguita la chiamata solo sull’ultimo click (o almeno dopo che ha smesso di cliccare per almeno 500 millisecondi).

Sempre a proposito del dataRpc notiamo che viene anche chiesto al datastore il parametro _file_ext='=.file_ext' che viene prefissato da “_” perchè non deve essere passato al server e viene impiegato solo localmente nella clausola _if="_file_ext=='py'". In questo modo la chiamata al server è fatta solo per i file con estensione “py” mentre (clausola _else), per gli altri, viene resituito il valore “Only python files here…” senza nemmeno eseguire la chiamata remota.

Ultimo dettaglio: scriviamo _file_ext='=.file_ext' ovvero con il simbolo “=” e non “^” perchè non vogliamo essere richiamati al variare dell’estensione ma solo del nome del file. Usando il simbolo “=” il valore al path “.file_ext” viene valutato all’esecuzione del dataRpc. Solo il parametro filepath='^.abs_path' ha il puntatore attivo “^” e provoca l’esecuzione del dataRpc. Inoltre è l’unico parametro che verrà passato al server dato che non inizia per “_”.

Vediamo ora il metodo topPane:

def topPane(self,pane):
fb=pane.formbuilder(cols=1)
fb.div('^.abs_path',lbl='Selected disk path',
       lbl_color='white',background='white',padding='2px',rounded=8)

Il metodo è assolutamente ovvio e ci mostra il filepath correntemente selezionato nell’albero.

Ed infine veniamo alla chiamata rpc getContent:

@public_method
def getContent(self,filepath=None,**kwargs):
    filepath=os.path.join('/genropy/gitrepos/genropy',filepath)
    with open(filepath,'r') as f:
        data=f.read()
    return data

Anche questo metodo è assolutamente ovvio e provvede a rendere il contenuto del file. Unica annotazione di rilievo è l’uso di **kwargs che è preferibile mettere in quanto, in certe occasioni, il sistema può passare parametri supplementari e non usando **kwargs potremmo trovarci una chiamata in errore.

L’albero può anche essere usato per selezionare degli elementi in una gerarchia.

In questo caso mostriamo lo stesso albero della directory e aggiungiamo al tree degli attributi che servono ad abilitare questa funzione.

Notiamo per prima cosa checkedPaths='.checked': questo significa che per ogni elemento dell’albero che viene marcato, il suo path viene aggiunto in una stringa di testo separata da un separatore (di norma il simbolo “,”). In questo caso però, con l’attributo checkedPaths_joiner='\n' richiediamo che questa stringa sia separata da “n”.

Se contrassegniamo alcune voci notiamo che nella colonna centrale abbiamo la concatenazione dei path contrassegnati mentre, per la presenza dell’attributo checked_abs_path='.checked_abs_path:\n', nella terza colonna vediamo concatenati i valori dell’attributo abs_path.

Dato un albero che rappresenta una bag con attributi, possiamo avere il valore concatenato di qualsiasi attributo voluto con la sintassi checked_ seguito dal nome dell’attributo.

Vediamo ora un component derivato dall’albero ovvero il treegrid.

Come nei casi precedenti prepariamo il resolver ma invece di un albero costruiamo un treegrid. Leggendo l’esempio la sintassi dovrebbe essere molto facile da capire.

Da notare che nelle colonne si usa size invece che il più classico width perchè (a differenza delle grid) la larghezza della colonna è sempre data in pixel.