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.

A grans trets

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:

  1. L'autoformatter yapf
  2. El linter flake8
  3. Els tests a Python amb pytest
  4. Els GitHub Actions per autoexecutar-ho tot en un Pull Request

Disclaimer

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!

1. L'autoformatter 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...

Problemes per no seguir un estil definit

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:

  • Tenir un estil definit ajuda a identificar ràpidament incongruències, aquell espai fora de lloc, o aquella estructura illegible...
  • Diferents estils porten a que el Git entengui canvis al codi quan no hi ha canvi real. Imagina que el teu company amplia el teu codi, i mentre el.labora afegeix espais als parèntesis. Al final decideix que no hi havia per tant i funcionalment no canvia res, però el diff de l'arxiu et marca multitud de canvis, fen-te perdre un temps preciós revisant canvis que no ho són.
  • En serio, és un bon merder. Pots estar jugant al ping pong de canvis amb els companys, i a la llarga has d'invertir molt més temps en entendre el codi quan cada peça té un estil diferent.

La sol.lució, decidir i ajustar-te a un estil definit

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 format

googlelogoYapf é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.

2. El linter flake8

flake8Fa 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:

  • El teu codi s'adhereix als estàndars de la indústria per al teu llenguatge
  • No has comès errors de sintaxi, typos, errors de format o incorreccions en l'estil.
  • Ajuda a estalviar-te temps a tu i al teu equip assenyalant errors evidents.

Podeu llegir un article en el que parla de linters i en especial de flake8 , si encara necessiteu més per convencer-vos.

Afegir flake8 al projecte

Com 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.

3. Els tests a Python amb pytest

pytestI 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.

4. Els GitHub Actions autoexecutant-se a cada Pull Request

gh_actionsVale, 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.

I tot això em cal?

Screenshot%202022-12-07%20at%2013.56.23 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.

En vull saber més!

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.

Crear un Github Action - Pull Request.

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:
    ...
  • name: és el nom que li donem a aquest workflow
  • on: són les accions que faran que aquest workflow s'executi. En el nostre cas volem que s'executi a cada Pull Request i també vull poder-ho executar manualment, per això el workflow_dispatch. Podeu consultar la gran quantitat d'opcions que tenim aquí en la documentació oficial.
  • jobs: cada una de les "feines" (una feina tindrà vàries tasques) que volem que s'executin (vegeu més avall)

I llavors, a continuació, tots i cadascun dels jobs, un per cada acció o comprobació. Deixa'm que t'afegeixi algunes notes:

  • Fixa't que els jobs venen tabulats
  • S'admeten comentaris
  • Cada step és cada tasca a fer. Pensa en un job com a un docker linux pelat. Necessites unes tasques prèvies que et portin el sistema al punt de poder executar la comprobació en sí (que també serà una step ella mateixa). Jo aquí faig ús d'algunes accions ja predefinides com instal.lar una base i el Python, instal.lar Poetry, i tot seguit fer ús dels meus propis 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: manual-workflow

Epíleg

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.

Referències

Previous Post Next Post