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
45 changes: 45 additions & 0 deletions streamlitfront/elements/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
specific abstract elements class defined in front.
"""

import dataclasses
from dataclasses import dataclass
from inspect import Parameter
from functools import cached_property
from pathlib import Path
from time import sleep
Expand All @@ -26,6 +28,7 @@
OutputBase,
SelectorBase,
TextInputBase,
ValueNotSet,
TextSectionBase,
ELEMENT_KEY,
)
Expand Down Expand Up @@ -195,6 +198,48 @@ def _render_section_content(self):
except ValidationError as e:
st.error(e)

def _render_inputs(self):
"""Render the inputs; a ``*args`` parameter gets a "+" button and one field per press."""
inputs = {}
for child in self.children:
if not isinstance(child, InputBase):
continue
if getattr(child.obj, "kind", None) is Parameter.VAR_POSITIONAL:
inputs[child.obj.name] = self._render_var_positional(child)
else:
if child.is_noneable:
child = self._noneable(child)
inputs[child.obj.name] = child()
return inputs

def _render_var_positional(self, template: InputBase) -> tuple:
"""The values of a ``*args`` parameter: the user asks for more fields with "+", fewer with "-".

Each field is a copy of ``template`` (so ``Iterable[float]``-style annotations give
number fields) under its own key.
"""
name = template.obj.name
count_key = f"{template.input_key}_count"
st.session_state.setdefault(count_key, 0)

def bump(by):
st.session_state[count_key] = max(0, st.session_state[count_key] + by)

st.markdown(name.replace("_", " ").title())
plus, minus, _ = st.columns([1, 1, 4])
plus.button("+", key=f"{count_key}_plus", on_click=bump, args=(1,), help=f"Add another {name}")
minus.button(
"-", key=f"{count_key}_minus", on_click=bump, args=(-1,), disabled=not st.session_state[count_key],
help=f"Remove the last {name}",
)
values = []
for i in range(st.session_state[count_key]):
field = dataclasses.replace(
template, name=f"{name} {i + 1}", input_key=f"{template.input_key}_{i}", value=ValueNotSet
)
values.append(field())
return tuple(values)

def _submit(self, inputs):
super()._submit(inputs)
if not self.auto_submit:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_var_positional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""A ``*args`` parameter gets a "+" button: one more field per press, and the
function is called with what was entered (i2mint/streamlitfront#28)."""

import pytest

pytest.importorskip("streamlit.testing.v1")
from streamlit.testing.v1 import AppTest

APP = """
from streamlitfront import mk_app
def add_nums(*nums_to_add: int):
return sum(nums_to_add)
def tag(prefix: str, *names: str):
return prefix + ",".join(names)
mk_app([add_nums, tag])()
"""


def _plus(at):
return next(b for b in at.button if b.key and b.key.endswith("_plus"))


def _minus(at):
return next(b for b in at.button if b.key and b.key.endswith("_minus"))


def test_plus_adds_a_field_per_press_and_submit_calls_the_function():
at = AppTest.from_string(APP, default_timeout=30).run()
assert not at.exception
assert len(at.number_input) == 0
for _ in range(3):
_plus(at).click().run()
assert len(at.number_input) == 3
for field, v in zip(at.number_input, (1, 2, 3)):
field.set_value(v)
at.run()
_minus(at).click().run()
assert len(at.number_input) == 2
next(b for b in at.button if b.label == "Submit").click().run()
assert not at.exception
assert any(m.value == "`3`" for m in at.markdown)


def test_no_press_means_no_extra_arguments():
at = AppTest.from_string(APP, default_timeout=30).run()
next(b for b in at.button if b.label == "Submit").click().run()
assert not at.exception
Loading