Minimalna aplikacja we Flasku, done right

Ten post został napisany ponad 2 lata temu, do wszystkich porad technologicznych w nim zawartych lepiej będzie podejść z dużą rezerwą, bo bardzo możliwe że tego rodzaju informacje są już nieaktualne.

Opublikowano: 17.07.2020

Ostatnia modyfikacja: 07.02.2024

flask

pcp

programowanie

projekty

python

Przyglądając się baczniej mojemu Brewlogowi, który akurat przechodzi transformację do wersji 3 przy okazji tracąc kolejne nigdy nie użyte warstwy abstrakcji, doszedłem do wniosku, że nawet robiąc małą aplikację, zaczynam ze zbyt wysokiego poziomu. Jak zrobić małą aplikację we Flasku (naprawdę małą) ale tak, żeby miało to ręce i nogi?

Niebo nad Mazowszem

Na pierwszy rzut oka wydaje się to bajecznie proste - obiekt aplikacji, parę funkcji widoków, definicja modeli, dopóki wszystko to siedzi w jednym module, to trudno sobie nawet wyobrazić, że coś mogłoby nie działać. Schody zaczynają się wtedy, gdy spróbujemy porozdzielać to na oddzielne moduły, w szczególności funkcje widoków. Otóż żeby działała rejestracja widoków jako handlerów dla ścieżek URL, to potrzebny jest obiekt aplikacji w momencie definicji, a w runtime aplikacja musi mieć zaimportowane pod ręką wszystkie funkcje widoków. Czy to czegoś nie przypomina? W sportach walki jest takie określenie jak klincz, a w języku polskim przysłowie że złapał kozak tatarzyna, a tatarzyn za łeb trzyma. W Pythonie określa się to jako circular import.

Czy aby na pewno? Czy to jest sytuacja bez wyjścia?

Otóż nie tym razem

No nie do końca.

Rzeczywiście gdybyśmy chcieli wszystko sobie ładnie statycznie poimportować, to oczywiście się nie da i ten import w kółko rzeczywiście wystąpi, wykonanie się załamie i nic z tego nie będzie. Tymczasem nie jest to takie trudne do obejścia, trzeba tylko się zorientować kiedy co się dzieje, a w efekcie kiedy który obiekt potrzebuje co wiedzieć. Jak to zrobić opisał Charles Leifer w 2013 roku, a ja przygotowałem coś w rodzaju minimalnego przykładu, którym teraz posłużę się do wyjaśnienia ocb.

Obiekt funkcji widoku musi mieć dostęp do obiektu aplikacji w momencie definicji po to, by móc zarejestrować się jako obsługujący jakąś ścieżkę URL. Z kolei obiekt aplikacji musi wiedzieć która z fukcji zarejestrowała się jako obsługująca ścieżkę URL w momencie uruchamiania, a żeby do tego doszło to musi zaimportować wszystkie funkcje widoków. Te dwie rzeczy nie dzieją się jednocześnie, czyli już pojawia się jakaś iskierka nadziei.

Aby ten problem ostatecznie rozwiązać skorzystamy z pewnej dozy magii Pythona, czyli z faktu że obiekty modułów są niezmienialne, a jednocześnie identyfikatory obiektów w modułach są tylko nazwami wskazującymi na obiekty występujące w jednym egzemplarzu w czasie wykonywania kodu przez maszynę wirtualną. Wykorzystując to zjawisko możemy użyć identyfikatora obiektu aplikacji z jednego modułu w momencie definicji funkcji widoku, a z zupełnie innego modułu w momencie uruchamiania samej aplikacji. Ważne jest tylko to, by nie był to ten sam identyfikator w obu przypadkach. Dzięki temu w obu przypadkach nie będzie importowana ta sama nazwa (choć za każdym razem wskazująca na ten sam obiekt aplikacji).

Przykład który zmontowałem oprócz tego tricku zawiera kilka innych przydatnych rzeczy, które ułatwią utrzymanie zbudowanego na jego podstawie kodu w 2020. Przede wszystkim, jest oparty na src-layout, który rozwiązuje 2 poważne problemy, testowalność i instalowalność. Tego układu kodu używają np. wszystkie projekty Pallets (w tym Flask) i ma on bardzo wyraźne zalety. Oprócz tego mój przykład dostarcza kilku rzeczy które są już zdefiniowane i gotowe do wykorzystania, jak ładowanie konfiguracji z plików .env.

Fajne, co?