Skip to content

ENH: Allow to assign/create custom body_style and header_style property to an instance of ExcelFormatter used in method df.to_excel() #53973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Other enhancements
- Added a new parameter ``by_row`` to :meth:`Series.apply` and :meth:`DataFrame.apply`. When set to ``False`` the supplied callables will always operate on the whole Series or DataFrame (:issue:`53400`, :issue:`53601`).
- Groupby aggregations (such as :meth:`DataFrameGroupby.sum`) now can preserve the dtype of the input instead of casting to ``float64`` (:issue:`44952`)
- Improved error message when :meth:`DataFrameGroupBy.agg` failed (:issue:`52930`)
- Made :attr:`ExcelFormatter.header_style` a class attribute instead of a property. Default styles for :meth:`DataFrame.to_excel` are set to None. (:issue:`52369`)
- Many read/to_* functions, such as :meth:`DataFrame.to_pickle` and :func:`read_csv`, support forwarding compression arguments to lzma.LZMAFile (:issue:`52979`)
- Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`)
- Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`)
Expand Down
35 changes: 24 additions & 11 deletions pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,19 +577,32 @@ def __init__(
self.header = header
self.merge_cells = merge_cells
self.inf_rep = inf_rep
self._header_styledict[str, Any] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be self._header_style: dict[str, Any] | None = None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between defaulting to None and {} here?

self._body_styledict[str, Any] = None

@property
def header_style(self) -> dict[str, dict[str, str | bool]]:
return {
"font": {"bold": True},
"borders": {
"top": "thin",
"right": "thin",
"bottom": "thin",
"left": "thin",
},
"alignment": {"horizontal": "center", "vertical": "top"},
}
def header_style(self):
return self._header_style

@header_style.setter
def header_style(self, val):
if not isinstance(val, dict):
return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setters shouldn't return a value

else:
self._header_style = val
return self._header_style

@property
def body_style(self):
return self._body_style

@body_style.setter
def body_style(self, val):
if not isinstance(val, dict):
return None
else:
self._body_style = val
return self._body_style

def _format_value(self, val):
if is_scalar(val) and missing.isna(val):
Expand Down
58 changes: 57 additions & 1 deletion pandas/tests/io/excel/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pandas.util._test_decorators as td

import pandas as pd
from pandas import (
DataFrame,
read_excel,
Expand All @@ -15,7 +16,7 @@
from pandas.io.excel import ExcelWriter
from pandas.io.formats.excel import ExcelFormatter

pytest.importorskip("jinja2")
# pytest.importorskip("jinja2")
# jinja2 is currently required for Styler.__init__(). Technically Styler.to_excel
# could compute styles and render to excel without jinja2, since there is no
# 'template' file, but this needs the import error to delayed until render time.
Expand Down Expand Up @@ -254,6 +255,61 @@ def test_styler_to_excel_border_style(engine, border_style):
assert s_cell == expected


def test_styler_update_values():
openpyxl = pytest.importorskip("openpyxl")
pd.io.formats.excel.ExcelFormatter.header_style = {
"font": {"bold": True},
"borders": {
"top": "thin",
"right": "thin",
"bottom": "thin",
"left": "thin",
},
"alignment": {"horizontal": "center", "vertical": "top"},
}

df = DataFrame([{"A": 1, "B": 2, "C": 3}, {"A": 1, "B": 2, "C": 3}])
with tm.ensure_clean(".xlsx") as path:
with ExcelWriter(path, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name="custom")

with contextlib.closing(openpyxl.load_workbook(path)) as wb:
# Check font, spacing, indentation
assert wb["custom"].cell(1, 2).font.bold is True
assert wb["custom"].cell(1, 2).alignment.horizontal == "center"
assert wb["custom"].cell(1, 2).alignment.vertical == "top"

# Check border
wb["custom"].cell(1, 2).border
assert wb["custom"].cell(1, 2).border.bottom.border_style == "thin"
assert wb["custom"].cell(1, 2).border.top.border_style == "thin"
assert wb["custom"].cell(1, 2).border.left.border_style == "thin"
assert wb["custom"].cell(1, 2).border.right.border_style == "thin"


def test_styler_default_values():
openpyxl = pytest.importorskip("openpyxl")

df = DataFrame([{"A": 1, "B": 2, "C": 3}, {"A": 1, "B": 2, "C": 3}])
with tm.ensure_clean(".xlsx") as path:
with ExcelWriter(path, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name="custom")

with contextlib.closing(openpyxl.load_workbook(path)) as wb:
# Check font, spacing, indentation
assert wb["custom"].cell(1, 1).font.color.value == 1
assert wb["custom"].cell(1, 1).alignment.horizontal is None
assert wb["custom"].cell(1, 1).alignment.vertical is None
assert wb["custom"].cell(1, 1).alignment.indent == 0.0

# Check border
wb["custom"].cell(1, 1).border
assert wb["custom"].cell(1, 1).border.bottom.color is None
assert wb["custom"].cell(1, 1).border.top.color is None
assert wb["custom"].cell(1, 1).border.left.color is None
assert wb["custom"].cell(1, 1).border.right.color is None


def test_styler_custom_converter():
openpyxl = pytest.importorskip("openpyxl")

Expand Down