Python: wyjątkowo głupi pomysł

Nigdy nie rób­cie tak:

try:
    # ...
except FooException, e:
    # ...
    raise e

Wyjątki prze­pusz­cza się tak:

try:
    # ...
except FooException:
    # ...
    raise

Istotna róż­nica polega na nie­znisz­cze­niu całego stosu wywo­łań. Z góry dziękuję.

Faktury przez API

Odkąd roz­po­czą­łem współ­pracę z Miru­mee, w listach od moich czy­tel­ni­czek wciąż powta­rzało się jedno pyta­nie — kiedy? W końcu zro­bi­łem jed­nak te bada­nia, a blog to nie miej­sce na dys­ku­sje o moim życiu łóżkowym, dla­tego dziś pomó­wimy o Cen­trum­Fak­tur.

Co wybrał­byś zatem, gdyby zaofe­ro­wano ci body shot z ciała dowol­nie wska­za­nej przez sie­bie kobiety¹ lub nie­skrę­po­wany dostęp do API pozwa­la­ją­cego szybko, łatwo i bez­piecz­nie wysta­wiać fak­tury z poziomu two­jego programu?

¹ Chcia­łem tu napi­sać, że panie mogą sobie wybrać męż­czy­znę, ale w ten spo­sób zmniej­szył­bym szanse na to, że kto­kol­wiek wybie­rze API.

Mamy cho­lerną nadzieję, że nie lubisz tequ­ili, bo w stwo­rze­nie tego API zain­we­sto­wa­li­śmy już sporo czasu. Nie owi­ja­jąc w bawełnę, prze­cho­dząc do meri­tum, bez ogró­dek i zbęd­nych cere­gieli, waląc pro­sto z mostu i nie lejąc wody przed­sta­wiam doku­men­ta­cję API Cen­trum­Fak­tur.

Zdra­dzę przy oka­zji, że odno­śni­kiem tym dzielę się z wami nie­przy­pad­kowo. Wiem bowiem, że prze­czy­ta­cie, bez wyjątku, wszystko, co tylko napi­szę. I będzie się wam podo­bało. To moja strona i ja tu usta­lam zasady.

Django: abstrakcji ciąg dalszy

Tym razem inne podej­ście, natu­ralne dzie­dzi­cze­nie abs­trak­cyj­nych modeli z dwoma dodat­ko­wymi metodami.

W prze­ci­wień­stwie do poprzed­niego przy­kładu, pozwala uży­wać super() w abs­trak­cyj­nych kla­sach pośrednich.

from django.db import models

class AbstractMixin(object):
    _classcache = {}

    @classmethod
    def contribute(cls):
        return {}

    @classmethod
    def construct(cls, *args, **kwargs):
        attrs = cls.contribute(*args, **kwargs)
        attrs.update({
            '__module__': cls.__module__,
            'Meta': type('Meta', (), {'abstract': True}),
        })
        key = (args, tuple(kwargs.items()))
        if not key in cls._classcache:
            clsname = ('%s%x' % (cls.__name__, hash(key))) \
                    .replace('-', '_')
            cls._classcache[key] = type(clsname, (cls, ), attrs)
        return cls._classcache[key]

Przy­kład użycia:

class CategoryFactory(models.Model, AbstractMixin):
    name = models.CharField(max_length=100)

    class Meta:
        abstract = True

    def __unicode__(self):
        return self.name

class ProductFactory(models.Model, AbstractMixin):
    name = models.CharField(max_length=100)

    class Meta:
        abstract = True

    @classmethod
    def contribute(cls, category):
        return {'category': models.ForeignKey(category)}

    def __unicode__(self):
        return u'%s / %s' % (self.category, self.name)

Kon­kre­ty­za­cja klas:

class MyCategory(CategoryFactory.construct()):
    pass

class MyProduct(ProductFactory.construct(category=MyCategory)):
    pass

Działa rów­nież nasz test:

from . import models

c = models.MyCategory(name='cat')
p = models.MyProduct(name='prod', category=c)
print p
# cat / prod

PIT 37

Przy­cho­dzi taki moment, kiedy nie da się już odwle­kać roz­li­cze­nia podat­ków. Ja swoją dekla­ra­cję posta­no­wi­łem zło­żyć dopiero dzi­siaj. Nie obyło się bez przygód.

