Closed8

Pythonのパッケージ管理ツール「uv」を試す

kun432kun432

GitHubレポジトリ

https://github.com/astral-sh/uv

uv

Rustで書かれた、非常に高速なPythonパッケージおよびプロジェクトマネージャー。

ハイライト

uvはRuffのクリエイターであるAstralによって開発されている。

ドキュメント
https://docs.astral.sh/uv/

kun432kun432

Getting Startedに従って進める。今回はローカルのMac上で。

https://docs.astral.sh/uv/getting-started/

インストール

https://docs.astral.sh/uv/getting-started/installation/

uvのインストール方法は、インストールスクリプト、Pythonパッケージ、Cargoなど複数あるが、MacなのでHomebrewで。

$ brew install uv

バージョン確認。なお、uvxというのは、ruffやmypyなどの「ツール」を実行するための専用のコマンドらしい。

$ uv --version
uv 0.4.19 (Homebrew 2024-10-07)

$ uvx --version
uv-tool-uvx 0.4.19 (Homebrew 2024-10-07)
$ uv --help
An extremely fast Python package manager.

Usage: uv [OPTIONS] <COMMAND>

Commands:
  run      Run a command or script
  init     Create a new project
  add      Add dependencies to the project
  remove   Remove dependencies from the project
  sync     Update the project's environment
  lock     Update the project's lockfile
  export   Export the project's lockfile to an alternate format
  tree     Display the project's dependency tree
  tool     Run and install commands provided by Python packages
  python   Manage Python versions and installations
  pip      Manage Python packages with a pip-compatible interface
  venv     Create a virtual environment
  build    Build Python packages into source distributions and wheels
  publish  Upload distributions to an index
  cache    Manage uv's cache
  version  Display uv's version
  help     Display documentation for a command

Cache options:
  -n, --no-cache               Avoid reading from or writing to the cache, instead using a temporary directory for the
                               duration of the operation [env: UV_NO_CACHE=]
      --cache-dir <CACHE_DIR>  Path to the cache directory [env: UV_CACHE_DIR=]

Python options:
      --python-preference <PYTHON_PREFERENCE>  Whether to prefer uv-managed or system Python installations [env:
                                               UV_PYTHON_PREFERENCE=] [possible values: only-managed, managed, system,
                                               only-system]
      --no-python-downloads                    Disable automatic downloads of Python. [env: "UV_PYTHON_DOWNLOADS=never"]

Global options:
  -q, --quiet                      Do not print any output
  -v, --verbose...                 Use verbose output
      --color <COLOR_CHOICE>       Control colors in output [default: auto] [possible values: auto, always, never]
      --native-tls                 Whether to load TLS certificates from the platform's native certificate store [env:
                                   UV_NATIVE_TLS=]
      --offline                    Disable network access
      --no-progress                Hide all progress outputs
      --directory <DIRECTORY>      Change to the given directory prior to running the command
      --project <PROJECT>          Run the command within the given project directory
      --config-file <CONFIG_FILE>  The path to a `uv.toml` file to use for configuration [env: UV_CONFIG_FILE=]
      --no-config                  Avoid discovering configuration files (`pyproject.toml`, `uv.toml`) [env:
                                   UV_NO_CONFIG=]
  -h, --help                       Display the concise help for this command
  -V, --version                    Display the uv version

Use `uv help` for more details.
$ uvx --help
Run a command provided by a Python package.

Usage: uvx [OPTIONS] [COMMAND]

Options:
      --from <FROM>                            Use the given package to provide the command
      --with <WITH>                            Run with the given packages installed
      --with-editable <WITH_EDITABLE>          Run with the given packages installed as editables
      --with-requirements <WITH_REQUIREMENTS>  Run with all packages listed in the given `requirements.txt` files
      --isolated                               Run the tool in an isolated virtual environment, ignoring any
                                               already-installed tools

