Skip to content

Commit 5e948d7

Browse files
authored
Generic type classes (#18)
* Generic typing * simplify * docs
1 parent 48c39a9 commit 5e948d7

File tree

7 files changed

+105
-7
lines changed

7 files changed

+105
-7
lines changed

docs/source/changelog.rst

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

27+
v1.2
28+
================
29+
- Generic types support (Parametric types)
2730

2831
v1.1.1
2932
================

docs/source/guide/generics.rst

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
=========================
2+
Generic types (classes)
3+
=========================
4+
5+
A generic type in Python is a class, that inherits the :class:`typing.Generic` class.
6+
It can be used to define arbitrary type parameters, which can be given a value using a subscription operator ``[]``.
7+
8+
Arbitrary types / type variables can be created using the :class:`typing.TypeVar` class.
9+
10+
.. note::
11+
12+
Since Python 3.12, the :class:`typing.TypeVar` no longer requires manual definition
13+
and can be omitted.
14+
15+
16+
.. code-block:: python
17+
:linenos:
18+
19+
from typing import Generic, TypeVar
20+
21+
22+
T = TypeVar('T') # From Python 3.12 forward this is optional
23+
24+
class MyClass(Generic[T]):
25+
def __init__(self, a: int, b: T):
26+
...
27+
28+
29+
my_instance1: MyClass[float] = MyClass(5, 5.5)
30+
my_instance2: MyClass[str] = MyClass(5, "Some text")
31+
my_instance3: MyClass[int] = MyClass(5, 0)
32+
33+
34+
In the above example, the ``T`` is a type variable. It is also a type parameter inside ``MyClass``.
35+
When instances are created, the variables are annotated with a type subscription (e. g., ``MyClass[int]``).
36+
37+
While Python itself doesn't care about the types given in the subscription, and just ignores incorrect types,
38+
TkClassWizard resolves these subscripted types into the ``__init__`` function of a class, allowing the definition
39+
of arbitrary types with the use of type generics.
40+
41+
If we take the above example and pop it into TkClassWizard with ``MyClass`` having a :class:`float` annotation,
42+
we would get the following types in the ``New <type>`` options:
43+
44+
.. image:: ./images/new_define_frame_struct_generics.png
45+
:width: 15cm
46+
47+
48+
.. code-block:: python
49+
:caption: Generic type, given :class:`float` as type parameter
50+
:linenos:
51+
:emphasize-lines: 21
52+
53+
from typing import Generic, TypeVar
54+
55+
import tkclasswiz as wiz
56+
import tkinter as tk
57+
import tkinter.ttk as ttk
58+
59+
T = TypeVar('T') # From Python 3.12 forward this is optional
60+
61+
class MyClass(Generic[T]):
62+
def __init__(self, a: int, b: T):
63+
...
64+
65+
root = tk.Tk("Test")
66+
67+
combo = wiz.ComboBoxObjects(root)
68+
combo.pack(fill=tk.X, padx=5)
69+
70+
def open():
71+
window = wiz.ObjectEditWindow() # The object definition window / wizard
72+
window.open_object_edit_frame(
73+
MyClass[float],
74+
combo
75+
) # Open the actual frame
76+
77+
ttk.Button(text="Define", command=open).pack()
78+
root.mainloop()
27 KB
Loading

docs/source/guide/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ Index:
1414
annotations
1515
conversion
1616
polymorphism
17-
abstractclasses
17+
abstractclasses
18+
generics

tkclasswiz/annotations.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Module used for managing annotations.
33
"""
44
from datetime import datetime, timedelta, timezone
5-
from typing import Union, Optional
5+
from typing import Union, Optional, get_args, Generic, get_origin
66
from contextlib import suppress
77
from inspect import isclass
88
from .doc import doc_category
@@ -88,13 +88,25 @@ def get_annotations(class_) -> dict:
8888
"""
8989
Returns class / function annotations including the ones extended with ``register_annotations``.
9090
It does not return the return annotation.
91+
92+
Additionally, this function resolves any generic types to their parameterized types, but
93+
only for classes, functions don't support this yet as support for generics on functions was added
94+
in Python 3.12.
9195
"""
9296
annotations = {}
9397
with suppress(AttributeError):
9498
if isclass(class_):
95-
annotations = class_.__init__.__annotations__
99+
annotations = class_.__init__.__annotations__.copy()
100+
elif isclass(origin_class := get_origin(class_)) and issubclass(origin_class, Generic):
101+
# Resolve generics
102+
annotations = origin_class.__init__.__annotations__.copy()
103+
generic_types = get_args(origin_class.__orig_bases__[0])
104+
generic_values = get_args(class_)
105+
generic_name_value = {generic_types[i]: generic_values[i] for i in range(len(generic_types))}
106+
for k, v in annotations.items():
107+
annotations[k] = generic_name_value.get(v, v)
96108
else:
97-
annotations = class_.__annotations__
109+
annotations = class_.__annotations__.copy()
98110

99111
additional_annotations = ADDITIONAL_ANNOTATIONS.get(class_, {})
100112
annotations = {**annotations, **additional_annotations}

tkclasswiz/object_frame/frame_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ def get_cls_name(cls: T) -> Union[str, T]:
9696
Returns the name of the class ``cls`` or
9797
the original class when the name cannot be obtained.
9898
"""
99-
if hasattr(cls, "_name"):
100-
return cls._name
10199
if hasattr(cls, "__name__"):
102100
return cls.__name__
101+
if hasattr(cls, "_name"):
102+
return cls._name
103103
else:
104104
return cls
105105

tkclasswiz/object_frame/frame_struct.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,11 @@ def to_object(self, *, ignore_checks = False) -> ObjectInfo:
245245
map_[attr] = value
246246

247247
object_ = ObjectInfo(self.class_, map_) # Abstraction of the underlaying object
248-
if not ignore_checks and self.check_parameters and inspect.isclass(self.class_): # Only check objects
248+
if (
249+
not ignore_checks and
250+
self.check_parameters and
251+
(inspect.isclass(self.class_) or inspect.isclass(get_origin(self.class_))) # Only check objects
252+
):
249253
# Cache the object created for faster
250254
_convert_to_objects_cached(object_) # Tries to create instances to check for errors
251255

0 commit comments

Comments
 (0)