Skip to content

Commit c384bc7

Browse files
Merge pull request #2 from awesome-panel/enhancement/support-decimal-symbol-and-index
support decimal symbol
2 parents 8277776 + c92adb1 commit c384bc7

7 files changed

Lines changed: 89 additions & 20 deletions

File tree

.pre-commit-config.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ repos:
1616
- repo: https://github.com/astral-sh/ruff-pre-commit
1717
rev: v0.8.0
1818
hooks:
19-
# Run the linter.
19+
# Run the import sorter.
2020
- id: ruff
21-
args: [ --fix ]
21+
args: ["check", "--select", "I", "--fix"]
22+
files: "^src/"
2223
# Run the formatter.
2324
- id: ruff-format
2425
types_or: [ python, pyi ]
26+
# Run the linter.
27+
- id: ruff
28+
args: [ --fix ]
2529
- repo: https://github.com/hoxbro/clean_notebook
2630
rev: v0.1.15
2731
hooks:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ exclude = [
8787
]
8888
line-length = 165
8989
fix = true
90-
9190
[tool.ruff.lint]
9291
ignore = [
9392
"D203", # one-blank-line-before-class and `no-blank-line-before-class` (D211) are incompatible.
@@ -118,6 +117,8 @@ select = [
118117
"UP034",
119118
"UP036",
120119
]
120+
[tool.ruff.lint.isort]
121+
force-single-line = true
121122

122123
[tool.pytest.ini_options]
123124
addopts = "--pyargs --doctest-ignore-import-errors --color=yes"

src/panel_copy_paste/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import warnings
55

66
from panel_copy_paste._copy_button import CopyButton
7-
from panel_copy_paste._paste_button import PasteButton, PasteToDataFrameButton
7+
from panel_copy_paste._paste_button import PasteButton
8+
from panel_copy_paste._paste_button import PasteToDataFrameButton
89

910
try:
1011
__version__ = importlib.metadata.version(__name__)

src/panel_copy_paste/_copy_button.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import panel as pn
44
import param
5-
from narwhals.dependencies import is_into_dataframe, is_pandas_dataframe, is_polars_dataframe
5+
from narwhals.dependencies import is_into_dataframe
6+
from narwhals.dependencies import is_pandas_dataframe
7+
from narwhals.dependencies import is_polars_dataframe
68
from narwhals.typing import IntoDataFrame
79

810
logger = logging.getLogger(__name__)
@@ -24,15 +26,35 @@ class CopyButton(pn.custom.JSComponent):
2426
value = param.Parameter(doc="""A String or DataFrame. Or a callback, Parameter or Parameterized object providing such types.""")
2527
button = pn.custom.Child(constant=True, doc="""An optional custom Button or ButtonIcon to use.""")
2628

29+
decimal_separator = param.Selector(
30+
default=None,
31+
objects=[None, ".", ","],
32+
doc="""The decimal symbol used when transforming a DataFrame. If not provided set to the decimal symbol of the client.""",
33+
)
34+
index = param.Boolean(default=False, doc="""Whether to include the index when copying a Pandas DataFrame.""")
35+
2736
_DEFAULT_BUTTON = pn.widgets.ButtonIcon(description="Copy to clip board.", icon="copy", active_icon="check", toggle_duration=1500)
2837
_data = param.Parameter(doc="""The value to be transferred to the clip board.""")
2938

30-
_rename = {"value": None}
39+
_rename = {"value": None, "index": None}
3140
_esm = """
41+
function getDecimalSeparator(locale) {
42+
const numberWithDecimalSeparator = 1.1;
43+
return Intl.NumberFormat(locale)
44+
.formatToParts(numberWithDecimalSeparator)
45+
.find(part => part.type === 'decimal')
46+
.value;
47+
}
48+
3249
export function render({ model, el }) {
3350
const button = model.get_child("button")
3451
el.appendChild(button)
3552
53+
if (model.decimal_separator === null) {
54+
model.decimal_separator = getDecimalSeparator();
55+
console.log("set")
56+
}
57+
3658
model.on("_data", (e)=>{
3759
navigator.clipboard.writeText(model._data).then(function() { }, function(err) {
3860
console.error('Could not write to clipboard: ', err);
@@ -52,20 +74,27 @@ def _get_new_button(cls):
5274

5375
@param.depends("button.clicks", watch=True)
5476
def _handle_clicks(self):
55-
self._data = self._transform_value(self.value)
77+
self._data = self._transform_value(self.value, decimal_separator=self.decimal_separator)
5678

5779
@staticmethod
58-
def _transform_frame(value: IntoDataFrame) -> str:
80+
def _transform_frame(value: IntoDataFrame, index=False, decimal_separator=None) -> str:
81+
if decimal_separator not in [".", ","]:
82+
decimal_separator = "."
83+
5984
if is_pandas_dataframe(value):
60-
return value.to_csv(sep="\t")
85+
return value.to_csv(sep="\t", decimal=decimal_separator, index=index)
6186
if is_polars_dataframe(value):
87+
# Polars does not support ",": https://github.com/pola-rs/polars/issues/19963
88+
# This assumes pandas and pyarrow is installed
89+
if decimal_separator == ",":
90+
return value.to_pandas().to_csv(sep="\t", decimal=decimal_separator, index=False)
6291
return value.write_csv(separator="\t")
6392

6493
msg = f"Value of type '{type(value)} is not supported yet."
6594
raise ValueError(msg)
6695

6796
@classmethod
68-
def _transform_value(cls, value, transform_func=None) -> str:
97+
def _transform_value(cls, value, transform_func=None, index=False, decimal_separator=None) -> str:
6998
if isinstance(value, param.Parameterized):
7099
if hasattr(value, "value"):
71100
return cls._transform_frame(value.value)
@@ -81,7 +110,7 @@ def _transform_value(cls, value, transform_func=None) -> str:
81110
if isinstance(value, str):
82111
return value
83112
if is_into_dataframe(value):
84-
return cls._transform_frame(value)
113+
return cls._transform_frame(value, index=index, decimal_separator=decimal_separator)
85114

86115
msg = f"Value of type '{type(value)} is not supported yet."
87116
raise ValueError(msg)

tests/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import pytest
44
from panel.config import panel_extension
5-
from panel.io.reload import _local_modules, _modules, _watched_files
5+
from panel.io.reload import _local_modules
6+
from panel.io.reload import _modules
7+
from panel.io.reload import _watched_files
68
from panel.io.state import state
79
from panel.theme import Design
810

tests/test_copy_button.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pandas as pd
44
import param
55
import polars as pl
6+
import pytest
67

78
from panel_copy_paste import CopyButton
89

@@ -39,17 +40,47 @@ def test_transform_str():
3940
assert CopyButton._transform_value(value) == value
4041

4142

42-
def test_transform_pandas_dataframe():
43+
@pytest.mark.parametrize(
44+
["decimal_seperator", "expected"],
45+
[
46+
(None, "x\n1.1\n"),
47+
(".", "x\n1.1\n"),
48+
(",", "x\n1,1\n"),
49+
],
50+
)
51+
def test_transform_pandas_dataframe(decimal_seperator, expected):
4352
"""Can copy with the CopyButton."""
44-
value = pd.DataFrame({"x": [1, 2], "y": ["a", "b"]})
53+
value = pd.DataFrame({"x": [1.1]})
4554

46-
assert CopyButton._transform_value(value) == "\tx\ty\n0\t1\ta\n1\t2\tb\n"
55+
assert CopyButton._transform_value(value, decimal_separator=decimal_seperator) == expected
4756

4857

49-
def test_transform_polars_dataframe():
58+
@pytest.mark.parametrize(
59+
["index", "expected"],
60+
[
61+
(True, "\tx\n0\t1.1\n"),
62+
(False, "x\n1.1\n"),
63+
],
64+
)
65+
def test_transform_pandas_dataframe_index(index, expected):
66+
"""Can copy with the CopyButton."""
67+
value = pd.DataFrame({"x": [1.1]})
68+
69+
assert CopyButton._transform_value(value, index=index) == expected
70+
71+
72+
@pytest.mark.parametrize(
73+
["decimal_seperator", "expected"],
74+
[
75+
(None, "x\n1.1\n"),
76+
(".", "x\n1.1\n"),
77+
(",", "x\n1,1\n"),
78+
],
79+
)
80+
def test_transform_polars_dataframe(decimal_seperator, expected):
5081
"""Can transform Polars DataFrame."""
51-
value = pl.DataFrame({"x": [1, 2], "y": ["a", "b"]})
52-
assert CopyButton._transform_value(value) == "x\ty\n1\ta\n2\tb\n"
82+
value = pl.DataFrame({"x": [1.1]})
83+
assert CopyButton._transform_value(value, decimal_separator=decimal_seperator) == expected
5384

5485

5586
def test_transform_callback():
@@ -58,7 +89,7 @@ def test_transform_callback():
5889
def callback():
5990
return pd.DataFrame({"x": [1, 2], "y": ["a", "b"]})
6091

61-
assert CopyButton._transform_value(callback) == "\tx\ty\n0\t1\ta\n1\t2\tb\n"
92+
assert CopyButton._transform_value(callback) == "x\ty\n1\ta\n2\tb\n"
6293

6394

6495
def test_transform_parameter():

tests/test_paste_button.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import pandas as pd
44
import panel as pn
55

6-
from panel_copy_paste import PasteButton, PasteToDataFrameButton
6+
from panel_copy_paste import PasteButton
7+
from panel_copy_paste import PasteToDataFrameButton
78

89

910
def test_paste_string_input():

0 commit comments

Comments
 (0)