Skip to content

Commit 0e49645

Browse files
authored
Merge pull request #29 from davidhozic/develop
Develop
2 parents 9c7277e + e8e9f52 commit 0e49645

File tree

10 files changed

+223
-58
lines changed

10 files changed

+223
-58
lines changed

docs/source/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ Glossary
2424
Releases
2525
---------------------
2626

27+
v1.3.0
28+
================
29+
- The types will now have their subscripted type displayed alongside them.
30+
- Custom repr display of structured objects via
31+
:py:meth:`tkclasswiz.convert.ObjectInfo.register_repr` method.
32+
2733
v1.2.3
2834
================
2935
- Fixed annotations not getting obtained for function definitions.

docs/source/guide/customrepr.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
============================
2+
Custom object display (repr)
3+
============================
4+
5+
The default way of displaying objects is as follows: ``ClassName(param1=..., param2=..., param3=..., ...)``.
6+
This printout / repr can b overridden with the use of :py:meth:`tkclasswiz.convert.ObjectInfo.register_repr` method.
7+
8+
9+
.. automethod:: tkclasswiz.convert.ObjectInfo.register_repr
10+
:no-index:
11+
12+
13+
For example, let's say I want to define some logical operators and want them to be displayed differently.
14+
The example shows how a custom repr display can be made. It may look a bit much, but the only thing that matters
15+
is the emphasized :py:meth:`~tkclasswiz.convert.ObjectInfo.register_repr` method call.
16+
Inside the call, a lambda function is provided. The lambda accepts
17+
the :class:`~tkclasswiz.convert.ObjectInfo` object and outputs a string.
18+
That string is made up of the name of a logical operator and the operants in the following format:
19+
``<op1> <operator name> <op2> <operator name> <op3> ...``. If the operator's name is ``or_op``, it will be displayed
20+
as ``<op1> or <op2> or <op3> ...``. If the operator's name is ``and_op``, it will be displayed as
21+
``<op1> and <op2> and <op3> ...``.
22+
23+
24+
.. image:: ./images/object_info_custom_repr.png
25+
:width: 20cm
26+
27+
28+
.. code-block:: python
29+
:linenos:
30+
:emphasize-lines: 38-45
31+
32+
from typing import List
33+
from abc import ABC, abstractmethod
34+
35+
import tkinter as tk
36+
import tkinter.ttk as ttk
37+
import tkclasswiz as wiz
38+
39+
class base_op(ABC):
40+
@abstractmethod
41+
def evaluate(self):
42+
pass
43+
44+
class bool_op(base_op):
45+
def __init__(self, operants: List["base_op"]) -> None:
46+
self.operants = operants
47+
48+
class and_op(bool_op):
49+
def evaluate(self):
50+
return all(op.evaluate() for op in self.operants)
51+
52+
class or_op(bool_op):
53+
def evaluate(self):
54+
return any(op.evaluate() for op in self.operants)
55+
56+
class contains(base_op):
57+
def __init__(self, op: str) -> None:
58+
pass
59+
60+
def evaluate(self):
61+
# For demonstration purposes. Otherwise, we would usually check if op is contained within string
62+
return True
63+
64+
class MyLogicResult:
65+
def __init__(self, expression: base_op) -> None:
66+
self.expression = expression
67+
68+
69+
wiz.ObjectInfo.register_repr(
70+
bool_op,
71+
lambda oi: "(" +
72+
f' {oi.class_.__name__.removesuffix("_op")} '
73+
.join(map(repr, oi.data["operants"])) +
74+
")",
75+
True
76+
)
77+
78+
# Tkinter main window
79+
root = tk.Tk("Test")
80+
81+
# Modified tkinter Combobox that will store actual objects instead of strings
82+
combo = wiz.ComboBoxObjects(root)
83+
combo.pack(fill=tk.X, padx=5)
84+
85+
def define(old = None):
86+
"""
87+
Function for opening a window either in new definition mode (old = None) or
88+
edit mode (old != None)
89+
"""
90+
assert old is None or isinstance(old, wiz.ObjectInfo)
91+
window = wiz.ObjectEditWindow()
92+
window.open_object_edit_frame(MyLogicResult, combo)
93+
94+
# Main GUI structure
95+
ttk.Button(text="Open", command=define).pack()
96+
root.mainloop()
97+

docs/source/guide/defining.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ This is how our frame looks after defining 4 ``Wheel`` objects:
171171
.. image:: ./images/new_define_frame_list_defined.png
172172
:width: 15cm
173173

174+
175+
Final definition
176+
=================
177+
174178
If we click "Save", we go back to our ``Car`` definition frame, that has all the parameters defined.
175179
Clicking on "Save" once again will save the ``Car`` object to our original GUI Combobox.
176180

29.3 KB
Loading

docs/source/guide/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ Index:
1515
conversion
1616
polymorphism
1717
abstractclasses
18+
customrepr
1819
aliasing
1920
generics

