Skip to content

Commit

Permalink
Fix
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhozic committed Jan 28, 2024
1 parent 060042c commit d690b83
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 82 deletions.
6 changes: 6 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Glossary
Releases
---------------------

v1.4.5
================
- Further generic type fixes
- Fixed type deprecations to also consider subclasses


v1.4.4
================
- Fixed generic types under iterable types expansion.
Expand Down
2 changes: 1 addition & 1 deletion tkclasswiz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
SOFTWARE.
"""

__version__ = "1.4.4"
__version__ = "1.4.5"


from .object_frame import *
Expand Down
80 changes: 77 additions & 3 deletions tkclasswiz/annotations.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"""
Module used for managing annotations.
"""
from typing import Union, Optional, get_args, Generic, get_origin, get_type_hints
from typing import Union, Optional, Generic, Iterable, get_args, get_origin, get_type_hints
from datetime import datetime, timedelta, timezone
from inspect import isclass, isabstract
from itertools import product, chain
from contextlib import suppress
from inspect import isclass
from .doc import doc_category

from .utilities import issubclass_noexcept
from .doc import doc_category


__all__ = (
"register_annotations",
"get_annotations",
"convert_types",
)


Expand Down Expand Up @@ -116,3 +119,74 @@ def get_annotations(class_) -> dict:
del annotations["return"]

return annotations


def convert_types(input_type: type):
"""
Type preprocessing method, that extends the list of types with inherited members (polymorphism)
and removes classes that are wrapped by some other class, if the wrapper class also appears in
the annotations.
"""
def remove_classes(types: list):
r = types.copy()
for type_ in types:
# It's a wrapper of some class -> remove the wrapped class
if hasattr(type_, "__wrapped__"):
if type_.__wrapped__ in r:
r.remove(type_.__wrapped__)

# Abstract classes are classes that don't allow instantiation -> remove the class
if isabstract(type_):
r.remove(type_)

return tuple({a:0 for a in r})


if isinstance(input_type, str):
raise TypeError(
f"Provided type '{input_type}' is not a type - it is a string!\n"
"Potential subscripted type problem?\n"
"Instead of e. g., list['type'], try using typing.List['type']."
)

origin = get_origin(input_type)
if issubclass_noexcept(origin, Generic):
# Patch for Python versions < 3.10
input_type.__name__ = origin.__name__

# Unpack Union items into a tuple
if origin is Union or issubclass_noexcept(origin, (Iterable, Generic)):
new_types = []
for arg_group in get_args(input_type):
new_types.append(remove_classes(list(convert_types(arg_group))))

if origin is Union:
return tuple(chain.from_iterable(new_types)) # Just expand unions

# Process abstract classes and polymorphism
new_origins = []
for origin in convert_types(origin):
if issubclass_noexcept(origin, Generic):
for comb in product(*new_types):
new_origins.append(origin[comb])
elif issubclass_noexcept(origin, Iterable):
new = origin[tuple(chain.from_iterable(new_types))] if len(new_types) else origin
new_origins.append(new)
else:
new_origins.append(origin)

return remove_classes(new_origins)

if input_type.__module__ == "builtins":
# Don't consider built-int types for polymorphism
# No removal of abstract classes is needed either as builtins types aren't abstract
return (input_type,)

# Extend subclasses
subtypes = []
if hasattr(input_type, "__subclasses__"):
for st in input_type.__subclasses__():
subtypes.extend(convert_types(st))

# Remove wrapped classes (eg. wrapped by decorator) + ABC classes
return remove_classes([input_type, *subtypes])
21 changes: 19 additions & 2 deletions tkclasswiz/deprecation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""
Module can be used to mark deprecated classes / parameters / parameter types.
"""
from typing import overload
from typing import overload, get_origin
from itertools import chain

from .utilities import issubclass_noexcept
from .annotations import convert_types
from .doc import doc_category


Expand Down Expand Up @@ -42,6 +45,7 @@ def register_deprecated(cls, parameter: str = None, *types: type):
cls.__wiz_deprecated_params__ = getattr(cls, "__wiz_deprecated_params__", set())
cls.__wiz_deprecated_params__.add(parameter)
else:
types = tuple(chain.from_iterable(map(convert_types, types)))
cls.__wiz_deprecated_param_types__ = getattr(cls, "__wiz_deprecated_param_types__", dict())
cls.__wiz_deprecated_param_types__[parameter] = cls.__wiz_deprecated_param_types__.get(parameter, set())
cls.__wiz_deprecated_param_types__[parameter].update(types)
Expand Down Expand Up @@ -74,5 +78,18 @@ def is_deprecated(cls: type, parameter: str = None, type_: type = None):
if type_ is None:
params = getattr(cls, "__wiz_deprecated_params__", set())
return parameter in params

