From d690b834a62ac145ef99b27dd5edb6bb4b3f2007 Mon Sep 17 00:00:00 2001 From: David Hozic Date: Sun, 28 Jan 2024 13:43:34 +0100 Subject: [PATCH] Fix --- docs/source/changelog.rst | 6 ++ tkclasswiz/__init__.py | 2 +- tkclasswiz/annotations.py | 80 ++++++++++++++++++++++++- tkclasswiz/deprecation.py | 21 ++++++- tkclasswiz/object_frame/frame_base.py | 74 +---------------------- tkclasswiz/object_frame/frame_struct.py | 6 +- 6 files changed, 107 insertions(+), 82 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 2c16d69..ba4bca1 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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. diff --git a/tkclasswiz/__init__.py b/tkclasswiz/__init__.py index 0da0e71..5cc15f2 100644 --- a/tkclasswiz/__init__.py +++ b/tkclasswiz/__init__.py @@ -27,7 +27,7 @@ SOFTWARE. """ -__version__ = "1.4.4" +__version__ = "1.4.5" from .object_frame import * diff --git a/tkclasswiz/annotations.py b/tkclasswiz/annotations.py index 7003f1b..5f9b369 100644 --- a/tkclasswiz/annotations.py +++ b/tkclasswiz/annotations.py @@ -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", ) @@ -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]) diff --git a/tkclasswiz/deprecation.py b/tkclasswiz/deprecation.py index 2fe73da..61e8fc8 100644 --- a/tkclasswiz/deprecation.py +++ b/tkclasswiz/deprecation.py @@ -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 @@ -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) @@ -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 diff --git a/tkclasswiz/object_frame/frame_base.py b/tkclasswiz/object_frame/frame_base.py index 08cc1ef..961fb92 100644 --- a/tkclasswiz/object_frame/frame_base.py +++ b/tkclasswiz/object_frame/frame_base.py @@ -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 * @@ -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) diff --git a/tkclasswiz/object_frame/frame_struct.py b/tkclasswiz/object_frame/frame_struct.py index 3d09b68..4393ffc 100644 --- a/tkclasswiz/object_frame/frame_struct.py +++ b/tkclasswiz/object_frame/frame_struct.py @@ -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 @@ -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") @@ -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