Vai al contenuto

Python Scaffolding

Pubblicato:

Premessa

L’obiettivo è quello di descrivere la creazione e utilizzo di un progetto python che sia strutturato in maniera compatibile con il layout src usato da molti framework e tool.

Creazione del progetto

Vi sono più modi per inizializzare il progetto. Quello diretto e manuale non prevede dipendenze in questa fase, mentre affidandosi ad un tool esterno come PyScaffold si può ottenere lo stesso risultato lasciando che sia la libreria a creare la struttura di base.

Inizializzazione manuale

Creare una cartella con il nome del progetto, ad esempio <project>. All’interno di questa cartella, i file più importanti da creare sono:

<project>
├── README.md      # Descrizione del progetto
├── pyproject.toml # Configurazione del progetto, dipendenze e tool
├── tox.ini        # Configurazione di tox (opzionale)

├── docs           # Cartella contenente la documentazione
  ├── _static     # Cartella contenente file statici
  ├── conf.py     # Configurazione della documentazione
  ├── index.rst   # Indice della documentazione
  └── requirements.txt # Dipendenze della documentazione

├── tests          # Cartella contenente i test
  └── conftest.py # Configurazione dei test

└── src            # Cartella contenente il codice sorgente
    └── <project>
        ├── __main__.py # File che viene eseguito quando si lancia il progetto (opzionale)
        └── __init__.py # File che rende la cartella un package

Vedremo successivamente come configurare i vari file.

Inizializzazione con PyScaffold

PyScaffold è uno strumento che permette di creare la struttura di base di un progetto python. Per installarlo:

pip install --upgrade pyscaffold
# si può anche usare pipx
pipx install pyscaffold
# o anche conda
conda install -c conda-forge pyscaffold

Dopo averlo installato, è sufficiente eseguire il comando putup per creare la struttura di base del progetto:

putup <project>

Tutta la struttura generata può poi essere modificata a piacimento. Vedi PyScaffold per maggiori informazioni.

Configurazione

Dopo aver creato la struttura del progetto, è necessario configurare alcuni file per poterla utilizzare. Ovviamente le scelte da compiere dipendono tantissimo dalla natura e dagli obiettivi dello stesso, per cui mi limiterò a descrivere delle configurazioni abbastanza generiche che dovrebbero adattarsi alla maggior parte dei casi.

pyproject.toml

Il file pyproject.toml è il file principale di configurazione del progetto. Introdotto con PEP518 e successivamente esteso, si tratta a tutti gli effetti di un componente standard con lo scopo di definire dipendenze, metadati e requisiti al fine di poter buildare e distribuire il progetto. Per maggiori informazioni, vedi PEP518 e PEP621, nonché la pagina di pip.

La struttura potrebbe essere la seguente:

# Build system che andremo ad usare
# In questo caso, setuptools, che è il più diffuso
[build-system]
requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"]
build-backend = "setuptools.build_meta"

# Informazioni sul nostro progetto.
# I campi obbligatori sono 'name' e 'version'
[project]
name = "<project>"
version = "0.0.1" # Versione hardcoded del package
# oppure
# dynamic = ["version"] # In questo caso, va aggiunta la sezione '[tool.setuptools.dynamic]'
description = "Add a short description here!"
readme = "README.md"
requires-python = ">=3.8" # Versione di python richiesta. In questo caso, python 3.8 o superiore
license.file = "LICENSE"
authors = [{ name = "<author>", email = "<author>@mail.com" }]
maintainers = [{ name = "<maintainer>", email = "<maintainer>@mail.com" }]
keywords = ["python"] # Parole chiave del progetto
classifiers = [ # Classificazione del progetto (vedi https://pypi.org/classifiers/)
    "Development Status :: 4 - Beta",
    "Programming Language :: Python",
]
# Tutte le dipendenze che il progetto richiede per funzionare
# In questo caso, importlib-metadata (qualsiasi versione), PyYAML (versione 5.x), requests (versione < 3) e subprocess32 (solo per python < 3.9)
dependencies = [
    "importlib-metadata",
    'PyYAML ~= 5.0',
    'requests[security] < 3',
    'subprocess32; python_version < "3.2"',
]

