← Retour au Blog
Tutorials & How-Tos

Liste de contrôle pour uv build et publish : quand le backend uv suffit et quand il ne suffit pas

uv peut simplifier la construction et la publication de paquets Python, mais il reste nécessaire de démontrer que votre wheel ou votre distribution de sources fonctionne en dehors de votre espace de travail local. Cette liste de contrôle aide les mainteneurs à décider quand le backend de construction uv suffit, quand un autre backend est nécessaire et comment vérifier les artefacts avant publication.

Rédigé par Hamza Diaz
3 octobre 202610 min de lecture90 vues

uv build peut faciliter la publication de paquets Python, mais ne garantit pas à lui seul la validité du wheel produit ou de la distribution de sources produite. Un projet peut résoudre les dépendances de son fichier de verrouillage, réussir ses tests depuis la copie de travail et démarrer dans Docker, tout en échouant chez une personne qui installe l’artefact publié.

Cet échec tient généralement à un simple décalage : un fichier local, une dépendance définie par un chemin, un répertoire de données du paquet ou un module importé par un script console était présent dans l’espace de travail, mais absent de la distribution. Cette liste de contrôle s’adresse aux mainteneurs qui veulent utiliser uv sans confondre un dépôt fonctionnel avec un paquet prêt à être publié. Les commandes ci-dessous sont des vérifications proposées pour votre projet. Elles n’ont pas été exécutées dans le cadre de ce travail, et aucune sortie de commande n’a été inventée.

La règle : tester l’artefact, pas la copie de travail

uv fournit aux mainteneurs un même outil pour la gestion de projet, la construction des distributions et la publication. Le guide officiel des paquets documente uv build et uv publish, et le guide du backend présente uv_build comme backend de construction. Ces outils sont utiles. Ils laissent néanmoins une question essentielle sans réponse : un utilisateur externe peut-il installer et utiliser ce que vous prévoyez de publier ?

Un utilisateur reçoit des métadonnées et des fichiers provenant d’un wheel, d’un sdist ou d’un index de paquets. Il ne reçoit pas votre copie de travail en mode éditable, la configuration des sources de l’espace de travail, une dépendance privée définie par un chemin, le cache des couches Docker ou un dossier de données local, sauf si ces éléments font partie de l’artefact ou sont accessibles à partir des métadonnées déclarées. C’est pourquoi l’artefact mérite ses propres tests.

C’est la même démarche fondée sur les preuves que dans l’observabilité de la construction d’un moteur TensorRT. Un message de progression ne constitue pas une preuve. En matière de création de paquets, des tests locaux au vert n’équivalent pas à une installation par un utilisateur à partir d’un wheel.

