Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion TermTk/TTkCore/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,72 @@
.. autofunction:: TermTk.pyTTkSignal
.. autodecorator:: TermTk.pyTTkSlot
'''

import weakref

class WeakrefSlot:
'''
A WeakrefSlot instance saves a slot as weakref (or WeakMethod).

Hence the slots may be garbage collected, leaving this WeakrefSlot
without a referent.

WeakrefSlots are callable. They call the referent slot (if available).

WeakrefSlots have no __eq__ method hence they are compared with
python's "is" operator (identitiy check). This is important for
WekrefSlots inside lists.

WeakrefSlots have the _referent_gone flag which is set after the
WeakrefSlot has detected that the referent was already garbage
collected. This flag may be used to easily delete WeakrefSlots
without referents.
'''
__slots__ = ('_weak_slotref', '_referent_gone')

def __init__(self, slot):
'''
initialize WekrefSlot for the input argument slot (which must be callable).

WeakMethod is used for bounded methods.

Attention:
A slot object which has no (other) references than the given
slot may be garbage collected immediately after this call.
They are "dead on arrival". A typical example for this is a
inline lambda function. That's *the* feature of WeakrefSlots.
'''
if not callable(slot):
raise TypeError(f'slot must be callable; I found {type(slot)=}')
self._referent_gone = False
bound_method = hasattr(slot, '__self__') and hasattr(slot, '__func__')
self._weak_slotref = (
weakref.WeakMethod(slot) if bound_method else weakref.ref(slot))

def __str__(self):
'''show target (if avail).'''
target = None if self._referent_gone else self._weak_slotref()
target_str = 'dead' if target is None else f'to {str(target)}'
return f'<{self.__class__.__name__} at {hex(id(self))}; {target_str}>'

def __call__(self, *args, **kwargs):
'''
If the referent of _weak_slotref was not garbage collected, then
call the referent.