# Tutte le dipendenze opzionali o aggiuntive del progetto.
# Possono essere installate con `pip install <project>[<dependency>]`
# e.g. `pip install <project>[testing]`
[project.optional-dependencies]
testing = ["pytest", "pytest-cov"]
linting = ["pylint", "black", "mypy", "isort"]

# URLs del progetto
[project.urls]
Homepage = "https://tendto.github.io/dasaturn/"
Documentation = "https://tendto.github.io/dasaturn/api/modules"
Repository = "https://github.com/TendTo/dasaturn.git"
Changelog = "https://github.com/TendTo/dasaturn/blob/main/.github/CHANGELOG.md"

# Permette di creare degli script che chi installa il progetto
# potrà eseguire direttamente da terminale.
# Nell'esempio, `fibonacci` è uno script che esegue la funzione `run` del modulo `skeleton` del package `<project>`
# e.g. `fibonacci 10`
[project.scripts]
fibonacci = "<project>.skeleton:run"

# Configurazione di Setuptool (build system)
[tool.setuptools]
include-package-data = true # Includi tutti i file non python nella distribuzione
zip-safe = false # Non comprimere il package in un file zip (deprecated)
package-dir = { "" = "src" } # Directory del package
packages = ["<project>"] # Lista di packages da includere nella distribuzione

# Se si vuole usare una versione dinamica, è necessario aggiungere questa sezione
# e specificare il campo '__version__' nel file '__init__.py' del package
# [tool.setuptools.dynamic]
# version = { attr = "<project>.__version__" }

# Configurazione di Pytest (test runner)
[tool.pytest.ini_options]
minversion = "6.0" # Versione minima di pytest
addopts = [ # Opzioni aggiuntive da passare a pytest.
    "--cov=<project>", # Effettua una coverage del package <project>
    "--cov-report=term-missing", # Mostra la coverage in terminale
    "--cov-report=html", # Mostra la coverage in un file html
    "--cov-fail-under=80", # Fallisce se la coverage è minore dell'80%
    "--verbose", # Mostra i test che vengono eseguiti
]
testpaths = ["tests"] # Cartelle contenenti i test

# Configurazione di Black (code formatter)
[tool.black]
target-version = ['py38', 'py39', 'py310', 'py311'] # Versioni di python da supportare
line-length = 120 # Lunghezza massima delle righe
include_trailing_comma = false # Aggiungi una virgola alla fine di una lista o di un dizionario
include = '(src|tests)\/.*\.py' # Include solo i file python nella cartella src e tests

# Configurazione di Isort (import sorter)
[tool.isort]
profile = "black" # Usa il profilo black, per evitare conflitti tra i due tool

# Configurazione di Pylint (code linter)
[tool.pylint.MASTER]
fail-under = '10.0' # Fallisce se la valutazione è minore di 10

[tool.pylint.'MESSAGES CONTROL']
disable = ["missing-module-docstring"] # Disabilita gli errori di tipo 'missing-module-docstring'

[tool.pylint.format]
max-line-length = 120 # Lunghezza massima delle righe

tox.ini (opzionale)

Tox è uno strumento che permette di automatizzare la build e i test di un progetto. Il suo utilizzo assicura che il progetto sia compatibile con tutte le versioni di python e con tutti i sistemi operativi che si vogliono supportare. Tutte i comandi vengono eseguiti in un ambiente virtuale, isolato dal resto del sistema.

Per installarlo:

pip install --upgrade tox
# si può anche usare pipx
pipx install tox

Il file di configurazione è tox.ini e la struttura potrebbe essere la seguente:

# Configurazioni globali di Tox
[tox]
requires = tox>=4 # Versione minima di tox
envlist = py{38,39,310,311}-{test,lint} # Lista di ambienti da eseguire nulla è specificato
isolated_build = True # Builda in un ambiente virtuale isolato

# Configurazioni di default per tutti gli ambienti
[testenv]
passenv =
    SETUPTOOLS_*

# Ambiente di test che supporta python 3.8, 3.9, 3.10 e 3.11
[testenv:py{,38,39,310,311}-test]
description = Invoke pytest to run automated tests
setenv =
    TOXINIDIR = {toxinidir} # Directory del progetto