Index options:
  -i, --index-url <INDEX_URL>
          The URL of the Python package index (by default: <https://pypi.org/simple>) [env: UV_INDEX_URL=]
      --extra-index-url <EXTRA_INDEX_URL>
          Extra URLs of package indexes to use, in addition to `--index-url` [env: UV_EXTRA_INDEX_URL=]
  -f, --find-links <FIND_LINKS>
          Locations to search for candidate distributions, in addition to those found in the registry indexes [env:
          UV_FIND_LINKS=]
      --no-index
          Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via
          `--find-links`
      --index-strategy <INDEX_STRATEGY>
          The strategy to use when resolving against multiple index URLs [env: UV_INDEX_STRATEGY=] [possible values:
          first-index, unsafe-first-match, unsafe-best-match]
      --keyring-provider <KEYRING_PROVIDER>
          Attempt to use `keyring` for authentication for index URLs [env: UV_KEYRING_PROVIDER=] [possible values:
          disabled, subprocess]
      --allow-insecure-host <ALLOW_INSECURE_HOST>
          Allow insecure connections to a host [env: UV_INSECURE_HOST=]

Resolver options:
  -U, --upgrade                            Allow package upgrades, ignoring pinned versions in any existing output file.
                                           Implies `--refresh`
  -P, --upgrade-package <UPGRADE_PACKAGE>  Allow upgrades for a specific package, ignoring pinned versions in any existing
                                           output file. Implies `--refresh-package`
      --resolution <RESOLUTION>            The strategy to use when selecting between the different compatible versions
                                           for a given package requirement [env: UV_RESOLUTION=] [possible values:
                                           highest, lowest, lowest-direct]
      --prerelease <PRERELEASE>            The strategy to use when considering pre-release versions [env: UV_PRERELEASE=]
                                           [possible values: disallow, allow, if-necessary, explicit,
                                           if-necessary-or-explicit]
      --exclude-newer <EXCLUDE_NEWER>      Limit candidate packages to those that were uploaded prior to the given date
                                           [env: UV_EXCLUDE_NEWER=]
      --no-sources                         Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock
                                           against the standards-compliant, publishable package metadata, as opposed to
                                           using any local or Git sources

Installer options:
      --reinstall                              Reinstall all packages, regardless of whether they're already installed.
                                               Implies `--refresh`
      --reinstall-package <REINSTALL_PACKAGE>  Reinstall a specific package, regardless of whether it's already installed.
                                               Implies `--refresh-package`
      --link-mode <LINK_MODE>                  The method to use when installing packages from the global cache [env:
                                               UV_LINK_MODE=] [possible values: clone, copy, hardlink, symlink]
      --compile-bytecode                       Compile Python files to bytecode after installation [env:
                                               UV_COMPILE_BYTECODE=]

Build options:
  -C, --config-setting <CONFIG_SETTING>
          Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs
      --no-build-isolation
          Disable isolation when building source distributions [env: UV_NO_BUILD_ISOLATION=]
      --no-build-isolation-package <NO_BUILD_ISOLATION_PACKAGE>
          Disable isolation when building source distributions for a specific package
      --no-build
          Don't build source distributions
      --no-build-package <NO_BUILD_PACKAGE>
          Don't build source distributions for a specific package
      --no-binary
          Don't install pre-built wheels
      --no-binary-package <NO_BINARY_PACKAGE>
          Don't install pre-built wheels for a specific package

Cache options:
  -n, --no-cache                           Avoid reading from or writing to the cache, instead using a temporary directory
                                           for the duration of the operation [env: UV_NO_CACHE=]
      --cache-dir <CACHE_DIR>              Path to the cache directory [env: UV_CACHE_DIR=]
      --refresh                            Refresh all cached data
      --refresh-package <REFRESH_PACKAGE>  Refresh cached data for a specific package

Python options:
  -p, --python <PYTHON>                        The Python interpreter to use to build the run environment. [env:
                                               UV_PYTHON=]
      --python-preference <PYTHON_PREFERENCE>  Whether to prefer uv-managed or system Python installations [env:
                                               UV_PYTHON_PREFERENCE=] [possible values: only-managed, managed, system,
                                               only-system]
      --no-python-downloads                    Disable automatic downloads of Python. [env: "UV_PYTHON_DOWNLOADS=never"]

Global options:
  -q, --quiet                      Do not print any output
  -v, --verbose...                 Use verbose output
      --color <COLOR_CHOICE>       Control colors in output [default: auto] [possible values: auto, always, never]
      --native-tls                 Whether to load TLS certificates from the platform's native certificate store [env:
                                   UV_NATIVE_TLS=]
      --offline                    Disable network access
      --no-progress                Hide all progress outputs
      --directory <DIRECTORY>      Change to the given directory prior to running the command
      --project <PROJECT>          Run the command within the given project directory
      --config-file <CONFIG_FILE>  The path to a `uv.toml` file to use for configuration [env: UV_CONFIG_FILE=]
      --no-config                  Avoid discovering configuration files (`pyproject.toml`, `uv.toml`) [env:
                                   UV_NO_CONFIG=]
  -h, --help                       Display the concise help for this command
  -V, --version                    Display the uv version

Use `uv help tool run` for more details.

シェルの補完を有効化しておく。

$ echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bash_profile
$ echo 'eval "$(uvx --generate-shell-completion bash)"' >> ~/.bash_profile

反映

$ source ~/.bash_profile
kun432kun432

Pythonのインストール

https://docs.astral.sh/uv/getting-started/installation/

まず、uvでpython本体のバージョン管理およびインストールもできるらしい。

なお、自分の環境では、グローバルなpythonはmiseによってインストールされているものを使っていて、pyenvを使っていたりもしてる。

$ which python
/Users/kun432/.local/share/mise/installs/python/3.12/bin/python

$ python --version
Python 3.12.6

uvでPythonをインストール。

$ uv python install

事前にインストールされているものなどを探しているようだが、新しいバージョンがインストールされた。

Searching for Python installations
Installed Python 3.12.7 in 2.77s
 + cpython-3.12.7-macos-aarch64-none

ただしPATHなどを変更していないので、シェルではまだmiseでインストールしたpythonになっている。

$ which python
/Users/kun432/.local/share/mise/installs/python/3.12/bin/python

uvでインストールしたpythonを実行するにはuv runで実行。

$ uv run python --version
Python 3.12.7

別のバージョンをインストールしてみる。

$ uv python install 3.11
Searching for Python versions matching: Python 3.11
Installed Python 3.11.10 in 2.94s
 + cpython-3.11.10-macos-aarch64-none

uv runでバージョン指定しつつ実行もできる

$ uv run --python 3.11 python --version
Python 3.11.10

uvでインストール可能・インストール済みのPythonの一覧を見てみる。

$ uv python list
cpython-3.13.0rc3-macos-aarch64-none    <download available>
cpython-3.12.7-macos-aarch64-none       /opt/homebrew/opt/python@3.12/bin/python3.12 -> ../Frameworks/Python.framework/Versions/3.12/bin/python3.12
cpython-3.12.7-macos-aarch64-none       .local/share/uv/python/cpython-3.12.7-macos-aarch64-none/bin/python3 -> python3.12
cpython-3.12.6-macos-aarch64-none       .local/share/mise/installs/python/3.12/bin/python3.12
cpython-3.12.6-macos-aarch64-none       .local/share/mise/installs/python/3.12/bin/python3 -> python3.12
cpython-3.12.6-macos-aarch64-none       .local/share/mise/installs/python/3.12/bin/python -> .local/share/mise/installs/python/3.12.6/bin/python3
cpython-3.11.10-macos-aarch64-none      /opt/homebrew/opt/python@3.11/bin/python3.11 -> ../Frameworks/Python.framework/Versions/3.11/bin/python3.11
cpython-3.11.10-macos-aarch64-none      .local/share/uv/python/cpython-3.11.10-macos-aarch64-none/bin/python3 -> python3.11
cpython-3.11.9-macos-aarch64-none       .pyenv/versions/3.11.9/bin/python3.11
cpython-3.11.9-macos-aarch64-none       .pyenv/versions/3.11.9/bin/python3 -> python3.11
cpython-3.11.9-macos-aarch64-none       .pyenv/versions/3.11.9/bin/python -> python3.11
cpython-3.10.15-macos-aarch64-none      <download available>
cpython-3.9.20-macos-aarch64-none       <download available>
cpython-3.9.6-macos-aarch64-none        /Library/Developer/CommandLineTools/usr/bin/python3 -> ../../Library/Frameworks/Python3.framework/Versions/3.9/bin/python3
cpython-3.8.20-macos-aarch64-none       <download available>
pypy-3.10.14-macos-aarch64-none         <download available>
pypy-3.9.19-macos-aarch64-none          <download available>
pypy-3.8.16-macos-aarch64-none          <download available>

もともとMacOSのシステムでインストール済みのもの、Homebrewでインストールされているもの、pyenvやmiseなど他の管理ツールでインストールしたものなどがすべて表示される。

kun432kun432

Pythonスクリプトの実行

