From 8513f46c2b04d0841fd340147c071c82e1be820b Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 8 Jan 2026 12:54:11 +0200 Subject: [PATCH 1/3] Bump minimum Python version to 3.10 We could just stop testing with 3.9... but I think this will lead to unintentionally breaking 3.9 anyway sooner or later. Signed-off-by: Jussi Kukkonen --- .github/workflows/_test.yml | 4 ++-- pyproject.toml | 4 ++-- tests/test_trusted_metadata_set.py | 3 ++- tests/test_updater_ng.py | 4 ++-- tests/utils.py | 5 +++-- tuf/api/_payload.py | 8 ++------ 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 601ad3736b..4fe5c77946 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python (oldest supported version) uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: - python-version: "3.9" + python-version: "3.10" cache: 'pip' cache-dependency-path: | requirements/*.txt @@ -38,7 +38,7 @@ jobs: needs: lint-test strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest] include: - python-version: "3.x" diff --git a/pyproject.toml b/pyproject.toml index d0bed22ce8..cce1fc5487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "A secure updater framework for Python" readme = "README.md" license = "Apache-2.0 OR MIT" license-files = ["LICENSE", "LICENSE-MIT"] -requires-python = ">=3.8" +requires-python = ">=3.10" authors = [ { email = "theupdateframework@googlegroups.com" }, ] @@ -31,11 +31,11 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Software Development", diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index bd8113eb4a..965d71546b 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -6,8 +6,9 @@ import os import sys import unittest +from collections.abc import Callable from datetime import datetime, timezone -from typing import Callable, ClassVar +from typing import ClassVar from securesystemslib.signer import Signer diff --git a/tests/test_updater_ng.py b/tests/test_updater_ng.py index 50ef5ee3be..6f07b4a0dc 100644 --- a/tests/test_updater_ng.py +++ b/tests/test_updater_ng.py @@ -11,8 +11,8 @@ import sys import tempfile import unittest -from collections.abc import Iterable -from typing import TYPE_CHECKING, Callable, ClassVar +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, ClassVar from unittest.mock import MagicMock, patch from securesystemslib.signer import Signer diff --git a/tests/utils.py b/tests/utils.py index bbfb07dbaa..bb9d98df21 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -31,8 +31,9 @@ import threading import time import warnings +from collections.abc import Callable from contextlib import contextmanager -from typing import IO, TYPE_CHECKING, Any, Callable +from typing import IO, TYPE_CHECKING, Any if TYPE_CHECKING: import unittest @@ -111,7 +112,7 @@ def wait_for_server( sock.settimeout(remaining_timeout) sock.connect((host, port)) succeeded = True - except socket.timeout: + except TimeoutError: pass except OSError as e: # ECONNREFUSED is expected while the server is not started diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 8a8c40ffdb..8f04362f24 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -396,19 +396,15 @@ def verified(self) -> bool: def signed(self) -> dict[str, Key]: """Dictionary of all signing keys that have signed, from both VerificationResults. - return a union of all signed (in python<3.9 this requires - dict unpacking) """ - return {**self.first.signed, **self.second.signed} + return self.first.signed | self.second.signed @property def unsigned(self) -> dict[str, Key]: """Dictionary of all signing keys that have not signed, from both VerificationResults. - return a union of all unsigned (in python<3.9 this requires - dict unpacking) """ - return {**self.first.unsigned, **self.second.unsigned} + return self.first.unsigned | self.second.unsigned class _DelegatorMixin(metaclass=abc.ABCMeta): From 0785c78b33d7ef3f683cb9af11b3650c9d0e1fb4 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 8 Jan 2026 13:06:24 +0200 Subject: [PATCH 2/3] Make linter happy after python upgrade Signed-off-by: Jussi Kukkonen --- tests/test_fetcher_ng.py | 7 ++++--- tests/test_trusted_metadata_set.py | 6 ++++-- tests/test_updater_ng.py | 4 ++-- tests/utils.py | 3 +-- tuf/api/_payload.py | 4 ++-- tuf/ngclient/_internal/trusted_metadata_set.py | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/test_fetcher_ng.py b/tests/test_fetcher_ng.py index d04b09f427..7ef7c11b70 100644 --- a/tests/test_fetcher_ng.py +++ b/tests/test_fetcher_ng.py @@ -170,9 +170,10 @@ def test_download_file_upper_length(self) -> None: # Download a file bigger than expected def test_download_file_length_mismatch(self) -> None: - with self.assertRaises( - exceptions.DownloadLengthMismatchError - ), self.fetcher.download_file(self.url, self.file_length - 4): + with ( + self.assertRaises(exceptions.DownloadLengthMismatchError), + self.fetcher.download_file(self.url, self.file_length - 4), + ): pass # we never get here as download_file() raises diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index 965d71546b..fd59635ed8 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -6,9 +6,8 @@ import os import sys import unittest -from collections.abc import Callable from datetime import datetime, timezone -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar from securesystemslib.signer import Signer @@ -31,6 +30,9 @@ ) from tuf.ngclient.config import EnvelopeType +if TYPE_CHECKING: + from collections.abc import Callable + logger = logging.getLogger(__name__) diff --git a/tests/test_updater_ng.py b/tests/test_updater_ng.py index 6f07b4a0dc..5393aa3c21 100644 --- a/tests/test_updater_ng.py +++ b/tests/test_updater_ng.py @@ -11,7 +11,7 @@ import sys import tempfile import unittest -from collections.abc import Callable, Iterable +from collections.abc import Iterable from typing import TYPE_CHECKING, ClassVar from unittest.mock import MagicMock, patch @@ -30,7 +30,7 @@ from tuf.ngclient import Updater, UpdaterConfig if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable logger = logging.getLogger(__name__) diff --git a/tests/utils.py b/tests/utils.py index bb9d98df21..f4310d0aec 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -31,13 +31,12 @@ import threading import time import warnings -from collections.abc import Callable from contextlib import contextmanager from typing import IO, TYPE_CHECKING, Any if TYPE_CHECKING: import unittest - from collections.abc import Iterator + from collections.abc import Callable, Iterator logger = logging.getLogger(__name__) diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 8f04362f24..89f58ab94d 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -1191,8 +1191,8 @@ def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool: # Every part in the pathpattern could include a glob pattern, that's why # each of the target and pathpattern parts should match. - for target_dir, pattern_dir in zip(target_parts, pattern_parts): - if not fnmatch.fnmatch(target_dir, pattern_dir): + for target, pattern in zip(target_parts, pattern_parts, strict=False): + if not fnmatch.fnmatch(target, pattern): return False return True diff --git a/tuf/ngclient/_internal/trusted_metadata_set.py b/tuf/ngclient/_internal/trusted_metadata_set.py index 179a65ed87..689eef01de 100644 --- a/tuf/ngclient/_internal/trusted_metadata_set.py +++ b/tuf/ngclient/_internal/trusted_metadata_set.py @@ -66,7 +66,7 @@ import datetime import logging from collections import abc -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, cast from tuf.api import exceptions from tuf.api.dsse import SimpleEnvelope @@ -88,7 +88,7 @@ logger = logging.getLogger(__name__) -Delegator = Union[Root, Targets] +Delegator = Root | Targets class TrustedMetadataSet(abc.Mapping): From 7ecb67d83e5e005a3be3e84b733e0af07a119512 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 8 Jan 2026 13:15:37 +0200 Subject: [PATCH 3/3] api: make the zip() usage clearer We manually enforce matching lengths but there's no harm in doing this too. Signed-off-by: Jussi Kukkonen --- tuf/api/_payload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 89f58ab94d..c4a64bb565 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -1191,7 +1191,7 @@ def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool: # Every part in the pathpattern could include a glob pattern, that's why # each of the target and pathpattern parts should match. - for target, pattern in zip(target_parts, pattern_parts, strict=False): + for target, pattern in zip(target_parts, pattern_parts, strict=True): if not fnmatch.fnmatch(target, pattern): return False