From 29d1bc0fd6a1cf0bc7dd85dd7c6fb8153601872a Mon Sep 17 00:00:00 2001 From: Edvard Rejthar Date: Thu, 7 Nov 2024 21:22:14 +0100 Subject: [PATCH] WIP3 --- mininterface/cli_parser.py | 17 +------------- mininterface/common.py | 45 +++++++++++++++++++++++++++++++++++-- mininterface/tag.py | 3 --- mininterface/tag_factory.py | 13 ----------- tests/configs.py | 21 ++++++++--------- tests/tests.py | 29 +++++++++++++++++++----- 6 files changed, 79 insertions(+), 49 deletions(-) diff --git a/mininterface/cli_parser.py b/mininterface/cli_parser.py index 9ace96a..210cfad 100644 --- a/mininterface/cli_parser.py +++ b/mininterface/cli_parser.py @@ -166,23 +166,8 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]], raise -# TODO move this help somewhere -# -# `./program.py` -> UI started with choose_subcommand -# `./program.py subcommand --flag` -> special class SubcommandPlaceholder allows using flag while still starting UI with choose_subcommand -# `./program.py subcommand1 --flag` -> program run -# `./program.py subcommand1` -> fails with tyro now # NOTE nice to have implemented -# @dataclass -# class SharedArgs: -# foo: int -# @dataclass -# class Subcommand1(SharedArgs): -# a: int = 1 -# @dataclass -# class Subcommand2(SharedArgs): -# b: int -# subcommand = run(Subcommand1 | Subcommand2) def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg: str): + """ See the [mininterface.common.Subcommand] for CLI expectation """ if arg.startswith("{"): # we should never come here, as treating missing subcommand should be treated by run/start.choose_subcommand return diff --git a/mininterface/common.py b/mininterface/common.py index 4843d90..7458b2e 100644 --- a/mininterface/common.py +++ b/mininterface/common.py @@ -4,11 +4,12 @@ # TODO create an exceptions.py + @dataclass class Command(ABC): """ The Command is automatically run while instantanied. TODO Example - docs + Experimental. Should it receive facet? (It cannot be called from the Mininterface init then, but after the adaptor init.) @@ -20,6 +21,44 @@ class Command(ABC): Alternative to argparse [subcommands](https://docs.python.org/3/library/argparse.html#sub-commands). from tyro.conf import Positional + + The CLI behaviour: + * `./program.py` -> UI started with choose_subcommand + * `./program.py subcommand --flag` -> special class SubcommandPlaceholder allows using flag + while still starting UI with choose_subcommand + * `./program.py subcommand1 --flag` -> program run + * `./program.py subcommand1` -> fails with tyro now # NOTE nice to have implemented + + + An example of Command usage: + + ```python + @dataclass + class SharedArgs: + common: int + files: Positional[list[Path]] = field(default_factory=list) + + def __post_init__(self): + self.internal = "value" + + @dataclass + class Subcommand1(SharedArgs): + my_local: int = 1 + + def run(self): + print("Common", self.common) # user input + print("Number", self.my_local) # 1 or user input + ValidationFail("The submit button blocked!") + + @dataclass + class Subcommand2(SharedArgs): + def run(self): + print("Common files", self.files) + + subcommand = run(Subcommand1 | Subcommand2) + ``` + + TODO img """ # Why to not document the Subcommand in the Subcommand class itself? It would be output to the user with --help, # I need the text to be available to the developer in the docs, not to the user. @@ -48,12 +87,14 @@ class Cancelled(SystemExit): # We inherit from SystemExit so that the program exits without a traceback on ex. GUI escape. pass + class ValidationFail(ValueError): """ Signal to the form that submit failed and we want to restore it. - TODO + TODO example """ pass + class DependencyRequired(ImportError): def __init__(self, extras_name): super().__init__(extras_name) diff --git a/mininterface/tag.py b/mininterface/tag.py index c1d0dfc..e64c42e 100644 --- a/mininterface/tag.py +++ b/mininterface/tag.py @@ -251,9 +251,6 @@ def check(tag.val): def __post_init__(self): # Fetch information from the nested tag: `Tag(Tag(...))` - # print("254: self", self) # TODO - # import ipdb - # ipdb.set_trace() # TODO if isinstance(self.val, Tag): if self._src_obj or self._src_key: raise ValueError("Wrong Tag inheritance, submit a bug report.") diff --git a/mininterface/tag_factory.py b/mininterface/tag_factory.py index bbebec4..790eae2 100644 --- a/mininterface/tag_factory.py +++ b/mininterface/tag_factory.py @@ -6,9 +6,7 @@ from typing import get_type_hints -# TODO def _get_annotation_from_class_hierarchy(cls, key): - """Získá anotaci pro daný klíč ze třídy nebo jejích předků.""" for base in cls.__mro__: if key in getattr(base, '__annotations__', {}): return base.__annotations__[key] @@ -16,7 +14,6 @@ def _get_annotation_from_class_hierarchy(cls, key): def get_type_hint_from_class_hierarchy(cls, key): - """Získá anotaci typu pro daný klíč z třídy nebo jejích předků.""" for base in cls.__mro__: hints = get_type_hints(base) if key in hints: @@ -35,22 +32,13 @@ def tag_factory(val=None, description=None, annotation=None, *args, _src_obj=Non kwargs |= {"_src_obj": _src_obj, "_src_key": _src_key, "_src_class": _src_class} if _src_class: if not annotation: # when we have _src_class, we assume to have _src_key too - # raise ValueError(_src_class.__mro__) - # annotation = get_type_hints(_src_class).get(_src_key) annotation = get_type_hint_from_class_hierarchy(_src_class, _src_key) - # raise ValueError(annotation) - # print("25: annotation", annotation) # TODO - # print("35: _src_class", _src_class) # TODO - # if annotation != get_type_hint_from_class_hierarchy(_src_class, _src_key): - # print("35: annotation", annotation, get_type_hint_from_class_hierarchy(_src_class, _src_key)) # TODO - # print("37: cls.__class__", _src_class, _src_class.__class__, _src_class.__class__.__mro__) # TODO if annotation is TagCallback: return CallbackTag(val, description, *args, **kwargs) else: # We now have annotation from `field: list[Path]` or `field: Annotated[list[Path], ...]`. # But there might be still a better annotation in metadata `field: Annotated[list[Path], Tag(...)]`. field_type = _get_annotation_from_class_hierarchy(_src_class, _src_key) - # field_type = _src_class.__annotations__.get(_src_key) if field_type: if hasattr(field_type, '__metadata__'): for metadata in field_type.__metadata__: @@ -60,5 +48,4 @@ def tag_factory(val=None, description=None, annotation=None, *args, _src_obj=Non # Why fetching metadata name? The name would be taken from _src_obj. # But the user defined in metadata is better. return Tag(val, description, name=metadata.name, *args, **kwargs)._fetch_from(metadata) - # print("62: ---------") # TODO return Tag(val, description, annotation, *args, **kwargs) diff --git a/tests/configs.py b/tests/configs.py index 7d136b5..fd4ec71 100644 --- a/tests/configs.py +++ b/tests/configs.py @@ -133,15 +133,15 @@ class ComplicatedTypes: # NOTE not used yet -# TODO non/nested and defaulted/required @dataclass class AnnotatedClass: # NOTE some of the entries are not well supported - files1: list[Path] - # files2: Positional[list[Path]] + # files2: Positional[list[Path]] # raises error + # files7: Annotated[list[Path], None] + # files8: Annotated[list[Path], Tag(annotation=str)] files3: list[Path] = field(default_factory=list) - # files4: Positional[list[Path]] = field(default_factory=list) + # files4: Positional[list[Path]] = field(default_factory=list) # raises error files5: Annotated[list[Path], None] = field(default_factory=list) files6: Annotated[list[Path], Tag(annotation=str)] = field(default_factory=list) """ Files """ @@ -150,13 +150,14 @@ class AnnotatedClass: @dataclass class AnnotatedClassInner: # NOTE some of the entries are not well supported - foo: int = 1 - # files1: list[Path] + files1: list[Path] # files2: Positional[list[Path]] - # files3: list[Path] = field(default_factory=list) - # files4: Positional[list[Path]] = field(default_factory=list) - # files5: Annotated[list[Path], None] = field(default_factory=list) - # files6: Annotated[list[Path], Tag(annotation=str)] = field(default_factory=list) + # files7: Annotated[list[Path], None] + # files8: Annotated[list[Path], Tag(annotation=str)] + files3: list[Path] = field(default_factory=list) + files4: Positional[list[Path]] = field(default_factory=list) + files5: Annotated[list[Path], None] = field(default_factory=list) + files6: Annotated[list[Path], Tag(annotation=str)] = field(default_factory=list) """ Files """ diff --git a/tests/tests.py b/tests/tests.py index 0a08da7..762a868 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -668,6 +668,7 @@ def test_nested_restraint(self): class TestAnnotated(TestAbstract): + # NOTE some of the entries are not well supported def test_annotated(self): m = run(ConstrainedEnv) d = dataclass_to_tagdict(m.env) @@ -677,13 +678,31 @@ def test_annotated(self): self.assertTrue(d[""]["test2"].update(" ")) def test_class(self): + m = run(AnnotatedClass, interface=Mininterface) + d = dataclass_to_tagdict(m.env)[""] + self.assertEqual(list[Path], d["files1"].annotation) + # self.assertEqual(list[Path], d["files2"].annotation) does not work + self.assertEqual(list[Path], d["files3"].annotation) + # self.assertEqual(list[Path], d["files4"].annotation) does not work + self.assertEqual(list[Path], d["files5"].annotation) + # This does not work, however I do not know what should be the result + # self.assertEqual(list[Path], d["files6"].annotation) + # self.assertEqual(list[Path], d["files7"].annotation) + # self.assertEqual(list[Path], d["files8"].annotation) + + def test_nested_class(self): + # TODO since an old "ask for missing" commit, nesting this issues a UserWarning. m = run(NestedAnnotatedClass, interface=Mininterface) d = dataclass_to_tagdict(m.env)[""] - # self.assertEqual(list[Path], d["files1"].annotation) - # self.assertEqual(d["files2"]) - # self.assertEqual(d["files3"]) - # self.assertEqual(d["files4"]) - # self.assertEqual(d["files5"]) + self.assertEqual(list[Path], d["files1"].annotation) + # self.assertEqual(list[Path], d["files2"].annotation) does not work + self.assertEqual(list[Path], d["files3"].annotation) + # self.assertEqual(list[Path], d["files4"].annotation) does not work + self.assertEqual(list[Path], d["files5"].annotation) + # This does not work, however I do not know what should be the result + # self.assertEqual(list[Path], d["files6"].annotation) + # self.assertEqual(list[Path], d["files7"].annotation) + # self.assertEqual(list[Path], d["files8"].annotation) class TestTagAnnotation(TestAbstract):