depr_types = getattr(cls, "__wiz_deprecated_param_types__", dict()).get(parameter, set())
if is_deprecated(type_) or type_ in depr_types:
return True

return is_deprecated(type_) or type_ in getattr(cls, "__wiz_deprecated_param_types__", dict()).get(parameter, set())
origin = get_origin(type_)
if origin is not None and origin in depr_types:
return True

type_ = origin or type_
for depr_type in depr_types:
if issubclass_noexcept(type_, depr_type):
return True

return False
74 changes: 1 addition & 73 deletions tkclasswiz/object_frame/frame_base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar, Generic
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar
from abc import ABC, abstractmethod
from inspect import isabstract
from contextlib import suppress
from itertools import chain, product
from functools import cache

from ..convert import *
Expand Down Expand Up @@ -181,76 +179,6 @@ def cast_type(cls, value: Any, types: Iterable):

return value

@classmethod
def convert_types(cls, input_type: type):
"""
Type preprocessing method, that extends the list of types with inherited members (polymorphism)
and removes classes that are wrapped by some other class, if the wrapper class also appears in
the annotations.
"""
def remove_classes(types: list):
r = types.copy()
for type_ in types:
# It's a wrapper of some class -> remove the wrapped class
if hasattr(type_, "__wrapped__"):
if type_.__wrapped__ in r:
r.remove(type_.__wrapped__)

# Abstract classes are classes that don't allow instantiation -> remove the class
if isabstract(type_):
r.remove(type_)

return tuple({a:0 for a in r})


if isinstance(input_type, str):
raise TypeError(
f"Provided type '{input_type}' is not a type - it is a string!\n"
"Potential subscripted type problem?\n"
"Instead of e. g., list['type'], try using typing.List['type']."
)

origin = get_origin(input_type)
if issubclass_noexcept(origin, Generic):
# Patch for Python versions < 3.10
input_type.__name__ = origin.__name__

# Unpack Union items into a tuple
if origin is Union or issubclass_noexcept(origin, (Iterable, Generic)):
new_types = []
for arg_group in get_args(input_type):
new_types.append(remove_classes(list(cls.convert_types(arg_group))))

if origin is Union:
return tuple(chain.from_iterable(new_types)) # Just expand unions

# Process abstract classes and polymorphism
new_origins = []
for origin in cls.convert_types(origin):
if issubclass_noexcept(origin, Iterable):
new_origins.append(origin[tuple(chain.from_iterable(new_types))])
elif issubclass_noexcept(origin, Generic):
for comb in product(*new_types):
new_origins.append(origin[comb])
else:
new_origins.append(origin)

return remove_classes(new_origins)

if input_type.__module__ == "builtins":
# Don't consider built-int types for polymorphism
# No removal of abstract classes is needed either as builtins types aren't abstract
return (input_type,)

# Extend subclasses
subtypes = []
if hasattr(input_type, "__subclasses__"):
for st in input_type.__subclasses__():
subtypes.extend(cls.convert_types(st))

# Remove wrapped classes (eg. wrapped by decorator) + ABC classes
return remove_classes([input_type, *subtypes])

def init_main_frame(self):
frame_main = ttk.Frame(self)
frame_main.pack(expand=True, fill=tk.BOTH)
Expand Down
6 changes: 3 additions & 3 deletions tkclasswiz/object_frame/frame_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ..storage import *
from ..messagebox import Messagebox
from ..extensions import extendable
from ..annotations import get_annotations
from ..annotations import get_annotations, convert_types
from ..deprecation import *
from ..doc import doc_category

Expand Down Expand Up @@ -155,7 +155,7 @@ def _create_fields(self, annotations: dict[str, type], additional_values: dict,

for (k, v) in annotations.items():
# Init widgets
entry_types = self.convert_types(v)
entry_types = convert_types(v)
frame_annotated = ttk.Frame(frame)
frame_annotated.pack(fill=tk.BOTH, expand=True, pady=dpi_5)
ttk.Label(frame_annotated, text=k, width=label_width).pack(side="left")
Expand Down Expand Up @@ -339,7 +339,7 @@ def _edit_selected(self, key: str, combo: ComboBoxObjects):
return self.new_object_frame(selection.class_, combo, old_data=selection)
else:
type_sel = type(selection)
for t in self.convert_types(get_annotations(self.class_)[key]):
for t in convert_types(get_annotations(self.class_)[key]):
if (get_origin(t) or t) == type_sel:
type_ = t
break
Expand Down

0 comments on commit d690b83

Please sign in to comment.