Skip to content

Commit 860ffb0

Browse files
committed
Support Python 3.10 through 3.15
Use Python 3.10-compatible enum, typing, and integer conversion forms while preserving existing behavior. Advertise and test Python 3.10 through 3.15, and make create tests robust to interpreter-level terminal probes. Security: no intended change to codex32 encoding, recovery, creation preflight, or wallet behavior. Validation: - python -m pytest -q tests/test_cli.py (223 passed) - python -m pytest -q (864 passed) - python -m mypy src/codex32 - python -m ruff check --target-version py310 . - python -m ruff format --check . - python -m build --no-isolation - python -m twine check built artifacts Refs #18.
1 parent 90e5460 commit 860ffb0

9 files changed

Lines changed: 51 additions & 20 deletions

File tree

‎.github/workflows/python-package.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,21 @@ jobs:
3131
- run: python tools/differential_correction.py --verify
3232
- run: python -m build --no-isolation
3333
- run: python tools/verify_wheel_environment.py
34+
35+
compatibility:
36+
runs-on: ubuntu-latest
37+
strategy:
38+
fail-fast: false
39+
matrix:
40+
python-version: ["3.10", "3.11", "3.14", "3.15"]
41+
steps:
42+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
43+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
44+
with:
45+
python-version: ${{ matrix.python-version }}
46+
allow-prereleases: true
47+
- run: python -m pip install --upgrade pip
48+
- run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt
49+
- run: python -m pip install --no-build-isolation -e '.[dev]'
50+
- run: python -m pip check
51+
- run: python -m pytest -q

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ contains offline verification utilities.
2828

2929
## Development and verification
3030

31-
Use the existing virtual environment when available. Python 3.12 is the minimum;
32-
CI also covers 3.13. Install development dependencies only when needed:
31+
Use the existing virtual environment when available. Python 3.10 is the minimum;
32+
the supported range is Python 3.10 through 3.15. Install development
33+
dependencies only when needed:
3334
`python -m pip install -e '.[dev]'`. Run the CLI with `codex32 --help`.
3435

3536
Choose checks according to the changed behavior:

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ obtain an independent review before relying on it with funds. See
2727

2828
## Install
2929

30-
Python 3.12 or 3.13 is required. The installed package has no third-party
30+
Python 3.10 through 3.15 is supported. The installed package has no third-party
3131
runtime dependencies. To install it with the pinned build backend, run these
3232
commands from the project folder:
3333

