diff --git a/swh/model/model.py b/swh/model/model.py --- a/swh/model/model.py +++ b/swh/model/model.py @@ -29,7 +29,7 @@ from . import git_objects from .collections import ImmutableDict -from .hashutil import DEFAULT_ALGORITHMS, MultiHash +from .hashutil import DEFAULT_ALGORITHMS, MultiHash, hash_to_hex from .swhids import CoreSWHID from .swhids import ExtendedObjectType as SwhidExtendedObjectType from .swhids import ExtendedSWHID @@ -58,6 +58,13 @@ VT = TypeVar("VT") +def hash_repr(h: bytes) -> str: + if h is None: + return "None" + else: + return f"hash_to_bytes('{hash_to_hex(h)}')" + + def freeze_optional_dict( d: Union[None, Dict[KT, VT], ImmutableDict[KT, VT]] # type: ignore ) -> Optional[ImmutableDict[KT, VT]]: @@ -491,7 +498,9 @@ ["created", "ongoing", "full", "partial", "not_found", "failed"] ), ) - snapshot = attr.ib(type=Optional[Sha1Git], validator=type_validator()) + snapshot = attr.ib( + type=Optional[Sha1Git], validator=type_validator(), repr=hash_repr + ) # Type is optional be to able to use it before adding it to the database model type = attr.ib(type=Optional[str], validator=type_validator(), default=None) metadata = attr.ib( @@ -522,6 +531,9 @@ SNAPSHOT = "snapshot" ALIAS = "alias" + def __repr__(self): + return f"TargetType.{self.name}" + class ObjectType(Enum): """The type of content pointed to by a release. Usually a revision""" @@ -532,6 +544,9 @@ RELEASE = "release" SNAPSHOT = "snapshot" + def __repr__(self): + return f"ObjectType.{self.name}" + @attr.s(frozen=True, slots=True) class SnapshotBranch(BaseModel): @@ -539,7 +554,7 @@ object_type: Final = "snapshot_branch" - target = attr.ib(type=bytes, validator=type_validator()) + target = attr.ib(type=bytes, validator=type_validator(), repr=hash_repr) target_type = attr.ib(type=TargetType, validator=type_validator()) @target.validator @@ -566,7 +581,7 @@ validator=type_validator(), converter=freeze_optional_dict, ) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) def compute_hash(self) -> bytes: git_object = git_objects.snapshot_git_object(self) @@ -594,7 +609,7 @@ name = attr.ib(type=bytes, validator=type_validator()) message = attr.ib(type=Optional[bytes], validator=type_validator()) - target = attr.ib(type=Optional[Sha1Git], validator=type_validator()) + target = attr.ib(type=Optional[Sha1Git], validator=type_validator(), repr=hash_repr) target_type = attr.ib(type=ObjectType, validator=type_validator()) synthetic = attr.ib(type=bool, validator=type_validator()) author = attr.ib(type=Optional[Person], validator=type_validator(), default=None) @@ -607,7 +622,7 @@ converter=freeze_optional_dict, default=None, ) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) def compute_hash(self) -> bytes: git_object = git_objects.release_git_object(self) @@ -656,6 +671,9 @@ CVS = "cvs" BAZAAR = "bzr" + def __repr__(self): + return f"RevisionType.{self.name}" + def tuplify_extra_headers(value: Iterable): return tuple((k, v) for k, v in value) @@ -673,7 +691,7 @@ type=Optional[TimestampWithTimezone], validator=type_validator() ) type = attr.ib(type=RevisionType, validator=type_validator()) - directory = attr.ib(type=Sha1Git, validator=type_validator()) + directory = attr.ib(type=Sha1Git, validator=type_validator(), repr=hash_repr) synthetic = attr.ib(type=bool, validator=type_validator()) metadata = attr.ib( type=Optional[ImmutableDict[str, object]], @@ -682,7 +700,7 @@ default=None, ) parents = attr.ib(type=Tuple[Sha1Git, ...], validator=type_validator(), default=()) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) extra_headers = attr.ib( type=Tuple[Tuple[bytes, bytes], ...], validator=type_validator(), @@ -750,8 +768,8 @@ name = attr.ib(type=bytes, validator=type_validator()) type = attr.ib(type=str, validator=attr.validators.in_(["file", "dir", "rev"])) - target = attr.ib(type=Sha1Git, validator=type_validator()) - perms = attr.ib(type=int, validator=type_validator(), converter=int) + target = attr.ib(type=Sha1Git, validator=type_validator(), repr=hash_repr) + perms = attr.ib(type=int, validator=type_validator(), converter=int, repr=oct) """Usually one of the values of `swh.model.from_disk.DentryPerms`.""" @@ -760,7 +778,7 @@ object_type: Final = "directory" entries = attr.ib(type=Tuple[DirectoryEntry, ...], validator=type_validator()) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) def compute_hash(self) -> bytes: git_object = git_objects.directory_git_object(self) @@ -821,10 +839,10 @@ class Content(BaseContent): object_type: Final = "content" - sha1 = attr.ib(type=bytes, validator=type_validator()) - sha1_git = attr.ib(type=Sha1Git, validator=type_validator()) - sha256 = attr.ib(type=bytes, validator=type_validator()) - blake2s256 = attr.ib(type=bytes, validator=type_validator()) + sha1 = attr.ib(type=bytes, validator=type_validator(), repr=hash_repr) + sha1_git = attr.ib(type=Sha1Git, validator=type_validator(), repr=hash_repr) + sha256 = attr.ib(type=bytes, validator=type_validator(), repr=hash_repr) + blake2s256 = attr.ib(type=bytes, validator=type_validator(), repr=hash_repr) length = attr.ib(type=int, validator=type_validator()) @@ -904,10 +922,14 @@ class SkippedContent(BaseContent): object_type: Final = "skipped_content" - sha1 = attr.ib(type=Optional[bytes], validator=type_validator()) - sha1_git = attr.ib(type=Optional[Sha1Git], validator=type_validator()) - sha256 = attr.ib(type=Optional[bytes], validator=type_validator()) - blake2s256 = attr.ib(type=Optional[bytes], validator=type_validator()) + sha1 = attr.ib(type=Optional[bytes], validator=type_validator(), repr=hash_repr) + sha1_git = attr.ib( + type=Optional[Sha1Git], validator=type_validator(), repr=hash_repr + ) + sha256 = attr.ib(type=Optional[bytes], validator=type_validator(), repr=hash_repr) + blake2s256 = attr.ib( + type=Optional[bytes], validator=type_validator(), repr=hash_repr + ) length = attr.ib(type=Optional[int], validator=type_validator()) @@ -985,6 +1007,9 @@ FORGE = "forge" REGISTRY = "registry" + def __repr__(self): + return f"MetadataAuthorityType.{self.name}" + @attr.s(frozen=True, slots=True) class MetadataAuthority(BaseModel): @@ -1090,7 +1115,7 @@ type=Optional[CoreSWHID], default=None, validator=type_validator() ) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) def compute_hash(self) -> bytes: git_object = git_objects.raw_extrinsic_metadata_git_object(self) @@ -1282,7 +1307,7 @@ target = attr.ib(type=CoreSWHID, validator=type_validator()) extid_version = attr.ib(type=int, validator=type_validator(), default=0) - id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"") + id = attr.ib(type=Sha1Git, validator=type_validator(), default=b"", repr=hash_repr) @classmethod def from_dict(cls, d): diff --git a/swh/model/swhids.py b/swh/model/swhids.py --- a/swh/model/swhids.py +++ b/swh/model/swhids.py @@ -79,7 +79,7 @@ _TSWHID = TypeVar("_TSWHID", bound="_BaseSWHID") -@attr.s(frozen=True, kw_only=True) +@attr.s(frozen=True, kw_only=True, repr=False) class _BaseSWHID(Generic[_TObjectType]): """Common base class for CoreSWHID, QualifiedSWHID, and ExtendedSWHID. @@ -132,6 +132,9 @@ ] ) + def __repr__(self) -> str: + return f"{self.__class__.__name__}.from_string('{self}')" + @classmethod def from_string(cls: Type[_TSWHID], s: str) -> _TSWHID: parts = _parse_swhid(s) @@ -145,7 +148,7 @@ ) from None -@attr.s(frozen=True, kw_only=True) +@attr.s(frozen=True, kw_only=True, repr=False) class CoreSWHID(_BaseSWHID[ObjectType]): """ Dataclass holding the relevant info associated to a SoftWare Heritage @@ -223,7 +226,7 @@ return urllib.parse.unquote_to_bytes(path) -@attr.s(frozen=True, kw_only=True) +@attr.s(frozen=True, kw_only=True, repr=False) class QualifiedSWHID(_BaseSWHID[ObjectType]): """ Dataclass holding the relevant info associated to a SoftWare Heritage @@ -361,6 +364,9 @@ swhid += "%s%s=%s" % (SWHID_CTXT_SEP, k, v) return swhid + def __repr__(self) -> str: + return super().__repr__() + @classmethod def from_string(cls, s: str) -> QualifiedSWHID: parts = _parse_swhid(s) @@ -379,7 +385,7 @@ ) from None -@attr.s(frozen=True, kw_only=True) +@attr.s(frozen=True, kw_only=True, repr=False) class ExtendedSWHID(_BaseSWHID[ExtendedObjectType]): """ Dataclass holding the relevant info associated to a SoftWare Heritage diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py --- a/swh/model/tests/test_model.py +++ b/swh/model/tests/test_model.py @@ -13,11 +13,13 @@ from hypothesis import given from hypothesis.strategies import binary import pytest +import zoneinfo from swh.model.collections import ImmutableDict from swh.model.from_disk import DentryPerms from swh.model.hashutil import MultiHash, hash_to_bytes import swh.model.hypothesis_strategies as strategies +import swh.model.model from swh.model.model import ( BaseModel, Content, @@ -40,6 +42,7 @@ TimestampWithTimezone, type_validator, ) +import swh.model.swhids from swh.model.swhids import CoreSWHID, ExtendedSWHID, ObjectType from swh.model.tests.swh_model_data import TEST_OBJECTS from swh.model.tests.test_identifiers import ( @@ -75,6 +78,27 @@ assert obj_as_dict == type(obj).from_dict(obj_as_dict).to_dict() +@given(strategies.objects()) +def test_repr(objtype_and_obj): + """Checks every model object has a working repr(), and that it can be eval()uated + (so that printed objects can be copy-pasted to write test cases.)""" + (obj_type, obj) = objtype_and_obj + + r = repr(obj) + env = { + "tzutc": lambda: datetime.timezone.utc, + "tzfile": lambda path: zoneinfo.ZoneInfo.from_file(open(path, "rb")), + "hash_to_bytes": hash_to_bytes, + **swh.model.swhids.__dict__, + **swh.model.model.__dict__, + } + try: + obj2 = eval(r, env) + except Exception: + raise + assert obj2 == obj + + @attr.s class Cls1: pass