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
def test_str_and_repr(): """ `__str__` and `__repr__` should give formatted strings that represent nested components nicely. """ @component class Child: a: int b: str c: List[float] @component class Parent: b: str = "bar" child: Child = Child() p = Parent() configure(p, {"a": 10, "b": "foo", "c": [1.5, -1.2]}, name="parent") assert (unstyle(repr(p)) == """Parent(b="foo", child=Child(a=10, b="foo", c=[1.5, -1.2]))""") assert (unstyle(str(p)) == """Parent( b="foo", child=Child( a=10, b="foo", c=[1.5, -1.2] ) )""")
def test_component_pre_configure_setattr_with_nesting(): @component class Child: a: int = Field() @component class Parent: child_1: Child = ComponentField(Child) child_2: Child = ComponentField(Child, a=-1) a: int = Field(5) instance = Parent(a=100) assert instance.a == 100 assert instance.child_1.a == 100 assert instance.child_2.a == -1 instance.a = 2020 instance.child_2.a = -7 assert instance.a == 2020 assert instance.child_1.a == 2020 assert instance.child_2.a == -7 configure(instance, {"a": 0, "child_2.a": 5}) assert instance.a == 0 assert instance.child_1.a == 0 assert instance.child_2.a == 5
def test_itemsview_protocol(): @component class Child: a: int = Field() b: Tuple[int, float] = Field() c: str = Field(allow_missing=True) @component class Parent: a: int = Field() b: Tuple[int, float] = Field((5, -42.3)) child: Child = ComponentField() instance = Parent() configure(instance, {"a": 10, "child.b": (-1, 0.0)}) instance_dict = dict(instance) # Check it's what we expect assert instance_dict == { "a": 10, "b": (5, -42.3), "child": "test_itemsview_protocol.<locals>.Child", "child.b": (-1, 0.0), } # Check that, using the old config, we can instantiate and configure an # identical component tree. new_instance = Parent() configure(new_instance, instance_dict) assert new_instance.a == instance.a assert new_instance.b == instance.b assert new_instance.child.a == instance.child.a assert new_instance.child.b == instance.child.b
def test_len(): with pytest.raises( TypeError, match="Component classes must not define a custom `__len__` method." ): @component class InvalidComponent: def __len__(): return @component class Component: a: int = Field() b: Tuple[int, float] = Field((5, -42.3)) c: str = Field(allow_missing=True) instance = Component() configure(instance, {"a": 10}) # When a field is `allow_missing` it should not be counted. assert len(instance) == 2 instance = Component() configure(instance, {"a": 10, "c": "foo"}) # But when the `allow_missing` field gets assigned a value it should be # counted. assert len(instance) == 3
def test_configure_override_field_values(ExampleComponentClass): """Component fields should be overriden correctly.""" x = ExampleComponentClass() configure(x, {"a": 0, "b": "bar"}) assert x.a == 0 assert x.b == "bar"
def test_component_inherited_factory_value(): """https://github.com/larq/zookeeper/issues/123.""" @factory class IntFactory: def build(self) -> int: return 5 @component class Child: x: int = ComponentField() @component class Parent: child: Child = ComponentField(Child) x: int = ComponentField(IntFactory) p = Parent() configure(p, {}) assert p.x == 5 assert p.child.x == 5 p = Parent() configure(p, {"child.x": 7}) assert p.x == 5 assert p.child.x == 7
def test_component_configure_component_field_allow_missing(): class Base: a: int = Field() @component class Child1(Base): a = Field(5) @component class Child2(Base): a = Field(5) @component class Parent: child: Base = ComponentField() child_allow_missing: Base = ComponentField(allow_missing=True) # Missing out "child" should cause an error. with pytest.raises( ValueError, match= "Component field 'Parent.child' of type 'Base' has no default or configured class.", ): configure(Parent(), {"child_allow_missing": "Child2"}) # But missing out "child_allow_missing" should succeed. instance = Parent() configure(instance, {"child": "Child1"}) assert not hasattr(instance, "child_allow_missing")
def test_component_pre_configure_setattr(): @component class A: a: int = Field(6) b: float = Field(allow_missing=True) c: float = Field() # Setting default values on fields before configuration is fine instance = A() instance.a = 3 instance.b = 5.0 instance.c = 7.8 configure(instance, {"a": 0}) assert instance.a == 0 assert instance.b == 5.0 assert instance.c == 7.8 # Setting values after configuration is prohibited with pytest.raises( ValueError, match=( "Setting already configured component field values directly is prohibited. " "Use Zookeeper component configuration to set field values." ), ): instance.a = 5
def test_configure_non_interactive_missing_field_value(ExampleComponentClass): """When not configuring interactively, an error should be raised if a field has neither a default nor a configured value.""" with pytest.raises( ValueError, match=r"^No configuration value found for annotated field 'FAKE_NAME.a' of type 'int'.", ): configure(ExampleComponentClass(), {"b": "bar"}, name="FAKE_NAME")
def test_configure_scoped_override_field_values(): """Field overriding should respect component scope.""" @component class Child: a: int b: str c: List[float] @component class Parent: b: str = "bar" child: Child = Child() @component class GrandParent: a: int b: str parent: Parent = Parent() grand_parent = GrandParent() configure( grand_parent, { "a": 10, "parent.a": 15, "b": "foo", "parent.child.b": "baz", "c": [1.5, -1.2], "parent.c": [-17.2], "parent.child.c": [0, 4.2], }, ) # The grand-parent `grand_parent` should have the value `a` = 10. Even # though a config value is declared for its scope, `grand_parent.child` # should have no `a` value set, as it doesn't declare `a` as a field. # Despite this, `grand_parent.parent.child` should get the value `a` = 15, # as it lives inside the configuration scope of its parent, # `grand_parent.parent`. assert grand_parent.a == 10 assert not hasattr(grand_parent.parent, "a") assert grand_parent.parent.child.a == 15 # `b` is declared as a field at all three levels. The 'baz' value should be # scoped only to the child, so 'foo' will apply to both the parent and # grand-parent. assert grand_parent.b == "foo" assert grand_parent.parent.b == "foo" assert grand_parent.parent.child.b == "baz" # `c` is declared as a field only in the child. The more specific scopes # override the more general. assert grand_parent.parent.child.c == [0, 4.2]
def test_kwargs_accept_nested_partial_component(ExampleComponentClasses): Parent, Child1, _ = ExampleComponentClasses # This should succeed without error. partial = PartialComponent(Parent, child=PartialComponent(Child1, a=5)) # Generate a component instance from the partial, and configure it. p = partial() configure(p, {}) assert isinstance(p.child, Child1) assert p.child.a == 5
def test_type_check(ExampleComponentClass): """During configuration we should type-check all field values.""" instance = ExampleComponentClass() configure(instance, {"a": 4.5}, name="x") # Attempting to access the field should now raise a type error. with pytest.raises( TypeError, match="Field 'a' of component 'x' is annotated with type '<class 'int'>', which is not satisfied by value 4.5.", ): instance.a
def test_configure_interactive_prompt_missing_field_value(ExampleComponentClass): """When configuring interactively, fields without default or configured values should prompt for value input through the CLI.""" x = ExampleComponentClass() a_value = 42 with patch("click.prompt", return_value=a_value) as prompt: configure(x, {"b": "bar"}, name="FAKE_NAME", interactive=True) assert x.a == a_value assert x.b == "bar" prompt.assert_called_once()
def test_kwargs_accept_component_class(ExampleComponentClasses): Parent, Child1, _ = ExampleComponentClasses # This should succeed without error. partial = PartialComponent(Parent, child=Child1) # Generate a component instance from the partial, and configure it. p = partial() configure(p, {}) assert isinstance(p.child, Child1) assert p.child.b == "foo" assert p.child.a == 10 # This tests that field value inheritence still works.
def test_str_and_repr(): """`__str__` and `__repr__` should give formatted strings that represent nested components nicely.""" @component class Child1: a: int = Field() @component class Child2: a: int = Field() b: str = Field() c: List[float] = Field() d: int = Field(allow_missing=True) child_1: Child1 = ComponentField() @component class Parent: b: str = Field("bar") child_1: Child1 = ComponentField(Child1) child_2: Child2 = ComponentField(Child2) p = Parent() configure( p, {"child_1.a": 5, "child_2.a": 10, "b": "foo", "child_2.c": [1.5, -1.2]}, name="parent", ) assert ( click.unstyle(repr(p)) == """Parent(b="foo", child_1=Child1(a=5), child_2=Child2(a=10, b=<inherited value>, c=[1.5, -1.2], d=<missing>, child_1=<inherited component instance>))""" ) assert ( click.unstyle(str(p)) == """Parent( b="foo", child_1=Child1( a=5 ), child_2=Child2( a=10, b=<inherited value>, c=[1.5, -1.2], d=<missing>, child_1=<inherited component instance> ) )""" )
def test_component_allow_missing_field_inherits_defaults(): @component class Child: a: int = Field(allow_missing=True) @component class Parent: a: int = Field(5) child: Child = ComponentField(Child) # This should succeed without error. instance = Parent() configure(instance, {}) assert instance.child.a == 5
def test_configure_interactive_prompt_for_subcomponent_choice(): """ When configuring interactively, sub-component fields without default or configured values should prompt for a choice of subcomponents to instantiate through the CLI. """ class AbstractChild: pass @component class Child1(AbstractChild): pass @component class Child2(AbstractChild): pass class Child3_Abstract(AbstractChild): pass @component class Child3A(Child3_Abstract): pass @component class Child3B(Child3_Abstract): pass @component class Parent: child: AbstractChild = ComponentField() # The prompt lists the concrete component subclasses (alphabetically) and # asks for an an integer input corresponding to an index in this list. # We expect the list to therefore be as follows (`AbstractChild` and # `Child3_Abstract` are excluded because although they live in the subclass # hierarchy, neither is a component): expected_class_choices = [Child1, Child2, Child3A, Child3B] for i, expected_choice in enumerate(expected_class_choices): p = Parent() with patch("zookeeper.core.utils.prompt", return_value=str(i + 1)) as prompt: configure(p, {}, interactive=True) assert isinstance(p.child, expected_choice) prompt.assert_called_once()
def test_type_check(ExampleComponentClass): """During configuration we should type-check all field values.""" # Attempting to set an int field with a float. with pytest.raises( TypeError, match=r"^Attempting to set field 'x.a' which has annotated type 'int' with value '4.5'.$", ): configure(ExampleComponentClass(), {"a": 4.5}, name="x") # Attempting to set a str field with a bool. with pytest.raises( TypeError, match=r"^Attempting to set field 'x.b' which has annotated type 'str' with value 'True'.$", ): configure(ExampleComponentClass(), {"a": 3, "b": True}, name="x")
def test_component_configure_component_passed_as_config(): @component class Child: x: int = Field() # Inherited from parent @component class Parent: x: int = Field(7) child: Child = ComponentField(Child) instance = Parent() new_child_instance = Child() configure(instance, {"child": new_child_instance}) assert instance.child is new_child_instance assert instance.child.__component_parent__ is instance assert instance.child.x == 7 # This value should be correctly inherited.
def test_configure_scoped_override_field_values(): """Field overriding should respect component scope.""" @component class Child: a: int = Field() b: str = Field() c: List[float] = Field() @component class Parent: b: str = Field("bar") child: Child = ComponentField(Child) @component class GrandParent: a: int = Field() b: str = Field() parent: Parent = ComponentField(Parent) grand_parent = GrandParent() configure( grand_parent, { "a": 10, "parent.child.a": 15, "b": "foo", "parent.child.b": "baz", "parent.child.c": [0, 4.2], }, ) # The grand-parent `grand_parent` should have the value `a` = 10, but the # child `grand_parent.parent.child` should get the value `a` = 15. assert grand_parent.a == 10 assert grand_parent.parent.child.a == 15 # `b` is declared as a field at all three levels. The 'baz' value should be # scoped only to the child, so 'foo' will apply to both the parent and # grand-parent. assert grand_parent.b == "foo" assert grand_parent.parent.b == "foo" assert grand_parent.parent.child.b == "baz" # `c` is declared as a field only in the child. assert grand_parent.parent.child.c == [0, 4.2]
def test_iter(): with pytest.raises( TypeError, match="Component classes must not define a custom `__iter__` method." ): @component class InvalidComponent: def __iter__(): return @component class Child: a: int = Field() b: Tuple[int, float] = Field() c: str = Field(allow_missing=True) @component class Parent: a: int = Field() b: Tuple[int, float] = Field((5, -42.3)) child: Child = ComponentField() # When no configured value is provided for `child.c`, there should be four # values in the iterator: `a` and `b` and `child` on the parent, and `b` on # the child. Notice that the `a` on the child (i.e. `child.a`) is not # included, because that value is inherited from the parent. instance = Parent() configure(instance, {"a": 10, "child.b": (-1, 0.0)}) assert list(iter(instance)) == [ ("a", 10), ("b", (5, -42.3)), ("child", "test_iter.<locals>.Child"), ("child.b", (-1, 0.0)), ] # The only difference from the above is that `child.c` should be included. instance = Parent() configure(instance, {"a": 10, "child.b": (-1, 0.0), "child.c": "foo"}) assert list(iter(instance)) == [ ("a", 10), ("b", (5, -42.3)), ("child", "test_iter.<locals>.Child"), ("child.b", (-1, 0.0)), ("child.c", "foo"), ]
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_configure_breadth_first(): """See https://github.com/larq/zookeeper/issues/226.""" @component class GrandChild: a: int = Field(5) @component class Child: grand_child: GrandChild = ComponentField() @component class Parent: child: Child = ComponentField(Child) grand_child: GrandChild = ComponentField(GrandChild) p = Parent() configure(p, {"grand_child.a": 3}) assert p.grand_child.a == 3 assert p.child.grand_child.a == 3
def test_component_configure_field_allow_missing(): @component class A: a: int = Field() b: float = Field(allow_missing=True) @Field def c(self) -> float: if hasattr(self, "b"): return self.b return self.a # Missing field 'a' should cause an error. with pytest.raises( ValueError, match= "No configuration value found for annotated field 'A.a' of type 'int'.", ): configure(A(), {"b": 3.14}) # But missing field 'b' should not cause an error. instance = A() configure(instance, {"a": 0}) assert instance.c == 0 instance = A() configure(instance, {"a": 0, "b": 3.14}) assert instance.c == 3.14
def test_component_configure_error_non_component_instance(): class A: a: int = Field() with pytest.raises( TypeError, match= "Only @component, @factory, and @task instances can be configured.", ): configure(A(), conf={"a": 5}) @component class B: b: int = Field() with pytest.raises( TypeError, match= "Only @component, @factory, and @task instances can be configured.", ): # The following we expect to fail because it is a component class, not # an instance. configure(B, conf={"b": 3}) class C(B): c: int = Field() with pytest.raises( TypeError, match= "Only @component, @factory, and @task instances can be configured.", ): # Even the an instance of a class that subclasses a component class # should fail. configure(C(), conf={"b": 3, "c": 42})
def test_contains(): with pytest.raises( TypeError, match="Component classes must not define a custom `__contains__` method.", ): @component class InvalidComponent: def __contains__(): return @component class Child: a: int = Field() b: Tuple[int, float] = Field() c: str = Field(allow_missing=True) @component class Parent: a: int = Field() b: Tuple[int, float] = Field((5, -42.3)) child: Child = ComponentField() instance = Parent() configure(instance, {"a": 10, "child.b": (-1, 0.0)}) assert "a" in instance assert "b" in instance assert "child" in instance assert "child.a" in instance assert "child.b" in instance assert "child.c" not in instance instance = Parent() configure(instance, {"a": 10, "child.b": (-1, 0.0), "child.c": "foo"}) assert "a" in instance assert "b" in instance assert "child" in instance assert "child.a" in instance assert "child.b" in instance assert "child.c" in instance
def test_component_pre_configure_setattr_with_component_instance(): @component class Child: a: int = Field(5) @component class Parent: child: Child = ComponentField() instance = Parent() child_instance = Child(a=15) instance.child = child_instance configure(instance, {}) assert instance.child is child_instance # Test reference equality assert instance.child.a == 15 assert instance.child.__component_configured__ new_child_instance = Child() # Trying to set a field value with a component instance should throw. with pytest.raises( ValueError, match=( "Component instances can only be set as values for `ComponentField`s, " "but Child.a is a `Field`." ), ): new_child_instance.a = Child() # Trying with a configured child instance should raise an error. instance = Parent() configure(new_child_instance, {"a": 43}) with pytest.raises( ValueError, match=( "Component instances can only be set as values if they are not yet " "configured." ), ): instance.child = new_child_instance
def test_str_and_repr(): """ `__str__` and `__repr__` should give formatted strings that represent nested components nicely. """ @component class Child: a: int = Field() b: str = Field() c: List[float] = Field() d: int = Field(allow_missing=True) @component class Parent: b: str = Field("bar") child: Child = ComponentField(Child) p = Parent() configure(p, { "child.a": 10, "b": "foo", "child.c": [1.5, -1.2] }, name="parent") assert ( click.unstyle(repr(p)) == """Parent(b="foo", child=Child(a=10, b="foo", c=[1.5, -1.2], d=<missing>))""" ) assert (click.unstyle(str(p)) == """Parent( b="foo", child=Child( a=10, b="foo", c=[1.5, -1.2], d=<missing> ) )""")
def test_configure_automatically_instantiate_subcomponent(): """ If there is only a single component subclass of a field type, an instance of the class should be automatically instantiated during configuration. """ class AbstractChild: pass @component class Child1(AbstractChild): pass @component class Parent: child: AbstractChild = ComponentField() # There is only a single defined component subclass of `AbstractChild`, # `Child1`, so we should be able to configure an instance of `Parent` and # have an instance automatically instantiated in the process. p = Parent() configure(p, {}) assert isinstance(p.child, Child1) @component class Child2(AbstractChild): pass # Now there is another defined component subclass of `AbstractChild`, # so configuration will now fail (as we cannot choose between the two). p = Parent() with pytest.raises( ValueError, match= r"^Component field 'Parent.child' of type 'AbstractChild' has no default or configured class.", ): configure(p, {})