From d603edb192c98a235c095ef22c42d03ae3dff465 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:49:43 +0000 Subject: [PATCH 1/3] feat(FileUploader): empty the uploader after a successful submit (i2mint/front#24) Co-Authored-By: Claude Sonnet 5.5 --- streamlitfront/elements/elements.py | 63 ++++++++++++++++++++ tests/test_file_uploader_clear.py | 89 +++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tests/test_file_uploader_clear.py diff --git a/streamlitfront/elements/elements.py b/streamlitfront/elements/elements.py index 392d310..1e3b07d 100644 --- a/streamlitfront/elements/elements.py +++ b/streamlitfront/elements/elements.py @@ -189,6 +189,37 @@ def _render_section_content(self): self._submit(inputs) except ValidationError as e: st.error(e) + else: + self._render_flash_output() + + @property + def _output_component(self) -> OutputBase: + return next(c for c in self.children if isinstance(c, OutputBase)) + + @property + def _flash_output_key(self) -> str: + return f"{self.name}_flash_output" + + def _submit(self, inputs): + super()._submit(inputs) + # Streamlit can only empty a file uploader by giving it a new key, which takes + # a rerun: keep the output we just rendered for that one rerun. + if not self.auto_submit and self._clear_file_uploaders(): + st.session_state[self._flash_output_key] = self._output_component.output + _rerun() + + def _render_flash_output(self): + if self._flash_output_key in st.session_state: + output_component = self._output_component + output_component.output = st.session_state.pop(self._flash_output_key) + output_component() + + def _clear_file_uploaders(self) -> bool: + """Empty the file uploaders that asked for it; return whether there were any.""" + uploaders = list(_file_uploaders(self.children)) + for uploader in uploaders: + uploader.clear() + return bool(uploaders) def _noneable(self, input_instance: InputBase) -> InputBase: # input_render = input_instance.render @@ -262,16 +293,48 @@ def render(self): class FileUploader(FileUploaderBase): display_label: bool = True + @property + def _generation_key(self) -> str: + return self._build_key(f"{self.name}_upload_generation") + + def clear(self): + """Empty the uploader on the next run, by giving the widget a new key. + + Streamlit keeps an uploader's files across reruns for as long as its key lives + and offers no way to empty it otherwise. + """ + st.session_state[self._generation_key] = ( + st.session_state.get(self._generation_key, 0) + 1 + ) + def render(self): label_visibility = "visible" if self.display_label else "collapsed" + generation = st.session_state.get(self._generation_key, 0) return st.file_uploader( label=self.name, label_visibility=label_visibility, type=self.type, accept_multiple_files=self.accept_multiple_files, + key=f"{self.view_key}_{self.name}_{generation}", ) +def _file_uploaders(components) -> Iterable[FileUploader]: + """The ``FileUploader`` inputs among ``components`` (looking inside multi-source + inputs) that ask to be cleared after a submit.""" + for component in components: + if isinstance(component, MultiSourceInputBase): + yield from _file_uploaders(component.input_components) + elif isinstance(component, FileUploader) and getattr( + component, "clear_after_submit", True + ): + yield component + + +def _rerun(): + (getattr(st, "rerun", None) or st.experimental_rerun)() + + class AudioRecorder(InputBase): def render(self): # # Design move app further up and remove top padding diff --git a/tests/test_file_uploader_clear.py b/tests/test_file_uploader_clear.py new file mode 100644 index 0000000..1fcf1d6 --- /dev/null +++ b/tests/test_file_uploader_clear.py @@ -0,0 +1,89 @@ +"""A file uploader is emptied after a successful submit, so the next upload doesn't start +from the previous selection (i2mint/front#24). Runs the app headlessly with ``AppTest``. +""" + +import pytest + +pytest.importorskip("streamlit.testing.v1") +from streamlit.testing.v1 import AppTest + +APP = """ +from front import APP_KEY, RENDERING_KEY, ELEMENT_KEY +from streamlitfront import mk_app +from streamlitfront.elements import FileUploader, MultiSourceInput, TextOutput + +def count_bytes(data: list) -> int: + if not data: + raise ValueError("no data") + return sum(len(f.getvalue()) for f in data) + +class Files(FileUploader): + def render(self): + return super().render() or [] + +data_spec = {ELEMENT_KEY: Files, "accept_multiple_files": True, **EXTRA} +if MULTI: + data_spec = {ELEMENT_KEY: MultiSourceInput, "From files": data_spec} + +mk_app( + [count_bytes], + config={ + APP_KEY: {"title": "t"}, + RENDERING_KEY: { + "count_bytes": { + "execution": { + "inputs": {"data": data_spec}, + "output": {ELEMENT_KEY: TextOutput}, + } + } + }, + }, +)() +""" + + +def _run_app(*, multi=False, **uploader_spec): + script = APP.replace("EXTRA", repr(uploader_spec)).replace("MULTI", repr(multi)) + at = AppTest.from_string(script, default_timeout=30).run() + assert not at.exception + return at + + +def _names(at): + return [f.name for f in at.file_uploader[0].value] + + +def _output(at): + return [m.value for m in at.markdown if m.value.startswith("`")] + + +@pytest.mark.parametrize("multi", [False, True]) +def test_uploader_is_emptied_after_submit_and_output_is_kept(multi): + at = _run_app(multi=multi) + at.file_uploader[0].upload("a.txt", b"12345").run() + assert _names(at) == ["a.txt"] + at.button[0].click().run() + assert not at.exception + assert _names(at) == [] + assert _output(at) == ["`5`"] + # a new upload starts from a clean selection; the stale output goes away as before + at.file_uploader[0].upload("b.txt", b"67").run() + assert _names(at) == ["b.txt"] + assert _output(at) == [] + + +def test_clear_after_submit_false_keeps_the_selection(): + at = _run_app(clear_after_submit=False) + at.file_uploader[0].upload("a.txt", b"12345").run() + at.button[0].click().run() + assert _names(at) == ["a.txt"] + assert _output(at) == ["`5`"] + + +def test_failed_submit_keeps_the_selection(): + """The function raises on empty data; nothing was submitted, so nothing is cleared.""" + at = _run_app() + at.button[0].click().run() + assert at.exception # ValueError from the function propagates, as before + at.file_uploader[0].upload("a.txt", b"").run() + assert _names(at) == ["a.txt"] From e85b0e1f113e02eab37c109bcf6a15a18f03a050 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:55:04 +0000 Subject: [PATCH 2/3] fix(FileUploader): clear in place instead of rerunning, so the output and anything drawn on submit stay (#front24) Co-Authored-By: Claude Sonnet 5.5 --- streamlitfront/elements/elements.py | 74 +++++++++++++---------------- tests/test_file_uploader_clear.py | 6 +++ 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/streamlitfront/elements/elements.py b/streamlitfront/elements/elements.py index 1e3b07d..1e08e9e 100644 --- a/streamlitfront/elements/elements.py +++ b/streamlitfront/elements/elements.py @@ -189,37 +189,16 @@ def _render_section_content(self): self._submit(inputs) except ValidationError as e: st.error(e) - else: - self._render_flash_output() - - @property - def _output_component(self) -> OutputBase: - return next(c for c in self.children if isinstance(c, OutputBase)) - - @property - def _flash_output_key(self) -> str: - return f"{self.name}_flash_output" def _submit(self, inputs): super()._submit(inputs) - # Streamlit can only empty a file uploader by giving it a new key, which takes - # a rerun: keep the output we just rendered for that one rerun. - if not self.auto_submit and self._clear_file_uploaders(): - st.session_state[self._flash_output_key] = self._output_component.output - _rerun() - - def _render_flash_output(self): - if self._flash_output_key in st.session_state: - output_component = self._output_component - output_component.output = st.session_state.pop(self._flash_output_key) - output_component() - - def _clear_file_uploaders(self) -> bool: - """Empty the file uploaders that asked for it; return whether there were any.""" - uploaders = list(_file_uploaders(self.children)) - for uploader in uploaders: + if not self.auto_submit: + self._clear_file_uploaders() + + def _clear_file_uploaders(self): + """Empty the file uploaders that asked for it.""" + for uploader in _file_uploaders(self.children): uploader.clear() - return bool(uploaders) def _noneable(self, input_instance: InputBase) -> InputBase: # input_render = input_instance.render @@ -298,26 +277,41 @@ def _generation_key(self) -> str: return self._build_key(f"{self.name}_upload_generation") def clear(self): - """Empty the uploader on the next run, by giving the widget a new key. + """Empty the uploader, now and on the following runs. - Streamlit keeps an uploader's files across reruns for as long as its key lives - and offers no way to empty it otherwise. + Streamlit keeps an uploader's files for as long as its widget key lives and + offers no other way to empty it. So: move to a new key, and draw a fresh + uploader in the slot of the old one right away, so that this run's output and + everything else drawn stay in place (no rerun). """ - st.session_state[self._generation_key] = ( - st.session_state.get(self._generation_key, 0) + 1 - ) + self._generation += 1 + slot = getattr(self, "_slot", None) + if slot is not None: + with slot: + self._file_uploader() - def render(self): - label_visibility = "visible" if self.display_label else "collapsed" - generation = st.session_state.get(self._generation_key, 0) + @property + def _generation(self) -> int: + return st.session_state.get(self._generation_key, 0) + + @_generation.setter + def _generation(self, value: int): + st.session_state[self._generation_key] = value + + def _file_uploader(self): return st.file_uploader( label=self.name, - label_visibility=label_visibility, + label_visibility="visible" if self.display_label else "collapsed", type=self.type, accept_multiple_files=self.accept_multiple_files, - key=f"{self.view_key}_{self.name}_{generation}", + key=f"{self.view_key}_{self.name}_{self._generation}", ) + def render(self): + self._slot = st.empty() + with self._slot: + return self._file_uploader() + def _file_uploaders(components) -> Iterable[FileUploader]: """The ``FileUploader`` inputs among ``components`` (looking inside multi-source @@ -331,10 +325,6 @@ def _file_uploaders(components) -> Iterable[FileUploader]: yield component -def _rerun(): - (getattr(st, "rerun", None) or st.experimental_rerun)() - - class AudioRecorder(InputBase): def render(self): # # Design move app further up and remove top padding diff --git a/tests/test_file_uploader_clear.py b/tests/test_file_uploader_clear.py index 1fcf1d6..15b6ef1 100644 --- a/tests/test_file_uploader_clear.py +++ b/tests/test_file_uploader_clear.py @@ -8,6 +8,7 @@ from streamlit.testing.v1 import AppTest APP = """ +import streamlit as st from front import APP_KEY, RENDERING_KEY, ELEMENT_KEY from streamlitfront import mk_app from streamlitfront.elements import FileUploader, MultiSourceInput, TextOutput @@ -15,6 +16,7 @@ def count_bytes(data: list) -> int: if not data: raise ValueError("no data") + st.info("drawn by the function") return sum(len(f.getvalue()) for f in data) class Files(FileUploader): @@ -65,7 +67,11 @@ def test_uploader_is_emptied_after_submit_and_output_is_kept(multi): at.button[0].click().run() assert not at.exception assert _names(at) == [] + assert len(at.file_uploader) == 1 # replaced, not added assert _output(at) == ["`5`"] + assert [i.value for i in at.info] == [ + "drawn by the function" + ] # nothing redrawn away # a new upload starts from a clean selection; the stale output goes away as before at.file_uploader[0].upload("b.txt", b"67").run() assert _names(at) == ["b.txt"] From 1c864804799a208711fe9789e01ba7a10f540d58 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:57:40 +0000 Subject: [PATCH 3/3] fix(FileUploader): only redraw into slots drawn in the current run Co-Authored-By: Claude Sonnet 5.5 --- streamlitfront/elements/elements.py | 12 +-- streamlitfront/examples/crude.py | 16 ++-- streamlitfront/examples/custom_output.py | 30 ++++--- streamlitfront/examples/dag_app.py | 16 ++-- streamlitfront/examples/data_binding.py | 87 +++++++++---------- streamlitfront/examples/data_prep.py | 76 ++++++++-------- streamlitfront/examples/edge_impulse_like.py | 83 ++++++++++-------- streamlitfront/examples/experimentation.py | 10 +-- streamlitfront/examples/handle_none_params.py | 15 ++-- streamlitfront/examples/issues.py | 8 +- streamlitfront/examples/kaggle_front.py | 36 ++++---- streamlitfront/examples/mk_app.py | 19 ++-- streamlitfront/examples/pipeline_maker.py | 24 ++--- streamlitfront/examples/pipeline_maker_2.py | 22 ++--- streamlitfront/examples/pos_key_args.py | 16 ++-- streamlitfront/examples/simple.py | 10 +-- .../examples/simple_real_audio_ml.py | 25 +++--- streamlitfront/examples/store_filtering.py | 19 ++-- streamlitfront/examples/tw_data_binding.py | 12 +-- streamlitfront/examples/util.py | 6 +- .../examples/various_input_kinds.py | 10 +-- streamlitfront/page_funcs.py | 11 +-- streamlitfront/scrap/alt_renderer.py | 19 ++-- .../scrap/front_routing_proposals.py | 20 ++--- streamlitfront/scrap/nested_select.py | 26 +++--- streamlitfront/spec_maker.py | 1 - streamlitfront/tests/common.py | 25 +++--- streamlitfront/tests/conftest.py | 6 +- streamlitfront/tests/dummy_app.py | 12 +-- streamlitfront/tests/fake_app.py | 2 +- streamlitfront/tests/test_base.py | 2 +- streamlitfront/tests/test_pydantic_widgets.py | 1 - streamlitfront/util.py | 3 +- 33 files changed, 358 insertions(+), 322 deletions(-) diff --git a/streamlitfront/elements/elements.py b/streamlitfront/elements/elements.py index 1e08e9e..1c71e6a 100644 --- a/streamlitfront/elements/elements.py +++ b/streamlitfront/elements/elements.py @@ -183,6 +183,9 @@ def render(self): self._render_section_content() def _render_section_content(self): + # Only the uploaders drawn in *this* run have a slot to redraw into. + for uploader in _file_uploaders(self.children): + uploader._slot = None inputs = self._render_inputs() if self.auto_submit or st.button(self.submit_button_label): try: @@ -198,7 +201,8 @@ def _submit(self, inputs): def _clear_file_uploaders(self): """Empty the file uploaders that asked for it.""" for uploader in _file_uploaders(self.children): - uploader.clear() + if getattr(uploader, "clear_after_submit", True): + uploader.clear() def _noneable(self, input_instance: InputBase) -> InputBase: # input_render = input_instance.render @@ -315,13 +319,11 @@ def render(self): def _file_uploaders(components) -> Iterable[FileUploader]: """The ``FileUploader`` inputs among ``components`` (looking inside multi-source - inputs) that ask to be cleared after a submit.""" + inputs).""" for component in components: if isinstance(component, MultiSourceInputBase): yield from _file_uploaders(component.input_components) - elif isinstance(component, FileUploader) and getattr( - component, "clear_after_submit", True - ): + elif isinstance(component, FileUploader): yield component diff --git a/streamlitfront/examples/crude.py b/streamlitfront/examples/crude.py index 34fd95f..f461a44 100644 --- a/streamlitfront/examples/crude.py +++ b/streamlitfront/examples/crude.py @@ -18,7 +18,7 @@ def bar(foo_output: float): return foo_output * 10 -if __name__ == '__main__': +if __name__ == "__main__": # param_to_mall_map = dict(a='a', b='b_store') if not b.mall(): @@ -58,17 +58,19 @@ def bar(foo_output: float): app = mk_app( [foo, bar], config={ - APP_KEY: {'title': 'Crude App'}, + APP_KEY: {"title": "Crude App"}, # OBJ_KEY: {'trans': crudify}, RENDERING_KEY: { Callable: { - 'execution': { - 'inputs': { - 'save_name': { + "execution": { + "inputs": { + "save_name": { ELEMENT_KEY: TextInput, - NAME_KEY: 'Save output as', + NAME_KEY: "Save output as", + }, + str: { + ELEMENT_KEY: SelectBox, }, - str: {ELEMENT_KEY: SelectBox,}, } }, # 'code': { diff --git a/streamlitfront/examples/custom_output.py b/streamlitfront/examples/custom_output.py index a503415..4ec8365 100644 --- a/streamlitfront/examples/custom_output.py +++ b/streamlitfront/examples/custom_output.py @@ -19,9 +19,9 @@ class HitTheOutputValue(OutputBase): margin: int = 20 def render(self): - template = f'{self.name}: {self.output}. {{0}}' + template = f"{self.name}: {self.output}. {{0}}" if self.output == self.value_to_hit: - st.success(template.format('You hit it! Congratulations!')) + st.success(template.format("You hit it! Congratulations!")) st.balloons() elif abs(self.output - self.value_to_hit) <= self.margin: st.warning(template.format("You're warm...")) @@ -30,28 +30,30 @@ def render(self): st.snow() -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( [foo], config={ - APP_KEY: {'title': 'My Custom Output'}, + APP_KEY: {"title": "My Custom Output"}, RENDERING_KEY: { - 'foo': { - NAME_KEY: 'Hit the value', - 'description': { - 'content': ''' + "foo": { + NAME_KEY: "Hit the value", + "description": { + "content": """ Try to hit the secret output value by playing with the inputs. \n Hint: It is the answer to the ultimate question of life, the universe, and everything. - ''', + """, }, - 'execution': { - 'output': { + "execution": { + "output": { ELEMENT_KEY: HitTheOutputValue, - 'value_to_hit': 42, - 'margin': 10, + "value_to_hit": 42, + "margin": 10, }, }, - 'code': {ELEMENT_KEY: SourceCodeSection,}, + "code": { + ELEMENT_KEY: SourceCodeSection, + }, } }, }, diff --git a/streamlitfront/examples/dag_app.py b/streamlitfront/examples/dag_app.py index de352d3..d932b62 100644 --- a/streamlitfront/examples/dag_app.py +++ b/streamlitfront/examples/dag_app.py @@ -6,11 +6,11 @@ def b(a: int): - return 2 ** a + return 2**a def d(c: int): - return 10 - (5 ** c) + return 10 - (5**c) def result(b, d): @@ -19,13 +19,13 @@ def result(b, d): dag = DAG((b, d, result)) -if __name__ == '__main__': +if __name__ == "__main__": # Have special element to indicate removal of field in recursive mapping merge app = mk_app( [dag], config={ - APP_KEY: {'title': 'DAG App'}, + APP_KEY: {"title": "DAG App"}, RENDERING_KEY: { DAG: { # Here, we add a new home made element. Note that this "Graph" element @@ -34,12 +34,12 @@ def result(b, d): # along with its parameters. # You can do exactly the same at every level of the tree by defining # your own elements and include them at the right place in the configuration. - 'graph': { + "graph": { ELEMENT_KEY: Graph, - NAME_KEY: 'Flow', - 'use_container_width': True, + NAME_KEY: "Flow", + "use_container_width": True, }, - 'description': {'content': ''}, + "description": {"content": ""}, } }, }, diff --git a/streamlitfront/examples/data_binding.py b/streamlitfront/examples/data_binding.py index b406340..e20951a 100644 --- a/streamlitfront/examples/data_binding.py +++ b/streamlitfront/examples/data_binding.py @@ -29,26 +29,25 @@ from streamlitfront import mk_app, binder as b from streamlitfront.elements import SelectBox - data = { - 'Pink Floyd': { - 'David Gilmour': 'Guitar', - 'Roger Waters': 'Bass Guitar', - 'Syd Barrett': 'Guitar', - 'Richard Wright': 'Keyboards', - 'Nick Mason': 'Drums', + "Pink Floyd": { + "David Gilmour": "Guitar", + "Roger Waters": "Bass Guitar", + "Syd Barrett": "Guitar", + "Richard Wright": "Keyboards", + "Nick Mason": "Drums", }, - 'The Doors': { - 'Jim Morrison': 'Lead vocalist', - 'Ray Manzarek': 'Keyboards', - 'Robby Krieger': 'Guitar', - 'John Densmore': 'Drums', + "The Doors": { + "Jim Morrison": "Lead vocalist", + "Ray Manzarek": "Keyboards", + "Robby Krieger": "Guitar", + "John Densmore": "Drums", }, - 'The Beatles': { - 'John Lennon': 'Guitar', - 'Paul McCartney': 'Bass Guitar', - 'George Harrison': 'Guitar', - 'Ringo Starr': 'Drums', + "The Beatles": { + "John Lennon": "Guitar", + "Paul McCartney": "Bass Guitar", + "George Harrison": "Guitar", + "Ringo Starr": "Drums", }, } @@ -69,7 +68,7 @@ def get_instrument_from_member_only(member: str): def get_members_from_instrument(instrument: str): return [ - f'{m} ({band})' + f"{m} ({band})" for band, members in data.items() for m, i in members.items() if i == instrument @@ -98,7 +97,7 @@ def init_selected(): on_select_band(band) -if __name__ == '__main__': +if __name__ == "__main__": init_selected() app = mk_app( @@ -108,42 +107,42 @@ def init_selected(): get_members_from_instrument, ], config={ - APP_KEY: {'title': 'Data Binding'}, + APP_KEY: {"title": "Data Binding"}, # OBJ_KEY: {'trans': crudify}, RENDERING_KEY: { - 'get_instrument_from_band_and_member': { - NAME_KEY: 'From an input to another', - 'description': { - 'content': 'Select a band and see how the list of members is updated.' + "get_instrument_from_band_and_member": { + NAME_KEY: "From an input to another", + "description": { + "content": "Select a band and see how the list of members is updated." }, - 'execution': {'on_submit': set_output_instrument}, + "execution": {"on_submit": set_output_instrument}, }, - 'get_instrument_from_member_only': { - NAME_KEY: 'From a screen to another', - 'description': { - 'content': 'Select a band from the previous page ("From an input to another") and see how the list of members (of this page) is updated.' + "get_instrument_from_member_only": { + NAME_KEY: "From a screen to another", + "description": { + "content": 'Select a band from the previous page ("From an input to another") and see how the list of members (of this page) is updated.' }, - 'execution': {'on_submit': set_output_instrument}, + "execution": {"on_submit": set_output_instrument}, }, - 'get_members_from_instrument': { - NAME_KEY: 'From an output to an input', - 'description': { - 'content': 'Submit any other screen and see that the output is automatically selected from the "Instrument" select box.' + "get_members_from_instrument": { + NAME_KEY: "From an output to an input", + "description": { + "content": 'Submit any other screen and see that the output is automatically selected from the "Instrument" select box.' }, }, Callable: { - 'execution': { - 'inputs': { + "execution": { + "inputs": { str: {ELEMENT_KEY: SelectBox}, - 'band': { - 'options': list(data), - 'value': b.selected_band, - 'on_value_change': on_select_band, + "band": { + "options": list(data), + "value": b.selected_band, + "on_value_change": on_select_band, }, - 'member': {'options': b.members_of_selected_band}, - 'instrument': { - 'options': instruments, - 'value': b.output_instrument, + "member": {"options": b.members_of_selected_band}, + "instrument": { + "options": instruments, + "value": b.output_instrument, }, } } diff --git a/streamlitfront/examples/data_prep.py b/streamlitfront/examples/data_prep.py index 1aed74b..f25e685 100644 --- a/streamlitfront/examples/data_prep.py +++ b/streamlitfront/examples/data_prep.py @@ -36,23 +36,23 @@ ) mall = b.mall() if not b.selected_step_factory(): - b.selected_step_factory = 'files_of_folder' + b.selected_step_factory = "files_of_folder" crudifier = partial(Crudifier, mall=mall) -@crudifier(param_to_mall_map=dict(step_factory='step_factories'), output_store='steps') +@crudifier(param_to_mall_map=dict(step_factory="step_factories"), output_store="steps") def mk_step(step_factory: Callable, kwargs: dict): return partial(step_factory, **kwargs) -@crudifier(output_store='pipelines') +@crudifier(output_store="pipelines") def mk_pipeline(steps: Iterable[Callable]): return Pipe(*steps) @crudifier( - param_to_mall_map=dict(pipeline='pipelines'), + param_to_mall_map=dict(pipeline="pipelines"), # output_store='exec_outputs' ) def exec_pipeline(pipeline: Callable, kwargs): @@ -60,68 +60,68 @@ def exec_pipeline(pipeline: Callable, kwargs): def get_step_name(step): - return [k for k, v in mall['steps'].items() if v == step][0] + return [k for k, v in mall["steps"].items() if v == step][0] def get_selected_pipeline_sig(): if not b.selected_pipeline(): return Sig() - return Sig(mall['pipelines'][b.selected_pipeline()]) + return Sig(mall["pipelines"][b.selected_pipeline()]) config = { - APP_KEY: {'title': 'Data Preparation'}, + APP_KEY: {"title": "Data Preparation"}, RENDERING_KEY: { - 'mk_step': { - NAME_KEY: 'Pipeline Step Maker', - 'execution': { - 'inputs': { - 'step_factory': { + "mk_step": { + NAME_KEY: "Pipeline Step Maker", + "execution": { + "inputs": { + "step_factory": { ELEMENT_KEY: SelectBox, - 'options': mall['step_factories'], - 'value': b.selected_step_factory, + "options": mall["step_factories"], + "value": b.selected_step_factory, }, - 'kwargs': { + "kwargs": { ELEMENT_KEY: KwargsInput, - 'func_sig': Sig( - mall['step_factories'][b.selected_step_factory()] + "func_sig": Sig( + mall["step_factories"][b.selected_step_factory()] ), }, }, - 'output': { + "output": { ELEMENT_KEY: SuccessNotification, - 'message': 'The step has been created successfully.', + "message": "The step has been created successfully.", }, }, }, - 'mk_pipeline': { - NAME_KEY: 'Pipeline Maker', - 'execution': { - 'inputs': { - 'steps': { + "mk_pipeline": { + NAME_KEY: "Pipeline Maker", + "execution": { + "inputs": { + "steps": { ELEMENT_KEY: PipelineMaker, - 'items': list(mall['steps'].values()), - 'serializer': get_step_name, + "items": list(mall["steps"].values()), + "serializer": get_step_name, }, }, - 'output': { + "output": { ELEMENT_KEY: SuccessNotification, - 'message': 'The pipeline has been created successfully.', + "message": "The pipeline has been created successfully.", }, }, }, - 'exec_pipeline': { - NAME_KEY: 'Pipeline Executor', - 'execution': { - 'inputs': { - 'pipeline': { + "exec_pipeline": { + NAME_KEY: "Pipeline Executor", + "execution": { + "inputs": { + "pipeline": { ELEMENT_KEY: SelectBox, - 'options': mall['pipelines'], - 'value': b.selected_pipeline, + "options": mall["pipelines"], + "value": b.selected_pipeline, }, - 'kwargs': { + "kwargs": { ELEMENT_KEY: KwargsInput, - 'func_sig': get_selected_pipeline_sig(), + "func_sig": get_selected_pipeline_sig(), }, } }, @@ -129,7 +129,7 @@ def get_selected_pipeline_sig(): }, } -if __name__ == '__main__': +if __name__ == "__main__": funcs = [mk_step, mk_pipeline, exec_pipeline] app = mk_app(funcs, config=config) app() diff --git a/streamlitfront/examples/edge_impulse_like.py b/streamlitfront/examples/edge_impulse_like.py index dd0941b..48b0c3f 100644 --- a/streamlitfront/examples/edge_impulse_like.py +++ b/streamlitfront/examples/edge_impulse_like.py @@ -42,21 +42,24 @@ # tagged_wf_store = appendable(Files, item2kv=tagged_timestamped_kv) if not b.mall(): - b.mall = dict(tagged_wf=dict(), dummy_store=dict(),) + b.mall = dict( + tagged_wf=dict(), + dummy_store=dict(), + ) mall = b.mall() crudifier = partial(Crudifier, mall=mall) def auto_namer(*, arguments): - return '_'.join(map(str, arguments.values())) + return "_".join(map(str, arguments.values())) -@crudifier(output_store='tagged_wf', auto_namer=auto_namer) +@crudifier(output_store="tagged_wf", auto_namer=auto_namer) def tag_wf(wf: WaveForm, tag: str): return (wf, tag) -@crudifier(param_to_mall_map=dict(x='tagged_wf')) +@crudifier(param_to_mall_map=dict(x="tagged_wf")) def get_tagged_wf(x: Any): return x @@ -107,66 +110,76 @@ class TaggedAudioPlayer(OutputBase): def render(self): if self.output: sound, tag = self.output - arr = sf.read(BytesIO(sound), dtype='int16')[0] - tab1, tab2 = st.tabs(['Audio Player', 'Waveform']) + arr = sf.read(BytesIO(sound), dtype="int16")[0] + tab1, tab2 = st.tabs(["Audio Player", "Waveform"]) with tab1: st.audio(sound) with tab2: fig, ax = plt.subplots(figsize=(15, 5)) - ax.plot(arr, label=f'Tag={tag}') + ax.plot(arr, label=f"Tag={tag}") ax.legend() st.pyplot(fig) # st.write(arr[:10]) -get_data_description = ''' +get_data_description = """ Feed the system with wave forms from wav files or directly \ from your microphone, and tag them. -''' +""" config_ = { - APP_KEY: {'title': 'Edge-Impulse-like App'}, + APP_KEY: {"title": "Edge-Impulse-like App"}, RENDERING_KEY: { - 'tag_wf': { - NAME_KEY: 'Get Data', - 'description': {'content': get_data_description}, - 'execution': { - 'inputs': { - 'wf': { + "tag_wf": { + NAME_KEY: "Get Data", + "description": {"content": get_data_description}, + "execution": { + "inputs": { + "wf": { ELEMENT_KEY: MultiSourceInput, - NAME_KEY: 'Wave Form', - 'From a file': { + NAME_KEY: "Wave Form", + "From a file": { ELEMENT_KEY: FileReader, - 'type': 'wav', - 'display_label': False, + "type": "wav", + "display_label": False, }, - 'From the microphone': { + "From the microphone": { ELEMENT_KEY: AudioRecorder, # "save_dir": rootdir, }, } }, - 'output': { + "output": { ELEMENT_KEY: SuccessNotification, - 'message': 'The wave form has been tagged successfully.', + "message": "The wave form has been tagged successfully.", }, }, }, - 'get_tagged_wf': { - NAME_KEY: 'Data Explorer', - 'description': { - 'content': '''Explore the existing tagged wave forms and play them.''' + "get_tagged_wf": { + NAME_KEY: "Data Explorer", + "description": { + "content": """Explore the existing tagged wave forms and play them.""" }, - 'execution': { - 'output': {ELEMENT_KEY: TaggedAudioPlayer,}, - 'auto_submit': True, + "execution": { + "output": { + ELEMENT_KEY: TaggedAudioPlayer, + }, + "auto_submit": True, }, }, - DAG: {'graph': {ELEMENT_KEY: Graph, NAME_KEY: 'Flow',}}, + DAG: { + "graph": { + ELEMENT_KEY: Graph, + NAME_KEY: "Flow", + } + }, Callable: { - 'execution': { - 'inputs': { - 'save_name': {ELEMENT_KEY: TextInput, NAME_KEY: 'Save output as',} + "execution": { + "inputs": { + "save_name": { + ELEMENT_KEY: TextInput, + NAME_KEY: "Save output as", + } } } }, @@ -174,6 +187,6 @@ def render(self): } # ============ END FRONTEND ============ -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app(funcs, config=config_) app() diff --git a/streamlitfront/examples/experimentation.py b/streamlitfront/examples/experimentation.py index d306018..a3b5001 100644 --- a/streamlitfront/examples/experimentation.py +++ b/streamlitfront/examples/experimentation.py @@ -8,20 +8,20 @@ def foo(a: int = 0, b: int = 0, c=0): return (a * b) + c -def bar(x, greeting='hello'): +def bar(x, greeting="hello"): """bar greets its input""" - return f'{greeting} {x}' + return f"{greeting} {x}" def confuser(a: int = 0, x: float = 3.14): - return (a ** 2) * x + return (a**2) * x funcs = [foo, bar, confuser] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") app = dispatch_funcs(funcs) app() diff --git a/streamlitfront/examples/handle_none_params.py b/streamlitfront/examples/handle_none_params.py index f9362c2..500a1c7 100644 --- a/streamlitfront/examples/handle_none_params.py +++ b/streamlitfront/examples/handle_none_params.py @@ -15,18 +15,23 @@ def foo(a: int = 1, b: int | None = 2, c: int | None = None, d: str = None): """This is foo. It computes something""" b = b if b is not None else 1 c = c if c is not None else 0 - d = d or '' + d = d or "" return str((a * b) + c) + d -def bar(x: str = 'World!', greeting: str = 'Hello'): +def bar(x: str = "World!", greeting: str = "Hello"): """bar greets its input""" - return f'{greeting} {x}' + return f"{greeting} {x}" # ====== -if __name__ == '__main__': - app = mk_app([foo, bar], config={APP_KEY: {'title': 'Handle None Parameters'},},) +if __name__ == "__main__": + app = mk_app( + [foo, bar], + config={ + APP_KEY: {"title": "Handle None Parameters"}, + }, + ) app() diff --git a/streamlitfront/examples/issues.py b/streamlitfront/examples/issues.py index 050d3c1..13f4218 100644 --- a/streamlitfront/examples/issues.py +++ b/streamlitfront/examples/issues.py @@ -24,8 +24,8 @@ def increment(self): def display_selectbox(store): - _keys = filt_iter(store, filt=lambda k: k.endswith('.csv')) - st.selectbox('Choose a file', list(_keys)) + _keys = filt_iter(store, filt=lambda k: k.endswith(".csv")) + st.selectbox("Choose a file", list(_keys)) def dacc(zip_path: type(lambda a: a), file: str): # file: list = []): @@ -41,10 +41,10 @@ def foo(state: type(get_state()), word: str): funcs = [my_incrementer, foo] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") app = dispatch_funcs(funcs) diff --git a/streamlitfront/examples/kaggle_front.py b/streamlitfront/examples/kaggle_front.py index 5497e2a..6542a4c 100644 --- a/streamlitfront/examples/kaggle_front.py +++ b/streamlitfront/examples/kaggle_front.py @@ -17,19 +17,19 @@ # TODO: Resolve the UnhashableTypeError: Cannot hash object of type _thread.Rlock, found in something. -DFLT_FEATURIZER = {'supervised': LdaChkToFv, 'unsupervised': PcaChkToFv} +DFLT_FEATURIZER = {"supervised": LdaChkToFv, "unsupervised": PcaChkToFv} def search_kaggle( - keyword_search: str = '', num_to_display: int = 10, metadata: bool = False + keyword_search: str = "", num_to_display: int = 10, metadata: bool = False ): s = KaggleDatasets() results = s.search(keyword_search) if metadata: df = pd.DataFrame(results.values())[ - ['ref', 'title', 'subtitle', 'downloadCount', 'totalBytes'] + ["ref", "title", "subtitle", "downloadCount", "totalBytes"] ] - df = df.set_index('ref').sort_values('downloadCount', ascending=False) + df = df.set_index("ref").sort_values("downloadCount", ascending=False) st.write(df.head(num_to_display)) else: st.write(list(results)[:10]) @@ -51,30 +51,34 @@ def create_kaggle_dacc( zip_path: type(lambda a: a), annots_path: str, wrangle_func: type(lambda a: a), - extension: str = '.wav', - key_path: str = '', + extension: str = ".wav", + key_path: str = "", ): wrangle_func = dill.load(wrangle_func) return mk_dacc( - zip_path, annots_path, wrangle_func, extension=extension, key_path=key_path, + zip_path, + annots_path, + wrangle_func, + extension=extension, + key_path=key_path, ) def build_and_run_model(tag: str, state: type(get_state())): - dacc = state['Create Kaggle Dacc'] + dacc = state["Create Kaggle Dacc"] chks, tags = zip(*dacc.chk_tag_gen(tag)) scores, fvs, featurizer, model = produce_results(chks, tags) return str( - 'Percentage correct: ' + "Percentage correct: " + str(sum(scores == tags) / len(tags)) - + ' vs. Random guessing: ' + + " vs. Random guessing: " + str(1 / len(set(tags))) ) def get_featurizer(chks, tags=None, **kwargs): - proj_type = 'unsupervised' if (tags is None) else 'unsupervised' - featurizer = mk_callable('transform')(DFLT_FEATURIZER[proj_type])(**kwargs) + proj_type = "unsupervised" if (tags is None) else "unsupervised" + featurizer = mk_callable("transform")(DFLT_FEATURIZER[proj_type])(**kwargs) featurizer.fit(chks, tags) return featurizer @@ -87,7 +91,7 @@ def get_outlier_model(fvs, **kwargs): def get_classification_model(fvs, tags, **kwargs): - CS = mk_callable('predict')(CentroidSmoothing) + CS = mk_callable("predict")(CentroidSmoothing) model = CentroidSmoothing(**kwargs) return model.fit(fvs, tags) @@ -122,12 +126,12 @@ def produce_results( build_and_run_model, ] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs from streamlitfront.page_funcs import StatePageFunc - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") - app = dispatch_funcs(funcs, configs={'page_factory': StatePageFunc}) + app = dispatch_funcs(funcs, configs={"page_factory": StatePageFunc}) app() diff --git a/streamlitfront/examples/mk_app.py b/streamlitfront/examples/mk_app.py index 156de38..0c5c8e9 100644 --- a/streamlitfront/examples/mk_app.py +++ b/streamlitfront/examples/mk_app.py @@ -4,6 +4,7 @@ from streamlitfront.base import mk_app from streamlitfront.elements.elements import TextInput + # BACKEND def foo(a: int = 1, b: int = 2, c=3): """This is foo. It computes something""" @@ -12,11 +13,11 @@ def foo(a: int = 1, b: int = 2, c=3): def bar(x, greeting): """bar greets its input""" - return f'{greeting} {x}' + return f"{greeting} {x}" def confuser(a: int, x: float = 3.14): - return (a ** 2) * x + return (a**2) * x def proportion(x: int = 100, p: float = 0.5): @@ -25,18 +26,24 @@ def proportion(x: int = 100, p: float = 0.5): # ====== -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( [foo, bar, confuser, proportion], config={ - APP_KEY: {'title': 'My app'}, + APP_KEY: {"title": "My app"}, RENDERING_KEY: { # 'foo': { # 'execution': {'inputs': {'a': {ELEMENT_KEY: TextInput,}},} # }, - 'proportion': { - 'execution': {'inputs': {'p': {ELEMENT_KEY: FloatSliderInput,}},} + "proportion": { + "execution": { + "inputs": { + "p": { + ELEMENT_KEY: FloatSliderInput, + } + }, + } } }, }, diff --git a/streamlitfront/examples/pipeline_maker.py b/streamlitfront/examples/pipeline_maker.py index b364913..77d2112 100644 --- a/streamlitfront/examples/pipeline_maker.py +++ b/streamlitfront/examples/pipeline_maker.py @@ -32,32 +32,32 @@ def __post_init__(self): self.item_template = f.read() def render(self): - st_multiselect = mk_element_factory('st_multiselect') + st_multiselect = mk_element_factory("st_multiselect") value = st_multiselect(options=self.options, item_template=self.item_template) return value -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( [execute_pipe], config={ - APP_KEY: {'title': 'Pipeline Maker'}, + APP_KEY: {"title": "Pipeline Maker"}, RENDERING_KEY: { Callable: { - 'execution': { - 'inputs': { - 'pipe': { + "execution": { + "inputs": { + "pipe": { ELEMENT_KEY: MultiSelect, - 'options': [ - {'value': 'blue', 'name': 'Blue'}, - {'value': 'white', 'name': 'White'}, - {'value': 'red', 'name': 'Red'}, + "options": [ + {"value": "blue", "name": "Blue"}, + {"value": "white", "name": "White"}, + {"value": "red", "name": "Red"}, ], - 'item_template': ''' + "item_template": """
{name} 😀
- ''', + """, } } } diff --git a/streamlitfront/examples/pipeline_maker_2.py b/streamlitfront/examples/pipeline_maker_2.py index 86c02ca..2956781 100644 --- a/streamlitfront/examples/pipeline_maker_2.py +++ b/streamlitfront/examples/pipeline_maker_2.py @@ -12,27 +12,27 @@ def execute_pipe(pipe: Iterable): return -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( [execute_pipe], config={ - APP_KEY: {'title': 'Pipeline Maker'}, + APP_KEY: {"title": "Pipeline Maker"}, RENDERING_KEY: { Callable: { - 'execution': { - 'inputs': { - 'pipe': { + "execution": { + "inputs": { + "pipe": { ELEMENT_KEY: MultiSelect, - 'options': [ - {'value': 'blue', 'name': 'Blue'}, - {'value': 'white', 'name': 'White'}, - {'value': 'red', 'name': 'Red'}, + "options": [ + {"value": "blue", "name": "Blue"}, + {"value": "white", "name": "White"}, + {"value": "red", "name": "Red"}, ], - 'item_template': ''' + "item_template": """
{name} 😀
- ''', + """, } } } diff --git a/streamlitfront/examples/pos_key_args.py b/streamlitfront/examples/pos_key_args.py index b8afeac..524b765 100644 --- a/streamlitfront/examples/pos_key_args.py +++ b/streamlitfront/examples/pos_key_args.py @@ -13,29 +13,29 @@ def add_floats(*nums_to_add: Iterable[float]): def add_strs(*strs_to_add: Iterable[str]): - return ' '.join(strs_to_add) + return " ".join(strs_to_add) def add_bools(*bools_to_add: Iterable[bool]): return sum(list(map(int, bools_to_add))) -def make_a_table_of_ints(col_name: str = 'integers', **keys_and_values: dict[str, int]): +def make_a_table_of_ints(col_name: str = "integers", **keys_and_values: dict[str, int]): return pd.Series(data=keys_and_values, name=col_name) def make_a_table_of_floats( - col_name: str = 'floats', **keys_and_values: dict[str, float] + col_name: str = "floats", **keys_and_values: dict[str, float] ): return pd.Series(data=keys_and_values, name=col_name) -def make_a_table_of_strs(col_name: str = 'strings', **keys_and_values: dict[str, str]): +def make_a_table_of_strs(col_name: str = "strings", **keys_and_values: dict[str, str]): return pd.Series(data=keys_and_values, name=col_name) def make_a_table_of_bools( - col_name: str = 'booleans', **keys_and_values: dict[str, bool] + col_name: str = "booleans", **keys_and_values: dict[str, bool] ): return pd.Series(data=keys_and_values, name=col_name) @@ -51,12 +51,12 @@ def make_a_table_of_bools( make_a_table_of_bools, ] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs from streamlitfront.page_funcs import ArgsPageFunc - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") - app = dispatch_funcs(funcs, configs={'page_factory': ArgsPageFunc}) + app = dispatch_funcs(funcs, configs={"page_factory": ArgsPageFunc}) app() diff --git a/streamlitfront/examples/simple.py b/streamlitfront/examples/simple.py index d306018..a3b5001 100644 --- a/streamlitfront/examples/simple.py +++ b/streamlitfront/examples/simple.py @@ -8,20 +8,20 @@ def foo(a: int = 0, b: int = 0, c=0): return (a * b) + c -def bar(x, greeting='hello'): +def bar(x, greeting="hello"): """bar greets its input""" - return f'{greeting} {x}' + return f"{greeting} {x}" def confuser(a: int = 0, x: float = 3.14): - return (a ** 2) * x + return (a**2) * x funcs = [foo, bar, confuser] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") app = dispatch_funcs(funcs) app() diff --git a/streamlitfront/examples/simple_real_audio_ml.py b/streamlitfront/examples/simple_real_audio_ml.py index 0605891..10a927c 100644 --- a/streamlitfront/examples/simple_real_audio_ml.py +++ b/streamlitfront/examples/simple_real_audio_ml.py @@ -10,7 +10,6 @@ MultiSourceInput, ) - WaveForm = Iterable[int] @@ -20,24 +19,30 @@ def learn_model(train_audio: WaveForm, raw_audio_learner): config_ = { - APP_KEY: {'title': 'Simple Real Audio ML'}, + APP_KEY: {"title": "Simple Real Audio ML"}, RENDERING_KEY: { DAG: { - 'description': {'content': 'A very simple learn model example.'}, - 'execution': { - 'inputs': { - 'train_audio': { + "description": {"content": "A very simple learn model example."}, + "execution": { + "inputs": { + "train_audio": { ELEMENT_KEY: MultiSourceInput, - 'From a file': {ELEMENT_KEY: FileUploader, 'type': 'wav',}, - 'From the microphone': {ELEMENT_KEY: AudioRecorder}, + "From a file": { + ELEMENT_KEY: FileUploader, + "type": "wav", + }, + "From the microphone": {ELEMENT_KEY: AudioRecorder}, } }, }, - 'graph': {ELEMENT_KEY: Graph, NAME_KEY: 'Flow',}, + "graph": { + ELEMENT_KEY: Graph, + NAME_KEY: "Flow", + }, } }, } -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app([learn_model], config=config_) app() diff --git a/streamlitfront/examples/store_filtering.py b/streamlitfront/examples/store_filtering.py index b1fc2e0..d35466b 100644 --- a/streamlitfront/examples/store_filtering.py +++ b/streamlitfront/examples/store_filtering.py @@ -10,7 +10,6 @@ from streamlitfront.base import mk_app from streamlitfront.elements import TextOutput - # TODO: Being able to use Pipe to do pre and post processing in front elements # TODO: have front (output) elements for iterables (example: paged iteration) # TODO: Currying functionality in front @@ -21,11 +20,11 @@ def filter_store(store: str, filt: str, flags: int = 0): store_url = store store = grab(store_url) else: - raise TypeError(f'store can only be str at this point: {store}') + raise TypeError(f"store can only be str at this point: {store}") if isinstance(filt, str): filt = re.compile(filt, flags).search else: - raise TypeError(f'filt can only be str at this point: {filt}') + raise TypeError(f"filt can only be str at this point: {filt}") return filt_iter(store, filt=filt) @@ -44,29 +43,29 @@ def list_keys(store_, max_keys: int = 100): from itertools import islice n = len(store_) - first_keys = '\n\r'.join(islice(store_, 0, max_keys)) + first_keys = "\n\r".join(islice(store_, 0, max_keys)) import pandas as pd return pd.DataFrame(store_) - return f'----- {n} keys. Below, the first {min(n, max_keys)} ----- \n\r{first_keys}' + return f"----- {n} keys. Below, the first {min(n, max_keys)} ----- \n\r{first_keys}" from lined import LineParametrized f = wrap(filter_store, egress=list_keys) -f.__name__ = 'filter_store' +f.__name__ = "filter_store" -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( # [filter_store], [wrap(filter_store, egress=list_keys)], # [LineParametrized(filter_store, list_keys)], # [postprocess(list)(filter_store)], config={ - APP_KEY: {'title': 'My app'}, + APP_KEY: {"title": "My app"}, RENDERING_KEY: { - 'filter_store': { - 'execution': {'output': TextOutput}, + "filter_store": { + "execution": {"output": TextOutput}, # 'execution': {'output': Pipe(list, TextOutput)}, }, }, diff --git a/streamlitfront/examples/tw_data_binding.py b/streamlitfront/examples/tw_data_binding.py index 3643ce6..b09db42 100644 --- a/streamlitfront/examples/tw_data_binding.py +++ b/streamlitfront/examples/tw_data_binding.py @@ -6,7 +6,7 @@ def foo(text: str, integer: int, floating: float = 1.0): return text, integer, floating -def bar(a: str = 'boo'): +def bar(a: str = "boo"): """About bar""" return a @@ -27,17 +27,17 @@ def pages_app(funcs, configs): state = get_state(hash_funcs=dflt_hash_funcs) # TODO: get from configs # Page setup - st.set_page_config(layout='wide') + st.set_page_config(layout="wide") # Make page objects views = get_pages_specs(funcs, **configs) - state['views'] = views + state["views"] = views # TODO: The above is static: Should the above be done only once, and cached? # Perhaps views should be cached in state? # Setup navigation - view_key = _get_view_key(tuple(views.keys()), label='Select your view') + view_key = _get_view_key(tuple(views.keys()), label="Select your view") # Display the selected page with the session state # This is the part that actually runs the functionality that pages specifies @@ -48,11 +48,11 @@ def pages_app(funcs, configs): state.sync() -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs from streamlitfront.page_funcs import ExperimentalViewFunc - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") app = dispatch_funcs(funcs, configs=dict(page_factory=ExperimentalViewFunc)) diff --git a/streamlitfront/examples/util.py b/streamlitfront/examples/util.py index 45c14e9..80c9931 100644 --- a/streamlitfront/examples/util.py +++ b/streamlitfront/examples/util.py @@ -27,8 +27,8 @@ def get_code_of_current_file(): SourceCodeSection = partial( TextSection, - name='Source Code', - kind='code', - language='python', + name="Source Code", + kind="code", + language="python", content=get_code_of_current_file(), ) diff --git a/streamlitfront/examples/various_input_kinds.py b/streamlitfront/examples/various_input_kinds.py index 7e2650c..3944dc9 100644 --- a/streamlitfront/examples/various_input_kinds.py +++ b/streamlitfront/examples/various_input_kinds.py @@ -10,20 +10,20 @@ def foo(a: int = 0, b: int = 0, c=0): class Klass: - def __init__(self, greeting='hello', confuser_x: float = 3.14): + def __init__(self, greeting="hello", confuser_x: float = 3.14): self.greeting = greeting self.confuser_x = confuser_x def bar(self, x): """bar greets its input""" - return f'{self.greeting} {x}' + return f"{self.greeting} {x}" # def konfuser(self, a: int = 0): # return (a ** 2) * self.confuser_x def i_am_confused(self, a: int = 0): - return (a ** 2) * self.confuser_x + return (a**2) * self.confuser_x klass_instance = Klass() @@ -34,9 +34,9 @@ def i_am_confused(self, a: int = 0): funcs = [foo, klass_instance.bar, klass_instance.konfuser] -if __name__ == '__main__': +if __name__ == "__main__": from streamlitfront.base import dispatch_funcs - print(f'file: {os.path.realpath(__file__)}') + print(f"file: {os.path.realpath(__file__)}") app = dispatch_funcs(funcs) app() diff --git a/streamlitfront/page_funcs.py b/streamlitfront/page_funcs.py index 6fec3ac..1142bf9 100644 --- a/streamlitfront/page_funcs.py +++ b/streamlitfront/page_funcs.py @@ -17,7 +17,6 @@ signature_defaults, ) - # TODO: Extract the page setup (view_title, etc.) and make it injectable. {int: st.number_input, float: st.number_input, str: st.text_input, bool: st.checkbox} @@ -206,9 +205,9 @@ def __call__(self, state): else: if "options" in args_specs[argname]["element_factory"][1]: options = func_inputs[int_args[idx - 1]].split(", ") - args_specs[argname]["element_factory"][1]["options"] = ( - options - ) + args_specs[argname]["element_factory"][1][ + "options" + ] = options element_factory, kwargs = args_specs[argname]["element_factory"] func_inputs[argname] = element_factory(**kwargs) submit = st.button("Submit") @@ -302,7 +301,9 @@ def __call__(self, state): mytype = self.func.__annotations__["return"] output_model = pydantic_model_from_type(mytype) - name = self.func.__name__ # check in sig, dag, lined a better way, i2, may be displayed name: name_of_obj + name = ( + self.func.__name__ + ) # check in sig, dag, lined a better way, i2, may be displayed name: name_of_obj data = sp.pydantic_input(key=f"my_form_{name}", model=mymodel) diff --git a/streamlitfront/scrap/alt_renderer.py b/streamlitfront/scrap/alt_renderer.py index 316a7a9..517a92b 100644 --- a/streamlitfront/scrap/alt_renderer.py +++ b/streamlitfront/scrap/alt_renderer.py @@ -81,12 +81,11 @@ class StreamlitRenderers: import streamlit as st from importlib_resources import files # importlib.resources once only 3.9+ maintained - -data_files = files('streamlitfront').joinpath('data') +data_files = files("streamlitfront").joinpath("data") streamlit_element_func_names = list( - filter(None, data_files.joinpath('streamlit_elements.txt').read_text().split('\n')) + filter(None, data_files.joinpath("streamlit_elements.txt").read_text().split("\n")) ) @@ -97,7 +96,7 @@ class StreamlitRenderers: StreamlitRenderers, name, staticmethod(mk_renderer_factory_for_func(func)) ) else: - print(f'Missing {name}') + print(f"Missing {name}") # TODO: Add warning if func missing? @@ -126,20 +125,20 @@ def mk_component_base( name: str = None, ) -> type: # TODO: could "camelize" name_of_obj output. - name = name or name_of_obj(component_factory) or 'ComponentBase' + name = name or name_of_obj(component_factory) or "ComponentBase" component_sig = Sig(component_factory) - sig: Sig = 'self' + component_sig + Sig(base_cls) + sig: Sig = "self" + component_sig + Sig(base_cls) @sig def __init__(*args, **kwargs) -> None: kw = sig.map_arguments(args, kwargs, apply_defaults=True) - self = kw.pop('self') + self = kw.pop("self") call_forgivingly(base_cls.__init__, self, **kw) for name in component_sig: if not hasattr(self, name): setattr(self, name, kw[name]) - return type(name, (base_cls,), {'__init__': __init__, 'render': render}) + return type(name, (base_cls,), {"__init__": __init__, "render": render}) def implement_component_from_factory( @@ -173,8 +172,8 @@ def test_mk_component_base(): # InputBase: assert sorted(Sig(C).names) == sorted((Sig(st.audio) + Sig(InputBase)).names) - c = C(1, name='tag') - assert (c.data, c.name) == (1, 'tag') + c = C(1, name="tag") + assert (c.data, c.name) == (1, "tag") def test_implement_component_from_factory(): diff --git a/streamlitfront/scrap/front_routing_proposals.py b/streamlitfront/scrap/front_routing_proposals.py index 5bd1aee..1a3f967 100644 --- a/streamlitfront/scrap/front_routing_proposals.py +++ b/streamlitfront/scrap/front_routing_proposals.py @@ -1,6 +1,4 @@ -"""Tools made with streamlitfront - -""" +"""Tools made with streamlitfront""" from typing import Tuple, KT, VT from collections.abc import Iterable, Callable @@ -26,9 +24,9 @@ def first_element_matching_type(obj, types): # else return None -OBJECT = 'OBJECT' -NAME_OF_OBJ = 'NAME_OF_OBJ' -SUBTYPE = 'SUBTYPE' +OBJECT = "OBJECT" +NAME_OF_OBJ = "NAME_OF_OBJ" +SUBTYPE = "SUBTYPE" # TODO: Finish. Intened to be used with _find_render_keys function that handles a @@ -58,6 +56,7 @@ def first_element_matching_type(obj, types): # f"Object couldn't be mapped to a render_keys key: {obj}" # ) + # TODO: Extract routing logic (the if/elifs) and expose control to interface def _find_render_keys(objs, render_keys: dict): """Make a static configs that has a one to one relationship with objects""" @@ -147,8 +146,8 @@ def alt_mk_render_keys(objs, render_keys, resolver: Callable): from typing import TypeVar, Tuple from collections.abc import Iterable, Callable -Obj = TypeVar('Obj') -Output = TypeVar('Output') +Obj = TypeVar("Obj") +Output = TypeVar("Output") Cond = Callable[[Obj], bool] Then = Callable[[Obj], Output] Rule = tuple[Cond, Then] @@ -168,6 +167,7 @@ def alt_mk_render_keys(objs, render_keys, resolver: Callable): # ----------------------------------------------------------------------------- # Say we want to make a function like the following have a plugin architecture + # TODO: Extract routing logic (the if/elifs) and expose control to interface def target_render_keys_func(obj, render_keys): if obj in render_keys: @@ -292,9 +292,7 @@ def if_feature_then_feature_and_obj(feature, cond, then, obj): def rule_applier(obj, rules, render_keys, sentinel=None): - """Applies rules to obj and returns the first non-sentinel value - - """ + """Applies rules to obj and returns the first non-sentinel value""" for rule in rules: if (result := rule(obj, render_keys)) is not sentinel: return result diff --git a/streamlitfront/scrap/nested_select.py b/streamlitfront/scrap/nested_select.py index 32a6085..495ac37 100644 --- a/streamlitfront/scrap/nested_select.py +++ b/streamlitfront/scrap/nested_select.py @@ -31,29 +31,29 @@ def render(self): # level_1 = st_multiselect(options=self.options) # value = st_multiselect(options=self.options[level_1]) # return value - with st.form('first_form2'): - level_1 = st.selectbox(label='l1', options=self.options) - change = st.form_submit_button('Select') - level_2 = st.selectbox(label='l2', options=self.options[level_1]) - submit = st.form_submit_button('Submit') + with st.form("first_form2"): + level_1 = st.selectbox(label="l1", options=self.options) + change = st.form_submit_button("Select") + level_2 = st.selectbox(label="l2", options=self.options[level_1]) + submit = st.form_submit_button("Submit") value = self.options[level_1][level_2] return value -if __name__ == '__main__': +if __name__ == "__main__": app = mk_app( [dummy], config={ - APP_KEY: {'title': 'Nested Chooser'}, + APP_KEY: {"title": "Nested Chooser"}, RENDERING_KEY: { Callable: { - 'execution': { - 'inputs': { - 'my_dict': { + "execution": { + "inputs": { + "my_dict": { ELEMENT_KEY: NestedSelect, - 'options': { - 'level_0_a': {'level_1_a': 'aa', 'level_2': 'bb'}, - 'level_0_b': {'level_1_b': 'cc', 'level_2': 'dd'}, + "options": { + "level_0_a": {"level_1_a": "aa", "level_2": "bb"}, + "level_0_b": {"level_1_b": "cc", "level_2": "dd"}, }, } } diff --git a/streamlitfront/spec_maker.py b/streamlitfront/spec_maker.py index f842ffe..70a0191 100644 --- a/streamlitfront/spec_maker.py +++ b/streamlitfront/spec_maker.py @@ -27,7 +27,6 @@ ) from streamlitfront.data_binding import BoundData - DFLT_CONVENTION_DICT = deep_merge( BASE_DFLT_CONVENTION, { diff --git a/streamlitfront/tests/common.py b/streamlitfront/tests/common.py index 48d562a..4ad01ba 100644 --- a/streamlitfront/tests/common.py +++ b/streamlitfront/tests/common.py @@ -19,7 +19,7 @@ import dill import pickle -STREAMLIT_APP_URL = 'http://localhost:8501' +STREAMLIT_APP_URL = "http://localhost:8501" @contextmanager @@ -34,12 +34,12 @@ def dispatch_funcs_with_selenium(funcs, headless=False): except: serialize_funcs = True _funcs = dill.dumps(funcs) if serialize_funcs else funcs - with run_process(func=run_app, func_kwargs={'funcs': _funcs}, is_ready=3) as proc: + with run_process(func=run_app, func_kwargs={"funcs": _funcs}, is_ready=3) as proc: options = ChromeOptions() # options.add_argument('--no-sandbox') - options.add_argument('--window-size=1920,1080') + options.add_argument("--window-size=1920,1080") if headless: - options.add_argument('--headless') + options.add_argument("--headless") # options.add_argument('--disable-gpu') # options.add_argument('--allow-running-insecure-content') dom = Chrome(service=Service(ChromeDriverManager().install()), options=options) @@ -55,6 +55,7 @@ def give_a_chance_to_render_element(func): Gives a chance to the application to render the element by trying up to three times with 1 second of interval to find it before raising an error. """ + # @wrap(func) def wrapper(*args, **kwargs): def _try_to_find_element(intent_nb): @@ -88,9 +89,9 @@ def select_func(idx, root): def send_input(input_, idx, root): def get_input_type(): if isinstance(input_, Number): - return 'number' + return "number" if isinstance(input_, str): - return 'text' + return "text" input_type = get_input_type() input_el = find_element_by_css_selector( @@ -98,16 +99,16 @@ def get_input_type(): root, ) input_el.click() - select_all_first_key = Keys.COMMAND if platform == 'darwin' else Keys.CONTROL - input_el.send_keys(select_all_first_key, 'a') + select_all_first_key = Keys.COMMAND if platform == "darwin" else Keys.CONTROL + input_el.send_keys(select_all_first_key, "a") input_el.send_keys(str(input_)) def compute_output(func, root): def get_output(previous_output=None, intent_nb=1): output_el = find_element_by_css_selector(output_css_selector, root) - if output_el.find_elements(By.TAG_NAME, 'code'): - output_el = find_element_by_css_selector('code', output_el) + if output_el.find_elements(By.TAG_NAME, "code"): + output_el = find_element_by_css_selector("code", output_el) output = output_el.text return_annot = Sig(func).return_annotation if return_annot not in (Parameter.empty, Any): @@ -122,10 +123,10 @@ def get_previous_output(): return get_output() nb_args = len(Sig(func)) - output_css_selector = f'.element-container:nth-child({nb_args + 3}) .stMarkdown p' + output_css_selector = f".element-container:nth-child({nb_args + 3}) .stMarkdown p" previous_output = get_previous_output() submit_button = find_element_by_css_selector( - f'.element-container:nth-child({nb_args + 2}) button', root + f".element-container:nth-child({nb_args + 2}) button", root ) submit_button.click() return get_output(previous_output) diff --git a/streamlitfront/tests/conftest.py b/streamlitfront/tests/conftest.py index 0a73d0c..7dccf25 100644 --- a/streamlitfront/tests/conftest.py +++ b/streamlitfront/tests/conftest.py @@ -1,8 +1,8 @@ def pytest_addoption(parser): - parser.addoption('--headless', action='store_true', default=False) + parser.addoption("--headless", action="store_true", default=False) def pytest_generate_tests(metafunc): option_value = metafunc.config.option.headless - if 'headless' in metafunc.fixturenames and option_value is not None: - metafunc.parametrize('headless', [option_value]) + if "headless" in metafunc.fixturenames and option_value is not None: + metafunc.parametrize("headless", [option_value]) diff --git a/streamlitfront/tests/dummy_app.py b/streamlitfront/tests/dummy_app.py index ab35ff4..222107e 100644 --- a/streamlitfront/tests/dummy_app.py +++ b/streamlitfront/tests/dummy_app.py @@ -23,10 +23,10 @@ class SimplePageFunc2(BasePageFunc): def __call__(self, state): self.prepare_view(state) # args_specs = get_func_args_specs(self.func) - element = sp.pydantic_input('input', Input) + element = sp.pydantic_input("input", Input) st.write(element) # func_inputs = dict(self.sig.defaults, **state['page_state'][self.func]) - func_inputs = {'input': element} + func_inputs = {"input": element} st.write(func_inputs) # for argname, spec in args_specs.items(): # st.write(f"argname:{argname}") @@ -36,15 +36,15 @@ def __call__(self, state): # st.write(f"element_factory:{element_factory}") # st.write(f"kwargs:{kwargs}") - submit = st.button('Submit') + submit = st.button("Submit") if submit: - st.write(self.func(func_inputs['input'])) + st.write(self.func(func_inputs["input"])) # state['page_state'][self.func].clear() DFLT_PAGE_FACTORY = SimplePageFunc2 -if __name__ == '__main__': +if __name__ == "__main__": app = get_pages_specs([multiple_input], page_factory=DFLT_PAGE_FACTORY) - app['Multiple Input'](None) + app["Multiple Input"](None) diff --git a/streamlitfront/tests/fake_app.py b/streamlitfront/tests/fake_app.py index 3082e83..16d8490 100644 --- a/streamlitfront/tests/fake_app.py +++ b/streamlitfront/tests/fake_app.py @@ -1,3 +1,3 @@ import streamlit as st -st.write('fake app!') +st.write("fake app!") diff --git a/streamlitfront/tests/test_base.py b/streamlitfront/tests/test_base.py index c70ce16..3cff850 100644 --- a/streamlitfront/tests/test_base.py +++ b/streamlitfront/tests/test_base.py @@ -8,5 +8,5 @@ def double_func(x: int) -> int: def test_func_to_page_name(): result = func_to_page_name(double_func) - expected = 'Double Func' + expected = "Double Func" assert result == expected diff --git a/streamlitfront/tests/test_pydantic_widgets.py b/streamlitfront/tests/test_pydantic_widgets.py index e248786..0a9298d 100644 --- a/streamlitfront/tests/test_pydantic_widgets.py +++ b/streamlitfront/tests/test_pydantic_widgets.py @@ -11,7 +11,6 @@ from streamlitfront.pydantic_widgets import _unwrap_optional - # ---------------------------------------------------------------------------- # Pure-helper unit tests diff --git a/streamlitfront/util.py b/streamlitfront/util.py index 57c1381..d3e64fd 100644 --- a/streamlitfront/util.py +++ b/streamlitfront/util.py @@ -76,7 +76,8 @@ def mk_next_str(): def func_name(func): """The func.__name__ of a callable func, or makes and returns one if that fails. - To make one, it calls unamed_func_name which produces incremental names to reduce the chances of clashing""" + To make one, it calls unamed_func_name which produces incremental names to reduce the chances of clashing + """ name = name_of_obj(func) if name is None or name == "": return unnamed_page()