5 points par GN⁺ 2025-03-31 | 1 commentaires | Partager sur WhatsApp
  • Même pour un petit script de test d’API, il faut généralement aligner l’environnement d’exécution à cause des dépendances. Avec le shebang d’exécution uv, on peut en faire un script exécutable immédiatement, sans procédure d’installation.
  • L’exemple jam_users.py teste l’endpoint /users d’une API Go : il supprime et recrée des utilisateurs avec httpx, IPython et loguru, puis ouvre un REPL.
  • Avec la méthode classique, il faut soit installer les paquets globalement dans le Python système, soit préparer manuellement un environnement virtuel, ce qui rend le partage et la réexécution du script fastidieux.
  • En déclarant dependencies = ["ipython", "httpx", "loguru"] dans l’en-tête # /// script, puis en lançant uv run jam_users.py, uv se charge de l’environnement isolé et de l’installation des dépendances.
  • En ajoutant le shebang #!/usr/bin/env -S uv run --script et les droits d’exécution, on peut l’exécuter sur un système Unix avec seulement uv installé, via ./jam_users.py.

Déclarer les dépendances dans le script

  • L’exemple jam_users.py prépare des données utilisateur de test pour l’API locale http://localhost:4000/v1/users.
    • Envoie des requêtes API avec httpx
    • Ouvre le REPL de IPython pour inspecter les réponses et poursuivre les tests
    • Journalise les suppressions et créations avec loguru
  • Le script de base suit un flux qui réinitialise la liste des utilisateurs avant de recharger les données de test.
    • Récupère la liste existante avec GET /v1/users
    • Appelle DELETE /v1/users/{id} pour chaque utilisateur
    • Envoie la liste d’utilisateurs préparée en JSON vers POST /v1/users
    • Lance ensuite un REPL avec IPython.embed()
  • Pour l’exécuter avec python jam_users.py, httpx, IPython et loguru doivent déjà être installés dans l’environnement d’exécution.
  • On peut aussi les installer globalement dans le Python système ou créer un environnement virtuel séparé, mais les deux approches demandent une préparation avant l’exécution.
    • L’installation globale peut encombrer le Python système avec des paquets
    • Avec un environnement virtuel, il faut gérer soi-même la création, l’activation, l’installation et l’exécution
    • Dans les deux cas, il faut aussi disposer d’un Python système compatible avec les paquets requis

Créer un script exécutable directement avec uv

  • En plaçant le tag # /// script de uv en haut du script, on peut déclarer les dépendances directement dans le fichier.
# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • Une fois cet en-tête présent, on peut lancer le script avec uv run jam_users.py.
    • uv crée un environnement virtuel isolé pour le script
    • Télécharge et installe les dépendances nécessaires
    • Exécute ensuite le script dans le contexte de cet environnement virtuel
  • Un shebang Python classique comme #!/usr/bin/env python ne permet pas d’exploiter l’en-tête de script de uv, car Python ignore les commentaires # /// script.
  • En appelant uv directement dans le shebang, on peut traiter le script comme un exécutable.
#!/usr/bin/env -S uv run --script

# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • Le flag -S de env indique que la chaîne qui suit doit être découpée en arguments distincts et transmise à env.
  • Après avoir accordé les droits d’exécution avec chmod +x jam_users.py, on peut l’exécuter directement ainsi :
./jam_users.py
  • Avec cette méthode, tant que uv est installé sur le système Unix, le script peut être exécuté sans installation séparée des dépendances ni gestion manuelle d’un environnement virtuel.
  • Lorsqu’on transmet un script Python complexe à d’autres utilisateurs, cela réduit la charge consistant à devoir expliquer en détail toute la préparation du système avant l’exécution.

