Posty oznaczone etykietą python

Colander i Django: ModelChoice

Budując REST API mogą przydać się walidatory danych wejściowych bardziej elastyczne niż formularze Django. Do realizacji tego celu polecam i używam Colander.

Colander jest świetnym pakietem do (de)serializacji danych. Jest to odpowiednik formularzy Django dla Pyramida, lecz zdecydowanie bardziej elastyczny oraz bez związku HTML. Formularze Django są źle zaprojektowane - zbyt ściśle związane z HTML (widgety), których przecież nie używamy budując REST API, oraz są ograniczone do płaskich struktur. Jedynie jest są dyspozycji formsety, co pozwala zwalidować i zdeserializować listę obiektów jednego typu.

Colander wolny jest od tych wad, a ponieważ nie ma zależności od Pyramid (jest pakietem samodzielnym) to można go użyć z Django bez żadnego problemu. Jednak w niektórych sytuacjach brakuje mi pola typu ModelChoice, co utrudnia fabrykowanie instancji modeli. Z tego powodu zrobiłem poniższy snippet definiujący walidator ModelOneOf oraz klasę węzła ModelChoice. Jest na tyle przydatny, że postanowiłem się z nim podzielić jak najszybciej:

import colander
import types


class ModelOneOf(object):
    def __init__(self, qs):
        self._qs = qs

    def __call__(self, node, value):
        if not self._qs.filter(pk=value).exists():
            raise colander.Invalid(node, '%r is not valid choice' % value)


class ModelChoice(colander.SchemaType):
    def __init__(self, qs, *args, **kw):
        self._qs = qs
        self._model = qs.model
        self._validate = ModelOneOf(self._qs)

        super(ModelChoice, self).__init__(*args, **kw)

    def serialize(self, node, appstruct):
        if appstruct is colander.null:
            return colander.null
        if not isinstance(appstruct, self._model):
            raise colander.Invalid(
                    node, '%r is not a %s' % (appstruct, self._model))
        return appstruct.pk

    def deserialize(self, node, cstruct):
        if cstruct is colander.null:
            return colander.null
        if not isinstance(cstruct, (types.StringType, int)):
            raise colander.Invalid(
                    node, '%r is not a string nor int' % cstruct)

        self._validate(node, cstruct)
        return self._qs.get(pk=cstruct)

Przykładowe użycie (forms.py):

import colander
from .models import MyModel


class MySchema(colander.MappingSchema):
    model = colander.SchemaNode(ModelChoice(MyModel.objects.all()))
In [1]: s = MySchema()
In [2]: s.deserialize({'model': '1'})  # gdzie '1' to wartość PK
Out[2]: {'model': <MyModel pk=1>} 

Teraz zdeserializowane dane wejściowe da się wprost przekazać do fabryk lub konstruktora modeli, np:

instance = MyModel(**s.deserialize(...))

W ten sposób można całkowicie pominąć formularze django.

Django - migracje bazy, które nie zawsze działają

O kiepskim podejściu do migracji schematów baz danych w Django pisałem już w 2014r. Co jakiś czas jednak temat do mnie wraca, gdyż w niektórych projektach używam (niestety) tego rozwiązania. Powody są różne, a główny to "oszczędność czasu". No bo trudno zaprzeczyć, że automatyczne wygenerowanie plików z migracjami jest wolniejsze od klepania XML-i Liquibase lub plain SQL, prawda? Sam z tego przecież korzystam...

Jednak bywają takie momenty, gdzie zostaję z tymi migracjami w "czterech literach", gdzie nawet nie dochodzi ani jeden promyk światła. I taką sytuacją jest m.in. usuwanie atrybutu z modelu, co generuje operację RemoveField.

Bodaj wszystkie wbudowane w Django operacje posiadają mechanikę pozwalającą wycofać daną operację, RemoveField także. Ta akurat nie zadziała, gdy pole było zadeklarowane jako NOT NULL, ponieważ:

  • Django nie ustawia defaultów na poziomie baz danych, więc nigdy nie wygeneruje defaulta na poziomie SQL (co byłoby rozwiązaniem w sytuacjach, gdy default masz określony)
  • Implementacja backward w RemoveField jest oparta o pseudo-automat, tzw. schema editor, a ten "wie" że ma być NOT NULL (i nikt go nie przekona)
  • Sekwencja rollback dla przypadku usuwania pola jest bardziej złożona i zależna od kontekstu (dodanie pola nullable, wypełnienie danymi, zmiana na not null).

Często jest tak, że sekwencja backward (rollback) jest inna od sekwencji forward i nie zawsze każdej operacji forward odpowiada dokładnie jedna operacja backward (i odwrotnie). To skłania mnie do postawienia tezy:

Sekwencje rollback (backward operations) powinny być definiowane niezależne od forward operations, tj. plik migracji powinien mieć dwie oddzielne ścieżki, które mogą być oczywiście automatycznie generowane. 

To kolejny argument za stwierdzeniem, że wbudowane w Django migracje są zaimplementowane w oparciu o wadliwy koncept. Ale jest na to trochę nieczytelne obejście - operacja RunSQL i puste przebiegi forward.

Rozwiązanie tego konkretnego zagadnienia?

Odstawić czarodzieja schema editor, czyli zamienić operację RemoveField na RunSQL usuwającą kolumnę w forwardzie, ale przywracającą w backwardzie z dozwolonym null. Następnie zadeklarować RunSQL uzupełniający dane usuwanej kolumny w backwardzie, a w forwardzie nie robiący nic (''). Tę operację (lub ich sekwencję) należy umieścić przed alterem usuwającym kolumnę, aby w backwardzie wykonała się po przywróceniu tejże, a zakończyć (rozpocząć) od operacji RunSQL zakładającej w backwardzie not null. Partyzanckie, ale działa.

