Skip to content

Commit

Permalink
parametrized generic in union with None WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Sep 25, 2024
1 parent 4702c49 commit daf1b33
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 35 deletions.
4 changes: 2 additions & 2 deletions mininterface/form_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion mininterface/gui_interface/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 50 additions & 27 deletions mininterface/tag.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -334,41 +334,61 @@ 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

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):
""" To be overrided by the subclasses.
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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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`
Expand All @@ -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):
Expand Down
14 changes: 10 additions & 4 deletions mininterface/types.py
Original file line number Diff line number Diff line change
@@ -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]]):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"]
license = "GPL-3.0-or-later"
Expand Down

0 comments on commit daf1b33

Please sign in to comment.