Skip to content

Commit dbb171d

Browse files
committed
feat: config file tuple annotation support
1 parent e521590 commit dbb171d

File tree

10 files changed

+502
-144
lines changed

10 files changed

+502
-144
lines changed

docs/Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Changelog
22

3-
## 1.0.3 (unreleased)
3+
## 1.0.3 (2025-06-18)
44
* enh: Tk better file dialog
55
* feat: [Tag.mnemonic][mininterface.Tag.mnemonic]
6+
* feat: config file tuple annotation support
67

78
## 1.0.2 (2025-05-10)
89
* fix: mute TextInterface on Win

docs/Config-file.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Config file
2+
Any settings you see in the `--help` command can be modified via a YAML config file.
3+
4+
By default, we try to find one in the current working dir, whose name stem is the same as the program's. Ex: program.py will search for program.yaml. This behaviour can be changed via the [run][mininterface.run] method.
5+
6+
!!! Tip
7+
You do not have to re-define all the settings in the config file, you can choose a few.
8+
9+
## Basic example
10+
11+
We have this nested structure:
12+
13+
```python
14+
# program.py
15+
@dataclass
16+
class FurtherConfig:
17+
token: str
18+
host: str = "example.org"
19+
20+
@dataclass
21+
class Env:
22+
further: FurtherConfig
23+
24+
...
25+
m = run(Env)
26+
print(m.env.further.host) # example.com
27+
```
28+
29+
The config file being used:
30+
31+
```yaml
32+
# program.yaml
33+
further:
34+
host: example.com
35+
```
36+
37+
## Complex example
38+
39+
Nested container structures are supported too.
40+
41+
```python
42+
from mininterface import run
43+
@dataclass
44+
class ComplexEnv:
45+
a1: dict[int, str]
46+
a2: dict[int, tuple[str, int]]
47+
a3: dict[int, list[str]]
48+
a4: list[int]
49+
a5: tuple[str, int]
50+
a6: list[int | str]
51+
a7: list[tuple[str, float]]
52+
53+
m = run(ComplexEnv)
54+
m.env
55+
# ComplexEnv(
56+
# a1={1: 'a'},
57+
# a2={2: ('b', 22), 3: ('c', 33), 4: ('d', 44)},
58+
# a3={5: ['e', 'ee', 'eee']},
59+
# a4=[6, 7],
60+
# a5=('h', 8),
61+
# a6=['i', 9],
62+
# a7=[('j', 10.0), ('k', 11), ('l', 12)])
63+
```
64+
65+
The YAML file used. (Note the various YAML syntax and the automatic YAML-list to Python-tuple conversion.)
66+
67+
```yaml
68+
{% include-markdown '../tests/complex.yaml' %}
69+
```

mininterface/__init__.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
try:
1717
from ._lib.start import ChooseSubcommandOverview, Start
1818
from .cli import Command, SubcommandPlaceholder
19-
from ._lib.cli_parser import assure_args, parse_cli, parse_config_file, parser_to_dataclass
19+
from ._lib.cli_parser import (
20+
assure_args,
21+
parse_cli,
22+
parse_config_file,
23+
parser_to_dataclass,
24+
)
2025
except DependencyRequired as e:
2126
assure_args, parse_cli, parse_config_file, parser_to_dataclass = (e,) * 4
2227
ChooseSubcommandOverview, Start, SubcommandPlaceholder = (e,) * 3
@@ -27,18 +32,27 @@ class _Empty:
2732
pass
2833

2934

30-
def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | None = None,
31-
ask_on_empty_cli: bool = False,
32-
title: str = "",
33-
config_file: Path | str | bool = True,
34-
add_verbose: bool = True,
35-
ask_for_missing: bool = True,
36-
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
37-
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | Literal["web"] | None = None,
38-
args: Optional[Sequence[str]] = None,
39-
settings: Optional[MininterfaceSettings] = None,
40-
**kwargs) -> Mininterface[EnvClass]:
41-
""" The main access, start here.
35+
def run(
36+
env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | None = None,
37+
ask_on_empty_cli: bool = False,
38+
title: str = "",
39+
config_file: Path | str | bool = True,
40+
add_verbose: bool = True,
41+
ask_for_missing: bool = True,
42+
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
43+
interface: (
44+
Type[Mininterface]
45+
| Literal["gui"]
46+
| Literal["tui"]
47+
| Literal["text"]
48+
| Literal["web"]
49+
| None
50+
) = None,
51+
args: Optional[Sequence[str]] = None,
52+
settings: Optional[MininterfaceSettings] = None,
53+
**kwargs
54+
) -> Mininterface[EnvClass]:
55+
"""The main access, start here.
4256
Wrap your configuration dataclass into `run` to access the interface. An interface is chosen automatically,
4357
with the preference of the graphical one, regressed to a text interface for machines without display.
4458
Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly
@@ -59,9 +73,9 @@ def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | No
5973
```python
6074
@dataclass
6175
class Env:
62-
number: int = 3
63-
text: str = ""
64-
m = run(Env, ask_on_empty=True)
76+
number: int = 3
77+
text: str = ""
78+
m = run(Env, ask_on_empty=True)
6579
```
6680
6781
```bash
@@ -77,6 +91,7 @@ class Env:
7791
whose name stem is the same as the program's.
7892
Ex: `program.py` will search for `program.yaml`.
7993
If False, no config file is used.
94+
See the [Config file](Config-file.md) section.
8095
add_verbose: Adds the verbose flag that automatically sets the level to `logging.INFO` (*-v*) or `logging.DEBUG` (*-vv*).
8196
8297
```python
@@ -203,18 +218,24 @@ class Env:
203218
if superform_args is not None:
204219
# Run Superform as multiple subcommands exist and we have to decide which one to run.
205220
m = get_interface(interface, title, settings, None)
206-
ChooseSubcommandOverview(env_or_list, m, args=superform_args, ask_for_missing=ask_for_missing)
221+
ChooseSubcommandOverview(
222+
env_or_list, m, args=superform_args, ask_for_missing=ask_for_missing
223+
)
207224
return m # m with added `m.env`
208225

209226
# B) A single Env object, or a list of such objects (with one is being selected via args)
210227
# C) No Env object
211228

212229
# Parse CLI arguments, possibly merged from a config file.
213-
kwargs, settings = parse_config_file(env_or_list or _Empty, config_file, settings, **kwargs)
230+
kwargs, settings = parse_config_file(
231+
env_or_list or _Empty, config_file, settings, **kwargs
232+
)
214233
if env_or_list:
215234
# B) single Env object
216235
# Load configuration from CLI and a config file
217-
env, wrong_fields = parse_cli(env_or_list, kwargs, add_verbose, ask_for_missing, args)
236+
env, wrong_fields = parse_cli(
237+
env_or_list, kwargs, add_verbose, ask_for_missing, args
238+
)
218239
m = get_interface(interface, title, settings, env)
219240

220241
# Empty CLI → GUI edit
@@ -243,6 +264,4 @@ class Env:
243264
return m
244265

245266

246-
__all__ = ["run", "Mininterface", "Tag",
247-
"Cancelled",
248-
"Validation", "Options"]
267+
__all__ = ["run", "Mininterface", "Tag", "Cancelled", "Validation", "Options"]

0 commit comments

Comments
 (0)