Linux x86-64

e-Deklaracje wyma­gają Adobe AIR. Adobe AIR nie obsłu­guje 64-bitowego Linuksa. Tyle.

Dane z poprzed­niego roku (nie­zbędne do roz­li­cze­nia elek­tro­nicz­nego) udało się wydo­być ręcznie:

$ sqlite3 ~/.appdata/e-Deklaracje.*/Local Store/settings.dat 'SELECT xmlDocument FROM EDK_DECLARATIONS;'

Z Wine jest podob­nie — jedyna wspie­rana archi­tek­tura, to x86.

Linux x86

Adobe AIR 1.5 jest zje­bany, robi w środku bar­dzo złe rze­czy (budowa pli­ków .rpm z roz­sze­rze­niem .deb i tym podobne) i nie działa nigdzie poza Fedorą.

Adobe AIR 2.0 insta­luje się bez pro­blemu, przy oka­zji psu­jąc upraw­nie­nia kata­lo­gów z iko­nami, pli­kami .desk­top i typami MIME. Adobe Reader 9 insta­luje się bez pro­ble­mów. e-Deklaracje uru­cha­miają się, pozwa­lają na stwo­rze­nie pro­filu i w zasa­dzie tyle. Pro­ces acroread zaj­muje całą dostępną pamieć i moc pro­ce­sora. Dzię­ku­jemy ci, Adobe.

Online

Osta­tecz­nie roz­li­czy­łem się w prze­glą­darce, za pomocą apli­ka­cji pitroczny.pl¹. Pole­cam tę wer­sję — oszczę­dza ner­wów, ma lep­szy inter­fejs (nie wygląda jak zeska­no­wany PIT) i, przede wszyst­kim, działa.

¹ Korzy­sta­jąc z tego odno­śnika prze­zna­czasz 1% podatku na Fun­da­cję Nowo­cze­sna Pol­ska.

Django: zabawa z abstrakcją

Zasta­na­wia­li­śmy się ostat­nimi czasy, w jaki spo­sób zbu­do­wać apli­ka­cję tak, by z jed­nej strony była uni­wer­salna (abs­trak­cyjne modele bazowe), a z dru­giej zawie­rała całą nie­zbędną logikę. Pro­blem polega na tym, że — z oczy­wi­stych wzglę­dów — klu­cza obcego do modelu abs­trak­cyj­nego stwo­rzyć się nie da. Pozo­staje więc zwa­lić pracę na pro­gra­mi­stę, który kon­kre­ty­zuje abs­trak­cyjne pro­to­typy, prawda?

Dzi­siaj do głowy przy­szło mi roz­wią­za­nie może nie do końca ładne, ale na pewno dzia­ła­jące. Fabryka abs­trak­cyj­nych modeli:

from django.db import models

class AbstractModelMetaclass(type):
    def __new__(cls, name, bases, attrs):
        super_new = super(AbstractModelMetaclass, cls).__new__
        if bases == (object, ):
            return super_new(cls, name, bases, attrs)
        new_attrs = attrs.copy()
        module = attrs['__module__']
        new_cls = super_new(cls, name, bases, {'__module__': module})
        fields = getattr(new_cls, '_fields', {}).copy()
        for key, val in attrs.items():
            if key not in ['construct', '__new__']:
                fields[key] = val
        new_attrs['_fields'] = fields
        return super_new(cls, name, bases, new_attrs)

class AbstractModelFactory(object):
    __metaclass__ = AbstractModelMetaclass

    @staticmethod
    def construct():
        return {}

    def __new__(cls, *args, **kwargs):
        attrs = cls._fields.copy()
        attrs.update(cls.construct(*args, **kwargs))
        attrs['__module__'] = cls.__module__
        attrs['Meta'] = type('Meta', (), {'abstract': True})
        return type(cls.__name__, (models.Model, ), attrs)

Idea jest taka, że imple­men­tu­jąc swoją pod­klasę fabryki modeli mamy moż­li­wość zade­cy­do­wać o polach, jakie tra­fią do osta­tecz­nego modelu. Dla lep­szego zro­zu­mie­nia, roz­ważmy abs­trak­cyjne klasy pro­duktu i kategorii:

