type
status
date
slug
summary
tags
category
comment
icon
password
用UV进行包管理的笔记:
像专家一样分享 Python 脚本:使用 uv 和 PEP 723 实现轻松部署
共 3384 字 阅读需 4 分钟
我们都喜欢 Python 全面的标准库,但面对现实吧——PyPI 丰富的包资源往往也必不可少。分享依赖这些外部工具的单文件、自包含 Python 脚本可能会很麻烦。过去,我们依赖
requirements.txt
或像 Poetry、pipenv 这样的全功能包管理器,但这对于简单脚本来说可能过于复杂,对新手也不够友好。但如果有一种更简单的方法呢?这就是 uv
和 PEP 723 发挥作用的地方。本文深入探讨了 uv
如何利用 PEP 723 将依赖项直接嵌入脚本中,从而极大地简化了分发和执行过程。uv 和 PEP 723
我最喜欢 uv 及其下一代 Python 工具链的功能之一是,它能够轻松运行包含外部 Python 包引用的单文件 Python 脚本,无需繁琐的设置。
uv
借助 PEP 723 实现了这一壮举,该 PEP 专注于“内联脚本元数据”。此 PEP 定义了一种标准化方法,用于将脚本元数据(包括外部包依赖项)直接嵌入单文件 Python 脚本中。PEP 723 已经通过了 Python 增强提案流程,并获得了 Python 指导委员会的批准,现已成为官方 Python 规范的一部分。Python 生态系统中的各种工具已经实现了支持,包括 uv、PDM (Python Development Master) 和 Hatch。在本文中,我们重点关注 uv 对 PEP 723 的出色支持,以创建和分发单文件 Python 脚本。
准备工作
我们创建了一个名为
wordlookup.py
的 Python 脚本,用于从字典 API 获取定义。它看起来相当不错,但我们希望能够轻松地分发给他人运行:该脚本导入了几个 Python 模块,为与字典 API Web 服务交互、处理 JSON 数据、处理命令行参数、利用异步操作、格式化文本输出以及与操作系统交互以获取终端宽度奠定了基础。除了
httpx
(一个 HTTP 客户端库包)之外,我们导入的所有其他 Python 模块都是 Python 标准库的一部分。虽然我技术上可以使用 Python 内置的 urllib.request 模块实现目标,但我更喜欢 httpx
。然而,这带来了一个难题,因为我需要一种好的方式来分发这个脚本,以便我的朋友和同事可以轻松使用它,而无需费力安装所需的 httpx
依赖项。我们如何解决这个难题?uv 来救场!接下来我们将详细说明其工作原理。
注意:这篇文章在 Hacker News 上引起了广泛关注并引发了一些很棒的讨论 (链接)。其中一个提出的观点是关于选择 Python 内置的 Web 客户端选项与像
httpx
或 requests
这样的库。需要澄清的是,本文的重点是演示如何创建和分享依赖外部 Python 包的单文件 Python 脚本,而不是具体主张使用哪个 Web 客户端。使用 httpx
仅仅是这个概念的一个演示。安装 uv
第一步,我们需要先安装 uv。请参考官方 uv 文档获取 安装 uv 的指导。几种常见的安装 uv 的方式包括:
uv 是一个功能极其强大且用途广泛的工具,在我看来,它很大程度上代表了 Python 工具链的未来。然而,在本文中,我仅演示 uv 的一个很棒的功能:调用带有外部依赖项的单文件脚本。
使用 uv 在单文件脚本中添加包依赖项
现在我们准备将
httpx
作为依赖项添加到我们的 wordlookup.py
脚本中!方法如下:就这样!执行此命令后,uv 会在脚本顶部的注释中添加元数据。以下是脚本的开头部分以及随后的几行代码,以便您可以看到实际效果:
如果您使用过
pyproject.toml
文件与 Poetry、Flit、Hatch、Maturin、setuptools 等各种 Python 工具,这种语法至少看起来会有些熟悉。例如,Poetry 可能看起来像这样:您会注意到 uv 添加了
httpx
的元数据,但没有指定版本。uv 将从 PyPI 获取最新稳定版本的 httpx
供脚本使用。您可以通过事后直接修改元数据来添加依赖项约束,或者通过命令行指定版本依赖:使用 uv 运行你的脚本
我们准备好运行脚本了。uv 工具使运行变得非常简单(请注意,我还向脚本传递了一个
--help
参数):首次使用
uv run
调用脚本时,您会在开头看到一些额外的活动,因为 uv 会在后台自动创建一个隔离的虚拟环境,并获取和安装 httpx
包及其相关依赖项。这就是为什么我们在终端输出中看到 已在 74ms 内安装 7 个包
。如果您尝试使用
python wordlookup.py
运行脚本,除非您恰好在全局环境或当前虚拟环境中安装了 httpx
,否则脚本将失败。uv 是如何使用脚本元数据的呢?当使用 uv run
调用脚本时,uv 会:- 检查所需的 Python 版本是否可用。
- 自动创建一个隔离的虚拟环境(不会修改您的全局 Python 环境)。
- 如果尚未安装,则安装列出的依赖项(在本例中是
httpx
)。
- 执行脚本。
对于之后每次使用
uv run
启动脚本,uv 将利用它在后台创建的虚拟环境并调用脚本:如果我们向脚本添加额外的依赖项,或者在元数据中更改 Python 或
httpx
的版本,uv run
将在下次调用时创建一个新的隔离虚拟环境。通过 Python shebang 让运行更简单
我们可以在 Python 脚本的顶部添加一个 shebang(有时称为 hashbang),使其通过 uv 调用更加容易。我从 Trey Hunner 这里 学到了这个绝妙的技巧。
Linux/macOS 用户
对于 Linux 和 macOS(以及 BSD 用户),在脚本顶部添加以下行:
完整的脚本上下文在文件顶部将如下所示:
接下来,使文件可执行:
完成后,您可以直接运行脚本,无需使用完整的
uv run wordlookup.py
命令:(注意:为了清晰起见,我保留了 .py 后缀,但通常在将其设为可执行文件后可以省略)
Windows 用户
对于 Windows 用户,您也很幸运,因为 Windows 的 py 启动器也能够解释 shebang。当您在 Windows 上安装 Python 时,默认会包含 py 启动器。请注意,您需要从 shebang 中省略
-S
才能使脚本正常工作。脚本的第一行应如下所示:然后,您可以在 Windows 上使用
py
命令调用脚本,如下所示:注意:如果您通过
python wordlookup.py
调用脚本,这将不起作用,因为 shebang 不会被解释。设置让你的 uv 脚本可以在计算机上的任何位置调用
为了让您的 uv(Python)脚本可以轻松地从系统上的任何位置执行,您可以将其移动到包含在系统 PATH 中的常用可执行目录。
Linux/macOS 用户
对于 Linux 和 macOS 用户,将
wordlookup.py
脚本复制到您系统 $PATH
中的一个目录。在我的系统上,$HOME/bin
文件夹在路径中,我将其移动到那里:我还选择重命名文件并删除了
.py
文件扩展名,使其调用更符合人体工程学,因为 shebang 包含了识别文件为 Python 脚本所需的所有信息:
(上一步已合并重命名)我现在可以从任何地方调用它。(您还会观察到,当 Python 脚本首次从新位置调用时,uv 将创建一个新的虚拟环境并解析包依赖项。)
Windows 用户
对于 Windows 用户,您可以将脚本移动到已包含在系统
PATH
环境变量中的目录之一,或者将新文件夹添加到 PATH
。我将假设您创建了一个名为 c:\\scripts
的文件夹并将其添加到了您的 PATH
。接下来,创建一个名为
wordlookup.cmd
的文件,并添加以下内容:然后您将能够从 Windows Terminal 或命令提示符中的任何位置像这样调用脚本:
额外内容:uv 将其虚拟环境安装在哪里?
作为一个好奇的软件工程师,我决定深入研究,看看能否发现在我的 Fedora Linux 系统上 uv 将其虚拟环境安装在哪里。毕竟,我将
wordlookup.py
放在了它自己的专用目录中。在运行 uv add --script
添加 httpx
包依赖元数据并调用 uv run
后,本地文件夹中根本看不到像 .venv
这样的虚拟环境目录。我首先通过查找系统上所有名为
httpx
的目录开始,因为在脚本创建后首次调用 uv run
时,可能会创建一个以此命名的文件夹。瞧,我在一个名为
./.cache/uv/environments-v2
的父文件夹中找到了一个名为 httpx
的文件夹。这看起来很有希望。然后我发现了一个可以运行的命令 (uv cache clean) 来清除所有 uv 虚拟环境。这些操作是无害的,因为虚拟环境可以轻松地重新创建。
(注意:输出路径和大小会因用户而异)
为了在我的 Linux 系统上观察整个过程(也许这有点小题大做 😃),我使用了
inotifywait
来监控当我调用 uv run wordlookup.py
时发生的所有文件创建事件,因为在我清除了缓存后,uv 需要重新创建其虚拟环境。inotifywait
命令(属于 inotify-tools
包)等待文件系统事件并输出它们。以下是我使用的参数:m
(monitor): 此选项告诉inotifywait
持续监控指定目录的事件。没有这个选项,inotifywait
只会报告第一个事件然后退出。
r
(recursive): 此选项告诉inotifywait
递归监控指定目录及其所有子目录的事件。在.cache/
或其任何子目录中创建的任何新文件或目录都将触发事件。
e create
(event: create): 此选项指定inotifywait
只应报告创建事件。当在受监控目录中创建新文件或目录时,会发生创建事件。
~/.cache/
: 这是要求inotifywait
监控的目录。
果然,
inotifywait
揭示了当 uv run wordlookup.py
启动时动态创建的文件夹。当我将
wordlookup.py
脚本复制到我的 $HOME/bin
文件夹并从那里调用它时,我检查了 ./.cache/uv/environments-v2/
,发现那里又创建了另一个 wordlookup-*
文件夹来存放虚拟环境。在检查我的 Windows 虚拟机时,我同样在
%LOCALAPPDATA%\\uv\\cache
下找到了 uv
安装的虚拟环境。经过进一步调查,我找到了一些 uv 缓存目录文档,其中描述了 uv 如何确定其缓存目录的位置。其工作原理如下:
uv 按以下顺序确定缓存目录:
- 如果请求了
-no-cache
,则为临时缓存目录。
- 通过
-cache-dir
、UV_CACHE_DIR
或tool.uv.cache-dir
指定的特定缓存目录。
- 系统适用的缓存目录,例如 Unix 上的
$XDG_CACHE_HOME/uv
或$HOME/.cache/uv
,以及 Windows 上的%LOCALAPPDATA%\\uv\\cache
。
通常,在像我的 Fedora 设置这样的类 Unix 系统上,uv 将其缓存存储在
$HOME/.cache/uv
中。但是,您可以通过设置 $XDG_CACHE_HOME
环境变量来更改此位置。对于不熟悉 XDG 的人来说,XDG 基本目录规范是一组应用程序遵循以组织其文件的指南。它定义了几个指向特定目录的关键环境变量,确保不同类型的应用程序数据存储在指定的地点。更多信息请参见这里。总而言之,如果您没有进行特殊操作来更改默认设置,uv 会将其用于单文件 Python 脚本的虚拟环境存储在其缓存中,通常位于这些特定于操作系统的位置:
操作系统 | 虚拟环境位置 |
Linux | ~/.cache/uv/environments-v2/ |
macOS | ~/.cache/uv/environments-v2/ |
Windows | %LOCALAPPDATA%\\uv\\cache\\environments-v2 |
更新:Hacker News 上一位敏锐的用户 (sorenjan) 指出,您也可以运行
uv cache dir
来查找缓存目录的根位置(例如 ~/.cache
)。uv 如何派生其虚拟环境文件夹名称?
看看我 Linux 系统上的以下 uv 虚拟环境文件夹。
wordlookup-f6e73295bfd5f60b
这个文件夹名称是如何生成的?我对 uv 的 Rust 代码和其他资源的初步调查表明,虚拟环境文件夹名称是根据 Python 版本和外部包依赖项版本(例如在我的情境中是
httpx
)的哈希值生成的。这种设计确保了对这些元素的任何修改,包括脚本的名称(它本身嵌入在文件夹名称中),都会导致在缓存中创建一个唯一的虚拟环境。我通过观察发现,如果我在元数据中指定了不同版本的 httpx
,或者如果我更改了脚本文件的名称,uv 都会创建一个新的虚拟环境,从而凭经验验证了这一点。结论
总而言之,uv 及其对 PEP 723 的实现是一个很棒的工具,它简化了我们处理带有外部依赖项的单文件 Python 脚本的方式。通过将元数据直接嵌入脚本中,uv 消除了对单独的
requirements.txt
文件和复杂包管理器的需求。uv 简化了安装依赖项和管理虚拟环境的过程,使得运行这些脚本变得异常容易。shebang 和系统范围可执行文件的额外便利性进一步增强了可用性。最终,这种组合使得 Python 脚本编写更易于访问,特别是对于单文件脚本,并为开发者和用户带来了更流畅的工作流程。- 作者:KAI
- 链接:https://blog.985864.xyz/technology/1d8805b5-5b95-8076-a970-ecc792d379e1
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。