passenv = # Variabili d'ambiente da passare all'ambiente di test
    HOME
    SETUPTOOLS_*
extras = testing # Installa le dipendenze opzionali 'testing' (vedi pyproject.toml)
commands = # Comandi da eseguire
    pytest {posargs}

# Ambiente di linting che supporta python 3.8, 3.9, 3.10 e 3.11
[testenv:py{,38,39,310,311}-lint]
description = Perform static analysis and style checks
extras = # Installa le dipendenze opzionali 'linting' e 'testing' (vedi pyproject.toml)
    linting
    testing
setenv = # Variabili d'ambiente da passare all'ambiente di linting
    PATHS = {toxinidir}/src {toxinidir}/tests
commands = # Comandi da eseguire
    black --check {posargs:{env:PATHS}}
    pylint {posargs:{env:PATHS}}
    mypy {posargs:{env:PATHS}}
    isort --check-only --diff {posargs:{env:PATHS}}

# Ambiente di build e clean
[testenv:{build,clean}]
description =
    build: Build the package in isolation according to PEP517, see https://github.com/pypa/build
    clean: Remove old distribution files and temporary build artifacts (./build and ./dist)
skip_install = True # Non installa il package, si limita a buildarlo
changedir = {toxinidir} # Directory di lavoro che verrà usata per eseguire i comandi
deps = # Installa la dipendenza 'build'
    build: build[virtualenv]
commands =
    clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]'
    clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]'
    build: python -m build {posargs}
# Di default, vengono buildati sia `sdist` che `wheel`. Se l'sdist è troppo grande o non si vuole
# renderla disponibile, considerare di eseguire: `tox -e build -- --wheel`

# Ambiente per il testing, verifica e creazione della documentazione
[testenv:{docs,doctests,linkcheck}]
description =
    docs: Invoke sphinx-build to build the docs
    doctests: Invoke sphinx-build to run doctests
    linkcheck: Check for broken links in the documentation
setenv = # Variabili d'ambiente da passare all'ambiente di testing
    DOCSDIR = {toxinidir}/docs
    BUILDDIR = {toxinidir}/docs/_build
    docs: BUILD = html
    doctests: BUILD = doctest
    linkcheck: BUILD = linkcheck
deps =
    -r {toxinidir}/docs/requirements.txt
commands = # Richiama sphinx-build con i parametri specificati
    sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs}

# Ambiente per la pubblicazione del package
[testenv:publish]
description =
    Publish the package you have been developing to a package index server.
    By default, it uses testpypi. If you really want to publish your package
    to be publicly accessible in PyPI, use the `-- --repository pypi` option.
skip_install = True
changedir = {toxinidir}
passenv = # Variabili d'ambiente da passare all'ambiente di testing (https://twine.readthedocs.io/en/latest/)
    TWINE_USERNAME
    TWINE_PASSWORD
    TWINE_REPOSITORY
    TWINE_REPOSITORY_URL
