Skip to content

Commit

Permalink
gui descriptions back to the bottom
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Jan 8, 2025
1 parent 2954a93 commit 7defc95
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 32 deletions.
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ pip install --no-dependencies mininterface
pip install tyro typing_extensions pyyaml
```

## MacOS GUI

If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`

# Docs
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).

Expand Down
55 changes: 33 additions & 22 deletions mininterface/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
if ask_for_missing and getattr(e, "code", None) == 2 and eavesdrop:
# Some required arguments are missing. Determine which.
wf = {}
for arg in eavesdrop.partition(":")[2].strip().split(", "):
for arg in _fetch_eavesdrop_args():
treat_missing(type_form, kwargs, parser, wf, arg)

# Second attempt to parse CLI
Expand Down Expand Up @@ -195,7 +195,6 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
# However, the UI then is not able to use ex. the number filtering capabilities.
# Putting there None is not a good idea as dataclass_to_tagdict fails if None is not allowed by the annotation.
tag = wf[field_name] = tag_factory(MissingTagValue(),
# tag = wf[field_name] = tag_factory(MISSING,
argument.help.replace("(required)", ""),
validation=not_empty,
_src_class=env_class,
Expand All @@ -205,9 +204,17 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
# A None would be enough because Mininterface will ask for the missing values
# promply, however, Pydantic model would fail.
# As it serves only for tyro parsing and the field is marked wrong, the made up value is never used or seen.
if "default" not in kwargs:
kwargs["default"] = SimpleNamespace()
setattr(kwargs["default"], field_name, tag._make_default_value())
set_default(kwargs, field_name, tag._make_default_value())


def _fetch_eavesdrop_args():
return eavesdrop.partition(":")[2].strip().split(", ")


def set_default(kwargs, field_name, val):
if "default" not in kwargs:
kwargs["default"] = SimpleNamespace()
setattr(kwargs["default"], field_name, val)


def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
Expand All @@ -228,45 +235,49 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
Returns:
Configuration namespace.
"""
if isinstance(env_or_list, list):
subcommands, env = env_or_list, None
else:
subcommands, env = None, env_or_list

# Load config file
if config_file and isinstance(env_or_list, list):
# NOTE. Reading config files when using subcommands is not implemented.
if config_file and subcommands:
# Reading config files when using subcommands is not implemented.
static = {}
kwargs["default"] = None
warnings.warn(f"Config file {config_file} is ignored because subcommands are used."
"It is not easy to set who this should work. "
"Describe the developer your usecase so that they might implement this.")
if "default" not in kwargs and not isinstance(env_or_list, list):
" It is not easy to set how this should work."
" Describe the developer your usecase so that they might implement this.")
if "default" not in kwargs and not subcommands:
# Undocumented feature. User put a namespace into kwargs["default"]
# that already serves for defaults. We do not fetch defaults yet from a config file.
disk = {}
if config_file:
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
# Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
for key in (key for key, val in disk.items() if isinstance(val, dict)):
disk[key] = env_or_list.__annotations__[key](**disk[key])
disk[key] = env.__annotations__[key](**disk[key])

# Fill default fields
if pydantic and issubclass(env_or_list, BaseModel):
if pydantic and issubclass(env, BaseModel):
# Unfortunately, pydantic needs to fill the default with the actual values,
# the default value takes the precedence over the hard coded one, even if missing.
static = {key: env_or_list.model_fields.get(key).default
for ann in yield_annotations(env_or_list) for key in ann if not key.startswith("__") and not key in disk}
# static = {key: env_or_list.model_fields.get(key).default
# for key, _ in iterate_attributes(env_or_list) if not key in disk}
elif attr and attr.has(env_or_list):
static = {key: env.model_fields.get(key).default
for ann in yield_annotations(env) for key in ann if not key.startswith("__") and not key in disk}
# static = {key: env_.model_fields.get(key).default
# for key, _ in iterate_attributes(env_) if not key in disk}
elif attr and attr.has(env):
# Unfortunately, attrs needs to fill the default with the actual values,
# the default value takes the precedence over the hard coded one, even if missing.
# NOTE Might not work for inherited models.
static = {key: field.default
for key, field in attr.fields_dict(env_or_list).items() if not key.startswith("__") and not key in disk}
for key, field in attr.fields_dict(env).items() if not key.startswith("__") and not key in disk}
else:
# To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
# Otherwise, tyro will spawn warnings about missing fields.
static = {key: val
for key, val in yield_defaults(env_or_list) if not key.startswith("__") and not key in disk}
kwargs["default"] = SimpleNamespace(**(disk | static))
for key, val in yield_defaults(env) if not key.startswith("__") and not key in disk}
kwargs["default"] = SimpleNamespace(**(static | disk))

# Load configuration from CLI
env, wrong_fields = run_tyro_parser(env_or_list, kwargs, add_verbosity, ask_for_missing, args)
return env, wrong_fields
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
73 changes: 73 additions & 0 deletions mininterface/tk_interface/external_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# The purpose of the file is to put the descriptions to the bottom of the widgets as it was in the former version of the tkinter_form.
from tkinter import ttk

from tkinter_form import Form, Value, FieldForm

orig = Form._Form__create_widgets


def __create_widgets_monkeypatched(
self, form_dict: dict, name_config: str, button_command: callable
) -> None:
"""
Create form widgets
Args:
form_dict (dict): form dict base
name_config (str): name_config
button (bool): button_config
"""

index = 0
for _, (name_key, value) in enumerate(form_dict.items()):
index += 1
description = None
if isinstance(value, Value):
value, description = value.val, value.description

self.rowconfigure(index, weight=1)

if isinstance(value, dict):
widget = Form(self, name_key, value)
widget.grid(row=index, column=0, columnspan=3, sticky="nesw")

self.fields[name_key] = widget
last_index = index
continue

variable = self._Form__type_vars[type(value)]()
widget = self._Form__type_widgets[type(value)](self)

self.columnconfigure(1, weight=1)
widget.grid(row=index, column=1, sticky="nesw", padx=2, pady=2)
label = ttk.Label(self, text=name_key)
self.columnconfigure(0, weight=1)
label.grid(row=index, column=0, sticky="nes", padx=2, pady=2)

# Add a further description to the row below the widget
description_label = None
if not description is None:
index += 1
description_label = ttk.Label(self, text=description)
description_label.grid(row=index, column=1, columnspan=2, sticky="nesw", padx=2, pady=2)

self.fields[name_key] = FieldForm(
master=self,
label=label,
widget=widget,
variable=variable,
value=value,
description=description_label,
)

last_index = index

if button_command:
self._Form__command = button_command
self.button = ttk.Button(
self, text=name_config, command=self._Form__command_button
)
self.button.grid(row=last_index + 1, column=0, columnspan=3, sticky="nesw")


Form._Form__create_widgets = __create_widgets_monkeypatched
1 change: 1 addition & 0 deletions mininterface/tk_interface/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ..tag import Tag
from ..types import DatetimeTag, PathTag
from .date_entry import DateEntryFrame
from .external_fix import __create_widgets_monkeypatched

if TYPE_CHECKING:
from tk_window import TkWindow
Expand Down
13 changes: 3 additions & 10 deletions mininterface/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,7 @@ def __post_init__(self):
@dataclass(repr=False)
class DatetimeTag(Tag):
"""
!!! warning
Experimental. Still in development.
Datetime is supported.
Datetime, date and time types are supported.
```python3
from datetime import datetime
Expand Down Expand Up @@ -214,13 +211,12 @@ class Env:
# ![Time only](asset/datetime_time.avif)

# NOTE: It would be nice we might put any date format to be parsed.
# NOTE: The parameters are still ignored.

date: bool = False
""" The date part is active """
""" The date part is active. True for datetime and date. """

time: bool = False
""" The time part is active """
""" The time part is active. True for datetime and time. """

full_precision: bool = False
""" Include full time precison, seconds, microseconds. """
Expand All @@ -230,9 +226,6 @@ def __post_init__(self):
if self.annotation:
self.date = issubclass(self.annotation, date)
self.time = issubclass(self.annotation, time) or issubclass(self.annotation, datetime)
# NOTE: remove
# if not self.time and self.full_precision:
# self.full_precision = False

def _make_default_value(self):
return datetime.now()

0 comments on commit 7defc95

Please sign in to comment.