Czyli:

operations = [ 
    migrations.RemoveField('table', 'attribute'),
]

zamieniamy na:

operations = [
     migrations.RunSQL('', 'alter table <table> alter column <column> set not null'),
     migrations.RunSQL('', 'update <table> set <column> = ....'),
     migrations.RunSQL(
         'alter table <table> drop column <column>',
         'alter table <table> add column <column> [...] NULL'),
]

A na przyszłość usuwając pola z modeli należy wygenerować trzy oddzielne migracje:

  • pierwsza ma ustawiać nullable
  • druga ma być data migration, która w forwadzie nie robi nic (lub robi, jeśli dane przenosimy), a w backwardzie ma kod uzupełniający dane
  • trzecia usuwa kolumnę.

Odwracając proces uzyskamy:

  • dodanie kolumny nullable (schema editor będzie wówczas "mądrzejszy")
  • uzupełnienie kolumny danymi
  • założenie not null constraint.

Prosty automat skończony (FSM) w Python

Na GitHub i PyPi wrzuciłem implementację prostego automatu skończonego https://github.com/marcinn/dsm

Instalacja

pip install dsm

Przykład 1: Stany zamówienia w sklepie

Załóżmy, że mamy do oprogramowania automat stanów zamówienia w sklepie:

Deklaracja

Wystarczy zdefiniować przejścia za pomocą listy krotek ([stan], [wartość], [nowy stan]), aby automat spełniał swoją rolę.

Deklaracja automatu z przykładu wygląda następująco:

import dsm

class OrderFSM(dsm.StateMachine):
    class Meta:
        transitions = (
            ('new', 'accept', 'accepted'),
            ('new', 'cancel', 'cancelled'),
            ('accepted', 'mark_ready', 'ready'),
            ('accepted', 'cancel', 'cancelled'),
            ('ready', 'send', 'sent'),
            ('ready', 'cancel', 'cancelled'),
            ('sent', 'finalize', 'finalized'),
        )
        initial = 'new'

Symulacja

Kod symulacji:

fsm = OrderFSM()

print 'State: `%s`, trying to accept' % fsm.state

fsm.process('accept')
print 'State: `%s`' % fsm.state

print "Can I send order now? - %s." % ('Yes' if fsm.can('send') else 'No')
print 'Marking as ready.'
fsm.process('mark_ready')

print "Can I send order now? - %s." % ('Yes' if fsm.can('send') else 'No')

fsm.process('send')
print "Order is %s" % fsm.state

print "Trying to cancel order..."
fsm.process('cancel')

Wynik:

State: `new`, trying to accept
State: `accepted`
Can I send order now? - No.
Marking as ready.
Can I send order now? - Yes.
Order is sent
Trying to cancel order...
Traceback (most recent call last):
  File "bin/python", line 78, in <module>
    exec(compile(__file__f.read(), __file__, "exec"))
  File "dsmtest.py", line 39, in <module>
    fsm.process('cancel')
  File "/home/marcin/src/projekty/archikat/eggs/dsm-0.2-py2.7.egg/dsm.py", line 97, in process
    new_state = self._transitions.execute(value, self.state)
  File "/home/marcin/src/projekty/archikat/eggs/dsm-0.2-py2.7.egg/dsm.py", line 46, in execute
    raise UnknownTransition('Can not find transition for `%s` in state `%s`' % (value, current_state))
dsm.UnknownTransition: Can not find transition for `cancel` in state `sent`

Użycie z Django

Moduł nie ma żadnych zależności od Django, ale można go łatwo zintegrować z modelem. Na przykład tak:

class Order(models.Model):
    status = models.CharField(max_length=32)

    def change_status(self, operation):
        # process() zwraca nowy stan
        # nowa instancja OrderFSM() zapewnia prawidłowy stan początkowy
        self.status = OrderFSM(initial=self.status).process(operation) 

Wywołanie zmiany stanu zamówienia może być następujące:

order = Order.objects.get(pk=666)

try:
    order.change_status('send')
except OrderFSM.UnknownTransition:
    print "Buuu... :("

Inne zastosowania

W module dsm.py zawarłem przykład automatu sumującego wprowadzane na wejściu cyfry jako przykład użycia w innych celach niż tylko manipulacja stanami jakichś tam dokumentów. DSM emituje zdarzenia i dzięki możliwości rejestrowania callbacków można zbudować coś znacznie ciekawszego.

Przykład 2: Parzysta ilość zer w ciągu

Jako kolejny przykład zaimplementowałem automat sprawdzający, czy liczność zer w ciągu wejściowym jest parzysta, opisany na Wikipedii.

import dsm

class ParityChecker(dsm.StateMachine):
    class Meta:
        transitions = (
                ('even', '0', 'odd'),
                ('even', '1', 'even'),
                ('odd', '1', 'odd'),
                ('odd', '0', 'even'),
            )
        initial = 'even'


pc = ParityChecker()


while True:
    pc.reset()

    digits = raw_input('Podaj ciag zlozony z zer i jedynek: ')

    if not digits:
        break

    pc.process_many(digits)

    print "%sparzysta ilos zer (%s)" % ('Nie' if pc.state=='odd' else '',
            digits.count('0'))

Django i None/null w BooleanField (forms)

Dostałem kolejnym issue związany z "nietypowym" zastosowaniem Django, a typowym w prawdziwym świecie.