class CategoryFactory(AbstractModelFactory):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name

class ProductFactory(AbstractModelFactory):
    name = models.CharField(max_length=100)

    @staticmethod
    def construct(category):
        return {'category': models.ForeignKey(category)}

    def __unicode__(self):
        return u'%s / %s' % (self.category, self.name)

Skon­kre­ty­zujmy klasy wła­sną implementacją:

class MyCategory(CategoryFactory()):
    pass

class MyProduct(ProductFactory(category=MyCategory)):
    pass

Upew­nijmy się, że zależ­no­ści dzia­łają prawidłowo:

from . import models

c = models.MyCategory(name='cat')
p = models.MyProduct(name='prod', category=c)
print p
# cat / prod

Oczy­wi­ście — ktoś zaraz powie — to samo można uzy­skać two­rząc funk­cję zwra­ca­jącą klasę zde­fi­nio­waną w jej ciele. Można, ale nie da się po niej wygod­nie dzie­dzi­czyć (roz­sze­rze­nie ist­nie­ją­cej fabryki).

Django: Pinax i Satchmo

Obie­cy­wa­łem w skró­cie przed­sta­wić cuda, jakimi są oba pro­jekty. Jak obie­ca­łem, tak czynię.

Marketing kołem napędowym gospodarki

Oba pro­jekty zna­łem od dość dawna z prasy, jaką zapew­niają im dewe­lo­pe­rzy i fani na wsze­la­kich djan­go­wych por­ta­lach, pla­ne­tach i kon­fe­ren­cjach. Na siłę uży­wał ich nie będę — pomy­śla­łem — ale może kie­dyś się trafi oka­zja, to zoba­czę, czym się tak wszy­scy eks­cy­tują. W bebe­chy nie zaglą­da­łem, wie­dzia­łem tylko, że jeden ma być uni­wer­sal­nym szkie­le­tem dla wsze­la­kich ser­wi­sów spo­łecz­no­ścio­wych, a drugi kloc­kami do budowy skle­pów internetowych.

Bliskie spotkania I stopnia

Oka­zja się tra­fiła, choć nie taka, jak pla­no­wa­łem. Naj­pierw przy­szło mi bowiem robić jedy­nie nie­wiel­kie poprawki we wdro­żo­nym już ser­wi­sie opar­tym o Satchmo, póź­niej dopi­sać jakąś pro­stą apli­ka­cję do roz­wi­ja­nej jesz­cze spo­łecz­no­ściówki, która do dzi­siaj chyba nie doro­biła się ani wdro­że­nia, ani nawet nazwy.

Dzia­łać to dzia­łało, pod maskę nie bar­dzo mia­łem czas zaglą­dać. Dal­szy kon­takt spro­wa­dzał się głów­nie do czy­ta­nia samych super­la­tyw o owych — chcia­łoby się rzec — prze­bły­skach geniu­szu inży­nie­rii programowania.

Pinax — pierwsza krew

Do czasu. Jeden z kon­trak­tów z Miru­mee wyma­gał wbu­do­wa­nia w ser­wis pod­sta­wo­wych funk­cji spo­łecz­no­ścio­wych. Jed­no­gło­śnie zde­cy­do­wa­li­śmy się wypró­bo­wać Pinaksa.

Mie­siąc póź­niej mie­li­śmy już wła­sny fork, z któ­rego sys­te­ma­tycz­nie usu­wa­li­śmy kawałki takiego kodu, że star­czy­łoby na dok­to­rat z archi­tek­tury opro­gra­mo­wa­nia. Obej­ścia na wor­ka­ro­undy, zaślepki na hac­kach. Widoki z pomi­nię­ciem for­mu­la­rzy roz­bie­ra­jące requ­est na zmienne i bez żadnej wali­da­cji paku­jące je do bazy. Widoki pozwa­la­jące na edy­cję cudzych obiek­tów, jeśli tylko znało się ich id. Apli­ka­cje, które nie dzia­łały wcale, albo te dzia­ła­jące czę­ściowo, z resztą kodu wyko­men­to­waną i oto­czoną FIXME.

Jedno jest pewne, zmar­no­wa­li­śmy na to masę czasu i jeśli będę musiał do Pinaksa podejść drugi raz, to tylko po to, żeby ze strzelbą pod pachą wypro­wa­dzić go na tyły domu.

