Skip to content

Commit 8e9afc9

Browse files
committed
Revert "Move to/from_dict metadata API methods..."
Revert an earlier commit that moved to/from_dict metadata class model methods to a util module of the serialization sub-package. We keep to/from_dict methods on the metadata classes because: - It seems **idiomatic** (see e.g. 3rd-party libaries such as attrs, pydantic, marshmallow, or built-ins that provide default or customizable dict representation for higher-level objects). The idiomatic choice should make usage more intuitive. - It feels better **structured** when each method is encapsulated within the corresponding class, which in turn should make maintaining/modifying/extending the class model easier. - It allows us to remove function-scope imports (see subsequent commit). Caveat: Now that "the meat" of the sub-packaged JSON serializer is implemented on the class, it might make it harder to create a non-dict based serializer by copy-paste-amending the JSON serializer. However, the benefits from above seem to outweigh the disadvantage. See option 5 of ADR0006 for further details (#1270). Signed-off-by: Lukas Puehringer <[email protected]>
1 parent e1be085 commit 8e9afc9

File tree

5 files changed

+137
-168
lines changed

5 files changed

+137
-168
lines changed

tests/test_api.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
CanonicalJSONSerializer
3939
)
4040

41-
from tuf.api.serialization.util import metadata_to_dict
42-
4341
from securesystemslib.interface import (
4442
import_ed25519_publickey_from_file,
4543
import_ed25519_privatekey_from_file
@@ -49,7 +47,6 @@
4947
format_keyval_to_metadata
5048
)
5149

52-
5350
logger = logging.getLogger(__name__)
5451

5552

@@ -114,9 +111,9 @@ def test_generic_read(self):
114111
self.assertTrue(
115112
isinstance(metadata_obj2.signed, inner_metadata_cls))
116113

117-
# ... and are equal (compared by dict representation)
118-
self.assertDictEqual(metadata_to_dict(metadata_obj),
119-
metadata_to_dict(metadata_obj2))
114+
# ... and return the same object (compared by dict representation)
115+
self.assertDictEqual(
116+
metadata_obj.to_dict(), metadata_obj2.to_dict())
120117

121118

122119
# Assert that it chokes correctly on an unknown metadata type
@@ -148,8 +145,9 @@ def test_read_write_read_compare(self):
148145
metadata_obj.to_file(path_2)
149146
metadata_obj_2 = Metadata.from_file(path_2)
150147

151-
self.assertDictEqual(metadata_to_dict(metadata_obj),
152-
metadata_to_dict(metadata_obj_2))
148+
self.assertDictEqual(
149+
metadata_obj.to_dict(),
150+
metadata_obj_2.to_dict())
153151

154152
os.remove(path_2)
155153

tuf/api/metadata.py

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import tuf.exceptions
2424

2525

26+
2627
# Types
2728
JsonDict = Dict[str, Any]
2829

@@ -55,6 +56,49 @@ def __init__(self, signed: 'Signed', signatures: list) -> None:
5556
self.signatures = signatures
5657

5758

