Repository: larq/zookeeper

Issue:
Include some sort of post-configure hook
We got rid of `validate_configuration`; maybe it can be replaced with `__component_configured__` or something similar.

Discussion:
AdamHillier: The behaviour here mimics a [similar method](https://docs.python.org/3/library/dataclasses.html#post-init-processing) defined for Python dataclasses.
leonoverweel: I'll take a look at this today.

Patch:
diff --git a/zookeeper/core/component.py b/zookeeper/core/component.py
index 6180ab1..ebf57a5 100644
--- a/zookeeper/core/component.py
+++ b/zookeeper/core/component.py
@@ -385,6 +385,20 @@ def component(cls: Type):
         raise TypeError("Component classes must not define a custom `__init__` method.")
     cls.__init__ = __component_init__
 
+    if hasattr(cls, "__post_configure__"):
+        if not callable(cls.__post_configure__):
+            raise TypeError(
+                "The `__post_configure__` attribute of a @component class must be a "
+                "method."
+            )
+        call_args = inspect.signature(cls.__post_configure__).parameters
+        if len(call_args) > 1 or len(call_args) == 1 and "self" not in call_args:
+            raise TypeError(
+                "The `__post_configure__` method of a @component class must take no "
+                f"arguments except `self`, but `{cls.__name__}.__post_configure__` "
+                f"accepts arguments {tuple(name for name in call_args)}."
+            )
+
     # Populate `__component_fields__` with all fields defined on this class and
     # all superclasses. We have to go through the MRO chain and collect them in
     # reverse order so that they are correctly overriden.
@@ -611,3 +625,6 @@ def configure(
             )
 
     instance.__component_configured__ = True
+
+    if hasattr(instance.__class__, "__post_configure__"):
+        instance.__post_configure__()


Test patch:
diff --git a/zookeeper/core/component_test.py b/zookeeper/core/component_test.py
index 7c1f232..f98f2c3 100644
--- a/zookeeper/core/component_test.py
+++ b/zookeeper/core/component_test.py
@@ -409,3 +409,39 @@ def test_component_field_factory_type_check(capsys):
         captured.err
         == "WARNING: Unable to check that typing.Tuple[int, int, int] is a sub-type of typing.Tuple[float, float, float].\n"
     )
+
+
+def test_component_post_configure():
+    with pytest.raises(
+        TypeError,
+        match="The `__post_configure__` attribute of a @component class must be a method.",
+    ):
+
+        @component
+        class A:
+            __post_configure__ = 3.14
+
+    with pytest.raises(
+        TypeError,
+        match="The `__post_configure__` method of a @component class must take no arguments except `self`",
+    ):
+
+        @component
+        class B:
+            def __post_configure__(self, x):
+                pass
+
+    # This definition should succeed.
+    @component
+    class C:
+        a: int = Field(0)
+        b: float = Field(3.14)
+
+        def __post_configure__(self):
+            self.c = self.a + self.b
+
+    c = C()
+
+    configure(c, {"a": 1, "b": -3.14})
+
+    assert c.c == 1 - 3.14