Skip to content

Commit

Permalink
dataclasses in the Textual form
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Oct 22, 2024
1 parent c108a6d commit 1a1cf1f
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 55 deletions.
13 changes: 0 additions & 13 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,3 @@ m.form(my_dictionary)
```

![List of paths](asset/list_of_paths.avif)













15 changes: 7 additions & 8 deletions mininterface/form_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .tag import Tag, TagValue

if TYPE_CHECKING:
from .facet import Facet
from . import Mininterface

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 15 additions & 12 deletions mininterface/mininterface.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}")

Expand Down
8 changes: 6 additions & 2 deletions mininterface/text_interface.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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:
Expand Down
29 changes: 10 additions & 19 deletions mininterface/textual_interface/__init__.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion mininterface/textual_interface/textual_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down

0 comments on commit 1a1cf1f

Please sign in to comment.