Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: CZ-NIC/mininterface
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: CZ-NIC/mininterface
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: refs/heads/dev
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 2 commits
  • 43 files changed
  • 1 contributor

Commits on Mar 28, 2025

  1. fix: textual widgets

    e3rd committed Mar 28, 2025
    Copy the full SHA
    0956133 View commit details
  2. UI options

    e3rd committed Mar 28, 2025
    Copy the full SHA
    27beba9 View commit details
Showing with 799 additions and 480 deletions.
  1. +3 −1 docs/Changelog.md
  2. +1 −1 docs/Facet.md
  3. +7 −4 docs/Interfaces.md
  4. +27 −7 docs/{Configuration.md → Options.md}
  5. +1 −1 docs/Overview.md
  6. +2 −1 docs/Types.md
  7. +1 −1 docs/index.md
  8. +14 −9 mininterface/__init__.py
  9. +53 −21 mininterface/cli_parser.py
  10. +0 −37 mininterface/config.py
  11. +49 −17 mininterface/interfaces.py
  12. +34 −24 mininterface/{mininterface.py → mininterface/__init__.py}
  13. +64 −0 mininterface/mininterface/adaptor.py
  14. +6 −49 mininterface/{ → mininterface}/facet.py
  15. +55 −0 mininterface/options.py
  16. +3 −3 mininterface/start.py
  17. +28 −0 mininterface/static.py
  18. +2 −2 mininterface/subcommands.py
  19. +28 −4 mininterface/tag.py
  20. 0 mininterface/text_interface.py
  21. +3 −9 mininterface/text_interface/__init__.py
  22. +41 −43 mininterface/text_interface/adaptor.py
  23. +1 −1 mininterface/text_interface/facet.py
  24. +7 −4 mininterface/textual_interface/__init__.py
  25. +92 −0 mininterface/textual_interface/adaptor.py
  26. +2 −2 mininterface/textual_interface/{textual_facet.py → facet.py}
  27. +3 −5 mininterface/textual_interface/file_picker_input.py
  28. +0 −94 mininterface/textual_interface/textual_adaptor.py
  29. +1 −1 mininterface/textual_interface/textual_app.py
  30. +1 −1 mininterface/textual_interface/textual_button_app.py
  31. +7 −12 mininterface/textual_interface/widgets.py
  32. +11 −12 mininterface/tk_interface/__init__.py
  33. +21 −10 mininterface/tk_interface/{tk_window.py → adaptor.py}
  34. +2 −2 mininterface/tk_interface/date_entry.py
  35. +3 −3 mininterface/tk_interface/{tk_facet.py → facet.py}
  36. +64 −68 mininterface/tk_interface/utils.py
  37. +8 −0 mininterface/types/internal.py
  38. +20 −6 mininterface/web_interface/__init__.py
  39. +13 −19 mininterface/web_interface/child_adaptor.py
  40. +12 −3 mininterface/web_interface/parent_adaptor.py
  41. +1 −1 mkdocs.yml
  42. +58 −0 tests/dumb_options.py
  43. +50 −2 tests/tests.py
4 changes: 3 additions & 1 deletion docs/Changelog.md
Original file line number Diff line number Diff line change
@@ -5,9 +5,11 @@
* SecretTag
* much better TextInterface
* experimental WebInterface
* PathTag UI in TextualInterface
* UI options available from the program

