Skip to content

Commit

Permalink
facet
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Sep 11, 2024
1 parent 865a773 commit c3ca5c0
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 27 deletions.
Binary file added asset/facet_backend.avif
Binary file not shown.
Binary file added asset/facet_frontend.avif
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/Facet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Facet
::: mininterface.facet.Facet
2 changes: 2 additions & 0 deletions docs/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Use a common [dataclass](https://docs.python.org/3/library/dataclasses.html#data

To do any advanced things, stick the value to a powerful [`Tag`][mininterface.Tag]. For a validation only, use its [`Validation alias`](/Validation/#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

Expand Down
13 changes: 13 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ m.form(my_dictionary)



















Expand Down
2 changes: 2 additions & 0 deletions mininterface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ class Env:
if "prog" not in kwargs:
kwargs["prog"] = title
try:
if interface == "tui": # undocumented feature
interface = TuiInterface
interface = interface(title, env, descriptions)
except InterfaceNotAvailable: # Fallback to a different interface
interface = TuiInterface(title, env, descriptions)
Expand Down
39 changes: 29 additions & 10 deletions mininterface/facet.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
from abc import ABC, abstractmethod
from typing import overload

from .form_dict import TagDict
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
Expand All @@ -26,3 +18,30 @@ 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. """
...


class MinFacet(Facet):
""" A mininterface needs a facet and the base Facet is abstract and cannot be instanciated. """

def __init__(self, window=None):
pass
22 changes: 16 additions & 6 deletions mininterface/form_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"""
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, Self, TypeVar, Union, get_type_hints


from .tag import Tag, TagValue

if TYPE_CHECKING:
from .facet import Facet

logger = logging.getLogger(__name__)

EnvClass = TypeVar("EnvClass")
Expand Down Expand Up @@ -72,14 +76,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


Expand Down Expand Up @@ -112,7 +122,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.
Expand Down
25 changes: 21 additions & 4 deletions mininterface/gui_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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
Expand Down Expand Up @@ -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]
Expand All @@ -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 = {}
Expand All @@ -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 = []
Expand All @@ -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),
Expand Down Expand Up @@ -193,3 +197,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
19 changes: 18 additions & 1 deletion mininterface/mininterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -59,6 +61,21 @@ class Env:
```
"""

self.facet: Facet = MinFacet()
""" Access to the UI from the back-end side.
(Read [`Tag.facet`][mininterface.Tag.facet] to see how 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 """

Expand Down Expand Up @@ -209,7 +226,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:
Expand Down
36 changes: 34 additions & 2 deletions mininterface/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -162,6 +164,28 @@ class Env:
#
# Following attributes are not meant to be set externally.
#
facet: Optional["Facet"] = None
""" Access to the UI from the front-end side.
(Read [`Mininterface.facet`][mininterface.Mininterface.facet] to see how 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.
Expand All @@ -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 """

Expand All @@ -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
Expand All @@ -226,11 +255,12 @@ 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']:
if getattr(self, attr) is None:
setattr(self, attr, getattr(tag, attr))
return self

def _is_a_callable(self) -> bool:
""" True, if the value is a callable function.
Expand Down Expand Up @@ -325,6 +355,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):
Expand Down
Loading

0 comments on commit c3ca5c0

Please sign in to comment.