Hi, there. I want to talk to you about ducts, czyli porozmawiajmy o szablonach Mako

:  

Z ciekawości spojrzałem, co się pisze o szablonach Mako w internetach po polsku, i... no właśnie. Nic. Raptem kilka wpisów wspominających, że jest, a jak jest, to do dupy. Hmm.. Szczerze mówiąc, po ponad roku pracy z tymi szablonami przy różnych projektach, jestem dokładnie odwrotnego zdania. Jeśli szukacie pythonowych systemu szablonów elastycznego jak guma w gaciach, który nie będzie was ograniczał, a jednocześnie będzie szybki, to Mako jest właśnie tym, czego szukacie.

WIELKI CZERWONY NAPIS! Ten wpis nie zastąpi lektury dokumentacji. Jest to zbiór tanich chwytów do wykorzystania w kodzie

Tips & tricks

  • Wprawdzie Mako jest inkorporowane w Pylonsach, ale nic nie przeszkadza użyć tego gdzie indziej, tym bardziej, że integracja z dowolną aplikacją pythonową jest prosta (nie licząc GAE). W naszym przykładzie zakładamy, że używamy dziedziczenia i inkludowania zawartości, więc Mako wymaga zainicjowania obiektu TemplateLookup, który będzie wewnętrznie rozwiązywał zależności między szablonami.
    W momencie, kiedy chcemy przywołać szablon i wyrenderować go, należy wykonać parę TemplateLookup.get_template(uri) oraz Template.render().

    Przykład:

    szablon.mako:

    ## -*- coding: utf-8 -*-
    <%inherit file="/base.mako"/>
    <%def name="content()">
    ${ x }
    </%def>
    ${ content()}

    template.py:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    from mako.template import Template
    from mako.lookup import TemplateLookup
    lookup = TemplateLookup(directories=[ '.'], output_encoding = 'utf-8', default_filters=['decode.utf-8'])
    t = lookup.get_template('szablon.mako')
    print t.render(x='x')

    bądź t.render_unicode(), aby uzyskać obiekt unikodowy.

  • Przydatne parametry TemplateLookup:

    directories=None
    lista katalogów, w których mają być szukane pliki szablonów.
    module_directory=None
    katalog, w którym przechowywane są skompilowane szablony
    filesystem_checks=True
    sprawdzanie, czy skompilowany szablon jest aktualny. można wyłączyć dla zaoszczędzenia na IO ustawiają na False
    collection_size=-1
    wielkość kolekcji skompilowanych szablonów, które trzymane są w pamięci.
    modulename_callable=None
    Callable, które zwraca nazwę modułu szablonu.

    pozostałe parametry trafiają do Template:

    format_exceptions=False
    włącza/wyłącza formatowanie wyjątków
    error_handler=None
    callable przyjmujące context i wyjątek, wywoływane w przypadku wyjątku
    disable_unicode=False
    Flaga wyłączająca wewnętrzne używanie unikodu. Traktuje wszystkie podstawienia (${}) jako czyste stringi. Odradzana w użyciu ze względu na implikacje (WSZYSTKIE podstawienia muszą być typu <str>, szablon nie będzie działał z Py3k), choć daje pewne zyski w wydajności
    output_encoding=None
    Kodowanie wykonanej zawartości szablonu. Wewnętrznie utworzony unikod zostanie zakodowany do tego kodowania
    encoding_errors='strict'
    Sposób traktowania błędów kodowania - więcej o tym można przeczytać w dokumentacji Pythona.
    cache_type=None
    Typ kontenera keszu - patrz: Beaker
    cache_dir=None
    Katalog z keszem, jeśli cache_type został określony jako file
    cache_url=None,
    URL dla keszowania z użyciem memcache
    default_filters=None
    Lista filtrów, które będą aplikowane do każdego podstawienia
    buffer_filters=[]
    ???
    imports=None
    Lista stringów z importami modułów, które zostaną dodane do skompilowanego szablonu
    input_encoding=None
    domyślne kodowanie wejściowe szablonu - w tym kodowaniu oczekiwane są stringi (mniej więcej równoważne nagłówkowi coding, ale to ostatnie, jeśli jest zdefiniowane w szablonie, nadpisuje input_encoding)
    preprocessor=None
    Funkcja, która przetwarza tekst szablonu przed jego kompilacją.
  • Wywołując TemplateLookup.get_template podajemy uri, czyli ścieżkę do szablonu liczoną od katalogów zdefiniowanych w directories. Ma to pewne implikacje, które opisałem trochę dalej.
  • Metoda render przyjmuje argumenty nazwane, które są przekazywane do szablonu jako zmienne globalne. Czyli wywołanie template.render(test='dupa') spowoduje w renderowanym szablonie


    ${ test }

    wyrenderowanie

    dupa

    Oczywiście ilość parametrów przekazanych w ten sposób jest dowolna.

  • Jeśli chcesz wykorzystać te same szablony w różnych procesach, np. klastrując aplikację, pamiętaj, żeby dla każdego procesu ustawić oddzielny katalog module_dir w instancji TemplateLookup
  • Można podać listę katalogów z szablonami w parametrze directories. Jeśli występuje w nich kilka szablonów o takich samych ścieżkach URI, to pierwszeństwo będzie miał ten, z pierwszej ścieżki podanej w directories
  • Jeśli ustawisz parametr filesystem_checks na True i chcesz używać tego w środowisku produkcyjnym, to polecam utworzyć ramdysk do trzymania skompilowanych szablonów (parametr module_directory)
  • Parametr collection_size warto ustawić na trochę większą wartość, aby skompilowane szablony były pobierane z pamięci.
  • Default_filters to lista domyślnych filtrów (podawana jako stringi, które odpowiadają obiektom callable), które będą aplikowane do każdego podstawienia w kolejności od lewej do prawej.

    Jeśli nie ustawiałeś flagi disable_unicode=False (99% przypadków), i używasz jednego kodowania, to domyślnym filtrem, jeśli tego nie zmienisz, będzie ['unicode']. Możesz jednak zaaplikować filtr 'decode.<nazwa_kodowania_którego_używasz>', np. 'decode.utf-8', który będzie konwertował nieunikodową zawartość do unikodu, zakładając, że jest to string w kodowaniu, które podałeś.
    Decode, to sprytna klasa, dekoduje do unikodu z zadanego kodowania. Jeśli chesz użyć własnych filtrów, to należy je zaimportować w liście podanej przez imports, np:


    TemplateLookup(imports = ['from mojmodul import funkcja_filtra'], default_filters =['funkcja_filtra'])

  • Jeśli używasz dziedziczenia lub inkludowania, a szablony są rozsiane w podkatalogach, używaj bezwzględnych ścieżek (względem zerowego poziomu katalogu) w dyrektywach inherit i include, inaczej plik, do którego chcesz się odwołać, będzie szukany w podkatalogu szablonu, który renderujesz.
  • Mako posiada jedną ciekawą właściwość: pozwala na dynamiczne określanie parametrów dyrektywy include oraz pagepoprzez podstawienie, np:


    <%include file="${ c['zmienna'] }"/>

  • Stringi generowane w blokach kodu <% %> są filtrowane - znaki diakrytyczne są zamieniane do formy \uXXX. Wyprintowanie takiego znaku brzydko wygląda w html'u, dlatego lepiej deklarować stringi jako obiekty unikodowe:


    <%

    napis = 'Zażółć gęślą jaźń' # zostanie skompilowany do 'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144' i tak wyświetlony

    %>

    kontra


    <%

    napis = u'Zażółć gęślą jaźń' # zostanie skompilowany do 'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144', ale wyświetlony z poprawnymi znakami

    %>

    Sytuacja ta dotyczy tylko stringów utworzonych w szablonie. Jeśli przekazujesz string np. z kontrolera, to będzie on nienaruszony.

  • Jeśli chcesz porównywać stringi z diakrytykami, przekazany z kontrolera i zdeklarowany w szablonie, to, że kodowanie stringu to utf-8, możesz użyć konstrukcji u'Zaźółć gęślą jaźń'.encode('utf-8') == napis_z_kontrolera
  • Jeśli importujesz funkcje za pomocą <%namespace/>, które nie są definiowane w innym szablonie Mako, to używając takiej funkcji w podstawieniu, musisz pamiętać, że pierwszym argumentem zawsze będzie obiekt context.
  • Nie mogłem się powstrzymać przed tym punktem: wsparcie społeczności (a przede wszystkim deweloperów) Mako jest rewelacyjne. Jeśli masz jakiś problem z Mako, uderzaj na listę dyskusyjną, zadaj dobre pytanie, a na pewno dostaniesz odpowiedź.