diff --git a/docs/index.md b/docs/index.md index 507bc80..b6ceffb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -162,16 +162,3 @@ m.form(my_dictionary) ``` ![List of paths](asset/list_of_paths.avif) - - - - - - - - - - - - - diff --git a/mininterface/form_dict.py b/mininterface/form_dict.py index ab1c5cb..09878fd 100644 --- a/mininterface/form_dict.py +++ b/mininterface/form_dict.py @@ -17,7 +17,7 @@ from .tag import Tag, TagValue if TYPE_CHECKING: - from .facet import Facet + from . import Mininterface logger = logging.getLogger(__name__) @@ -85,13 +85,13 @@ def formdict_resolve(d: FormDict, extract_main=False, _root=True) -> dict: return out -def dict_to_tagdict(data: dict, facet: "Facet" = None) -> TagDict: +def dict_to_tagdict(data: dict, mininterface: Optional["Mininterface"] = None) -> TagDict: fd = {} for key, val in data.items(): if isinstance(val, dict): # nested config hierarchy - fd[key] = dict_to_tagdict(val, facet) + fd[key] = dict_to_tagdict(val, mininterface) else: # scalar or Tag value - d = {"facet": facet} + d = {"facet": getattr(mininterface, "facet", None)} if not isinstance(val, Tag): tag = Tag(val, "", name=key, _src_dict=data, _src_key=key, **d) else: @@ -111,8 +111,7 @@ def formdict_to_widgetdict(d: FormDict | Any, widgetize_callback: Callable, _key return d -# TODO accept rather interface and fetch facet from within -def dataclass_to_tagdict(env: EnvClass, facet: "Facet" = None, _nested=False) -> TagDict: +def dataclass_to_tagdict(env: EnvClass, mininterface: Optional["Mininterface"] = None, _nested=False) -> TagDict: """ Convert the dataclass produced by tyro into dict of dicts. """ main = {} if not _nested: # root is nested under "" path @@ -140,9 +139,9 @@ def dataclass_to_tagdict(env: EnvClass, facet: "Facet" = None, _nested=False) -> if hasattr(val, "__dict__") and not isinstance(val, (FunctionType, MethodType)): # nested config hierarchy # nested config hierarchy # Why checking the isinstance? See Tag._is_a_callable. - subdict[param] = dataclass_to_tagdict(val, facet, _nested=True) + subdict[param] = dataclass_to_tagdict(val, mininterface, _nested=True) else: # scalar or Tag value - d = {"description": get_description(env.__class__, param), "facet": facet} + d = {"description": get_description(env.__class__, param), "facet": getattr(mininterface, "facet", None)} if not isinstance(val, Tag): tag = tag_factory(val, _src_key=param, _src_obj=env, **d) else: diff --git a/mininterface/mininterface.py b/mininterface/mininterface.py index 40d166a..81f85df 100644 --- a/mininterface/mininterface.py +++ b/mininterface/mininterface.py @@ -1,16 +1,15 @@ -from enum import Enum import logging from dataclasses import is_dataclass +from enum import Enum from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload, Type +from typing import TYPE_CHECKING, Any, Generic, Type, TypeVar, overload +from .cli_parser import run_tyro_parser from .common import Cancelled from .facet import Facet -from .form_dict import DataClass, EnvClass, FormDict, FormDictOrEnv, dict_to_tagdict, formdict_resolve +from .form_dict import (DataClass, EnvClass, FormDict, dataclass_to_tagdict, + dict_to_tagdict, formdict_resolve) from .tag import ChoicesType, Tag, TagValue -from .form_dict import DataClass, FormDict, dataclass_to_tagdict, dict_to_tagdict, formdict_resolve -from .cli_parser import run_tyro_parser - if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self` from typing import Self @@ -262,12 +261,17 @@ class Color(Enum): ![Complex form dict](asset/complex_form_dict.avif) Args: - form: Dict of `{labels: value}`. The form widget infers from the default value type. + form: We accept a dataclass type, a dataclass instance, a dict or None. + + * If dict, we expect a dict of `{labels: value}`. + The form widget infers from the default value type. The dict can be nested, it can contain a subgroup. The value might be a [`Tag`][mininterface.Tag] that allows you to add descriptions. - If None, the `self.env` is being used as a form, allowing the user to edit whole configuration. - (Previously fetched from CLI and config file.) + A checkbox example: `{"my label": Tag(True, "my description")}` + + * If None, the `self.env` is being used as a form, allowing the user to edit whole configuration. + (Previously fetched from CLI and config file.) title: Optional form title Returns: @@ -323,14 +327,13 @@ def _form(self, launch_callback=None) -> FormDict | DataClass | EnvClass: _form = self.env if form is None else form if isinstance(_form, dict): - # TODO integrate to TextualMininterface and others, test and docs # TODO After launching a callback, a TextualInterface stays, the form re-appears. - return formdict_resolve(launch_callback(dict_to_tagdict(_form, self.facet), title=title), extract_main=True) + return formdict_resolve(launch_callback(dict_to_tagdict(_form, self), title=title), extract_main=True) if isinstance(_form, type): # form is a class, not an instance _form, wf = run_tyro_parser(_form, {}, False, False, args=[]) # TODO what to do with wf if is_dataclass(_form): # -> dataclass or its instance # the original dataclass is updated, hence we do not need to catch the output from launch_callback - launch_callback(dataclass_to_tagdict(_form, self.facet)) + launch_callback(dataclass_to_tagdict(_form, self), title=title) return _form raise ValueError(f"Unknown form input {_form}") diff --git a/mininterface/text_interface.py b/mininterface/text_interface.py index a494fcb..cacdc3a 100644 --- a/mininterface/text_interface.py +++ b/mininterface/text_interface.py @@ -1,8 +1,9 @@ from pprint import pprint +from typing import Type from .common import Cancelled -from .form_dict import EnvClass, FormDict, FormDictOrEnv +from .form_dict import EnvClass, FormDict, DataClass from .mininterface import Mininterface @@ -22,7 +23,10 @@ def ask(self, text: str = None): raise Cancelled(".. cancelled") return txt - def form(self, form: FormDictOrEnv | None = None, title: str = "") -> FormDictOrEnv | EnvClass: + def form(self, + form: DataClass | Type[DataClass] | FormDict | None = None, + title: str = "" + ) -> FormDict | DataClass | EnvClass: # NOTE: This is minimal implementation that should rather go the ReplInterface. # NOTE: Concerning Dataclass form. # I might build some menu of changing dict through: diff --git a/mininterface/textual_interface/__init__.py b/mininterface/textual_interface/__init__.py index 6959ffc..ac4645e 100644 --- a/mininterface/textual_interface/__init__.py +++ b/mininterface/textual_interface/__init__.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from typing import Any +from typing import Any, Type -from mininterface.textual_interface.textual_button_app import TextualButtonApp from mininterface.textual_interface.textual_app import TextualApp +from mininterface.textual_interface.textual_button_app import TextualButtonApp from mininterface.textual_interface.textual_facet import TextualFacet try: @@ -11,7 +11,7 @@ from mininterface.common import InterfaceNotAvailable raise InterfaceNotAvailable -from ..form_dict import (EnvClass, FormDictOrEnv, dataclass_to_tagdict, dict_to_tagdict, formdict_resolve) +from ..form_dict import DataClass, EnvClass, FormDict from ..redirectable import Redirectable from ..tag import Tag from ..text_interface import TextInterface @@ -33,24 +33,15 @@ def alert(self, text: str) -> None: def ask(self, text: str = None): return self.form({text: ""})[text] - def _ask_env(self) -> EnvClass: - """ Display a window form with all parameters. """ - form = dataclass_to_tagdict(self.env, self.facet) - - # fetch the dict of dicts values from the form back to the namespace of the dataclasses - return TextualApp.run_dialog(self._get_app(), form) - - # NOTE: This works bad with lists. GuiInterface considers list as combobox (which is now suppressed by str conversion), - # TextualInterface as str. We should decide what should happen. - def form(self, form: FormDictOrEnv | None = None, title: str = "") -> FormDictOrEnv | EnvClass: - if form is None: - return self._ask_env() # NOTE should be integrated here when we integrate dataclass, see FormDictOrEnv - else: - return formdict_resolve(TextualApp.run_dialog(self._get_app(), dict_to_tagdict(form, self.facet), title), extract_main=True) + def form(self, + form: DataClass | Type[DataClass] | FormDict | None = None, + title: str = "" + ) -> FormDict | DataClass | EnvClass: + def clb(form, title, c=self): return TextualApp.run_dialog(c._get_app(), form, title) + return self._form(form, title, clb) - # NOTE we should implement better, now the user does not know it needs an int def ask_number(self, text: str): - return self.form({text: Tag("", "", int, text)})[text].val + return self.form({text: Tag("", "", int, text)})[text] def is_yes(self, text: str): return TextualButtonApp(self).yes_no(text, False).val diff --git a/mininterface/textual_interface/textual_app.py b/mininterface/textual_interface/textual_app.py index 3a4c919..643d62a 100644 --- a/mininterface/textual_interface/textual_app.py +++ b/mininterface/textual_interface/textual_app.py @@ -71,7 +71,13 @@ def widgetize(tag: Tag) -> Widget | Changeable: else: if not isinstance(v, (float, int, str, bool)): v = str(v) - o = MyInput(str(v), placeholder=tag.name or "") + if issubclass(tag.annotation, int): + type_ = "integer" + elif issubclass(tag.annotation, float): + type_ = "number" + else: + type_ = "text" + o = MyInput(str(v), placeholder=tag.name or "", type=type_) o._link = tag # The Textual widgets need to get back to this value tag._last_ui_val = o.get_ui_value() diff --git a/tests/tests.py b/tests/tests.py index f1b9a7f..d1202f1 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -147,6 +147,13 @@ def test_ask_form(self): self.assertEqual(m2.env, m2.form()) self.assertTrue(m2.env.test) + # Form accepts a dataclass type + m3 = run(interface=Mininterface) + self.assertEqual(SimpleEnv(), m3.form(SimpleEnv)) + + # Form accepts a dataclass instance + self.assertEqual(SimpleEnv(), m3.form(SimpleEnv())) + def test_form_output(self): m = run(SimpleEnv, interface=Mininterface) d1 = {"test1": "str", "test2": Tag(True)}