Skip to content

Commit

Permalink
minor bug fixes of registration, add test
Browse files Browse the repository at this point in the history
  • Loading branch information
hanjinliu committed Jan 15, 2023
1 parent ccaaafc commit 90566e1
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 39 deletions.
2 changes: 1 addition & 1 deletion tabulous/_keymap/_keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ def wrapper(func):
def unbind(self, key: str) -> None:
_key = _normalize_key_combo(key)
if isinstance(_key, (str, QtKeys)):
del self._current_map[_key]
del self.current_map[_key]
elif isinstance(_key, Sequence):
current = self
*pref, last = _key
Expand Down
3 changes: 3 additions & 0 deletions tabulous/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def __init__(
tooltip: str | None = None,
visible: bool | None = None,
enabled: bool = True,
show: bool = False,
):
super().__init__(
widget_type=QBaseWidget,
Expand All @@ -91,6 +92,8 @@ def __init__(
mgui_native.setLayout(QtW.QVBoxLayout())
mgui_native.layout().addWidget(self._qwidget)
mgui_native.setContentsMargins(0, 0, 0, 0)
if show:
self.show(run=False)

@property
def native(self):
Expand Down
3 changes: 3 additions & 0 deletions tabulous/_qt/_action_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def __init__(self, parent=None):
self._current_index = None
self._actions: dict[str, QAction | QContextMenu] = {}

def __repr__(self) -> str:
return f"{type(self).__name__}({self._actions})"

def addMenu(self, title: str) -> QContextMenu:
"""Add a submenu to the contextmenu."""
menu = self.__class__(self)
Expand Down
5 changes: 5 additions & 0 deletions tabulous/widgets/_component/_cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ def _normalize_key(
col = slice(col, col + 1)
return row, col

@property
def _qcontextmenu(self):
"""The QContextMenu widget."""
return self.parent.native._qt_context_menu

def _get_qregistry(self):
return self.parent.native

Expand Down
5 changes: 5 additions & 0 deletions tabulous/widgets/_component/_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ def _set_value(self, idx: int, val: Any):
def _get_header(self) -> QDataFrameHeaderView:
raise NotImplementedError()

@property
def _qcontextmenu(self):
"""The QContextMenu widget."""
return self._get_header()._qt_context_menu

def __repr__(self) -> str:
return f"<{type(self).__name__}({self._get_axis()!r}) of {self.parent!r}>"

Expand Down
13 changes: 12 additions & 1 deletion tabulous/widgets/_component/_keymap.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Any
import re
from ._base import Component

if TYPE_CHECKING:
from tabulous.widgets._keymap_abc import SupportKeyMap

# e.g. f() takes 0 positional arguments but 1 was given
_PATTERN = re.compile(r".*takes 0 positional arguments but (\d+) w.+ given")


class KeyMap(Component["SupportKeyMap"]):
def register(
Expand All @@ -16,7 +20,14 @@ def register(
):
def wrapper(f):
def _inner(*_):
return f(self.parent)
try:
out = f(self.parent)
except TypeError as e:
if _PATTERN.match(str(e)):
out = f()
else:
raise e
return out

return self.parent._qwidget._keymap.bind(key, _inner, overwrite=overwrite)

Expand Down
77 changes: 40 additions & 37 deletions tabulous/widgets/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,6 @@ def _get_qregistry(self) -> QActionRegistry:
def parent(self) -> _S:
raise NotImplementedError()

def register_action(self, *args):
"""Register an contextmenu action."""
import warnings

warnings.warn(
"`register_action` is deprecated. Use `register` instead.",
DeprecationWarning,
)

reg = self._get_qregistry()
nargs = len(args)
if nargs == 0 or nargs > 2:
raise TypeError("One or two arguments are allowed.")
if nargs == 1:
arg = args[0]
if callable(arg):
loc, func = getattr(arg, "__name__", repr(arg)), arg
else:
loc, func = arg, None
else:
loc, func = args

# check type
if not isinstance(loc, str) or (func is not None and not callable(func)):
arg = type(loc).__name__
if func is not None:
arg += f", {type(func).__name__}"
raise TypeError(f"No overloaded method matched the input ({arg}).")

def wrapper(f: Callable[[int], Any]):
reg.registerAction(loc, f)
return f

return wrapper if func is None else wrapper(func)

# fmt: off
@overload
def register(self, location: str) -> Callable[[Callable[[_S, _T], Any]], Callable[[_S, _T], Any]]: ... # noqa: E501
Expand Down Expand Up @@ -105,8 +70,46 @@ def unregister(self, location: str):
self._get_qregistry().unregisterAction(location)


# f() takes from 0 to 1 positional arguments but 2 were given
_PATTERN = re.compile(r".*takes .* positional arguments? but (\d+) were given")
def register_action(self: SupportActionRegistration, *args):
"""Register an contextmenu action."""
import warnings

warnings.warn(
"`register_action` is deprecated. Use `register` instead.",
DeprecationWarning,
)

reg = self._get_qregistry()
nargs = len(args)
if nargs == 0 or nargs > 2:
raise TypeError("One or two arguments are allowed.")
if nargs == 1:
arg = args[0]
if callable(arg):
loc, func = getattr(arg, "__name__", repr(arg)), arg
else:
loc, func = arg, None
else:
loc, func = args

# check type
if not isinstance(loc, str) or (func is not None and not callable(func)):
arg = type(loc).__name__
if func is not None:
arg += f", {type(func).__name__}"
raise TypeError(f"No overloaded method matched the input ({arg}).")

def wrapper(f: Callable[[int], Any]):
reg.registerAction(loc, f)
return f

return wrapper if func is None else wrapper(func)


SupportActionRegistration.register_action = register_action

# e.g. f() takes from 0 to 1 positional arguments but 2 were given
_PATTERN = re.compile(r".*takes .* positional arguments? but (\d+) w.+ given")


class _NormalizedFunction(Generic[_P]):
Expand Down
7 changes: 7 additions & 0 deletions tabulous/widgets/_tablelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ def __init__(self, parent: TableViewer):

@property
def parent(self):
"""The parent viewer widget."""
return self._parent

@property
def _qcontextmenu(self):
"""The QContextMenu widget."""
return self.parent.native._tablestack._qt_context_menu

def insert(self, index: int, table: TableBase):
"""Insert a table at index `index`."""
if not isinstance(table, TableBase):
raise TypeError(
f"Cannot insert {type(table)} to {self.__class__.__name__}."
Expand Down
54 changes: 54 additions & 0 deletions tests/test_register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from tabulous import TableViewer, TableViewerWidget, MagicTableViewer
from tabulous.widgets import TableViewerBase
import pytest
from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
from unittest.mock import MagicMock

@pytest.mark.parametrize("cls", [TableViewer, TableViewerWidget, MagicTableViewer])
def test_register_table_keymap(qtbot: QtBot, cls: type[TableViewerBase]):
viewer = cls(show=False)
mock = MagicMock()

qtbot.addWidget(viewer._qwidget)
sheet = viewer.add_spreadsheet()
@sheet.keymap.register("Ctrl+U")
def test_func():
mock()

mock.assert_not_called()
qtbot.keyClick(sheet._qwidget, "U", Qt.KeyboardModifier.ControlModifier)
mock.assert_called_once()

mock.reset_mock()
sheet.keymap.unregister("Ctrl+U")
mock.assert_not_called()

@pytest.mark.parametrize("cls", [TableViewer, TableViewerWidget, MagicTableViewer])
def test_register_viewer_keymap(qtbot: QtBot, cls: type[TableViewerBase]):
viewer = TableViewer(show=False)
mock = MagicMock()

qtbot.addWidget(viewer._qwidget)

@viewer.keymap.register("Ctrl+U")
def test_func():
mock()

mock.assert_not_called()
qtbot.keyClick(viewer._qwidget, "U", Qt.KeyboardModifier.ControlModifier)
mock.assert_called_once()

mock.reset_mock()
viewer.keymap.unregister("Ctrl+U")
mock.assert_not_called()

@pytest.mark.parametrize("attr", ["cell", "index", "columns"])
def test_register_actions(attr: str):
viewer = TableViewer(show=False)
sheet = viewer.add_spreadsheet()
name = "TEST"

assert name not in getattr(sheet, attr)._qcontextmenu._actions
getattr(sheet, attr).register(name, lambda: None)
assert name in getattr(sheet, attr)._qcontextmenu._actions

0 comments on commit 90566e1

Please sign in to comment.