deps = twine # Installa la dipendenza 'twine'
commands = # Comandi da eseguire. Di default utilizza la repo 'testpypi'
    python -m twine check dist/*
    python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/*

Sviluppare il progetto

Quello che segue sono le operazioni più comuni da compiere durante lo sviluppo del progetto. Verrà presentata sia la variante manuale che quella automatizzata con Tox, qualora lo si voglia utilizzare.

Note

Se si lavora senza Tox, è comunque consigliabile creare un ambiente virtuale. Per farlo, basta eseguire python -m venv .venv nella cartella del progetto ed assicurarsi di attivarlo prima di eseguire i comandi con source .venv/bin/activate (linux) o .venv\Scripts\activate (windows).

Installare le dipendenze

# Installa il progetto localmente in modalità editable.
# Ogni modifica fatta al codice verrà automaticamente applicata.
pip install -e .
# Installa le dipendenze opzionali 'testing' e 'linting'
pip install -e .[testing,linting]

Eseguire i test

# Installa le dipendenze per i test
pip install -e .[testing]
# Esegue i test (pip)
pytest
# Esegue i test (tox)
tox -e py-test
# Esegue i test su tutte le versioni di python supportate (tox)
tox -e py{38,39,310,311}-test

Eseguire il linting

# Installa le dipendenze per il linting
pip install -e .[linting]
# Esegue il linting (pip)
black --check src tests
pylint src tests
mypy src tests
isort --check-only --diff src tests
# Esegue il linting (tox)
tox -e py-lint
# Esegue il linting su tutte le versioni di python supportate (tox)
tox -e py{38,39,310,311}-lint

Eseguire la build

# Installa le dipendenze per la build
pip install build
# Esegue la build (pip)
python -m build
# Esegue la build (tox)
tox -e build

Eseguire la documentazione

# Installa le dipendenze della documentazione
pip install -r docs/requirements.txt
# Esegue la documentazione (pip)
sphinx-build --color -b html -d "docs/_build/doctrees" "docs" "docs/_build/html"
# Esegue la documentazione (tox)
tox -e docs

Pubblicare il progetto

# Installa le dipendenze per la pubblicazione
pip install twine
# Esegue la pubblicazione (pip)
python -m twine upload dist/*
# Esegue la pubblicazione (tox)
tox -e publish

CI

Per automatizzare il processo di build e test, è possibile utilizzare un servizio di CI come GitHub Actions. Per configurarlo, è sufficiente creare dei file .github/workflows/<ci>.yml con una serie di passaggi che il runner dovrà eseguire. Ecco alcuni esempi che utilizzano Tox, ma che si possono facilmente adattare per utilizzare solo pip.

name: "Lint Python code"

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.8", "3.9", "3.10", "3.11"]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install tox

      - name: Test with tox
        run: tox -e py-lint
name: "Test Python code"

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.8", "3.9", "3.10", "3.11"]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install tox

      - name: Test with tox
        run: tox -e py-test
name: "Docs test, build and deploy"

on:
  push:
    branches:
      - main

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install tox

      - name: Check all links
        run: |
          tox -e linkcheck

      - name: Doc test
        run: |
          tox -e doctests

  docs:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ammaraskar/sphinx-action@master
        with:
          docs-folder: "docs/"

      - name: Setup Pages
        uses: actions/configure-pages@v3

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: docs/_build/html/

      - name: Deploy to GitHub Pages
        id: docs
        uses: actions/deploy-pages@v2
name: "Build Python package"

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install tox

      - name: Test with tox
        run: tox -e build
name: "Publish Python package"

on:
  push:
    tags:
      - "*"

jobs:
  publish:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install tox

      - name: Build with tox
        run: tox -e build

      - name: Publish to PyPI
        run: tox -e publish
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
          TWINE_REPOSITORY: testpypi

Contenuti aggiuntivi

Code of Conduct

Il code of conduct è un documento che descrive le regole di comportamento che tutti i partecipanti al progetto devono seguire. Può essere inserito facilmente nel progetto aggiungendo un file CODE_OF_CONDUCT.md con il contenuto preso dal template di Contributor Covenant.

Pull Request Template

Il pull request template è un documento che permette a chi è in procinto di aprire una pull request di descrivere in maniera più dettagliata le modifiche che ha apportato seguendo uno schema prestabilito.
Per fare si che GitHub lo riconosca, è necessario creare un file .github/PULL_REQUEST_TEMPLATE.md con il contenuto che si vuole.
Un possibile template è il seguente:

### Prerequisites

- [ ] I have read and understood the [contributing guide](https://github.com/<TODO>/blob/main/.github/CONTRIBUTING.rst).
- [ ] The commit message follows the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) guidelines.
- [ ] Introduced tests for the changes have been added (for bug fixes / features).
- [ ] Docs have been added/updated (for bug fixes / features).

### Description

Brief description of what this PR is about.

### (If applicable) Issue closed by this PR

- [closes #issue_number]

### Does this PR introduce a breaking change?

- [ ] Yes
- [ ] No

#### (If yes) What are the changes that might break existing applications?

Description of the changes that might break existing applications.

### Python version you are using

Python version you are using (e.g. 3.8.5).
You can find it out by running `python --version` in your terminal.

### Other information

Any other information that is important to this PR, such as inspirations, further development plans, possible issues left to be solved, etc.

Varie ed eventuali

Altri file interessanti da aggiungere al progetto sono: