Repository: larq/zookeeper

Issue:
`__getattribute__` can incorrectly call `build()` on factory instances
Implicit calling of `build()` is desired behaviour when accessing a field, but the current behaviour is for this to happen always, including e.g. when accessing `instance.__component_parent__`, as is frequently done internally. This is incorrect behaviour, and causes failures with confusing error messages (and sometimes infinite recursion). The behaviour in question comes from here:

https://github.com/larq/zookeeper/blob/c8eaab4bff5aaff6781617c7ec5b7eb9f04fb6d0/zookeeper/core/component.py#L215-L220

We need to check that `name` is a valid field name before calling `build()` implicitly.

Apologies to @timdebruin for this bug....

Discussion:

Patch:
diff --git a/zookeeper/core/component.py b/zookeeper/core/component.py
index aa248b9..8ce0517 100644
--- a/zookeeper/core/component.py
+++ b/zookeeper/core/component.py
@@ -215,7 +215,9 @@ def _wrap_getattribute(component_cls: Type) -> None:
     @functools.wraps(base_wrapped_fn)
     def wrapped_fn(instance, name):
         result = base_wrapped_fn(instance, name)
-        if utils.is_factory_instance(result):
+        if name in base_wrapped_fn(
+            instance, "__component_fields__"
+        ) and utils.is_factory_instance(result):
             return result.build()
         return result
 


Test patch:
diff --git a/zookeeper/core/component_test.py b/zookeeper/core/component_test.py
index 7f57380..8495571 100644
--- a/zookeeper/core/component_test.py
+++ b/zookeeper/core/component_test.py
@@ -393,6 +393,30 @@ def test_component_field_factory_type_check(capsys):
     )
 
 
+def test_component_getattr_value_via_factory_parent():
+    """See https://github.com/larq/zookeeper/issues/121."""
+
+    @component
+    class Child:
+        x: int = Field()
+
+    @factory
+    class Factory:
+        child: Child = ComponentField(Child)
+
+        x: int = Field(5)
+
+        def build(self) -> int:
+            return self.child.x
+
+    f = Factory()
+
+    configure(f, {})
+
+    assert f.child.x == 5
+    assert f.build() == 5
+
+
 def test_component_post_configure():
     with pytest.raises(
         TypeError,