tkclasswiz/__init__.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
"""
22
TkClassWizard - Library for graphically defining objects based on class annotations.
33
Works with Tkinter / TTKBootstrap.
4-
"""
54
6-
__version__ = "1.2.3"
5+
-------------------
6+
7+
MIT License
8+
9+
Copyright (c) 2023 David Hozic
10+
11+
Permission is hereby granted, free of charge, to any person obtaining a copy
12+
of this software and associated documentation files (the "Software"), to deal
13+
in the Software without restriction, including without limitation the rights
14+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
copies of the Software, and to permit persons to whom the Software is
16+
furnished to do so, subject to the following conditions:
17+
18+
The above copyright notice and this permission notice shall be included in all
19+
copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27+
SOFTWARE.
28+
"""
29+
__version__ = "1.3.0"
730

831
from .object_frame import *
932
from .annotations import *

tkclasswiz/convert.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
22
Modules contains definitions related to GUI object transformations.
33
"""
4-
5-
from typing import Any, Union, List, Generic, TypeVar, Mapping, Optional
4+
from __future__ import annotations
5+
from typing import Any, Union, List, Generic, TypeVar, Mapping, Optional, Callable
66
from contextlib import suppress
77
from inspect import signature
88
from enum import Enum
@@ -125,6 +125,7 @@ class ObjectInfo(Generic[TClass]):
125125
recognition.
126126
"""
127127
CHARACTER_LIMIT = 150
128+
custom_display_map: dict[type, Callable] = {}
128129

129130
def __init__(
130131
self,
@@ -158,23 +159,31 @@ def __repr__(self) -> str:
158159
if self._repr is not None:
159160
return self._repr
160161

162+
repr_get = ObjectInfo.custom_display_map.get(self.class_, self._repr_default)
163+
_ret = repr_get(self)
164+
165+
self._repr = _ret
166+
return _ret
167+
168+
@staticmethod
169+
def _repr_default(object_info: ObjectInfo) -> str:
161170
_ret: List[str] = []
162-
if self.nickname:
163-
_ret += f"({self.nickname}) "
171+
if object_info.nickname:
172+
_ret += f"({object_info.nickname}) "
164173

165-
name = get_aliased_name(self.class_)
174+
name = get_aliased_name(object_info.class_)
166175
if name is not None:
167-
name += f'({self.class_.__name__})'
176+
name += f'({object_info.class_.__name__})'
168177
else:
169-
name = self.class_.__name__
178+
name = object_info.class_.__name__
170179

171180
_ret += name + "("
172181
private_params = set()
173-
if hasattr(self.class_, "__passwords__"):
174-
private_params = private_params.union(self.class_.__passwords__)
182+
if hasattr(object_info.class_, "__passwords__"):
183+
private_params = private_params.union(object_info.class_.__passwords__)
175184

176-
for k, v in self.data.items():
177-
if len(_ret) > self.CHARACTER_LIMIT:
185+
for k, v in object_info.data.items():
186+
if len(_ret) > object_info.CHARACTER_LIMIT:
178187
break
179188

180189
if isinstance(v, str):
@@ -189,12 +198,34 @@ def __repr__(self) -> str:
189198

190199
_ret: str = ''.join(_ret)
191200
_ret = _ret.rstrip(", ") + ")"
192-
if len(_ret) > self.CHARACTER_LIMIT:
193-
_ret = _ret[:self.CHARACTER_LIMIT] + "...)"
201+
if len(_ret) > object_info.CHARACTER_LIMIT:
202+
_ret = _ret[:object_info.CHARACTER_LIMIT] + "...)"
194203

195-
self._repr = _ret
196204
return _ret
197205

206+
@classmethod
207+
def register_repr(cls, class_: type, repr: Callable[[ObjectInfo], str], inherited: bool = False):
208+
"""
209+
Registers a custom __repr__ (string representation of object) function.
210+
The function must as a single parameter accept the ``ObjectInfo`` instance
211+
being represented as a string.
212+
213+
Parameters
214+
------------
215+
class_: type
216+
The class for which this custom ``repr`` is being register.
217+
repr: Callable[[ObjectInfo], str]
218+
The function that will provide custom ``__repr__``.
219+
As a parameter it accepts the ``ObjectInfo`` object.
220+
It returns a :class:`str` (string).
221+
inherited: bool
222+
Boolean flag. Setting it to True will register repr for inherited members as well.
223+
Defaults to False.
224+
"""
225+
cls.custom_display_map[class_] = repr
226+
if inherited:
227+
for type_ in class_.__subclasses__():
228+
cls.register_repr(type_, repr, True)
198229

199230
@cache_result(max=1024)
200231
@doc_category("Conversion")

tkclasswiz/object_frame/frame_base.py

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar
2-
from abc import ABC
1+
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar, Generic
32
from inspect import isabstract
43
from contextlib import suppress
4+
from itertools import chain
5+
from functools import cache
56

67
from ..convert import *
78
from ..aliasing import *
@@ -92,31 +93,30 @@ def __init__(
9293
self.init_main_frame()
9394

9495
@staticmethod
95-
def get_cls_name(cls: T) -> Union[str, T]:
96+
@cache
97+
def get_cls_name(cls: Any, args: bool = False) -> str:
9698
"""
9799
Returns the name of the class ``cls`` or
98100
the original class when the name cannot be obtained.
99101
If alias exists, alias is returned instead.
100102
"""
101-
name = NewObjectFrameBase._get_cls_unaliased_name(cls)
102103
if (alias := get_aliased_name(cls)) is not None:
103104
return alias + f" ({name})"
105+
elif hasattr(cls, "__name__"):
106+
name = cls.__name__
107+
elif hasattr(cls, "_name") and cls._name:
108+
name = cls._name
109+
else:
110+
name = str(cls)
111+
112+
if args and (type_args := get_args(cls)):
113+
name = (
114+
name +
115+
f"[{ ', '.join([NewObjectFrameBase.get_cls_name(x) for x in type_args]) }]"
116+
)
104117

105118
return name
106119

107-
@staticmethod
108-
def _get_cls_unaliased_name(cls: T) -> Union[str, T]:
109-
"""
110-
Returns the name of the class ``cls`` or
111-
the original class when the name cannot be obtained.
112-
"""
113-
if hasattr(cls, "__name__"):
114-
return cls.__name__
115-
if hasattr(cls, "_name"):
116-
return cls._name
117-
118-
return cls
119-
120120
@classmethod
121121
def set_origin_window(cls, window: "ObjectEditWindow"):
122122
cls.origin_window = window
@@ -143,7 +143,7 @@ def cast_type(cls, value: Any, types: Iterable):
143143

144144
return value
145145

146-
for type_ in filter(lambda t: cls._get_cls_unaliased_name(t) in __builtins__, types):
146+
for type_ in filter(lambda t: t.__module__ == "builtins", types):
147147
with suppress(Exception):
148148
cast_funct = CAST_FUNTIONS.get(type_)
149149
if cast_funct is None:
@@ -157,7 +157,7 @@ def cast_type(cls, value: Any, types: Iterable):
157157
return value
158158

159159
@classmethod
160-
def convert_types(cls, types_in):
160+
def convert_types(cls, input_type: type):
161161
"""
162162
Type preprocessing method, that extends the list of types with inherited members (polymorphism)
163163
and removes classes that are wrapped by some other class, if the wrapper class also appears in
@@ -177,33 +177,36 @@ def remove_classes(types: list):
177177

