L'altre dia li donava una volta al mòdul de Python que mantinc, amb les llibreries bàsiques que uso en els meus projectes personals. Tenia ganes d'aplicar les comprovacions que en el meu equip apliquem al nostre codi, com a pràctica i com a repàs personal, i ja de pas documentar-ho en una sèrie d'articles.

En aquest primer parlo de l'estructura bàsica d'un mòdul en Python i de les èines que hi utilitzo, Poetry i el Makefile, com a preparació de la segona part en la que parlaré d'autoformat, linter, tests i un xic d'automatització.

A grans trets

En tot l'article em basaré en el repo pyxavi que tinc públic a GitHub. No és per tirar cohets, però em serveix tan pel meu codi com per a zona de proves, ja que són poques classes i fan coses prou concretes. I a partir d'aquí, parlaré de:

  1. Estructura bàsica d'un mòdul de Python
  2. El pyproject.toml juntament amb Poetry
  3. Inicializació del projecte amb Poetry
  4. El Makefile com a accessos ràpids a comandes

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. Estructura bàsica d'un paquet de Python

python-logo A l'hora de treballar en un (altre) projecte en Python et trobes repetint molts cops les mateixes operacions bàsiques: llegir d'un arxiu de configuració, registrar una acció al Log, ... El primer que ve al cap és abstraure codi a llibreries. El segon és si podríem tenir aquestes llibreries incús fora del projecte. El tercer és si no hi ha un gestor de paquets o mòduls que ens facin la vida més fàcil. La resposta a totes es si. El que anem a fer aquí és preparar un repositori de GitHub per a fer-lo servir com a mòdul extern de Python, com si fos qualsevol dels paquets que ja estàs acostumat a incloure en el teu programa com a dependència. Sobreentenc que tens una conta a GitHub. La majoria d'accions aquí funcionen en qualsevol gestor de repositoris: Gitlab, Gitea, ...

Sigui com sigui:

  1. Crea un nou repositori. Entenc que hi afegeixes un arxiu README i un .gitignore genèric per codi Python. Potser vols donar un cop d'ull a l'article sobre llicències de software que vaig escriure. D'ara endavant li diré a aquest repositori pyxavi.
  2. Clona'l al teu local. El més segur és que hauràs de configurar les claus d'accés si no ho has fet ja anteriorment.
  3. Obre el codi amb el teu editor preferit.

Tal com estem ara, hauries de tenir una estructura tal com:

repositori
├── .gitignore
├── README.md
└── LICENSE

Anem a afegir alguns directoris i alguns arxius. Tot el següent, va a l'arrel del repositori.

Registre de canvis CHANGELOG.md

M'agrada tenir les coses endreçades, i saber quan es van fer certes accions. Aquest és un arxiu Markdown amb un format bastant simple: un títol amb la versió i una llista dins amb les accions que es fan per a cadascuna:

# v0.0.1

- Created package
- `Logger`

A mesura que es van tenint noves versions, s'afegeixen entrades a sobre, per tal que quedi ordenat del més nou al més vell.

Directori per al codi

Tenint en compte que això és un mòdul, que s'usarà des d'una altra aplicació, l'habitual directori src/ passa a anomenar-se de la mateixa manera que el mòdul que publicarem. Hi ha altres formes de fer-ho, aquest cas s'anomena flat layout o adhoc, podeu veure altres estils aquí. Per què uso aquesta estructura? Primer, perque a l'equip es va decidir així. Però per què? Perque és simple i també perque es va decidir usar Poetry com a gestor de dependències i paquets (vegeu més avall), i és l'estructura que aquest usa per defecte.

Afegim un arxiu de codi

Per anar muntant-ho, afegim un arxiu Python que contindrà la nostra primera classe, diguem-li el Logger, dins del directori del codi. Els arxius van tots amb minúscules, creem-ne un: pyxavi/logger.py. Per tal que l'article tingui una mica de sentit, posem-hi una mica de contingut, per que faci alguna cosa. Simplifico el codi del meu logger real:

import logging
import sys

class SimpleLogger:

    CONFIG = {
        "format": "[%(asctime)s] %(levelname)-8s %(name)-12s %(message)s",
        "to_file": True,
        "filename": "debug.log",
        "to_stdout": True,
        "log_level": 20,
        "name": "custom_logger"
    }

    def __init__(self, name: str = None) -> None:
        log_format = self.CONFIG["format"]

        handlers = []
        if self.CONFIG["to_file"]:
            handlers.append(logging.FileHandler(self.CONFIG["filename"]))
        if self.CONFIG["to_stdout"]:
            handlers.append(logging.StreamHandler(sys.stdout))

        # Define basic configuration
        logging.basicConfig(
            # Define logging level
            level=self.CONFIG["log_level"],
            # Define the format of log messages
            format=log_format,
            # Declare handlers
            handlers=handlers
        )
        # Define your own logger name
        self._logger = logging.getLogger(name if name else self.CONFIG["name"])

    def getLogger( self) -> logging:
        return self._logger

