Migrate to UV (#1748)

* Switch to using `uv` for dependency management and update related project workflows and scripts

* Expand contributing documentation with instructions for using `uv`, including dependency management, testing, linting, and docs workflows.

* Add changelog entry for migration to `uv` for dependency management and workflows
This commit is contained in:
Alex Root Junior 2026-01-02 01:27:37 +02:00 committed by GitHub
parent 7201e82238
commit ce4ddb77f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 3203 additions and 149 deletions

View file

@ -10,25 +10,22 @@ jobs:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python 3.11 - name: Set up Python 3.14
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.11" python-version: "3.14"
- name: Install build dependencies - name: Install uv
run: python -m pip install --upgrade build uses: astral-sh/setup-uv@v7
- name: Resolve version - name: Resolve version
id: package-version id: package-version
run: echo "value=$(echo ${{ github.ref }} | sed -e 's/refs\/tags\/v//')" >> $GITHUB_OUTPUT run: echo "value=$(echo ${{ github.ref }} | sed -e 's/refs\/tags\/v//')" >> $GITHUB_OUTPUT
# - name: Bump version - name: Build distribution
# run: hatch version ${{ steps.package-version.outputs.value }} run: uv build
- name: Build source distribution
run: python -m build .
- name: Try install wheel - name: Try install wheel
run: | run: |
@ -39,7 +36,7 @@ jobs:
venv/bin/pip install ../dist/aiogram-*.whl venv/bin/pip install ../dist/aiogram-*.whl
venv/bin/python -c "import aiogram; print(aiogram.__version__)" venv/bin/python -c "import aiogram; print(aiogram.__version__)"
- name: Publish artifacts - name: Publish artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: dist name: dist
path: dist/* path: dist/*
@ -55,7 +52,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: dist name: dist
path: dist path: dist

View file

@ -58,18 +58,21 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml - name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install project dependencies - name: Install project dependencies
run: | run: |
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast,signature] uv sync --all-extras --group dev --group test
- name: Lint code - name: Lint code
run: | run: |
ruff check --output-format=github aiogram examples uv run ruff check --output-format=github aiogram examples
mypy aiogram uv run mypy aiogram
black --check --diff aiogram tests uv run black --check --diff aiogram tests
- name: Setup redis - name: Setup redis
if: ${{ env.IS_WINDOWS == 'false' }} if: ${{ env.IS_WINDOWS == 'false' }}
@ -91,10 +94,10 @@ jobs:
flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml" flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml"
[[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0" [[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0"
[[ "$IS_UBUNTU" == "true" ]] && flags="$flags --mongo mongodb://mongo:mongo@localhost:27017" [[ "$IS_UBUNTU" == "true" ]] && flags="$flags --mongo mongodb://mongo:mongo@localhost:27017"
pytest $flags uv run pytest $flags
- name: Upload coverage data - name: Upload coverage data
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml files: coverage.xml
@ -123,20 +126,23 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml - name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install project dependencies - name: Install project dependencies
run: | run: |
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast,signature] uv sync --all-extras --group dev --group test
- name: Run tests - name: Run tests
run: | run: |
flags="" flags=""
pytest $flags uv run pytest $flags

21
CHANGES/1748.misc.rst Normal file
View file

@ -0,0 +1,21 @@
Migrated from ``hatch`` to ``uv`` for dependency management and development workflows.
This change improves developer experience with significantly faster dependency resolution (10-100x faster than pip), automatic virtual environment management, and reproducible builds through lockfile support.
**What changed for contributors:**
- Install dependencies with ``uv sync --all-extras --group dev --group test`` instead of ``pip install -e .[dev,test,docs]``
- Run commands with ``uv run`` prefix (e.g., ``uv run pytest``, ``uv run black``)
- All Makefile commands now use ``uv`` internally (``make install``, ``make test``, ``make lint``, etc.)
- Version bumping now uses a custom ``scripts/bump_version.py`` script instead of ``hatch version``
**What stayed the same:**
- Build backend remains ``hatchling`` (no changes to package building)
- Dynamic version reading from ``aiogram/__meta__.py`` still works
- All GitHub Actions CI/CD workflows updated to use ``uv``
- ReadTheDocs builds continue to work without changes
- Development dependencies (``dev``, ``test``) moved to ``[dependency-groups]`` section
- Documentation dependencies (``docs``) remain in ``[project.optional-dependencies]`` for compatibility
Contributors can use either the traditional ``pip``/``venv`` workflow or the new ``uv`` workflow - both are documented in the contributing guide.

View file

@ -28,8 +28,8 @@ clean:
.PHONY: install .PHONY: install
install: clean install: clean
pip install -e ."[dev,test,docs]" -U --upgrade-strategy=eager uv sync --all-extras --group dev --group test
pre-commit install uv run pre-commit install
# ================================================================================================= # =================================================================================================
# Code quality # Code quality
@ -37,15 +37,15 @@ install: clean
.PHONY: lint .PHONY: lint
lint: lint:
isort --check-only $(code_dir) uv run isort --check-only $(code_dir)
black --check --diff $(code_dir) uv run black --check --diff $(code_dir)
ruff check --show-fixes --preview $(package_dir) $(examples_dir) uv run ruff check --show-fixes --preview $(package_dir) $(examples_dir)
mypy $(package_dir) uv run mypy $(package_dir)
.PHONY: reformat .PHONY: reformat
reformat: reformat:
black $(code_dir) uv run black $(code_dir)
isort $(code_dir) uv run isort $(code_dir)
# ================================================================================================= # =================================================================================================
# Tests # Tests
@ -56,18 +56,18 @@ test-run-services:
.PHONY: test .PHONY: test
test: test-run-services test: test-run-services
pytest --cov=aiogram --cov-config .coveragerc tests/ --redis $(redis_connection) --mongo $(mongo_connection) uv run pytest --cov=aiogram --cov-config .coveragerc tests/ --redis $(redis_connection) --mongo $(mongo_connection)
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: test-run-services test-coverage: test-run-services
mkdir -p $(reports_dir)/tests/ mkdir -p $(reports_dir)/tests/
pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ --redis $(redis_connection) --mongo $(mongo_connection) uv run pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ --redis $(redis_connection) --mongo $(mongo_connection)
coverage html -d $(reports_dir)/coverage uv run coverage html -d $(reports_dir)/coverage
.PHONY: test-coverage-view .PHONY: test-coverage-view
test-coverage-view: test-coverage-view:
coverage html -d $(reports_dir)/coverage uv run coverage html -d $(reports_dir)/coverage
python -c "import webbrowser; webbrowser.open('file://$(shell pwd)/reports/coverage/index.html')" uv run python -c "import webbrowser; webbrowser.open('file://$(shell pwd)/reports/coverage/index.html')"
# ================================================================================================= # =================================================================================================
# Docs # Docs
@ -79,12 +79,12 @@ locales_pot := _build/gettext
docs_dir := docs docs_dir := docs
docs-gettext: docs-gettext:
hatch run docs:bash -c 'cd $(docs_dir) && make gettext' uv run --extra docs bash -c 'cd $(docs_dir) && make gettext'
hatch run docs:bash -c 'cd $(docs_dir) && sphinx-intl update -p $(locales_pot) $(addprefix -l , $(locales))' uv run --extra docs bash -c 'cd $(docs_dir) && sphinx-intl update -p $(locales_pot) $(addprefix -l , $(locales))'
.PHONY: docs-gettext .PHONY: docs-gettext
docs-serve: docs-serve:
hatch run docs:sphinx-autobuild --watch aiogram/ --watch CHANGES.rst --watch README.rst docs/ docs/_build/ $(OPTS) uv run --extra docs sphinx-autobuild --watch aiogram/ --watch CHANGES.rst --watch README.rst docs/ docs/_build/ $(OPTS)
.PHONY: docs-serve .PHONY: docs-serve
$(locale_targets): docs-serve-%: $(locale_targets): docs-serve-%:
@ -97,31 +97,31 @@ $(locale_targets): docs-serve-%:
.PHONY: build .PHONY: build
build: clean build: clean
hatch build uv build
.PHONY: bump .PHONY: bump
bump: bump:
hatch version $(args) uv run python scripts/bump_version.py $(args)
python scripts/bump_versions.py uv run python scripts/bump_versions.py
update-api: update-api:
butcher parse uv run --extra cli butcher parse
butcher refresh uv run --extra cli butcher refresh
butcher apply all uv run --extra cli butcher apply all
@$(MAKE) bump @$(MAKE) bump
.PHONY: towncrier-build .PHONY: towncrier-build
towncrier-build: towncrier-build:
hatch run docs:towncrier build --yes uv run --extra docs towncrier build --yes
.PHONY: towncrier-draft .PHONY: towncrier-draft
towncrier-draft: towncrier-draft:
hatch run docs:towncrier build --draft uv run --extra docs towncrier build --draft
.PHONY: towncrier-draft-github .PHONY: towncrier-draft-github
towncrier-draft-github: towncrier-draft-github:
mkdir -p dist mkdir -p dist
hatch run docs:towncrier build --draft | pandoc - -o dist/release.md uv run --extra docs towncrier build --draft | pandoc - -o dist/release.md
.PHONY: prepare-release .PHONY: prepare-release
prepare-release: bump towncrier-build prepare-release: bump towncrier-build
@ -129,5 +129,5 @@ prepare-release: bump towncrier-build
.PHONY: release .PHONY: release
release: release:
git add . git add .
git commit -m "Release $(shell poetry version -s)" git commit -m "Release $(shell uv run python -c 'from aiogram import __version__; print(__version__)')"
git tag v$(shell hatch version -s) git tag v$(shell uv run python -c 'from aiogram import __version__; print(__version__)')

View file

@ -679,6 +679,6 @@ class Dispatcher(Router):
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner: with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
return runner.run(coro) return runner.run(coro)
else: else: # pragma: no cover
uvloop.install() uvloop.install()
return asyncio.run(coro) return asyncio.run(coro)

View file

@ -88,6 +88,88 @@ Windows:
It will install :code:`aiogram` in editable mode into your virtual environment and all dependencies. It will install :code:`aiogram` in editable mode into your virtual environment and all dependencies.
Alternative: Using uv (Modern Approach)
----------------------------------------
As an alternative to the traditional :code:`pip` and :code:`venv` workflow, you can use `uv <https://github.com/astral-sh/uv>`_ -
a modern, fast Python package manager that handles virtual environments, dependency resolution, and package installation.
**Benefits of using uv:**
- 10-100x faster dependency resolution than pip
- Automatic virtual environment management
- Reproducible builds with lockfile
- Single tool for all package management needs
**Installing uv:**
Linux / macOS:
.. code-block:: bash
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows:
.. code-block:: powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Or using pip:
.. code-block:: bash
pip install uv
**Setup project with uv:**
Instead of manually creating and activating a virtual environment, :code:`uv` handles this automatically:
.. code-block:: bash
# Clone the repository
git clone https://github.com/aiogram/aiogram.git
cd aiogram
# Install all dependencies (creates .venv automatically)
uv sync --all-extras --group dev --group test
# Install pre-commit hooks
uv run pre-commit install
That's it! The :code:`uv sync` command creates a virtual environment in :code:`.venv/`,
installs all dependencies including optional extras and development tools, and generates
a :code:`uv.lock` file for reproducible builds.
**Running commands with uv:**
When using :code:`uv`, prefix commands with :code:`uv run` to execute them in the managed environment:
.. code-block:: bash
# Format code
uv run black aiogram tests examples
uv run isort aiogram tests examples
# Run tests
uv run pytest tests
# Run linting
uv run ruff check aiogram examples
uv run mypy aiogram
# Start documentation server
uv run sphinx-autobuild --watch aiogram/ docs/ docs/_build/
Or use the Makefile commands which now support :code:`uv`:
.. code-block:: bash
make install # Uses uv sync
make lint # Uses uv run
make reformat # Uses uv run
make test # Uses uv run
Making changes in code Making changes in code
---------------------- ----------------------
@ -101,38 +183,89 @@ Format the code (code-style)
Note that this project is Black-formatted, so you should follow that code-style, Note that this project is Black-formatted, so you should follow that code-style,
too be sure You're correctly doing this let's reformat the code automatically: too be sure You're correctly doing this let's reformat the code automatically:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
black aiogram tests examples black aiogram tests examples
isort aiogram tests examples isort aiogram tests examples
Or with uv:
.. code-block:: bash
uv run black aiogram tests examples
uv run isort aiogram tests examples
Or simply use Makefile:
.. code-block:: bash
make reformat
Run tests Run tests
--------- ---------
All changes should be tested: All changes should be tested:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
pytest tests pytest tests
Or with uv:
.. code-block:: bash
uv run pytest tests
Or use Makefile:
.. code-block:: bash
make test
Also if you are doing something with Redis-storage or/and MongoDB-storage, Also if you are doing something with Redis-storage or/and MongoDB-storage,
you will need to test everything works with Redis or/and MongoDB: you will need to test everything works with Redis or/and MongoDB:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
pytest --redis redis://<host>:<port>/<db> --mongo mongodb://<user>:<password>@<host>:<port> tests pytest --redis redis://<host>:<port>/<db> --mongo mongodb://<user>:<password>@<host>:<port> tests
Or with uv:
.. code-block:: bash
uv run pytest --redis redis://<host>:<port>/<db> --mongo mongodb://<user>:<password>@<host>:<port> tests
Docs Docs
---- ----
We are using `Sphinx` to render docs in different languages, all sources located in `docs` directory, We are using `Sphinx` to render docs in different languages, all sources located in `docs` directory,
you can change the sources and to test it you can start live-preview server and look what you are doing: you can change the sources and to test it you can start live-preview server and look what you are doing:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
sphinx-autobuild --watch aiogram/ docs/ docs/_build/ sphinx-autobuild --watch aiogram/ docs/ docs/_build/
Or with uv:
.. code-block:: bash
uv run --extra docs sphinx-autobuild --watch aiogram/ docs/ docs/_build/
Or use Makefile:
.. code-block:: bash
make docs-serve
Docs translations Docs translations
----------------- -----------------
@ -143,12 +276,27 @@ into different languages.
Before start, let's up to date all texts: Before start, let's up to date all texts:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
cd docs cd docs
make gettext make gettext
sphinx-intl update -p _build/gettext -l <language_code> sphinx-intl update -p _build/gettext -l <language_code>
Or with uv:
.. code-block:: bash
uv run --extra docs bash -c 'cd docs && make gettext'
uv run --extra docs bash -c 'cd docs && sphinx-intl update -p _build/gettext -l <language_code>'
Or use Makefile:
.. code-block:: bash
make docs-gettext
Change the :code:`<language_code>` in example below to the target language code, after that Change the :code:`<language_code>` in example below to the target language code, after that
you can modify texts inside :code:`docs/locale/<language_code>/LC_MESSAGES` as :code:`*.po` files you can modify texts inside :code:`docs/locale/<language_code>/LC_MESSAGES` as :code:`*.po` files
by using any text-editor or specialized utilites for GNU Gettext, by using any text-editor or specialized utilites for GNU Gettext,
@ -156,10 +304,18 @@ for example via `poedit <https://poedit.net/>`_.
To view results: To view results:
Using traditional approach:
.. code-block:: bash .. code-block:: bash
sphinx-autobuild --watch aiogram/ docs/ docs/_build/ -D language=<language_code> sphinx-autobuild --watch aiogram/ docs/ docs/_build/ -D language=<language_code>
Or with uv:
.. code-block:: bash
uv run --extra docs sphinx-autobuild --watch aiogram/ docs/ docs/_build/ -D language=<language_code>
Describe changes Describe changes
---------------- ----------------

View file

@ -78,17 +78,6 @@ cli = [
signature = [ signature = [
"cryptography>=46.0.0", "cryptography>=46.0.0",
] ]
test = [
"pytest==9.0.1",
"pytest-html==4.1.1",
"pytest-mock==3.15.1",
"pytest-mypy==1.0.1",
"pytest-cov==7.0.0",
"pytest-aiohttp==1.1.0",
"aresponses==3.0.0",
"pytz==2025.2",
"pycryptodomex==3.23.0",
]
docs = [ docs = [
"Sphinx~=8.0.2", "Sphinx~=8.0.2",
"sphinx-intl~=2.2.0", "sphinx-intl~=2.2.0",
@ -102,6 +91,8 @@ docs = [
"markdown-include~=0.8.1", "markdown-include~=0.8.1",
"sphinxcontrib-towncrier~=0.4.0a0", "sphinxcontrib-towncrier~=0.4.0a0",
] ]
[dependency-groups]
dev = [ dev = [
"black~=25.9", "black~=25.9",
"isort~=7.0", "isort~=7.0",
@ -112,95 +103,23 @@ dev = [
"packaging~=25.0", "packaging~=25.0",
"motor-types==1.0.0b4", "motor-types==1.0.0b4",
] ]
test = [
"pytest==9.0.1",
"pytest-html==4.1.1",
"pytest-mock==3.15.1",
"pytest-mypy==1.0.1",
"pytest-cov==7.0.0",
"pytest-aiohttp==1.1.0",
"aresponses==3.0.0",
"pytz==2025.2",
"pycryptodomex==3.23.0",
]
[project.urls] [project.urls]
Homepage = "https://aiogram.dev/" Homepage = "https://aiogram.dev/"
Documentation = "https://docs.aiogram.dev/" Documentation = "https://docs.aiogram.dev/"
Repository = "https://github.com/aiogram/aiogram/" Repository = "https://github.com/aiogram/aiogram/"
[tool.hatch.envs.default]
features = [
"dev",
"fast",
"redis",
"mongo",
"proxy",
"i18n",
"cli",
]
post-install-commands = [
"pre-commit install",
]
[tool.hatch.envs.default.scripts]
reformat = [
"black aiogram tests examples",
"isort aiogram tests examples",
]
lint = "ruff check aiogram tests examples"
[tool.hatch.envs.docs]
features = [
"fast",
"redis",
"mongo",
"proxy",
"i18n",
"docs",
"cli",
]
[tool.hatch.envs.docs.scripts]
serve = "sphinx-autobuild --watch aiogram/ --watch CHANGES.rst --watch README.rst docs/ docs/_build/ {args}"
[tool.hatch.envs.dev]
python = "3.12"
features = [
"dev",
"fast",
"redis",
"mongo",
"proxy",
"i18n",
"test",
"cli",
]
[tool.hatch.envs.dev.scripts]
update = [
"butcher parse",
"butcher refresh",
"butcher apply all",
]
[tool.hatch.envs.test]
features = [
"fast",
"redis",
"mongo",
"proxy",
"i18n",
"test",
"cli",
]
[tool.hatch.envs.test.scripts]
cov = [
"pytest --cov-config pyproject.toml --cov=aiogram --html=reports/py{matrix:python}/tests/index.html {args}",
"coverage html -d reports/py{matrix:python}/coverage",
]
cov-redis = [
"pytest --cov-config pyproject.toml --cov=aiogram --html=reports/py{matrix:python}/tests/index.html --redis {env:REDIS_DNS:'redis://localhost:6379'} {args}",
"coverage html -d reports/py{matrix:python}/coverage",
]
cov-mongo = [
"pytest --cov-config pyproject.toml --cov=aiogram --html=reports/py{matrix:python}/tests/index.html --mongo {env:MONGO_DNS:'mongodb://mongo:mongo@localhost:27017'} {args}",
"coverage html -d reports/py{matrix:python}/coverage",
]
view-cov = "google-chrome-stable reports/py{matrix:python}/coverage/index.html"
[[tool.hatch.envs.test.matrix]]
python = ["310", "311", "312", "313"]
[tool.ruff] [tool.ruff]
line-length = 99 line-length = 99
src = ["aiogram", "tests"] src = ["aiogram", "tests"]
@ -277,6 +196,8 @@ exclude_lines = [
"if TYPE_CHECKING:", "if TYPE_CHECKING:",
"@abstractmethod", "@abstractmethod",
"@overload", "@overload",
"if sys.version_info",
"except ImportError:"
] ]
[tool.mypy] [tool.mypy]

52
scripts/bump_version.py Normal file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Version bumping script for aiogram (replaces hatch version)."""
import re
import sys
from pathlib import Path
def bump_version(part: str) -> str:
"""Bump version in __meta__.py."""
meta_path = Path("aiogram/__meta__.py")
content = meta_path.read_text()
# Extract current version
version_match = re.search(r'__version__ = "(\d+)\.(\d+)\.(\d+)"', content)
if not version_match:
raise ValueError("Could not find version in __meta__.py")
major, minor, patch = map(int, version_match.groups())
# Bump appropriate part
if part == "major":
major += 1
minor = 0
patch = 0
elif part == "minor":
minor += 1
patch = 0
elif part == "patch":
patch += 1
elif part.startswith("to:"):
new_version = part.replace("to:", "")
content = re.sub(
r'__version__ = "\d+\.\d+\.\d+"', f'__version__ = "{new_version}"', content
)
meta_path.write_text(content)
return new_version
else:
raise ValueError(f"Unknown part: {part}. Use major, minor, patch, or to:X.Y.Z")
new_version = f"{major}.{minor}.{patch}"
content = re.sub(r'__version__ = "\d+\.\d+\.\d+"', f'__version__ = "{new_version}"', content)
meta_path.write_text(content)
return new_version
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python scripts/bump_version.py [major|minor|patch|to:X.Y.Z]")
sys.exit(1)
new_version = bump_version(sys.argv[1])
print(f"Bumped version to {new_version}")

2901
uv.lock generated Normal file

File diff suppressed because it is too large Load diff