178178
return tuple(r)
179179

180-
if get_origin(types_in) is Union:
181-
types_in = cls.convert_types(get_args(types_in))
180+
origin = get_origin(input_type)
181+
# Unpack Union items into a tuple
182+
if origin is Union or issubclass_noexcept(origin, Iterable):
183+
new_types = dict()
184+
for type_ in chain.from_iterable([cls.convert_types(r) for r in get_args(input_type)]):
185+
new_types[type_] = 0 # Use dictionary's keys as OrderedSet, with dummy value 0
182186

183-
elif issubclass_noexcept(origin := get_origin(types_in), Iterable) and types_in is not str:
184-
types_in = origin[cls.convert_types(get_args(types_in))]
187+
new_types = tuple(new_types)
188+
if origin is Union:
189+
return new_types
185190

186-
if isinstance(types_in, tuple):
187-
new_types = []
188-
for t in types_in:
189-
new_types.extend(cls.convert_types(t))
191+
return (origin[new_types],)
190192

191-
types_in = new_types
192-
else:
193-
types_in = (types_in,)
193+
if issubclass_noexcept(origin, Generic):
194+
# Patch for Python versions < 3.10
195+
input_type.__name__ = origin.__name__
194196

195-
# Also include inherited objects
196-
subtypes = []
197-
for t in types_in:
198-
if cls.get_cls_name(t) in __builtins__:
199-
continue # Don't consider built-int types for polymorphism
197+
if input_type.__module__ == "builtins":
198+
# Don't consider built-int types for polymorphism
199+
# No removal of abstract classes is needed either as builtins types aren't abstract
200+
return (input_type,)
200201

201-
if hasattr(t, "__subclasses__"):
202-
for st in t.__subclasses__():
203-
subtypes.extend(cls.convert_types(st))
202+
# Extend subclasses
203+
subtypes = []
204+
if hasattr(input_type, "__subclasses__"):
205+
for st in input_type.__subclasses__():
206+
subtypes.extend(cls.convert_types(st))
204207

205208
# Remove wrapped classes (eg. wrapped by decorator) + ABC classes
206-
return remove_classes([*types_in, *subtypes])
209+
return remove_classes([input_type, *subtypes])
207210

208211
def init_main_frame(self):
209212
frame_main = ttk.Frame(self)

0 commit comments

Comments
 (0)