diff --git a/asset/choices_default.avif b/asset/choices_default.avif new file mode 100644 index 0000000..8497d3b Binary files /dev/null and b/asset/choices_default.avif differ diff --git a/asset/facet_backend.avif b/asset/facet_backend.avif new file mode 100644 index 0000000..37cc4c2 Binary files /dev/null and b/asset/facet_backend.avif differ diff --git a/asset/facet_frontend.avif b/asset/facet_frontend.avif new file mode 100644 index 0000000..9a45419 Binary files /dev/null and b/asset/facet_frontend.avif differ diff --git a/docs/Facet.md b/docs/Facet.md new file mode 100644 index 0000000..c64b1b6 --- /dev/null +++ b/docs/Facet.md @@ -0,0 +1,2 @@ +# Facet +::: mininterface.facet.Facet \ No newline at end of file diff --git a/docs/Overview.md b/docs/Overview.md index 7f3a8ee..969e833 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -1,7 +1,9 @@ ## Basic usage Use a common [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass), a Pydantic [BaseModel](https://brentyi.github.io/tyro/examples/04_additional/08_pydantic/) or an [attrs](https://brentyi.github.io/tyro/examples/04_additional/09_attrs/) model to store the configuration. Wrap it to the [run][mininterface.run] method that returns an interface `m`. Access the configuration via [`m.env`][mininterface.Mininterface.env] or use it to prompt the user [`m.is_yes("Is that alright?")`][mininterface.Mininterface.is_yes]. -To do any advanced things, stick the value to a powerful [`Tag`][mininterface.Tag]. For a validation only, use its [`Validation alias`](#validation-alias). +To do any advanced things, stick the value to a powerful [`Tag`][mininterface.Tag]. For a validation only, use its [`Validation alias`](Validation.md/#validation-alias). + +At last, use [`Facet`](Facet.md) to tackle the interface from the back-end (`m`) or the front-end (`Tag`) side. ## Supported types @@ -109,7 +111,7 @@ further: ## All possible interfaces -Normally, you get an interface through [mininterface.run](#run) +Normally, you get an interface through [mininterface.run][] but if you do not wish to parse CLI and config file, you can invoke one directly. Several interfaces exist: diff --git a/docs/index.md b/docs/index.md index 8ad7667..9b85424 100644 --- a/docs/index.md +++ b/docs/index.md @@ -196,6 +196,37 @@ m.form(my_dictionary) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mininterface/__init__.py b/mininterface/__init__.py index 1ffdb26..3db4648 100644 --- a/mininterface/__init__.py +++ b/mininterface/__init__.py @@ -101,7 +101,7 @@ class Env: $ program.py # omitting --required-number # Dialog for `required_number` appears ``` - interface: Which interface to prefer. By default, we use the GUI, the fallback is the TUI. See the full [list](/Mininterface#all-possible-interfaces) of possible interfaces. + interface: Which interface to prefer. By default, we use the GUI, the fallback is the TUI. See the full [list](Overview.md#all-possible-interfaces) of possible interfaces. Kwargs: The same as for [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html). @@ -112,19 +112,26 @@ class Env: You cay context manager the function by a `with` statement. The stdout will be redirected to the interface (ex. a GUI window). - TODO add wrap example - - Undocumented experimental: The `env_class` may be a function as well. We invoke its parameters. - However, as Mininterface.env stores the output of the function instead of the Argparse namespace, - methods like `Mininterface.form(None)` (to ask for editing the env values) will work unpredictibly. - Also, the config file seems to be fetched only for positional (missing) parameters, - and ignored for keyword (filled) parameters. - It seems to be this is the tyro's deal and hence it might start working any time. - If not, we might help it this way: - `if isinstance(config, FunctionType): config = lambda: config(**kwargs["default"])` - - Undocumented experimental: `default` keyword argument for tyro may serve for default values instead of a config file. + + ```python + with run(Env) as m: + print(f"Your important number is {m.env.important_number}") + boolean = m.is_yes("Is that alright?") + ``` + + ![Small window with the text 'Your important number'](asset/hello-with-statement.webp "With statement to redirect the output") + ![The same in terminal'](asset/hello-with-statement-tui.avif "With statement in TUI fallback") """ + # Undocumented experimental: The `env_class` may be a function as well. We invoke its parameters. + # However, as Mininterface.env stores the output of the function instead of the Argparse namespace, + # methods like `Mininterface.form(None)` (to ask for editing the env values) will work unpredictibly. + # Also, the config file seems to be fetched only for positional (missing) parameters, + # and ignored for keyword (filled) parameters. + # It seems to be this is the tyro's deal and hence it might start working any time. + # If not, we might help it this way: + # `if isinstance(config, FunctionType): config = lambda: config(**kwargs["default"])` + # + # Undocumented experimental: `default` keyword argument for tyro may serve for default values instead of a config file. # Prepare the config file if config_file is True and not kwargs.get("default") and env_class: @@ -153,6 +160,10 @@ class Env: if "prog" not in kwargs: kwargs["prog"] = title try: + if interface == "tui": # undocumented feature + interface = TuiInterface + elif interface == "gui": # undocumented feature + interface = GuiInterface interface = interface(title, env, descriptions) except InterfaceNotAvailable: # Fallback to a different interface interface = TuiInterface(title, env, descriptions) diff --git a/mininterface/auxiliary.py b/mininterface/auxiliary.py index 6b5089f..63b86f2 100644 --- a/mininterface/auxiliary.py +++ b/mininterface/auxiliary.py @@ -1,7 +1,7 @@ import os import re from argparse import ArgumentParser -from tkinter import Button, StringVar, Variable +from tkinter import Button, Label, StringVar, Variable from tkinter.ttk import Frame, Radiobutton from types import SimpleNamespace from typing import TYPE_CHECKING, Iterable, Literal, TypeVar @@ -69,7 +69,7 @@ def get(self): return self.val -def replace_widget_with(target: Literal["button"] | Literal["radio"], widget: Widget, name, value: "Tag") -> Widget: +def replace_widget_with(target: Literal["button"] | Literal["radio"], widget: Widget, name, tag: "Tag") -> Widget: if widget.winfo_manager() == 'grid': grid_info = widget.grid_info() widget.grid_forget() @@ -79,17 +79,20 @@ def replace_widget_with(target: Literal["button"] | Literal["radio"], widget: Wi # NOTE tab order broken, injected to another position match target: case "radio": - choices = value._get_choices() - master._Form__vars[name] = variable = Variable(value=value.val) # the chosen default + choices = tag._get_choices() + master._Form__vars[name] = variable = Variable(value=tag.val) # the chosen default nested_frame = Frame(master) nested_frame.grid(row=grid_info['row'], column=grid_info['column']) - for i, choice in enumerate(choices): - radio = Radiobutton(nested_frame, text=choice, variable=variable, value=choice) + + for i, (label, val) in enumerate(choices.items()): + radio = Radiobutton(nested_frame, text=label, variable=variable, value=val) radio.grid(row=i, column=1) case "button": - master._Form__vars[name] = AnyVariable(value.val) - radio = Button(master, text=name, command=lambda tag=value: tag.val(tag)) + # TODO should the button receive tag or directly the whole facet (to change the current form). + # Implement to textual. Docs. + master._Form__vars[name] = AnyVariable(tag.val) + radio = Button(master, text=name, command=lambda tag=tag: tag.val(tag.facet)) radio.grid(row=grid_info['row'], column=grid_info['column']) else: warn(f"GuiInterface: Cannot tackle the form, unknown winfo_manager {widget.winfo_manager()}.") diff --git a/mininterface/facet.py b/mininterface/facet.py index 2f337c3..5daaa3d 100644 --- a/mininterface/facet.py +++ b/mininterface/facet.py @@ -4,17 +4,8 @@ from .tag import Tag -class Facet: - """ A frontend side of the interface. While a dialog is open, - this allows to set frontend properties like the heading. """ - # Every UI adopts this object through BackendAdaptor methods. - # TODO - - def set_heading(self, text): - pass - - class BackendAdaptor(ABC): + facet: "Facet" @staticmethod @abstractmethod @@ -26,3 +17,38 @@ def widgetize(tag: Tag): def run_dialog(self, form: TagDict, title: str = "") -> TagDict: """ Let the user edit the dict values. """ pass + + +class Facet: + """ A frontend side of the interface. While a dialog is open, + this allows to set frontend properties like the heading. + + + Read [`Tag.facet`][mininterface.Tag.facet] to see how to access from the front-end side. + Read [`Mininterface.facet`][mininterface.Mininterface.facet] to see how to access from the back-end side. + """ + # Every UI adopts this object through BackendAdaptor methods. + + @abstractmethod + def __init__(self, window: BackendAdaptor): + ... + + @abstractmethod + def set_title(self, text): + """ Set the main heading. """ + ... + + # @abstractmethod + # def submit(self, text): + # """ Submits the whole form """ + # (ex for checkbox on_change) + # raise NotImplementedError # NOTE + # + # Access to the fields. + + +class MinFacet(Facet): + """ A mininterface needs a facet and the base Facet is abstract and cannot be instanciated. """ + + def __init__(self, window=None): + pass diff --git a/mininterface/form_dict.py b/mininterface/form_dict.py index 25690bb..bf4cda6 100644 --- a/mininterface/form_dict.py +++ b/mininterface/form_dict.py @@ -3,10 +3,17 @@ """ import logging from types import FunctionType, MethodType -from typing import Any, Callable, Optional, Self, TypeVar, Union, get_type_hints +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, get_type_hints + +if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self` + from typing import Self + from .tag import Tag, TagValue +if TYPE_CHECKING: + from .facet import Facet + logger = logging.getLogger(__name__) EnvClass = TypeVar("EnvClass") @@ -72,14 +79,20 @@ def formdict_resolve(d: FormDict, extract_main=False, _root=True) -> dict: return out -def dict_to_tagdict(data: dict) -> TagDict: +def dict_to_tagdict(data: dict, facet: "Facet" = None) -> TagDict: fd = {} for key, val in data.items(): if isinstance(val, dict): # nested config hierarchy - fd[key] = dict_to_tagdict(val) + fd[key] = dict_to_tagdict(val, facet) else: # scalar value - fd[key] = Tag(val, "", name=key, _src_dict=data, _src_key=key) \ - if not isinstance(val, Tag) else val + # TODO implement object fetching to the dataclasses below too + # dataclass_to_tagdict + d = {"facet": facet, "_src_dict":data, "_src_key":key} + if not isinstance(val, Tag): + tag = Tag(val, "", name=key, **d) + else: + tag = Tag(**d)._fetch_from(val) + fd[key] = tag return fd @@ -112,7 +125,7 @@ def dataclass_to_tagdict(env: EnvClass, descr: dict, _path="") -> TagDict: # Since tkinter_form does not handle None yet, this will display as checkbox. # Which is not probably wanted. val = False - logger.warn(f"Annotation {annotation} of `{param}` not supported by Mininterface." + logger.warning(f"Annotation {annotation} of `{param}` not supported by Mininterface." "None converted to False.") if hasattr(val, "__dict__") and not isinstance(val, (FunctionType, MethodType)): # nested config hierarchy # Why checking the isinstance? See Tag._is_a_callable. diff --git a/mininterface/gui_interface.py b/mininterface/gui_interface.py index d734bf3..4532af0 100644 --- a/mininterface/gui_interface.py +++ b/mininterface/gui_interface.py @@ -1,10 +1,10 @@ import sys from typing import Any, Callable -from .facet import BackendAdaptor +from .facet import BackendAdaptor, Facet try: - from tkinter import TclError, LEFT, Button, Frame, Label, Text, Tk + from tkinter import TclError, LEFT, Button, Frame, Label, Text, Tk, Widget from tktooltip import ToolTip from tkinter_form import Form, Value except ImportError: @@ -54,7 +54,7 @@ def form(self, form: FormDictOrEnv | None = None, title: str = "") -> FormDictOr # NOTE should be integrated here when we integrate dataclass, see FormDictOrEnv return self._ask_env() else: - return formdict_resolve(self.window.run_dialog(dict_to_tagdict(form), title=title), extract_main=True) + return formdict_resolve(self.window.run_dialog(dict_to_tagdict(form, self.facet), title=title), extract_main=True) def ask_number(self, text: str) -> int: return self.form({text: 0})[text] @@ -71,6 +71,7 @@ class TkWindow(Tk, BackendAdaptor): def __init__(self, interface: GuiInterface): super().__init__() + self.facet = interface.facet = TkFacet(self) self.params = None self._result = None self._event_bindings = {} @@ -81,6 +82,9 @@ def __init__(self, interface: GuiInterface): self.frame = Frame(self) """ dialog frame """ + self.label = Label(self, text="") + self.label.pack_forget() + self.text_widget = Text(self, wrap='word', height=20, width=80) self.text_widget.pack_forget() self.pending_buffer = [] @@ -105,8 +109,8 @@ def run_dialog(self, form: TagDict, title: str = "") -> TagDict: On abrupt window close, the program exits. """ if title: - label = Label(self.frame, text=title) - label.pack(pady=10) + self.facet.set_title(title) + self.form = Form(self.frame, name_form="", form_dict=formdict_to_widgetdict(form, self.widgetize), @@ -117,10 +121,14 @@ def run_dialog(self, form: TagDict, title: str = "") -> TagDict: # Add radio nested_widgets = widgets_to_dict(self.form.widgets) for tag, (label, widget) in zip(flatten(form), flatten(nested_widgets)): + tag: Tag + label: Widget + widget: Widget if tag.choices: - replace_widget_with("radio", widget, label.cget("text"), tag) + replace_widget_with("radio", widget, tag.name or label.cget("text"), tag) if tag._is_a_callable(): - replace_widget_with("button", widget, label.cget("text"), tag) + replace_widget_with("button", widget, tag.name or label.cget("text"), tag) + label.grid_forget() # Change label name as the field name might have changed (ex. highlighted by an asterisk) # But we cannot change the dict key itself @@ -193,3 +201,16 @@ def _clear_dialog(self): self.unbind(key) self._event_bindings.clear() self._result = None + + +class TkFacet(Facet): + def __init__(self, window: TkWindow): + self.window = window + + def set_title(self, title: str): + if not title: + self.window.label.pack_forget() + else: + self.window.label.config(text=title) + self.window.label.pack(pady=10) + pass diff --git a/mininterface/mininterface.py b/mininterface/mininterface.py index 0e0686b..b5641b1 100644 --- a/mininterface/mininterface.py +++ b/mininterface/mininterface.py @@ -3,6 +3,8 @@ from types import SimpleNamespace from typing import TYPE_CHECKING +from .facet import Facet, MinFacet + if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self` from typing import Generic, Self else: @@ -22,7 +24,7 @@ class Cancelled(SystemExit): class Mininterface(Generic[EnvClass]): """ The base interface. You get one through [`mininterface.run`](run.md) which fills CLI arguments and config file to `mininterface.env` - or you can create [one](.#all-possible-interfaces) directly (without benefiting from the CLI parsing). + or you can create [one](Overview.md#all-possible-interfaces) directly (without benefiting from the CLI parsing). """ # This base interface does not require any user input and hence is suitable for headless testing. @@ -59,6 +61,21 @@ class Env: ``` """ + + self.facet: Facet = MinFacet() + """ Access to the UI [`facet`][mininterface.facet.Facet] from the back-end side. + (Read [`Tag.facet`][mininterface.Tag.facet] to access from the front-end side.) + + ```python + from mininterface import run + with run(title='My window title') as m: + m.facet.set_title("My form title") + m.form({"My form": 1}) + ``` + + ![Facet back-end](asset/facet_backend.avif) + """ + self._descriptions = _descriptions or {} """ Field descriptions """ @@ -86,7 +103,7 @@ def ask_number(self, text: str) -> int: return 0 def choice(self, choices: ChoicesType, title: str = "", _guesses=None, - skippable: bool = True, launch: bool = True, _multiple=True, _default: str | None = None + skippable: bool = True, launch: bool = True, _multiple=True, default: str | None = None ) -> TagValue | list[TagValue] | None: """ Prompt the user to select. Useful for a menu creation. @@ -116,7 +133,13 @@ def choice(self, choices: ChoicesType, title: str = "", _guesses=None, ![Choices with labels](asset/choices_labels.avif) title: Form title - skippable: If there is a single option, choose it directly, without dialog. + default: The value of the checked choice. + + ```python + m.choice({"one": 1, "two": 2}, default=2) # returns 2 + ``` + ![Default choice](asset/choices_default.avif) + skippable: If there is a single option, choose it directly, without a dialog. launch: If the chosen value is a callback, we directly call it. Then, the function returns None. Returns: @@ -128,19 +151,18 @@ def choice(self, choices: ChoicesType, title: str = "", _guesses=None, # Args: # guesses: Choices to be highlighted. # multiple: Multiple choice. - # default: The name of the checked choice. # Returns: If multiple=True, list of the chosen values. # - # * When inputing choices as Tags, make sure the original Tag.val changes too. - # * multiple, checked, guesses + # * Check: When inputing choices as Tags, make sure the original Tag.val changes too. # # NOTE UserWarning: GuiInterface: Cannot tackle the form, unknown winfo_manager . + # (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: out = choices[0] else: - tag = Tag(choices=choices) + tag = Tag(val=default, choices=choices) key = title or "Choose" out = self.form({key: tag})[key] @@ -209,7 +231,7 @@ def form(self, form: FormDictOrEnv | None = None, title: str = "") -> FormDictOr f = form print(f"Asking the form {title}".strip(), f) - tag_dict = dict_to_tagdict(f) + tag_dict = dict_to_tagdict(f, self.facet) if True: # NOTE for testing, this might validate the fields with Tag._submit(ddd, ddd) return formdict_resolve(tag_dict, extract_main=True) else: diff --git a/mininterface/tag.py b/mininterface/tag.py index 71f4dd5..2531416 100644 --- a/mininterface/tag.py +++ b/mininterface/tag.py @@ -5,9 +5,11 @@ from typing import TYPE_CHECKING, Callable, Iterable, Optional, TypeVar, get_args, get_origin, get_type_hints from warnings import warn + from .auxiliary import flatten if TYPE_CHECKING: + from .facet import Facet from .form_dict import TagDict from typing import Self # remove the line as of Python3.11 and make `"Self" -> Self` @@ -162,6 +164,28 @@ class Env: # # Following attributes are not meant to be set externally. # + facet: Optional["Facet"] = None + """ Access to the UI [`facet`][mininterface.facet.Facet] from the front-end side. + (Read [`Mininterface.facet`][mininterface.Mininterface.facet] to access from the back-end side.) + + Set the UI facet from within a callback, ex. a validator. + + ```python + from mininterface import run, Tag + + def my_check(tag: Tag): + tag.facet.set_title("My form title") + return "Validation failed" + + with run(title='My window title') as m: + m.form({"My form": Tag(1, validation=my_check)}) + ``` + + This happens when you click ok. + + ![Facet front-end](asset/facet_frontend.avif) + """ + original_val = None """ Meant to be read only in callbacks. The original value, preceding UI change. Handy while validating. @@ -173,6 +197,7 @@ def check(tag.val): ``` """ + _error_text = None """ Meant to be read only. Error text if type check or validation fail and the UI has to be revised """ @@ -198,11 +223,15 @@ def __post_init__(self): self._attrs_field: dict | None = attr.fields_dict(self._src_class.__class__).get(self._src_key) except attr.exceptions.NotAnAttrsClassError: pass - if not self.annotation and not self.choices: + if not self.annotation and self.val is not None and not self.choices: # When having choices with None default self.val, this would impose self.val be of a NoneType, # preventing it to set a value. + # Why checking self.val is not None? We do not want to end up with + # annotated as a NoneType. self.annotation = type(self.val) + + if not self.name and self._src_key: self.name = self._src_key self._original_desc = self.description @@ -226,11 +255,14 @@ def __repr__(self): field_strings.append(v) return f"{self.__class__.__name__}({', '.join(field_strings)})" - def _fetch_from(self, tag: "Self"): + def _fetch_from(self, tag: "Self") -> "Self": """ Fetches public attributes from another instance. """ - for attr in ['val', 'description', 'annotation', 'name', 'validation', 'choices']: + for attr in ['val', 'annotation', 'name', 'validation', 'choices']: if getattr(self, attr) is None: setattr(self, attr, getattr(tag, attr)) + if self.description == "": + self.description = tag.description + return self def _is_a_callable(self) -> bool: """ True, if the value is a callable function. @@ -325,6 +357,8 @@ def _edit(v): return v.name return v + if self.choices is None: + return {} if isinstance(self.choices, dict): return self.choices if isinstance(self.choices, common_iterables): diff --git a/mininterface/textual_interface.py b/mininterface/textual_interface.py index e1f3e43..3a409a3 100644 --- a/mininterface/textual_interface.py +++ b/mininterface/textual_interface.py @@ -12,7 +12,7 @@ raise InterfaceNotAvailable from .auxiliary import flatten -from .facet import BackendAdaptor +from .facet import BackendAdaptor, Facet, MinFacet from .form_dict import (EnvClass, FormDict, FormDictOrEnv, TagDict, dataclass_to_tagdict, dict_to_tagdict, formdict_resolve, formdict_to_widgetdict) @@ -31,6 +31,10 @@ class DummyWrapper: class TextualInterface(Redirectable, TextInterface): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.facet: TextualAppFacet = TextualAppFacet(self) # since no app is running + def alert(self, text: str) -> None: """ Display the OK dialog with text. """ TextualButtonApp(self).buttons(text, [("Ok", None)]).run() @@ -51,7 +55,7 @@ def form(self, form: FormDictOrEnv | None = None, title: str = "") -> FormDictOr 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(TextualApp(self), dict_to_tagdict(form), title), extract_main=True) + return formdict_resolve(TextualApp.run_dialog(TextualApp(self), dict_to_tagdict(form, self.facet), title), extract_main=True) # NOTE we should implement better, now the user does not know it needs an int def ask_number(self, text: str): @@ -81,7 +85,8 @@ class TextualApp(App[bool | None]): def __init__(self, interface: TextualInterface): super().__init__() - self.title = "" + self.facet = interface.facet + self.title = self.facet._title self.widgets = None self.focused_i: int = 0 self.interface = interface @@ -93,7 +98,7 @@ def widgetize(tag: Tag) -> Checkbox | Input: if tag.annotation is bool or not tag.annotation and (v is True or v is False): o = Checkbox(tag.name or "", v) elif tag._get_choices(): - o = RadioSet(*(RadioButton(ch, value=ch == tag.val) for ch in tag.choices)) + o = RadioSet(*(RadioButton(label, value=val == tag.val) for label, val in tag.choices.items())) else: if not isinstance(v, (float, int, str, bool)): v = str(v) @@ -169,6 +174,8 @@ def on_key(self, event: events.Key) -> None: class TextualButtonApp(App): + """ A helper TextualApp, just for static dialogs, does not inherit from BackendAdaptor and thus has not Facet. """ + CSS = """ Screen { layout: grid; @@ -243,3 +250,13 @@ def on_button_pressed(self, event: Button.Pressed) -> None: def action_exit(self): self.exit() + + +class TextualAppFacet(Facet): + def __init__(self, window: TextualApp): + self.window = window + # Since TextualApp turns off, we need to have its values stored somewhere + self._title = "" + + def set_title(self, title: str): + self._title = title diff --git a/mkdocs.yml b/mkdocs.yml index 05f0245..6e441a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ plugins: show_source: false members_order: source show_bases: false + show_labels: false markdown_extensions: - pymdownx.highlight: @@ -39,6 +40,7 @@ nav: - Run: "run.md" - Mininterface: "Mininterface.md" - Tag: "Tag.md" + - Facet: "Facet.md" - Validation: "Validation.md" - Standalone: "Standalone.md" - Changelog: "Changelog.md" diff --git a/tests/tests.py b/tests/tests.py index e926ed8..e08ea72 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -301,7 +301,7 @@ def test_run_ask_empty(self): self.assertEqual("", stdout.getvalue().strip()) def test_run_ask_for_missing(self): - form = """Asking the form {'token': Tag(val='', description='', annotation=, name='token', validation=not_empty, choices=None)}""" + form = """Asking the form {'token': Tag(val='', description='', annotation=, name='token', validation=not_empty, choices=None, facet=None)}""" # Ask for missing, no interference with ask_on_empty_cli with patch('sys.stdout', new_callable=StringIO) as stdout: run(FurtherEnv2, True, interface=Mininterface) @@ -533,10 +533,15 @@ def test_path_cli(self): self.assertEqual([Path("/var")], f.val) self.assertEqual(['/var'], f._get_ui_val()) - # NOTE Mininterface.choice cannot set the chosen value. - # def test_choice(self): - # m = run(interface=Mininterface) - # m.choice() + def test_choice(self): + m = run(interface=Mininterface) + self.assertIsNone(None, m.choice((1,2,3))) + self.assertEqual(2, m.choice((1,2,3), default=2)) + self.assertEqual(2, m.choice((1,2,3), default=2)) + self.assertEqual(2, m.choice({"one": 1, "two": 2}, default=2)) + self.assertEqual(2, m.choice([Tag(1, name="one"), Tag(2, name="two")],default=2)) + + if __name__ == '__main__':