Skip to content

Commit

Permalink
basic usage docs
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Sep 6, 2024
1 parent 5c7a5e0 commit 9edf1b5
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 33 deletions.
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ with run(Env) as m:
- [Background](#background)
- [Installation](#installation)
- [Docs](#docs)
* [Basic usage](#basic-usage)
+ [Supported types](#supported-types)
+ [Nested configuration](#nested-configuration)
* [`mininterface`](#mininterface)
+ [`run`](#run)
+ [`FormField`](#formfield)
Expand Down Expand Up @@ -106,6 +109,69 @@ pip install mininterface

# Docs

Use a common [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) for the configuration. Wrap it to the [run](#run) method that returns an interface `m`. Access the configuration via `m.env` or use it to prompt the user `m.ask_yes("Is that alright?")`.

## Basic usage

### Supported types

Various types are supported. Variables organized in a dataclass:

```python
from dataclasses import dataclass
from pathlib import Path
from mininterface import run
@dataclass
class Env:
my_number: int = 1
""" A dummy number """
my_boolean: bool = True
""" A dummy boolean """
my_path: Path = Path("/tmp")
""" A dummy path """
m = run(Env) # m.env contains an Env instance
m.form() # Prompt a dialog; m.form() without parameter edits m.env
print(m.env)
# Env(my_number=1, my_boolean=True, my_path=PosixPath('/tmp'),
# my_point=<__main__.Point object at 0x7ecb5427fdd0>)
```

![GUI window](asset/supported_types_1.avif "A prompted dialog")

Variables organized in a dict:

Along scalar types, there is (basic) support for common iterables or custom classes.

```python
from mininterface import run
class Point:
def __init__(self, i: int):
self.i = i
def __str__(self):
return str(self.i)
values = {"my_number": 1,
"my_list": [1, 2, 3],
"my_point": Point(10)
}
m = run()
m.form(values) # Prompt a dialog
print(values) # {'my_number': 2, 'my_list': [2, 3], 'my_point': <__main__.Point object...>}
print(values["my_point"].i) # 100
```

![GUI window](asset/supported_types_2.avif "A prompted dialog after editation")

### Nested configuration
You can easily nest the configuration. (See also [Tyro Hierarchical Configs](https://brentyi.github.io/tyro/examples/02_nesting/01_nesting/)).

Just put another dataclass inside the config file:
Expand Down
Binary file added asset/supported_types_1.avif
Binary file not shown.
Binary file added asset/supported_types_2.avif
Binary file not shown.
6 changes: 4 additions & 2 deletions mininterface/FormDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ def dict_to_formdict(data: dict) -> FormDict:
return fd


def formdict_to_widgetdict(d: FormDict | Any, widgetize_callback: Callable):
def formdict_to_widgetdict(d: FormDict | Any, widgetize_callback: Callable, _key=None):
if isinstance(d, dict):
return {k: formdict_to_widgetdict(v, widgetize_callback) for k, v in d.items()}
return {k: formdict_to_widgetdict(v, widgetize_callback, k) for k, v in d.items()}
elif isinstance(d, FormField):
if not d.name: # restore the name from the user provided dict
d.name = _key
return widgetize_callback(d)
else:
return d
Expand Down
27 changes: 2 additions & 25 deletions mininterface/FormField.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
ValidationResult = bool | ErrorMessage
""" Callback validation result is either boolean or an error message. """

# TODO rename to Field?


@dataclass
class FormField:
""" Enrich a value with a description, validation etc.
Expand Down Expand Up @@ -53,9 +50,8 @@ class FormField:
name: str | None = None
""" Name displayed in the UI.
NOTE: Only TextualInterface uses this by now.
GuiInterface reads the name from the dict.
In the future, Textual should be able to do the same
and both, Gui and Textual should use FormField.name as override.
GuiInterface reads the name from the dict only.
Thus, it is not easy to change the dict key as the user expects the original one in the dict.
"""

validation: Callable[["FormField"], ValidationResult | tuple[ValidationResult,
Expand Down Expand Up @@ -148,25 +144,6 @@ def remove_error_text(self):
self.name = self._original_name
self.error_text = None

# NOTE remove
# def _get_ui_val(self, allowed_types: tuple | None = None):
# """ Internal function used from within a UI only, not from the program.

# It returns the val, however the field was already displayed in the UI, it preferably
# returns the value as presented in the UI (self._ui_val). NOTE bad description

# :param allowed_types If the value is not their instance, convert to str.
# Because UIs are not able to process all types.
# """
# # NOTE remove
# # if self._has_ui_val and self._ui_val is not None:
# # v = self._ui_val
# # else:
# v = self.val
# if allowed_types and not isinstance(v, allowed_types):
# v = str(v)
# return v

def _repr_annotation(self):
if isinstance(self.annotation, UnionType):
return repr(self.annotation)
Expand Down
1 change: 0 additions & 1 deletion mininterface/GuiInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def __init__(self, interface: GuiInterface):
@staticmethod
def widgetize(ff: FormField) -> Value:
""" Wrap FormField to a textual widget. """
# NOTE remove: v = ff._get_ui_val((float, int, str, bool))
v = ff.val
if ff.annotation is bool and not isinstance(v, bool):
# tkinter_form unfortunately needs the bool type to display correct widget,
Expand Down
6 changes: 2 additions & 4 deletions mininterface/TextualInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def _ask_env(self) -> EnvClass:
return self.env

# 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. Is there a tyro default for list?
# 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
Expand Down Expand Up @@ -89,8 +89,6 @@ def __init__(self, interface: TextualInterface):
@staticmethod
def widgetize(ff: FormField) -> Checkbox | Input:
""" Wrap FormField to a textual widget. """

# NOTE remove, ff._get_ui_val() was used here
v = ff.val
if ff.annotation is bool or not ff.annotation and (v is True or v is False):
o = Checkbox(ff.name or "", v)
Expand All @@ -107,7 +105,7 @@ def run_dialog(cls, window: "TextualApp", formDict: FormDict, title: str = "") -
if title:
window.title = title

# NOTE Sections (~ nested dicts) are not implemented, they flatten
# NOTE Sections (~ nested dicts) are not implemented, they flatten.
# Maybe just 'flatten' might be removed.
widgets: list[Checkbox | Input] = [f for f in flatten(formdict_to_widgetdict(formDict, cls.widgetize))]
window.widgets = widgets
Expand Down
2 changes: 2 additions & 0 deletions mininterface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class Env:

# Build the interface
title = title or kwargs.get("prog") or Path(sys.argv[0]).name
if "prog" not in kwargs:
kwargs["prog"] = title
try:
interface = interface(title, env, descriptions)
except InterfaceNotAvailable: # Fallback to a different interface
Expand Down
1 change: 0 additions & 1 deletion mininterface/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def run_tyro_parser(env_class: Type[EnvClass],
wf[argument.dest] = FormField("",
argument.help.replace("(required)", ""),
type_,
# validation=FormField.validators.not_empty
validation=not_empty
)
setattr(kwargs["default"], argument.dest, None)
Expand Down

0 comments on commit 9edf1b5

Please sign in to comment.