diff --git a/TermTk/TTkCore/signal.py b/TermTk/TTkCore/signal.py index ea3a3142..63ddb5c7 100644 --- a/TermTk/TTkCore/signal.py +++ b/TermTk/TTkCore/signal.py @@ -65,6 +65,7 @@ def pyTTkSlot(*args, **kwargs): def pyTTkSlot_d(func): # Add signature attributes to the function func._TTkslot_attr = args + func._TTkslot_sigList = [] return func return pyTTkSlot_d @@ -72,7 +73,6 @@ def pyTTkSignal(*args, **kwargs): return _pyTTkSignal_obj(*args, **kwargs) class _pyTTkSignal_obj(): - _signals = [] __slots__ = ('_types', '_name', '_revision', '_connected_slots', '_mutex') def __init__(self, *args, **kwargs): # ref: http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#PyQt5.QtCore.pyqtSignal @@ -92,7 +92,6 @@ class _pyTTkSignal_obj(): self._revision = kwargs.get('revision', 0) self._connected_slots = {} self._mutex = Lock() - _pyTTkSignal_obj._signals.append(self) def connect(self, slot): # ref: http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#connect @@ -126,6 +125,9 @@ class _pyTTkSignal_obj(): 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 hasattr(slot, '_TTkslot_sigList'): + if self not in slot._TTkslot_sigList: + slot._TTkslot_sigList.append(self) if slot not in self._connected_slots: self._connected_slots[slot]=slice(nargs) @@ -133,6 +135,9 @@ class _pyTTkSignal_obj(): for slot in args: if slot in self._connected_slots: del self._connected_slots[slot] + if hasattr(slot, '_TTkslot_sigList'): + if self in slot._TTkslot_sigList: + slot._TTkslot_sigList.remove(self) def emit(self, *args, **kwargs): if not self._mutex.acquire(False): return @@ -147,9 +152,8 @@ class _pyTTkSignal_obj(): self._connected_slots = {} @staticmethod - def clearAll(): - for s in _pyTTkSignal_obj._signals: - s.clear() + def clearAll(self): + pass def forward(self): def _ret(*args, **kwargs): diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index 2e095268..09407c06 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -77,7 +77,7 @@ class TTk(TTkContainer): #canvas.drawChar((0,0),'✜') __slots__ = ( - '_input', '_termMouse', '_termDirectMouse', + '_termMouse', '_termDirectMouse', '_title', '_showMouseCursor', '_sigmask', '_timer', @@ -327,6 +327,7 @@ class TTk(TTkContainer): '''Tells the application to exit with a return code.''' if self._timer: self._timer.timeout.disconnect(self._time_event) + self.dispose(children=True) TTkInput.inputEvent.clear() self._paintEvent.set() TTkInput.close() diff --git a/TermTk/TTkWidgets/container.py b/TermTk/TTkWidgets/container.py index 71fdfc6e..a41b0008 100644 --- a/TermTk/TTkWidgets/container.py +++ b/TermTk/TTkWidgets/container.py @@ -82,6 +82,14 @@ class TTkContainer(TTkWidget): self._layout.setParent(self) self.update(updateLayout=True) + def dispose(self, children:bool=False): + super().dispose() + if children: + cl = [_w for _w in self._layout.iterWidgets(onlyVisible=False, recurse=True)] + for wid in reversed(cl): + wid.dispose() + TTkLog.debug(wid._name) + def addWidget(self, widget): ''' .. warning:: diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py index 18372076..4e12981b 100644 --- a/TermTk/TTkWidgets/widget.py +++ b/TermTk/TTkWidgets/widget.py @@ -22,6 +22,8 @@ __all__ = ['TTkWidget'] +import types + from TermTk.TTkCore.cfg import TTkCfg, TTkGlbl from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.log import TTkLog @@ -98,7 +100,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): '_maxw', '_maxh', '_minw', '_minh', '_focus','_focus_policy', '_canvas', '_widgetItem', - '_visible', '_transparent', + '_visible', '_pendingMouseRelease', '_enabled', '_style', '_currentStyle', @@ -168,7 +170,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): def __del__(self): ''' .. caution:: Don't touch this! ''' - # TTkLog.debug("DESTRUCTOR") + # TTkLog.debug(f"DESTRUCTOR -> {self._name}") # clean all the signals, slots #for an in dir(self): @@ -465,6 +467,19 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): @pyTTkSlot() def close(self): '''close''' + self._dispose() + self.closed.emit(self) + + def _dispose(self): + refSig = type(pyTTkSignal()) + for p in dir(self): + if type(pr:=getattr(self,p)) != types.MethodType: + continue + if hasattr(pr, '_TTkslot_sigList'): + for sig in pr._TTkslot_sigList.copy(): + sig.disconnect(pr) + if type(pr) is refSig: + pr.clear() if _p := self._parent: if _rl := _p.rootLayout(): _rl.removeWidget(self) @@ -472,7 +487,13 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): TTkHelper.removeOverlayAndChild(self) self._parent = None self.hide() - self.closed.emit(self) + # self._widgetItem._widget = None + # self._widgetItem = None + # self._canvas = None + + def dispose(self): + '''dispose''' + self._dispose() @pyTTkSlot(bool) def setVisible(self, visible: bool): diff --git a/tests/t.ui/test.ui.016.about.py b/tests/t.ui/test.ui.016.about.py index 18f29d0b..88c368e5 100755 --- a/tests/t.ui/test.ui.016.about.py +++ b/tests/t.ui/test.ui.016.about.py @@ -31,6 +31,6 @@ ttk.TTkLog.use_default_file_logging() root = ttk.TTk() -win = ttk.TTkAbout(parent=root, pos=(1,1)) +ttk.TTkAbout(parent=root, pos=(1,1)) root.mainloop() \ No newline at end of file diff --git a/tests/weakref/test.06.gc.01.py b/tests/weakref/test.06.gc.01.py new file mode 100755 index 00000000..65c1c077 --- /dev/null +++ b/tests/weakref/test.06.gc.01.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref, time +import os,sys + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +ttk.TTkLog.use_default_stdout_logging() + +class testClass(): + pass + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +def _ref(o,ch="*"): + # print(f"\n### -> Referents - {o}") + # for i,r in enumerate(gc.get_referents(o)): + # print(f" - {i} ) {r}") + print(f"\n### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + print(f" {ch} {i} ) ({type(r)})-> {r}") + print("") + +v1 = {'b':2345} + +print(f"\nStart {gc.isenabled()=}") +# print(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") + +# gc.callbacks.append(_gccb) + +print("\n############# Phase 1 ##################") +foo = ttk.TTkAbout() +bar1 = ttk.TTkButton() +bar2 = ttk.TTkWidget() +pep = testClass() + +ttk.TTkHelper._updateWidget.clear() +ttk.TTkHelper._updateBuffer.clear() + +print(f"{gc.collect()=}") +print(f"Mid {gc.get_count()=}") + +_ref(foo) +_ref(foo) +_ref(foo, "-Active-") + +foo.close() +ttk.TTkHelper._updateWidget.clear() +ttk.TTkHelper._updateBuffer.clear() + +_ref(foo, "-Closed-") +del foo +# _ref(bar1) +# _ref(pep) + +print(f"{gc.collect()=}") +print(f"End {gc.get_count()=}") +# time.sleep(3) + diff --git a/tests/weakref/test.06.gc.02.py b/tests/weakref/test.06.gc.02.py new file mode 100755 index 00000000..d9e14890 --- /dev/null +++ b/tests/weakref/test.06.gc.02.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref, time +import os,sys + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +# ttk.TTkLog.use_default_stdout_logging() + +class testClass(): + pass + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + ttk.TTkLog.info(f" * {i} - {ss}") + +def _ref(o,ch="*"): + # ttk.TTkLog.info(f"\n### -> Referents - {o}") + # for i,r in enumerate(gc.get_referents(o)): + # ttk.TTkLog.info(f" - {i} ) {r}") + ttk.TTkLog.info(f"### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + ttk.TTkLog.info(f" {ch} {i} ) ({type(r)})-> {r}") + ttk.TTkLog.info("") + +def _gccb(phase,info): + ttk.TTkLog.info(f" ---> {gc.garbage=}") + ttk.TTkLog.info(f" ---> {phase=} {info=}") + +class Button2(ttk.TTkButton):pass + +root = ttk.TTk() +ttk.TTkLogViewer(parent=root, pos=(20,0), size=(150,30)) + +ttk.TTkLog.info(f"Start {gc.isenabled()=}") +# ttk.TTkLog.info(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +# gc.callbacks.append(_gccb) + +ttk.TTkLog.info("############# Phase 1 ##################") + +foo = ttk.TTkAbout(parent=root) + +bar1 = Button2(parent=root, border=True, text="Do it") +bar2 = ttk.TTkWidget() +pep = testClass() + +ttk.TTkLog.info(f"{gc.collect()=}") +ttk.TTkLog.info(f"Mid {gc.get_count()=}") + + +@ttk.pyTTkSlot() +def _doit(): + ttk.TTkLog.info(f"{gc.collect()=}") + ttk.TTkLog.info(f"End {gc.get_count()=}") +bar1.clicked.connect(_doit) +bar1.clicked.connect(foo.close) + +foo.closed.connect(lambda _x: _ref(_x, "-Closed-")) + +_ref(foo, "-Active-") +del foo + +root.mainloop() + +foo.close() + +_ref(foo, "-Closed-") diff --git a/tests/weakref/test.06.gc.03.py b/tests/weakref/test.06.gc.03.py new file mode 100755 index 00000000..34c41169 --- /dev/null +++ b/tests/weakref/test.06.gc.03.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref, time +import os,sys + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +# ttk.TTkLog.use_default_stdout_logging() + +class testClass(): + pass + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + ttk.TTkLog.info(f" * {i} - {ss}") + +def _ref(o,ch="*"): + # ttk.TTkLog.info(f"\n### -> Referents - {o}") + # for i,r in enumerate(gc.get_referents(o)): + # ttk.TTkLog.info(f" - {i} ) {r}") + ttk.TTkLog.info(f"### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + ttk.TTkLog.info(f" {ch} {i} ) ({type(r)})-> {r}") + ttk.TTkLog.info("") + +def _gccb(phase,info): + ttk.TTkLog.info(f" ---> {gc.garbage=}") + ttk.TTkLog.info(f" ---> {phase=} {info=}") + +class Button2(ttk.TTkButton):pass + +root = ttk.TTk() + +ttk.TTkLog.info(f"Start {gc.isenabled()=}") +# ttk.TTkLog.info(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +# gc.callbacks.append(_gccb) + +ttk.TTkLog.info("############# Phase 1 ##################") + +foo = ttk.TTkAbout(parent=root) + +bar1 = Button2(parent=root, border=True, text="Do it") +bar2 = ttk.TTkWidget() +pep = testClass() + +win = ttk.TTkWindow(parent=root,pos = (0,10), size=(50,30), title="Test Window 1") +win.setLayout(ttk.TTkHBoxLayout()) +ttk.TTkTestWidget(parent=win, border=False) + +win = ttk.TTkWindow(parent=root,pos=(20,0), size=(150,30), title="Log Window", flags=ttk.TTkK.WindowFlag.WindowCloseButtonHint) +win.setLayout(ttk.TTkHBoxLayout()) +ttk.TTkLogViewer(parent=win, follow=True ) + +win = ttk.TTkWindow(parent=root,pos = (5,35), size=(85,7), title="Captured Input") +win.setLayout(ttk.TTkHBoxLayout()) +ttk.TTkKeyPressView(parent=win) + +del win + +ttk.TTkAbout(parent=root, pos=(5,17)) + + + +ttk.TTkLog.info(f"{gc.collect()=}") +ttk.TTkLog.info(f"Mid {gc.get_count()=}") + + +@ttk.pyTTkSlot() +def _doit(): + ttk.TTkLog.info(f"{gc.collect()=}") + ttk.TTkLog.info(f"End {gc.get_count()=}") +bar1.clicked.connect(_doit) +bar1.clicked.connect(foo.close) + +foo.closed.connect(lambda _x: _ref(_x, "-Closed-")) + +_ref(foo, "-Active-") +del foo + +root.mainloop() + +foo.close() + +_ref(foo, "-Closed-")