From 7564596424a426a972ba74b423d78fe87476f317 Mon Sep 17 00:00:00 2001
From: David Douard <david.douard@sdfa3.org>
Date: Tue, 29 Oct 2019 14:33:57 +0100
Subject: [PATCH] model: make model entities frozen

we do not really need them to be mutable, plus we gain their instances now
being hashable, so we can add them in set() for example.
---
 swh/model/hypothesis_strategies.py |  8 +++++---
 swh/model/model.py                 | 24 ++++++++++++------------
 swh/model/tests/test_model.py      |  6 ++++--
 3 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index 3a205a3c..ca568ee7 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -3,6 +3,7 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import attr
 import datetime
 
 from hypothesis.strategies import (
@@ -94,9 +95,10 @@ def releases(draw):
         author=none(),
         date=none(),
         target=sha1_git()))
-    rel.date = date
-    rel.author = author
-    return rel
+    return attr.evolve(
+        rel,
+        date=date,
+        author=author)
 
 
 def revision_metadata():
diff --git a/swh/model/model.py b/swh/model/model.py
index 217e3632..baf4f541 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -51,7 +51,7 @@ class BaseModel:
         return cls(**d)
 
 
-@attr.s
+@attr.s(frozen=True)
 class Person(BaseModel):
     """Represents the author/committer of a revision or release."""
     name = attr.ib(type=bytes)
@@ -59,7 +59,7 @@ class Person(BaseModel):
     fullname = attr.ib(type=bytes)
 
 
-@attr.s
+@attr.s(frozen=True)
 class Timestamp(BaseModel):
     """Represents a naive timestamp from a VCS."""
     seconds = attr.ib(type=int)
@@ -78,7 +78,7 @@ class Timestamp(BaseModel):
             raise ValueError('Microseconds must be in [0, 1000000[.')
 
 
-@attr.s
+@attr.s(frozen=True)
 class TimestampWithTimezone(BaseModel):
     """Represents a TZ-aware timestamp from a VCS."""
     timestamp = attr.ib(type=Timestamp)
@@ -105,7 +105,7 @@ class TimestampWithTimezone(BaseModel):
             negative_utc=d['negative_utc'])
 
 
-@attr.s
+@attr.s(frozen=True)
 class Origin(BaseModel):
     """Represents a software source: a VCS and an URL."""
     url = attr.ib(type=str)
@@ -117,7 +117,7 @@ class Origin(BaseModel):
         return r
 
 
-@attr.s
+@attr.s(frozen=True)
 class OriginVisit(BaseModel):
     """Represents a visit of an origin at a given point in time, by a
     SWH loader."""
@@ -176,7 +176,7 @@ class ObjectType(Enum):
     SNAPSHOT = 'snapshot'
 
 
-@attr.s
+@attr.s(frozen=True)
 class SnapshotBranch(BaseModel):
     """Represents one of the branches of a snapshot."""
     target = attr.ib(type=bytes)
@@ -198,7 +198,7 @@ class SnapshotBranch(BaseModel):
             target_type=TargetType(d['target_type']))
 
 
-@attr.s
+@attr.s(frozen=True)
 class Snapshot(BaseModel):
     """Represents the full state of an origin at a given point in time."""
     id = attr.ib(type=Sha1Git)
@@ -214,7 +214,7 @@ class Snapshot(BaseModel):
             })
 
 
-@attr.s
+@attr.s(frozen=True)
 class Release(BaseModel):
     id = attr.ib(type=Sha1Git)
     name = attr.ib(type=bytes)
@@ -261,7 +261,7 @@ class RevisionType(Enum):
     MERCURIAL = 'hg'
 
 
-@attr.s
+@attr.s(frozen=True)
 class Revision(BaseModel):
     id = attr.ib(type=Sha1Git)
     message = attr.ib(type=bytes)
@@ -291,7 +291,7 @@ class Revision(BaseModel):
             **d)
 
 
-@attr.s
+@attr.s(frozen=True)
 class DirectoryEntry(BaseModel):
     name = attr.ib(type=bytes)
     type = attr.ib(type=str,
@@ -301,7 +301,7 @@ class DirectoryEntry(BaseModel):
     """Usually one of the values of `swh.model.from_disk.DentryPerms`."""
 
 
-@attr.s
+@attr.s(frozen=True)
 class Directory(BaseModel):
     id = attr.ib(type=Sha1Git)
     entries = attr.ib(type=List[DirectoryEntry])
@@ -314,7 +314,7 @@ class Directory(BaseModel):
                      for entry in d['entries']])
 
 
-@attr.s
+@attr.s(frozen=True)
 class Content(BaseModel):
     sha1 = attr.ib(type=bytes)
     sha1_git = attr.ib(type=Sha1Git)
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index b2cc3edc..2900cd1f 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -3,6 +3,7 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import attr
 import copy
 
 from hypothesis import given
@@ -44,8 +45,9 @@ def test_todict_origin_visits(origin_visit):
     obj = origin_visit.to_dict()
 
     assert 'type' not in obj['origin']
-    origin_visit.origin.type = None
-    assert origin_visit == type(origin_visit).from_dict(obj)
+    origin2 = attr.evolve(origin_visit.origin, type=None)
+    origin_visit2 = attr.evolve(origin_visit, origin=origin2)
+    assert origin_visit2 == type(origin_visit).from_dict(obj)
 
 
 def test_content_get_hash():
-- 
GitLab