Skip to content

Commit 0b67bcd

Browse files
committed
text interface
1 parent bd048e5 commit 0b67bcd

23 files changed

+469
-212
lines changed

docs/Changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.8.0 (unreleased)
44
* better annotated fetching
55
* SecretTag
6+
* much better TextInterface
67

78
## 0.7.5 (2025-01-29)
89
* UI [configuration](Configuration.md)

mininterface/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pathlib import Path
44
from typing import Literal, Optional, Sequence, Type
55

6+
from .config import Config
7+
68
from .types.alias import Choices, Validation
79

810
from .exceptions import Cancelled, InterfaceNotAvailable
@@ -38,7 +40,8 @@ def run(env_or_list: Type[EnvClass] | list[Type[Command]] | None = None,
3840
config_file: Path | str | bool = True,
3941
add_verbosity: bool = True,
4042
ask_for_missing: bool = True,
41-
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | None = None,
43+
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
44+
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | None = None,
4245
args: Optional[Sequence[str]] = None,
4346
**kwargs) -> Mininterface[EnvClass]:
4447
""" The main access, start here.

mininterface/__main__.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from dataclasses import dataclass
21
import sys
2+
from dataclasses import dataclass
33
from typing import Literal, Optional
4+
45
from tyro.conf import FlagConversionOff
56

7+
from . import Mininterface, run
68
from .exceptions import DependencyRequired
7-
8-
from . import run, Mininterface
9-
from .showcase import showcase
9+
from .showcase import ChosenInterface, showcase
1010

1111
__doc__ = """Simple GUI dialog. Outputs the value the user entered."""
1212

@@ -25,7 +25,6 @@ class Web:
2525
port: int = 64646
2626

2727

28-
InterfaceType = Literal["gui"] | Literal["tui"] | Literal["all"]
2928
Showcase = Literal[1] | Literal[2]
3029

3130

@@ -43,7 +42,7 @@ class CliInteface:
4342
is_no: str = ""
4443
""" Display confirm box, focusing 'no'. """
4544

46-
showcase: Optional[tuple[InterfaceType, Showcase]] = None
45+
showcase: Optional[tuple[ChosenInterface, Showcase]] = None
4746
""" Prints various form just to show what's possible."""
4847

4948

mininterface/cli_parser.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,9 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
256256
if mininterface := disk.pop("mininterface", None):
257257
# Section 'mininterface' in the config file changes the global configuration.
258258
for key, value in vars(_create_with_missing(MininterfaceConfig, mininterface)).items():
259-
setattr(Config, key, value)
260-
kwargs["default"] = _create_with_missing(env, disk)
259+
if value is not MISSING_NONPROP:
260+
setattr(Config, key, value)
261+
kwargs["default"] = _create_with_missing(env, disk)
261262

262263
# Load configuration from CLI
263264
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
@@ -267,6 +268,8 @@ def _create_with_missing(env, disk: dict):
267268
"""
268269
Create a default instance of an Env object. This is due to provent tyro to spawn warnings about missing fields.
269270
Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
271+
272+
The result contains MISSING_NONPROP on the places the original Env object must have a value.
270273
"""
271274

272275
# Determine model

mininterface/config.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Configuration used by all minterfaces in the program.
22
# Might be changed by a 'mininterface' section in a config file.
33
from dataclasses import dataclass
4-
from typing import Literal
4+
from typing import Literal, Optional
5+
6+
# We do not use InterfaceType as a type in run because we want the documentation to show full alias.
7+
InterfaceName = Literal["gui"] | Literal["tui"] | Literal["text"]
58

69

710
@dataclass
@@ -16,13 +19,19 @@ class Tui:
1619
...
1720

1821

22+
@dataclass
23+
class Text:
24+
...
25+
26+
1927
@dataclass # (slots=True)
2028
class MininterfaceConfig:
2129
gui: Gui
2230
tui: Tui
23-
interface: Literal["gui"] | Literal["tui"] | None = None
31+
text: Text
32+
interface: Optional[InterfaceName] = None
2433
""" Enforce an interface. By default, we choose automatically. """
2534

2635

27-
Config = MininterfaceConfig(Gui(), Tui())
36+
Config = MininterfaceConfig(Gui(), Tui(), Text())
2837
""" Global configuration singleton to be accessed by all minterfaces. """

mininterface/facet.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ class BackendAdaptor(ABC):
3434
post_submit_action: Optional[Callable] = None
3535
interface: "Mininterface"
3636

