From 11fbecd8a485679e1e74f82ef5f6b878c8bc6ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Parodi=2C=20Eugenio=20=F0=9F=8C=B6?= Date: Mon, 26 Jan 2026 15:23:53 +0000 Subject: [PATCH] chore: prototype pytests plugin --- apps/ttkode/ttkode/plugins/_010/findwidget.py | 2 +- .../ttkode/ttkode/plugins/_030/pytest_data.py | 17 ++- .../ttkode/ttkode/plugins/_030/pytest_tree.py | 111 +++++++++++++++--- .../ttkode/plugins/_030/pytest_widget.py | 86 +++++++++----- 4 files changed, 170 insertions(+), 46 deletions(-) diff --git a/apps/ttkode/ttkode/plugins/_010/findwidget.py b/apps/ttkode/ttkode/plugins/_010/findwidget.py index 9ed1ed91..680e9f41 100644 --- a/apps/ttkode/ttkode/plugins/_010/findwidget.py +++ b/apps/ttkode/ttkode/plugins/_010/findwidget.py @@ -36,7 +36,7 @@ import TermTk as ttk import os import fnmatch -from ttkode import ttkodeProxy +from ttkode.proxy import ttkodeProxy from ttkode.app.ttkode import TTKodeFileWidgetItem import mimetypes diff --git a/apps/ttkode/ttkode/plugins/_030/pytest_data.py b/apps/ttkode/ttkode/plugins/_030/pytest_data.py index 17322c68..8773d613 100644 --- a/apps/ttkode/ttkode/plugins/_030/pytest_data.py +++ b/apps/ttkode/ttkode/plugins/_030/pytest_data.py @@ -20,11 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = ['PTP_TestResult', 'PTP_ScanResult'] +__all__ = ['PTP_TestResult', 'PTP_ScanResult', 'PTP_Node'] from dataclasses import dataclass from typing import Optional,List,Tuple +@dataclass +class PTP_Node(): + nodeId: str + filename: str + lineNumber:int = 0 + @dataclass class PTP_TestResult(): nodeId: str @@ -39,4 +45,11 @@ class PTP_ScanResult(): nodeId: str path:str lineno:int - testname:str \ No newline at end of file + testname:str + + def get_node(self) -> PTP_Node: + return PTP_Node( + nodeId=self.nodeId, + filename=self.path, + lineNumber=self.lineno + ) \ No newline at end of file diff --git a/apps/ttkode/ttkode/plugins/_030/pytest_tree.py b/apps/ttkode/ttkode/plugins/_030/pytest_tree.py index f38775fd..65afe7a8 100644 --- a/apps/ttkode/ttkode/plugins/_030/pytest_tree.py +++ b/apps/ttkode/ttkode/plugins/_030/pytest_tree.py @@ -22,12 +22,18 @@ from __future__ import annotations -__all__ = ['PTP_TreeItem', 'PTP_TreeItemPath', 'PTP_TreeItemMethod'] +__all__ = [ + 'PTP_Tree', + 'PTP_Action', + 'PTP_TreeItem', 'PTP_TreeItemPath', 'PTP_TreeItemMethod'] from enum import IntEnum +from dataclasses import dataclass +from typing import Optional import TermTk as ttk +from _030.pytest_data import PTP_Node from ttkode.app.ttkode import TTKodeFileWidgetItem class _testStatus(IntEnum): @@ -44,15 +50,26 @@ _statusMarks = { def _toMark(status:_testStatus) -> ttk.TTkString: return _statusMarks.get(status, ttk.TTkString('- ')) -class PTP_TreeItem(TTKodeFileWidgetItem): - __slots__ = ('_testStatus') - def __init__(self, *args, testStatus:_testStatus=_testStatus.Undefined, **kwargs): +class PTP_TreeItem(ttk.TTkTreeWidgetItem): + __slots__ = ('_testStatus', '_ptp_node', '_test_id') + def __init__(self, name:str, test_id:str, node:PTP_Node, testStatus:_testStatus=_testStatus.Undefined, **kwargs): self._testStatus = testStatus - super().__init__(*args, **kwargs) + self._ptp_node = node + self._test_id = test_id + super().__init__([ttk.TTkString(name)], **kwargs) + + def test_id(self) -> str: + return self._test_id + + def node(self) -> PTP_Node: + return self._ptp_node def data(self, col, role = None) -> ttk.TTkString: return _toMark(self._testStatus) + super().data(col, role) + def testStatus(self) -> _testStatus: + return self._testStatus + def setTestStatus(self, status:_testStatus) -> None: if status == self._testStatus: return @@ -63,12 +80,78 @@ class PTP_TreeItemPath(PTP_TreeItem): pass class PTP_TreeItemMethod(PTP_TreeItem): - __slots__ = ('_test_call') - def __init__(self, *args, **kwargs): - path_method = kwargs.pop('path','') - path = path_method.split('::')[0] - mathods = path_method.split('::')[1:] - super().__init__(*args, **kwargs|{'path':path}) - - def test_call(self) -> str: - return self._test_call \ No newline at end of file + pass + + +@dataclass +class _PTP_Highlight(): + pos:int + run:bool + item:PTP_TreeItem + +@dataclass +class PTP_Action(): + item:PTP_TreeItem + +class PTP_TreeWidget(ttk.TTkTreeWidget): + __slots__ = ('_PTP_highight', 'actionPressed') + + _PTP_highight:Optional[_PTP_Highlight] + + def __init__(self, **kwargs): + self._PTP_highight = None + self.actionPressed = ttk.pyTTkSignal(PTP_Action) + super().__init__(**kwargs) + self.mergeStyle({'default':{'hoveredColor':ttk.TTkColor.bg('#666666')}}) + + def mousePressEvent(self, evt:ttk.TTkMouseEvent) -> bool: + y,x = evt.y, evt.x + w = self.width() + if (_item:=self.itemAt(y)) and x>=w-3: + self.actionPressed.emit(PTP_Action(item=_item)) + self.update() + return True + return super().mousePressEvent(evt) + + def mouseMoveEvent(self, evt:ttk.TTkMouseEvent) -> bool: + y,x = evt.y, evt.x + w = self.width() + if _item:=self.itemAt(y): + self._PTP_highight = _PTP_Highlight( + pos=evt.y, + run=x>=w-3, + item=_item + ) + self.update() + elif self._PTP_highight is not None: + self._PTP_highight = None + self.update() + return super().mouseMoveEvent(evt) + + def leaveEvent(self, evt:ttk.TTkMouseEvent) -> bool: + if self._PTP_highight is not None: + self._PTP_highight = None + self.update() + super().leaveEvent(evt) + + def paintEvent(self, canvas:ttk.TTkCanvas) -> None: + super().paintEvent(canvas) + style = self.currentStyle() + hoveredColor=style['hoveredColor'] + if _ph:=self._PTP_highight: + w = self.width() + if _ph.run: + canvas.drawText(text='...[ ]', pos=(w-6,_ph.pos), color=hoveredColor+ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD) + canvas.drawText(text= '▶' , pos=(w-2,_ph.pos), color=hoveredColor+ttk.TTkColor.RED) + else: + canvas.drawText(text='...[ ]', pos=(w-6,_ph.pos), color=hoveredColor+ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD) + canvas.drawText(text= '▶' , pos=(w-2,_ph.pos), color=hoveredColor+ttk.TTkColor.GREEN) + # canvas.drawText(text= '▷' , pos=(w-3,_ph.pos), color=hoveredColor+ttk.TTkColor.GREEN) + + +class PTP_Tree(ttk.TTkTree): + __slots__ = ('actionPressed') + def __init__(self, **kwargs): + tw = PTP_TreeWidget(**kwargs) + super().__init__(treeWidget=tw, **kwargs) + self.actionPressed = tw.actionPressed diff --git a/apps/ttkode/ttkode/plugins/_030/pytest_widget.py b/apps/ttkode/ttkode/plugins/_030/pytest_widget.py index ce487959..97bf0e01 100644 --- a/apps/ttkode/ttkode/plugins/_030/pytest_widget.py +++ b/apps/ttkode/ttkode/plugins/_030/pytest_widget.py @@ -30,11 +30,15 @@ from pygments.formatters import TerminalTrueColorFormatter import TermTk as ttk -from ttkode import ttkodeProxy +from ttkode.proxy import ttkodeProxy from ttkode.app.ttkode import TTKodeFileWidgetItem -from .pytest_tree import _testStatus, PTP_TreeItemPath, PTP_TreeItemMethod +from .pytest_tree import ( + _testStatus, + PTP_TreeItem, PTP_TreeItemPath, PTP_TreeItemMethod, + PTP_Tree, PTP_Action) from .pytest_engine import PTP_Engine, PTP_TestResult, PTP_ScanResult +from .pytest_data import PTP_Node def _strip_result(result: str) -> str: lines = result.splitlines() @@ -52,7 +56,7 @@ class PTP_PyTestWidget(ttk.TTkContainer): __slots__ = ( '_test_engine', '_res_tree','_test_results') - _res_tree:ttk.TTkTree + _res_tree:PTP_Tree _test_results:ttk.TTkTextEdit def __init__(self, testResults=ttk.TTkTextEdit, **kwargs): @@ -63,7 +67,7 @@ class PTP_PyTestWidget(ttk.TTkContainer): layout.addWidget(btn_scan:=ttk.TTkButton(text="Scan", border=False), 0,0) layout.addWidget(btn_run:=ttk.TTkButton(text="Run", border=False), 0,1) - layout.addWidget(res_tree:=ttk.TTkTree(dragDropMode=ttk.TTkK.DragDropMode.AllowDrag), 1,0,1,2) + layout.addWidget(res_tree:=PTP_Tree(dragDropMode=ttk.TTkK.DragDropMode.AllowDrag), 1,0,1,2) res_tree.setHeaderLabels(["Tests"]) testResults.setText('Info Tests') @@ -74,16 +78,28 @@ class PTP_PyTestWidget(ttk.TTkContainer): btn_run.clicked.connect(self._run_tests) self._test_engine.testResultReady.connect(self._test_updated) - @ttk.pyTTkSlot(ttk.TTkTreeWidgetItem, int) - def _activated(self, item:ttk.TTkTreeWidgetItem, _): + res_tree.actionPressed.connect(self._tree_action_pressed) + res_tree.itemActivated.connect(self._tree_item_activated) + + @ttk.pyTTkSlot(PTP_Action) + def _tree_action_pressed(self, action:PTP_Action) -> None: + item = action.item + if isinstance(item, PTP_TreeItem): + self._mark_node(item.test_id(), outcome='undefined') + self._test_results.clear() + self._test_engine.run_tests(item.test_id()) + + @ttk.pyTTkSlot(PTP_TreeItem, int) + def _tree_item_activated(self, item:PTP_TreeItem, _): + ttk.TTkLog.debug(item) if isinstance(item, PTP_TreeItemMethod): - file = item.path() - line = item.lineNumber() + file = item.node().filename + line = item.node().lineNumber ttkodeProxy.openFile(file, line) - def _get_node_from_path(self, _n:PTP_TreeItemPath, _p:str) -> Optional[PTP_TreeItemPath]: + def _get_node_from_path(self, _n:PTP_TreeItem, _p:str) -> Optional[PTP_TreeItem]: for _c in _n.children(): - if isinstance(_c, PTP_TreeItemPath) and _c.path() == _p: + if isinstance(_c, PTP_TreeItem) and _c.test_id() == _p: return _c if _cc:= self._get_node_from_path(_n=_c, _p=_p): return _cc @@ -94,30 +110,38 @@ class PTP_PyTestWidget(ttk.TTkContainer): self._res_tree.clear() self._test_results.clear() - def _get_or_add_path_in_node(_n:PTP_TreeItemPath, _p:str, _name:str) -> PTP_TreeItemPath: + def _get_or_add_path_in_node(_n:PTP_TreeItem, _p:str, _name:str, _node:PTP_ScanResult) -> PTP_TreeItem: for _c in _n.children(): - if isinstance(_c, PTP_TreeItemPath) and _c.path() == _p: + if isinstance(_c, PTP_TreeItem) and _c.test_id() == _p: return _c - _n.addChild(_c:=PTP_TreeItemPath([ttk.TTkString(_name)], path=_p, expanded=True)) + _ptp_node = _node.get_node() + if _node.nodeId==_p: # This is the leaf test + _n.addChild(_c:=PTP_TreeItemMethod(test_id=_p, name=_name, node=_ptp_node, expanded=True)) + else: + _n.addChild(_c:=PTP_TreeItemPath(test_id=_p, name=_name, node=_ptp_node, expanded=True)) return _c @ttk.pyTTkSlot(PTP_ScanResult) def _add_node(_node:PTP_ScanResult)->None: + ttk.TTkLog.debug(_node) self._test_results.append(_node.nodeId) _node_id_split = _node.nodeId.split('::') - _full_path = _node_id_split[0] + _full_test_path = _node_id_split[0] _leaves = _node_id_split[1:] _tree_node = self._res_tree.invisibleRootItem() - _full_composite_path = '' - for _path in _full_path.split('/'): - if _full_composite_path: - _full_composite_path = '/'.join([_full_composite_path,_path]) + _full_composite_test_path = '' + ttk.TTkLog.debug(_node_id_split) + ttk.TTkLog.debug(_full_test_path) + for _test_path in _full_test_path.split('/'): + if _full_composite_test_path: + _full_composite_test_path = '/'.join([_full_composite_test_path,_test_path]) else: - _full_composite_path = _path - _tree_node = _get_or_add_path_in_node(_tree_node, _full_composite_path, _path) + _full_composite_test_path = _test_path + ttk.TTkLog.debug(_full_composite_test_path) + _tree_node = _get_or_add_path_in_node(_n=_tree_node, _p=_full_composite_test_path, _name=_test_path, _node=_node) for _leaf in _leaves: - _full_composite_path = '::'.join([_full_composite_path,_leaf]) - _tree_node = _get_or_add_path_in_node(_tree_node, _full_composite_path, _leaf) + _full_composite_test_path = '::'.join([_full_composite_test_path,_leaf]) + _tree_node = _get_or_add_path_in_node(_n=_tree_node, _p=_full_composite_test_path, _name=_leaf, _node=_node) # _tree_node = _tree_node.addChild(TestTreeItemMethod([ttk.TTkString(_leaf)],test_call=_node.nodeId)) @ttk.pyTTkSlot(str) @@ -135,7 +159,6 @@ class PTP_PyTestWidget(ttk.TTkContainer): self._test_engine.scan() - def _mark_node(self, _nodeid:str, outcome:str) -> None: status = { 'passed' : _testStatus.Pass, @@ -144,20 +167,25 @@ class PTP_PyTestWidget(ttk.TTkContainer): def _recurse_node(_n:ttk.TTkTreeWidgetItem): for _c in _n.children(): # ttk.TTkLog.debug(_c.data(0)) - if isinstance(_c, PTP_TreeItemPath) and _nodeid.startswith(_c.path()): - _c.setTestStatus(status) - elif isinstance(_c, PTP_TreeItemMethod) and _nodeid.startswith(_c.test_call()): + if isinstance(_c, PTP_TreeItemPath): + if _nodeid.startswith(_c.test_id()): + if not ( _c.testStatus() == _testStatus.Fail and status == _testStatus.Pass ): + _c.setTestStatus(status) + elif _c.test_id().startswith(_nodeid): + _c.setTestStatus(status) + elif isinstance(_c, PTP_TreeItemMethod) and _nodeid.startswith(_c.test_id()): _c.setTestStatus(status) _recurse_node(_c) _recurse_node(self._res_tree.invisibleRootItem()) - def _clear_nodes(self) -> None: + def _clear_nodes(self, node:Optional[PTP_TreeItem] = None) -> None: status = _testStatus.Undefined def _recurse_node(_n:ttk.TTkTreeWidgetItem): for _c in _n.children(): - _c.setTestStatus(status) + if isinstance(_c, PTP_TreeItem): + _c.setTestStatus(status) _recurse_node(_c) - _recurse_node(self._res_tree.invisibleRootItem()) + _recurse_node(node if node else self._res_tree.invisibleRootItem()) @ttk.pyTTkSlot(PTP_TestResult) def _test_updated(self, test:PTP_TestResult) -> None: