Skip to content

Commit

Permalink
PasteButtons
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcSkovMadsen committed Nov 24, 2024
1 parent a3808aa commit 645859a
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 5 deletions.
3 changes: 2 additions & 1 deletion src/panel_copy_paste/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import warnings

from panel_copy_paste._copy_button import CopyButton
from panel_copy_paste._paste_button import PasteButton, PasteToDataFrameButton

try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError as e: # pragma: no cover
warnings.warn(f"Could not determine version of {__name__}\n{e!s}", stacklevel=2)
__version__ = "unknown"

__all__ = ["CopyButton"] # <- IMPORTANT FOR DOCS: fill with imports
__all__ = ["CopyButton", "PasteButton", "PasteToDataFrameButton"] # <- IMPORTANT FOR DOCS: fill with imports
4 changes: 0 additions & 4 deletions src/panel_copy_paste/_copy_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,3 @@ def _create_test_app(cls):

text_area_input = pn.widgets.TextAreaInput(rows=10)
return pn.Row(pn.Column(str_button, pandas_button, polars_button), text_area_input)


if pn.state.served:
CopyButton._create_test_app().servable()
101 changes: 101 additions & 0 deletions src/panel_copy_paste/_paste_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import logging
from io import StringIO
from typing import TYPE_CHECKING

import panel as pn
import param

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
import pandas as pd


def do_nothing(self, data: str) -> str:
"""Return the data."""
return data


def read_csv(self, data: str) -> "pd.DataFrame":
"""Return the data as a DataFrame."""
import pandas as pd

if not data:
return pd.DataFrame()
return pd.read_csv(StringIO(data), sep="\t", header=None)


class PasteButtonBase(pn.custom.JSComponent):
"""A custom Panel widget to paste a value from the clipboard onto an optional target."""

data = param.String(default="", doc="""The string value transferred from the clip board.""")
button = pn.custom.Child(constant=True, doc="""A custom Button or ButtonIcon to use.""")
target = param.Parameter(
doc="""If a widget its value is set to value when it changes. If a Pane its object will be
set to the value. If a callable the callable will be executed on the value.""",
allow_refs=False,
)

transform_func = do_nothing
_DEFAULT_BUTTON = pn.widgets.ButtonIcon(description="Paste from clipboard.", icon="clipboard", active_icon="check", toggle_duration=1500)

_rename = {"value": None, "target": None}

_esm = """
export function render({ model, el }) {
const button = model.get_child("button")
el.appendChild(button)
button.addEventListener('click', (event) => {
navigator.clipboard.readText()
.then(pastedData => {
if (model.data==pastedData){
model.data=pastedData + " ";
} else {
model.data = pastedData;
}
})
});
}
"""

def __init__(self, **params):
if "button" not in params:
params["button"] = self._get_new_button()
self.transform_func = params.pop("transform_func", self.transform_func) # type: ignore[method-assign]
super().__init__(**params)

@classmethod
def _get_new_button(cls):
return cls._DEFAULT_BUTTON.clone()

@param.depends("data", watch=True)
def _handle_data(self):
self.value = self.transform_func(self.data)
if self.target:
self._set_target_value(self.target, self.value)

@staticmethod
def _set_target_value(target, value):
if callable(target):
target(value)
elif hasattr(target, "object"):
target.object = value
elif hasattr(target, "value"):
target.value = value
else:
msg = f"Target of type '{type(target)}' is not supported."
logger.error(msg)


class PasteButton(PasteButtonBase):
value = param.String(default="", doc="""The value from the clip board as a string.""")

transform_func = do_nothing


class PasteToDataFrameButton(PasteButtonBase):
value = param.DataFrame(doc="""The value from the clip board as a Pandas DataFrame.""")

transform_func = read_csv
22 changes: 22 additions & 0 deletions tests/test_paste_input_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""We can paste csv data into a Tabulator widget."""

import pandas as pd
import panel as pn

from panel_copy_paste import PasteToDataFrameButton


def test_paste_csv_input():
"""We can paste csv data into a Tabulator widget."""
# When
target = pn.widgets.Tabulator()
widget = PasteToDataFrameButton(target=target)
# Then
assert not widget.value
assert not target.value
# When
widget.data = """1\t2\t3\t4"""
# Then
expected = pd.DataFrame([{0: 1, 1: 2, 2: 3, 3: 4}])
pd.testing.assert_frame_equal(widget.value, expected)
pd.testing.assert_frame_equal(widget.value, target.value)

0 comments on commit 645859a

Please sign in to comment.