37-
@staticmethod
3837
@abstractmethod
39-
def widgetize(tag: Tag):
38+
def widgetize(self, tag: Tag):
4039
""" Wrap Tag to a UI widget. """
4140
pass
4241

@@ -63,7 +62,7 @@ def __init__(self, interface: "Mininterface"):
6362
self.facet = Facet(self, interface.env)
6463
self.interface = interface
6564

66-
def widgetize(tag: Tag):
65+
def widgetize(self, tag: Tag):
6766
pass
6867

6968
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:

mininterface/form_dict.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def dict_to_tagdict(data: dict, mininterface: Optional["Mininterface"] = None) -
106106
if not isinstance(val, Tag):
107107
tag = Tag(val, "", name=key, _src_dict=data, _src_key=key, **d)
108108
else:
109-
tag = tag_fetch(val, d)
109+
tag = tag_fetch(val, d, key)
110110
tag = tag_assure_type(tag)
111111
fd[key] = tag
112112
return fd
@@ -186,7 +186,7 @@ def dataclass_to_tagdict(env: EnvClass | Type[EnvClass], mininterface: Optional[
186186
if not isinstance(val, Tag):
187187
tag = tag_factory(val, _src_key=param, _src_obj=env, **d)
188188
else:
189-
tag = tag_fetch(val, d)
189+
tag = tag_fetch(val, d, param)
190190
tag = tag_assure_type(tag)
191191
(subdict if _nested else main)[param] = tag
192192
return subdict

mininterface/interfaces.py

+43-32
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,44 @@
33
import sys
44
from typing import Literal, Type
55

6-
7-
from .config import Config
86
from .mininterface import Mininterface
7+
from .config import Config, InterfaceName
98
from .exceptions import InterfaceNotAvailable
10-
from .text_interface import TextInterface
119

12-
# We do not use InterfaceType as a type in run because we want the documentation to show full alias.
13-
InterfaceType = Type[Mininterface] | Literal["gui"] | Literal["tui"] | None
10+
InterfaceType = Type[Mininterface] | InterfaceName | None
1411

1512

1613
def __getattr__(name):
17-
# shortcuts
18-
if name == "GuiInterface":
19-
return __getattr__("TkInterface")
20-
if name == "TuiInterface":
21-
# if textual not installed or isatty False, return TextInterface
22-
return __getattr__("TextualInterface") or TextInterface
23-
24-
# real interfaces
25-
if name == "TkInterface":
26-
try:
27-
globals()[name] = import_module("..tk_interface", __name__).TkInterface
28-
return globals()[name]
29-
except InterfaceNotAvailable:
30-
return None
31-
32-
if name == "TextualInterface":
33-
try:
34-
globals()[name] = import_module("..textual_interface", __name__).TextualInterface
35-
return globals()[name]
36-
except InterfaceNotAvailable:
37-
return None
38-
return None # such attribute does not exist
14+
match name:
15+
# shortcuts
16+
case "GuiInterface":
17+
return __getattr__("TkInterface")
18+
case "TuiInterface":
19+
# if textual not installed or isatty False, return TextInterface
20+
return __getattr__("TextualInterface") or __getattr__("TextInterface")
21+
22+
# real interfaces
23+
case "TextInterface":
24+
try:
25+
globals()[name] = import_module("..text_interface", __name__).TextInterface
26+
return globals()[name]
27+
except InterfaceNotAvailable:
28+
return None
29+
case "TkInterface":
30+
try:
31+
globals()[name] = import_module("..tk_interface", __name__).TkInterface
32+
return globals()[name]
33+
except InterfaceNotAvailable:
34+
return None
35+
36+
case "TextualInterface":
37+
try:
38+
globals()[name] = import_module("..textual_interface", __name__).TextualInterface
39+
return globals()[name]
40+
except InterfaceNotAvailable:
41+
return None
42+
case _:
43+
return None # such attribute does not exist
3944

4045

4146
def get_interface(title="", interface: InterfaceType = None, env=None):
@@ -44,11 +49,17 @@ def get_interface(title="", interface: InterfaceType = None, env=None):
4449
if isinstance(interface, type) and issubclass(interface, Mininterface):
4550
# the user gave a specific interface, let them catch InterfaceNotAvailable then
4651
return interface(*args)
47-
if interface == "gui" or interface is None:
48-
try:
49-
return __getattr__("GuiInterface")(*args)
50-
except InterfaceNotAvailable:
51-
pass
52+
match interface:
53+
case "gui" | None:
54+
try:
55+
return __getattr__("GuiInterface")(*args)
56+
except InterfaceNotAvailable:
57+
pass
58+
case "text":
59+
try:
60+
return __getattr__("TextInterface")(*args)
61+
except InterfaceNotAvailable:
62+
pass
5263
try:
5364
return __getattr__("TuiInterface")(*args)
5465
except InterfaceNotAvailable:

mininterface/mininterface.py

+3
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ def _form(self,
426426
# the original dataclass is updated, hence we do not need to catch the output from launch_callback
427427
adaptor.run_dialog(dataclass_to_tagdict(_form, self), title=title, submit=submit)
428428
return _form
429+
if isinstance(_form, SimpleNamespace) and not vars(_form):
430+
# There is no env, return the empty env. Not well documented.
431+
return self.env
429432
raise ValueError(f"Unknown form input {_form}")
430433

431434
def is_yes(self, text: str) -> bool:

mininterface/showcase.py

+23-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
2+
from datetime import datetime
3+
from pathlib import Path
24
from typing import Annotated, Literal
35

4-
from .types.alias import Validation
6+
from tyro.conf import Positional
57

6-
from . import Tag, run
8+
from .exceptions import ValidationFail
9+
from .subcommands import Command, SubcommandPlaceholder
10+
from .types.rich_tags import SecretTag
11+
12+
from . import run, Choices
13+
from .interfaces import InterfaceName
14+
from .types.alias import Validation
715
from .validators import not_empty
816

9-
from dataclasses import dataclass, field
10-
from pathlib import Path
11-
from mininterface import run
12-
from mininterface.exceptions import ValidationFail
13-
from mininterface.subcommands import Command, SubcommandPlaceholder
14-
from tyro.conf import Positional
17+
18+
ChosenInterface = InterfaceName | Literal["all"]
1519

1620

1721
@dataclass
@@ -72,8 +76,17 @@ class Env:
7276
my_complex: list[tuple[int, str]] = field(default_factory=lambda: [(1, 'foo')])
7377
""" List of tuples. """
7478

79+
my_password: Annotated[str, SecretTag()] = "TOKEN"
80+
""" Masked input """
81+
82+
my_time: datetime = datetime.now()
83+
""" Nice date handling """
84+
85+
my_choice: Annotated[str, Choices("one", "two", "three")] = "two"
86+
""" Choose between values """
87+
7588

76-
def showcase(interface: Literal["gui"] | Literal["tui"] | Literal["all"], case: int):
89+
def showcase(interface: ChosenInterface, case: int):
7790
if interface == "all":
7891
interface = None
7992
kw = {"args": [], "interface": interface}

mininterface/tag.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def __hash__(self):
344344
# Hence, I add a hash function with no intention yet.
345345
return hash(str(self))
346346

347-
def _fetch_from(self, tag: "Self") -> "Self":
347+
def _fetch_from(self, tag: "Self", name: str = "") -> "Self":
348348
""" Fetches attributes from another instance.
349349
(Skips the attributes that are already set.)
350350
"""
@@ -355,6 +355,8 @@ def _fetch_from(self, tag: "Self") -> "Self":
355355
setattr(self, attr, getattr(tag, attr))
356356
if self.description == "":
357357
self.description = tag.description
358+
if name and self.name is None:
359+
self._original_name = self.name = name
358360
return self
359361

360362
def _is_a_callable(self) -> bool:
@@ -486,8 +488,7 @@ def set_error_text(self, s):
486488
self.description = f"{s} {o}"
487489
if self.name:
488490
# Why checking self.name?
489-
# If not set, we would end up with '* None'
490-
# `m.form({"my_text": Tag("", validation=validators.not_empty)})`
491+
# If for any reason (I do not know the use case) is not set, we would end up with '* None'
491492
self.name = f"* {n}"
492493
self._error_text = s
493494

mininterface/tag_factory.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def _get_tag_type(tag: Tag) -> Type[Tag]:
3434
return type(tag)
3535

3636

37-
def tag_fetch(tag: Tag, ref: dict | None):
38-
return tag._fetch_from(Tag(**ref))
37+
def tag_fetch(tag: Tag, ref: dict | None, name: str):
38+
return tag._fetch_from(Tag(**ref), name)
3939

4040

4141
def tag_assure_type(tag: Tag):

0 commit comments

Comments
 (0)