diff --git a/mininterface/form_dict.py b/mininterface/form_dict.py index 756b167..d32ffce 100644 --- a/mininterface/form_dict.py +++ b/mininterface/form_dict.py @@ -3,7 +3,7 @@ """ import logging from types import FunctionType, MethodType -from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, get_type_hints +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, get_args, get_type_hints if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self` from typing import Self @@ -116,7 +116,7 @@ def dataclass_to_tagdict(env: EnvClass, descr: dict, facet: "Facet" = None, _pat for param, val in vars(env).items(): annotation = get_type_hints(env.__class__).get(param) if val is None: - if annotation in (Optional[int], Optional[str]): + if type(None) in get_args(annotation): # Since tkinter_form does not handle None yet, we have help it. # We need it to be able to write a number and if empty, return None. # This would fail: `severity: int | None = None` diff --git a/mininterface/gui_interface/utils.py b/mininterface/gui_interface/utils.py index 8308d3a..e3c2959 100644 --- a/mininterface/gui_interface/utils.py +++ b/mininterface/gui_interface/utils.py @@ -103,7 +103,7 @@ def _fetch(variable): if path_tag := tag._morph(PathTag, Path): grid_info = widget.grid_info() - widget2 = Button(master, text='👓', command=choose_file_handler(variable, path_tag)) + widget2 = Button(master, text='…', command=choose_file_handler(variable, path_tag)) widget2.grid(row=grid_info['row'], column=grid_info['column']+1) # Special type: Submit button diff --git a/mininterface/tag.py b/mininterface/tag.py index d310634..3ee74dd 100644 --- a/mininterface/tag.py +++ b/mininterface/tag.py @@ -1,7 +1,7 @@ from ast import literal_eval from dataclasses import dataclass, fields from datetime import datetime -from types import FunctionType, MethodType, UnionType +from types import FunctionType, MethodType, NoneType, UnionType from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, TypeVar, get_args, get_origin, get_type_hints from warnings import warn @@ -334,7 +334,7 @@ class Env: 'TypeError: cannot be a parameterized generic' """ - if self.annotation is None: + if self.annotation is None: # no annotation check, everything is fine then return True elif self.annotation is SubmitButton: # NOTE EXPERIMENTAL return val is True or val is False @@ -342,14 +342,20 @@ class Env: try: return isinstance(val, self.annotation) except TypeError: - if complex_ := self._get_annotation_parametrized(): - origin, subtype = complex_ - return isinstance(val, origin) and all(isinstance(item, subtype) for item in val) + if val is None and NoneType in get_args(self.annotation): + return True + for origin, subtype in self._get_possible_types(): + if isinstance(val, origin) and all(isinstance(item, subtype) for item in val): + return True + return False def _is_subclass(self, class_type): try: return issubclass(self.annotation, class_type) except TypeError: # None, Union etc cast an error + for _, subtype in self._get_possible_types(): + if issubclass(class_type, subtype): + return True return False def _morph(self, class_type: "Self", morph_if: type): @@ -357,18 +363,32 @@ def _morph(self, class_type: "Self", morph_if: type): The user used a Path within a Tag and that will turn it into a PathTag when the UI needs it. """ if self._is_subclass(morph_if): # return a blank PathTag - return class_type(self.val) - - def _get_annotation_parametrized(self): - if origin := get_origin(self.annotation): # list[str] -> list, list -> None - if origin == UnionType: # might be just `int | None` - return - subtype = get_args(self.annotation) # list[str] -> (str,), list -> () - if (len(subtype) == 1): - return origin, subtype[0] - else: - warn(f"This parametrized generic not implemented: {self.annotation}") - return None + return class_type(self.val, annotation=self.annotation) + + def _get_possible_types(self) -> list[tuple]: + """ Possible types we can cast the value to. + For annotation `list[int] | tuple[str] | str | None`, + it returns `[(list,int), (tuple,str), (None,str)]`. + + Filters out None. + """ + def _(annot): + if origin := get_origin(annot): # list[str] -> list, list -> None + subtype = get_args(annot) # list[str] -> (str,), list -> () + if origin == UnionType: # ex: `int | None`, `list[int] | None`` + return [_(subt) for subt in subtype] + if (len(subtype) == 1): + return origin, subtype[0] + else: + warn(f"This parametrized generic not implemented: {annot}") + elif annot is not None and annot is not NoneType: + # from UnionType, we get a NoneType + return None, annot + return None # to be filtered out + out = _(self.annotation) + return [x for x in (out if isinstance(out, list) else [out]) if x is not None] + # return out if isinstance(out, list) else [out] + def set_error_text(self, s): self._original_desc = o = self.description @@ -402,9 +422,9 @@ def _get_ui_val(self): Ex: [Path("/tmp"), Path("/usr")] -> ["/tmp", "/usr"]. We need the latter in the UI because in the first case, ast_literal would not not reconstruct it later. """ - if complex_ := self._get_annotation_parametrized(): - origin, _ = complex_ - return origin(str(v)for v in self.val) + for origin, _ in self._get_possible_types(): + if origin: + return origin(str(v)for v in self.val) return self.val def _get_choices(self) -> dict[ChoiceLabel, TagValue]: @@ -505,7 +525,7 @@ def update(self, ui_value: TagValue) -> bool: # Even though GuiInterface does some type conversion (str → int) independently, # other interfaces does not guarantee that. Hence, we need to do the type conversion too. if self.annotation: - if ui_value == "" and type(None) in get_args(self.annotation): + if ui_value == "" and NoneType in get_args(self.annotation): # The user is not able to set the value to None, they left it empty. # Cast back to None as None is one of the allowed types. # Ex: `severity: int | None = None` @@ -530,14 +550,17 @@ def update(self, ui_value: TagValue) -> bool: if not self._is_right_instance(out_value) and isinstance(out_value, str): try: - if complex_ := self._get_annotation_parametrized(): - origin, cast_to = complex_ - # Textual ask_number -> user writes '123', this has to be converted to int 123 - # NOTE: Unfortunately, type(list) looks awful here. @see TextualInterface.form comment. - # (Maybe that's better now.) - candidate = origin(cast_to(v) for v in literal_eval(ui_value)) + for origin, cast_to in self._get_possible_types(): + if origin: + # Textual ask_number -> user writes '123', this has to be converted to int 123 + # NOTE: Unfortunately, type(list) looks awful here. @see TextualInterface.form comment. + # (Maybe that's better now.) + candidate = origin(cast_to(v) for v in literal_eval(ui_value)) + else: + candidate = cast_to(ui_value) if self._is_right_instance(candidate): out_value = candidate + break else: out_value = self.annotation(ui_value) except (TypeError, ValueError, SyntaxError): diff --git a/mininterface/types.py b/mininterface/types.py index a2bae30..a287397 100644 --- a/mininterface/types.py +++ b/mininterface/types.py @@ -1,8 +1,8 @@ from dataclasses import dataclass from pathlib import Path -from typing import Callable, override -from typing_extensions import Self -from .tag import Tag, ValidationResult, TagValue +from typing import Callable +from typing_extensions import Self, override +from .tag import Tag, ValidationResult, TagValue, common_iterables def Validation(check: Callable[["Tag"], ValidationResult | tuple[ValidationResult, TagValue]]): @@ -65,7 +65,13 @@ class PathTag(Tag): def __post_init__(self): super().__post_init__() - self.annotation = list[Path] if self.multiple else Path + if not self.annotation: + self.annotation = list[Path] if self.multiple else Path + else: + for origin, _ in self._get_possible_types(): + if origin in common_iterables: + self.multiple = True + break @override def _morph(self, class_type: "Self", morph_if: type): diff --git a/pyproject.toml b/pyproject.toml index 6704c74..c92c33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "mininterface" -version = "0.6.0rc2" +version = "0.6.0rc4" description = "A minimal access to GUI, TUI, CLI and config" authors = ["Edvard Rejthar "] license = "GPL-3.0-or-later"