Początek przygody z Satchmo

Cał­kiem nie­dawno zosta­li­śmy z kolei zakon­trak­to­wani przez firmę z UK, która spe­cja­li­zuje się w budo­wie skle­pów inter­ne­to­wych w Satchmo. Nie­stety pro­gra­mi­ści ode­szli tuż po roz­po­czę­ciu pro­jektu i ktoś musiał zada­nie skoń­czyć. Chcia­łem poznać Satchmo, teraz mia­łem oka­zję. A dokład­niej dwa tygo­dnie na prze­ro­bie­nie gar­ści pli­ków PNG z pro­jek­tami gra­ficz­nymi na w pełni dzia­ła­jące wdrożenie.

Pierw­sze dwa dni spę­dzi­łem na lek­tu­rze doku­men­ta­cji i dopro­wa­dze­niu kodu do dzia­ła­nia na swo­jej maszy­nie. Szybko też oka­zało się, że Satchmo to taki sam zestaw kloc­ków, jak Pinax czy — nie przy­mie­rza­jąc — samo­chód. Oczy­wi­ście, można wymon­to­wać tylną kanapę i zmie­nić cho­inkę zapa­chową na lusterku, ale próba wycią­gnię­cia radia szybko poka­zuje, że jest trwale przy­krę­cone do kok­pitu. Jakie­kol­wiek próby dopa­so­wa­nia sklepu do potrzeb sprze­dawcy albo przy­po­mi­nają próbę ucha­rak­te­ry­zo­wa­nia polo­neza na cią­gnik sio­dłowy z uży­ciem pla­ste­liny, albo koń­czą się głę­boką inge­ren­cją w kod poszcze­gól­nych modułów.

I wtedy zaczęły się schody

Wła­śnie kon­takt z modu­łami był pierw­szym momen­tem, kiedy krzyk­ną­łem WTF. Kiedy pierw­szy raz mia­łem kon­takt z Satchmo, poszcze­gólne moduły i apli­ka­cje miesz­kały we wspól­nej prze­strzeni nazw satchmo. Oka­zuje się jed­nak, że ktoś wpadł na genialny pomysł zaosz­czę­dze­nia kilku lite­rek i całość by dzia­łać musi zostać umiesz­czona bez­po­śred­nio w ścieżce Pythona. Wiel­kie brawa za poten­cjalne kon­flikty nazw z innymi modu­łami Pythona oraz za zaję­cie co bar­dziej oczy­wi­stych nazw dla lokal­nych apli­ka­cji projektu.

To jed­nak nie koniec, szybko oka­zało się, że doku­men­ta­cja to tylko cie­ka­wostka, a jakie­kol­wiek poję­cie o dzia­ła­niu cało­ści daje dopiero lek­tura kodu. Tam można zna­leźć praw­dziwe perełki.

Miłe złego początki

Na początku było śmiesznie:

{% for product in products %}
    {% if forloop.first %} <ul>  {% endif %}
        <li>{% thumbnail product.main_image.picture 85x85 as image %}
        <a href="{{ product.get_absolute_url }}"><img src="{{ image }}" width="{{ image.width }}" height="{{ image.height }}" /></a>
        <a href="{{ product.get_absolute_url }}">{{ product.translated_name }}</a></li>
    {% if forloop.last %} </ul> {% endif %}
{% endfor %}

Oczy­wi­ście, zda­rza się, że pierw­szy i ostatni ele­ment listy wystę­pują gdzieś w środku, dla­tego bez­piecz­niej spraw­dzić wszystkie.

Powrót do przyszłości

Wkrótce oka­zało się, że sys­tem płat­no­ści dla nie­któ­rych ope­ra­to­rów wymaga dodat­kowo poda­nia daty wysta­wie­nia karty. Z tym, że data wysta­wie­nia i waż­no­ści ofe­rują ten sam zestaw opcji: od stycz­nia bie­żą­cego roku, do grud­nia za cztery lata. Cóż, widać karty star­sze niż trzy mie­siące nie są zbyt popu­larne, za do zda­rzają się w obro­cie karty wysta­wione za dwa lata.

Działa doskonale, działa bez przerwy