Checks and updates _referent_gone flag.
'''
if self._referent_gone:
return # Nothing to do; we already know the referent is gone
func = self._weak_slotref()
if func is None:
# referent was gargabe collected (since the last test)
self._referent_gone = True
return # Nothing to do
func(*args, **kwargs) # call referent slot
# throw away the (strong) reference "func" now


def pyTTkSlot(*args, **kwargs):
def pyTTkSlot_d(func):
# Add signature attributes to the function
Expand Down Expand Up @@ -88,7 +154,7 @@ def __init__(self, *args, **kwargs):
self._connected_slots = []
_pyTTkSignal_obj._signals.append(self)

def connect(self, slot):
def connect(self, slot, use_weak_ref=False):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to think about having the "weak ref" true by default,
I am not sure what it is going to happen if connected to lambda or nedted functions.
For the naming convention I tried to use the camel case although not recommended by PEP8
because I started coding it following the QT api structure

# ref: http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#connect

# connect(slot[, type=PyQt5.QtCore.Qt.AutoConnection[, no_receiver_check=False]]) -> PyQt5.QtCore.QMetaObject.Connection
Expand All @@ -108,8 +174,11 @@ def connect(self, slot):
if not issubclass(a,b):
error = "Decorated slot has no signature compatible: "+slot.__name__+str(slot._TTkslot_attr)+" != signal"+str(self._types)
raise TypeError(error)
if use_weak_ref:
slot = WeakrefSlot(slot) # replace slot with WeakrefSlot
if slot not in self._connected_slots:
self._connected_slots.append(slot)
return slot # may return WeakrefSlot which may be used for disconnect

def disconnect(self, *args, **kwargs):
for slot in args:
Expand All @@ -120,8 +189,18 @@ def emit(self, *args, **kwargs):
if len(args) != len(self._types):
error = "func"+str(self._types)+" signal has "+str(len(self._types))+" argument(s) but "+str(len(args))+" provided"
raise TypeError(error)

need_weakref_cleanup = False # have we seen WeakrefSlots without referent?
for slot in self._connected_slots:
slot(*args, **kwargs)
if isinstance(slot, WeakrefSlot) and slot._referent_gone:
need_weakref_cleanup = True

if need_weakref_cleanup:
# remove WeakrefSlots where referent was gone
for slot in self._connected_slots[:]: # do a slice for safe removal
if isinstance(slot, WeakrefSlot) and slot._referent_gone:
self._connected_slots.remove(slot)

def clear(self):
self._connected_slots = []
Expand Down
2 changes: 1 addition & 1 deletion TermTk/TTkWidgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,4 +658,4 @@ def setLookAndFeel(self, laf):
if not laf:
laf = TTkLookAndFeel()
self._lookAndFeel = laf
self._lookAndFeel.modified.connect(self.update)
self._lookAndFeel.modified.connect(self.update, use_weak_ref=True)
17 changes: 10 additions & 7 deletions TermTk/TTkWidgets/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ class _MinimizedButton(TTkButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._windowWidget = kwargs.get('windowWidget')
def _cb():
self.clicked.connect(self.perfomAction, use_weak_ref=True)

def perfomAction(self):
'''show _windowWidget and close self.'''
if self._windowWidget is not None:
self._windowWidget.show()
self.close()
self.clicked.connect(_cb)
self.close()

class TTkWindow(TTkResizableFrame):
__slots__ = (
Expand All @@ -61,18 +64,18 @@ def __init__(self, *args, **kwargs):
self.rootLayout().addItem(self._winTopLayout)
# Close Button
self._btnClose = TTkButton(border=False, text="x", size=(3,1), maxWidth=3, minWidth=3, visible=False)
self._btnClose.clicked.connect(self.close)
self._btnClose.clicked.connect(self.close, use_weak_ref=True)
# Max Button
self._maxBk = None
self._btnMax = TTkButton(border=False, text="^", size=(3,1), maxWidth=3, minWidth=3, visible=False)
self._btnMax.clicked.connect(self._maximize)
self._btnMax.clicked.connect(self._maximize, use_weak_ref=True)
# Min Button
self._btnMin = TTkButton(border=False, text="_", size=(3,1), maxWidth=3, minWidth=3, visible=False)
self._btnMin.clicked.connect(self._minimize)
self._btnMin.clicked.connect(self._minimize, use_weak_ref=True)
# Button Reduce_border
self._redBk = None
self._btnReduce = TTkButton(border=False, text=".", size=(3,1), maxWidth=3, minWidth=3, visible=False)
self._btnReduce.clicked.connect(self._reduce)
self._btnReduce.clicked.connect(self._reduce, use_weak_ref=True)

self._winTopLayout.addItem(TTkLayout(),0,0)
self._winTopLayout.addWidget(self._btnClose, 0,4)
Expand Down
122 changes: 122 additions & 0 deletions tests/test.ui.024.weakslots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# vim:ts=4:sw=4:fdm=indent:cc=79:

# MIT License
#
# Copyright (c) 2022 Luchr <https://github.com/luchr>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import datetime
import gc

import TermTk

WIN_COUNTER = 0

class ShowEventWindow(TermTk.TTkWindow):
__slots__ = ('_win_number', '_event_info')

def __init__(self, parent, event):
global WIN_COUNTER
WIN_COUNTER += 1
win_number = WIN_COUNTER
self._win_number = win_number
win_layout = TermTk.TTkGridLayout(columnMinHeight=1)
TermTk.TTkWindow.__init__(
self, parent=parent, pos=(10, 18+win_number), size=(45, 5),
title=f'Window {win_number}',
layout=win_layout)
if win_number == 1:
self.setWindowFlag(0x0)
self._event_info = TermTk.TTkLabel(
text='updated if main button is clicked')
win_layout.addWidget(self._event_info, 0, 0)
event.connect(self.show_event, use_weak_ref=True)

def show_event(self):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self._event_info.setText(f'last seen event: {now}')
TermTk.TTkLog.debug(f'show_event of window {self._win_number}')
gc.collect()
refs = gc.get_referrers(self)
TermTk.TTkLog.debug(f'{len(refs)=}')
for ref in refs:
TermTk.TTkLog.debug(f'{id(ref)=}: {repr(ref)}')


class MainWindow(TermTk.TTkWindow):
__slots__ = ('_main_button', '_slot_info', '_info_timer', '_timer_delay')

def __init__(self, parent, timer_delay):
self._timer_delay = timer_delay
win_layout = TermTk.TTkGridLayout(columnMinHeight=1)
TermTk.TTkWindow.__init__(
self, parent=parent, pos=(1, 1), size=(50, 17), title='Main Window',
layout=win_layout)
self.setWindowFlag(0x0)

main_button = TermTk.TTkButton(text='main test button', border=True)
self._main_button = main_button
win_layout.addWidget(main_button, 0, 0)

new_win_button = TermTk.TTkButton(
text='create new listener window', border=True)
new_win_button.clicked.connect(
lambda: create_show_event_window(parent, main_button.clicked))
win_layout.addWidget(new_win_button, 2, 0)

slot_info = TermTk.TTkLabel(text='')
self._slot_info = slot_info
win_layout.addWidget(slot_info, 4, 0)

quit_button = TermTk.TTkButton(text='quit', border=True)
quit_button.clicked.connect(parent.quit)
win_layout.addWidget(quit_button, 6, 0)

self._info_timer = TermTk.TTkTimer()
self._info_timer.timeout.connect(self.show_info)
self._info_timer.start(timer_delay)

create_show_event_window(parent, main_button.clicked)

def show_info(self):
len_slots = len(self._main_button.clicked._connected_slots)
self._slot_info.setText(f"Main button has {len_slots} slots/listeners ")
self._info_timer.start(self._timer_delay)


def create_show_event_window(parent, event):
win = ShowEventWindow(parent, event)
win.setFocus()

def main():
'''show main window and start event loop.'''
root = TermTk.TTk()
TermTk.TTkLog.use_default_file_logging()

MainWindow(root, 0.5)

root.mainloop()


if __name__ == '__main__':
main()

# EOF