Skip to content

Commit 5a389fd

Browse files
authored
Merge pull request #9 from davidhozic/develop
Merge develop for release
2 parents 35e9582 + 0c4f069 commit 5a389fd

File tree

8 files changed

+234
-6
lines changed

8 files changed

+234
-6
lines changed

docs/source/changelog.rst

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

27+
v1.1.0
28+
================
29+
- :ref:`Abstract classes` (those that directly inherit :class:`abc.ABC`) are no longer
30+
definable through TkClassWizard.
31+
- :ref:`Polymorphism` support
32+
33+
34+
2735
v1.0.1
2836
=================
2937
- Fixed a bug where the window didn't close and couldn't be closed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
===================
2+
Abstract classes
3+
===================
4+
5+
Abstract classes in Object Oriented Programming (OOP) are classes that cannot be instantiated.
6+
In Python, abstract classes can be by inheriting the :class:`abc.ABC` class. Abstract classes also need to have
7+
abstract methods, otherwise Python will not treat the class as abstract. Trying to create an instance (object)
8+
from an abstract class results in a TypeError exception.
9+
10+
.. code-block:: python
11+
12+
from abc import ABC, abstractmethod
13+
14+
class MyAbstractClass(ABC):
15+
@abstractmethod
16+
def some_method(self):
17+
pass
18+
19+
class MyClass(MyAbstractClass):
20+
def some_method(self):
21+
return 5 * 5
22+
23+
# OK
24+
my_instance = MyClass()
25+
26+
# TypeError: Can't instantiate abstract class MyClass with abstract method some_method
27+
my_abstract_instance = MyAbstractClass()
28+
29+
30+
Let's modify our example from :ref:`Polymorphism`.
31+
32+
.. code-block:: python
33+
:linenos:
34+
:emphasize-lines: 1, 8, 12
35+
36+
from abc import ABC, abstractmethod
37+
38+
import tkinter as tk
39+
import tkinter.ttk as ttk
40+
import tkclasswiz as wiz
41+
42+
# An abstract class
43+
class Wheel(ABC):
44+
def __init__(self, diameter: float):
45+
self.diameter = diameter
46+
47+
@abstractmethod
48+
def get_info(self) -> str:
49+
pass
50+
51+
class WinterWheel(Wheel):
52+
def get_info(self) -> str:
53+
return "Wheel for winter."
54+
55+
class SummerWheel(Wheel):
56+
def get_info(self) -> str:
57+
return "Wheel for summer."
58+
59+
60+
class Car:
61+
def __init__(self, name: str, speed: float, wheels: list[Wheel]):
62+
self.name = name
63+
self.speed = speed
64+
self.wheels = wheels
65+
66+
if speed > 50_000:
67+
raise ValueError("Car can go up to 50 000 km / h")
68+
69+
if len(wheels) != 4:
70+
raise ValueError("The car must have 4 wheels!")
71+
72+
# Tkinter main window
73+
root = tk.Tk("Test")
74+
75+
# Modified tkinter Combobox that will store actual objects instead of strings
76+
combo = wiz.ComboBoxObjects(root)
77+
combo.pack(fill=tk.X, padx=5)
78+
79+
def make_car(old = None):
80+
"""
81+
Function for opening a window either in new definition mode (old = None) or
82+
edit mode (old != None)
83+
"""
84+
assert old is None or isinstance(old, wiz.ObjectInfo)
85+
86+
window = wiz.ObjectEditWindow() # The object definition window / wizard
87+
window.open_object_edit_frame(Car, combo, old_data=old) # Open the actual frame
88+
89+
def print_defined():
90+
data = combo.get()
91+
data = wiz.convert_to_objects(data) # Convert any abstract ObjectInfo objects into actual Python objects
92+
print(f"Object: {data}; Type: {type(data)}",) # Print the object and it's datatype
93+
94+
95+
# Main GUI structure
96+
ttk.Button(text="Define Car", command=make_car).pack()
97+
ttk.Button(text="Edit Car", command=lambda: make_car(combo.get())).pack()
98+
ttk.Button(text="Print defined", command=print_defined).pack()
99+
root.mainloop()
100+
101+
102+
We can see that the ``Wheel`` is now an abstract class.
103+
It is then inherited by ``WinterWheel`` and ``SummerWheel``.
104+
If we try to define the ``wheels`` parameter of our ``Car`` object, only these two inherited classes
105+
will be definable.
106+
107+
.. image:: ./images/new_define_frame_list_abstractclass.png
108+
:width: 15cm
109+
110+
We can see that while ``WinterWheel`` and ``SummerWheel`` are definable (due to :ref:`Polymorphism`),
111+
``Wheel`` is not.
34.2 KB
Loading
35.8 KB
Loading

docs/source/guide/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ Index:
1313
validation
1414
annotations
1515
conversion
16+
polymorphism
17+
abstractclasses

