Skip to content

Commit

Permalink
Merge pull request #9 from davidhozic/develop
Browse files Browse the repository at this point in the history
Merge develop for release
  • Loading branch information
davidhozic authored Dec 31, 2023
2 parents 35e9582 + 0c4f069 commit 5a389fd
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 6 deletions.
8 changes: 8 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ Glossary
Releases
---------------------

v1.1.0
================
- :ref:`Abstract classes` (those that directly inherit :class:`abc.ABC`) are no longer
definable through TkClassWizard.
- :ref:`Polymorphism` support



v1.0.1
=================
- Fixed a bug where the window didn't close and couldn't be closed
Expand Down
111 changes: 111 additions & 0 deletions docs/source/guide/abstractclasses.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
===================
Abstract classes
===================

Abstract classes in Object Oriented Programming (OOP) are classes that cannot be instantiated.
In Python, abstract classes can be by inheriting the :class:`abc.ABC` class. Abstract classes also need to have
abstract methods, otherwise Python will not treat the class as abstract. Trying to create an instance (object)
from an abstract class results in a TypeError exception.

.. code-block:: python
from abc import ABC, abstractmethod
class MyAbstractClass(ABC):
@abstractmethod
def some_method(self):
pass
class MyClass(MyAbstractClass):
def some_method(self):
return 5 * 5
# OK
my_instance = MyClass()
# TypeError: Can't instantiate abstract class MyClass with abstract method some_method
my_abstract_instance = MyAbstractClass()
Let's modify our example from :ref:`Polymorphism`.

.. code-block:: python
:linenos:
:emphasize-lines: 1, 8, 12
from abc import ABC, abstractmethod
import tkinter as tk
import tkinter.ttk as ttk
import tkclasswiz as wiz
# An abstract class
class Wheel(ABC):
def __init__(self, diameter: float):
self.diameter = diameter
@abstractmethod
def get_info(self) -> str:
pass
class WinterWheel(Wheel):
def get_info(self) -> str:
return "Wheel for winter."
class SummerWheel(Wheel):
def get_info(self) -> str:
return "Wheel for summer."
class Car:
def __init__(self, name: str, speed: float, wheels: list[Wheel]):
self.name = name
self.speed = speed
self.wheels = wheels
if speed > 50_000:
raise ValueError("Car can go up to 50 000 km / h")
if len(wheels) != 4:
raise ValueError("The car must have 4 wheels!")
# Tkinter main window
root = tk.Tk("Test")
# Modified tkinter Combobox that will store actual objects instead of strings
combo = wiz.ComboBoxObjects(root)
combo.pack(fill=tk.X, padx=5)
def make_car(old = None):
"""
Function for opening a window either in new definition mode (old = None) or
edit mode (old != None)
"""
assert old is None or isinstance(old, wiz.ObjectInfo)
window = wiz.ObjectEditWindow() # The object definition window / wizard
window.open_object_edit_frame(Car, combo, old_data=old) # Open the actual frame
def print_defined():
data = combo.get()
data = wiz.convert_to_objects(data) # Convert any abstract ObjectInfo objects into actual Python objects
print(f"Object: {data}; Type: {type(data)}",) # Print the object and it's datatype
# Main GUI structure
ttk.Button(text="Define Car", command=make_car).pack()
ttk.Button(text="Edit Car", command=lambda: make_car(combo.get())).pack()
ttk.Button(text="Print defined", command=print_defined).pack()
root.mainloop()
We can see that the ``Wheel`` is now an abstract class.
It is then inherited by ``WinterWheel`` and ``SummerWheel``.
If we try to define the ``wheels`` parameter of our ``Car`` object, only these two inherited classes
will be definable.

.. image:: ./images/new_define_frame_list_abstractclass.png
:width: 15cm

We can see that while ``WinterWheel`` and ``SummerWheel`` are definable (due to :ref:`Polymorphism`),
``Wheel`` is not.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/source/guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ Index:
validation
annotations
conversion
polymorphism
abstractclasses
90 changes: 90 additions & 0 deletions docs/source/guide/polymorphism.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

====================================
Polymorphism
====================================

Polymorphism is a term used to describe the phenomenon of something appearing in multiple forms.
In nature an example would be a dog. A dog can appear in different forms (breeds). It also means
that multiple dog forms (breeds) are all derived from a common form - dog.

In the Object Oriented Programming (OOP), this means that an object might appear as either
an instance of a its actual class or an instance of a superclass.

Let's modify our original guide example:


.. code-block:: python
:linenos:
:emphasize-lines: 10,11,13,14,22
import tkinter as tk
import tkinter.ttk as ttk
import tkclasswiz as wiz
# Normal Python classes with annotations (type hints)
class Wheel:
def __init__(self, diameter: float):
self.diameter = diameter
class WinterWheel(Wheel):
pass
class SummerWheel(Wheel):
pass
class Car:
def __init__(
self,
name: str,
speed: float,
wheels: list[Wheel]
):
self.name = name
self.speed = speed
self.wheels = wheels
if speed > 50_000:
raise ValueError("Car can go up to 50 000 km / h")
if len(wheels) != 4:
raise ValueError("The car must have 4 wheels!")
# Tkinter main window
root = tk.Tk("Test")
# Modified tkinter Combobox that will store actual objects instead of strings
combo = wiz.ComboBoxObjects(root)
combo.pack(fill=tk.X, padx=5)
def make_car(old = None):
"""
Function for opening a window either in new definition mode (old = None) or
edit mode (old != None)
"""
assert old is None or isinstance(old, wiz.ObjectInfo)
window = wiz.ObjectEditWindow() # The object definition window / wizard
window.open_object_edit_frame(Car, combo, old_data=old) # Open the actual frame
def print_defined():
data = combo.get()
data = wiz.convert_to_objects(data) # Convert any abstract ObjectInfo objects into actual Python objects
print(f"Object: {data}; Type: {type(data)}",) # Print the object and it's datatype
# Main GUI structure
ttk.Button(text="Define Car", command=make_car).pack()
ttk.Button(text="Edit Car", command=lambda: make_car(combo.get())).pack()
ttk.Button(text="Print defined", command=print_defined).pack()
root.mainloop()
We can see that two new classes are created - ``WinterWheel`` and ``SummerWheel``.
We also see that ``Car``'s ``wheels`` parameter is still a list of type ``Wheel``.
TkClassWizard not only considers the annotated type when constructing a GUI, but also the annotated type's subclasses,
implementing the concept of polymorphism, thus allowing us definition of
``Wheel``, ``WinterWheel`` and ``SummerWheel`` classes.

.. image:: ./images/new_define_frame_list_polymorphism.png
:width: 15cm
2 changes: 1 addition & 1 deletion tkclasswiz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Works with Tkinter / TTKBootstrap.
"""

__version__ = "1.0.1"
__version__ = "1.1.0"

from .object_frame import *
from .annotations import *
Expand Down
27 changes: 22 additions & 5 deletions tkclasswiz/object_frame/frame_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import get_args, get_origin, Iterable, Union, Literal, Any, TYPE_CHECKING, TypeVar
from abc import ABC
from contextlib import suppress

from ..convert import *
Expand All @@ -11,6 +12,7 @@

import tkinter.ttk as ttk
import tkinter as tk
import json


if TYPE_CHECKING:
Expand Down Expand Up @@ -116,7 +118,7 @@ def cast_type(cls, value: Any, types: Iterable):
"""

CAST_FUNTIONS = {
# dict: lambda v: convert_dict_to_object_info(json.loads(v))
dict: lambda v: convert_to_object_info(json.loads(v))
}

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

@classmethod
def convert_types(cls, types_in):
def remove_wrapped(types: list):
"""
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
# Use the __bases__ instead of issubclass, because ABC is only supposed to denote
# classes abstract if they directly inherit it. In the case of multi-level inheritance
# issubclass would still return True, even though type_ is not a direct subclass ABC.
if ABC in type_.__bases__:
r.remove(type_)

return r

while get_origin(types_in) is Union:
Expand All @@ -163,12 +177,15 @@ def remove_wrapped(types: list):
# Also include inherited objects
subtypes = []
for t in types_in:
if hasattr(t, "__subclasses__") and t.__module__.split('.', 1)[0] in {"_discord", "daf"}:
if cls.get_cls_name(t) in __builtins__:
continue # Don't consider built-int types for polymorphism

if hasattr(t, "__subclasses__"):
for st in t.__subclasses__():
subtypes.extend(cls.convert_types(st))

# Remove wrapped classes (eg. wrapped by decorator)
return remove_wrapped(types_in + subtypes)
# Remove wrapped classes (eg. wrapped by decorator) + ABC classes
return remove_classes(types_in + subtypes)

def init_main_frame(self):
frame_main = ttk.Frame(self)
Expand Down

0 comments on commit 5a389fd

Please sign in to comment.