Powszechnie wiadomo, że formularze w Django służą do walidacji danych. Niestety błąd projektowy Django polega na tym, że formularze są ściśle związane z danymi wejściowymi pochodzącymi z HTML, a konkretnie związane są widgetami HTML.

Formularze można wykorzystać do walidacji jakichkolwiek wejściowych danych (np. przy imporcie z CSV), przełykając niepotrzebne widgety HTML oraz godząc się na komunikaty o błędach walidacji formatowane do HTML (co jest także swoistym nonsensem, ale od v1.7 będzie już możliwość zwrócenia jako dict lub JSON). Jednak kiedy spróbujecie zrobić API i formularz walidujący dane wejściowe z requestu z użyciem NullBooleanField, to się niemiło zaskoczycie.

Praktyka, czyli rozbicie się o mur

Potrzebujemy filtrowania zasobu restful API i parametry przkazywane przez GET mają charakter opcjonalny. W przypadku parametrów typu bool potrzebujemy rozróżnienia trzech stanów:

  • nie użyto filtra
  • filtr dla wartości True - cleaning z [1,'1','true','True'], jak w to_python()
  • filtr dla wartości False

Dobrym zwyczajem walidację filtra przepuszczamy przez formularz. Podczas definicji formularza mamy dwie możliwości:

  • forms.BooleanField, który mimo required=False defaultowo zwraca zawsze False, zatem odpada
  • forms.NullBooleanField, który zezwala na None

Niestety żadne z powyższych nie działa dla wartości wejściowych ze zbioru [1,'1','True','true']. Pierwsze wiadomo dlaczego: z definicji nie przyjmuje None, więc mamy False by default. Ale dlaczego nie działa NullBooleanField? Bo NullBoolenField w cleaningu odwołuje się do parsowania danych za pomocą... widgetu NullBooleanSelect, którego implementacja wygląda tak:

def value_from_datadict(self, data, files, name):
    value = data.get(name, None)
    return {u'2': True,
            True: True,
            'True': True,
            u'3': False,
            'False': False,
            False: False}.get(value, None)

Niech was gęś kopnie, cudaczni developerzy Django! Nie wszędzie używa się wejścia z waszych wbudowanych widgetów HTML! Dlaczego widget HTML jest domyślny i używany nawet przy wejściu nie pochodzącym z tego widgetu? Nie wiedzieć... Może to Django ethos.

Obejście problemu

class MyNullBooleanSelectWidget(forms.NullBooleanSelect):
    def __init__(self, *args, **kw):
        super(MyNullBooleanSelectWidget, self).__init__(*args, **kw)
        self.choices = ((u'', _('Unknown')),
                   (u'1', _('Yes')),
                   (u'0', _('No')))

    def render(self, *args, **kw):
        """
        tu mozna sobie zaimplementowac rendering w razie potrzeby,
        bo Django developers zlamali DRY i nie uzywaja choices 
        w renderingu; (kolejny klops na kolejny wpis)
        """
        raise NotImplementedError("Not used")

    def value_from_datadict(self, data, files, name):
        value = data.get(name, None)
        if value is None or value=='':
            return None
        if value in ('True', 'true', '1', 1):
            return True
        return False


class MyNullBooleanField(forms.NullBooleanField):
    widget = MyNullBooleanSelectWidget

W formsach API używamy MyNullBooleanField zamiast forms.NullBooleanField. A dla profesjonalnych zastosowań w kontekście budowania API polecam Colander i oczywiście Pyramid.

Konkluzja

Nie pchajcie się ludzie w Django, jeśli nie robicie kolejnego bloga albo frontendu dla jakiegoś portalu.

Opisane zachowanie oparłem o Django v1.4, ale w źródłach master widzę ten sam gnój.

RQ - Alternatywa dla Celery Task Queue?

Celery

Celery był obiecującym pakietem umożliwiającym kolejkowanie i asynchroniczne wykonywanie zadań, oraz w pewnym stopniu rozproszenie systemu. Kiedyś udało mi się nawet pracować z w miarę stabilną wersją, lecz wskutek błędu związanego z task.retry zostałem zmuszony poszukać rozwiązania w upgrade pakietu. Obecnie major release Celery nosi numer 3 i niestety niewiele się zmieniło w kwestii stabilności.

Autor pakietu, Ask Solem, ma łeb na karku ale wziął sobie na niego za dużo projektów. Mam wrażenie, że facet już nad tym nie panuje. Celery opiera się na jego dwóch innych produktach: Kombu i Billiard. Kombu zastępuje kilka starszych pakietów (również jego autorstwa). Billiard zastępuje standardowy multiprocessing (autor chciałby, aby kiedyś znalazł się w dystrybucji Pythona). Niestety Billiard również jest delikatnie zmaszczony.

Za tymi pakietami głównie włóczy się smród Django, choć Ask Solem stara się od dawna pozbyć tejże zależności. Włóczy się też atmosfera niestabilności mimo zaklęć w setup.py.

Główne wady Celery:

  • brak stabilności mimo intensywnego rozwoju, nieoczekiwane zachowanie podczas pracy
  • przeładowanie features, z których podstawowe nadal nie działają stabilnie
  • morderstwo standardowego loggera
  • zbyt dużo punktów podatnych na błędy i awarie (celery -> kombu -> billiard -> amqplib -> erlang -> rabbitmq)
  • zależności z django na każdym kroku
  • trudność w debugowaniu
  • brak możlwości samodzielnej poprawy przez zbyt skomplikowaną budowę pakietu.

RQ

