diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 3c25f5496c..d76228e153 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" cache: 'pip' cache-dependency-path: | requirements/*.txt @@ -38,7 +38,7 @@ jobs: needs: lint-test strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] os: [ubuntu-latest] include: - python-version: "3.x" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 52ea7b6a20..48ab67ff01 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Changed + +* Add Python 3.15 to tested versions, remove 3.10 from tested versions. + Require at least 3.11 + ## v7.0.1 ### Fixed diff --git a/examples/manual_repo/basic_repo.py b/examples/manual_repo/basic_repo.py index a974460767..0447df0739 100644 --- a/examples/manual_repo/basic_repo.py +++ b/examples/manual_repo/basic_repo.py @@ -28,7 +28,7 @@ import os import tempfile -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from securesystemslib.signer import CryptoSigner, Signer @@ -50,9 +50,7 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.now(timezone.utc).replace(microsecond=0) + timedelta( - days=days - ) + return datetime.now(UTC).replace(microsecond=0) + timedelta(days=days) # Create top-level metadata diff --git a/examples/manual_repo/hashed_bin_delegation.py b/examples/manual_repo/hashed_bin_delegation.py index f84a2acc03..c335814e4d 100644 --- a/examples/manual_repo/hashed_bin_delegation.py +++ b/examples/manual_repo/hashed_bin_delegation.py @@ -24,7 +24,7 @@ import hashlib import os import tempfile -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING @@ -45,9 +45,7 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.now(timezone.utc).replace(microsecond=0) + timedelta( - days=days - ) + return datetime.now(UTC).replace(microsecond=0) + timedelta(days=days) roles: dict[str, Metadata[Targets]] = {} diff --git a/examples/manual_repo/succinct_hash_bin_delegations.py b/examples/manual_repo/succinct_hash_bin_delegations.py index d0c94bd91f..1ce504bda2 100644 --- a/examples/manual_repo/succinct_hash_bin_delegations.py +++ b/examples/manual_repo/succinct_hash_bin_delegations.py @@ -23,7 +23,7 @@ import math import os import tempfile -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from securesystemslib.signer import CryptoSigner @@ -95,9 +95,7 @@ # NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py' # example for more details about the Targets object. -expiration_date = datetime.now(timezone.utc).replace(microsecond=0) + timedelta( - days=7 -) +expiration_date = datetime.now(UTC).replace(microsecond=0) + timedelta(days=7) targets = Metadata(Targets(expires=expiration_date)) succinct_roles = SuccinctRoles( diff --git a/examples/repository/_simplerepo.py b/examples/repository/_simplerepo.py index 3d19c8de83..bff3586c8f 100644 --- a/examples/repository/_simplerepo.py +++ b/examples/repository/_simplerepo.py @@ -9,7 +9,7 @@ import json import logging from collections import defaultdict -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from securesystemslib.signer import CryptoSigner, Key, Signer @@ -130,7 +130,7 @@ def open(self, role: str) -> Metadata: def close(self, role: str, md: Metadata) -> None: """Store a version of metadata. Handle version bumps, expiry, signing""" md.signed.version += 1 - md.signed.expires = datetime.now(timezone.utc) + self.expiry_period + md.signed.expires = datetime.now(UTC) + self.expiry_period md.signatures.clear() for signer in self.signer_cache[role]: diff --git a/examples/uploader/_localrepo.py b/examples/uploader/_localrepo.py index 7d8181b44e..0fb9e1e3cd 100644 --- a/examples/uploader/_localrepo.py +++ b/examples/uploader/_localrepo.py @@ -10,7 +10,7 @@ import json import logging import os -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from securesystemslib.signer import CryptoSigner, Signer from urllib3 import request @@ -87,7 +87,7 @@ def close(self, role_name: str, md: Metadata) -> None: signer = Signer.from_priv_key_uri(uri, public_key) md.signed.version += 1 - md.signed.expires = datetime.now(timezone.utc) + self.expiry_period + md.signed.expires = datetime.now(UTC) + self.expiry_period md.sign(signer, append=False) diff --git a/pyproject.toml b/pyproject.toml index 7025417d83..988d1630ab 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.10" +requires-python = ">=3.11" 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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Software Development", diff --git a/tests/generated_data/generate_md.py b/tests/generated_data/generate_md.py index cb9b04577d..e75cf3291f 100644 --- a/tests/generated_data/generate_md.py +++ b/tests/generated_data/generate_md.py @@ -7,7 +7,7 @@ import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from securesystemslib.signer import CryptoSigner, Signer, SSlibKey @@ -55,7 +55,7 @@ private_key = Ed25519PrivateKey.from_private_bytes(private_values[index]) signers.append(CryptoSigner(private_key, key)) -EXPIRY = datetime(2050, 1, 1, tzinfo=timezone.utc) +EXPIRY = datetime(2050, 1, 1, tzinfo=UTC) SERIALIZER = JSONSerializer() diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index bd175e7244..60c156f43a 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -129,7 +129,7 @@ def __init__(self) -> None: self.fetch_tracker = FetchTracker() - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) self.safe_expiry = now.replace(microsecond=0) + datetime.timedelta( days=30 ) diff --git a/tests/test_api.py b/tests/test_api.py index e156d6b332..df8fbc3832 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,7 +12,7 @@ import tempfile import unittest from copy import copy, deepcopy -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import ClassVar @@ -312,7 +312,7 @@ def test_metadata_signed_is_expired(self) -> None: snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json") md = Metadata.from_file(snapshot_path) - expected_expiry = datetime(2030, 1, 1, 0, 0, tzinfo=timezone.utc) + expected_expiry = datetime(2030, 1, 1, 0, 0, tzinfo=UTC) self.assertEqual(md.signed.expires, expected_expiry) # Test is_expired with reference_time provided @@ -326,10 +326,10 @@ def test_metadata_signed_is_expired(self) -> None: # Test is_expired without reference_time, # manipulating md.signed.expires expires = md.signed.expires - md.signed.expires = datetime.now(timezone.utc) + md.signed.expires = datetime.now(UTC) is_expired = md.signed.is_expired() self.assertTrue(is_expired) - md.signed.expires = datetime.now(timezone.utc) + timedelta(days=1) + md.signed.expires = datetime.now(UTC) + timedelta(days=1) is_expired = md.signed.is_expired() self.assertFalse(is_expired) md.signed.expires = expires diff --git a/tests/test_repository.py b/tests/test_repository.py index 5f43e8e3b8..0c74347d31 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -10,7 +10,7 @@ import sys import unittest from collections import defaultdict -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from securesystemslib.signer import CryptoSigner, Signer @@ -107,7 +107,7 @@ def open(self, role: str) -> Metadata: def close(self, role: str, md: Metadata) -> None: """Store a version of metadata. Handle version bumps, expiry, signing""" md.signed.version += 1 - md.signed.expires = datetime.now(timezone.utc) + self.expiry_period + md.signed.expires = datetime.now(UTC) + self.expiry_period md.signatures.clear() for signer in self.signer_cache[role]: diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index 19aa927ad2..be2356e958 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -9,7 +9,7 @@ import os import sys import unittest -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, ClassVar from securesystemslib.signer import Signer @@ -299,7 +299,7 @@ def test_update_root_new_root_ver_same_as_trusted_root_ver(self) -> None: def test_root_expired_final_root(self) -> None: def root_expired_modifier(root: Root) -> None: - root.expires = datetime(1970, 1, 1, tzinfo=timezone.utc) + root.expires = datetime(1970, 1, 1, tzinfo=UTC) # intermediate root can be expired root = self.modify_metadata(Root.type, root_expired_modifier) @@ -349,7 +349,7 @@ def bump_snapshot_version(timestamp: Timestamp) -> None: def test_update_timestamp_expired(self) -> None: # new_timestamp has expired def timestamp_expired_modifier(timestamp: Timestamp) -> None: - timestamp.expires = datetime(1970, 1, 1, tzinfo=timezone.utc) + timestamp.expires = datetime(1970, 1, 1, tzinfo=UTC) # expired intermediate timestamp is loaded but raises timestamp = self.modify_metadata( @@ -426,7 +426,7 @@ def test_update_snapshot_expired_new_snapshot(self) -> None: self.trusted_set.update_timestamp(self.metadata[Timestamp.type]) def snapshot_expired_modifier(snapshot: Snapshot) -> None: - snapshot.expires = datetime(1970, 1, 1, tzinfo=timezone.utc) + snapshot.expires = datetime(1970, 1, 1, tzinfo=UTC) # expired intermediate snapshot is loaded but will raise snapshot = self.modify_metadata( @@ -506,7 +506,7 @@ def test_update_targets_expired_new_target(self) -> None: # new_delegated_target has expired def target_expired_modifier(target: Targets) -> None: - target.expires = datetime(1970, 1, 1, tzinfo=timezone.utc) + target.expires = datetime(1970, 1, 1, tzinfo=UTC) targets = self.modify_metadata(Targets.type, target_expired_modifier) with self.assertRaises(exceptions.ExpiredMetadataError): diff --git a/tests/test_updater_top_level_update.py b/tests/test_updater_top_level_update.py index 76c74d4b57..5faae0850b 100644 --- a/tests/test_updater_top_level_update.py +++ b/tests/test_updater_top_level_update.py @@ -11,7 +11,6 @@ import sys import tempfile import unittest -from datetime import timezone from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import MagicMock, call, patch @@ -50,7 +49,7 @@ class TestRefresh(unittest.TestCase): # set dump_dir to trigger repository state dumps dump_dir: str | None = None - past_datetime = datetime.datetime.now(timezone.utc).replace( + past_datetime = datetime.datetime.now(datetime.UTC).replace( microsecond=0 ) - datetime.timedelta(days=5) @@ -312,7 +311,7 @@ def test_expired_timestamp_version_rollback(self) -> None: - Second updater refresh performed on day 18: assert that rollback check uses expired timestamp v1""" - now = datetime.datetime.now(timezone.utc) + now = datetime.datetime.now(datetime.UTC) self.sim.timestamp.expires = now + datetime.timedelta(days=7) self.sim.timestamp.version = 2 @@ -325,7 +324,7 @@ def test_expired_timestamp_version_rollback(self) -> None: self.sim.timestamp.version = 1 patcher = freezegun.freeze_time( - datetime.datetime.now(timezone.utc) + datetime.timedelta(days=18) + datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=18) ) # Check that a rollback protection is performed even if # local timestamp has expired @@ -346,7 +345,7 @@ def test_expired_timestamp_snapshot_rollback(self) -> None: - Second updater refresh performed on day 18: assert that rollback protection is done with expired timestamp v1""" - now = datetime.datetime.now(timezone.utc) + now = datetime.datetime.now(datetime.UTC) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Bump the snapshot version number to 3 @@ -362,7 +361,7 @@ def test_expired_timestamp_snapshot_rollback(self) -> None: self.sim.timestamp.expires = now + datetime.timedelta(days=21) patcher = freezegun.freeze_time( - datetime.datetime.now(timezone.utc) + datetime.timedelta(days=18) + datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=18) ) # Assert that rollback protection is done even if # local timestamp has expired @@ -822,7 +821,7 @@ def test_expired_metadata(self) -> None: - Second updater refresh performed on day 18, it is successful and timestamp/snapshot final versions are v2""" - now = datetime.datetime.now(timezone.utc) + now = datetime.datetime.now(datetime.UTC) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Make a successful update of valid metadata which stores it in cache @@ -835,7 +834,7 @@ def test_expired_metadata(self) -> None: # Mocking time so that local timestam has expired # but the new timestamp has not with freezegun.freeze_time( - datetime.datetime.now(timezone.utc) + datetime.timedelta(days=18) + datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=18) ): self._run_refresh() diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 7b63cf5e01..53f2b64d6a 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -11,9 +11,8 @@ import hashlib import io import logging -import sys from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import ( IO, TYPE_CHECKING, @@ -68,16 +67,7 @@ def _hash_bytes(data: bytes, algo: str) -> str: def _hash_file(f: IO[bytes], algo: str) -> str: """Returns hexdigest for file using algo.""" f.seek(0) - if sys.version_info >= (3, 11): - digest = hashlib.file_digest(f, lambda: _get_digest(algo)) # type: ignore[arg-type] - - else: - # Fallback for older Pythons. Chunk size is taken from the previously - # used and now deprecated `securesystemslib.hash.digest_fileobject`. - digest = _get_digest(algo) - for chunk in iter(lambda: f.read(4096), b""): - digest.update(chunk) - + digest = hashlib.file_digest(f, lambda: _get_digest(algo)) # type: ignore[arg-type] return digest.hexdigest() @@ -127,8 +117,8 @@ def expires(self, value: datetime) -> None: self._expires = value.replace(microsecond=0) if self._expires.tzinfo is None: # Naive datetime: just make it UTC - self._expires = self._expires.replace(tzinfo=timezone.utc) - elif self._expires.tzinfo != timezone.utc: + self._expires = self._expires.replace(tzinfo=UTC) + elif self._expires.tzinfo != UTC: raise ValueError(f"Expected tz UTC, not {self._expires.tzinfo}") # NOTE: Signed is a stupid name, because this might not be signed yet, but @@ -156,7 +146,7 @@ def __init__( self.spec_version = spec_version - self.expires = expires or datetime.now(timezone.utc) + self.expires = expires or datetime.now(UTC) if version is None: version = 1 @@ -228,7 +218,7 @@ def _common_fields_from_dict( # what the constructor expects and what we store. The inverse operation # is implemented in '_common_fields_to_dict'. expires = datetime.strptime(expires_str, "%Y-%m-%dT%H:%M:%SZ").replace( - tzinfo=timezone.utc + tzinfo=UTC ) return version, spec_version, expires @@ -260,7 +250,7 @@ def is_expired(self, reference_time: datetime | None = None) -> bool: ``True`` if expiration time is less than the reference time. """ if reference_time is None: - reference_time = datetime.now(timezone.utc) + reference_time = datetime.now(UTC) return reference_time >= self.expires diff --git a/tuf/ngclient/_internal/trusted_metadata_set.py b/tuf/ngclient/_internal/trusted_metadata_set.py index 4a3362ad95..0781466882 100644 --- a/tuf/ngclient/_internal/trusted_metadata_set.py +++ b/tuf/ngclient/_internal/trusted_metadata_set.py @@ -116,7 +116,7 @@ def __init__(self, root_data: bytes, envelope_type: EnvelopeType): """ self._trusted_set: dict[str, Signed] = {} self._trusted_delegations: set[tuple[str, str]] = set() - self.reference_time = datetime.datetime.now(datetime.timezone.utc) + self.reference_time = datetime.datetime.now(datetime.UTC) if envelope_type is EnvelopeType.SIMPLE: self._load_data = _load_from_simple_envelope