Skip to content

Commit 689ef15

Browse files
committed
testing coverage
1 parent 9b08a36 commit 689ef15

File tree

9 files changed

+250
-156
lines changed

9 files changed

+250
-156
lines changed

.github/workflows/run-unittest.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ jobs:
1515
- name: Install dependencies
1616
run: pip install -e .
1717
- name: Run tests
18-
run: python3 tests.py
18+
run: python3 tests/tests.py

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Mininterface – access to GUI, TUI, CLI and config files
22
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
3+
[![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/mininterface/actions)
34

45
Write the program core, do not bother with the input/output.
56

mininterface/GuiInterface.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
from .common import InterfaceNotAvailable
14-
from .auxiliary import FormDict, RedirectText, dataclass_to_dict, dict_to_dataclass, recursive_set_focus, normalize_types
14+
from .auxiliary import FormDict, RedirectText, config_to_dict, config_from_dict, recursive_set_focus, normalize_types
1515
from .Mininterface import Cancelled, ConfigInstance, Mininterface
1616

1717

@@ -46,11 +46,11 @@ def ask(self, text: str) -> str:
4646

4747
def ask_args(self) -> ConfigInstance:
4848
""" Display a window form with all parameters. """
49-
params_ = dataclass_to_dict(self.args, self.descriptions)
49+
params_ = config_to_dict(self.args, self.descriptions)
5050

5151
# fetch the dict of dicts values from the form back to the namespace of the dataclasses
5252
data = self.window.run_dialog(params_)
53-
dict_to_dataclass(self.args, data)
53+
config_from_dict(self.args, data)
5454
return self.args
5555

5656
def ask_form(self, args: FormDict, title: str = "") -> dict:

mininterface/TuiInterface.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pprint import pprint
2-
from .auxiliary import ConfigInstance, FormDict, dataclass_to_dict, dict_to_dataclass
2+
from .auxiliary import ConfigInstance, FormDict, config_to_dict, config_from_dict
33
from .Mininterface import Cancelled, Mininterface
44

55

mininterface/auxiliary.py

+44-23
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ def set_error_text(self, s):
4141
""" Nested form that can have descriptions (through Value) instead of plain values. """
4242

4343

44-
def dataclass_to_dict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
44+
def config_to_dict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
4545
""" Convert the dataclass produced by tyro into dict of dicts. """
4646
main = ""
47+
# print(args)# TODO
4748
params = {main: {}} if not _path else {}
4849
for param, val in vars(args).items():
4950
annotation = None
@@ -64,46 +65,66 @@ def dataclass_to_dict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
6465
logger.warn(f"Annotation {wanted_type} of `{param}` not supported by Mininterface."
6566
"None converted to False.")
6667
if hasattr(val, "__dict__"): # nested config hierarchy
67-
params[param] = dataclass_to_dict(val, descr, _path=f"{_path}{param}.")
68+
params[param] = config_to_dict(val, descr, _path=f"{_path}{param}.")
6869
elif not _path: # scalar value in root
6970
params[main][param] = Value(val, descr.get(param), annotation)
7071
else: # scalar value in nested
7172
params[param] = Value(val, descr.get(f"{_path}{param}"), annotation)
73+
# print(params) # TODO
7274
return params
7375

7476

7577
def normalize_types(origin: FormDict, data: dict) -> dict:
7678
""" Run validators of all Value objects. If fails, outputs info.
7779
Return corrected data. (Ex: Some values might be nulled from "".)
7880
"""
79-
for (group, params), params2 in zip(data.items(), origin.values()):
80-
for (key, val), pattern in zip(params.items(), params2.values()):
81-
if isinstance(pattern, Value) and pattern.annotation:
82-
if val == "" and type(None) in get_args(pattern.annotation):
83-
# The user is not able to set the value to None, they left it empty.
84-
# Cast back to None as None is one of the allowed types.
85-
# Ex: `severity: int | None = None`
86-
data[group][key] = val = None
87-
elif pattern.annotation == Optional[int]:
88-
try:
89-
data[group][key] = val = int(val)
90-
except ValueError:
91-
pass
92-
93-
if not isinstance(val, pattern.annotation):
94-
pattern.set_error_text(f"Type must be `{pattern.annotation}`!")
95-
return False
81+
def check(ordict, orkey, orval, dataPos: dict, dataKey, val):
82+
if isinstance(orval, Value) and orval.annotation:
83+
if val == "" and type(None) in get_args(orval.annotation):
84+
# The user is not able to set the value to None, they left it empty.
85+
# Cast back to None as None is one of the allowed types.
86+
# Ex: `severity: int | None = None`
87+
dataPos[dataKey] = val = None
88+
elif orval.annotation == Optional[int]:
89+
try:
90+
dataPos[dataKey] = val = int(val)
91+
except ValueError:
92+
pass
93+
94+
if not isinstance(val, orval.annotation):
95+
orval.set_error_text(f"Type must be `{orval.annotation}`!")
96+
raise RuntimeError # revision needed
97+
98+
# keep values if revision needed
99+
# We merge new data to the origin. If form is re-submitted, the values will stay there.
100+
if isinstance(orval, Value):
101+
orval.val = val
102+
else:
103+
ordict[orkey] = val
104+
105+
try:
106+
for (key1, val1), (orkey1, orval1) in zip(data.items(), origin.items()):
107+
if isinstance(val1, dict): # nested config hierarchy
108+
# NOTE: This allows only single nested dict.
109+
for (key2, val2), (orkey2, orval2) in zip(val1.items(), orval1.items()):
110+
check(orval1, orkey2, orval2, data[key1], key2, val2)
111+
else:
112+
check(origin, orkey1, orval1, data, key1, val1)
113+
except RuntimeError:
114+
return False
115+
96116
return data
97117

98118

99-
def dict_to_dataclass(args: ConfigInstance, data: dict):
100-
""" Convert the dict of dicts from the GUI back into the object holding the configuration. """
119+
def config_from_dict(args: ConfigInstance, data: dict):
120+
""" Fetch back data.
121+
Merge the dict of dicts from the GUI back into the object holding the configuration. """
101122
for group, params in data.items():
102123
for key, val in params.items():
103124
if group:
104-
setattr(getattr(args, group), key, val)
125+
setattr(getattr(args, group), key, val.val if isinstance(val, Value) else val)
105126
else:
106-
setattr(args, key, val)
127+
setattr(args, key, val.val if isinstance(val, Value) else val)
107128

108129

109130
def get_terminal_size():

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "mininterface"
7-
version = "0.1.1"
7+
version = "0.4.0"
88
description = "A minimal access to GUI, TUI, CLI and config"
99
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
1010
license = "GPL-3.0-or-later"

tests.py

-127
This file was deleted.

tests/configs.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from dataclasses import dataclass
2+
3+
@dataclass
4+
class SimpleConfig:
5+
"""Set of options."""
6+
test: bool = False
7+
"""My testing flag"""
8+
important_number: int = 4
9+
"""This number is very important"""
10+
11+
12+
@dataclass
13+
class FurtherConfig1:
14+
token: str = "filled"
15+
host: str = "example.org"
16+
17+
18+
@dataclass
19+
class NestedDefaultedConfig:
20+
further: FurtherConfig1
21+
22+
23+
@dataclass
24+
class FurtherConfig2:
25+
token: str
26+
host: str = "example.org"
27+
28+
@dataclass
29+
class NestedMissingConfig:
30+
further: FurtherConfig2
31+
32+
33+
@dataclass
34+
class FurtherConfig3:
35+
severity: int | None = None
36+
37+
@dataclass
38+
class OptionalFlagConfig:
39+
further: FurtherConfig3
40+
msg: str | None = None
41+
msg2: str | None = "Default text"

0 commit comments

Comments
 (0)