From d2f8c99c19e2de0496aca296bc2a0e30c8276fab Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 23 Mar 2022 16:43:50 +0200 Subject: [PATCH] Metadata API: Add default constructor arguments This allows creating new metadata with less boilerplate: root = Metadata(Root()) targets = Metadata(Targets()) Set reasonable default values for all the arguments -- version to 1, spec_version to current supported version, etc. Expires does not have a good default value and my original plan was to require expires argument to be set. That would mean an incompatible API change though as arguments before expires would be now optional... So expires now defaults to an arbitrary value of 1 day from moment of creation. One noteworthy special case is consistent_snapshot where the default value is True (since that's what we want people to use for new metadata) but None is also used to imply that metadata does not contain the field at all. Signed-off-by: Jussi Kukkonen --- tuf/api/metadata.py | 74 +++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 873ab85f..e7cc9a6d 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -32,7 +32,7 @@ import io import logging import tempfile -from datetime import datetime +from datetime import datetime, timedelta from typing import ( IO, Any, @@ -120,11 +120,11 @@ class Metadata(Generic[T]): def __init__( self, signed: T, - signatures: Dict[str, Signature], + signatures: Optional[Dict[str, Signature]] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): self.signed: T = signed - self.signatures = signatures + self.signatures = signatures or {} self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {} def __eq__(self, other: Any) -> bool: @@ -480,11 +480,13 @@ def expires(self, value: datetime) -> None: # or "inner metadata") def __init__( self, - version: int, - spec_version: str, - expires: datetime, - unrecognized_fields: Optional[Mapping[str, Any]] = None, + version: Optional[int], + spec_version: Optional[str], + expires: Optional[datetime], + unrecognized_fields: Optional[Mapping[str, Any]], ): + if spec_version is None: + spec_version = ".".join(SPECIFICATION_VERSION) # Accept semver (X.Y.Z) but also X.Y for legacy compatibility spec_list = spec_version.split(".") if len(spec_list) not in [2, 3] or not all( @@ -497,11 +499,15 @@ def __init__( raise ValueError(f"Unsupported spec_version {spec_version}") self.spec_version = spec_version - self.expires = expires - if version <= 0: + self.expires = expires or datetime.utcnow() + timedelta(days=1) + + if version is None: + version = 1 + elif version <= 0: raise ValueError(f"version must be > 0, got {version}") self.version = version + self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {} def __eq__(self, other: Any) -> bool: @@ -838,20 +844,22 @@ class Root(Signed): # pylint: disable=too-many-arguments def __init__( self, - version: int, - spec_version: str, - expires: datetime, - keys: Dict[str, Key], - roles: Mapping[str, Role], - consistent_snapshot: Optional[bool] = None, + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + keys: Optional[Dict[str, Key]] = None, + roles: Optional[Mapping[str, Role]] = None, + consistent_snapshot: Optional[bool] = True, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot - self.keys = keys - if set(roles) != TOP_LEVEL_ROLE_NAMES: - raise ValueError("Role names must be the top-level metadata roles") + self.keys = keys or {} + if roles is None: + roles = {r: Role([], 1) for r in TOP_LEVEL_ROLE_NAMES} + elif set(roles) != TOP_LEVEL_ROLE_NAMES: + raise ValueError("Role names must be the top-level metadata roles") self.roles = roles def __eq__(self, other: Any) -> bool: @@ -1129,14 +1137,14 @@ class Timestamp(Signed): def __init__( self, - version: int, - spec_version: str, - expires: datetime, - snapshot_meta: MetaFile, + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + snapshot_meta: Optional[MetaFile] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) - self.snapshot_meta = snapshot_meta + self.snapshot_meta = snapshot_meta or MetaFile(1) def __eq__(self, other: Any) -> bool: if not isinstance(other, Timestamp): @@ -1190,14 +1198,14 @@ class Snapshot(Signed): def __init__( self, - version: int, - spec_version: str, - expires: datetime, - meta: Dict[str, MetaFile], + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + meta: Optional[Dict[str, MetaFile]] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) - self.meta = meta + self.meta = meta if meta is not None else {"targets.json": MetaFile(1)} def __eq__(self, other: Any) -> bool: if not isinstance(other, Snapshot): @@ -1660,15 +1668,15 @@ class Targets(Signed): # pylint: disable=too-many-arguments def __init__( self, - version: int, - spec_version: str, - expires: datetime, - targets: Dict[str, TargetFile], + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + targets: Optional[Dict[str, TargetFile]] = None, delegations: Optional[Delegations] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: super().__init__(version, spec_version, expires, unrecognized_fields) - self.targets = targets + self.targets = targets or {} self.delegations = delegations def __eq__(self, other: Any) -> bool: