Skip to content

Commit 4140d14

Browse files
feat: support Union types in the signals/slots (#431)
1 parent c244d6f commit 4140d14

9 files changed

Lines changed: 215 additions & 49 deletions

File tree

.github/workflows/testing.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,5 @@ jobs:
7575
wget -O tmp/test.input.001.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.001.bin
7676
wget -O tmp/test.input.002.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.002.bin
7777
wget -O tmp/test.input.003.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.003.bin
78-
pytest ${DDDD}/tests/pytest/test_003_string.py
79-
pytest ${DDDD}/tests/pytest/test_002_textedit.py
80-
pytest ${DDDD}/tests/pytest/test_001_demo.py
78+
pytest ${DDDD}/tests/pytest/test_*
79+
pytest ${DDDD}/tests/pytest/run_*

.vscode/settings.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"-vv", "--color=yes", "-s"
55
],
66
"python.testing.unittestEnabled": false,
7-
"python.testing.pytestEnabled": true
7+
"python.testing.pytestEnabled": true,
8+
"mypy-type-checker.args": [
9+
"--config-file=pyproject.toml"
10+
],
11+
"python.analysis.extraPaths": [
12+
"./libs/pyTermTk",
13+
]
814
}

Makefile

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ test: .venv
141141
. .venv/bin/activate ; \
142142
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude .venv,build,tmp,experiments ;
143143
. .venv/bin/activate ; \
144-
pytest tests/pytest/test_003_string.py ;
144+
pytest tests/pytest/test_*
145145
. .venv/bin/activate ; \
146-
pytest tests/pytest/test_002_textedit.py ;
147-
. .venv/bin/activate ; \
148-
pytest -v tests/pytest/test_001_demo.py ;
149-
146+
pytest tests/pytest/run_*

libs/pyTermTk/TermTk/TTkCore/signal.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
# from typing import TypeVar, TypeVarTuple, Generic, List
6161
from inspect import getfullargspec, iscoroutinefunction
6262
from types import LambdaType
63+
from typing import Union, get_origin, get_args
6364
from threading import Lock
6465
import asyncio
6566

@@ -82,6 +83,29 @@ def _async_runner(coros):
8283
def _run_coroutines(coros):
8384
Thread(target=_async_runner, args=(coros,)).start()
8485

86+
def _check_types(slot_type, signal_type):
87+
''' Comparing the signal and slot types
88+
89+
return True if the slot_type is compatible with the signal_type
90+
'''
91+
if signal_type==slot_type: return True
92+
93+
if get_origin(slot_type) is Union:
94+
_sl_ts = get_args(slot_type)
95+
else:
96+
_sl_ts = (slot_type,)
97+
98+
if get_origin(signal_type) is Union:
99+
_sig_ts = get_args(signal_type)
100+
else:
101+
_sig_ts = (signal_type,)
102+
103+
for _sig_t in _sig_ts:
104+
if not any( issubclass(_sl_t, _sig_t) for _sl_t in _sl_ts):
105+
return False
106+
107+
return True
108+
85109
def pyTTkSlot(*args):
86110
def pyTTkSlot_d(func):
87111
# Add signature attributes to the function
@@ -139,10 +163,9 @@ def connect(self, slot):
139163
error = "Decorated slot has no signature compatible: "+slot.__name__+str(slot._TTkslot_attr)+" != signal"+str(self._types)
140164
raise TypeError(error)
141165
else:
142-
for a,b in zip(slot._TTkslot_attr, self._types):
143-
if a!=b and not issubclass(a,b):
144-
error = "Decorated slot has no signature compatible: "+slot.__name__+str(slot._TTkslot_attr)+" != signal"+str(self._types)
145-
raise TypeError(error)
166+
for slot_type,signal_type in zip(slot._TTkslot_attr, self._types):
167+
if not _check_types(slot_type, signal_type):
168+
raise TypeError("Decorated slot has no signature compatible: "+slot.__name__+str(slot._TTkslot_attr)+" != signal"+str(self._types))
146169
if iscoroutinefunction(slot):
147170
if slot not in self._connected_async_slots:
148171
self._connected_async_slots[slot]=slice(nargs)

libs/pyTermTk/TermTk/TTkCore/string.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from __future__ import annotations
2424

25-
__all__ = ['TTkString']
25+
__all__ = ['TTkString','TTkStringType']
2626

2727
import os
2828
import re
@@ -712,4 +712,6 @@ def _getDataW_tty(self):
712712
if os.environ.get("TERMTK_GPM",False):
713713
_getDataW = _getDataW_tty
714714
else:
715-
_getDataW = _getDataW_pts
715+
_getDataW = _getDataW_pts
716+
717+
TTkStringType = Union[str, TTkString]

libs/pyTermTk/pyproject.toml

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,49 @@
11
[build-system]
2-
requires = ["setuptools>=42", "wheel"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["setuptools>=42", "wheel"]
3+
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "pyTermTk"
7-
dynamic = ["version"]
8-
requires-python = ">=3.9"
9-
description = "Python Terminal Toolkit"
10-
readme = {file = "README.md", content-type = "text/markdown"}
11-
license = { text = "MIT License" }
12-
authors = [
13-
{ name = "Eugenio Parodi", email = "ceccopierangiolieugenio@googlemail.com" }
14-
]
15-
classifiers = [
16-
"Programming Language :: Python :: 3",
17-
"License :: OSI Approved :: MIT License",
18-
"Operating System :: OS Independent",
19-
"Development Status :: 3 - Alpha",
20-
"Environment :: Console",
21-
"Intended Audience :: Education",
22-
"Intended Audience :: Developers",
23-
"Intended Audience :: Information Technology",
24-
"Topic :: Terminals",
25-
"Topic :: Text Editors :: Text Processing",
26-
"Topic :: Software Development :: User Interfaces",
27-
"Topic :: Software Development :: Libraries",
28-
"Topic :: Software Development :: Libraries :: Python Modules",
29-
"Topic :: Software Development :: Libraries :: Application Frameworks"
30-
]
6+
name = "pyTermTk"
7+
dynamic = ["version"]
8+
requires-python = ">=3.9"
9+
description = "Python Terminal Toolkit"
10+
readme = {file = "README.md", content-type = "text/markdown"}
11+
license = { text = "MIT License" }
12+
authors = [
13+
{ name = "Eugenio Parodi", email = "ceccopierangiolieugenio@googlemail.com" }
14+
]
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"License :: OSI Approved :: MIT License",
18+
"Operating System :: OS Independent",
19+
"Development Status :: 3 - Alpha",
20+
"Environment :: Console",
21+
"Intended Audience :: Education",
22+
"Intended Audience :: Developers",
23+
"Intended Audience :: Information Technology",
24+
"Topic :: Terminals",
25+
"Topic :: Text Editors :: Text Processing",
26+
"Topic :: Software Development :: User Interfaces",
27+
"Topic :: Software Development :: Libraries",
28+
"Topic :: Software Development :: Libraries :: Python Modules",
29+
"Topic :: Software Development :: Libraries :: Application Frameworks"
30+
]
3131

3232
[project.urls]
33-
Homepage = "https://github.com/ceccopierangiolieugenio/pyTermTk"
34-
Documentation = "https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/"
35-
Repository = "https://github.com/ceccopierangiolieugenio/pyTermTk.git"
36-
Issues = "https://github.com/ceccopierangiolieugenio/pyTermTk/issues"
37-
Changelog = "https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/libs/pyTermTk/CHANGELOG.md"
33+
Homepage = "https://github.com/ceccopierangiolieugenio/pyTermTk"
34+
Documentation = "https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/"
35+
Repository = "https://github.com/ceccopierangiolieugenio/pyTermTk.git"
36+
Issues = "https://github.com/ceccopierangiolieugenio/pyTermTk/issues"
37+
Changelog = "https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/libs/pyTermTk/CHANGELOG.md"
3838

3939
[tool.setuptools.packages.find]
40-
where = ["."]
40+
where = ["."]
4141

4242
[tool.setuptools.dynamic]
43-
version = {attr = "TermTk.__version__"}
43+
version = {attr = "TermTk.__version__"}
44+
45+
[tool.mypy]
46+
ignore_missing_imports = true
47+
warn_unused_configs = true
48+
disallow_any_generics = true
49+
disallow_subclassing_any = true

tests/pytest/test_004_slots.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
import sys, os
24+
import pytest
25+
from typing import Union, Optional
26+
27+
sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
28+
29+
import TermTk as ttk
30+
31+
def test_slots_stringType():
32+
signal1 = ttk.pyTTkSignal(str)
33+
signal2 = ttk.pyTTkSignal(ttk.TTkString)
34+
signal3 = ttk.pyTTkSignal(ttk.TTkStringType)
35+
36+
@ttk.pyTTkSlot(str)
37+
def _test_slot1(txt:str):
38+
print("slot 1", txt)
39+
40+
@ttk.pyTTkSlot(ttk.TTkString)
41+
def _test_slot2(txt:ttk.TTkString):
42+
print("slot 2", txt)
43+
44+
@ttk.pyTTkSlot(ttk.TTkStringType)
45+
def _test_slot3(txt:ttk.TTkStringType):
46+
print("slot 3", txt)
47+
48+
signal1.connect(_test_slot1)
49+
signal2.connect(_test_slot2)
50+
signal3.connect(_test_slot3)
51+
52+
signal1.connect(_test_slot3)
53+
signal2.connect(_test_slot3)
54+
55+
with pytest.raises(TypeError):
56+
signal1.connect(_test_slot2)
57+
with pytest.raises(TypeError):
58+
signal2.connect(_test_slot1)
59+
with pytest.raises(TypeError):
60+
signal3.connect(_test_slot1)
61+
with pytest.raises(TypeError):
62+
signal3.connect(_test_slot2)
63+
64+
print('OKKK')
65+
66+
signal1.emit('Eugenio1')
67+
signal2.emit('Eugenio2')
68+
signal3.emit('Eugenio3')
69+
70+
def test_slots_unionType():
71+
t1 = ttk.TTkWidget
72+
t2 = ttk.TTkContainer
73+
t3 = ttk.TTkFrame
74+
# TTkContainer extends TTkWidget
75+
# TTkWindow extends TTkFrame
76+
# TTkFileButtonPicker extends TTkButton
77+
t4 = Union[ttk.TTkFrame, ttk.TTkButton]
78+
t5 = Union[ttk.TTkWindow, ttk.TTkFileButtonPicker]
79+
80+
signal1 = ttk.pyTTkSignal(t1)
81+
signal2 = ttk.pyTTkSignal(t2)
82+
signal3 = ttk.pyTTkSignal(t3)
83+
signal4 = ttk.pyTTkSignal(t4)
84+
signal5 = ttk.pyTTkSignal(t5)
85+
86+
@ttk.pyTTkSlot(t1)
87+
def _test_slot1(_): pass
88+
@ttk.pyTTkSlot(t2)
89+
def _test_slot2(_): pass
90+
@ttk.pyTTkSlot(t3)
91+
def _test_slot3(_): pass
92+
@ttk.pyTTkSlot(t4)
93+
def _test_slot4(_): pass
94+
@ttk.pyTTkSlot(t5)
95+
def _test_slot5(_): pass
96+
97+
signal1.connect(_test_slot1)
98+
signal1.connect(_test_slot2)
99+
signal1.connect(_test_slot3)
100+
signal1.connect(_test_slot4)
101+
signal1.connect(_test_slot5)
102+
103+
signal2.connect(_test_slot2)
104+
signal2.connect(_test_slot3)
105+
signal2.connect(_test_slot4)
106+
signal2.connect(_test_slot5)
107+
108+
signal3.connect(_test_slot3)
109+
signal3.connect(_test_slot4)
110+
signal3.connect(_test_slot5)
111+
112+
signal4.connect(_test_slot4)
113+
signal4.connect(_test_slot5)
114+
115+
signal5.connect(_test_slot5)
116+
117+
with pytest.raises(TypeError):
118+
signal5.connect(_test_slot4)
119+
with pytest.raises(TypeError):
120+
signal5.connect(_test_slot1)
121+
122+
with pytest.raises(TypeError):
123+
@ttk.pyTTkSlot(float)
124+
def _slot(_): pass
125+
ttk.pyTTkSignal(int).connect(_slot)
126+
with pytest.raises(TypeError):
127+
@ttk.pyTTkSlot(int)
128+
def _slot(_): pass
129+
ttk.pyTTkSignal(Union[int,float]).connect(_slot)
130+
with pytest.raises(TypeError):
131+
@ttk.pyTTkSlot(Union[int,str])
132+
def _slot(_): pass
133+
ttk.pyTTkSignal(Union[int,float]).connect(_slot)

0 commit comments

Comments
 (0)