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:
.github/CONTRIBUTING.rst
: Guida per i contributori.github/CHANGELOG.md
: Registro dei cambiamenti.github/ISSUE_TEMPLATE.md
: Template per la creazione di issue. Può essere specializzato.github/ISSUE_TEMPLATE/bug_report.md
: Template per la creazione di issue di tipo bug.github/ISSUE_TEMPLATE/feature_request.md
: Template per la creazione di issue di tipo feature
.github/AUTHORS.md
: Lista degli autori