A co powie­cie na nie­skoń­czoną pętlę reku­ren­cji wła­śnie w module płatności?

def confirm_info(request, template='shop/checkout/protx/confirm.html', extra_context={}):
    payment_module = config_get_group('PAYMENT_PROTX')
    controller = confirm.ConfirmController(request, payment_module)
    # ...
    controller.onForm = secure3d_form_handler
    controller.confirm()
    return controller.response

# ...

def secure3d_form_handler(controller):
    """At the confirmation step, protx may ask for a secure3d authentication.  This method
    catches that, and if so, sends to that step, otherwise the form as normal"""

    if controller.processorReasonCode == '3DAUTH':
        # ...
        return http.HttpResponseRedirect(redirectUrl)

    return controller.onForm(controller)

Ładnie, prawda?

Telekonkurs dla naszych widzów

Na koniec zagadka. Pro­blem: pod­czas testów użyt­kow­nik opła­cił za towar, ale na stro­nie z potwier­dze­niem zamó­wie­nia poja­wiła się infor­ma­cja o nie­uda­nej płat­no­ści. Roz­wią­za­nie: oka­zuje się, że wła­ści­ciel sklepu źle wypeł­nił pole na wła­sny adres e-mail. Czy ktoś z was potrafi bez zaglą­da­nia w kod zna­leźć logiczne powiązanie?

Otóż pole na e-mail obsługi sklepu nie jest typu EmailField i jest mu gene­ral­nie obo­jętne, jaką war­tość się poda. Sys­tem płat­no­ści zaś na koniec trans­ak­cji wysyła e-mail z potwier­dze­niem na adres sklepu. W bloku o takiej konstrukcji:

try:
    self.response = dict([row.split('=', 1) for row in result.splitlines()])

    # ...

    return ProcessorResult(self.key, success, detail, payment=payment)

except Exception, e:
    self.log.info('Error submitting payment: %s', e)
    payment = self.record_failure(order=order, amount=amount,
        transaction_id="", reason_code="error",
        details='Invalid response from payment gateway')

To oczy­wi­ście nie koniec. W kodzie można zna­leźć takie wyna­lazki, jak:

  • obiekt NOTSET, peł­niący funk­cję None;
  • apli­ka­cja livesettings, umoż­li­wia­jąca soft­co­ding rze­czy, które powinny być w kodzie (nie­które z nich wyma­gają do dzia­ła­nia restartu usługi, to się dopiero nazywa ironia);
  • bez­myślne zapy­cha­nie logów gdzie i czym popadnie;
  • i wiele, wiele innych.

Odpręż się, to nie będzie bolało

Nie mam poję­cia, jak oba pro­jekty wyro­biły sobie jaką­kol­wiek renomę. Pinax jest zwy­czaj­nie nie­groźną kupą kom­po­stu, którą należy omi­jać z daleka albo mieć potem pre­ten­sję do samego sie­bie. Satchmo z kolei ope­ruje cudzymi pieniędzmi.

Czy nie byłoby lepiej zamiast chwa­lić się uda­nymi wdro­że­niami (chyba, że jest to chwa­le­nie się na zasa­dzie: nie wiem jakim cudem, ale się udało), zająć się testo­wa­niem wła­snego kodu? Już nie mówię nawet o popra­wia­niu go, żeby przy­po­mi­nał coś napi­sa­nego w Pytho­nie, wystar­czy, żeby dzia­łała wię­cej niż jedna kom­bi­na­cja, zaś zepsute i nie­te­sto­wane od mie­sięcy moduły zostały w sto­sowny spo­sób oznaczone.

Mówiąc w skró­cie, jestem na nie.

twojastara.py

fat = unicode
print type(ur'mom') is fat

Tak, tak, poni­żej kry­tyki. Za to już nie­długo: seria nie­wy­ja­śnio­nych zna­le­zisk w kodzie Satchmo (czyli jak Pythona uży­wać nie należy).

Django: TestCase i testowanie szablonów

Bar­dzo brzydki hack, oparty o załącz­nik do tic­ketu 7611. Czy­ści wewnętrzny cache Django, ale pozwala testo­wać dzia­ła­nie kodu opie­ra­ją­cego się o ren­de­ro­wa­nie sza­blo­nów bez ryzyka, że apli­ka­cja owe sza­blony nadpisze:

