from __future__ import annotations
from typing import (
Any,
Callable,
Generic,
Hashable,
TypeVar,
TYPE_CHECKING,
overload,
Literal,
)
if TYPE_CHECKING:
from tabulous.widgets import TableViewerBase, TableBase
from typing_extensions import Self
_T = TypeVar("_T")
class _Joinable:
def __init__(self, name: str = ""):
self._name = name
self._instances: dict[Hashable, Self] = {}
def __get__(self, instance, owner):
if instance is None:
return self
if (out := self._instances.get(instance, None)) is None:
out = self._instances[instance] = self.__class__(self._name)
return out
def __set_name__(self, owner, name):
self._name = name
def _join(self, other: Self):
raise NotImplementedError()
class _Registerable(_Joinable):
def __init__(self, name: str = ""):
super().__init__(name)
self._registered: list[Any] = []
def _register(self, loc, func=None):
def wrapper(f):
self._registered.append((loc, f))
return f
return wrapper(func) if func is not None else wrapper
def _join(self, other: Self):
self._registered.extend(other._registered)
class _Updatable(_Joinable):
def __init__(self, name: str = ""):
super().__init__(name)
self._dict = {}
def __setitem__(self, key: str, value: Any):
self._dict[key] = value
def update(self, *args, **kwargs):
self._dict.update(*args, **kwargs)
def add(self, obj: _T) -> _T:
"""A decorator to add an callable object to the namespace."""
if callable(obj) or isinstance(obj, type):
name = obj.__name__
self[name] = obj
else:
raise TypeError(f"Expected to be used as a decorator, got {type(obj)}")
return obj
def _join(self, other: Self):
self._dict.update(other._dict)
[docs]class ContextRegisterable(_Registerable, Generic[_T]):
"""A mock for the `register` method."""
# fmt: off
@overload
def register(self, loc: str, func: Literal[None] = None) -> Callable[[Callable[[_T, Any], None]], Callable[[_T, Any], None]]: ... # noqa: E501
@overload
def register(self, loc: str, func: Callable[[_T, Any], None]) -> Callable[[_T, Any], None]: ... # noqa: E501
# fmt: on
[docs] def register(self, loc, func=None):
return self._register(loc, func)
[docs]class KeyMapMock(_Registerable):
"""A mock for the `keymap` attribute of a table viewer instance."""
# fmt: off
@overload
def register(self, key: str, func: Literal[None] = None) -> Callable[[Callable[[TableViewerBase, Any], None]], Callable[[TableViewerBase, Any], None]]: ... # noqa: E501
@overload
def register(self, key: str, func: Callable[[TableViewerBase, Any], None]) -> Callable[[TableViewerBase, Any], None]: ... # noqa: E501
# fmt: on
[docs] def register(self, key, func=None):
return self._register(key, func)
[docs]class CellNamespaceMock(_Updatable):
"""A mock for the `cell_namespace` attribute of a table viewer instance."""
[docs]class ConsoleMock(_Updatable):
"""A mock for the `console` attribute of a table viewer instance."""
[docs]class CommandPaletteMock(_Registerable):
# fmt: off
@overload
def register(self, command: Callable[[TableViewerBase], Any], title: str, desc: str | None, key: str | None): ... # noqa: E501
@overload
def register(self, command: Literal[None], title: str, desc: str | None, key: str | None): ... # noqa: E501
# fmt: on
[docs] def register(self, command, title="User defined", desc=None, key=None):
return self._register((command, title, desc, key))
[docs]class Initializer:
_fields = ()
def __hash__(self):
return id(self)
[docs] def join(self, other: Initializer):
"""Join initializers together."""
for name in self._fields:
self_field: _Registerable = getattr(self, name)
other_field: _Registerable = getattr(other, name)
self_field._join(other_field)
return self
[docs]class ViewerInitializer(Initializer):
tables: ContextRegisterable[TableViewerBase] = ContextRegisterable()
keymap: ContextRegisterable[TableViewerBase] = KeyMapMock()
console: ConsoleMock[TableViewerBase] = ConsoleMock()
cell_namespace: CellNamespaceMock[TableViewerBase] = CellNamespaceMock()
command_palette: CommandPaletteMock[TableViewerBase] = CommandPaletteMock()
_fields = ("tables", "keymap", "console", "cell_namespace", "command_palette")
[docs] def initialize_viewer(self, viewer: TableViewerBase):
for args in self.tables._registered:
viewer.tables.register(*args)
for args in self.keymap._registered:
# NOTE: The QtKeyMap object is currently a class variable. When the second
# viewer is launched, old keybindings are still registered. To avoid this,
# we just allow overwriting the keymap.
viewer.keymap.register(*args, overwrite=True)
viewer.cell_namespace.update_safely(self.cell_namespace._dict)
viewer.console.update(self.console._dict)
for args in self.command_palette._registered:
viewer.command_palette.register(*args)
[docs]class TableInitializer(Initializer):
cell: ContextRegisterable[TableBase] = ContextRegisterable()
index: ContextRegisterable[TableBase] = ContextRegisterable()
columns: ContextRegisterable[TableBase] = ContextRegisterable()
keymap: ContextRegisterable[TableBase] = KeyMapMock()
_fields = ("cell", "index", "columns", "keymap")
[docs] def initialize_table(self, table: TableBase):
for args in self.cell._registered:
table.cell.register(*args)
for args in self.index._registered:
table.index.register(*args)
for args in self.columns._registered:
table.columns.register(*args)
for args in self.keymap._registered:
table.keymap.register(*args, overwrite=True)
_VIEWER_INITIALIZER = ViewerInitializer()
_TABLE_INITIALIZER = TableInitializer()
[docs]def get_initializers() -> tuple[ViewerInitializer, TableInitializer]:
"""Get viewer and table initializers."""
return _VIEWER_INITIALIZER, _TABLE_INITIALIZER