Эта статья представляет собой руководство по использованию UV для улучшения и совершенствования вашего рабочего процесса на Python.
Если вы работаете с Python, вы, скорее всего, использовали один или все следующие инструменты:
- Pip для установки пакетов или pipx для их установки в виртуальных средах.
- Anaconda для установки пакетов, пользовательских версий Python и управления зависимостями.
- Poetry (и pipx) для управления проектом Python и его упаковкой.
Зачем вам нужен еще один инструмент для управления упаковкой Python или установки ваших любимых инструментов Python? Для меня использование uv было решением, основанным на следующих особенностях:
- Простота: uv может справиться со всеми задачами по упаковке или установке инструментов с помощью очень простого в использовании CLI.
- Улучшенное управление зависимостями: При возникновении конфликтов инструмент прекрасно объясняет, что пошло не так.
- Скорость: Если вы когда-нибудь использовали Anaconda для установки множества зависимостей, например PyTorch, Ansible, Pandas и т. д., вы оцените, насколько быстро uv может это сделать.
- Простота установки: Не требует установки сторонних зависимостей, поставляется с батарейками в комплекте (это будет продемонстрировано в следующем разделе).
- Документация: Да, онлайн-документация проста и понятна. Не нужно иметь степень магистра в области оккультизма, чтобы научиться пользоваться этим инструментом.
Давайте с самого начала проясним, что не существует универсального инструмента, который исправит все проблемы с рабочими процессами Python. Здесь я попытаюсь показать, почему вам имеет смысл попробовать uv и switch.
Чтобы следовать этому руководству, вам понадобится несколько вещей:
- Установка Linux: Я использую Fedora, но любой другой дистрибутив будет работать практически одинаково.
- Подключение к Интернету, чтобы скачать uv с их сайта.
- Будьте знакомы с pip и виртуальными средами: Это необязательно, но будет полезно, если вы уже устанавливали пакеты Python.
- Опыт программирования на Python: Мы не будем много кодить здесь, но знание модулей Python и того, как упаковать проект с помощью pyproject.toml и таких фреймворков, как setuptools, облегчит работу.
- Опционально, повышенные привилегии (SUDO), если вы хотите установить двоичные файлы по всей системе (как RPMS).
Давайте начнем с установки uv, если вы еще не сделали этого.
Установка UV
Если у вас установлен Linux, вы можете установить uv следующим образом:
curl -LsSf https://astral.sh/uv/install.sh | sh
Используете RPM? В списке Fedora есть несколько пакетов, начиная с версии 40. Поэтому вы можете сделать что-то вроде этого:
sudo dnf install -y uv
Или сделайте себе RPM, используя статически скомпилированные двоичные файлы из Astral и небольшую помощь от Podman и fpm:
podman run --mount type=bind,src=$HOME/tmp,target=/mnt/result --rm --privileged --interactive --tty fedora:37 bash gem install --user-install fpm
curl --location --fail --remote-name https://github.com/astral-sh/uv/releases/download/0.6.9/uv-x86_64-unknown-linux-gnu.tar.gz % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 15.8M 100 15.8M 0 0 8871k 0 0:00:01 0:00:01 --:--:-- 11.1M [root@a9e9dc561788 /]# fpm -t rpm -s tar --name uv --rpm-autoreq --rpm-os linux --rpm-summary 'An extremely fast Python package and project manager, written in Rust.' --license 'Apache 2.0' --version v0.6.9 --depends bash --maintainer 'Jose Vicente Nunez <kodegeek.com@protonmail.com>' --url https://github.com/astral-sh/uv uv-x86_64-unknown-linux-gnu.tar.gz Created package {:path=>"uv-v0.6.9-1.x86_64.rpm"} mv uv-v0.6.9-1.x86_64.rpm /mnt/result/ exit the container exit
Затем вы можете установить его в /usr/local
, используя --prefix
:
sudo -i rpm --force --prefix /usr/local -ihv /mnt/result/uv-v0.6.9-1.x86_64.rpm
Verifying... ################################# [100%] Preparing... ################################# [100%] Updating / installing... 1:uv-v0.6.9-1 ################################# [100%]
rpm -qil uv-v0.6.9-1
Name : uv Version : v0.6.9 Release : 1 Architecture: x86_64 Install Date: Sat Mar 22 23:32:49 2025 Group : default Size : 40524181 License : Apache 2.0 Signature : (none) Source RPM : uv-v0.6.9-1.src.rpm Build Date : Sat Mar 22 23:28:48 2025 Build Host : a9e9dc561788 Relocations : / Packager : Jose Vicente Nunez <kodegeek.com@protonmail.com> Vendor : none URL : https://github.com/astral-sh/uv Summary : An extremely fast Python package and project manager, written in Rust. Description : no description given /usr/local/usr/lib/.build-id /usr/local/usr/lib/.build-id/a1 /usr/local/usr/lib/.build-id/a1/8ee308344b9bd07a1e3bb79a26cbb47ca1b8e0 /usr/local/usr/lib/.build-id/e9 /usr/local/usr/lib/.build-id/e9/4f273a318a0946893ee81326603b746f4ffee1 /usr/local/uv-x86_64-unknown-linux-gnu/uv /usr/local/uv-x86_64-unknown-linux-gnu/uvx
Опять же, у вас есть несколько вариантов.
Теперь пора перейти к следующему разделу и посмотреть, что uv может сделать для ускорения работы с Python.
Использование UV для запуска повседневных инструментов, таких как Ansible, Glances, Autopep8
Одна из лучших вещей в uv – это то, что вы можете загружать и устанавливать инструменты на свой аккаунт с меньшим количеством ввода.
Один из моих любимых инструментов мониторинга, Glances, может быть установлен с помощью pip на учетную запись пользователя:
pip install --user glances glances
Но это загрязнит мою пользовательскую установку Python зависимостями glances. Поэтому лучше всего изолировать его в виртуальной среде:
python -m venv ~/venv/glances . ~/venv/glances/bin/activate pip install glances glances
Теперь вы видите, к чему это приведет. Вместо этого я могу сделать следующее с помощью uv:
uv tool run glances
Это одна строка для запуска и установки glances. Это создаст временное окружение, которое можно будет удалить, как только мы закончим работу с инструментом.
Позвольте мне показать вам эквивалентную команду, она называется uvx:
uvx --from glances glances
Если команда и дистрибутив совпадают, то мы можем пропустить явное указание на то, откуда она берется:
uvx glances
Чтобы не набирать текст, uv создал для меня виртуальную среду и загрузил туда glances. Теперь предположим, что я хочу использовать другой Python, версии__3.12, для его запуска:
uvx --from glances --python 3.12 glances
Если вы снова вызовете эту команду, uvx повторно использует созданную им виртуальную среду, используя выбранный вами интерпретатор Python.
Вы только что видели, как uv позволяет устанавливать пользовательские интерпретаторы Python. Более подробно эта тема рассматривается в следующем разделе.
Хорошая ли это идея – устанавливать пользовательские интерпретаторы Python?
Разрешение разработчикам и DevOps устанавливать пользовательские интерпретаторы Python может сэкономить время, поскольку для этого не требуются повышенные привилегии и нет необходимости создавать RPM для распространения нового Python.
Предположим, что вы хотите использовать Python 3.13:
[josevnz@dmaf5 ~]$ uv python install 3.13 Installed Python 3.13.1 in 3.21s + cpython-3.13.1-linux-x86_64-gnu
Где он был установлен? Давайте найдем его и запустим:
which python3
/usr/bin/python3 And not in the default PATH
which python3.13
/usr/bin/which: no python3.13 in (/home/josevnz/.cargo/bin:/home/josevnz/.local/bin:/home/josevnz/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/home/josevnz/.local/share/JetBrains/Toolbox/scripts) Let's find it (Pun intended)
find ~/.local -name python3.13
/home/josevnz/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/bin/python3.13 /home/josevnz/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/include/python3.13 /home/josevnz/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/lib/python3.13 Ah it is inside /home/josevnz/.local/share/uv/python, Let's run it:
/home/josevnz/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/bin/python3.13
Python 3.13.1 (main, Jan 14 2025, 22:47:38) [Clang 19.1.6 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Интересно, что настраиваемое расположение, которого нет в PATH, позволяет смешивать и сопоставлять версии Python.
Давайте посмотрим, может ли uv теперь повторно использовать установки. Представьте, что я хочу установить инструмент autopep8 (используемый для исправления проблем со стилем в коде Python), используя Python 3.13:
uv tool install autopep8 --python 3.13.1
Resolved 2 packages in 158ms Prepared 2 packages in 72ms Installed 2 packages in 8ms + autopep8==2.3.2 + pycodestyle==2.12.1 Installed 1 executable: autopep8
Будет ли новая установка autopep8 повторно использовать Python3.13, который мы установили ранее?
which autopep8~/.local/bin/autopep8
head -n 1 ~/.local/bin/autopep8#!/home/josevnz/.local/share/uv/tools/autopep8/bin/python
ls -l /home/josevnz/.local/share/uv/tools/autopep8/bin/python
lrwxrwxrwx. 1 josevnz josevnz 83 Mar 22 16:50 /home/josevnz/.local/share/uv/tools/autopep8/bin/python -> /home/josevnz/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/bin/python3.13
Да, это так, очень хорошо, мы не тратим место на дублирующие установки интерпретатора Python.
Но что, если мы захотим повторно использовать существующий системный python3? Если мы установим его принудительно, будет ли у нас дубликат (вновь загруженный и существующий в системе)?
В моей системе установлен Python 3.11, давайте принудительно установим autopep8 и посмотрим, что произойдет:
uv tool install autopep8 --force --python 3.11
Resolved 2 packages in 3ms Uninstalled 1 package in 1ms Installed 1 package in 3ms ~ autopep8==2.3.2 Installed 1 executable: autopep8 Where ia autopep8
which autopep8
~/.local/bin/autopep8 What python is used to run autopep8? Check the Shebang on the script
head -n 1 ~/.local/bin/autopep8#!/home/josevnz/.local/share/uv/tools/autopep8/bin/python3 Where does that Python point to?
ls -l /home/josevnz/.local/share/uv/tools/autopep8/bin/python3
lrwxrwxrwx. 1 josevnz josevnz 6 Mar 22 16:56 /home/josevnz/.local/share/uv/tools/autopep8/bin/python3 -> python
ls -l /home/josevnz/.local/share/uv/tools/autopep8/bin/python
lrwxrwxrwx. 1 josevnz josevnz 19 Mar 22 16:56 /home/josevnz/.local/share/uv/tools/autopep8/bin/python -> /usr/bin/python3.11
uv достаточно умен, чтобы использовать системный Python.
Теперь предположим, что вы хотите сделать эту версию Python3 версией по умолчанию для вашего пользователя. Есть способ сделать это с помощью экспериментальных флагов —preview (добавить в PATH) и —default (сделать ссылку на python3):
uv python install 3.13 --default --preview
Installed Python 3.13.1 in 23ms + cpython-3.13.1-linux-x86_64-gnu (python, python3, python3.13) Which one is now python3
which python3 ~/.local/bin/python3 Is python3.13 our default python3?
which python3.13 ~/.local/bin/python3.13
Если вы хотите обеспечить более строгий контроль над тем, какие интерпретаторы могут быть установлены, вы можете создать файл $XDG_CONFIG_DIRS/uv/uv.toml или ~/.config/uv/uv.toml и поместить туда следующие настройки:
Location: ~/.config/uv/uv.toml or /etc/uv/uv.toml https://docs.astral.sh/uv/reference/settings/#python-preference: only-managed, *managed*, system, only-system python-preference = "only-system" https://docs.astral.sh/uv/reference/settings/#python-downloads: *automatic*, manual or never python-downloads = "manual"
В системе Fedora в файле uv.toml есть эти настройки, причем общесистемные.
Чтобы завершить этот раздел, позвольте мне показать вам, как удалить установленный Python с помощью uv:
uv python uninstall 3.9 ... Searching for Python versions matching: Python 3.9 Uninstalled Python 3.9.21 in 212ms - cpython-3.9.21-linux-x86_64-gnu
Теперь пришло время вернуться к другим функциям экономии времени. Есть ли способ набирать меньше текста при установке приложений? Давайте узнаем это в следующем разделе.
Bash на помощь
Нет ничего, что не мог бы исправить старый добрый Bourne Shell (или ваша любимая оболочка). Поместите это в ваш ~/.profile
или файл конфигурации инициализации среды:
Use a function instead of an alias (aliases were deprecated but still supported) function glances { uvx --from glances --python 3.12 glances $* }
Еще один классный трюк, которому можно научить bash, – это автозаполнение команд uv. Просто настройте его следующим образом:
uv --generate-shell-completion bash > ~/.uv_autocomplete cat<<UVCONF>>~/.bash_profile
> if [[ -f ~/.uv_autocomplete ]]; then > . ~/.uv_autocomplete > fi > UVCONF [josevnz@dmaf5 docs]$ . ~/.uv_autocomplete
Прежде чем вы начнете писать функции для всех ваших инструментов Python, я покажу вам еще лучший способ установить их в нашу среду.
Подумайте об установке вашего инструмента вместо того, чтобы запускать его с помощью переходного развертывания.
Вы, вероятно, постоянно используете Ansible для управления своей инфраструктурой как кодом. И вы не хотите использовать uv или uvx для его вызова. Пришло время установить его:
uv tool install --force ansible Resolved 10 packages in 17ms Installed 10 packages in 724ms + ansible==11.3.0 + ansible-core==2.18.3 + jinja2==3.1.6 ...
Теперь мы можем вызывать его без использования uv или uvx, если только вы добавите ~/.local/bin в переменную окружения PATH. Убедиться в этом можно с помощью команды which:
which ansible-playbook ~/.local/bin/ansible-playbook
Еще одно преимущество использования „tool install“ заключается в том, что если установка большая (как Ansible), или у вас медленное сетевое соединение, вам нужно установить только один раз, так как он будет кэширован локально и готов к использованию в следующий раз.
Последний трюк в этом разделе – если вы установили несколько инструментов Python с помощью uv, вы можете обновить их все за один раз с помощью флага —upgrade:
uv tool upgrade --all Updated glances v4.3.0.8 -> v4.3.1 - glances==4.3.0.8 + glances==4.3.1 Installed 1 executable: glances
Это очень удобно!
До сих пор мы видели, как управлять чужими пакетами, а что делать с нашими собственными? В следующем разделе мы рассмотрим этот вопрос.
Управление проектами Python с помощью UV
В конце концов, вы обнаружите, что упаковываете проект Python, который содержит множество модулей, скриптов и файлов данных. Python предлагает богатую экосистему для управления этим сценарием, а uv снимает часть сложностей.
Наш небольшой демонстрационный проект создаст приложение, которое будет использовать данные „Grocery Stores“ из портала Connecticut Data portal. Файл данных обновляется каждую неделю и имеет формат JSON. Приложение берет эти данные и отображает их в терминале в виде таблицы.
„Uv init“ позволяет мне инициализировать базовую структуру проекта, которую мы вскоре улучшим. Мне всегда нравится начинать проект с описания и названия:
uv init --description 'Grocery Stores in Connecticut' grocery_stores Initialized project `grocery_stores` at `/home/josevnz/tutorials/docs/Enhancing_Your_Python_Workflow_with_UV_on_Fedora/grocery_stores`
uv создал здесь несколько файлов:
ls -a grocery_stores/ . .. hello.py pyproject.toml .python-version README.md
Наиболее важной частью, на данный момент, является pyproject.toml. В нем, помимо прочего, содержится полное описание вашего проекта:
[project] name = "pretty-csv" version = "0.1.0" description = "Grocery Stores in Connecticut" readme = "README.md" requires-python = ">=3.13" dependencies = []
Также создается .python-version
, который содержит версию Python, поддерживаемую этим проектом. Так uv определяет версию Python, используемую в этом проекте.
Еще один файл – hello.py
. Вы можете избавиться от него, он содержит приветствие мира на Python. Позже мы также наполним README.md надлежащим содержанием.
Возвращаясь к нашему скрипту, мы будем использовать TUI-фреймворк под названием Textual, который позволит нам взять JSON-файл и отобразить его содержимое в виде таблицы. Поскольку мы знаем эту зависимость, давайте воспользуемся uv, чтобы добавить ее в наш проект:
uv add 'textual==2.1.2' Using CPython 3.13.1 Creating virtual environment at: .venv Resolved 11 packages in 219ms Prepared 2 packages in 143ms Installed 10 packages in 47ms + linkify-it-py==2.0.3 + markdown-it-py==3.0.0 + mdit-py-plugins==0.4.2 + mdurl==0.1.2 + platformdirs==4.3.7 + pygments==2.19.1 + rich==13.9.4 + textual==2.1.2 + typing-extensions==4.12.2 + uc-micro-py==1.0.3
Произошли три вещи:
- Мы загрузили текстовые и переходные зависимости.
pyproject.toml
был обновлен, и теперь в разделе зависимостей есть значения (откройте файл) и посмотрите:
[project] name = "pretty-csv" version = "0.1.0" description = "Simple program that shows contents of a CSV file as a table on the terminal" readme = "README.md" requires-python = ">=3.13" dependencies = [ "textual==2.1.2", ]
- uv создал файл
uv.lock
рядом сpyproject.toml
. Этот файл содержит точные версии всех пакетов, используемых в вашем проекте, что обеспечивает согласованность.
version = 1 requires-python = ">=3.13" [[package]] name = "linkify-it-py" version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uc-micro-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946 } wheels = [ { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820 }, ] ...
Вы можете видеть, что uv.lock очень явный, поскольку его цель – быть настолько конкретным и однозначным, насколько это возможно. Этот файл должен быть добавлен в ваш репозиторий на git, как и „.python-version“ . Это позволит разработчикам из вашей команды иметь согласованный набор инструментов.
Давайте также добавим библиотеку „httpx“, чтобы мы могли загружать данные о продуктах асинхронно:
uv add 'httpx==0.28.1' ... Resolved 18 packages in 229ms Prepared 6 packages in 108ms Installed 7 packages in 8ms + anyio==4.9.0 + certifi==2025.1.31 + h11==0.14.0 + httpcore==1.0.7 + httpx==0.28.1 + idna==3.10 + sniffio==1.3.1
Это зависимости времени выполнения, но что, если мы захотим использовать инструменты для таких вещей, как линтинг или профилирование? Мы рассмотрим это в следующем разделе.
Зависимости для разработки
В процессе разработки приложения вы можете захотеть использовать некоторые инструменты, например pytest для запуска модульных тестов или pylint для проверки корректности кода. Но вы не хотите внедрять эти инструменты в финальную версию приложения.
Это зависимость от разработки, и вы можете добавить их в специальную секцию „-_dev_“ вашего проекта следующим образом:
uv add --dev pylint==3.3.6 pytest==8.3.5 Resolved 29 packages in 15ms Installed 10 packages in 19ms + astroid==3.3.9 + dill==0.3.9 + iniconfig==2.1.0 + isort==6.0.1 + mccabe==0.7.0 + packaging==24.2 + pluggy==1.5.0 + pylint==3.3.6 + pytest==8.3.5 + tomlkit==0.13.2
В результате в моем файле pyproject.toml появилась следующая секция:
[dependency-groups] dev = [ "pylint==3.3.6", "pytest==8.3.5", ]
Написание Python-приложения для отображения JSON в таблицу
Первый шаг – это код, который загружает данные, а затем отображает необработанные данные Grocery store в виде таблицы. Я предоставлю вам возможность прочитать текстовое руководство о том, как это сделать, а вместо этого поделюсь основной частью кода, который я написал в файле под названием „groceries.py“:
""" Displays the latest Grocery Store data from the Connecticut Data portal. Author: Jose Vicente NunezPress ctrl+q to exit the application. """ import httpx from httpx import HTTPStatusError from textual.app import App, ComposeResult from textual.widgets import DataTable, Header, Footer from textual import work, on from orjson import loads GROCERY_API_URL = "https://data.ct.gov/resource/fv3p-tf5m.json" class GroceryStoreApp(App): def compose(self) -> ComposeResult: header = Header(show_clock=True) yield header table = DataTable(id="grocery_store_table") yield table yield Footer() @work(exclusive=True) async def update_grocery_data(self) -> None: """ Update the Grocery data table and provide some feedback to the user :return: """ table = self.query_one("#grocery_store_table", DataTable) async with httpx.AsyncClient() as client: response = await client.get(GROCERY_API_URL) try: response.raise_for_status() groceries_data = loads(response.text) table.add_columns(*[key.title() for key in groceries_data[0].keys()]) cnt = 0 for row in groceries_data[1:]: table.add_row(*(row.values())) cnt += 1 table.loading = False self.notify( message=f"Loaded {cnt} Grocery Stores", title="Data loading complete", severity="information" ) except HTTPStatusError: self.notify( message=f"HTTP code={response.status_code}, message={response.text}", title="Could not download grocery data", severity="error" ) def on_mount(self) -> None: """ Render the initial component status, show an initial loading message :return: """ table = self.query_one("#grocery_store_table", DataTable) table.zebra_stripes = True table.cursor_type = "row" table.loading = True self.notify( message=f"Retrieving information from CT Data portal", title="Loading data", severity="information", timeout=5 ) self.update_grocery_data() @on(DataTable.HeaderSelected) def on_header_clicked(self, event: DataTable.HeaderSelected): """ Sort rows by column header """ table = event.data_table table.sort(event.column_key) if __name__ == "__main__": app = GroceryStoreApp() app.title = "Grocery Stores" app.sub_title = "in Connecticut" app.run()
Теперь, когда у нас есть код, давайте его протестируем. Сначала используем режим редактирования (аналогично использованию pip):
[josevnz@dmaf5 grocery_stores]$ uv pip install --editable . Resolved 18 packages in 105ms Built grocery-stores @ file:///home/josevnz/tutorials/docs/Enhancing_Your_Python_Workflow_with_UV_on_Fedora/grocery_stores Prepared 18 packages in 1.07s Uninstalled 18 packages in 87ms Installed 18 packages in 53ms ~ anyio==4.9.0 ~ certifi==2025.1.31 ~ grocery-stores==0.1.0 (from file:///home/josevnz/tutorials/docs/Enhancing_Your_Python_Workflow_with_UV_on_Fedora/grocery_stores) ~ h11==0.14.0 ~ httpcore==1.0.7 ~ httpx==0.28.1 ~ idna==3.10 ~ linkify-it-py==2.0.3 ~ markdown-it-py==3.0.0 ~ mdit-py-plugins==0.4.2 ~ mdurl==0.1.2 ~ platformdirs==4.3.7 ~ pygments==2.19.1 ~ rich==13.9.4 ~ sniffio==1.3.1 ~ textual==2.1.2 ~ typing-extensions==4.12.2 ~ uc-micro-py==1.0.3
Теперь запустите наше приложение продуктового магазина с помощью uv. Uv подхватит нашу локальную установку и будет использовать ее:
uv run groceries.py
Приложение выглядит примерно так:

Приложение для продуктового магазина было написано с помощью Textual. Неплохо для нескольких строк кода.
Пора посмотреть, как мы можем проверить и протестировать наше новое приложение для продуктового магазина.
Вычистка кода с помощью pylint:
Мы используем pylint следующим образом (я люблю фиксировать версию, чтобы избежать нежелательных предупреждений из-за изменений в API):
uv run --with 'pylint==3.3.6' pylint groceries.py
************* Module groceries groceries.py:15:0: C0115: Missing class docstring (missing-class-docstring) groceries.py:25:8: W0612: Unused variable 'table' (unused-variable) groceries.py:27:12: W0612: Unused variable 'response' (unused-variable) groceries.py:29:4: C0116: Missing function or method docstring (missing-function-docstring) groceries.py:10:0: W0611: Unused work imported from textual (unused-import) ------------------------------------------------------------------ Your code has been rated at 7.73/10 (previous run: 7.73/10, +0.00) Fix the issues, run the tests again:
uv run --with 'pylint==3.3.6' pylint groceries.py
------------------------------------------------------------------- Your code has been rated at 10.00/10 (previous run: 9.04/10, +0.96)
Запуск модульных тестов с помощью pytest
Мое приложение textual использует async, поэтому ему требуется небольшая поддержка со стороны pytest. Это не проблема:
uv add --dev pytest_asyncio uv run --dev pytest test_groceries.py
=================================================================================================================== test session starts ==================================================================================================================== platform linux -- Python 3.13.1, pytest-8.3.5, pluggy-1.5.0 rootdir: /home/josevnz/tutorials/docs/Enhancing_Your_Python_Workflow_with_UV_on_Fedora/grocery_stores configfile: pyproject.toml plugins: anyio-4.9.0, asyncio-0.25.3 asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None collected 1 item test_groceries.py . [100%] ==================================================================================================================== 1 passed in 0.43s =====================================================================================================================
Мой тестовый код просто имитирует запуск приложения и нажатие_ ctrl-q_ для выхода из него. Это не очень полезно, но следующий тест дает вам представление о том, что можно сделать для тестирования вашего приложения, имитирующего нажатие клавиш:
""" Unit tests for Groceries application https://textual.textualize.io/guide/testing/ """ import pytest from grocery_stores_ct.groceries import GroceryStoreApp @pytest.mark.asyncio async def test_groceries_app(): groceries_app = GroceryStoreApp() async with groceries_app.run_test() as pilot: await pilot.press("ctrl+q") # Quit
Теперь запустите тесты:
uv run --dev pytest test_groceries.py
============================================ test session starts ============================================= platform linux -- Python 3.13.1, pytest-8.3.5, pluggy-1.5.0 rootdir: /home/josevnz/tutorials/docs/Enhancing_Your_Python_Workflow_with_UV_on_Fedora/grocery_stores configfile: pyproject.toml plugins: asyncio-0.25.3, anyio-4.9.0 asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None collected 1 item test/test_groceries.py . [100%] ============================================= 1 passed in 1.17s ==============================================
Упаковка и загрузка в репозиторий артефактов
Пришло время упаковать наше новое приложение. Давайте попробуем собрать его:
uv build
Building source distribution... error: Multiple top-level modules discovered in a flat-layout: ['groceries', 'test_groceries']. To avoid accidental inclusion of unwanted files or directories, setuptools will not proceed with this build. ...
Не так быстро_. uv запутался, так как у нас 2 основных модуля, а не один. Правильнее всего будет создать src-layout для нашего проекта, поэтому мы перемещаем некоторые файлы.
После перемещения groceries.py в модуль под названием „src/grocery_stores_ct“ и tests_groceries в test:
tree . ├── pyproject.toml ├── README.md ├── src │ ├── grocery_stores_ct │ │ ├── groceries.py │ │ └── __init__.py │ └── grocery_stores.egg-info │ ├── dependency_links.txt │ ├── PKG-INFO │ ├── requires.txt │ ├── SOURCES.txt │ └── top_level.txt ├── test │ └── test_groceries.py └── uv.lock
Повторное тестирование, lint-it:
uv pip install --editable .[dev] uv run --dev pytest test/test_groceries.py uv run --with 'pylint==3.3.6' pylint src/grocery_stores_ct/groceries.py
А теперь постройте его снова:
uv build
Building source distribution... running egg_info writing src/grocery_stores.egg-info/PKG-INFO writing dependency_links to src/grocery_stores.egg-info/dependency_links.txt removing build/bdist.linux-x86_64/wheel Successfully built dist/grocery_stores-0.1.0.tar.gz Successfully built dist/grocery_stores-0.1.0-py3-none-any.whl
Теперь наступает момент, когда вы хотите поделиться своим приложением с другими.
Загрузка в пользовательский индекс
Я не хочу засорять реальный pypi.org тестовым приложением, поэтому вместо этого я задам для своего индекса что-то другое, например test.pypi.org. В вашем случае это может быть репозиторий Nexus 3, репозиторий Artifactory или любое другое хранилище артефактов, созданное в вашей компании.
Для pypi добавьте следующее в файл pyproject.toml:
URL match your desired location [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true
Вам также потребуется сгенерировать токен приложения (это зависит от провайдера и здесь не рассматривается). Как только вы получите токен, вызовите uv publish —index testpypi $token:
uv publish --index testpypi --token pypi-AgENdGVzdC5weXBpLm9yZwIkYzFkODg5ODMtODUxZS00ODc2LWFhYzMtZjhhNWFmNjZhODJmAAIqWzMsIjZmZGNjMzc1LTYxNmEtNDA5Zi1hNTJkLWJhMDZmNWQ3N2NlZSJdAAAGIG3wrTZdgmOBlahBlahBlah warning: `uv publish` is experimental and may change without warning Publishing 2 files https://test.pypi.org/legacy/ Uploading grocery_stores-0.1.0-py3-none-any.whl (2.7KiB) Uploading grocery_stores-0.1.0.tar.gz (2.5KiB)
Другие вещи, которые должны быть в вашем pyproject.toml
UV делает много вещей, но не делает всего. Существует множество дополнительных метаданных, которые должны быть в файле pyproject.toml. Здесь я расскажу о некоторых из них:
[project] authors = [ {name = "Jose Vicente Nunez", email = "kodegeek.com@protonmail.com"} ] maintainers = [ {name = "Jose Vicente Nunez", email = "kodegeek.com@protonmail.com"} ] license = "MIT AND (Apache-2.0 OR BSD-2-Clause)" keywords = ["ct", "tui", "grocery stores", "store"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: End Users/Desktop", "Topic :: Desktop Environment", "Programming Language :: Python :: 3.13", ] [project.urls] Homepage = "https://github.com/josevnz/tutorials" Repository = "https://github.com/josevnz/tutorials.git"
Прежде чем завершить этот раздел, хочу сказать несколько слов:
- Полный список классификаторов вы можете посмотреть здесь.
- Если вы не хотите, чтобы проект был случайно загружен на Pypi, добавьте следующий классификатор: „Private:: Do Not Upload“.
- После внесения каких-либо изменений, например, добавления keywords (это полезно, чтобы указать миру, где найти ваше приложение), вам нужно будет изменить версию, пересобрать и загрузить заново.
Метаданные встроенных скриптов, самодостаточные скрипты
В Python есть функция, PEP-0723, которая позволяет включать метаданные, встроенные в скрипт, например, так:
/// script requires-python = ">=3.13" dependencies = [ "httpx==0.28.1", "orjson==3.10.15", "textual==2.1.2", ] /// ... Omitted rest of the code
Если вы помните наш файл pyproject.toml, это инструкции, используемые менеджерами пакетов, такими как setuptools и uv, для обработки зависимостей проекта, таких как версии python и необходимые библиотеки для запуска. Это очень удобно, так как инструментам, способным читать эти встроенные метаданные (между секциями «///»), не нужно проверять дополнительный файл.Теперь у uv(есть флаг) под названием «-script», который позволяет ему интерпретировать встроенные метаданные скрипта. Например, это позволит обновить зависимости для скрипта «example», прочитав их непосредственно из скрипта:
uv add --script example.py 'requests<3' 'rich' uv run example.py
Это удобно. Если мы объединим встроенные зависимости и uv, то получим самоисполняемый скрипт, который также может загружать свои собственные зависимости:
#!/usr/bin/env -S uv run --script
/// script
requires-python = ">=3.13"
dependencies = [
"httpx==0.28.1",
"orjson==3.10.15",
"textual==2.1.2",
]
///
"""
Displays the latest Grocery Store data from
the Connecticut Data portal.
Author: Jose Vicente Nunez <kodegeek.com@protonmail.com>
This version of the script uses inline script metadata:
https://packaging.python.org/en/latest/specifications/inline-script-metadata/
Press ctrl+q to exit the application.
"""
import httpx
from httpx import HTTPStatusError
from textual.app import App, ComposeResult
from textual.widgets import DataTable, Header, Footer
from textual import work, on
pylint: disable=no-name-in-module
from orjson import loads
GROCERY_API_URL = "https://data.ct.gov/resource/fv3p-tf5m.json"
class GroceryStoreApp(App):
"""
TUI application that shows grocery stores in CT
"""
current_sorts: set = set()
def compose(self) -> ComposeResult:
header = Header(show_clock=True)
yield header
table = DataTable(id="grocery_store_table")
yield table
yield Footer()
work(exclusive=True)
async def update_grocery_data(self) -> None:
"""
Update the Grocery data table and provide some feedback to the user
:return:
"""
table = self.query_one("#grocery_store_table", DataTable)
async with httpx.AsyncClient() as client:
response = await client.get(GROCERY_API_URL)
try:
response.raise_for_status()
groceries_data = loads(response.text)
table.add_columns(*[key.title() for key in groceries_data[0].keys()])
cnt = 0
for row in groceries_data[1:]:
table.add_row(*(row.values()))
cnt += 1
table.loading = False
self.notify(
message=f"Loaded {cnt} Grocery Stores",
title="Data loading complete",
severity="information"
)
except HTTPStatusError:
self.notify(
message=f"HTTP code={response.status_code}, message={response.text}",
title="Could not download grocery data",
severity="error"
)
def on_mount(self) -> None:
"""
Render the initial component status
:return:
"""
table = self.query_one("#grocery_store_table", DataTable)
table.zebra_stripes = True
table.cursor_type = "row"
table.loading = True
self.notify(
message="Retrieving information from CT Data portal",
title="Loading data",
severity="information",
timeout=5
)
self.update_grocery_data()
def sort_reverse(self, sort_type: str):
"""
Determine if `sort_type` is ascending or descending.
"""
reverse = sort_type in self.current_sorts
if reverse:
self.current_sorts.remove(sort_type)
else:
self.current_sorts.add(sort_type)
return reverse
on(DataTable.HeaderSelected)
def on_header_clicked(self, event: DataTable.HeaderSelected):
"""
Sort rows by column header
"""
table = event.data_table
table.sort(
event.column_key,
reverse=self.sort_reverse(event.column_key.value)
)
if __name__ == "__main__":
app = GroceryStoreApp()
app.title = "Grocery Stores"
app.sub_title = "in Connecticut"
app.run()
Это тот же самый скрипт, который мы написали ранее, за исключением того, что здесь мы используем последнюю большую магию:
!/usr/bin/env -S uv run --script
Мы вызываем env (часть coreutils), чтобы разделить аргументы (-S) для вызова uv с флагом —script. Затем uv считывает встроенные метаданные и автоматически загружает необходимый python со всеми зависимостями:
chmod a+xr inline_script_metadata/groceries.py ./inline_script_metadata/groceries.py
Installed 18 packages in 29ms And here the script starts running!!!
Проще не бывает. Это очень удобно, например, для запуска скриптов установки.
Узнать больше
Здесь рассмотрено много материала, но еще больше предстоит узнать. Как и во всем, вам придется попробовать, чтобы понять, что лучше соответствует вашему стилю и имеющимся ресурсам.
Ниже приведен список ссылок, которые я нашел полезными и которые могут помочь и вам:
- Официальная документация по uv очень полная, и вы, скорее всего, потратите время, читая ее снова и снова.
- Пользователи старых дистрибутивов Fedora могут взглянуть на UV Source RPM. Много полезного, включая автодополнение Bash для UV.
- У Anaconda и miniconda также есть аналоги, написанные на rust(mamba и micromamba), на случай, если вы решите, что переходить на uv слишком рано. Они обратно совместимы и гораздо быстрее.
- Вы помните файл uv.lock, о котором мы говорили раньше? Теперь Python согласовал способ управления зависимостями(PEP 751), гораздо более мощный, чем файл pip requirements.txt. Следите за подробностями на packaging.python.org.
- Я показал вам, как использовать pylint для проверки запахов кода. Я бы настоятельно рекомендовал вам попробовать ruff. Он написан на языке rust и работает довольно быстро:
uv tool install ruff@latest
Resolved 1 package in 255ms Prepared 1 package in 1.34s Installed 1 package in 4ms ruff==0.11.2 Installed 1 executable: ruff The lets check the code
ruff check src/grocery_stores_ct
All checks passed!
Комментарии (0)