From 6ef1dc1cdd7c5fdaf002bf31c1a2d3203815f436 Mon Sep 17 00:00:00 2001
From: Valentin Lorentz <vlorentz@softwareheritage.org>
Date: Thu, 9 May 2019 16:29:46 +0200
Subject: [PATCH] Explicitely implement from_dict instead of using
 introspection magic.

There is more repetition, but it's easier to read and
'%timeit Revision.from_dict(d)' is 5 times faster.
---
 swh/model/model.py | 98 ++++++++++++++++++++++------------------------
 1 file changed, 47 insertions(+), 51 deletions(-)

diff --git a/swh/model/model.py b/swh/model/model.py
index 25a565b4..6e3fd0e0 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -16,20 +16,6 @@ from .identifiers import normalize_timestamp
 Sha1Git = bytes
 
 
-def contains_optional_validator(validator):
-    """Inspects an attribute's validator to find its type.
-    Inspired by `hypothesis/searchstrategy/attrs.py`."""
-    if isinstance(validator, attr.validators._OptionalValidator):
-        return True
-    elif isinstance(validator, attr.validators._AndValidator):
-        for validator in validator._validators:
-            res = contains_optional_validator(validator)
-            if res:
-                return True
-    else:
-        return False
-
-
 class BaseModel:
     """Base class for SWH model classes.
 
@@ -45,31 +31,7 @@ class BaseModel:
     def from_dict(cls, d):
         """Takes a dictionary representing a tree of SWH objects, and
         recursively builds the corresponding objects."""
-        if not isinstance(d, dict):
-            raise TypeError(
-                '%s.from_dict expects a dict, not %r' % (cls.__name__, d))
-        kwargs = {}
-        for (name, attribute) in attr.fields_dict(cls).items():
-            type_ = attribute.type
-
-            # Heuristic to detect `Optional[X]` and unwrap it to `X`.
-            if contains_optional_validator(attribute.validator):
-                if name not in d:
-                    continue
-                if d[name] is None:
-                    continue
-                else:
-                    type_ = type_.__args__[0]
-
-            # Construct an object of the expected type
-            if issubclass(type_, BaseModel):
-                kwargs[name] = type_.from_dict(d[name])
-            elif issubclass(type_, Enum):
-                kwargs[name] = type_(d[name])
-            else:
-                kwargs[name] = d[name]
-
-        return cls(**kwargs)
+        return cls(**d)
 
 
 @attr.s
@@ -119,7 +81,11 @@ class TimestampWithTimezone(BaseModel):
     def from_dict(cls, d):
         """Builds a TimestampWithTimezone from any of the formats
         accepted by :py:`swh.model.normalize_timestamp`."""
-        return super().from_dict(normalize_timestamp(d))
+        d = normalize_timestamp(d)
+        return cls(
+            timestamp=Timestamp.from_dict(d['timestamp']),
+            offset=d['offset'],
+            negative_utc=d['negative_utc'])
 
 
 @attr.s
@@ -197,6 +163,12 @@ class SnapshotBranch(BaseModel):
         branch['target_type'] = branch['target_type'].value
         return branch
 
+    @classmethod
+    def from_dict(cls, d):
+        return cls(
+            target=d['target'],
+            target_type=TargetType(d['target_type']))
+
 
 @attr.s
 class Snapshot(BaseModel):
@@ -215,14 +187,12 @@ class Snapshot(BaseModel):
 
     @classmethod
     def from_dict(cls, d):
-        d = {
-            **d,
-            'branches': {
+        return cls(
+            id=d['id'],
+            branches={
                 name: SnapshotBranch.from_dict(branch)
                 for (name, branch) in d['branches'].items()
-            }
-        }
-        return cls(**d)
+            })
 
 
 @attr.s
@@ -253,6 +223,17 @@ class Release(BaseModel):
         rel['target_type'] = rel['target_type'].value
         return rel
 
+    @classmethod
+    def from_dict(cls, d):
+        d = d.copy()
+        if d.get('author'):
+            d['author'] = Person.from_dict(d['author'])
+        if d.get('date'):
+            d['date'] = TimestampWithTimezone.from_dict(d['date'])
+        return cls(
+            target_type=ObjectType(d.pop('target_type')),
+            **d)
+
 
 class RevisionType(Enum):
     GIT = 'git'
@@ -286,6 +267,22 @@ class Revision(BaseModel):
         rev['type'] = rev['type'].value
         return rev
 
+    @classmethod
+    def from_dict(cls, d):
+        return cls(
+            id=d['id'],
+            message=d['message'],
+            author=Person.from_dict(d['author']),
+            committer=Person.from_dict(d['committer']),
+            date=TimestampWithTimezone.from_dict(d['date']),
+            committer_date=TimestampWithTimezone.from_dict(
+                d['committer_date']),
+            type=RevisionType(d['type']),
+            directory=d['directory'],
+            synthetic=d['synthetic'],
+            metadata=d['metadata'],
+            parents=d['parents'])
+
 
 @attr.s
 class DirectoryEntry(BaseModel):
@@ -309,11 +306,10 @@ class Directory(BaseModel):
 
     @classmethod
     def from_dict(cls, d):
-        d = {
-            **d,
-            'entries': list(map(DirectoryEntry.from_dict, d['entries']))
-        }
-        return super().from_dict(d)
+        return cls(
+            id=d['id'],
+            entries=[DirectoryEntry.from_dict(entry)
+                     for entry in d['entries']])
 
 
 @attr.s
-- 
GitLab