Skip to content

Commit dd298d9

Browse files
rhshadrachyehoshuadimarsky
authored andcommitted
DEP: Protect some ExcelWriter attributes (pandas-dev#45795)
* DEP: Deprecate ExcelWriter attributes * DEP: Deprecate ExcelWriter attributes * Fixup for test * Move tests and restore check_extension y * Deprecate xlwt fm_date and fm_datetime; doc improvements
1 parent f27e215 commit dd298d9

File tree

11 files changed

+285
-62
lines changed

11 files changed

+285
-62
lines changed

doc/source/reference/io.rst

-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ Excel
5353

5454
.. autosummary::
5555
:toctree: api/
56-
:template: autosummary/class_without_autosummary.rst
5756

5857
ExcelWriter
5958

doc/source/whatsnew/v1.5.0.rst

+37
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,43 @@ use ``series.loc[i:j]``.
184184

185185
Slicing on a :class:`DataFrame` will not be affected.
186186

187+
.. _whatsnew_150.deprecations.excel_writer_attributes:
188+
189+
:class:`ExcelWriter` attributes
190+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
191+
192+
All attributes of :class:`ExcelWriter` were previously documented as not
193+
public. However some third party Excel engines documented accessing
194+
``ExcelWriter.book`` or ``ExcelWriter.sheets``, and users were utilizing these
195+
and possibly other attributes. Previously these attributes were not safe to use;
196+
e.g. modifications to ``ExcelWriter.book`` would not update ``ExcelWriter.sheets``
197+
and conversely. In order to support this, pandas has made some attributes public
198+
and improved their implementations so that they may now be safely used. (:issue:`45572`)
199+
200+
The following attributes are now public and considered safe to access.
201+
202+
- ``book``
203+
- ``check_extension``
204+
- ``close``
205+
- ``date_format``
206+
- ``datetime_format``
207+
- ``engine``
208+
- ``if_sheet_exists``
209+
- ``sheets``
210+
- ``supported_extensions``
211+
212+
The following attributes have been deprecated. They now raise a ``FutureWarning``
213+
when accessed and will removed in a future version. Users should be aware
214+
that their usage is considered unsafe, and can lead to unexpected results.
215+
216+
- ``cur_sheet``
217+
- ``handles``
218+
- ``path``
219+
- ``save``
220+
- ``write_cells``
221+
222+
See the documentation of :class:`ExcelWriter` for further details.
223+
187224
.. _whatsnew_150.deprecations.other:
188225

189226
Other Deprecations

pandas/io/excel/_base.py

+126-28
Original file line numberDiff line numberDiff line change
@@ -836,18 +836,8 @@ class ExcelWriter(metaclass=abc.ABCMeta):
836836
837837
Use engine_kwargs instead.
838838
839-
Attributes
840-
----------
841-
None
842-
843-
Methods
844-
-------
845-
None
846-
847839
Notes
848840
-----
849-
None of the methods and properties are considered public.
850-
851841
For compatibility with CSV writers, ExcelWriter serializes lists
852842
and dicts to strings before writing.
853843
@@ -1034,7 +1024,7 @@ def __new__(
10341024
return object.__new__(cls)
10351025

10361026
# declare external properties you can count on
1037-
path = None
1027+
_path = None
10381028

10391029
@property
10401030
@abc.abstractmethod
@@ -1054,7 +1044,16 @@ def sheets(self) -> dict[str, Any]:
10541044
"""Mapping of sheet names to sheet objects."""
10551045
pass
10561046

1047+
@property
10571048
@abc.abstractmethod
1049+
def book(self):
1050+
"""
1051+
Book instance. Class type will depend on the engine used.
1052+
1053+
This attribute can be used to access engine-specific features.
1054+
"""
1055+
pass
1056+
10581057
def write_cells(
10591058
self,
10601059
cells,
@@ -1066,6 +1065,8 @@ def write_cells(
10661065
"""
10671066
Write given formatted cells into Excel an excel sheet
10681067
1068+
.. deprecated:: 1.5.0
1069+
10691070
Parameters
10701071
----------
10711072
cells : generator
@@ -1077,12 +1078,47 @@ def write_cells(
10771078
freeze_panes: int tuple of length 2
10781079
contains the bottom-most row and right-most column to freeze
10791080
"""
1080-
pass
1081+
self._deprecate("write_cells")
1082+
return self._write_cells(cells, sheet_name, startrow, startcol, freeze_panes)
10811083

10821084
@abc.abstractmethod
1085+
def _write_cells(
1086+
self,
1087+
cells,
1088+
sheet_name: str | None = None,
1089+
startrow: int = 0,
1090+
startcol: int = 0,
1091+
freeze_panes: tuple[int, int] | None = None,
1092+
) -> None:
1093+
"""
1094+
Write given formatted cells into Excel an excel sheet
1095+
1096+
Parameters
1097+
----------
1098+
cells : generator
1099+
cell of formatted data to save to Excel sheet
1100+
sheet_name : str, default None
1101+
Name of Excel sheet, if None, then use self.cur_sheet
1102+
startrow : upper left cell row to dump data frame
1103+
startcol : upper left cell column to dump data frame
1104+
freeze_panes: int tuple of length 2
1105+
contains the bottom-most row and right-most column to freeze
1106+
"""
1107+
pass
1108+
10831109
def save(self) -> None:
10841110
"""
10851111
Save workbook to disk.
1112+
1113+
.. deprecated:: 1.5.0
1114+
"""
1115+
self._deprecate("save")
1116+
return self._save()
1117+
1118+
@abc.abstractmethod
1119+
def _save(self) -> None:
1120+
"""
1121+
Save workbook to disk.
10861122
"""
10871123
pass
10881124

@@ -1111,25 +1147,25 @@ def __init__(
11111147
mode = mode.replace("a", "r+")
11121148

11131149
# cast ExcelWriter to avoid adding 'if self.handles is not None'
1114-
self.handles = IOHandles(
1150+
self._handles = IOHandles(
11151151
cast(IO[bytes], path), compression={"compression": None}
11161152
)
11171153
if not isinstance(path, ExcelWriter):
1118-
self.handles = get_handle(
1154+
self._handles = get_handle(
11191155
path, mode, storage_options=storage_options, is_text=False
11201156
)
1121-
self.cur_sheet = None
1157+
self._cur_sheet = None
11221158

11231159
if date_format is None:
1124-
self.date_format = "YYYY-MM-DD"
1160+
self._date_format = "YYYY-MM-DD"
11251161
else:
1126-
self.date_format = date_format
1162+
self._date_format = date_format
11271163
if datetime_format is None:
1128-
self.datetime_format = "YYYY-MM-DD HH:MM:SS"
1164+
self._datetime_format = "YYYY-MM-DD HH:MM:SS"
11291165
else:
1130-
self.datetime_format = datetime_format
1166+
self._datetime_format = datetime_format
11311167

1132-
self.mode = mode
1168+
self._mode = mode
11331169

11341170
if if_sheet_exists not in (None, "error", "new", "replace", "overlay"):
11351171
raise ValueError(
@@ -1140,16 +1176,78 @@ def __init__(
11401176
raise ValueError("if_sheet_exists is only valid in append mode (mode='a')")
11411177
if if_sheet_exists is None:
11421178
if_sheet_exists = "error"
1143-
self.if_sheet_exists = if_sheet_exists
1179+
self._if_sheet_exists = if_sheet_exists
1180+
1181+
def _deprecate(self, attr: str):
1182+
"""
1183+
Deprecate attribute or method for ExcelWriter.
1184+
"""
1185+
warnings.warn(
1186+
f"{attr} is not part of the public API, usage can give in unexpected "
1187+
"results and will be removed in a future version",
1188+
FutureWarning,
1189+
stacklevel=find_stack_level(),
1190+
)
1191+
1192+
@property
1193+
def date_format(self) -> str:
1194+
"""
1195+
Format string for dates written into Excel files (e.g. ‘YYYY-MM-DD’).
1196+
"""
1197+
return self._date_format
1198+
1199+
@property
1200+
def datetime_format(self) -> str:
1201+
"""
1202+
Format string for dates written into Excel files (e.g. ‘YYYY-MM-DD’).
1203+
"""
1204+
return self._datetime_format
1205+
1206+
@property
1207+
def if_sheet_exists(self) -> str:
1208+
"""
1209+
How to behave when writing to a sheet that already exists in append mode.
1210+
"""
1211+
return self._if_sheet_exists
1212+
1213+
@property
1214+
def cur_sheet(self):
1215+
"""
1216+
Current sheet for writing.
1217+
1218+
.. deprecated:: 1.5.0
1219+
"""
1220+
self._deprecate("cur_sheet")
1221+
return self._cur_sheet
1222+
1223+
@property
1224+
def handles(self):
1225+
"""
1226+
Handles to Excel sheets.
1227+
1228+
.. deprecated:: 1.5.0
1229+
"""
1230+
self._deprecate("handles")
1231+
return self._handles
1232+
1233+
@property
1234+
def path(self):
1235+
"""
1236+
Path to Excel file.
1237+
1238+
.. deprecated:: 1.5.0
1239+
"""
1240+
self._deprecate("path")
1241+
return self._path
11441242

11451243
def __fspath__(self):
1146-
return getattr(self.handles.handle, "name", "")
1244+
return getattr(self._handles.handle, "name", "")
11471245

11481246
def _get_sheet_name(self, sheet_name: str | None) -> str:
11491247
if sheet_name is None:
1150-
sheet_name = self.cur_sheet
1248+
sheet_name = self._cur_sheet
11511249
if sheet_name is None: # pragma: no cover
1152-
raise ValueError("Must pass explicit sheet_name or set cur_sheet property")
1250+
raise ValueError("Must pass explicit sheet_name or set _cur_sheet property")
11531251
return sheet_name
11541252

11551253
def _value_with_fmt(self, val) -> tuple[object, str | None]:
@@ -1175,9 +1273,9 @@ def _value_with_fmt(self, val) -> tuple[object, str | None]:
11751273
elif is_bool(val):
11761274
val = bool(val)
11771275
elif isinstance(val, datetime.datetime):
1178-
fmt = self.datetime_format
1276+
fmt = self._datetime_format
11791277
elif isinstance(val, datetime.date):
1180-
fmt = self.date_format
1278+
fmt = self._date_format
11811279
elif isinstance(val, datetime.timedelta):
11821280
val = val.total_seconds() / 86400
11831281
fmt = "0"
@@ -1213,8 +1311,8 @@ def __exit__(self, exc_type, exc_value, traceback):
12131311

12141312
def close(self) -> None:
12151313
"""synonym for save, to make it more file-like"""
1216-
self.save()
1217-
self.handles.close()
1314+
self._save()
1315+
self._handles.close()
12181316

12191317

12201318
XLS_SIGNATURES = (

pandas/io/excel/_odswriter.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,18 @@ def __init__(
5555

5656
engine_kwargs = combine_kwargs(engine_kwargs, kwargs)
5757

58-
self.book = OpenDocumentSpreadsheet(**engine_kwargs)
58+
self._book = OpenDocumentSpreadsheet(**engine_kwargs)
5959
self._style_dict: dict[str, str] = {}
6060

61+
@property
62+
def book(self):
63+
"""
64+
Book instance of class odf.opendocument.OpenDocumentSpreadsheet.
65+
66+
This attribute can be used to access engine-specific features.
67+
"""
68+
return self._book
69+
6170
@property
6271
def sheets(self) -> dict[str, Any]:
6372
"""Mapping of sheet names to sheet objects."""
@@ -69,15 +78,15 @@ def sheets(self) -> dict[str, Any]:
6978
}
7079
return result
7180

72-
def save(self) -> None:
81+
def _save(self) -> None:
7382
"""
7483
Save workbook to disk.
7584
"""
7685
for sheet in self.sheets.values():
7786
self.book.spreadsheet.addElement(sheet)
78-
self.book.save(self.handles.handle)
87+
self.book.save(self._handles.handle)
7988

80-
def write_cells(
89+
def _write_cells(
8190
self,
8291
cells: list[ExcelCell],
8392
sheet_name: str | None = None,

0 commit comments

Comments
 (0)