Monorepos Python et uv : Quand les imports simples deviennent des puzzles complexes
Monorepos Python et uv : Quand les imports simples deviennent des puzzles complexes
"Ça devrait être simple", me suis-je dit en fixant mon écran. "Juste un from lib import monitoring_types
dans mon application FastAPI. Qu'est-ce qui pourrait mal se passer ?"
Dernières paroles célèbres. Ce qui a suivi fut une plongée profonde dans le packaging Python, les configurations de workspace, et l'art subtil de faire en sorte que différentes parties d'un monorepo communiquent réellement entre elles.
Le projet : Page Monitoring Made Modular
Laissez-moi planter le décor. Je construisais un système de monitoring de pages - pensez à un chien de garde personnel pour sites web qui vous alerte quand le contenu change. L'architecture semblait simple :
lib/
: Une bibliothèque partagée contenant toute la logique de monitoring, les types de données et les utilitairesapi/
: Un serveur FastAPI exposant les fonctionnalités de monitoring via des endpoints RESTcli/
: Une interface en ligne de commande pour les opérations de monitoring directes
Séparation classique des préoccupations. La bibliothèque gère la logique métier, tandis que l'API et la CLI ne sont que différentes interfaces vers la même fonctionnalité. Dans l'écosystème Node.js, ce serait un setup Lerna ou Nx standard. En Python avec uv... eh bien, c'est là que les choses sont devenues intéressantes.
La première tentative : "Ça devrait juste marcher"
Mon approche initiale était optimiste. J'ai structuré le projet avec trois packages séparés, chacun avec son propre pyproject.toml
. La lib serait installée comme dépendance dans les packages API et CLI. Simple, propre, logique.
$ tree -L 2
page-monitoring/
├── lib/ # Bibliothèque de monitoring centrale
├── api/ # Serveur FastAPI
├── cli/ # Interface en ligne de commande
└── pyproject.toml # Config workspace racine
Le premier uv run api/src/main.py
m'a ramené à la réalité :
ModuleNotFoundError: No module named 'lib'
Le combat avec les workspaces
C'est là que mes instincts de développeur pour le debugging ont pris le dessus. L'erreur était claire - l'API ne trouvait pas le package lib. Mais pourquoi ? La dépendance était déclarée dans api/pyproject.toml
:
dependencies = [
"fastapi[standard]>=0.116.1",
"lib",
]
Le problème n'était pas la déclaration - c'était de faire comprendre à uv que lib
était un membre du workspace, pas un package PyPI. Cela nécessitait la section [tool.uv.sources]
:
[tool.uv.sources]
lib = { workspace = true }
Mais même avec les sources de workspace configurées, je continuais à rencontrer des problèmes de contexte. Lancer uv run
depuis le mauvais répertoire, des commandes qui ne trouvaient pas le bon environnement Python, des imports qui marchaient en développement mais échouaient lors du packaging.
Le problème d'orchestration des commandes
C'est là que le vrai point douloureux est apparu. Ce que je voulais, c'était une ergonomie de développeur simple :
# Depuis la racine du projet, juste lancer :
uv run cli --help # Utiliser l'outil CLI
uv run fastapi dev api/app # Démarrer le serveur dev
Mais ce que j'obtenais à la place était une danse constante de changements de répertoire :
cd api && uv run fastapi dev app/main.py
cd ../cli && uv run python -m src.main --help
cd ..
La charge cognitive tuait mon flow. Chaque commande nécessitait un changement de contexte - pas seulement mentalement, mais littéralement naviguer entre les répertoires. Pour un développeur habitué à npm run dev
ou cargo run
qui fonctionnent depuis n'importe où dans le projet, cela ressemblait à un pas en arrière.
Le puzzle des imports : développement vs distribution
Un des aspects les plus déroutants était la différence entre comment les imports fonctionnent pendant le développement versus quand les packages sont réellement buildés et distribués. Pendant le développement avec des installations éditables, le système d'import de Python fonctionne d'une façon. Quand vous construisez des wheels et les installez, ça fonctionne différemment.
La solution impliquait de structurer soigneusement les configurations [tool.hatch.build.targets.wheel]
pour s'assurer que les packages soient construits correctement, tout en maintenant les relations de workspace qui rendent le développement possible.
Pour le package lib, cela signifiait :
[tool.hatch.build.targets.wheel]
packages = ["src/lib"]
Tandis que pour les packages consommateurs (API et CLI) :
[tool.hatch.build.targets.wheel]
packages = ["src"]
L'état d'esprit debugging appliqué
Ce qui m'a sauvé dans ce processus était de traiter cela comme le debugging d'un problème logiciel complexe :
- Isoler le problème : commencer par le cas d'import le plus simple possible
- Comprendre l'outil : lire la documentation uv en profondeur, pas juste la survoler
- Tester les hypothèses : vérifier chaque étape de la résolution de workspace
- Documenter la solution : le moi du futur remerciera le moi du présent
La percée est venue quand j'ai réalisé que je pensais aux packages Python comme je pense aux modules JavaScript - mais ce sont des bêtes fondamentalement différentes. Le système d'import de Python, combiné aux outils de packaging modernes comme uv, nécessite une approche plus explicite pour déclarer les relations entre packages.
La solution qui fonctionne
La percée est venue quand j'ai correctement configuré le pyproject.toml
racine pour exposer les scripts des sous-packages au niveau workspace :
pyproject.toml
racine :
[project]
name = "page-monitoring"
version = "0.1.0"
dependencies = ["cli", "api"]
[tool.uv.workspace]
members = ["lib", "cli", "api"]
[tool.uv.sources]
cli = { workspace = true }
api = { workspace = true }
lib = { workspace = true }
[project.scripts]
cli = "cli.src.main:cli"
pyproject.toml
API :
dependencies = [
"fastapi[standard]>=0.116.1",
"lib",
]
[tool.uv.sources]
lib = { workspace = true }
L'insight clé était qu'en déclarant les packages workspace comme dépendances dans le projet racine et en configurant correctement les points d'entrée des scripts, uv pouvait tout résoudre depuis le niveau supérieur.
Les commandes qui fonctionnent réellement
Avec le workspace correctement configuré, le workflow de rêve est finalement devenu réalité :
# Utiliser l'outil CLI depuis la racine du projet
uv run cli --help
uv run cli monitor https://example.com
# Démarrer le serveur dev FastAPI depuis la racine du projet
uv run fastapi dev api/app
# Installer toutes les dépendances du workspace
uv sync
# L'ancienne façon que je voulais éviter :
# cd api && uv run fastapi dev app/main.py
# cd ../cli && uv run python -m src.main --help
La magie était dans la section [project.scripts]
combinée aux dépendances de workspace. En faisant dépendre le projet racine de cli
et api
, et en exposant leurs points d'entrée comme scripts, uv pouvait tout résoudre dans le contexte du workspace.
L'avantage de l'IA
Tout au long de ce processus, j'ai trouvé les outils d'IA incroyablement utiles pour comprendre les nuances du système de workspace d'uv. Pouvoir coller des messages d'erreur et obtenir des explications contextualisées des concepts de packaging Python a considérablement accéléré le processus de debugging.
L'IA ne remplace pas l'approche de debugging systématique, mais elle peut rapidement faire remonter les sections de documentation pertinentes, suggérer des patterns de configuration, et aider à interpréter des messages d'erreur cryptiques.
Leçons pour les futurs monorepos
Si je devais commencer un projet similaire aujourd'hui, voici ce que je ferais différemment :
- Configurer les dépendances de workspace au niveau racine - faire dépendre votre projet racine des sous-packages
- Exposer les scripts dans le pyproject.toml racine pour les commandes que vous utilisez le plus
- Tester l'ergonomie des commandes tôt - si vous naviguez constamment avec cd, quelque chose cloche
- Documenter les commandes qui fonctionnent immédiatement quand vous les trouvez
L'écosystème de packaging Python est puissant mais complexe. Des outils comme uv l'améliorent, mais ils nécessitent encore de comprendre les concepts sous-jacents pour être utilisés efficacement.
Le parallèle avec le développement
Cette expérience a renforcé quelque chose que j'ai appris tout au long de ma carrière : les compétences de debugging que nous développons pour le code s'appliquent partout. Que vous traciez une condition de course dans un système distribué, diagnostiquiez des pannes matérielles, ou compreniez pourquoi vos imports de monorepo ne fonctionnent pas, la méthodologie reste cohérente.
Décomposer le problème, tester vos hypothèses, comprendre vos outils, et documenter vos solutions. Le domaine change, mais l'approche reste la même.
Le système de monitoring de pages fonctionne maintenant sans accroc, avec une séparation claire entre la bibliothèque centrale, le serveur d'API, et l'interface CLI. Les imports fonctionnent, les commandes s'exécutent, et les futurs contributeurs (y compris le futur moi) ont un chemin clair pour comprendre la structure du projet.
Parfois les sessions de debugging les plus précieuses ne concernent pas la correction de bugs dans notre code - elles concernent la compréhension des outils et systèmes qui rendent notre code possible en premier lieu.
Vous travaillez sur un monorepo Python ou vous vous battez avec les configurations de workspace uv ? J'aimerais entendre parler de vos expériences et solutions. Contactez-moi et partageons nos aventures de debugging.
Cet article a été rédigé avec l'aide de l'intelligence artificielle.