59+
# Deserialization (factories).
60+
@classmethod
61+
def from_dict(cls, metadata: JsonDict) -> 'Metadata':
62+
"""Creates Metadata object from its JSON/dict representation.
63+
64+
Calls 'from_dict' for any complex metadata attribute represented by a
65+
class also that has a 'from_dict' factory method. (Currently this is
66+
only the signed attribute.)
67+
68+
Arguments:
69+
metadata: TUF metadata in JSON/dict representation, as e.g.
70+
returned by 'json.loads'.
71+
72+
Raises:
73+
KeyError: The metadata dict format is invalid.
74+
ValueError: The metadata has an unrecognized signed._type field.
75+
76+
Returns:
77+
A TUF Metadata object.
78+
79+
"""
80+
# Dispatch to contained metadata class on metadata _type field.
81+
_type = metadata['signed']['_type']
82+
83+
if _type == 'targets':
84+
inner_cls = Targets
85+
elif _type == 'snapshot':
86+
inner_cls = Snapshot
87+
elif _type == 'timestamp':
88+
inner_cls = Timestamp
89+
elif _type == 'root':
90+
inner_cls = Root
91+
else:
92+
raise ValueError(f'unrecognized metadata type "{_type}"')
93+
94+
# NOTE: If Signature becomes a class, we should iterate over
95+
# metadata['signatures'], call Signature.from_dict for each item, and
96+
# pass a list of Signature objects to the Metadata constructor intead.
97+
return cls(
98+
signed=inner_cls.from_dict(metadata['signed']),
99+
signatures=metadata['signatures'])
100+
101+
58102
@classmethod
59103
def from_file(
60104
cls, filename: str, deserializer: MetadataDeserializer = None,
@@ -95,6 +139,14 @@ def from_file(
95139
return deserializer.deserialize(raw_data)
96140

97141

142+
# Serialization.
143+
def to_dict(self) -> JsonDict:
144+
"""Returns the JSON-serializable dictionary representation of self. """
145+
return {
146+
'signatures': self.signatures,
147+
'signed': self.signed.to_dict()
148+
}
149+
98150
def to_file(self, filename: str, serializer: MetadataSerializer = None,
99151
storage_backend: StorageBackendInterface = None) -> None:
100152
"""Writes TUF metadata to file storage.
@@ -250,6 +302,39 @@ def __init__(
250302
self.version = version
251303

252304

305+
# Deserialization (factories).
306+
@classmethod
307+
def from_dict(cls, signed_dict: JsonDict) -> 'Signed':
308+
"""Creates Signed object from its JSON/dict representation. """
309+
310+
# Convert 'expires' TUF metadata string to a datetime object, which is
311+
# what the constructor expects and what we store. The inverse operation
312+
# is implemented in 'to_dict'.
313+
signed_dict['expires'] = tuf.formats.expiry_string_to_datetime(
314+
signed_dict['expires'])
315+
# NOTE: We write the converted 'expires' back into 'signed_dict' above
316+
# so that we can pass it to the constructor as '**signed_dict' below,
317+
# along with other fields that belong to Signed subclasses.
318+
# Any 'from_dict'(-like) conversions of fields that correspond to a
319+
# subclass should be performed in the 'from_dict' method of that
320+
# subclass and also be written back into 'signed_dict' before calling
321+
# super().from_dict.
322+
323+
# NOTE: cls might be a subclass of Signed, if 'from_dict' was called on
324+
# that subclass (see e.g. Metadata.from_dict).
325+
return cls(**signed_dict)
326+
327+
328+
def to_dict(self) -> JsonDict:
329+
"""Returns the JSON-serializable dictionary representation of self. """
330+
return {
331+
'_type': self._type,
332+
'version': self.version,
333+
'spec_version': self.spec_version,
334+
'expires': self.expires.isoformat() + 'Z'
335+
}
336+
337+
253338
# Modification.
254339
def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None:
255340
"""Increments the expires attribute by the passed timedelta. """
@@ -261,7 +346,6 @@ def bump_version(self) -> None:
261346
self.version += 1
262347

263348

264-
265349
class Root(Signed):
266350
"""A container for the signed part of root metadata.
267351
@@ -311,6 +395,18 @@ def __init__(
311395
self.roles = roles
312396

313397

398+
# Serialization.
399+
def to_dict(self) -> JsonDict:
400+
"""Returns the JSON-serializable dictionary representation of self. """
401+
json_dict = super().to_dict()
402+
json_dict.update({
403+
'consistent_snapshot': self.consistent_snapshot,
404+
'keys': self.keys,
405+
'roles': self.roles
406+
})
407+
return json_dict
408+
409+
314410
# Update key for a role.
315411
def add_key(self, role: str, keyid: str, key_metadata: JsonDict) -> None:
316412
"""Adds new key for 'role' and updates the key store. """
@@ -332,6 +428,7 @@ def remove_key(self, role: str, keyid: str) -> None:
332428

333429

334430

431+
335432
class Timestamp(Signed):
336433
"""A container for the signed part of timestamp metadata.
337434
@@ -359,6 +456,16 @@ def __init__(
359456
self.meta = meta
360457

361458

459+
# Serialization.
460+
def to_dict(self) -> JsonDict:
461+
"""Returns the JSON-serializable dictionary representation of self. """
462+
json_dict = super().to_dict()
463+
json_dict.update({
464+
'meta': self.meta
465+
})
466+
return json_dict
467+
468+
362469
# Modification.
363470
def update(self, version: int, length: int, hashes: JsonDict) -> None:
364471
"""Assigns passed info about snapshot metadata to meta dict. """
@@ -369,7 +476,6 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None:
369476
}
370477

371478

372-
373479
class Snapshot(Signed):
374480
"""A container for the signed part of snapshot metadata.
375481
@@ -403,6 +509,15 @@ def __init__(
403509
# TODO: Add class for meta
404510
self.meta = meta
405511

512+
# Serialization.
513+
def to_dict(self) -> JsonDict:
514+
"""Returns the JSON-serializable dictionary representation of self. """
515+
json_dict = super().to_dict()
516+
json_dict.update({
517+
'meta': self.meta
518+
})
519+
return json_dict
520+
406521

407522
# Modification.
408523
def update(
@@ -419,7 +534,6 @@ def update(
419534
self.meta[metadata_fn]['hashes'] = hashes
420535

421536

422-
423537
class Targets(Signed):
424538
"""A container for the signed part of targets metadata.
425539
@@ -487,6 +601,16 @@ def __init__(
487601
self.delegations = delegations
488602

489603

604+
# Serialization.
605+
def to_dict(self) -> JsonDict:
606+
"""Returns the JSON-serializable dictionary representation of self. """
607+
json_dict = super().to_dict()
608+
json_dict.update({
609+
'targets': self.targets,
610+
'delegations': self.delegations,
611+
})
612+
return json_dict
613+
490614
# Modification.
491615
def update(self, filename: str, fileinfo: JsonDict) -> None:
492616
"""Assigns passed target file info to meta dict. """

tuf/api/pylintrc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,5 @@ good-names=e
88
indent-string=" "
99
max-line-length=79
1010

11-
[CLASSES]
12-
exclude-protected:_type
13-
1411
[DESIGN]
1512
min-public-methods=0

tuf/api/serialization/json.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
MetadataDeserializer,
2020
SignedSerializer,
2121
SerializationError,
22-
DeserializationError,
23-
util)
22+
DeserializationError)
2423

2524

2625
class JSONDeserializer(MetadataDeserializer):
@@ -30,7 +29,7 @@ def deserialize(self, raw_data: bytes) -> Metadata:
3029
"""Deserialize utf-8 encoded JSON bytes into Metadata object. """
3130
try:
3231
_dict = json.loads(raw_data.decode("utf-8"))
33-
return util.metadata_from_dict(_dict)
32+
return Metadata.from_dict(_dict)
3433

3534
except Exception as e: # pylint: disable=broad-except
3635
six.raise_from(DeserializationError, e)
@@ -52,7 +51,7 @@ def serialize(self, metadata_obj: Metadata) -> bytes:
5251
try:
5352
indent = (None if self.compact else 1)
5453
separators=((',', ':') if self.compact else (',', ': '))
55-
return json.dumps(util.metadata_to_dict(metadata_obj),
54+
return json.dumps(metadata_obj.to_dict(),
5655
indent=indent,
5756
separators=separators,
5857
sort_keys=True).encode("utf-8")
@@ -67,7 +66,7 @@ class CanonicalJSONSerializer(SignedSerializer):
6766
def serialize(self, signed_obj: Signed) -> bytes:
6867
"""Serialize Signed object into utf-8 encoded Canonical JSON bytes. """
6968
try:
70-
signed_dict = util.signed_to_dict(signed_obj)
69+
signed_dict = signed_obj.to_dict()
7170
return encode_canonical(signed_dict).encode("utf-8")
7271

7372
except Exception as e: # pylint: disable=broad-except

0 commit comments

Comments
 (0)