chore(python): migrate to pyproject.toml and uv
What changed, and why it matters
This commit is a routine build-system housekeeping change for the Trezor Python library. It replaces the old setup.py/requirements.txt packaging with a modern pyproject.toml and the uv build tool, drops Python 3.8 support, and changes how the package version is read. There is no security-relevant code change and no indication this fixes or introduces a vulnerability.
No security action required; treat as normal maintenance. Reviewers may optionally verify that the new pyproject.toml dependency pins and sdist/wheel exclusions match the previous setup.py behavior.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff migrates the python/ subproject from setuptools-based packaging to hatchling via pyproject.toml, managed with uv. It deletes setup.py, requirements.txt, requirements-optional.txt and MANIFEST.in, adds pyproject.toml with equivalent metadata/dependencies, updates .gitignore, tox.ini, and bump-version.py, and deprecates trezorlib.version in favor of importlib.metadata.version(‘trezor’). Dependency version pins are mostly preserved; click upper bound moves from <8.2 to <8.3. No cryptographic, transport, or firmware logic is modified.
Changed components
python packaging configurationpython/src/trezorlib/__init__.pypython/src/trezorlib/cli/trezorctl.pypython/tools/encfs_aes_getpass.pypython/tox.initools/bump-version.pyInspect captured patch +134 / −143
diff --git a/python/.changelog.d/+1d468db6.deprecated b/python/.changelog.d/+1d468db6.deprecated
new file mode 100644
index 00000000..761ca842
--- /dev/null
+++ b/python/.changelog.d/+1d468db6.deprecated
@@ -0,0 +1 @@
+`trezorlib.__version__` is deprecated, use `importlib.metadata.version("trezor")`.
diff --git a/python/.changelog.d/+47cd2085.removed b/python/.changelog.d/+47cd2085.removed
new file mode 100644
index 00000000..09cab082
--- /dev/null
+++ b/python/.changelog.d/+47cd2085.removed
@@ -0,0 +1 @@
+Dropped support for Python 3.8.
diff --git a/python/.changelog.d/+cabe85ab.changed b/python/.changelog.d/+cabe85ab.changed
new file mode 100644
index 00000000..9365e091
--- /dev/null
+++ b/python/.changelog.d/+cabe85ab.changed
@@ -0,0 +1 @@
+Changed build system to uv.
diff --git a/python/.gitignore b/python/.gitignore
index c45fe6b7..169f85c7 100644
--- a/python/.gitignore
+++ b/python/.gitignore
@@ -8,3 +8,5 @@ MANIFEST
*.py.cache
/.tox
mypy_report
+uv.lock
+/.venv
diff --git a/python/MANIFEST.in b/python/MANIFEST.in
deleted file mode 100644
index a55bb164..00000000
--- a/python/MANIFEST.in
+++ /dev/null
@@ -1,13 +0,0 @@
-recursive-include bash_completion.d *.sh
-include tools/*
-graft src
-graft tests
-graft stubs
-
-include AUTHORS README.md COPYING CHANGELOG.md
-include requirements*.txt
-include tox.ini pyrightconfig.json
-exclude src/trezorlib/_proto_messages.mako
-exclude tests/*.bin
-global-exclude *.pyc
-global-exclude */__pycache__/*
diff --git a/python/pyproject.toml b/python/pyproject.toml
new file mode 100644
index 00000000..91e71891
--- /dev/null
+++ b/python/pyproject.toml
@@ -0,0 +1,91 @@
+[project]
+name = "trezor"
+version = "0.14.0"
+description = "Python library for communicating with Trezor Hardware Wallet"
+readme = "README.md"
+license = "LGPL-3.0-only"
+license-files = ["COPYING"]
+authors = [
+ {name = "Trezor", email = "info@trezor.io"}
+]
+maintainers = [
+ {name = "matejcik", email = "jan.matejek@satoshilabs.com"}
+]
+keywords = ["trezor", "hardware", "wallet", "cryptocurrency", "bitcoin", "ethereum"]
+classifiers = [
+ "Operating System :: POSIX :: Linux",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: MacOS :: MacOS X",
+ "Programming Language :: Python :: 3 :: Only",
+]
+requires-python = ">=3.9"
+dependencies = [
+ "ecdsa>=0.9",
+ "mnemonic>=0.20",
+ "shamir-mnemonic>=0.3.0",
+ "slip10>=1.0.1",
+ "requests>=2.4.0",
+ "click>=8,<8.3",
+ "libusb1>=1.6.4",
+ "construct>=2.9,!=2.10.55",
+ "typing_extensions>=4.7.1",
+ "construct-classes>=0.1.2",
+ "cryptography>=41",
+ "noiseprotocol>=0.3.1,<0.4.0",
+]
+
+[project.optional-dependencies]
+hidapi = ["hidapi>=0.7.99.post20"]
+ethereum = ["web3>=5"]
+qt-widgets = ["PyQt5"]
+extra = ["Pillow>=10"]
+stellar = ["stellar-sdk>=6"]
+full = [
+ "hidapi>=0.7.99.post20",
+ "web3>=5",
+ "PyQt5",
+ "Pillow>=10",
+ "stellar-sdk>=6",
+]
+
+[project.urls]
+Homepage = "https://github.com/trezor/trezor-firmware/tree/main/python"
+Repository = "https://github.com/trezor/trezor-firmware"
+Documentation = "https://github.com/trezor/trezor-firmware/tree/main/python"
+
+[project.scripts]
+trezorctl = "trezorlib.cli.trezorctl:cli"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.uv]
+package = true
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/trezorlib"]
+
+[tool.hatch.build.targets.sdist]
+exclude = [
+ "/.*",
+ "/helper-scripts",
+ "/CHANGELOG.unreleased",
+ "/default.nix",
+ "/Makefile",
+ "/towncrier.toml",
+ "tests/*.bin",
+ "src/trezorlib/_proto_messages.mako",
+]
+
+[dependency-groups]
+dev = [
+ "autoflake>=2.3.1",
+ "black>=25",
+ "flake8>=2.3.0",
+ "isort>=5.13.2",
+ "pytest>=8.3.5",
+ "pytest-random-order>=1.2.0",
+ "tox>=4.25.0",
+ "tox-uv>=1.13.1",
+]
diff --git a/python/requirements-optional.txt b/python/requirements-optional.txt
deleted file mode 100644
index 4c0d35dc..00000000
--- a/python/requirements-optional.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-hidapi>=0.7.99.post20
-web3>=5
-Pillow>=10
-stellar-sdk>=6
diff --git a/python/requirements.txt b/python/requirements.txt
deleted file mode 100644
index 99cc0a0b..00000000
--- a/python/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-ecdsa>=0.9
-mnemonic>=0.20
-shamir-mnemonic>=0.3.0
-slip10>=1.0.1
-requests>=2.4.0
-click>=7,<8.2
-libusb1>=1.6.4
-construct>=2.9,!=2.10.55
-typing_extensions>=4.7.1
-construct-classes>=0.1.2
-cryptography>=41
-noiseprotocol>=0.3.1,<0.4.0
diff --git a/python/setup.cfg b/python/setup.cfg
index 430658bd..7ff9af9b 100644
--- a/python/setup.cfg
+++ b/python/setup.cfg
@@ -2,6 +2,7 @@
filename = *.py
exclude =
.tox/,
+ .venv/,
build/,
dist/,
vendor/,
diff --git a/python/setup.py b/python/setup.py
deleted file mode 100755
index cad22164..00000000
--- a/python/setup.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-
-# This file is part of the Trezor project.
-#
-# Copyright (C) SatoshiLabs and contributors
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3
-# as published by the Free Software Foundation.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the License along with this library.
-# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
-
-import re
-from pathlib import Path
-
-from setuptools import find_packages, setup
-
-CWD = Path(__file__).resolve().parent
-
-install_requires = (CWD / "requirements.txt").read_text().splitlines()
-
-extras_require = {
- "hidapi": ["hidapi>=0.7.99.post20"],
- "ethereum": ["web3>=5"],
- "qt-widgets": ["PyQt5"],
- "extra": ["Pillow>=10"],
- "stellar": ["stellar-sdk>=6"],
-}
-
-extras_require["full"] = sum(extras_require.values(), [])
-
-
-def find_version():
- version_file = (CWD / "src" / "trezorlib" / "__init__.py").read_text()
- version_match = re.search(r"^__version__ = \"(.*)\"$", version_file, re.M)
- if version_match:
- return version_match.group(1)
- else:
- raise RuntimeError("Version string not found")
-
-
-setup(
- name="trezor",
- version=find_version(),
- author="Trezor",
- author_email="info@trezor.io",
- license="LGPLv3",
- description="Python library for communicating with Trezor Hardware Wallet",
- long_description=(CWD / "README.md").read_text()
- + "\n\n"
- + (CWD / "CHANGELOG.md").read_text(),
- long_description_content_type="text/markdown",
- url="https://github.com/trezor/trezor-firmware/tree/master/python",
- package_data={"trezorlib": ["py.typed"]},
- packages=find_packages("src"),
- package_dir={"": "src"},
- entry_points={"console_scripts": ["trezorctl=trezorlib.cli.trezorctl:cli"]},
- install_requires=install_requires,
- extras_require=extras_require,
- python_requires=">=3.8",
- include_package_data=True,
- zip_safe=False,
- classifiers=[
- "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
- "Operating System :: POSIX :: Linux",
- "Operating System :: Microsoft :: Windows",
- "Operating System :: MacOS :: MacOS X",
- "Programming Language :: Python :: 3 :: Only",
- ],
-)
diff --git a/python/src/trezorlib/__init__.py b/python/src/trezorlib/__init__.py
index 35e1ac44..39fcc36f 100644
--- a/python/src/trezorlib/__init__.py
+++ b/python/src/trezorlib/__init__.py
@@ -14,4 +14,15 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
-__version__ = "0.14.0"
+import importlib.metadata
+import warnings
+
+
+def __getattr__(name: str) -> str:
+ if name == "__version__":
+ warnings.warn(
+ "__version__ is deprecated and will be removed in 0.15.0, use importlib.metadata.version('trezor') instead",
+ DeprecationWarning,
+ )
+ return importlib.metadata.version("trezor")
+ raise AttributeError(f"module {__name__} has no attribute {name}")
diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py
index 2fd52bf6..c23d596d 100755
--- a/python/src/trezorlib/cli/trezorctl.py
+++ b/python/src/trezorlib/cli/trezorctl.py
@@ -16,6 +16,7 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+import importlib.metadata
import json
import logging
import os
@@ -24,7 +25,7 @@ from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, TypeVar, ca
import click
-from .. import __version__, log, messages, protobuf
+from .. import log, messages, protobuf
from ..transport import DeviceIsBusy, enumerate_devices
from ..transport.session import Session
from ..transport.udp import UdpTransport
@@ -141,7 +142,7 @@ class TrezorctlGroup(AliasedGroup):
# This means that there is no reasonable way to use `hasattr` to detect where we
# are, unless we want to look at the private `_result_callback` attribute.
# Instead, we look at Click version and hope for the best.
- from click import __version__ as click_version
+ click_version = importlib.metadata.version("click")
if click_version.startswith("7."):
return super().resultcallback() # type: ignore [Cannot access attribute]
@@ -194,7 +195,7 @@ def configure_logging(verbose: int) -> None:
"--record",
help="Record screen changes into a specified directory.",
)
-@click.version_option(version=__version__)
+@click.version_option(package_name="trezor")
@click.pass_context
def cli_main(
ctx: click.Context,
@@ -310,7 +311,7 @@ def list_devices(no_resolve: bool) -> Optional[Iterable["Transport"]]:
@cli.command()
def version() -> str:
"""Show version of trezorctl/trezorlib."""
- return __version__
+ return importlib.metadata.version("trezor")
#
diff --git a/python/tests/test_protobuf_encoding.py b/python/tests/test_protobuf_encoding.py
index ebf55a41..514c72c4 100644
--- a/python/tests/test_protobuf_encoding.py
+++ b/python/tests/test_protobuf_encoding.py
@@ -165,7 +165,7 @@ def test_simple_message():
uvarint=12345678910,
svarint=-12345678910,
bool=True,
- bytes=b"\xDE\xAD\xCA\xFE",
+ bytes=b"\xde\xad\xca\xfe",
unicode="Příliš žluťoučký kůň úpěl ďábelské ódy 😊",
enum=SomeEnum.Five,
)
@@ -177,7 +177,7 @@ def test_simple_message():
assert retr.uvarint == 12345678910
assert retr.svarint == -12345678910
assert retr.bool is True
- assert retr.bytes == b"\xDE\xAD\xCA\xFE"
+ assert retr.bytes == b"\xde\xad\xca\xfe"
assert retr.unicode == "Příliš žluťoučký kůň úpěl ďábelské ódy 😊"
assert retr.enum == SomeEnum.Five
assert retr.enum == 5
diff --git a/python/tools/encfs_aes_getpass.py b/python/tools/encfs_aes_getpass.py
index 139979e1..c22f9639 100755
--- a/python/tools/encfs_aes_getpass.py
+++ b/python/tools/encfs_aes_getpass.py
@@ -25,6 +25,7 @@ encfs --standard --extpass=./encfs_aes_getpass.py ~/.crypt ~/crypt
"""
import hashlib
+import importlib.metadata
import json
import os
import sys
@@ -36,7 +37,8 @@ from trezorlib.client import TrezorClient
from trezorlib.tools import Address
from trezorlib.transport import enumerate_devices
-version_tuple = tuple(map(int, trezorlib.__version__.split(".")))
+trezor_version = importlib.metadata.version("trezor")
+version_tuple = tuple(map(int, trezor_version.split(".")))
if not (0, 11) <= version_tuple < (0, 14):
raise RuntimeError("trezorlib version mismatch (required: 0.13, 0.12, or 0.11)")
diff --git a/python/tox.ini b/python/tox.ini
index 9e832836..b97120fa 100644
--- a/python/tox.ini
+++ b/python/tox.ini
@@ -5,21 +5,17 @@
[tox]
envlist =
- py{38,39,310,311,312}-{minimal,default,full}
- py{38,39,310,311,312}-click{7,80}
- py{38,39,310,311,312}-click81
+ py{39,310,311,312,313}-{minimal,default,full}
+ py{39,310,311,312,313}-click8{0,1}
+ py{310,311,312,313}-click82
[testenv]
-deps =
- -rrequirements.txt
- !minimal: pytest>=3.6
- !minimal: pytest-random-order
- !minimal: importlib-metadata!=0.21
- full: -rrequirements-optional.txt
- py312: setuptools
+runner = uv-venv-runner
+dependency_groups =
+ !minimal: dev
+extras =
+ full: full
commands =
- # Generate local files
- python setup.py build
# Working in the local directory, try to compile all bytecode
python -m compileall src tests
# Smoke-test trezorctl
@@ -27,12 +23,10 @@ commands =
# Run test suite
!minimal: pytest -c setup.cfg --random-order tests
-[testenv:py{38,39,310,311,312}-click{7,80,81}]
-deps =
- -rrequirements.txt
- click7: click>=7,<8
- click80: click>=8.0,<8.1
- click81: click>=8.1,<8.2
+[testenv:py{39,310,311,312,313}-click{80,81,82}]
commands =
+ click80: uv pip install "click>=8.0,<8.1"
+ click81: uv pip install "click>=8.1,<8.2"
+ click82: uv pip install "click>=8.2,<8.3"
# Smoke-test trezorctl
trezorctl --version
diff --git a/tools/bump-version.py b/tools/bump-version.py
index 9caaaedd..43bbecf2 100755
--- a/tools/bump-version.py
+++ b/tools/bump-version.py
@@ -8,7 +8,6 @@ import click
VERSION_RE = re.compile(r"^(\d+)[.](\d+)[.](\d+)$")
HEADER_LINE_RE = re.compile(r"^#define ([A-Z_]+) \S+$")
-PYTHON_VERSION_RE = re.compile(r'^__version__ = "\d+[.]\d+[.]\d+"$', flags=re.MULTILINE)
def bump_header(filename, **kwargs):
@@ -29,14 +28,8 @@ def bump_header(filename, **kwargs):
fh.write(line)
-def bump_python(filename, new_version):
- with open(filename, "r+") as fh:
- contents = fh.read()
- result = PYTHON_VERSION_RE.sub(f'__version__ = "{new_version}"', contents)
-
- fh.seek(0)
- fh.truncate(0)
- fh.write(result)
+def bump_python(subdir: Path, new_version: str):
+ subprocess.check_call(["uv", "version", new_version], cwd=subdir)
def hex_lit(version):
@@ -89,9 +82,7 @@ def cli(project, version):
VERSION_PATCH=patch,
)
elif parts[-1] == "python":
- bump_python(
- project / "src" / "trezorlib" / "__init__.py", f"{major}.{minor}.{patch}"
- )
+ bump_python(project / "python", f"{major}.{minor}.{patch}")
else:
raise click.ClickException(f"Unknown project {project}.")
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.