RQ jest prostą implementacją kolejkowania zadań dla Pythona. Autor Vincent Driessen postawił na prostotę i czas wdrożenia. Przede wszystkim nie ma zależności od Django, choć istnieje pakiet ułatwiający taką integrację. Widać tu wyraźnie, jak wyglądał proces tworzenia pakietu. Nie ma mowy o zaszłościach i nieuzasadnionych zależnościach.

Zasada działania RQ jest prostsza od Celery, a punktów podatnych na wywalenie jest o wiele mniej - właściwie sam RQ albo Redis. Uruchomienie workerów pod kontrolą supervisord jest dziecinnie proste. Wielki plus dla łatwej integracji z Sentry.

Funkcje uruchamiane przez workery nie muszą być opakowywane żadnymi dekoratorami. Są zwykłymi funkcjami, którym worker przekaże argumenty. Jedyny warunek to konieczność umieszczenia funkcji w module (nie da się zdefiniować funkcji w __main__, albo użyć jakiegoś builtina), ale to ma marginalne znaczenie. Na koniec warto zauważyć, że pakiet nie ma idiotycznej nazwy - spróbujcie poszukać w Google czegoś o Celery.

Wady i zalety

Wady:

  • Python only
  • instancja Redis per projekt
  • rozpraszanie workerów za pomocą dostawiania instancji Redisa
  • brak wbudowanego mechanizmu task.retry
  • wolniejsze uruchamianie zadań (process fork)

Zalety:

  • szybkość i prostota wdrożenia
  • podobne high-level API do Celery, które pozwala na łatwą migrację
  • mało potencjalnych miejsc awarii
  • odporność na wycieki pamięci tasków

Co boli MVC

Prowadziliśmy swego czasu dyskusje o logice biznesowej i szukaliśmy rozwiązania starając się znaleźć jej miejsce w MVC. Stosując bowiem ten paradygmat logika naturalnie "rozmywa się" po kontrolerach i modelach (ekstremiści wstawiają ją nawet do widoków). Typowy przykład złego podejścia to kiepskie implementacje ORM (Propel-PHP, DjangoORM - Python):

class User(Model):
   def save(..):
      ...

W tym przypadku Model jest jednocześnie persistance managerem i wykonuje operacje, których nie powinien. Prawidłowe podejście to

class UserManager:
   def save(self, user):
       ....

Który kod łatwiej przetestować? Gdzie znajduje się logika zapisu stanu encji?

Inny przykład:

class Product(Model):
    def calculate_tax(self, tax):
       ...

vs

class TaxCalculator:
    def calculate_tax(self, product):
       ...

Jak widać w drugim podejściu to TaxCalculator zawiera logikę obliczania podatku dla produktu. Możemy ją łatwiej przetestować (bez uruchamiania całego środowiska i persistance) oraz możemy ją wymieniać wprowadzając różne kalkulatory, np. USATaxCalculator, VATTaxCalculator, etc.

Frameworki narzucają konwencje, ale nie ograniczają aż tak naszych działań. Dlatego w moich projektach, ostatnio opartych o Django, stosuję minimalny kod kontrolerów, operacje zamykam w formularzach (mimo, że nie są renderowane) lub funkcjach/metodach wykonujących proces na rzecz jakiegoś obiektu. W ten sposób modele są pozbawione metod będących operacjami na ich samych. Taki kod jest przede wszystkim łatwiej testowalny (mowa o unit tests), oraz nie wymaga uruchamiania i konfigurowania całego środowiska, co czasem bywa problematyczne i jest wolniejsze.

Jeśli już mowa o Django, to wyraźnie widać, gdzie popełniono błędy projektowe. Model już na wstępie posiada dwie operacje, których nie powinien mieć - model.save() i model.delete(). Zapis i odczyt stanu oraz usuwanie modelu powinny być wykonywane przez obiekt zewnętrzny (konkretnie persistence manager). Z kolei formularze są ściśle związane z widgetami HTML, co przy stosowaniu ich jako obiektów utility (np. do walidacji danych) pociąga za sobą niechciane zależności.

Warto zastosować się do kilku reguł:

  • ograniczyć ciało kontrolerów do wywoływania operacji sprowadzając je do roli przetwarzania żądań i generowania odpowiedzi; nie implementować w nich logiki biznesowej (tj. use cases/sekwencji)
  • każdy use case powinien mieć swoją implementację w oddzielnym bycie, co wyraźnie odseparuje warstwę logiki biznesowej
  • nie implementować operacji w klasach modeli, które zawierają logikę biznesową; tj. ograniczać do operacji modyfikujących lub odczytujących stan (w znakomitej większości pozostaną akcesory i mutatory lub po prostu properties)

Ciekawe artykuły:

Brak wyjątku przy błędach Django compilemessages [v1.4]

Zauważyłem, że compilemessages nie rzuca wyjątków przy błędach msgfmt. Jest to typowe wyciszenie błędu przez brak sprawdzania returncode. To praktyka naruszająca Zen of Python, tj. regułę "Errors should never pass silently".

Przed zgłoszeniem ticketa warto sprawdzić, czy ktoś już tego nie raportował. Okazuje się, że zgłoszenie już jest od dwóch miesięcy, ale uwaga! Jest już poprawka zamieniającą os.system() na subprocess.Popen()!

W przeciwieństwie do ticketów otwartych od wielu lat to na prawdę szybka reakcja. Jestem mile zaskoczony!

https://code.djangoproject.com/ticket/19584

Django NoDB Test Runner - czyli czuję zapach świeżej mogiły

A tak se wstawiłem, a co...

