D'un dossier de scripts à une bibliothèque installable
Du code qui vit dans un dossier de scripts fonctionne sur une machine, pour une personne, jusqu'au jour où quelqu'un d'autre en a besoin ou où vous en avez besoin dans un autre environnement. L'empaquetage transforme votre code en bibliothèque avec un nom, une version et des dépendances déclarées que chacun peut installer avec pip install. C'est ainsi qu'une équipe partage une bibliothèque de valorisation sans copier des fichiers, qu'un notebook de recherche et une tâche de production exécutent exactement le même code, et qu'un correctif atteint tout le monde par un nouveau numéro de version.
# Versions are numbers, not text: compare them as tuples
def parse_version(text):
parts = [int(part) for part in text.split(".")]
return tuple(parts + [0] * (3 - len(parts)))
print("1.10" > "1.9") # False as text, "1" < "9" decides it: wrong
print(parse_version("1.10") > parse_version("1.9")) # True as numbers (1, 10, 0) is newer than (1, 9, 0)
print(parse_version("2")) # (2, 0, 0)Arborescence, pyproject.toml et construction
Une disposition courante met le code dans src/yourlib/ (avec un __init__.py), les tests dans tests/ et un pyproject.toml à la racine. Ce fichier unique nomme l'outil de construction (la table [build-system]) et décrit le projet : son name, sa version, une description, les versions de Python prises en charge et ses dependencies. pip install -e . installe le dossier en mode éditable pendant le développement, donc les modifications prennent effet aussitôt ; python -m build produit une wheel (.whl, prête à installer) et une sdist (archive source) dans dist/. Mettez la version à un seul endroit et exposez-la comme yourlib.__version__ si les utilisateurs en ont besoin.
# A minimal pyproject.toml, generated and then read back with tomllib to prove it is valid
import tomllib
def pyproject_toml(name, version, dependencies):
lines = ["[build-system]", 'requires = ["setuptools>=68"]', 'build-backend = "setuptools.build_meta"', "",
"[project]", f'name = "{name}"', f'version = "{version}"']
if dependencies:
lines += ["dependencies = ["] + [f' "{d}",' for d in dependencies] + ["]"]
else:
lines.append("dependencies = []")
return "\n".join(lines) + "\n"
text = pyproject_toml("qcpricing", "0.3.0", ["numpy>=1.26", "pandas>=2"])
data = tomllib.loads(text)
print(data["project"]["name"], data["project"]["version"]) # qcpricing 0.3.0
print(data["project"]["dependencies"]) # ['numpy>=1.26', 'pandas>=2']Versions et spécificateurs de version
La plupart des bibliothèques suivent le versionnage sémantique : MAJEUR.MINEUR.CORRECTIF. Une correction de bug incrémente le correctif (1.4.2 en 1.4.3), une fonctionnalité rétrocompatible incrémente le mineur et remet le correctif à zéro (1.5.0), et un changement cassant incrémente le majeur (2.0.0). Les dépendances se déclarent avec des spécificateurs : numpy>=1.26,<3 accepte tout NumPy de la 1.26 jusqu'à la 3 exclue. Soyez aussi souple que vous pouvez l'être honnêtement : une borne basse pour la plus ancienne version testée, une borne haute seulement pour une version majeure dont vous savez qu'elle vous casse. Figer des versions exactes (==) relève du fichier de verrouillage d'une application, pas d'une bibliothèque que d'autres installent.
# Bumping a version and checking a requirement, with versions compared as tuples
import operator
def parse_version(text):
parts = [int(part) for part in text.split(".")]
return tuple(parts + [0] * (3 - len(parts)))
def bump_version(version, part):
major, minor, patch = parse_version(version)
return {"major": f"{major + 1}.0.0", "minor": f"{major}.{minor + 1}.0", "patch": f"{major}.{minor}.{patch + 1}"}[part]
OPS = {">=": operator.ge, "<": operator.lt, "==": operator.eq}
def satisfies(version, specifier):
for clause in specifier.split(","):
symbol = next(op for op in (">=", "==", "<") if clause.startswith(op))
if not OPS[symbol](parse_version(version), parse_version(clause[len(symbol):])):
return False
return True
print(bump_version("1.4.2", "minor")) # 1.5.0
print(satisfies("1.26.4", ">=1.26,<3"), satisfies("3.0.0", ">=1.26,<3")) # True False# One place for the version: read it from the source instead of repeating it in several files
import re
init_py = """__version__ = "0.3.0"
from .pricing import price
"""
match = re.search(r'^__version__ = "([^"]+)"', init_py, re.MULTILINE)
version = match.group(1)
print(version) # 0.3.0
print(tuple(int(part) for part in version.split("."))) # (0, 3, 0)- L'empaquetage donne au code un nom, une version et des dépendances déclarées, installable avec
pip install; développez avecpip install -e ., construisez avecpython -m build. pyproject.tomlcontient le système de construction et les métadonnées[project]; comparez les versions comme des tuples de nombres, jamais comme du texte.- Utilisez le versionnage sémantique (
MAJEUR.MINEUR.CORRECTIF) et des spécificateurs souples (numpy>=1.26,<3) ; ne changez jamais une version publiée et gardez les secrets hors du paquet.
À suivre : CI/CD pour le code quantitatif, exécuter automatiquement les contrôles à chaque modification.
Lab : ce qui est noté
- Comparer les versions comme des nombres avec
parse_version25 pts - Publier la version suivante avec
bump_version25 pts - Vérifier une exigence avec
satisfies25 pts - Décrire le paquet avec
pyproject_toml25 pts