Afegim l'arxiu init

Què és un arxiu init? És un arxiu que va a tots i cadascun dels directoris que contenen codi Python, i que identifica aquest directori com a package o paquet. És el primer codi que s'executa al carregar un paquet. Pot contenir codi però molt habitualment està buit: pyxavi/__init__.py. Podeu llegir més sobre els arxius init per exemple a aquí.

Directori per als tests

Hem dit que cobrirem el codi amb tests. Tot i que ho farem més endavant, anem a crear l'estructura de directori i afegir algun arxiu buit. El directori estarà a l'arrel de repositori i es dirà tests/, com a convenció. Podem anomenar el directori en singluar? Doncs si: el paquet pytest descobreix automàticament on tens els arxius de test, així que dóna una mica igual. Pel que he vist depén de cadascú i en el meu equip usem el plural per que (ehem) tenim varis arxius de test 😉 Aquí hi ha una entrada a Stackoverflow relacionada.

Afegim un arxiu de test

Els arxius de test en Python tenen la convenció d'anomenar-se test_[nom de la classe].py, en el nostre cas tests/test_logger.py. Més endavant ens endinsarem en aquest arxiu, de moment creem-lo buit.

Afegim l'arxiu init

Els tests no són més que arxius de codi, per tant afegim també l'arxiu init: tests/__init__.py

Estat final

Així doncs, tenim una estructura com la següent:

repository
├── .gitignore
├── README.md
├── LICENSE
├── pyxavi
│   ├── __init__.py
│   └── logger.py
└── tests
    ├── __init__.py
    └── test_logger.py

2. El pyproject.toml i la gestió de paquets amb Poetry