Developerzy Django swego czasu nie potrafili wyobrazić sobie, że może istnieć w naszym niemałym programistycznym świecie potrzeba odpalania testów bez bazy (baz) danych. Minęły trzy lata i zaserwowali nam w końcu (1.2) możliwość zdefiniowania własnego test runnera w prosty sposób (class based).

Prymitywny test runner bez bazy danych:

class NoDbTestRunner(DjangoTestSuiteRunner):
  """ A test runner to test without database creation """

  def setup_databases(self, **kwargs):
    """ Override the database creation defined in parent class """
    pass

  def teardown_databases(self, old_config, **kwargs):
    """ Override the database teardown defined in parent class """
    pass

który podpina się w settings równie trywialnie:

TEST_RUNNER = 'testrunner.NoDbTestRunner'

Pal sześć, że potrzebuję suite do testów z bazą równocześnie z suite bez niej, i żeby to osiągnąć będę musiał się jeszcze sporo napocić dorabiając najlepiej wykrywanie potrzeby użycia testowych baz. Najgorsze jednak w tym wszystkim jest to, że połączenia testowe tworzone są przez defaultowy test runner, który właśnie wywaliliśmy w kosmos. Jeśli myślicie, że brak jego odpalenia będzie skutkował brakiem zdefiniowanych połączeń i wyjątkami, to jesteście w błędzie. Oryginalny test runner ZAMIENIA parametry definicji ISTNIEJĄCYCH połączeń.

Skutkiem tego jest uruchamianie testów na ustawieniach defaultowych. A czy wiecie co robi Django przy wczytwaniu fixturek do testów? Flush DB. Dziękuję Ci, Django. Mam nadzieję, że teraz nie odpalicie NoDbTests na swoich serwerach live.

Fatalny ORM i, jak się okazuje, fatalne rozwiązanie frameworka testowego w Django zaczynają przekonywać mnie do skierowania się w stronę innego produktu, szczególnie gdy realizuje się systemy większe niż "przeciętny blogasek".

Zmarł John Hunter, twórca matplotlib

John Hunter, twórca bezpłatnej i wyśmienitej biblioteki matplotlib, zmarł na raka 28 sierpnia 2012r w wieku 44 lat. Przez wiele lat aktywnie wspierał społeczność związaną z Pythonem.

Utworzono fundusz, z którego środki będą przeznaczone na pomoc jego rodzinie, głównie na edukację jego dzieci. Każdy, kto docenia jego pracę i chciałby mu podziękować za jego wkład w naszą społeczność, może wpłacić dowolną sumę za pośrednictwem PayPal: http://numfocus.org/johnhunter/

Django Page CMS

Jest taki projekt, jak w tytule. Linkował nie będę, radzę tylko omijać.

  • niedeterministyczna identyfikacja stron wg slug ("item1/item2" jest tożsamy z "item1/costam/gdziestam/item2")
  • brak szablonów w BD (jedynie konfigurowane w settings; ich zmiana nie powoduje błędu tylko problemy wyświetlania)
  • nieoptymalne generowanie menu
  • publikacja strony odbywa się przez ustawienie wartości Published na "in navigation" lub "hidden" (sic!)
  • problemy z cache (źle użyty mechanizm powoduje problemy przy backendzie innym niż memcached)
  • parszywa implementacja wielojęzykowości
  • ograniczony panel admina przez przesłonięte szablony dla changelist_view (sztywne kolumny, brak features, etc)
  • wyświetlanie stron cms zawsze gdy 404 domyślna trasa do wyświetlenia strony z regexp .* (w połączeniu z błędem rozpoznawania po slug powoduje niesamowite wrażenia)

Lepiej od razu iść w kierunku https://www.django-cms.org/ albo własnej implementacji pod konkretny cel.

Vim jako IDE dla programisty Python

Tak kiedyś było

