Skip to content

Commit

Permalink
WIP3
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Nov 7, 2024
1 parent 639aa37 commit 29d1bc0
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 49 deletions.
17 changes: 1 addition & 16 deletions mininterface/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 43 additions & 2 deletions mininterface/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 0 additions & 3 deletions mininterface/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
13 changes: 0 additions & 13 deletions mininterface/tag_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
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]
return None


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:
Expand All @@ -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__:
Expand All @@ -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)
21 changes: 11 additions & 10 deletions tests/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand All @@ -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 """


Expand Down
29 changes: 24 additions & 5 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down

0 comments on commit 29d1bc0

Please sign in to comment.