poetryPrimer de tot val a dir que una cosa no té molt a veure amb l'altre. El pyproject.toml és un arxiu per a definir els aspectes del "build" l'aplicació / mòdul / paquet en Python. Ve com a evolució de l'ecosistema anterior i el merder d'èines per fer el mateix sense ordre ni seny (parlo de l'arxiu requirements.txt, el setup.py ...). No entraré aquí, però si teniu interès us recomano llegir aquest parell de posts que explica molt bé el per què i què permet. Si us va la marxa, també és interessant aquest document on es decideix el PEP 518 i descriu altres propostes descartades. Al nostre nivell, veiem un arxiu de text classificat per seccions que defineixen certes parts del mòdul.

Poetry és un gestor de paquets i dependències per a Python. Si no n'has sentit a parlar, el més segur és que uses pip. Doncs la següent "evolució" vindria a ser poetry. Per fer-te cinc cèntims, pots també llegir aquest article facilot, o aquest una mica més profund. Poetry fa ús del estàndard PEP 518, de manera que tenim un entorn modern on tot està ben entrellaçat. L'arxiu pyproject.toml conté les especificacions del paquet i tot el que Poetry necessita per funcionar. A més, més avall tenim altres utilitats que volem incloure i que també es configuren en el pyproject.toml, així que sembla ser una decisió fàcil.

Abans de continuar, assegura't d'instal.lar Poetry. Ves a la pàgina de la documentació oficial de Poetry i segueix els passos descrits allà.

A continuació, anem a escriure el primer pyproject.toml:

[tool.poetry]
name = "pyxavi"
version = "0.0.1"
description = "Set of utilities to assist on simple Python projects"
authors = ["Xavier Arnaus <***@*****.***>"]

[tool.poetry.dependencies]
python = "^3.9"

[build-system]
requires = ['poetry-core~=1.0']
build-backend = 'poetry.core.masonry.api'

Val a dir que veient això, sembla que el pyproject.toml sigui un arxiu propietari de Poetry. Diguem que l'evolució va a base d'iteracions. Poetry suporta l'estàndar PEP 518 però al darrera en venen més. Ara mateix s'està perfilant l'adopció de l'estàndar PEP 621 on les seccions que Poetry utilitza passen a ser les estàndar (molt interessant aquest Pull Request). Sigui com sigui, avui és avui i així és com ho configurem, tenint un ull al que la documentació oficial de Python descriu.

Doncs bé, amb aquest pyproject.toml tenim definit el bàsic. Ara volem veure que tot engega i funciona. De fet, l'únic que hem de fer és demanar-li a Poetry que instal.li les dependènies i que generi el poetry.lock, útil per a que les dependències quedin expressament definides i, en cas que treballéssim en un entorn amb varis desenvolupadors, tothom usi el mateix entorn eliminant així inconsistències:

$ poetry install
Creating virtualenv pyxavi-mv5KdjAA-py3.10 in /Users/****/Library/Caches/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies... (0.1s)

Writing lock file
Installing the current project: pyxavi (0.0.1)

Com que no usem llibreries externes no és que hagi fet massa cosa, només ;'arxiu poetry.lock nou i ja està. Però amb això ja podem veure que tenim el projecte llest i funcionant, i més endavant veurem com tot s'entrelliga.

3. Inicialització del projecte amb Poetry

Tot el que hem fet fins ara ens ha anat molt bé per explicar els conceptes bàsics, l'estructura de directoris, els arxius que hi han dins, per a què serveix cada peça. Fins i tot hem repassat l'èina principal per a manegar paquets i dependències. I si et dic que tot això es pot fer amb una sola comanda de Poetry?

Fem una cosa: ves fora del repositori (en el meu cas, això és al meu directori repos) i entra la següent comanda:

$ poetry new pyxavi2
Created package pyxavi2 in repos

Apa, ja tenim tota l'estructura creada. Espera, que en vols més? Anem a fer que el Poetry ens generi de forma interactiva el pyproject.toml. Entra al directori pyxavi2 i borra l'arxiu pyproject.toml que s'ha creat amb la comanda anterior:

$ rm pyproject.toml

I ara li demanem al Poetry que ens "inicialitzi el projecte"

$ poetry init

This command will guide you through creating your pyproject.toml config.

Package name [pyxavi2]:
Version [0.1.0]:  0.0.1
Description []:  I am a description
Author [Xavi <***@****>, n to skip]:
License []:
Compatible Python versions [^3.10]:

Would you like to define your main dependencies interactively? (yes/no) [yes] n

Package to add or search for (leave blank to skip):

Would you like to define your development dependencies interactively? (yes/no) [yes] n
Generated file

[tool.poetry]
name = "pyxavi2"
version = "0.0.1"
description = "I am a description"
authors = ["Xavi <***@****>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Do you confirm generation? (yes/no) [yes]

A falta del CHANGELOG.md que trobo tan important, tot està tal qual ho hem preparat durant les seccions anteriors. Ara que ja hem vist el funcionament del Poetry, tornem al directori del nostre repositori i continuem des d'allà.

4. El Makefile com a accessos ràpids a comandes

gnu-gpl-logoHi ha tot un conjunt de comandes que executem habitualment en el projecte i que (no ens enganyem) acabem oblidant els paràmetres o es tornen tediosos. A l'equip tenim la costum de crear un Makefile amb targets que ens ajuden en aquestes tasques. És veritat, els Makefile no estàn pensats per això, es fan servir per compilar programes. Però al cap i a la fi són formes d'abstraure instruccions de forma senzilla i funcionen prou bé. Llegeix més aquí sobre què son els Makefile.

Anem a crear un Makefile senzillet per les nostres necessitats actuals:

POETRY ?= poetry

.PHONY: init
init:
    $(POETRY) install

Què fa això? La primera línia defineix una variable d'entorn POETRY a la que (si no té valor, d'aquí l'interrogant) li assigno poetry. D'aquesta manera puc abstraure la comanda de forma que només haig de mantenir-ho a un sol lloc. Coses de programadors... La línia .PHONY defineix que el target a continuació no es referix a un arxiu. Podeu llegir més aquí. La línia init: és la definició del target, ara a continuació veureu què és un target. L'última línia és la comanda que realment volem executar, que un cop les substitucions estan fetes vindria a ser poetry install

Llavors, sapiguent que he d'estar al mateix directori que el Makefile, i que el nostre target és init, la instrucció vindria a ser:

$ make init

Això internament executarà poetry install. Per què volem tot això? Perque en les següents seccions començarem a afegir comandes com a targets nous al Makefile per tal de simplificar-les.

Epíleg

Arribats a aquest punt, tenim un projecte en un repositori Git, amb una estructura que ens permet usar el conjunt de classes com a un paquet en un altre projecte. Tenim una èina que ens gestiona les dependències i que amb prou feines n'hem explorat les funcionalitats, i tenim un espai on regitrar dreceres per fer-nos la vida més fàcil quan comencem a afegir comprobacions. Podem dir que estem preparats pel següent article, on afegirem comprobacions per assegurar un codi correcte. 😊

Referències

Previous Post Next Post