https://docs.astral.sh/uv/guides/scripts/

uvによるpythonスクリプトの実行には、上でも少し出てきたけど、uv runを使う。

とりあえず作業ディレクトリを作成。

$ mkdir uv-test && cd uv-test

以下のスクリプトを作成

example.py
import sys

v = sys.version_info
print(f"Hello, Python-{v.major}.{v.minor}.{v.micro}!")

普通に実行するとシステムでパスが通っているpythonになる。

$ python example.py
Hello, I'm Python-3.12.6!

uv runで実行すると、uvでインストールしたpythonで実行されているのがわかる。

$ uv run example.py
Hello, Python-3.12.7!

違うバージョンのpythonで実行する場合

$ uv run --python 3.11 example.py
Hello, Python-3.11.10!

コマンドライン引数の処理

example.py
import sys

v = sys.version_info
greeting = " ".join([a.capitalize() for a in sys.argv[1:]])
print(f"I'm Python-{v.major}.{v.minor}.{v.micro}! {greeting}!")
$ uv run example.py hello
I'm Python-3.12.7! Hello!
$ uv run example.py good morning
I'm Python-3.12.7! Good Morning!

パイプで渡す

 $ echo 'print("こんにちは!")' | uv run -
こんにちは!

ヒアドキュメントで渡す

$ uv run - <<EOF
print("こんにちは!")
EOF
こんにちは!

依存パッケージが必要な場合。ドキュメントでは例としてrichパッケージを使用している。

example.py
import time
from rich.progress import track

for i in track(range(20), description="例えば:"):
    time.sleep(0.05)

なにも考えずに実行するとこうなる。

$ uv run example.py
Traceback (most recent call last):
  File "/Users/kun432/work/uv-test/example.py", line 2, in <module>
    from rich.progress import track
ModuleNotFoundError: No module named 'rich'

通常ならば仮想環境などを用意してパッケージインストールすることになるが、uvではオンデマンドな環境を作成して実行してくれる。この場合は--with パッケージ名で明示的に指定する必要がある。

$ uv run --with rich example.py
Installed 4 packages in 9ms
例えば: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01

パッケージがインストールされているのがわかる。このパッケージは何処にインストールされているのか?というと多分この辺なのだろう。

$ tree ~/.cache/uv
/Users/kun432/.cache/uv
├── CACHEDIR.TAG
├── archive-v0
│   ├── 4MadhXi-Ruh6C3BGzJxbq
│   │   ├── rich
│   │   │   ├── __init__.py
│   │   │   ├── __main__.py
│   │   │   ├── _cell_widths.py
│   │   │   ├── _emoji_codes.py
(snip)

通常、プロジェクト内の場合はそちらでインストールするだろうし、もしくは仮想環境を用意することになるのが一般的だと思うが、この動きは少し認識しておく必要がありそう。

なお、キャッシュはuv cache clearで全部消える。

$ uv cache clear
$ tree ~/.cache/uv
/Users/kun432/.cache/uv  [error opening dir]

0 directories, 0 files

もう一つ、依存するパッケージを「インラインメタデータ」として定義するという方法がある。以下のようにuv add --scriptで対象のスクリプトに依存するパッケージのメタデータを追記・更新ができる

$ uv add --script example.py 'rich'
Updated `example.py`

中身を見ると、Pythonバージョンや依存パッケージがコメントとして記載されている。これが「インラインメタデータ」。

example.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "rich",
# ]
# ///
import time
from rich.progress import track

for i in track(range(20), description="例えば:"):
    time.sleep(0.05)

これで再度実行してみる。今度は--withなしで

$ uv run example.py
Reading inline script metadata from: example.py
Installed 4 packages in 9ms
例えば: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01

必要なパッケージが再度インストールされて実行されている。

kun432kun432

ツールを使う

https://docs.astral.sh/uv/guides/tools/

ruffやmypyなどコマンドを含むパッケージは「ツール」という特別な扱いになる。

例としてruffを使う。ツールの実行はuvxコマンドを使う。

$ uvx ruff

以下のようにインストールされて実行される。

Installed 1 package in 4ms
Ruff: An extremely fast Python linter and code formatter.

Usage: ruff [OPTIONS] <COMMAND>
(snip)

引数はそのまま渡せる。

$ uvx ruff check
All checks passed!

