Skip to content

Commit

Permalink
copy/paste API
Browse files Browse the repository at this point in the history
  • Loading branch information
hanjinliu committed Jul 24, 2022
1 parent 79fea2c commit 28a2cf9
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 28 deletions.
2 changes: 1 addition & 1 deletion tabulous/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.1.0a1"
__version__ = "0.1.0a2"

from .widgets import Table, TableViewer, TableViewerWidget
from .core import (
Expand Down
3 changes: 3 additions & 0 deletions tabulous/_qt/_table/_base/_table_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ def copyToClipboard(self, headers: bool = True):
ref = pd.concat([data.iloc[sel] for sel in selections], axis=axis)
ref.to_clipboard(index=headers, header=headers)

def pasteFromClipBoard(self):
raise TypeError("Table is immutable.")

def readClipBoard(self) -> pd.DataFrame:
"""Read clipboard data and return as pandas DataFrame."""
return pd.read_clipboard(header=None)
Expand Down
16 changes: 13 additions & 3 deletions tabulous/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@
else:
_TableLike = Any

# class DataFrameType(Protocol):
# iloc:

__all__ = [
"TableData",
"TableColumn",
"TableDataTuple",
"TableInfo",
"TablePosition",
"ItemInfo",
"HeaderInfo",
"SelectionRanges",
"SelectionType",
]

if TYPE_CHECKING:
Expand Down Expand Up @@ -62,6 +64,13 @@ def __getitem__(cls, names: str | tuple[str, ...]):


class TableInfo(metaclass=TableInfoAlias):
"""
A generic type to describe a DataFrame and its column names.
``TableInfo["x", "y"]`` is equivalent to ``tuple[pd.DataFrame, str, str]``
with additional information for magicgui construction.
"""

def __new__(cls, *args, **kwargs):
raise TypeError(f"Type {cls.__name__} cannot be instantiated.")

Expand Down Expand Up @@ -130,4 +139,5 @@ class HeaderInfo(NamedTuple):


_Sliceable = Union[SupportsIndex, slice]
SelectionType = List[Tuple[_Sliceable, _Sliceable]]
_SingleSelection = Tuple[_Sliceable, _Sliceable]
SelectionType = List[_SingleSelection]
22 changes: 21 additions & 1 deletion tabulous/widgets/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .keybindings import register_shortcut
from ._sample import open_sample

from ..types import TabPosition, _TableLike
from ..types import SelectionType, TabPosition, _TableLike, _SingleSelection

if TYPE_CHECKING:
from .table import TableBase
Expand Down Expand Up @@ -261,6 +261,26 @@ def open_sample(self, sample_name: str, plugin: str = "seaborn") -> Table:
df = open_sample(sample_name, plugin)
return self.add_table(df, name=sample_name)

def copy_data(
self,
selections: SelectionType | _SingleSelection | None = None,
*,
headers: bool = False,
) -> None:
"""Copy selected cells to clipboard."""
if selections is not None:
self.current_table.selections = selections
return self.current_table._qwidget.copyToClipboard(headers=headers)

def paste_data(
self,
selections: SelectionType | _SingleSelection | None = None,
) -> None:
"""Paste from clipboard."""
if selections is not None:
self.current_table.selections = selections
return self.current_table._qwidget.pasteFromClipBoard()

def _link_events(self):
_tablist = self._tablist
_qtablist = self._qwidget._tablestack
Expand Down
39 changes: 18 additions & 21 deletions tabulous/widgets/table.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Callable, Generic, TYPE_CHECKING, TypeVar
from typing import Any, Callable, TYPE_CHECKING
from functools import partial
from psygnal import SignalGroup, Signal

from .keybindings import register_shortcut
from .filtering import FilterProxy

from ..types import SelectionRanges, ItemInfo, HeaderInfo
from ..types import (
SelectionRanges,
ItemInfo,
HeaderInfo,
SelectionType,
_SingleSelection,
)

if TYPE_CHECKING:
import pandas as pd
Expand All @@ -27,10 +33,7 @@ class TableSignals(SignalGroup):
renamed = Signal(str)


_QW = TypeVar("_QW", bound="QBaseTable")


class TableBase(ABC, Generic[_QW]):
class TableBase(ABC):
"""The base class for a table layer."""

_Default_Name = "None"
Expand Down Expand Up @@ -58,7 +61,7 @@ def __repr__(self) -> str:
return f"{self.__class__.__name__}<{self.name!r}>"

@abstractmethod
def _create_backend(self, data: pd.DataFrame) -> _QW:
def _create_backend(self, data: pd.DataFrame) -> QBaseTable:
"""This function creates a backend widget."""

@abstractmethod
Expand Down Expand Up @@ -121,14 +124,6 @@ def editable(self) -> bool:
def editable(self, value: bool):
self._qwidget.setEditable(value)

@property
def columns(self):
return self._data.columns

@property
def index(self):
return self._data.index

@property
def selections(self):
"""Get the SelectionRanges object of current table selection."""
Expand All @@ -138,7 +133,9 @@ def selections(self):
return rngs

@selections.setter
def selections(self, value) -> None:
def selections(self, value: SelectionType | _SingleSelection) -> None:
if not isinstance(value, list):
value = [value]
self._qwidget.setSelections(value)

def refresh(self) -> None:
Expand All @@ -161,7 +158,7 @@ def register(f):
return register


class _DataFrameTableLayer(TableBase[_QW]):
class _DataFrameTableLayer(TableBase):
"""Table layer for DataFrame."""

def _normalize_data(self, data) -> pd.DataFrame:
Expand All @@ -172,7 +169,7 @@ def _normalize_data(self, data) -> pd.DataFrame:
return data


class Table(_DataFrameTableLayer["QTableLayer"]):
class Table(_DataFrameTableLayer):
_Default_Name = "table"

def _create_backend(self, data: pd.DataFrame) -> QTableLayer:
Expand All @@ -181,7 +178,7 @@ def _create_backend(self, data: pd.DataFrame) -> QTableLayer:
return QTableLayer(data=data)


class SpreadSheet(_DataFrameTableLayer["QSpreadSheet"]):
class SpreadSheet(_DataFrameTableLayer):
_Default_Name = "sheet"

def _create_backend(self, data: pd.DataFrame) -> QSpreadSheet:
Expand All @@ -190,7 +187,7 @@ def _create_backend(self, data: pd.DataFrame) -> QSpreadSheet:
return QSpreadSheet(data=data)


class GroupBy(TableBase["QTableGroupBy"]):
class GroupBy(TableBase):
_Default_Name = "groupby"

def _create_backend(self, data: pd.DataFrame) -> QTableGroupBy:
Expand Down Expand Up @@ -231,7 +228,7 @@ def current_group(self, val) -> None:
return self._qwidget.setCurrentGroup(val)


class TableDisplay(TableBase["QTableDisplay"]):
class TableDisplay(TableBase):
_Default_Name = "display"

def _create_backend(self, data: Callable[[], Any]) -> QTableDisplay:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_copy_paste.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from tabulous import TableViewerWidget
import numpy as np
from numpy import testing

def assert_equal(a, b):
return testing.assert_equal(np.asarray(a), np.asarray(b))

def test_copy_and_paste_on_table():
viewer = TableViewerWidget(show=False)
table = viewer.add_table({
"a": [0, 1, 2, 3, 4],
"b": [2, 4, 6, 8, 10],
"c": [-1, -1, -1, -1, -1],
}, editable=True)
sl_src = (2, 2)
sl_dst = (1, 1)
viewer.copy_data([sl_src]) # copy -1
old_value = table.data.iloc[sl_dst]
copied = table.data.iloc[sl_src]
viewer.paste_data([sl_dst]) # paste -1
assert table.data.iloc[sl_dst] == copied

table.undo_manager.undo()
assert table.data.iloc[sl_dst] == old_value

sl_src = (slice(3, 5), slice(1, 3))
sl_dst = (slice(2, 4), slice(0, 2))
viewer.copy_data([sl_src])
old_value = table.data.iloc[sl_dst].copy()
copied = table.data.iloc[sl_src].copy()
viewer.paste_data([sl_dst])
assert_equal(table.data.iloc[sl_dst], copied)

table.undo_manager.undo()
assert_equal(table.data.iloc[sl_dst], old_value)


sl_src = (slice(3, 5), slice(1, 3))
sl_dst = (slice(2, 4), slice(0, 2))
viewer.copy_data([sl_src])
old_value = table.data.iloc[sl_dst].copy()
copied = table.data.iloc[sl_src].copy()
viewer.paste_data([(2, 0)]) # paste with single cell selection
assert_equal(table.data.iloc[sl_dst], copied)

table.undo_manager.undo()
assert_equal(table.data.iloc[sl_dst], old_value)
4 changes: 2 additions & 2 deletions tests/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def test_display(df: pd.DataFrame):
table = Table(df)
viewer.add_layer(table)
assert table.data is df
assert table.columns is df.columns
assert table.index is df.index
assert table.data.columns is df.columns
assert table.data.index is df.index
assert table.table_shape == df.shape
assert get_cell_value(table._qwidget, 0, 0) == str(df.iloc[0, 0])

Expand Down

0 comments on commit 28a2cf9

Please sign in to comment.