1 commentaires

 
GN⁺ 2025-03-31
Avis Hacker News
  • Ce n’est pas UV en particulier, mais plus généralement la façon de contrôler l’exécution du code via des commentaires qui me dérange.
    Utiliser des commentaires pour des directives de linter ou des notes de développeur, très bien, mais s’il s’agit de configuration ou de données liées à l’exécution, une forme comme UV_ENV = { "dependencies": { "requests": "2.32.3", "pandas": "2.2.3" } } me semble bien meilleure.
    Cette approche est une syntaxe Python valide, utilise des structures de données standard plutôt qu’un parsing arbitraire de commentaires, ce qui facilite la génération et la validation, et surtout respecte le principe selon lequel le code doit s’exécuter de la même manière même si l’on supprime tous les commentaires.

    • Je suis d’accord, mais j’aimerais aller encore plus loin.
      La méthode proposée reste une constante magique qui ne fait rien au runtime et n’est parsée que par analyse statique ; un autre outil pourrait aussi la voir comme du code inutilisé et la supprimer au lieu de la laisser à uv.
      À la place, on pourrait faire un appel direct à uv pour lui dire quoi faire, par exemple import uv suivi de uv.exec(dependencies=["clown"], python=">=3.10").
      La première exécution se ferait avec n’importe quel runtime Python capable de trouver ce paquet uv hypothétique ; le paquet uv préparerait ensuite l’environnement virtuel et le runtime Python, puis ferait un re-exec(3) avec un indicateur, par exemple une variable d’environnement.
      Lors du second runtime, uv.exec détecterait cet indicateur et ne ferait rien.
    • Ce n’est pas quelque chose qu’uv a inventé : comme d’autres outils, il utilise le standard PEP 723.
      https://peps.python.org/pep-0723/
    • C’est pertinent, mais parser et évaluer du code impératif me semble beaucoup plus difficile et moins flexible que de conserver des données déclaratives, conformément au principe du moindre privilège.
      Et la ligne shebang elle-même fonctionne en fait comme un commentaire.
      Elle est tellement profondément ancrée depuis 45 ans que les gens ne pensent plus vraiment au fait qu’il s’agit d’un commentaire shell.
    • La condition « le code doit s’exécuter de la même manière même si l’on supprime les commentaires » reste vraie.
      Si les mêmes dépendances sont installées, le code s’exécute de la même manière.
      Cela ne change pas vraiment le sens du code lui-même, mais plutôt l’environnement dans lequel il s’exécute ; en ce sens, ce n’est pas différent du commentaire #!/bin/bash en tête d’un script shell.
    • Tout à fait d’accord, et j’espère que ce type de forme finira par être standardisé.
      Cela dit, uv ne voudra probablement pas exécuter le code pour déterminer les dépendances ; il faudra donc que ce soit un sous-ensemble très limité de la syntaxe Python.
      Le simple fait d’avoir besoin de ce genre de chose révèle une faiblesse du langage.
      L’instruction import elle-même devrait pouvoir transmettre toutes les informations relatives aux dépendances.
  • Ces derniers mois, ce sujet est revenu très souvent sur HN ; voici des exemples récents :
    https://news.ycombinator.com/item?id=43500124
    https://news.ycombinator.com/item?id=42463975
    J’aime bien uv, mais j’ai du mal avec l’expression self-contained pour deux raisons.
    Premièrement, pour exécuter le script, uv doit déjà être installé.
    On peut certes faire un script shell qui vérifie si uv est installé et, sinon, l’installe via curlpipe, mais cela ajoute pas mal de boilerplate, et la méthode curlpipe elle-même n’est pas idéale.
    Deuxièmement, créer automatiquement un environnement virtuel quelque part dans le répertoire personnel n’est pas vraiment self-contained.
    Même si on l’exécute une seule fois puis qu’on supprime le script, cet environnement virtuel reste là et occupe de l’espace ; je n’ai pas trouvé dans la documentation d’uv de garantie indiquant que ces environnements virtuels temporaires sont nettoyés automatiquement.

    • La critique selon laquelle uv est nécessaire et qu’en son absence il faut l’installer manuellement ou utiliser curl | sh est valable.
      Cela dit, à mesure que les gestionnaires de paquets commenceront à inclure uv dans leurs dépôts, ce sera de moins en moins un problème.
      Par exemple, uv est déjà disponible dans Alpine Linux et Homebrew : https://repology.org/project/uv/versions
      De plus, les métadonnées de script inline font partie du standard Python.
      Si uv n’est pas présent sur le système et n’est pas empaqueté, mais qu’une version de Python compatible avec le script est disponible, on peut l’exécuter avec pipx : https://pipx.pypa.io/stable/examples/#pipx-run-examples
      pipx est empaqueté beaucoup plus largement : https://repology.org/project/pipx/versions
    • Pour résoudre ce genre de problème, j’ai bricolé un shebang docker+uv, et ça fonctionne jusqu’à un certain point.
      Sur une machine de développeur quelconque, Docker peut être plus courant qu’uv, et comme l’article parle aussi d’un projet d’entreprise, ça paraissait plausible.
      En revanche, pour l’instant il ne met rien en cache, donc il retélécharge à chaque exécution, ce qui est maladroit ; cela devrait pouvoir se corriger avec des volumes.
      Cela ressemble à ceci : https://hugojosefson.github.io/docker-shebang/#python
    • En général, pour exécuter un programme sur un ordinateur, il faut bien installer quelque chose au préalable, donc l’installation d’uv en elle-même ne me paraît pas si problématique.
      Cela dit, si l’exécution télécharge depuis Internet des éléments dont on ne connaît pas vraiment la nature, il est difficile d’appeler cela self-contained ; le vrai self-contained complet ressemble davantage à une AppImage.
    • D’accord à 100 %.
      Avec quelque chose comme py2exe, on peut créer un « script Python » self-contained.
      Cela crée beaucoup de problèmes pour le développeur, mais en minimise pour l’utilisateur.
    • Petite précision : grâce à la déduplication des paquets d’uv, un virtualenv n’occupe pas d’espace tant qu’il n’a pas de dépendances uniques.
  • On peut faire la même chose avec Nix, avec une ligne shebang qui ressemble à ceci
    #! nix-shell -i python3 -p "python312.withPackages (pkgs: [ pkgs.boto3 pkgs.click ])"
    Ainsi, la seule chose nécessaire sur le système est Nix ; Python n’a même pas besoin d’être installé

    • C’est vrai, mais beaucoup de paquets PyPI ne sont pas encore empaquetés dans nixpkgs, donc ce n’est pas une approche aussi universelle que uv
    • Comme le dit précisément l’article original, uv installe Python à la volée quand c’est nécessaire
    • La même technique peut s’appliquer à n’importe quel langage
      L’exemple le plus obvious est un script bash avec toutes ses dépendances déclarées, et j’ai déjà fait de petits scripts Rust mono-fichier rapides avec un shebang Nix
      https://nixos.wiki/wiki/Nix-shell_shebang
    • La condition d’avoir Nix installé est beaucoup plus contraignante que celle d’avoir uv
    • Je serais curieux de savoir comment faire la même chose avec la commande basée sur les flakes, nix shell, plutôt qu’avec nix-shell
  • Comme le disent d’autres commentaires, l’affirmation « self-contained » repose sur le fait que uv soit installé
    Si vous voulez vraiment un script Python self-contained, le compilateur Nuitka vaut le détour
    Nous l’utilisons sans problème en production pour des services gRPC, et il suffit de lancer nuitka --onefile run.py
    Comme c’est un compilateur, le binaire obtenu peut aussi être plus rapide que le programme Python d’origine empaqueté avec PyInstaller
    La page GitHub de l’auteur dit : « La mission de ma vie est d’essayer, jusqu’à mourir de vieillesse, de créer le meilleur Python Compiler possible »
    https://nuitka.net/
    https://github.com/kayhayen

  • J’aime beaucoup ce pattern, mais malheureusement je n’ai pas réussi à le faire fonctionner correctement avec le LSP
    J’utilise pyright dans Helix, et même lancer l’éditeur avec uv run hx script.py ne marche pas
    On peut faire quelque chose comme uv run --with whatever-it-is-i-need hx script.py, mais les duplications s’accumulent vite

    • J’utilise un vilain script uve fait maison
      $ cat ~/.local/bin/uve
      #!/bin/bash
      temp=$(mktemp)
      uv export --script $1 --no-hashes > $temp
      uv run --with-requirements $temp vim $1
      unlink $temp
      J’aimerais que l’éditeur prenne bientôt en charge uv python find --script
  • Ça a l’air assez utile
    Je me demande si uv est un choix plus sûr pour distribuer des projets basés sur Python sur le long terme
    Je repense au rug pull d’Anaconda : il y a environ 5 ans, nous l’utilisions pour la gestion des dépendances, puis les règles ont changé et les clients appartenant à des organisations de plus de 200 employés ne pouvaient plus l’utiliser gratuitement ; ils devaient payer une licence commerciale

    • uv est sous licence MIT ou Apache-2.0
      Ils peuvent arrêter le développement ou déplacer les travaux futurs vers un fork sous une autre licence, mais ils ne peuvent pas modifier rétroactivement la licence passée ; ce qui existe aujourd’hui est donc garanti open source
      Si cela vous inquiète vraiment, vous pouvez créer un fork et continuer à le synchroniser
      En pratique, c’est presque la même chose pour les autres projets OSS, donc je ne m’en inquiéterais pas trop
      À ma connaissance, conda n’a jamais été open source et distribuait des binaires
      https://github.com/astral-sh/uv?tab=readme-ov-file#license
    • Il me semble que le rug pull d’Anaconda portait sur le dépôt
      Les paquets de conda-forge restent utilisables gratuitement
      uv utilise simplement PyPI, donc en cas de problème on peut passer de uv à pip, Poetry ou autre, et continuer à récupérer les paquets au même endroit
    • Je comprends que si un projet a un accord de licence contributeur (CLA) et fait céder les droits d’auteur des contributions au propriétaire du projet, il peut changer de licence
      Ce propriétaire finira forcément par être racheté par le pire milliardaire imaginable
      J’ai parcouru le guide de contribution et les issues d’uv, mais je n’ai pas vu de CLA ; PyTorch, lui, avait un CLA tout en haut de son guide de contribution
      Il aurait tout de même dû y avoir un fork communautaire de la dernière version FOSS d’Anaconda
      C’est ce qui s’est passé avec Redis, qui utilise aussi un CLA : https://github.com/redis/redis/blob/unstable/CONTRIBUTING.md#redis-software-grant-and-contributor-license-agreement
      Il ne faut jamais signer de CLA
      À mon avis, autant ne contribuer qu’à des projets copyleft
      Nous sommes trop bien payés pour travailler gratuitement
  • Pour de petits utilitaires, cela ressemble à une bonne alternative de packaging à la conteneurisation
    Il faut maintenant que je convainque tous mes collègues d’installer uv

    • uv est incroyablement rapide, donc ça devrait aider
    • Chez nous, le blocage est que notre scanner de vulnérabilités SCA ne fonctionne pas encore avec uv
  • Ça ressemble à bundler/inline côté Ruby
    Je suis content de voir quelque chose de similaire arriver en Python, et c’est vraiment très pratique
    https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html

  • Je me demande s’il y a des gens qui ont réussi à faire fonctionner ça sous Windows
    Je voulais utiliser cette astuce pour un outil destiné à un mod de jeu sur lequel je travaille, mais je n’ai pas réussi à faire fonctionner l’astuce du shebang

  • Ce cas d’usage m’a fait aimer uv, mais le fait qu’une PEP officielle et très utile ne soit pas prise en charge par les outils Python officiels me semble contraire au Zen of Python
    De mon point de vue, c’est la première fois que Python n’était pas “batteries included”
    Maintenant, j’ai aussi deux gestionnaires de dépendances Python sur mon système
    Je sais qu’il y a beaucoup à dire sur la gestion des dépendances en Python, mais ces dernières années, quand un projet avait seulement un requirements.txt, je m’en sortais avec le pip+venv de base

    • Il y a déjà eu un peu cette logique dans la spécification de build Python
      Si je me souviens bien, pyproject.toml est arrivé avant la bibliothèque tomllib
      Donc, pendant quelques versions, il fallait spécifier les modules dans un langage que Python ne pouvait pas lire nativement
      C’est pire que d’avoir une méthode par défaut qui inclut des métadonnées inutilisées
      C’est justement pour ça que ce sont des métadonnées au départ ; sinon, ce serait simplement de la syntaxe Python