バージョン指定したり、コマンドとパッケージ名が異なる場合、GitHubレポジトリからインストールする場合などもオプションで使える様子だが、ここは割愛。

uvxだと、なければ実行時にキャッシュにインストール、あればキャッシュから読み出されて実行されるが、恒久的にインストールすることもできる。

例えば先ほどuvx系でruffが使えたが、パスには含まれていないので、以下のように直接は見えない。

$ which ruff
$

uv tool installを使う。

$ uv tool install ruff

どうやら~/.local/binに入る模様。で、~/.local/binがPATHに含まれてないので更新するためのコマンドが出力される。

Resolved 1 package in 13ms
Installed 1 package in 2ms
 + ruff==0.6.9
Installed 1 executable: ruff
warning: `/Users/kun432/.local/bin` is not on your PATH. To use installed tools, run `export PATH="/Users/kun432/.local/bin:$PATH"` or `uv tool update-shell`.
$ uv tool update-shell
Updated configuration file: /Users/kun432/.bash_profile
Created configuration file: /Users/kun432/.bashrc
Restart your shell to apply changes

別シェルで確認してみると、パスが通っているので、uvxなしでも実行できるし、実体はシンボリックリンクになっていることがわかる。

$ which ruff
/Users/kun432/.local/bin/ruff

 $ ruff --version
ruff 0.6.9

$ ls -lt /Users/kun432/.local/bin/ruff
lrwxr-xr-x@ 1 kun432  staff  49 10  9 11:59 /Users/kun432/.local/bin/ruff -> /Users/kun432/.local/share/uv/tools/ruff/bin/ruff

ツールのアップグレード。ツール個別でもできるし、全部まるっともできる。

$ uv tool upgrade ruff
Nothing to upgrade

$ uv tool upgrade --all
Nothing to upgrade

個人的にはちょっとここは検討が必要かなあ。こういうツールにバージョン管理はそれほど必要ないような気がしないでもないけども、複数人で共有するようなプロジェクトの場合はこれプロジェクト外の管理になっちゃう気がする。(まあプロジェクトはpyproject.tomlで管理するだろうし、あとはdevcontainer用意しとけばいいだけの話かもしれない)

kun432kun432

プロジェクト

https://docs.astral.sh/uv/guides/projects/

Pythonのプロジェクトを管理できる。

プロジェクトの作成

$ uv init hello-world
Initialized project `hello-world` at `/Users/kun432/work/hello-world`

もしくは先にディレクトリがある場合

$ mkdir hello-world2 && cd hello-world2

$ uv init
Initialized project `hello-world2`

プロジェクトのディレクトリ構成はこんな感じ。

$ tree -a -L 1
.
├── .git
├── .gitignore
├── .python-version
├── README.md
├── hello.py
└── pyproject.toml

2 directories, 5 files
pyproject.toml
[project]
name = "hello-world2"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
.python-version
3.12

スクリプトを実行する場合はuv runで。

$ uv run hello.py

pythonのバージョンが設定されて、仮想環境の作成も行われる。

Using CPython 3.12.7
Creating virtual environment at: .venv
Hello from hello-world2!
$ tree -a -L 1
.
├── .git
├── .gitignore
├── .python-version
├── .venv
├── README.md
├── hello.py
├── pyproject.toml
└── uv.lock

3 directories, 6 files

pyproject.tomlとは別にロックファイルが作成され、ここにパッケージのバージョンなどが記録される。

uv.lock
version = 1
requires-python = ">=3.12"

[[package]]
name = "hello-world2"
version = "0.1.0"
source = { virtual = "." }

プロジェクトで使用するパッケージを追加する場合はuv addを使う。

$ uv add requests
Resolved 6 packages in 197ms
Prepared 5 packages in 126ms
Installed 5 packages in 5ms
 + certifi==2024.8.30
 + charset-normalizer==3.3.2
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.2.3

pyproject.tomlとuv.lockが更新される。uv.lockには追加したパッケージに依存するパッケージのバージョンなども記録される。

cat pyproject.toml
[project]
name = "hello-world2"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "requests>=2.32.3",
]
uv.lock
version = 1
requires-python = ">=3.12"

[[package]]
name = "certifi"
version = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
]

[[package]]
name = "charset-normalizer"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 },
    { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 },
    { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 },
    { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 },
    { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 },
    { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 },
    { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 },
    { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 },
    { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 },
    { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 },
    { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 },
    { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 },
    { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 },
    { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 },
    { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 },
    { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 },
]