Używałem wielu edytorów programisty. Zaczynałem od dostarczanych wraz z językiem programowania (Amos, BlitzBasic), używałem intensywnie CED-a, później Notatnika, przez jakieś Notatniki+. Był też krótki romans z Pajączkiem ze względu na wsparcie HTML, później używałem Quanty i edytora z Midnight Commandera. Kilka lat temu zachwyciło mnie IDE Microsoft Visual Studio (programowałem nieco w .NET/C#, świetna technologia). Szukałem później pocieszenia instalując Eclipse.

Pewnego spokojnego dnia, gdzieś między zaznaczeniem obszaru tekstu myszą i wciśnięciem Ctrl+C i Ctrl+V zwróciłem uwagę, jak Onjin pisze kod. W zasadzie to nie wyglądało jakby pisał, tylko bezpośrednio przelewał myśli w źródła, wskazując jedynie miejsca docelowe opuszkami z prędkością chyba 200 uderzeń na sekundę.

Dziś jest Vim

Poczułem moc Vim-a. Dziś to moje podstawowe narzędzie pracy. Zasadniczo Vim z kolorowaniem składni Pythona i ewentualnie ctags wystarcza. Jednak postanowiłem trochę rozbudować moje IDE:

  1. TagList

    Plugin, który wyświetla strukturę kodu źródłowego.

    Zrzut ekranu z pluginem TagList w akcji

    Instalacja:

    Otwarcie okna z listą: :Tlist

    Podstawowe skróty:

    `p`: skok do definicji klasy/funkcji
    `o`: otwarcie definicji w nowym oknie (split)
    `F1`: pomoc (lista skrótów)
    `/`: standardowe wyszukiwanie
    
  2. TaskList

    Plugin wyświetla listę zadań do wykonania w otwartym pliku. Działa podobnie jak zakładki TO-DO z innych IDE. Odnajduje komentarze z adnotacjami: TODO, FIXME, XXX. Listę tokenów może przekonfigurować (dokładny opis konfiguracji znajduje się w skrypcie).

    Screenshot przedstawiający działanie pluginu TaskList

    Do pobrania: http://www.vim.org/scripts/script.php?script_id=2607

    Instalacja:

    • skopiować tasklist.vim do katalogu plugin (u mnie ~/.vim/plugin/)
  3. Rope - wsparcie refaktoryzacji

    Ciekawie zapowiadający się pakiet napisany w Pythonie wspomagający zadania refaktoryzacji kodu. Jestem na etapie zaznajamiania się z możliwościami pakietu, aczkolwiek muszę przyznać, że zmiana nazw klas czy modułów sprawdza się rewelacyjnie.

    Do Vim-a istnieje adekwatny plugin (wymaga skompilowanego Vim-a z obsługą Pythona):

  4. Pyflakes

    Sprawdzanie poprawności kodu "w locie". Skrypt jest bardzo wydajny, bo uruchamia się podczas wyjścia z trybu insert.

    Pyflakes skutecznie powiadamia o błędach takich, jak:

    • redeklaracja zmiennych, które nie były używane,
    • brak importów,
    • nieużywane importy i zmienne,
    • użycie zmiennej przed jej definicją,
    • podwójne zdefiniowanie funkcji,
    • błędy składni,
    • itp..

    Więcej informacji: http://www.divmod.org/trac/wiki/DivmodPyflakes

    Skrypt Vim: http://www.vim.org/scripts/script.php?script_id=2441

    pyflakes.vim działa tylko z forkiem dostarczanym w paczce. Niestety rozpada się z wersją oficjalną. Jest to dość duży problem.

  5. SnipMate - snippety rodem z TextMate

    Zaawansowany pakiet skryptów ułatwiających pisanie kodu za pomocą tzw. snippetów. Snippety uaktywnia się wpisując odpowiednią frazę i klepiąc tabulator.

    snipMate.vim Introductory Screencast from Michael Sanders on Vimeo.

    Przydatne snippety dla Pythona:

    • docs: docstring dla modułu
    • for: pętla for
    • while: pętla while
    • def: definicja funkcji
    • cl: definicja klasy
    • defs: definicja metody
    • try: różne rodzaje bloku try-except (do wyboru)
    • ifmain: oj, złoty snippet używany do krótkich testów :)
  6. VimPDB - debugger Pythona w Vim

    Używa się analogicznie jak ipdb czy pdb. W kodzie źródłowym umieszczamy wywołanie:

      import vimpdb
      vimpdb.set_trace()
    

    Przed uruchomieniem skryptu do debugu należy odpalić sesję vim`a:

      vim --severname VIMPDB
    

    Interfejs VimPDB nie jest tak wygodny jak winpdb. Ja i tak najczęściej debuguje używając pdb/ipdb.

  7. Omnicompletion

    Znakomicie sprawdza się w przypadku pakietów zainstalowanych w systemie. Omnicompletion to inna nazwa wygodnego mechanizmu podpowiadania "intellisense".

    Skrót Ctrl+X Ctrl+O podpowiada metody, moduły do importu, nazwy klas. Ustawiając set completeopt=preview mamy na bieżąco wgląd w docstring dopełnianego elementu.

    Dla Pythona wystarczy ściągnąć skrypt Pythoncomplete i skopiować go do ~/.vim/autoload.

    W .vimrc ustawiamy:

     autocmd FileType python set omnifunc=pythoncomplete#Complete
     set completeopt=preview,longest,menu
     set completefunc=pythoncomplete#Complete
    

    (Nie)stety Używam buildout i omnicompletion nie działa w pełni z powodu nie ustawionego prawidłowo PYTHONPATH. Na ten czas nie mam rozwiązania tego problemu. Stąd nie używam omnicomplete za często i nie jest mi do życia potrzebne.

  8. Pylint

    Linter dla Pythona. Z jego zalet chyba wystarczy podać najważniejsze:

    • Pylint sprawdza zgodność z PEP,
    • ocenia kod i porównuje wynik z poprzednim.

    Jest trochę upierdliwy, więc nie uruchamiam go automatycznie (można go skonfigurować, aby nie uruchamiał się po każdym zapisie). Najczęściej uruchamiam go przed deploymentem wersji lub przed wysłaniem zmian do publicznego repozytorium.

    Źródła:

    Instalacja:

    • easy_install pylint
    • skrypt pylint.vim skopiować do ~/.vim/compiler/
    • dodać do .vimrc:

      autocmd FileType python compiler pylint

Rady

  1. Katalog ~/.vim dobrze jest wersjonować:

    cd ~/.vim
    git init
    git add .
    git commit -am 'moj dobry config'
    

    Jeśli plugin okaże się nieciekawy lub błędny wystarczy wycofać się do poprzednich rewizji lub po prostu użyć git clean -f.

  2. Przeładowanie Vim-a pluginami może go dość spowolnić.

Czy czegoś więcej potrzeba? Chyba tylko lepszej integracji z projektami opartymi na buildout. Będzie to temat na kolejnego posta.

PyCon 2010, 8-10 października

Prelekcje:

  • Aplikacje internetowe w czasie rzeczywistym w Pythonie
  • Optymalizacja i profiling metodami chałupniczymi
  • 3 kilo przyjemności
  • Python w laboratorium fizycznym
  • SymPy, czyli matematyka w Pythonie
  • Kup Pan cegłę, czyli wstęp do algorytmów rekomendacyjnych
  • Realizacja zadań administracyjnych za pomocą języka Python
  • Programowanie GPU z wykorzystaniem PyCUDA i PyOpenCL
  • PyPy czyli jak uczynić Pythona szybszym

Więcej informacji na http://pl.pycon.org/2010/agenda

Do zobaczenia na PyCon 2010!

Źródło: http://pl.pycon.org/2010/

Particle Swarm Optimization

Ostatnio interesuję się tematyką algorytmów metaheurystycznych. Ze znajomym pracujemy nad implementacją algorytmu zainspirowanego współpracą stadną nietoperzy (Swarm Intelligence, PSO) z użyciem mechanizmu echolokacji.

Drążąc temat znalazłem ciekawy i prosty przykład PSO zaimplementowanego w Pythonie na stronie francuskiego inżyniera Maxime Biais.

Implementacja wzorowana zachowaniami nietoperzy nieco się różni. Jako, że niewiele jest jeszcze opracowań w tym temacie, opiszę te zagadnienia szerzej na blogu w niedługim czasie.

Symfony forms vs Django forms

Mam okazję pracować z obydwoma frameworkami i mogę je porównać w praktyce. Django zacząłem używać jakieś dwa lata temu, a Symfony nieco wcześniej (od wydania stabilnej wersji 1.0).

Ostatnimi czasy, z braku możliwości upgrade Symfony w projekcie, przeportowałem mechanizm formularzy z wersji 1.1 do 1.0.20.

Piersze wrażenie

Mechanika formularzy w Symfony mocno przypomina newforms z Django. Powszechnie wiadomo, że Fabien jest fanem Django, więc nie zdziwiło mnie zbyt specjalnie, że wzorował się właśnie na nim.

Mamy do dyspozycji podstawową klasę formularza sfForm (django.forms.Form), oraz klasę dedykowaną do modelu Propela sfPropelForm (django.forms.ModelForm).

Jest do dyspozycji zbiór widgetów w klasach sfWidgetForm (django.forms.widgets) do renderingu pól oraz zestaw walidatorów w klasach sfValidator (w Django sprawdzanie poprawności danych jest rozwiązane przez wywoływanie metod clean_FIELDNAME() formularza, o ile zostały zdefiniowane, oraz metody clean() każdego pola). W Symfony jest możliwość napisania własnych specyficznych widgetów i walidatorów.

Używanie formularzy jest podobne - w widoku (sf: akcji), najczęściej przy requescie wysłanym POST-em instancjonujemy formularz i wypelniamy go danymi z requestu, sprawdzamy czy jest prawidlowy (sfForm::is_valid()). W przypadku sfPropelForm (ModelForm) po walidacji wywołujemy save().

Definiowanie formularzy

W Django używamy class properties i zagnieżdżonej klasy Meta.

class MyForm(forms.Form):
  name = forms.CharField()
  birthdate = formd.DateField()

  class Meta:
    exclude = ('birthdate',)

W Symfony formularz konfigurowany jest w metodzie sfForm::configure(), która uruchamiana jest domyslnie w konstruktorze.

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));
  }
}

Ops, ale.. jak wykluczyć datę urodzin?

Google dają odpowiedź:

  • Q: How to exclude a field?
  • A: You have to remove the widget and the validator.

W Symfony zwykle kończy się to na klasie pochodnej, przeciążeniu configure() z serią unset na widgetSchema i validators. Możliwe jest (wszystko jest możlwie do realizacji, ale z różną efektywnością i poziomem trudności), ale trochę uciążliwe.

Django-Symfony 1:0

Uważny czytelnik zwróci uwagę, że w Symfony nie definiujemy pół formularza (sic!) tylko oddzielnie widgety i validatory. Mimo, że istnieje klasa sfFormField, jest ona używana już po bindingu (zawiera w sobie wartość pola - w Django jest BoundField, które opakowuje Field i dostarcza wartość).

Jaka jest zatem kolejna wada formularzy Symfony? Nie ma zdefiniowanych klas pól formularzy, które naturalnie łączą widgety i walidaję danych, np. CharField związane jest domyślnie z widgetem TextInput i posiada domyślną walidację w metodzie CharField.clean(). W Symfony trzeba dodać walidację samodzielnie:

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));

    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        ));
  }
}

Walidacja pola jest zwykle specyficzna. W Symfony trzeba napisać dedykowaną klasę walidatora. W Django implementujemy metodę clean_FIELDNAME w klasie formularza. Można też przygotować dedykowaną klasę Field.

Django-Symfony 2:0

I18n (czyli formularze po polsku)

W Django etykiety i komunikaty walidacji muszą być opakowane w wywołanie funkcji gettext. W komunikatach podstawowych pól używany jest ugettext_lazy. Etykiety musimy zdefiniować ręcznie (za pomocą argumentu label przy definiowaniu pola formularza).

class MyForm(forms.Form):
  name = forms.CharField(label=_('Name'))
  birthdate = formd.DateField(label=_('Birth date'))

  class Meta:
    exclude = ('birthdate',)

Po stronie Symfony zrealizowano to nieco inaczej - przez nastawienie funkcji callbacka do funkcji translate:

  protected function configure() {
      $this->widgetSchema->getFormFormatter()->setTranslationCallable(
          array(sfContext::getInstance()->getI18N(), '__'));
      /* ... */
  }

Pełny kod formularza w Symfony:

class MyForm extends sfForm
{
  protected function configure() {

    $this->widgetSchema->setNameFormat('my_form[%s]');
    $this->widgetSchema->getFormFormatter()->setTranslationCallable(
        array(sfContext::getInstance()->getI18N(), '__'));

    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));

    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        'birthdate' => new sfValidatorDate(array('required'=>true)),
        ));
  }
}

Pamiętacie jeszcze, że w Django ten sam opis formularza składa się tylko 5 linii kodu? :)

Mimo innego sposobu używania gettext(), to zarówno Django, jak i w Symfony dają radę. Remis.

Django-Symfony 3:1

Domyślne dane (initial parameters)

Django:

  data = {'name': 'Stranger',}
  form = MyForm(initial=data)

Symfony:

  $form = new MyForm();
  $form->setDefaults(array('name'=>'Stranger'));

Praktycznie to samo. Jednak zauważyłem, że pomimo nastawienia wartości domyślnych w formularzu sfPropelForm, pusta wartość pola modelu napisuje to, co przekazujemy w defaults. Jest to dość duży problem, ale może dotyczyć tylko wersji formularzy z Symfony 1.1. Remis.

Django-Symfony 4:2

Binding

Formularze Django w konstruktorze przyjmują parametr data, do którego zwykle przekazuje się request.POST. Analogicznie jest z przesłanymi plikami - request.FILES. Przykład:

def my_form(request):
  if request.method == 'POST':
      form = MyForm(request.POST, request.FILES)
      if form.is_valid():
          model = form.save()
          return redirect(model)

W Symfony jest dość podobnie, aczkolwiek binding należy wykonać oddzielną metodą bind():

public function executeMy_form() {
  $form = new MyForm();
  if($this->getRequest()->getMethod() == sfWebRequest::POST) {
      $form->bind($this->getRequestParameter('my_form'),
        $this->getRequest()->getFiles('my_form'));
      if($form->isValid()) {
          $form->save();
          $this->redirect('/somewhere/');
      }
   }
}

Należy też pamiętać o użyciu prawidłowej zmiennej z requestu - w klasie formularza definiujemy name format (setNameFormat) i te obydwie części muszą się zgadzać. W ten sposób złamano DRY i nie jestem, czy w Symfony da się ten problem jakoś ominąć. Remis.

Django-Symfony 5:3

Częste problemy/tips

  • Własny rendering formularzy. W Symfony musicie pamiętać o jawnym używaniu gettext(), jeśli nie używacie metod render*() pól/formularza.

  • Dostosowywanie formularzy do potrzeb projektu. Django daje duże możliwości dzięki przestrzeniom nazw Pythona i konfiguracji urls/settings. Zwykle w projekcie dodaje się dedykowaną aplikację z modułem zawierającym dostosowane formularze i najczęściej zmienia się konfigurację urls (w reusable apps mamy zwykle do dyspozycji parametr form_class w widokach). Symfony, przynajmniej w wersji 1.0.X, nie daje mechanizmu zbliżonego do urls, chociaż można użyć factories (http://www.symfony-project.org/book/1_0/17-Extending-Symfony#chapter_17_factories) albo innych rozwiązań (ja stosowałem własny kontener IoC). Warto o tym pamiętać, bo we wdrożeniach bardzo często dostosowuje się właśnie formularze.

  • Odpowiednik klasy ModelForm w Symfony nie potrafi "w locie" zbudować definicji formularza. Bazowe klasy dla poszczególnych modeli muszą być wygenerowane (sic!). Zatem użyteczność sfPropelForm bez wygenerowanych bazowych klas jest niewielka i ogranicza się do zaimplementowanej metody save().

Fantazje Fabiena

Na koniec, jako ciekawostkę, dodam krótkie podsumowanie tego, co Fabien zaimplementował będąc albo przemęczonym, albo na kacu ;)

  • schemat widgetów/validatorów (kontener/dict) dziedziczy bezpośrednio po klasie Widget/Validator (znakomity przykład do czego nie używać dziedziczenia)

  • forms embedding (formularze w formularzach) - przyznam, że nie mogę tego pojąć. Nie mamy formsetow, ale możemy zagnieżdżać formularze w sobie.

  • form merging (łączenie formularzy) - nie widze zastosowania (formularz hermetyzuje logikę, sprawdzanie poprawności i sposób prezentacji - po co łączyć, użyjmy kilku formularzy lub Formsetu)

  • I18N w kontekście translacji (gettext) - zahermetyzowane wewnątrz formularza, lecz z możliwością wymiany callable na dowolny; trudne/każdorazowe definiowanie domyślnego callable; gettext powinien być używany, nie zawarty wewnątrz klasy formularza

Podsumowanie

Formularze w Symfony są, a to już lepsze niż nic, które było w wersji 1.0.X. Ostateczny wynik rozgrywki Django-Symfony 5:3. Jeśli ktoś nie używał formularzy w Symfony, to gorąco polecam. Jeśli ktoś ma dylemat co wybrać, polecam Django z racji szerszych możliwości i skrócenia czasu realizacji zadań.

Zmiana silnika bloga

Nadszedł ten czas. Rezygnuję z Bloggera na rzecz własnej strony domowej, dla realizacji której Blogger nie wystarcza.

Postanowiłem użyć aplikacji django-simpler-blog i rozszerzyć ją o brakujące funkcjonalności. Dzięki temu zabiegowi projekt mojej strony domowej jest łatwiejszy do realizacji, a swoją drogą mam wpływ na każdy detal. Ponad to pisanie w edytorze WYSIWYG lub bezopośrednio HTML nie jest sympatyczne. Wolę zdecydowanie Markdown.

Do realizacji tego bloga użyłem również:

  • django.contrib.comments
  • django-tagging