Skip to content

Commit 80372bb

Browse files
authored
ENH: add if_sheet_exists='overlay' to ExcelWriter (#42222)
1 parent 0d5ea68 commit 80372bb

File tree

4 files changed

+73
-5
lines changed

4 files changed

+73
-5
lines changed

doc/source/whatsnew/v1.4.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,14 @@ Other enhancements
207207
- Added :meth:`.ExponentialMovingWindow.sum` (:issue:`13297`)
208208
- :meth:`Series.str.split` now supports a ``regex`` argument that explicitly specifies whether the pattern is a regular expression. Default is ``None`` (:issue:`43563`, :issue:`32835`, :issue:`25549`)
209209
- :meth:`DataFrame.dropna` now accepts a single label as ``subset`` along with array-like (:issue:`41021`)
210+
- :class:`ExcelWriter` argument ``if_sheet_exists="overlay"`` option added (:issue:`40231`)
210211
- :meth:`read_excel` now accepts a ``decimal`` argument that allow the user to specify the decimal point when parsing string columns to numeric (:issue:`14403`)
211212
- :meth:`.GroupBy.mean` now supports `Numba <http://numba.pydata.org/>`_ execution with the ``engine`` keyword (:issue:`43731`)
212213
- :meth:`Timestamp.isoformat`, now handles the ``timespec`` argument from the base :class:``datetime`` class (:issue:`26131`)
213214
- :meth:`NaT.to_numpy` ``dtype`` argument is now respected, so ``np.timedelta64`` can be returned (:issue:`44460`)
214215
-
215216

217+
216218
.. ---------------------------------------------------------------------------
217219
218220
.. _whatsnew_140.notable_bug_fixes:

pandas/io/excel/_base.py

+33-3
Original file line numberDiff line numberDiff line change
@@ -701,15 +701,23 @@ class ExcelWriter(metaclass=abc.ABCMeta):
701701
be parsed by ``fsspec``, e.g., starting "s3://", "gcs://".
702702
703703
.. versionadded:: 1.2.0
704-
if_sheet_exists : {'error', 'new', 'replace'}, default 'error'
704+
705+
if_sheet_exists : {'error', 'new', 'replace', 'overlay'}, default 'error'
705706
How to behave when trying to write to a sheet that already
706707
exists (append mode only).
707708
708709
* error: raise a ValueError.
709710
* new: Create a new sheet, with a name determined by the engine.
710711
* replace: Delete the contents of the sheet before writing to it.
712+
* overlay: Write contents to the existing sheet without removing the old
713+
contents.
711714
712715
.. versionadded:: 1.3.0
716+
717+
.. versionchanged:: 1.4.0
718+
719+
Added ``overlay`` option
720+
713721
engine_kwargs : dict, optional
714722
Keyword arguments to be passed into the engine.
715723
@@ -775,6 +783,28 @@ class ExcelWriter(metaclass=abc.ABCMeta):
775783
>>> with pd.ExcelWriter("path_to_file.xlsx", mode="a", engine="openpyxl") as writer:
776784
... df.to_excel(writer, sheet_name="Sheet3")
777785
786+
Here, the `if_sheet_exists` parameter can be set to replace a sheet if it
787+
already exists:
788+
789+
>>> with ExcelWriter(
790+
... "path_to_file.xlsx",
791+
... mode="a",
792+
... engine="openpyxl",
793+
... if_sheet_exists="replace",
794+
... ) as writer:
795+
... df.to_excel(writer, sheet_name="Sheet1")
796+
797+
You can also write multiple DataFrames to a single sheet. Note that the
798+
``if_sheet_exists`` parameter needs to be set to ``overlay``:
799+
800+
>>> with ExcelWriter("path_to_file.xlsx",
801+
... mode="a",
802+
... engine="openpyxl",
803+
... if_sheet_exists="overlay",
804+
... ) as writer:
805+
... df1.to_excel(writer, sheet_name="Sheet1")
806+
... df2.to_excel(writer, sheet_name="Sheet1", startcol=3)
807+
778808
You can store Excel file in RAM:
779809
780810
>>> import io
@@ -962,10 +992,10 @@ def __init__(
962992

963993
self.mode = mode
964994

965-
if if_sheet_exists not in [None, "error", "new", "replace"]:
995+
if if_sheet_exists not in (None, "error", "new", "replace", "overlay"):
966996
raise ValueError(
967997
f"'{if_sheet_exists}' is not valid for if_sheet_exists. "
968-
"Valid options are 'error', 'new' and 'replace'."
998+
"Valid options are 'error', 'new', 'replace' and 'overlay'."
969999
)
9701000
if if_sheet_exists and "r+" not in mode:
9711001
raise ValueError("if_sheet_exists is only valid in append mode (mode='a')")

pandas/io/excel/_openpyxl.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,12 @@ def write_cells(
437437
f"Sheet '{sheet_name}' already exists and "
438438
f"if_sheet_exists is set to 'error'."
439439
)
440+
elif self.if_sheet_exists == "overlay":
441+
wks = self.sheets[sheet_name]
440442
else:
441443
raise ValueError(
442444
f"'{self.if_sheet_exists}' is not valid for if_sheet_exists. "
443-
"Valid options are 'error', 'new' and 'replace'."
445+
"Valid options are 'error', 'new', 'replace' and 'overlay'."
444446
)
445447
else:
446448
wks = self.sheets[sheet_name]

pandas/tests/io/excel/test_openpyxl.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def test_write_append_mode(ext, mode, expected):
139139
[
140140
("new", 2, ["apple", "banana"]),
141141
("replace", 1, ["pear"]),
142+
("overlay", 1, ["pear", "banana"]),
142143
],
143144
)
144145
def test_if_sheet_exists_append_modes(ext, if_sheet_exists, num_sheets, expected):
@@ -164,13 +165,46 @@ def test_if_sheet_exists_append_modes(ext, if_sheet_exists, num_sheets, expected
164165
wb.close()
165166

166167

168+
@pytest.mark.parametrize(
169+
"startrow, startcol, greeting, goodbye",
170+
[
171+
(0, 0, ["poop", "world"], ["goodbye", "people"]),
172+
(0, 1, ["hello", "world"], ["poop", "people"]),
173+
(1, 0, ["hello", "poop"], ["goodbye", "people"]),
174+
(1, 1, ["hello", "world"], ["goodbye", "poop"]),
175+
],
176+
)
177+
def test_append_overlay_startrow_startcol(ext, startrow, startcol, greeting, goodbye):
178+
df1 = DataFrame({"greeting": ["hello", "world"], "goodbye": ["goodbye", "people"]})
179+
df2 = DataFrame(["poop"])
180+
181+
with tm.ensure_clean(ext) as f:
182+
df1.to_excel(f, engine="openpyxl", sheet_name="poo", index=False)
183+
with ExcelWriter(
184+
f, engine="openpyxl", mode="a", if_sheet_exists="overlay"
185+
) as writer:
186+
# use startrow+1 because we don't have a header
187+
df2.to_excel(
188+
writer,
189+
index=False,
190+
header=False,
191+
startrow=startrow + 1,
192+
startcol=startcol,
193+
sheet_name="poo",
194+
)
195+
196+
result = pd.read_excel(f, sheet_name="poo", engine="openpyxl")
197+
expected = DataFrame({"greeting": greeting, "goodbye": goodbye})
198+
tm.assert_frame_equal(result, expected)
199+
200+
167201
@pytest.mark.parametrize(
168202
"if_sheet_exists,msg",
169203
[
170204
(
171205
"invalid",
172206
"'invalid' is not valid for if_sheet_exists. Valid options "
173-
"are 'error', 'new' and 'replace'.",
207+
"are 'error', 'new', 'replace' and 'overlay'.",
174208
),
175209
(
176210
"error",

0 commit comments

Comments
 (0)