## 0.7.5 (2025-01-29)
* UI [configuration](Configuration.md)
* UI [options](Options.md)
* experimental [Facet._layout](Facet.md#layout)

## 0.7.4 (2025-01-27)
2 changes: 1 addition & 1 deletion docs/Facet.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Facet
::: mininterface.facet.Facet
::: mininterface.mininterface.facet.Facet

## Layout

11 changes: 7 additions & 4 deletions docs/Interfaces.md
Original file line number Diff line number Diff line change
@@ -14,10 +14,17 @@ with TuiInterface("My program") as m:
number = m.ask_number("Returns number")
```

# `Mininterface`

When a GUI is not available (GuiInterface), nor the rich TUI is available (TextualInterface), nor the mere interactive TextInterface is available, the original non-interactive Mininterface is used. The ensures the program is still working in cron jobs etc.


# `GuiInterface` or `TkInterface` or 'gui'

A tkinter window.

It inherits from [`GuiOptions`][mininterface.options.GuiOptions]

# `TuiInterface` or 'tui'

An interactive terminal.
@@ -85,7 +92,3 @@ $ mininterface --web.cmd ./program.py --web.port 9997
# `ReplInterface`

A debug terminal. Invokes a breakpoint after every dialog.

# `Mininterface`

When a GUI is not available (GuiInterface), nor the rich TUI is available (TextualInterface), nor the mere interactive TextInterface is available, the original non-interactive Mininterface is used. The ensures the program is still working in cron jobs etc.
34 changes: 27 additions & 7 deletions docs/Configuration.md → docs/Options.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## Special section
# UI Options

The UI behaviour might be modified via an options object. This can be passed to the [run][mininterface.run] function or defined through a config file. Options defined in the config file have bigger priority. Every interface has its own options object.

# Config file special section
In a YAML config file, use a special section 'mininterface' to set up the UI. For example, this stub will enforce your program to use the Tui interface.

```yaml
@@ -36,9 +40,25 @@ The difference when using such configuration file.

![Configuration not used](asset/configuration-not-used.avif) ![Configuration used](asset/configuration-used.avif)

::: mininterface.config
options:
members:
- MininterfaceConfig
- Gui
- Tui
# The options object

```python
from mininterface.options import MininterfaceOptions
opt = MininterfaceOptions()
run(options=opt)
```


::: mininterface.options.MininterfaceOptions

## GuiOptions

```python
from mininterface.options import MininterfaceOptions, GuiOptions
opt = MininterfaceOptions(gui=GuiOptions(combobox_since=1))
run(options=opt)
```

::: mininterface.options.GuiOptions
2 changes: 1 addition & 1 deletion docs/Overview.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ graph LR
## 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] function 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].

There are a lot of supported [types](Types.md) you can use, not only scalars and well-known objects (`Path`, `datetime`), but also functions, iterables (like `list[Path]`) and union types (like `int | None`). To do even more advanced things, stick the value to a powerful [`Tag`][mininterface.Tag] or its subclasses. Ex. for a validation only, use its [`Validation alias`][mininterface.types.Validation].
There are a lot of supported [types](Types.md) you can use, not only scalars and well-known objects (`Path`, `datetime`), but also functions, iterables (like `list[Path]`) and union types (like `int | None`). To do even more advanced things, stick the value to a powerful [`Tag`][mininterface.Tag] or its subclasses. Ex. for a validation only, use its [`Validation alias`][mininterface.types.alias.Validation].

At last, use [`Facet`](Facet.md) to tackle the interface from the back-end (`m`) or the front-end (`Tag`) side.

3 changes: 2 additions & 1 deletion docs/Types.md
Original file line number Diff line number Diff line change
@@ -68,4 +68,5 @@ print(values["my_point"].i) # 100
![GUI window](asset/supported_types_2.avif "A prompted dialog after editation")


::: mininterface.types
::: mininterface.types.rich_tags
::: mininterface.types.alias
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ Should you need just the CLI part and you are happy with basic text dialogs, use

```bash
pip install --no-dependencies mininterface
pip install tyro typing_extensions pyyaml
pip install tyro typing_extensions pyyaml simple_term_menu
```

## MacOS GUI
23 changes: 14 additions & 9 deletions mininterface/__init__.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from pathlib import Path
from typing import Literal, Optional, Sequence, Type

from .config import Config
from .options import MininterfaceOptions

from .types.alias import Choices, Validation

@@ -13,7 +13,7 @@
from .interfaces import get_interface

from . import validators
from .cli_parser import parse_cli, assure_args
from .cli_parser import parse_config_file, assure_args, parse_cli
from .subcommands import Command, SubcommandPlaceholder
from .form_dict import DataClass, EnvClass
from .mininterface import EnvClass, Mininterface
@@ -44,6 +44,7 @@ def run(env_or_list: Type[EnvClass] | list[Type[Command]] | None = None,
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | None = None,
args: Optional[Sequence[str]] = None,
options: Optional[MininterfaceOptions] = None,
**kwargs) -> Mininterface[EnvClass]:
""" The main access, start here.
Wrap your configuration dataclass into `run` to access the interface. An interface is chosen automatically,
@@ -119,6 +120,7 @@ class Env:
see the full [list](Interfaces.md) of possible interfaces.
If not set, we look also for an environment variable MININTERFACE_INTERFACE and in the config file.
args: Parse arguments from a sequence instead of the command line.
options: Default options. These might be further modified by the 'mininterface' section in the config file.
Kwargs:
The same as for [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html).
@@ -190,16 +192,19 @@ class Env:
start.choose_subcommand(env_or_list, args=args[1:])
elif isinstance(env_or_list, list) and not args:
start.choose_subcommand(env_or_list)
elif env_or_list:
# Load configuration from CLI and a config file
env, wrong_fields = parse_cli(env_or_list, config_file, add_verbosity, ask_for_missing, args, **kwargs)
else: # even though there is no configuration, yet we need to parse CLI for meta-commands like --help or --verbose
parse_cli(_Empty, config_file, add_verbosity, ask_for_missing, args)
else:
# Parse CLI arguments, possibly merged from a config file.
kwargs, options = parse_config_file(env_or_list or _Empty, config_file, options, **kwargs)
if env_or_list:
# Load configuration from CLI and a config file
env, wrong_fields = parse_cli(env_or_list, kwargs, add_verbosity, ask_for_missing, args)
else: # even though there is no configuration, yet we need to parse CLI for meta-commands like --help or --verbose
parse_cli(_Empty, {}, add_verbosity, ask_for_missing, args)

# Build the interface
if i_ := os.environ.get("MININTERFACE_ENFORCED_WEB"):
if os.environ.get("MININTERFACE_ENFORCED_WEB"):
interface = "web"
m = get_interface(title, interface, env)
m = get_interface(title, interface, env, options)

# Empty CLI → GUI edit
if ask_for_missing and wrong_fields:
74 changes: 53 additions & 21 deletions mininterface/cli_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#
# CLI and config file parsing.
#
from dataclasses import dataclass, fields, asdict
import logging
import sys
import warnings
@@ -18,9 +19,13 @@
from tyro._singleton import MISSING_NONPROP
from tyro.extras import get_parser

from mininterface.static import merge_dicts

from .static import dataclass_asdict_no_defaults


from .auxiliary import yield_annotations
from .config import MininterfaceConfig, Config
from .options import GuiOptions, MininterfaceOptions, TextOptions, TextualOptions, WebOptions
from .form_dict import EnvClass, MissingTagValue
from .tag import Tag
from .tag_factory import tag_factory
@@ -101,11 +106,12 @@ def assure_args(args: Optional[Sequence[str]] = None):
return args


def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
kwargs: dict,
add_verbosity: bool,
ask_for_missing: bool,
args: Optional[Sequence[str]] = None) -> tuple[EnvClass, WrongFields]:
def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
kwargs: dict,
add_verbosity: bool = True,
ask_for_missing: bool = True,
args: Optional[Sequence[str]] = None) -> tuple[EnvClass, WrongFields]:
""" Run the tyro parser to fetch program configuration from CLI """
type_form = env_or_list
if isinstance(type_form, list):
# We have to convert the list of possible classes (subcommands) to union for tyro.
@@ -218,23 +224,22 @@ def set_default(kwargs, field_name, val):
setattr(kwargs["default"], field_name, val)


def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
config_file: Path | None = None,
add_verbosity=True,
ask_for_missing=True,
args=None,
**kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
""" Parse CLI arguments, possibly merged from a config file.
def parse_config_file(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
config_file: Path | None = None,
options: Optional[MininterfaceOptions] = None,
**kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
""" Fetches the config file into the program defaults kwargs["default"] and UI options.
Args:
env_class: Class with the configuration.
config_file: File to load YAML to be merged with the configuration.
You do not have to re-define all the settings in the config file, you can choose a few.
options: Used to complement the 'mininterface' config file section-
Kwargs:
The same as for argparse.ArgumentParser.
Returns:
Configuration namespace.
Tuple of kwargs and options.
"""
if isinstance(env_or_list, list):
subcommands, env = env_or_list, None
@@ -253,15 +258,42 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
# 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 = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
if mininterface := disk.pop("mininterface", None):
# Section 'mininterface' in the config file changes the global configuration.
for key, value in vars(_create_with_missing(MininterfaceConfig, mininterface)).items():
if value is not MISSING_NONPROP:
setattr(Config, key, value)
if confopt := disk.pop("mininterface", None):
# Section 'mininterface' in the config file.
options = _merge_options(options, confopt)

kwargs["default"] = _create_with_missing(env, disk)

# Load configuration from CLI
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
return kwargs, options


def _merge_options(runopt: MininterfaceOptions | None, confopt: dict, _def_fact=MininterfaceOptions):
# Options inheritance:
# Config file > program-given through run(options=) > the default options (original dataclasses)

# Assure the default options
# Either the program-given or create fresh defaults
if runopt:
# Merge the program-given options to the config file options if not yet present.
confopt = merge_dicts(dataclass_asdict_no_defaults(runopt), confopt)
else:
runopt = _def_fact()

# Merge option sections.
# Ex: TextOptions will derive from both Tui and Ui. You may specify a Tui default value, common for all Tui interfaces.
for sources in [("ui", "gui"),
("ui", "tui"),
("ui", "tui", "textual"),
("ui", "tui", "text"),
("ui", "tui", "textual", "web"),
]:
target = sources[-1]
confopt[target] = {**{k: v for s in sources for k, v in confopt.get(s, {}).items()}, **confopt.get(target, {})}

for key, value in vars(_create_with_missing(_def_fact, confopt)).items():
if value is not MISSING_NONPROP:
setattr(runopt, key, value)
return runopt


def _create_with_missing(env, disk: dict):
37 changes: 0 additions & 37 deletions mininterface/config.py

This file was deleted.

Loading