示例#1
0
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
示例#2
0
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]
    )
)""")
示例#3
0
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
示例#4
0
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
示例#5
0
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")
示例#9
0
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
示例#10
0
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")
示例#11
0
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]
示例#12
0
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
示例#13
0
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
示例#14
0
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()
示例#15
0
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.
示例#16
0
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
示例#18
0
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()
示例#19
0
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")
示例#20
0
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.
示例#21
0
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]
示例#22
0
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
示例#24
0
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})
示例#27
0
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
示例#28
0
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, {})