Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Oct 24, 2024
1 parent df6c695 commit ede9d4f
Show file tree
Hide file tree
Showing 12 changed files with 70 additions and 22 deletions.
Binary file added asset/choice_enum_instance.avif
Binary file not shown.
Binary file added asset/complex_example.avif
Binary file not shown.
Binary file added asset/complex_example_missing_field.avif
Binary file not shown.
Binary file modified asset/list_of_paths.avif
Binary file not shown.
Binary file added asset/tag_choices.avif
Binary file not shown.
10 changes: 9 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class Env:
nested_config: NestedEnv
mandatory_str: str
""" As there is not default value, you will be prompted automatically to fill up the field """
""" As there is no default value, you will be prompted automatically to fill up the field """
my_number: int | None = None
""" This is not just a dummy number, if left empty, it is None. """
Expand All @@ -143,6 +143,14 @@ print(m.env)
m.form()
```

As there is no default value at `mandatory_str`, you will be prompted automatically to fill up the field:

![Complex example missing field](asset/complex_example_missing_field.avif)

Then, full form appears:

![Complex example](asset/complex_example.avif)

## Form with paths

We have a dict with some paths. Here is how it looks.
Expand Down
29 changes: 22 additions & 7 deletions mininterface/mininterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

logger = logging.getLogger(__name__)

# TODO readme examples refresh imgs


class Mininterface(Generic[EnvClass]):
""" The base interface.
Expand Down Expand Up @@ -161,7 +159,18 @@ class Color(Enum):
![Choices from enum](asset/choice_enum_type.avif)
TODO Enum instance
Alternatively, you may use an Enum instance.
```python
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
m.choice(Color.BLUE)
```
![Choices from enum](asset/choice_enum_instance.avif)
Alternatively, you may use an Enum instances list.
Expand All @@ -185,6 +194,8 @@ class Color(Enum):
The chosen value.
If launch=True and the chosen value is a callback, we call it and return its result.
!!! info
To tackle a more detailed form, see [`Tag.choices`][mininterface.Tag.choices].
"""
# NOTE to build a nice menu, I need this
# Args:
Expand All @@ -198,15 +209,19 @@ class Color(Enum):
# (possibly because the lambda hides a part of GUI)
# m = run(Env)
# tag = Tag(x, choices=["one", "two", x])
if skippable and len(choices) == 1:
if isinstance(choices, type) and issubclass(choices, Enum):
if skippable and isinstance(choices, Enum): # Enum instance, ex: val=ColorEnum.RED
default = choices
choices = choices.__class__

if skippable and len(choices) == 1: # Directly choose the answer
if isinstance(choices, type) and issubclass(choices, Enum): # Enum type, ex: val=ColorEnum
out = list(choices)[0]
elif isinstance(choices, dict):
out = next(iter(choices.values()))
else:
out = choices[0]
tag = Tag(out)
else:
else: # Trigger the dialog
tag = Tag(val=default, choices=choices)
key = title or "Choose"
self.form({key: tag})[key]
Expand Down Expand Up @@ -321,7 +336,7 @@ class Color(Enum):
```
"""
print(f"Asking the form {title}".strip(), self.env if form is None else form)
return self._form(form, title, MinAdaptor(self)) # TODO does the adaptor works here?
return self._form(form, title, MinAdaptor(self))

def _form(self,
form: DataClass | Type[DataClass] | FormDict | None,
Expand Down
24 changes: 17 additions & 7 deletions mininterface/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
Either put options in an iterable or to a dict `{labels: value}`.
Values might be Tags as well.
See [mininterface.choice][mininterface.Mininterface.choice] for examples.
See [mininterface.choice][mininterface.Mininterface.choice] or [`Tag.choices`][mininterface.Tag.choices] for examples.
"""


Expand All @@ -67,7 +67,7 @@ class Tag:
"""

val: TagValue = None
""" The value wrapped by Tag.
""" The value wrapped by Tag. It can be any value.
```python
from mininterface import run, Tag
Expand All @@ -83,6 +83,14 @@ class Tag:
The encapsulated value is `True`, `tag.description` is 'This is my boolean',
`tag.annotation` is `bool` and 'My boolean' is used as `tag.name`.
!!! tip
If the Tag is nested, the info is fetched to the outer Tag.
When updated, the inner Tag value updates accordingly.
```python
tag = Tag(Tag(True))
```
"""
description: str = ""
""" The description displayed in the UI. """
Expand Down Expand Up @@ -133,7 +141,7 @@ class Env:
"""

choices: ChoicesType | None = None
""" Print the radio buttons. Constraint the value.
""" Print the radio buttons / select box. Constraint the value.
```python
from dataclasses import dataclass
Expand All @@ -143,14 +151,17 @@ class Env:
@dataclass
class Env:
foo: Annotated["str", Choices("one", "two")] = "one"
# `Choices` is an alias for `Tag(choices=)`
m = run(Env)
m.form() # prompts a dialog
```
![Form choice](asset/tag_choices.avif)
!!! info
When dealing with a simple use case, use the [mininterface.choice][mininterface.Mininterface.choice] dialog.
"""
# NOTE we should support
# NOTE we should support (maybe it is done)
# * Enums: Tag(enum) # no `choice` param`
# * more date types (now only str possible)
# * mininterface.choice `def choice(choices=, guesses=)`
Expand Down Expand Up @@ -235,7 +246,6 @@ def check(tag.val):

