Skip to content

Commit

Permalink
missing attrs with underscores WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Sep 25, 2024
1 parent b6445be commit 99f5485
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", 3.11, 3.12]
python-version: [3.12]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down
21 changes: 18 additions & 3 deletions docs/Overview.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
Via the [run][mininterface.run] function you get access to the CLI, possibly enriched from the config file. Then, you receive all data as [`m.env`][mininterface.Mininterface.env] object and dialog methods in a proper UI.

```mermaid
graph LR
subgraph mininterface
run --> GUI
run --> TUI
run --> env
CLI --> run
id1[config file] --> CLI
end
program --> run
```

## 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] method 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].
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].

To do any advanced things, stick the value to a powerful [`Tag`][mininterface.Tag] or its subclassed [types][mininterface.types]. Ex. for a validation only, use its [`Validation alias`](Validation.md/#validation-alias).

Expand Down Expand Up @@ -87,11 +101,12 @@ class FurtherConfig:
host: str = "example.org"

@dataclass
class Config:
class Env:
further: FurtherConfig

...
print(config.further.host) # example.org
m = run(Env)
print(m.env.further.host) # example.org
```

The attributes can by defaulted by CLI:
Expand Down
26 changes: 17 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ from mininterface import run

@dataclass
class Env:
"""Set of options."""
""" This calculates something. """

test: bool = False
""" My testing flag """
my_flag: bool = False
""" This switches the functionality """

important_number: int = 4
my_number: int = 4
""" This number is very important """

if __name__ == "__main__":
env = run(Env, prog="My application").env
# Attributes are suggested by the IDE
# along with the hint text 'This number is very important'.
print(env.important_number)
print(env.my_number)
```

# Contents
Expand All @@ -42,11 +42,14 @@ if __name__ == "__main__":
## You got CLI
It was all the code you need. No lengthy blocks of code imposed by an external dependency. Besides the GUI/TUI, you receive powerful YAML-configurable CLI parsing.

TODO regenerate output and all the images
TODO it seems that input missing dataclass fields is broken, it just shows it must be set via cli

```bash
$ ./hello.py
usage: My application [-h] [--test | --no-test] [--important-number INT]

Set of options.
This calculates something.

╭─ options ──────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit
Expand All @@ -59,15 +62,15 @@ Set of options.
Loading config file is a piece of cake. Alongside `program.py`, put `program.yaml` and put there some of the arguments. They are seamlessly taken as defaults.

```yaml
important_number: 555
my_number: 555
```
## You got dialogues
Check out several useful methods to handle user dialogues. Here we bound the interface to a `with` statement that redirects stdout directly to the window.

```python
with run(Env) as m:
print(f"Your important number is {m.env.important_number}")
print(f"Your important number is {m.env.my_number}")
boolean = m.is_yes("Is that alright?")
```

Expand All @@ -82,7 +85,7 @@ Writing a small and useful program might be a task that takes fifteen minutes. A

The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
* Call it as `program.py --help` to display full help.
* Use any flag in CLI: `program.py --test` causes `env.test` be set to `True`.
* Use any flag in CLI: `program.py --my-flag` causes `env.my_flag` be set to `True`.
* The main benefit: Launch it without parameters as `program.py` to get a full working window with all the flags ready to be edited.
* Running on a remote machine? Automatic regression to the text interface.

Expand Down Expand Up @@ -271,6 +274,11 @@ m.form(my_dictionary)











Expand Down
2 changes: 1 addition & 1 deletion mininterface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class Env:
```python
@dataclass
class Env:
required_number: int
required_number: int
m = run(Env, ask_for_missing=True)
```
Expand Down
1 change: 0 additions & 1 deletion mininterface/gui_interface/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ def _fetch(variable):
# File dialog
if path_tag := tag._morph(PathTag, Path):
grid_info = widget.grid_info()
master.grid(row=grid_info['row'], column=grid_info['column'])

widget2 = Button(master, text='👓', command=choose_file_handler(variable, path_tag))
widget2.grid(row=grid_info['row'], column=grid_info['column']+1)
Expand Down
19 changes: 17 additions & 2 deletions mininterface/mininterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,22 @@ def ask(self, text: str) -> str:
raise Cancelled(".. cancelled")

def ask_number(self, text: str) -> int:
""" Prompt the user to input a number. Empty input = 0. """
""" Prompt the user to input a number. Empty input = 0.
```python
m = run() # receives a Mininterface object
m.ask_number("What's your age?")
```
![Ask number dialog](asset/standalone_number.avif)
Args:
text: The question text.
Returns:
Number
"""
print("Asking number", text)
return 0

Expand All @@ -110,7 +125,7 @@ def choice(self, choices: ChoicesType, title: str = "", _guesses=None,
Either put options in an iterable:
```python
from mininterface import run, Tag
from mininterface import run
m = run()
m.choice([1, 2])
```
Expand Down
1 change: 0 additions & 1 deletion mininterface/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,6 @@ class Env:
return isinstance(val, origin) and all(isinstance(item, subtype) for item in val)

def _is_subclass(self, class_type):
print("350: self.annotation", self.annotation) # TODO
try:
return issubclass(self.annotation, class_type)
except TypeError: # None, Union etc cast an error
Expand Down
10 changes: 6 additions & 4 deletions mininterface/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
from typing import Callable#, override
from typing_extensions import Self
from .tag import Tag, ValidationResult, TagValue

Expand Down Expand Up @@ -58,7 +58,7 @@ class PathTag(Tag):
![File picker](asset/file_picker.avif)
"""
# NOTE turn SubmitButton into a Tag too and turn this into a types module.
# NOTE Missing in textual. Might implement file filter and be used for validation.
# NOTE Missing in textual. Might implement file filter and be used for validation. (ex: file_exist, is_dir)
# NOTE Path multiple is not recognized: "File 4": Tag([], annotation=list[Path])
multiple: str = False
""" The user can select multiple files. """
Expand All @@ -67,5 +67,7 @@ def __post_init__(self):
super().__post_init__()
self.annotation = list[Path] if self.multiple else Path

def _morph(self, *_):
return self
# @override
def _morph(self, class_type: "Self", morph_if: type):
if class_type == PathTag:
return self
7 changes: 6 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ theme:

plugins:
- search
- mermaid2
- mkdocstrings:
handlers:
python:
Expand All @@ -29,7 +30,11 @@ markdown_extensions:
- pymdownx.snippets
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.superfences:
custom_fences: # make exceptions to highlighting of code:
- name: mermaid
class: mermaid
format: !!python/name:mermaid2.fence_mermaid_custom
- pymdownx.keys # keyboard keys
- tables
- footnotes
Expand Down
5 changes: 5 additions & 0 deletions tests/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class FurtherEnv2:
token: str
host: str = "example.org"

@dataclass
class MissingUnderscore:
token_underscore: str
host: str = "example.org"


@dataclass
class NestedMissingEnv:
Expand Down

0 comments on commit 99f5485

Please sign in to comment.