flowchart TD A[Le projet fonctionne dans le checkout] --> B[Choisir le backend deliberement] B --> C[Construire la wheel et le sdist] C --> D[Inspecter les archives par nom exact] D --> E[Reconstruire depuis le sdist si pertinent] E --> F[Installer la wheel hors du checkout] F --> G[Importer le module et charger les donnees du package] G --> H{Pret a publier ?} H -->|Non| B H -->|Oui| I[Publier, puis verifier depuis l'index cible]

Quand uv_build suffit et quand il ne suffit pas

Le backend de construction uv est un bon candidat pour un paquet en Python pur à l’organisation classique : modules classiques, structure src claire, métadonnées simples, données du paquet simples et aucun point d’accroche de publication propre au backend. Une petite bibliothèque ou une CLI dotée d’une API publique limitée constitue un meilleur premier candidat à la migration qu’un paquet qui compile aussi du code natif ou assemble des artefacts générés lors de la publication.

La limite actuelle est importante : uv_build ne prend actuellement en charge que le Python pur. Si le paquet lui-même construit des modules d’extension, un autre backend est nécessaire. C’est la limite documentée. uv peut néanmoins être utile comme frontend associé à un autre backend, mais le remplacement du backend doit attendre que l’équivalence des artefacts ait été démontrée. Les projets Python utilisant le GPU ou proches du code natif, comme les charges de travail abordées dans le test d’acceptation du parcours NVIDIA Warp, doivent maintenir cette distinction. Empaqueter une couche d’interface Python ne revient pas à compiler toutes les dépendances qu’elle peut appeler.

Type de projetChoix du backendCe qu’il faut démontrer avant publication
Bibliothèque en Python pur organisée sous srcuv_build est un candidat raisonnableLes imports à partir du wheel fonctionnent, le sdist permet de reconstruire le wheel et les données du paquet sont présentes
Paquet CLI simpleuv_build peut convenirLe script console installé appelle le module inclus dans le paquet
Paquet avec des fichiers générésDécider une fois le processus de génération clarifiéLes fichiers générés sont présents dans les deux artefacts ou reconstruits à partir du sdist
Paquet avec des modules d’extensionUtiliser un autre backendLes étapes de construction native, les tags de plateforme et les chaînes d’outils sont pris en charge
Projet setuptools ou Hatchling mature et personnaliséConserver d’abord le backend actuelLe frontend uv fonctionne sans perdre les mécanismes de publication existants

Exemple détaillé : invoice-normalizer

Prenons un petit paquet hypothétique pour donner aux vérifications un objet concret à inspecter. Ce paquet normalise des dictionnaires de factures et inclut une table de correspondance des devises dans ses données. Il possède aussi un script console, ce qui offre une occasion supplémentaire de détecter des erreurs de création de paquets.

pyproject.toml :

[build-system]
requires = ["uv_build>=0.12.22,<0.13"]
build-backend = "uv_build"

[project]
name = "invoice-normalizer"
version = "0.1.0"
description = "Normalize simple invoice payloads."
readme = "README.md"
requires-python = ">=3.11"
dependencies = []

[project.scripts]
invoice-normalizer = "invoice_normalizer:main"

README.md n’est pas facultatif dans cet exemple, car pyproject.toml y fait référence. Créez-le avant de construire les distributions :

# invoice-normalizer

Normalize simple invoice payloads for packaging checks.

src/invoice_normalizer/__init__.py :

from importlib.resources import files
import json

def normalize_invoice(payload: dict) -> dict:
    currency = payload.get("currency", "USD").upper()
    data = json.loads(files(__package__).joinpath("data/currencies.json").read_text())
    if currency not in data["supported"]:
        raise ValueError(f"unsupported currency: {currency}")
    return {"invoice_id": str(payload["invoice_id"]), "currency": currency}

def main() -> None:
    print(normalize_invoice({"invoice_id": 123, "currency": "usd"}))

src/invoice_normalizer/data/currencies.json :

{"supported": ["USD", "EUR", "GBP"]}

Il ne s’agit pas d’un système de facturation destiné à la production. L’exemple est volontairement petit. Il comporte une fonction publique importable, un script console et des données du paquet, soit précisément les éléments qui fonctionnent souvent depuis la copie de travail, mais échouent depuis le wheel construit.

Construire et inspecter chaque archive en utilisant son nom exact

Construisez d’abord les deux distributions :

uv build

Inspectez ensuite les fichiers attendus en utilisant leur nom exact. Évitez les exemples approximatifs reposant sur des caractères génériques dans la documentation et la CI : des artefacts obsolètes peuvent rester dans dist et donner l’impression qu’une vérification est plus concluante qu’elle ne l’est réellement.

python -m zipfile --list dist/invoice_normalizer-0.1.0-py3-none-any.whl
python -m tarfile --list dist/invoice_normalizer-0.1.0.tar.gz

Recherchez invoice_normalizer/__init__.py, invoice_normalizer/data/currencies.json, les métadonnées du projet, les fichiers README utilisés le cas échéant, les fichiers de licence s’ils sont déclarés et les métadonnées du point d’entrée du script console. Le résultat attendu est que l’artefact contienne tous les fichiers dont votre API publique a besoin après installation. Il s’agit d’un résultat attendu, pas d’une sortie de commande enregistrée dans le cadre de ce travail.

VérificationCommande ou élément à examinerRésultat attendu
Contenu du wheelpython -m zipfile --list ...whlLe module et le fichier de données sont présents dans le wheel
Contenu du sdistpython -m tarfile --list ...tar.gzLes sources, les fichiers servant aux métadonnées et le fichier de données sont présents dans le sdist
Installation propre du wheelInstaller le wheel exact hors de la copie de travailLe chemin d’import correspond au paquet installé, pas au dépôt
Chargement des données du paquetAppeler normalize_invoice après installationLes données JSON sont disponibles via importlib.resources
Test rapide de la CLIExécuter la commande invoice-normalizer installéeLe point d’entrée renvoie au code inclus dans le paquet

Reconstruire à partir du sdist

Si vous publiez un sdist, démontrez qu’il permet de reconstruire le wheel à partir des fichiers qu’il contient. Cette vérification détecte une autre catégorie d’erreurs que l’inspection du premier wheel. L’absence d’un README, d’un fichier généré ou d’un répertoire de données peut passer inaperçue jusqu’à ce qu’un utilisateur construise le paquet à partir des sources.

REPO_DIR="$PWD"
SDIST_PATH="$REPO_DIR/dist/invoice_normalizer-0.1.0.tar.gz"
SDIST_CHECK_DIR="$(mktemp -d)"

cd "$SDIST_CHECK_DIR"
python -m tarfile --extract "$SDIST_PATH"
cd invoice_normalizer-0.1.0
uv build --wheel
python -m zipfile --list dist/invoice_normalizer-0.1.0-py3-none-any.whl

Le résultat attendu est un wheel reconstruit à partir du contenu extrait du sdist, puis une inspection de l’archive confirmant que le module et les données du paquet sont toujours présents. Considérez cette séquence comme une proposition de validation, pas comme un compte rendu d’exécution des commandes.

Installer le wheel hors de la copie de travail

Pour vérifier l’installation dans un environnement propre, créez cet environnement hors du dépôt, installez le wheel exact à l’aide d’un chemin absolu, puis vérifiez les imports et les données du paquet depuis cet emplacement. Ne considérez pas l’exécution de uv run depuis la copie de travail des sources comme une preuve que le paquet est correctement constitué.

REPO_DIR="$PWD"
WHEEL_PATH="$REPO_DIR/dist/invoice_normalizer-0.1.0-py3-none-any.whl"
WHEEL_CHECK_DIR="$(mktemp -d)"

cd "$WHEEL_CHECK_DIR"
uv venv .venv
. .venv/bin/activate
uv pip install "$WHEEL_PATH"
python -c 'from invoice_normalizer import normalize_invoice; assert normalize_invoice({"invoice_id": 7, "currency": "eur"}) == {"invoice_id": "7", "currency": "EUR"}'
python -c 'from importlib.resources import files; assert files("invoice_normalizer").joinpath("data/currencies.json").is_file()'
invoice-normalizer

L’assertion d’import vérifie la fonction publique. L’assertion sur les ressources vérifie que le fichier JSON est inclus dans le paquet, et pas seulement présent dans l’arborescence des sources. La commande CLI vérifie que les métadonnées du point d’entrée permettent son exécution après installation. Là encore, il s’agit de résultats attendus qu’un mainteneur doit consigner dans la CI ou les notes de version ; cet article ne prétend pas que ces commandes ont été exécutées localement.

Ce que prouve uv build --no-sources

uv build --no-sources est utile, mais il faut définir précisément la portée de cette vérification. Il désactive tool.uv.sources lors de la résolution des dépendances de construction déclarées dans build-system.requires. Cela peut révéler des hypothèses implicites sur la configuration des sources de ces dépendances.

Il ne résout pas lui-même les dépendances à l’exécution. Il n’audite pas les imports effectués à l’exécution. Il ne détecte pas tous les paquets non déclarés que votre code importe après installation. Si un module importe requests à l’exécution, mais oublie de le déclarer, --no-sources n’est pas la vérification qui permet de démontrer cette erreur. Une installation propre, suivie de tests rapides des imports et des scénarios d’utilisation, est plus adaptée pour la détecter.

Utilisez --no-sources comme l’une des vérifications du processus de publication, pas comme son critère décisif de validation. S’il échoue, déterminez à qui le paquet est destiné. Les paquets publics doivent avoir des dépendances accessibles dont les versions sont précisées. Pour les paquets privés, les modalités d’accès à l’index doivent être documentées. Les paquets réservés à un usage interne dans un espace de travail ne doivent pas être publiés comme si des utilisateurs externes pouvaient les installer. L’ancienne discussion sur GitHub au sujet de la séparation du backend uv offre un contexte pratique utile, mais le ticket 3957 ne doit pas être considéré comme la preuve d’un bug actuel de configuration des sources.

{
  "checklist": "Outside-Consumer Build Check",
  "minimumSignals": ["wheel built", "sdist built", "archives inspected by exact name", "wheel installed outside checkout", "import and package data verified"],
  "noSourcesBoundary": "Checks build dependency resolution without tool.uv.sources; it is not a runtime dependency audit."
}

Vérifications avec Docker et lors de la publication

Docker est utile pour disposer d’une version figée d’uv, d’une synchronisation reproductible des dépendances et d’un environnement de CI propre. Il peut aussi masquer des erreurs de création de paquets si l’image copie directement l’arborescence des sources ou réutilise des couches en cache. Vérifiez séparément l’image applicative et la distribution. Un conteneur qui démarre ne prouve pas que le wheel contient currencies.json.

Avant uv publish, confirmez le nom du paquet, sa version, la version de Python requise, les dépendances, les fichiers README utilisés, les métadonnées de licence et la configuration de l’index cible. Après publication, vérifiez la version exacte depuis l’index cible en vous plaçant du point de vue d’un utilisateur. La réussite du téléversement prouve seulement que le registre a accepté les fichiers. Elle ne prouve pas que les utilisateurs peuvent importer le paquet, exécuter la CLI ou charger les données qu’il contient.

PUBLISH_CHECK_DIR="$(mktemp -d)"
TARGET_INDEX_URL="https://test.pypi.org/simple"

cd "$PUBLISH_CHECK_DIR"
uv venv .venv
. .venv/bin/activate
uv pip install --index-url "$TARGET_INDEX_URL" "invoice-normalizer==0.1.0"
python -c 'from invoice_normalizer import normalize_invoice; assert normalize_invoice({"invoice_id": 42}) == {"invoice_id": "42", "currency": "USD"}'
python -c 'from importlib.resources import files; assert files("invoice_normalizer").joinpath("data/currencies.json").is_file()'
invoice-normalizer

Pour une publication réelle, faites pointer TARGET_INDEX_URL vers l’index sur lequel vous avez effectivement publié, et ajoutez explicitement les paramètres d’authentification requis ou toute configuration extra-index nécessaire. Le résultat attendu est une installation propre de la version exacte depuis cet index, suivie des mêmes vérifications des imports, des données et de la CLI qu’avant publication.

Erreurs courantes

La première erreur consiste à assimiler la réussite du verrouillage des dépendances à celle de la publication. Un fichier de verrouillage constitue une preuve liée au contexte du projet, pas à la distribution. Une autre erreur courante consiste à modifier en même temps le backend, l’organisation des dépendances, la CI et les identifiants de publication. Cela rend les échecs plus difficiles à interpréter. Changez une couche à la fois.

Une troisième erreur consiste à ignorer le sdist parce que le wheel s’est installé localement. Certains utilisateurs construisent encore les paquets à partir des sources, et un sdist défectueux peut être la première cause d’échec qu’ils rencontrent. Une quatrième consiste à supposer que Docker a répondu à la question de la validité de l’artefact. Ce n’est le cas que si le conteneur a installé le même wheel ou le même sdist qu’un utilisateur recevra.

Le meilleur chemin d’adoption est presque banal, ce qui est un compliment dans la gestion des publications : utilisez d’abord uv comme frontend, conservez le backend actuel s’il définit déjà les mécanismes de publication et adoptez uv_build uniquement lorsque les caractéristiques du projet s’y prêtent. Traitez le wheel et le sdist comme le produit. Validez-les depuis l’extérieur de la copie de travail. Choisissez le backend une fois que les vérifications des artefacts le justifient.

Points clés

  • 1uv simplifie les processus de construction et de publication de paquets Python, mais les artefacts doivent toujours être validés du point de vue d’un utilisateur externe.
  • 2uv_build ne prend actuellement en charge que les projets en Python pur ; les modules d’extension nécessitent un autre backend.
  • 3uv build --no-sources vérifie la résolution des dépendances de construction sans tool.uv.sources, pas l’exhaustivité des dépendances à l’exécution.
  • 4Une installation propre à partir des artefacts construits, hors de la copie de travail, est l’indicateur local le plus probant avant publication.
  • 5La reproductibilité de Docker ne prouve pas la validité du wheel ou du sdist.

Conclusion

uv est le plus utile lorsqu’il rend la création de paquets plus facile à comprendre, pas lorsqu’il masque des hypothèses liées à la publication derrière une commande plus rapide. Traitez le wheel et le sdist comme le produit, validez-les depuis l’extérieur de la copie de travail et choisissez uv_build uniquement lorsque le paquet est en Python pur et que les artefacts construits apportent les preuves nécessaires à la publication.

Questions fréquentes

Que crée uv build ?

uv build crée des artefacts de distribution Python, tels que des wheels et des distributions de sources, selon le projet et la commande utilisés.

Quand dois-je utiliser le backend de construction uv ?

Utilisez uv_build pour les paquets compatibles en Python pur à l’organisation classique. Utilisez un autre backend lorsque le paquet construit des modules d’extension ou nécessite un fonctionnement personnalisé du backend.

À quoi sert uv build --no-sources ?

Il désactive tool.uv.sources lors de la résolution des dépendances de construction déclarées dans build-system.requires, ce qui aide à révéler les hypothèses implicites sur les sources de ces dépendances. Il ne s’agit pas d’un audit des dépendances à l’exécution.

Une image Docker fonctionnelle prouve-t-elle que mon paquet Python est correct ?

Non. Docker peut copier des fichiers source ou utiliser des couches en cache qui masquent des éléments absents du wheel ou du sdist.

Que dois-je vérifier après publication ?

Installez la version publiée depuis l’index cible dans un environnement propre, puis vérifiez les imports, les scripts console, les données du paquet et un scénario d’utilisation minimal.

Sources

Partager cet article

Hamza Diaz

Rédigé par

Hamza Diaz

Hamza Diaz est le fondateur d’Optijara, où il conçoit des agents IA pratiques, des systèmes d’automatisation et des workflows Copilot pour les entreprises de services. Il écrit sur les opérations IA, la stratégie d’agents et la mise en œuvre concrète pour les équipes qui veulent des systèmes utiles plutôt que du battage médiatique.