def __post_init__(self):
# Fetch information from the nested tag: `Tag(Tag(...))`
# TODO docs, test
if isinstance(self.val, Tag):
if self._src_obj or self._src_key:
raise ValueError("Wrong Tag inheritance, submit a bug report.")
Expand Down Expand Up @@ -583,7 +593,7 @@ def update(self, ui_value: TagValue) -> bool:
# other interfaces does not guarantee that. Hence, we need to do the type conversion too.
if self.annotation:
if self.annotation == TagCallback:
return True # TODO
return True # NOTE, EXPERIMENTAL
if ui_value == "" and NoneType in get_args(self.annotation):
# The user is not able to set the value to None, they left it empty.
# Cast back to None as None is one of the allowed types.
Expand Down
6 changes: 3 additions & 3 deletions mininterface/tk_interface/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ def _fetch(variable):
widget['values'] = list(tag._get_choices())
widget.pack()
widget.bind('<Return>', lambda _: "break") # override default enter that submits the form
variable.set(chosen_val)
if chosen_val is not None:
variable.set(chosen_val)

else:
for i, (choice_label, choice_val) in enumerate(tag._get_choices().items()):
widget2 = Radiobutton(nested_frame, text=choice_label, variable=variable, value=choice_label)
widget2.grid(row=i, column=1, sticky="w")
subwidgets.append(widget2)
if choice_val is chosen_val:
if choice_val is tag.val:
variable.set(choice_label)
# TODO does this works in textual too?

# File dialog
elif path_tag := tag._morph(PathTag, (PosixPath, Path)):
Expand Down
6 changes: 5 additions & 1 deletion mininterface/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from .tag import Tag, ValidationResult, TagValue


from .type_stubs import TagCallback, TagType # Allow import from the module
from .type_stubs import TagCallback, TagType # Allow import from the module


def Validation(check: Callable[["Tag"], ValidationResult | tuple[ValidationResult, TagValue]]):
""" Alias to [`Tag(validation=...)`][mininterface.Tag.validation]
Expand Down Expand Up @@ -37,6 +38,9 @@ def Choices(*choices: list[str]):
class CallbackTag(Tag):
''' Callback function is guaranteed to receives the [Tag][mininterface.Tag] as a parameter.
!!! warning
Experimental. May change.
For the following examples, we will use these custom callback functions:
```python
from mininterface import run
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "mininterface"
version = "0.6.2rc2"
version = "0.6.2"
description = "A minimal access to GUI, TUI, CLI and config"
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
license = "GPL-3.0-or-later"
Expand Down
15 changes: 13 additions & 2 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,12 @@ def test_choice_callback(self):

self.assertEqual(50, m.choice(choices, default=callback_raw))

# TODO This test does not work
# TODO This test does not work. We have to formalize the callback.
# self.assertEqual(100, m.choice(choices, default=choices["My choice2"]))


class TestConversion(TestAbstract):

def test_tagdict_resolve(self):
self.assertEqual({"one": 1}, formdict_resolve({"one": 1}))
self.assertEqual({"one": 1}, formdict_resolve({"one": Tag(1)}))
Expand Down Expand Up @@ -421,7 +422,6 @@ def test_tag_src_update(self):
self.assertEqual("moo", m.env.test)

def test_nested_tag(self):
# TODO docs nested tags
t0 = Tag(5)
t1 = Tag(t0, name="Used name")
t2 = Tag(t1, name="Another name")
Expand All @@ -448,6 +448,12 @@ def test_nested_tag(self):
self.assertEqual(5, t4.val)
self.assertEqual(8, t5.val) # from t2, we iherited the hook to t1

# update triggers the value propagation
inner = Tag(2)
outer = Tag(Tag(Tag(inner)))
outer.update(3)
self.assertEqual(3, inner.val)


class TestRun(TestAbstract):
def test_run_ask_empty(self):
Expand Down Expand Up @@ -788,6 +794,11 @@ def test_choice_method(self):
# list of enums
self.assertEqual(ColorEnum.GREEN, m.choice([ColorEnum.BLUE, ColorEnum.GREEN], default=ColorEnum.GREEN))
self.assertEqual(ColorEnum.BLUE, m.choice([ColorEnum.BLUE]))
# this works but I'm not sure whether it is good to guarantee None
self.assertEqual(None, m.choice([ColorEnum.RED, ColorEnum.GREEN]))

# Enum instance signify the default
self.assertEqual(ColorEnum.RED, m.choice(ColorEnum.RED))


if __name__ == '__main__':
Expand Down

0 comments on commit ede9d4f

Please sign in to comment.