Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions front/elements/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,29 @@ def mk_input_spec(p):
return elements_spec


def _with_section_inputs(kwargs_spec: Mapping, section_inputs: Mapping) -> dict:
"""A copy of the kwargs input spec ``kwargs_spec`` whose nested ``inputs`` are
``section_inputs`` overridden by its own.

>>> _with_section_inputs({'inputs': {'a': {'x': 1}}}, {'a': {'y': 2}, 'b': {}})
{'inputs': {'a': {'y': 2, 'x': 1}, 'b': {}}}
"""
nested_inputs = deep_merge(section_inputs, kwargs_spec.get("inputs") or {})
return {**kwargs_spec, "inputs": nested_inputs}


def _is_spec_of(spec, element_cls) -> bool:
"""Whether ``spec`` is an element spec whose element is ``element_cls`` (or a subclass).

>>> _is_spec_of({ELEMENT_KEY: int}, object), _is_spec_of({ELEMENT_KEY: int}, str)
(True, False)
>>> _is_spec_of('not a spec', object)
False
"""
factory = spec.get(ELEMENT_KEY) if isinstance(spec, Mapping) else None
return isinstance(factory, type) and issubclass(factory, element_cls)


class FrontContainerBase(FrontElementBase):
"""An element with children, each built from a keyword argument holding an element spec.

Expand Down Expand Up @@ -372,7 +395,10 @@ def __init__(
):
self.inputs = inputs
self._feed_kwargs_input_spec()
element_specs = dict(mk_input_element_specs(obj, self.inputs), output=output)
element_specs = self._feed_multi_source_kwargs_input_specs(
mk_input_element_specs(obj, self.inputs)
)
element_specs = dict(element_specs, output=output)
super().__init__(
obj=obj, name=name, display=display, tooltip=tooltip, **element_specs
)
Expand All @@ -389,13 +415,44 @@ def _feed_kwargs_input_spec(self):
inputs_spec = dict(self.inputs)
kwargs_spec = inputs_spec.pop("kwargs", None)
if kwargs_spec:
kwargs_inputs = kwargs_spec.get("inputs", {})
kwargs_inputs = deep_merge(inputs_spec, kwargs_inputs)
self.inputs = {
**self.inputs,
"kwargs": {**kwargs_spec, "inputs": kwargs_inputs},
"kwargs": _with_section_inputs(kwargs_spec, inputs_spec),
}

def _feed_multi_source_kwargs_input_specs(self, element_specs: dict) -> dict:
"""Give a kwargs input that is a source of a multi-source input the section's
input specs as nested ``inputs``, as ``_feed_kwargs_input_spec`` does for the
``kwargs`` input (i2mint/front#45).

Without them, its nested inputs have no ``bound_data_factory`` and no element
for their types, so the app fails as soon as that source is selected. The
multi-source input's own spec (and the ``kwargs`` one) are left out, so that
a nested parameter of the same name does not nest the multi-source input again.
"""

def feed_sources(name, spec):
section_inputs = {
k: v for k, v in self.inputs.items() if k not in (name, "kwargs")
}
return {
k: (
_with_section_inputs(v, section_inputs)
if _is_spec_of(v, KwargsInputBase)
else v
)
for k, v in spec.items()
}

return {
name: (
feed_sources(name, spec)
if _is_spec_of(spec, MultiSourceInputBase)
else spec
)
for name, spec in element_specs.items()
}

def _render_inputs(self):
input_components = [
self._noneable(child) if child.is_noneable else child
Expand Down
138 changes: 138 additions & 0 deletions tests/test_kwargs_input_in_multi_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""A kwargs input can be one of the sources of a multi-source input.

Regression test for i2mint/front#45: such a kwargs input got no nested ``inputs``
(the section only fed the input of the ``kwargs`` parameter), so its nested inputs
had no ``bound_data_factory`` and no element for their types, and the app failed as
soon as that source was rendered.
"""

from dataclasses import dataclass

from i2 import Sig

from front.data_binding import BoundData
from front.elements.elements import (
DEFAULT_INPUT_KEY,
ELEMENT_KEY,
ExecContainerBase,
IntInputBase,
KwargsInputBase,
MultiSourceInputBase,
OutputBase,
TextInputBase,
)


@dataclass
class _Text(TextInputBase):
def render(self):
return self.view_value


@dataclass
class _OtherText(_Text):
pass


@dataclass
class _Int(IntInputBase):
def render(self):
return self.view_value


@dataclass
class _Out(OutputBase):
def render(self):
return self.output


class _Section(ExecContainerBase):
def render(self):
return self._render_inputs()

def _noneable(self, input):
return input


@dataclass
class _Kwargs(KwargsInputBase):
"""Renders its nested inputs the way a front implementation does: as a section."""

def render(self):
section = _Section(
obj=self.get_kwargs, inputs=self.inputs, output={ELEMENT_KEY: _Out}
)
self._nested_section = section
return section()


class _Multi(MultiSourceInputBase):
source = None

def render(self):
source = next(c for c in self.input_components if c.name == self.source)
return source()


def _params(a: int = 1, b: str = "x"):
"""The signature of the kwargs source."""


def f(data):
return data


def _section(state, *, multi_spec=None):
multi_spec = multi_spec or {
ELEMENT_KEY: _Multi,
"From text": {ELEMENT_KEY: _Text},
"From params": {ELEMENT_KEY: _Kwargs, "func_sig": Sig(_params)},
}
inputs = {
"data": multi_spec,
int: {ELEMENT_KEY: _Int},
str: {ELEMENT_KEY: _Text},
DEFAULT_INPUT_KEY: {"bound_data_factory": lambda k: BoundData(k, state)},
}
return _Section(obj=f, inputs=inputs, output={ELEMENT_KEY: _Out})


def _multi_input(section):
return next(c for c in section.children if isinstance(c, MultiSourceInputBase))


def test_kwargs_source_of_a_multi_source_input_renders_its_nested_inputs():
state = {}
multi = _multi_input(_section(state))
multi.source = "From params"
assert multi() == {"a": 1, "b": "x"}

kwargs_input = next(c for c in multi.input_components if c.name == "From params")
nested = {c.obj.name: c for c in kwargs_input._nested_section.children[:-1]}
assert {name: type(c) for name, c in nested.items()} == {"a": _Int, "b": _Text}
# nested keys are scoped to the multi-source input (i2mint/front#40)
assert {c.input_key for c in nested.values()} == {"f_data_a", "f_data_b"}


def test_the_multi_source_spec_is_not_nested_into_its_kwargs_source():
multi = _multi_input(_section({}))
kwargs_input = next(c for c in multi.input_components if c.name == "From params")
assert "data" not in kwargs_input.inputs
assert int in kwargs_input.inputs and DEFAULT_INPUT_KEY in kwargs_input.inputs


def test_own_nested_inputs_of_the_kwargs_source_win():
multi_spec = {
ELEMENT_KEY: _Multi,
"From params": {
ELEMENT_KEY: _Kwargs,
"func_sig": Sig(_params),
"inputs": {"b": {ELEMENT_KEY: _OtherText}},
},
}
multi = _multi_input(_section({}, multi_spec=multi_spec))
multi.source = "From params"
multi()
kwargs_input = multi.input_components[0]
nested = {c.obj.name: c for c in kwargs_input._nested_section.children[:-1]}
assert type(nested["b"]) is _OtherText
Loading