Ten artykuł przedstawia posługiwanie się Cythonem i budowanie obrazów za pomocą Cythona oraz poprawne praktyki CI/CD przy użyciu GitLaba i jego potoku CI/CD.
Ze względu na to, że nasz Python nagle musi zostać skompilowany, a skompilowany moduł wymagany jest do wszystkiego, począwszy od testów jednostkowych, poprzez dokumentację a na właściwym buildzie kończąc, uważam za stosowne rozbić joba na osobne etapy.
Załóżmy, że nasz projekt Cythonowy mamy przetestować jednostkowo, zrobić z niego dokumentację za pomocą Sphinxa (pamiętamy tylko że obiekty dokumentowane muszą być osiągalne przez Pythona, tak więc obiekty z cdef odpadają).
Jednocześnie mamy wymóg, że w projekcie znajduje się wiele plików pyx i każdy z nich chcemy mieć dostępny jako osobny moduł Pythona. Do tego posłuży nam snakehouse.
Załóżmy że pliki pyx
są w katalogu example
.
Caveaty podczas używania
Jeśli definiujesz funkcję C cdef
, pamiętaj o przesyłaniu wyjątków klauzulą except
. Inaczej Cython będzie zjadał te wyjątki, i jedyne co po nim zobaczysz to wynik na stdout czy tam stderr.
Budowanie
Zrobimy dwa osobne joby – w pierwszym skompilujemy nasz program do postaci pliku wheel, a w drugim zainstalujemy go na obrazie Dockerowym.
Kasowanie docstringów
Docstringi są fajną funkcją Pythona, ale nie w momencie gdy oddajesz klientowi projekt. Docstringi zagnieżdżone w Cythonie pojawią się również w pliku so
/DLL
.
Na szczęście jest dostępne narzędzie do automatycznego usuwania docstringów i komentarzy z Twojego kodu źródłowego!
W wyniku tego poniższe:
class Klasa:
"""Mój docstring"""
zostanie przerobione na:
class Klasa:
które poprawnym Pythonem nie jest. Aby uniknąć represji z tym związanych, musimy bloki zawierające docstringi lekko przeredagować:
class Klasa:
"""Mój docstring"""
pass
Można to rozegrać w .gitlab-ci.yml
następująco:
image: smokserwis/build:python3
.compile:
stage: compile
script:
- python setup.py bdist_wheel
- doctor-wheel dist/*.whl
- mv dist/*.whl .
artifacts:
paths:
- "*.whl"
compile_master:
extends: .compile
before_script:
- strip-docs .
only:
- master
compile_else:
extends: .compile
except:
- master
Kompilacja wheela
Zanim się za cokolwiek zabierzemy, musimy zainstalować snakehouse:
pip install satella cython
pip install snakehouse
Na wstępie pamiętać, że ograniczeniem snakehouse jest import modułów “na dzień dobry”. Musimy tak więc stworzyć boilerplate, który załaduje nasz Cythonowy projekt i dopiero potem go wywoła.
Tworzymy tak więc katalog start_example
i umieszczamy w nim pliki __init__.py
i __main__.py
. W pliku __main__.py
umieszczamy poniższy kod:
if __name__ == '__main__':
from example.run import run
run()
czy tam w jakikolwiek inny sposób uruchamiamy naszą aplikację. Chodzi tutaj tylko o to, żeby importy z snakehouse_ nie szły na górze pliku.
W Dockerfile-u czy czym tam uruchamiamy program ustawiamy:
CMD ["python", "-m", "start_example"]
czy tam ENTRYPOINT
jak wolimy.
Następnie tworzymy plik setup.py
:
from setuptools import find_packages
from distutils.core import setup
from snakehouse import build, Multibuild, monkey_patch_parallel_compilation, find_pyx_and_c
monkey_patch_parallel_compilation()
setup(name='example',
description='An example example',
packages=find_packages(include=['example', 'start_example']),
ext_modules=build([Multibuild('example', find_pyx_and_c('example')), ],
compiler_directives={
'language_level': '3',
})
)
monkey_patch_parallel_compilation
nie jest tutaj niezbędne, ale znacząco przyśpieszy kompilację.
Jesteśmy już prawie na miejscu! Odpalamy basha czy co tam lubimy:
python setup.py bdist_wheel
Teraz w katalogu :code:dist
będzie czekał na nas nowiutki wheel, który możemy zainstalować:
pip install dist/*.whl
Uwaga: Taka kompilacja wyrzuci nam adnotacje typowe. Adnotacje nie będą dostępne ani w dokumentacji, ani mechanizmem introspekcji Pythona. Jeśli chcemy zachować adnotacje typowe, (chociażby do celów dokumentacji ze Sphinxem), musimy to wywołać następująco:
setup(name='example',
description='An example example',
packages=find_packages(include=['example', 'start_example']),
ext_modules=build([Multibuild('example', find_pyx_and_c('example')), ],
compiler_directives={
'language_level': '3',
'embedsignature': True
})
)
Tak więc definiując adnotacje typowe jedynie dla buildów w których została zdefiniowana zmienna środowiskowa CI (większość środowisk buildowania ją definiuje, ale jak to bywa w tej branży skonsultuj się ze swoją dokumentacją) tak więc skrypt może wyglądać również tak:
import os
from setuptools import find_packages
from distutils.core import setup
from snakehouse import build, Multibuild, monkey_patch_parallel_compilation, find_pyx_and_c
monkey_patch_parallel_compilation()
comp_dirs = {'language_level': '3'}
if 'CI' in os.environ:
comp_dirs['embedsignature'] = True
setup(name='example',
description='An example example',
packages=find_packages(include=['example', 'start_example']),
ext_modules=build([Multibuild('example', find_pyx_and_c('example')), ],
compiler_directives=comp_dirs)
Można od razu zbudować i zainstalować projekt ze źródła:
python setup.py install
Ale nie zalecam tego, bo przynajmniej w Dockerfile’u zostanie ten kod źródłowy, a na usunięciu go przecież nam najbardziej zależy. Minus jest też taki że na docelowym systemie musi być zainstalowany Cython i snakehouse. Kopiujemy teraz naszego wheela i załączamy go do artefaktów, czyli plik .gitlab-ci.yml
będzie wyglądał teraz tak:
stages:
- compile
- build
compile:
stage: compile
image: smokserwis/build:python3
script:
- python setup.py bdist_wheel
- cp dist/*.whl .
artifacts:
paths:
- "*.whl"
Uwaga: Obraz smokserwis/build:python3 wykorzystujemy ponieważ ma on wbudowanego Cythona, snakehouse, satellę oraz inne moduły wspomniane w tym artykule, w szczególności doctor-wheel oraz strip-docs.
Jeśli chciałbyś budować wersję paczki na procesor ARM, użyj poniższej sentencji:
compile:
stage: compile
image: smokserwis/build:arm-python3
script:
- python setup.py bdist_wheel
- cp dist/*.whl .
artifacts:
paths:
- "*.whl"
tags:
- armv7l
smokserwis/build:arm-python3 jest ARM-owym odpowiednikiem smokserwis/build:python3.
Postprocessing
Zauważamy że zrobiony plik wheel jest trochę duży. No cóż, zostało w nim masę symboli które zostawia Cython. Aby je usunąć wydajemy:
pip install doctor-wheel
doctor-wheel dist/*.whl
Albo za pomocą CI:
compile:
stage: compile
image: smokserwis/build:python3
script:
- python setup.py bdist_wheel
- cp dist/*.whl .
- doctor-wheel *.whl
artifacts:
paths:
- "*.whl"
doctor-wheel to jeden z moich skryptów które rozpakowują zipa, którym jest wheel, wywołują polecenie strip
na każdym pliku so
który znajduje się w tym zipie, po czym spowrotem go pakują. Czynią to in-place, czyli nasz wheel w katalogu dist
zostanie zmieniony lokalnie.
Budowanie obrazu
Teraz czas na instalację naszej paczki w docelowym środowisku. Jeśli nie korzystasz z Dockera to śmiało
pomiń ten rozdział.
Teraz tworzymy sobie joba z buildem:
build:
stage: build
dependencies:
- compile
script:
- docker build -t tag_repa .
- docker push tag_repa
Pamiętamy o tym żeby w Dockerfile’u zawrzeć poniższe:
ADD *.whl /tmp/
RUN pip install /tmp/*.whl && \
rm -rf /tmp/*.whl
Pamiętamy jeszcze o ograniczeniach dotyczących uruchamiania rzeczy paczkowanych snakehouse’m:
CMD ["python", "-m", "start_example"]
I voila, obraz Dockera zbudowany! Obraz Dockera najlepiej zaczynać FROM python:3.8
.
Do zainstalowania tej paczki nie jest potrzebny ani Cython ani snakehouse.
Plik wheel to gotowa do zainstalowania binarka. Oczywiście zależności wymienione w setup.py
zostaną zainstalowane automatycznie. Właśnie taka jest przewaga kompilacji w osobnym kroku, że nie trzeba ze sobą nosić tych wszystkich śmieci.
Testy jednostkowe i dokumentacja
Aby odpalić unit testy posługujemy się przepisem z poprzedniego rozdziału instalując paczkę, a potem dalej normalnie jak dla unit testów/dokumentacji.
Pamiętaj tylko że skompilować wheel produkcyjny w osobnym stepie niż wheel do unit testów i dokumentacji, gdyż ten do unit testów i dokumentacji zawierać będzie adnotacje typowe, a nie chcesz przecież wrogowi zbytnio ułatwiać zadania, prawda?