[[package]]
name = "hello-world2"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
    { name = "requests" },
]

[package.metadata]
requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]

[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]

[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "certifi" },
    { name = "charset-normalizer" },
    { name = "idna" },
    { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]

[[package]]
name = "urllib3"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]

削除はuv remove

$ uv remove requests
Resolved 1 package in 1ms
Uninstalled 5 packages in 10ms
 - certifi==2024.8.30
 - charset-normalizer==3.3.2
 - idna==3.10
 - requests==2.32.3
 - urllib3==2.2.3

それにあわせてpyproject.tomlとuv.lockも更新される。

プロジェクト環境でインストールしたパッケージのコマンドもuv runできる。(以下はエラーになるが、実行はできている)

$ uv add flask

$ uv run -- flask run
Usage: flask run [OPTIONS]
Try 'flask run --help' for help
(snip)

直接スクリプトを実行。以下のファイルを作成。

app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'こんにちは!'

if __name__ == '__main__':
    app.run(debug=True)
$ uv run app.py
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000

uv syncしてvenvをactivateするやり方もあるらしい。uv syncは環境を手動で更新する、とあるがちょっと意味がわかっていない。

$ uv sync
Resolved 9 packages in 0.77ms
Audited 7 packages in 0.32ms

$ flask run

# または

$ python app.py

パッケージ化はuv build(上でapp.pyを作ってる場合は削除しておく必要がある)

$ uv build
Building source distribution...
running egg_info
creating hello_world2.egg-info
writing hello_world2.egg-info/PKG-INFO
(snip)
adding 'hello_world2-0.1.0.dist-info/top_level.txt'
adding 'hello_world2-0.1.0.dist-info/RECORD'
removing build/bdist.macosx-11.0-arm64/wheel
Successfully built dist/hello_world2-0.1.0.tar.gz and dist/hello_world2-0.1.0-py3-none-any.whl
$ tree -a -L 2
.
├── .git
(snip)
├── .gitignore
├── .python-version
├── .venv
│   ├── .gitignore
│   ├── CACHEDIR.TAG
│   ├── bin
│   ├── lib
│   └── pyvenv.cfg
├── README.md
├── __pycache__
│   └── app.cpython-312.pyc
├── dist
│   ├── .gitignore
│   ├── hello_world2-0.1.0-py3-none-any.whl
│   └── hello_world2-0.1.0.tar.gz
├── example.py
├── hello.py
├── hello_world2.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── requires.txt
│   └── top_level.txt
├── pyproject.toml
└── uv.lock

12 directories, 22 files

作成したパッケージの公開。自分はやったことがないのでわからないけど、認証情報の指定が別途必要っぽい。

$ uv publish

https://docs.astral.sh/uv/guides/publish/

kun432kun432

パッケージ仮想環境

https://docs.astral.sh/uv/pip/environments/

シンプルに仮想環境を作る場合、例えばGitHubレポジトリからクローンしたOSSのプロジェクトで仮想南疆を作るようなケース。

作業ディレクトリを作成

$ mkdir uv-venv-test && cd uv-venv-test

uv venvで仮想環境を作成

$ uv venv
Using CPython 3.12.7
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

.venvが作成される。パッケージの追加はuv pip install

$ uv pip install streamlit

ファイルを作成

app.py
import streamlit as st

st.title("こんにちは!")

uv runの場合はこう

$ uv run streamlit run app.py

仮想環境内に入って実行する場合はこう

$ source .venv/bin/activate
$ streamlit run app.py
$ deactivate

activate/deactivateが面倒なので、direnvといっしょに使いたいなぁと思うのだけど、この辺が参考になりそう。

https://github.com/direnv/direnv/issues/1250

kun432kun432

まとめ

自分は以下のような感じでpyenv+venv+direnvで作り込んでいて、何も困っていないのだけども、最近はmiseを使いだしているところ。

https://zenn.dev/kun432/scraps/63b09758291001

で、uv今回触ってみて、mise+uvでほぼ同じことは実現できそう+Pythonプロジェクトも作れる、ならば、そろそろpyenvはお役御免にできそうである。

https://qiita.com/jkawamoto/items/874bc0bb1bd5cf7aba5b

このスクラップは1ヶ月前にクローズされました