docs/source/guide/polymorphism.rst

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
====================================
3+
Polymorphism
4+
====================================
5+
6+
Polymorphism is a term used to describe the phenomenon of something appearing in multiple forms.
7+
In nature an example would be a dog. A dog can appear in different forms (breeds). It also means
8+
that multiple dog forms (breeds) are all derived from a common form - dog.
9+
10+
In the Object Oriented Programming (OOP), this means that an object might appear as either
11+
an instance of a its actual class or an instance of a superclass.
12+
13+
Let's modify our original guide example:
14+
15+
16+
.. code-block:: python
17+
:linenos:
18+
:emphasize-lines: 10,11,13,14,22
19+
20+
import tkinter as tk
21+
import tkinter.ttk as ttk
22+
import tkclasswiz as wiz
23+
24+
# Normal Python classes with annotations (type hints)
25+
class Wheel:
26+
def __init__(self, diameter: float):
27+
self.diameter = diameter
28+
29+
class WinterWheel(Wheel):
30+
pass
31+
32+
class SummerWheel(Wheel):
33+
pass
34+
35+
36+
class Car:
37+
def __init__(
38+
self,
39+
name: str,
40+
speed: float,
41+
wheels: list[Wheel]
42+
):
43+
self.name = name
44+
self.speed = speed
45+
self.wheels = wheels
46+
47+
if speed > 50_000:
48+
raise ValueError("Car can go up to 50 000 km / h")
49+
50+
if len(wheels) != 4:
51+
raise ValueError("The car must have 4 wheels!")
52+
53+
# Tkinter main window
54+
root = tk.Tk("Test")
55+
56+
# Modified tkinter Combobox that will store actual objects instead of strings
57+
combo = wiz.ComboBoxObjects(root)
58+
combo.pack(fill=tk.X, padx=5)
59+
60+
def make_car(old = None):
61+
"""
62+
Function for opening a window either in new definition mode (old = None) or
63+
edit mode (old != None)
64+
"""
65+
assert old is None or isinstance(old, wiz.ObjectInfo)
66+
67+
window = wiz.ObjectEditWindow() # The object definition window / wizard
68+
window.open_object_edit_frame(Car, combo, old_data=old) # Open the actual frame
69+
70+
def print_defined():
71+
data = combo.get()
72+
data = wiz.convert_to_objects(data) # Convert any abstract ObjectInfo objects into actual Python objects
73+
print(f"Object: {data}; Type: {type(data)}",) # Print the object and it's datatype
74+
75+
76+
# Main GUI structure
77+
ttk.Button(text="Define Car", command=make_car).pack()
78+
ttk.Button(text="Edit Car", command=lambda: make_car(combo.get())).pack()
79+
ttk.Button(text="Print defined", command=print_defined).pack()
80+
root.mainloop()
81+
82+
83+
We can see that two new classes are created - ``WinterWheel`` and ``SummerWheel``.
84+
We also see that ``Car``'s ``wheels`` parameter is still a list of type ``Wheel``.
85+
TkClassWizard not only considers the annotated type when constructing a GUI, but also the annotated type's subclasses,
86+
implementing the concept of polymorphism, thus allowing us definition of
87+
``Wheel``, ``WinterWheel`` and ``SummerWheel`` classes.
88+
89+
.. image:: ./images/new_define_frame_list_polymorphism.png
90+
:width: 15cm

tkclasswiz/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Works with Tkinter / TTKBootstrap.
44
"""
55

6-
__version__ = "1.0.1"
6+
__version__ = "1.1.0"
77

88
from .object_frame import *
99
from .annotations import *

tkclasswiz/object_frame/frame_base.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar
2+
from abc import ABC
23
from contextlib import suppress
34

45
from ..convert import *
@@ -11,6 +12,7 @@
1112

1213
import tkinter.ttk as ttk
1314
import tkinter as tk
15+
import json
1416

1517

1618
if TYPE_CHECKING:
@@ -116,7 +118,7 @@ def cast_type(cls, value: Any, types: Iterable):
116118
"""
117119

118120
CAST_FUNTIONS = {
119-
# dict: lambda v: convert_dict_to_object_info(json.loads(v))
121+
dict: lambda v: convert_to_object_info(json.loads(v))
120122
}
121123

122124
# Validate literals
@@ -141,14 +143,26 @@ def cast_type(cls, value: Any, types: Iterable):
141143

142144
@classmethod
143145
def convert_types(cls, types_in):
144-
def remove_wrapped(types: list):
146+
"""
147+
Type preprocessing method, that extends the list of types with inherited members (polymorphism)
148+
and removes classes that are wrapped by some other class, if the wrapper class also appears in
149+
the annotations.
150+
"""
151+
def remove_classes(types: list):
145152
r = types.copy()
146153
for type_ in types:
147154
# It's a wrapper of some class -> remove the wrapped class
148155
if hasattr(type_, "__wrapped__"):
149156
if type_.__wrapped__ in r:
150157
r.remove(type_.__wrapped__)
151158

159+
# Abstract classes are classes that don't allow instantiation -> remove the class
160+
# Use the __bases__ instead of issubclass, because ABC is only supposed to denote
161+
# classes abstract if they directly inherit it. In the case of multi-level inheritance
162+
# issubclass would still return True, even though type_ is not a direct subclass ABC.
163+
if ABC in type_.__bases__:
164+
r.remove(type_)
165+
152166
return r
153167

154168
while get_origin(types_in) is Union:
@@ -163,12 +177,15 @@ def remove_wrapped(types: list):
163177
# Also include inherited objects
164178
subtypes = []
165179
for t in types_in:
166-
if hasattr(t, "__subclasses__") and t.__module__.split('.', 1)[0] in {"_discord", "daf"}:
180+
if cls.get_cls_name(t) in __builtins__:
181+
continue # Don't consider built-int types for polymorphism
182+
183+
if hasattr(t, "__subclasses__"):
167184
for st in t.__subclasses__():
168185
subtypes.extend(cls.convert_types(st))
169186

170-
# Remove wrapped classes (eg. wrapped by decorator)
171-
return remove_wrapped(types_in + subtypes)
187+
# Remove wrapped classes (eg. wrapped by decorator) + ABC classes
188+
return remove_classes(types_in + subtypes)
172189

173190
def init_main_frame(self):
174191
frame_main = ttk.Frame(self)

0 commit comments

Comments
 (0)