‎pyproject.toml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ authors = [
1010
]
1111
description = "Python reference implementation for codex32 (BIP93) and codex32-encoded master seeds."
1212
readme = "README.md"
13-
requires-python = ">=3.12,<3.14"
13+
requires-python = ">=3.10,<3.16"
1414
license = "MIT"
1515
license-files = ["LICENSE*"]
1616
maintainers = [
@@ -22,8 +22,12 @@ classifiers = [
2222
"Intended Audience :: Developers",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
25+
"Programming Language :: Python :: 3.10",
26+
"Programming Language :: Python :: 3.11",
2527
"Programming Language :: Python :: 3.12",
2628
"Programming Language :: Python :: 3.13",
29+
"Programming Language :: Python :: 3.14",
30+
"Programming Language :: Python :: 3.15",
2731
"Operating System :: OS Independent",
2832
"Topic :: Security :: Cryptography",
2933
"Topic :: Software Development :: Libraries",
@@ -52,7 +56,7 @@ where = ["src"]
5256
testpaths = ["tests"]
5357

5458
[tool.mypy]
55-
python_version = "3.12"
59+
python_version = "3.10"
5660
strict = true
5761

5862
[tool.ruff]

‎src/codex32/generation.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Callable, Sequence
77
from collections.abc import Set as AbstractSet
88
from dataclasses import dataclass
9-
from typing import Never, SupportsIndex, cast
9+
from typing import NoReturn, SupportsIndex, cast
1010

1111
from codex32._bip32 import _valid_root
1212
from codex32.bech32 import CHARSET, _u5_to_chars, convertbits
@@ -198,7 +198,7 @@ class CreationCeremony:
198198
def __init__(self) -> None:
199199
raise TypeError("use a CreationCeremony class constructor")
200200

201-
def __reduce_ex__(self, _protocol: SupportsIndex) -> Never:
201+
def __reduce_ex__(self, _protocol: SupportsIndex) -> NoReturn:
202202
raise TypeError("creation ceremonies cannot be copied or serialized")
203203

204204
@classmethod

‎src/codex32/profiles/__init__.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22

33
from __future__ import annotations
44

5-
from enum import StrEnum
6-
from typing import TYPE_CHECKING
5+
from enum import Enum
6+
from typing import TYPE_CHECKING, TypeAlias, Union
77

88
from codex32.errors import UnknownProfile
99

1010
if TYPE_CHECKING:
1111
from codex32.profiles.bip39 import _Bip39Rules
1212
from codex32.profiles.cl32 import _Cl32Rules
1313
from codex32.profiles.ms32 import _Ms32Rules
14-
type _ProfileRules = _Ms32Rules | _Cl32Rules | _Bip39Rules
14+
_ProfileRules: TypeAlias = Union["_Ms32Rules", "_Cl32Rules", "_Bip39Rules"]
1515

1616

17-
class Profile(StrEnum):
17+
class Profile(str, Enum):
18+
__str__ = str.__str__
19+
1820
MS = "ms"
1921
CL = "cl"
2022
BIP39_12W = "bip39_12w"

‎src/codex32/profiles/bip39.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def validate_payload(self, symbols: tuple[int, ...], index: str) -> None:
5757
semantic = value >> self.outer_padding
5858
embedded = semantic & ((1 << self.checksum_bits) - 1)
5959
entropy = semantic >> self.checksum_bits
60-
entropy_bytes = entropy.to_bytes(self.entropy_bits // 8)
60+
entropy_bytes = entropy.to_bytes(self.entropy_bits // 8, "big")
6161
expected = hashlib.sha256(entropy_bytes).digest()[0] >> (8 - self.checksum_bits)
6262
if embedded != expected:
6363
raise InvalidBip39Checksum("embedded BIP39 entropy checksum is invalid")

‎tests/test_bip93.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
from codex32.errors import CodexError, InvalidChecksum
3333

3434

35+
def test_profile_string_matches_its_value() -> None:
36+
assert str(Profile.MS) == "ms"
37+
38+
3539
def test_vector_1_parts_and_seed() -> None:
3640
secret = parse_codex32(VECTOR_1["secret_s"])
3741
assert isinstance(secret, MasterSeed)

‎tests/test_cli.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,12 @@ def isatty(self) -> bool:
7878

7979

8080
class _CreationOutput(io.StringIO):
81-
def __init__(self, *, pretty: bool = False) -> None:
81+
def __init__(self) -> None:
8282
super().__init__()
83-
self.pretty = pretty
84-
self.checks = 0
83+
self.interactive = True
8584

8685
def isatty(self) -> bool:
87-
self.checks += 1
88-
return self.pretty or self.checks == 1
86+
return self.interactive
8987

9088

9189
@dataclass
@@ -151,12 +149,16 @@ def _invoke_terminal(args: list[str], *lines: str) -> _Result:
151149
def _invoke_confirmed_create(
152150
args: list[str],
153151
*lines: str,
154-
terminal_output: bool = False,
155152
core: _FakeBitcoinCore | None = None,
156153
) -> _Result:
157154
stdin = _TTYInput("\n".join(lines) + "\n")
158-
stdout = _CreationOutput(pretty=terminal_output)
155+
stdout = _CreationOutput()
159156
stderr = io.StringIO()
157+
selected_core = core or _FakeBitcoinCore()
158+
159+
def connect(*_args: object, **_kwargs: object) -> _FakeBitcoinCore:
160+
stdout.interactive = False
161+
return selected_core
160162

161163
def confirm_card(
162164
artifact: Share | Secret,
@@ -169,7 +171,7 @@ def confirm_card(
169171
with (
170172
patch.object(sys, "stdin", stdin),
171173
patch("codex32.cli._confirm_card", confirm_card),
172-
patch("codex32.cli.BitcoinCore.connect", return_value=core or _FakeBitcoinCore()),
174+
patch("codex32.cli.BitcoinCore.connect", side_effect=connect),
173175
contextlib.redirect_stdout(stdout),
174176
contextlib.redirect_stderr(stderr),
175177
):

0 commit comments

Comments
 (0)