Seguint amb el primer article sobre les èines del programador Python, aquí parlo de cobrir el codi amb un estil definit mitjançant un autoformatter i un linter, assegurar la funcionalitat amb tests, i que aquestes comprobacions es passin automàticament en cada push a un Pull Request de GitHub. De fet, aquest és el setup que tenim al meu equip i ens garanteix uns mínims de coherència i qualitat al nostre codi.
En aquest article em seguiré basant en el repo pyxavi que tinc públic a GitHub, ja que és petit, simple i reusable. Aquí cobriré els següents temes:
yapf
flake8
pytest
No sóc pas un crack, el que explico aquí ho he après a la feina envoltat d'absoluts genis. Si em veniu amb "això es pot fer millor així" no us rebatré: a mi em va bé com ho mostro, m'hi sento còmode i amb el meu equip ho hem anat polint sobre la marxa i segons les nostres necessitats. Intentaré aportar enllaços on trobar més info al respecte, i estaré encantat d'explicar amb més detall qualsevol punt, feu servir les xarxes socials!
yapf
Si una cosa té la programació i els programadors, és que el codi és una cosa viva... i molt personal. Cadascú té un estil, un gust, a cadascú li agrada programar d'una manera diferent. Que què vull dir? Mireu les següents dues funcions:
def getSumFromNumbers(first:int,second:int)->int:
return (first+second)
def get_sum_from_numbers( first:int, second:int ) -> int:
return ( first + second )
Les dues fan el mateix, les dues són exactament iguals, però ambdues tenen estil de codi molt diferent. CamelCase, snake_case, espais després del les comes i entre els operadors... Hi ha una barbaritat de possibles combinacions i variants. Moltes d'elles estan cobertes per la guia d'estils oficial de Python però com podràs entendre una guia d'estil és una recomanació, el codi funcionarà igual si no la segueixes...
Quan programes sol a casa o fas un codi ràpid, no diries que ho notes tant. Però tot es fa més evident quan programes molt o treballes en un equip i contribuiu junts al mateix codi. Sigui com sigui, trobo aquest article molt interessant (del 2012!), amb les claus de per què tenir un estil definit és important. Deixa'm que en posi algunes:
Decidir-ne un, i mantenir-lo. Aquesta és la clau. Si estàs sol el meu consell és que t'ajustis el més possible a la comunitat del teu llenguatge, i si treballes en un equip cal decidir quelcom comú, intentant que vegin el benefici d'adoptar un estil proper a l'estàndar.
Un cop es decideix un estil, ajuda moltíssim tenir alguna èina que supervisi el codi i et marqui les teves pròpies falles. Per un costat aprendràs i per l'altre t'assegures que tot el teu codi segueix el mateix fonament.
Yapf
com a validador de formatYapf és un autoformatter que, a diferència d'altres autoformatters, promou els canvis encara que el codi sigui correcte. El punt està en assegurar-se que s'està seguint un estil determinat. Això assegura uniformitat (la que triis) i consistència. A mi no m'agrada que l'autoformatter em canvii el codi, que ho pot fer. Si és capaç d'indentificar el problema i suggerir-te una sol.lució, per què no canviar el codi directament? Doncs per que llavors no aprenc. Llavors tinc un bot que em corregeix el codi mentre jo segueixo tenint un estil nefast. I es pot fer servir Yapf així
Primer de tot, cal definir yapf
com a dependència de desenvolupament. La diferència és que no necessites yapf
per fer córrer el codi, només quan estàs tirant línies i et vols assegurar que el codi que puges al servidor és correcte. Per això crearem una secció nova al pyproject.toml
(jo la poso just sota les dependències normals). Ja que hi som, afegeixo també suport pels arxius toml
. :
[tool.poetry.group.dev.dependencies]
yapf = "^0.32.0"
toml = "^0.10.2"
Llavors, cal instal.lar-ho com a dependència. Normalment seria un poetry install
però tenim el make init
per això
$ make init
poetry install
Installing dependencies from lock file
Package operations: 2 install, 0 updates, 0 removals
• Installing yapf (0.32.0)
• Installing toml (0.10.2)
⚠️ Què passa si rebem un missage com:
Installing dependencies from lock file
Warning: poetry.lock is not consistent with pyproject.toml. You may be getting improper dependencies. Run `poetry lock [--no-update]` to fix it.
Because pyxavi depends on yapf (^0.32.0) which doesn't match any versions, version solving failed.
Passa que tot el que intentem instal.lar està ja definit en l'arxiu poetry.lock
, que ens ajuda en general però per noves dependències es fa una mica un lio. El que hem de fer és dir-li que l'arxiu lock
el té antiquat i que l'ha de generar de nou amb:
$ poetry lock
Això farà que oblidi tot el que ha desat al poetry.lock
i que el generi de nou amb les noves dependències. I llavors ja podrem executar de nou el make init
i passar a usar el yapf
com esperàvem:
$ poetry run yapf -r --diff .
poetry run yapf -r --diff .
yapf: toml package is needed for using pyproject.toml as a configuration file
Què ha passat? Doncs que hem executat yapf
sense una configuració, perque yapf
necesita una mínima configuració dins d'un arxiu toml
. Ep! que en tenim un! doncs anem a afegir una secció nova:
[tool.yapf]
column_limit = 96
dedent_closing_brackets = 1
align_closing_bracket_with_visual_indent = 1
allow_split_before_dict_value = 0
blank_line_before_module_docstring = 1
each_dict_entry_on_separate_line = 1
split_all_top_level_comma_separated_values = 1
split_arguments_when_comma_terminated = 1
split_before_expression_after_opening_paren = 1
split_before_first_argument = 1
split_before_logical_operator = 0
Aquesta configuració dependrà de cadascú. És la que fem servir a l'equip i ens funciona prou bé. Ara, al executar la comanda d'abans:
$ poetry run yapf -r --diff . 1
--- ./pyxavi/simplelogger.py (original)
+++ ./pyxavi/simplelogger.py (reformatted)
@@ -22,7 +22,6 @@
if self.CONFIG["to_stdout"]:
handlers.append(logging.StreamHandler(sys.stdout))
-
# Define basic configuration
logging.basicConfig(
# Define logging level
@@ -35,5 +34,5 @@
# Define your own logger name
self._logger = logging.getLogger(name if name else self.CONFIG["name"])
- def getLogger( self) -> logging:
+ def getLogger(self) -> logging:
return self._logger
Aquí m'està indicant algunes falles en l'estil. El signe -
és el que ha trobat com a errada, i el signe +
és el que ell suggereix. Si no hi ha línia amb +
significa que la -
cal eliminar-la. Seguint error per error arribes a un punt on neteges el codi de falles, adherin-te a les regles d'estils que t'has configurat.
Llavors, com podem fer això més fàcil? Doncs afegint-ho com a target
nou al Makefile
que ja tenim:
.PHONY: yapf
yapf:
$(POETRY) run yapf -r --diff .
Això farà que amb la comanda make yapf
executem la validació de l'autoformatter.
I amb això tenim l'autoformatter afegit al projecte.
flake8
Fa un minut hem afegit un autoformatter al projecte. Ara un linter? Per què? Quina és la difernència entre un Formatter i un Linter?
Prenent-li la paraula a aquest blog post, els formatters arreglen el codi segons les guies d'estil i els linters marquen bugs i males pràctiques. Perfecte, però què és un linter?
Un linter és una aplicació que executa una sèrie de comprovacions de qualitatal codi per tal d'assegurar-se que:
Podeu llegir un article en el que parla de linters i en especial de flake8
, si encara necessiteu més per convencer-vos.
flake8
al projecteCom ja hem fet amb yapf
, volem definir flake8
com a dependència de desenvolupament, descarregar la dependència, i afegir-la al Makefile
per fer-la servir habitualment.
Primer de tot, editem l'arxiu pyprojecttoml
per tal d'incloure flake8
al projecte:
[tool.poetry.group.dev.dependencies]
yapf = "^0.32.0"
toml = "^0.10.2"
**flake8 = "^4.0.1"**
Llavors descarreguem les dependències amb el nostrat make init
$ make init
poetry install
Installing dependencies from lock file
Package operations: 4 installs, 0 updates, 0 removals
• Installing mccabe (0.6.1)
• Installing pycodestyle (2.8.0)
• Installing pyflakes (2.4.0)
• Installing flake8 (4.0.1)
⚠️ Podria ser que sortis el matex error referent a que l'arxiu lock
està antiquat. És cert, i per això hem de fer cas i executar el poetry lock
un altre cop per a generar un arxiu lock
amb les noves dependències i tot seguit podrem repetir el make init
.
Seguim configurant el flake8
afegint unes línies de configuració al pyproject.toml
. Podeu fer-vos una idea del que es pot configurar en aquest blog post. Nosaltres ho tenim definit prou minimalista:
[tool.isort]
profile = "hug"
line_length = 96
force_grid_wrap = 3
Per últim, i sabent que ho executarem sovint, millor afegir-ho al Makefile
sota un target
nou i així no ens oblidem:
.PHONY: flake8
flake8:
$(POETRY) run flake8 . \
--select=E9,F63,F7,F82 \
--show-source \
--statistics
# Full linter run.
$(POETRY) run flake8 --max-line-length=96 .
Amb això tindrem el Linter funcionant al nostre repositori.
pytest
I ara entrem a una zona ben interessant: els tests. Tot programador els coneix i diria que tothom definiria la seva relació amb ells com a "amor / odi". Els tests són unes petites aplicacions que executen una part específica del codi, assegurant-se que per un determinat conjunt de dades d'entrada tindrem sempre la mateixa sortida. Aquesta forma determinística de comprobar el resultat d'una operació coneguda ens dóna la certesa que l'algoritme que este executant és correcte (o no).
Hi ha tota una cultura la darrera dels tests. Et sona el mot TDD? Significa "Test Driven Development" i vol dir escriure primer un test definint quin és el resultat esperat, fallarà, i llavors ens dediquem a programar un algoritme per tal que el test funcioni. Hi havia una conya quan treballava a Softonic dient que TDD significava "Test Después del Desarrollo", perque en realitat et dediques a programar i quan sembla que el codi fa el que vols, hi afegeixes els tests per tal d'assegurar-te que funciona (eufemisme de "per tal que els companys t'acceptin el Pull Request).
Bromes a part, Python també té el seu framework de tests, i val la pena que li facis una ullada. De veritat que no entraré en els tests ara mateix, ja que és un tema prou dens en si mateix. Només diré que ens interessa tenir els tests al lloc que toca, tenir el pytest
com a dependència i que els tests puguin accedir al nostre codi, i tenir-los a mà dins del Makefile
:
Com ja comença a ser normal, primer de tot cal afegir pytest
com a dependència de desenvolupament:
[tool.poetry.group.dev.dependencies]
**pytest = "^7.0"**
yapf = "^0.32.0"
toml = "^0.10.2"
flake8 = "^4.0.1"
i també com és normal el segúent és resoldre les dependències, que (també) com hem dit abans el poetry.lock
podria estar desactualitzat i caldria fer un:
$ poetry lock
...ho diré un altre cop, resoldrem les dependències amb:
$ make init
En aquest punt ja estem preparats per fer servir els tests. Només cal executar la comanda:
$ poetry run pytest
Com és habitual, afegirem aquesta comanda al Makefile
:
.PHONY: test
test:
$(POETRY) run pytest
Ara només ens cal un arxiu de test on toca per a comprovar que el nostre codi és correcte. Per això afegirem el següent contingut a l'arxiu tests/test_logger.py
que hem creat al principi de l'article:
from pyxavi.simplelogger import SimpleLogger
import logging
CONFIG = {
"logger.format": "[%(asctime)s] %(levelname)-8s %(name)-12s %(message)s",
"logger.filename": "test.log",
"logger.to_file": False,
"logger.to_stdout": True,
"logger.loglevel": 45,
"logger.name": "testing"
}
def test_initialize_logger():
new_instance = SimpleLogger(CONFIG["logger.name"]).getLogger()
from_logging = logging.getLogger(CONFIG["logger.name"])
assert new_instance == from_logging
És un test molt senzill, comprobem que el logger que hem inicialitzat és el mateix que el que podem recuperar des de la instància logging
.
Tornem a executar el test:
$ poetry run pytest
=============================================== test session starts ================================================
platform darwin -- Python 3.10.1, pytest-7.2.0, pluggy-1.0.0
rootdir: /Users/xarnaus/Repositories/xavier/pyxavi2/pyxavi2
collected 1 item
tests/test_simplelogger.py . [100%]
================================================ 1 passed in 0.03s =================================================
Tot correcte. Com que sabem que executarem els tests sovint, afegim-los també al Makefilke
:
.PHONY: test
test:
$(POETRY) run pytest
Aconsello llegir molt més sobre els tests. Hi ha tècniques i estratègies molt bones com el Mock
i el Patch
que permeten aïllar la funció que estàs testejant, o els Fixtures per tenir un executor simple de tests contra un conjunt de dades específic per probar combinacions... Treballant amb els tests t'endinses en un món que et fa pensar el codi de forma diferent, dissenyant-lo d'una forma més naturalment abstracte, més senzilla de separar i de "testejar" les peces individualment. Potser trobareu aquest enllaç interessant: Effective Python testing with pytest.
Vale, tot això és molt maco: tenim tot d'èines a executar que ens identifiquen o fins i tot ens corregeixen errades o estils al nostre coldi. Però què et semblaria si cada cop que crees un Pull Request (acció d'enviar canvis en una branca de Git per a ser revisats pels altres contribuidors al projecte)? Per a mi és una gran ventatge poder assegurar-me que el codi que estic enviant a revisar és el més correcte possible, i no m'avergonyeixo de fer servir programes que m'ajuden a tenir un bon codi.
GitHub té una funcionalitat per configurar tot un seguit d'accions lligades a algun event del teu repositori. Per exemple l'acció més comú (per a mi) és el Pull Request. Hi ha d'altres, com el Release, però em centraré en el primer. En el meu cas, vull que a cada Pull Request i a cada git push
a la seva branca s'executin un seguit de comprobacions. El resultat d'aquestes es reflectirà en vermell (alguna comprobació ha fallat) o en verd (tot està correcte). Si totes elles acaben bé, entendré que el meu codi és un candidat a ser fusionat amb el reste del codi del repositori.
Per què voldria això? En un entorn de treball amb més programadors, les comprobacions automàtiques asseguren que el codi està escrit sota l'estil acordat, està lliure dels problemes més obvis i també que tots els tests han passat (en el cas que apliquem tot el descrit en aquest article). Això ja assegura que en gran part el codi és correcte, i llavors l'equip només ha d'assegurar-se que la sol.lució adreça el problema correctament, deixant les formalitats pels processos automàtics. I com a programador individual, també ho recomano com a mesura per aprendre i acostumar-se els estàndars de la comunitat, a més de la tranquil.litat de tenir uns tests comprobant tot el codi a cada canvi, tot automàtic.
Recomano llegir la documentació oficial. Els Github Actions són una plataforma de CD/CI (Continuous Deployment and Continuous Integration) que es pot usar per córrer accions senzilles com les meves en aquest article o complexes com empaquetar i distribuir el codi. Al meu nivell, ho faig servir com unes màquines virtuals que engeguen, es configuren, i executen una tasca específica sobre el codi rebut.
Primer de tot hem de saber que els workflows estan definits dins del directori .github/workflows
. Crearem el directori i també un arxiu nou anomenat pull_request.yml
:
$ mkdir -p .github/workflows
$ touch .github/workflows/pull_request.yml
...i obrim l'arxiu en el nostre editor preferit. L'estructura és senzilla: és un arxiu Yaml amb tres paràmetres al nivell principal:
name: Pull Request
on: [pull_request, workflow_dispatch]
jobs:
...
workflow_dispatch
. Podeu consultar la gran quantitat d'opcions que tenim aquí en la documentació oficial.I llavors, a continuació, tots i cadascun dels jobs, un per cada acció o comprobació. Deixa'm que t'afegeixi algunes notes:
targets
del Makefile
que hem anat preparant fins ara. Les accions predefinides es no són més que repositoris (versionats) de l'usuari actions
, així que podeu veure totes les accions llistant els seus repositoris. De la mateixa manera, es poden fer ús d'accions que algun altre usuari ha preparat, com és el cas que usem aquí per preparar el Poetry.A continuació, l'arxiu complet amb els jobs que utilitzo en el workflow:
name: Pull Request
on: [pull_request, workflow_dispatch]
jobs:
# Autoformatter
yapf:
name: Yapf
runs-on: [ubuntu-latest]
strategy:
matrix:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v2
- name: Setting up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setting up Poetry
uses: Gr1N/setup-poetry@v7
with:
poetry-preview: true
poetry-version: "1.2.2"
- name: Install Dependencies
run: |
make init
- name: Run Yapf
run: make yapf
# Linter
flake8:
name: Flake8
runs-on: [ubuntu-latest]
strategy:
matrix:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v2
- name: Setting up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setting up Poetry
uses: Gr1N/setup-poetry@v7
with:
poetry-preview: true
poetry-version: "1.2.2"
- name: Install Dependencies
run: |
make init
- name: Setup flake8 Annotations
uses: rbialon/flake8-annotations@v1
- name: Run flake8
run: make flake8
# Tests
tests:
name: Tests
runs-on: [ubuntu-latest]
strategy:
matrix:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v2
- name: Setting up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setting up Poetry
uses: Gr1N/setup-poetry@v7
with:
poetry-preview: true
poetry-version: "1.2.2"
- name: Install Dependencies
run: |
make init
- name: Run Tests
run: make test
Un cop l'arxiu ja està enviat al repository (commit
& push
), cada nou Pull Request executarà automàticament les comprobacions definides. També podem executar el workflow manualment des de la pàgina del repositori a Github, al menú Actions i llavors sel.leccionar "Pull Request", ja que és el nom que li hem posat:
En l'article anterior vam preparar l'estructura del repositori per a un aplicatiu Python. Vam passar per Poetry i pel Makefile, i vam afegir un codi d'exemple. En aquest article hi hem afegit unes comprobacions: el yapf
com a autoformatter, el flake8
com a linter i els tests amb pytest
. Llavors ho hem lligat tot en un workflow de Github Actions per tal d'executar totes les comprobacions cada cop que enviem un nou codi al repositori via Pull Request.
Amb això hauríem de sentir-nos coberts: el codi s'ajusta a un estil definit, és correcte i hem comprobat que realitza les funcions esperades, cada cop que parim codi nou.