import os
import sys
from django.conf import settings
from django.template import loader
from django.test import TestCase

class TemplateTestCase(TestCase):
    def _pre_setup(self):
        self._template_setup()
        super(TemplateTestCase, self)._pre_setup()

    def _post_teardown(self):
        self._template_teardown()
        super(TemplateTestCase, self)._post_teardown()

    def _template_setup(self):
        if hasattr(self, 'template_loaders'):
            self._old_template_loaders = settings.TEMPLATE_LOADERS
            settings.TEMPLATE_LOADERS = self.template_loaders
            loader.template_source_loaders = None
        if hasattr(self, 'template_dirs'):
            self._old_template_dirs = settings.TEMPLATE_DIRS
            test_dir = os.path.dirname(sys.modules[self.__module__].__file__)
            settings.TEMPLATE_DIRS = [ os.path.join(test_dir, dirname) for dirname in self.template_dirs ]

    def _template_teardown(self):
        if hasattr(self, '_old_template_loaders'):
            settings.TEMPLATE_LOADERS = self._old_template_loaders
            loader.template_source_loaders = None
        if hasattr(self, '_old_template_dirs'):
            settings.TEMPLATE_DIRS = self._old_template_dirs

Uży­cie:

class TestSomething(TemplateTestCase):
    template_loaders = ('django.template.loaders.app_directories.load_template_source',)

    def test_foo(self):
        pass

Django: pluralize

Naj­waż­niej­szą rze­czą, jaką powin­ni­ście wie­dzieć na temat fil­tra pluralize jest ta, żeby go nigdy nie uży­wać. Nigdy. Jego ist­nie­nie wynika z leni­stwa ludzi, któ­rzy nigdy w życiu nie przy­go­to­wy­wali apli­ka­cji do tłu­ma­cze­nia. Jeśli nie wyra­zi­łem się dość jasno, uży­wa­nie fil­tra pluralize wywoła u cie­bie raka, ste­reo­po­roże i zatwar­dze­nie rozsiane.

Jak­kol­wiek spryt­nie by nie wyglą­dał, ten kod nie nadaje się do niczego:

<p>
    There {{ item_count|pluralize:"is,are" }}
    {{ item_count }} thing{{ item_count|pluralize:"s" }}.
</p>

Dla­czego się nie nadaje? Spró­buj­cie go prze­tłu­ma­czyć na kilka języ­ków. Nie­któ­rzy chwy­tają się brzyd­kich obejść, inni wolą zro­bić to zgod­nie ze sztuką. Jeśli kie­dy­kol­wiek pra­co­wa­li­ście przy loka­li­za­cji opro­gra­mo­wa­nia, dosko­nale wie­cie, że tłu­ma­cze­nie jed­nego słowa jest tak samo sen­sowne, jak tłu­ma­cze­nie każ­dej litery z osobna.

Poprawna wer­sja jest przy oka­zji czytelna:

<p>
{% blocktrans count item_count as items %}
    There is {{ items }} thing.
{% plural %}
    There are {{ items }} things.
{% endblocktrans %}
</p>

{% blocktrans %} z {% plural %} uży­wają ungettext, dzięki czemu bez koniecz­no­ści odpra­wia­nia cza­rów radzą sobie z pol­skimi liczeb­ni­kami (jeden tysiąc, dwa tysiące, pięć tysięcy).

Python: alternatywa dla dict.update

Kolejny skrót. Dość regu­larny przy­pa­dek zwró­ce­nia sumy słow­ni­ków bez nisz­cze­nia któ­re­go­kol­wiek z nich:

def foo(bar, baz):
    tmpdict = dict(bar)
    tmpdict.update(baz)
    return tmpdict

Można rów­nież roz­wią­zać ina­czej, korzy­sta­jąc z faktu, że dict akcep­tuje nazwane parametry:

def foo(bar, baz):
    return dict(bar, **baz)

Korzy­sta­jąc ze skró­tów zawsze upew­nij się, że na pierw­szy rzut oka wia­domo, co dany kod robi.

Creative Commons Attribution-NonCommercial-ShareAlike 2.5 Poland
This work by Patryk Zawadzki is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 Poland.