Skip to content

DEP: Protect some ExcelWriter attributes #45795

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

Merged
merged 7 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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: 0 additions & 1 deletion doc/source/reference/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ Excel

.. autosummary::
:toctree: api/
:template: autosummary/class_without_autosummary.rst

ExcelWriter

Expand Down
13 changes: 13 additions & 0 deletions doc/source/whatsnew/v1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ use ``series.loc[i:j]``.

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

.. _whatsnew_150.deprecations.excel_writer_attributes:

:class:`ExcelWriter` attributes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The attributes and methods of :class:`ExcelWriter` were previously documented as not
being public. However some third party Excel engines documented accessing
``ExcelWriter.book`` or ``ExcelWriter.sheets``, and users were utilizing these
and possibly other attributes. In order to support this, pandas has made some
attributes and methods public. Other attributes and methods have been deprecated
and will raise a ``FutureWarning`` when accessed as they will be removed in a future
version. See the documentation of :class:`ExcelWriter` for further details.

.. _whatsnew_150.deprecations.other:

Other Deprecations
Expand Down
153 changes: 125 additions & 28 deletions pandas/io/excel/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,18 +836,8 @@ class ExcelWriter(metaclass=abc.ABCMeta):

Use engine_kwargs instead.

Attributes
----------
None

Methods
-------
None

Notes
-----
None of the methods and properties are considered public.

For compatibility with CSV writers, ExcelWriter serializes lists
and dicts to strings before writing.

Expand Down Expand Up @@ -1034,7 +1024,7 @@ def __new__(
return object.__new__(cls)

# declare external properties you can count on
path = None
_path = None

@property
@abc.abstractmethod
Expand All @@ -1054,7 +1044,15 @@ def sheets(self) -> dict[str, Any]:
"""Mapping of sheet names to sheet objects."""
pass

@property
@abc.abstractmethod
def book(self):
"""Book instance. Class type will depend on the engine used.

This attribute can be used to access engine-specific features.
"""
pass

def write_cells(
self,
cells,
Expand All @@ -1066,6 +1064,8 @@ def write_cells(
"""
Write given formatted cells into Excel an excel sheet

.. deprecated:: 1.5.0

Parameters
----------
cells : generator
Expand All @@ -1077,12 +1077,47 @@ def write_cells(
freeze_panes: int tuple of length 2
contains the bottom-most row and right-most column to freeze
"""
pass
self._deprecate("write_cells")
return self._write_cells(cells, sheet_name, startrow, startcol, freeze_panes)

@abc.abstractmethod
def _write_cells(
self,
cells,
sheet_name: str | None = None,
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
) -> None:
"""
Write given formatted cells into Excel an excel sheet

Parameters
----------
cells : generator
cell of formatted data to save to Excel sheet
sheet_name : str, default None
Name of Excel sheet, if None, then use self.cur_sheet
startrow : upper left cell row to dump data frame
startcol : upper left cell column to dump data frame
freeze_panes: int tuple of length 2
contains the bottom-most row and right-most column to freeze
"""
pass

def save(self) -> None:
"""
Save workbook to disk.

.. deprecated:: 1.5.0
"""
self._deprecate("save")
return self._save()

@abc.abstractmethod
def _save(self) -> None:
"""
Save workbook to disk.
"""
pass

Expand Down Expand Up @@ -1111,25 +1146,25 @@ def __init__(
mode = mode.replace("a", "r+")

# cast ExcelWriter to avoid adding 'if self.handles is not None'
self.handles = IOHandles(
self._handles = IOHandles(
cast(IO[bytes], path), compression={"compression": None}
)
if not isinstance(path, ExcelWriter):
self.handles = get_handle(
self._handles = get_handle(
path, mode, storage_options=storage_options, is_text=False
)
self.cur_sheet = None
self._cur_sheet = None

if date_format is None:
self.date_format = "YYYY-MM-DD"
self._date_format = "YYYY-MM-DD"
else:
self.date_format = date_format
self._date_format = date_format
if datetime_format is None:
self.datetime_format = "YYYY-MM-DD HH:MM:SS"
self._datetime_format = "YYYY-MM-DD HH:MM:SS"
else:
self.datetime_format = datetime_format
self._datetime_format = datetime_format

self.mode = mode
self._mode = mode

if if_sheet_exists not in (None, "error", "new", "replace", "overlay"):
raise ValueError(
Expand All @@ -1140,16 +1175,78 @@ def __init__(
raise ValueError("if_sheet_exists is only valid in append mode (mode='a')")
if if_sheet_exists is None:
if_sheet_exists = "error"
self.if_sheet_exists = if_sheet_exists
self._if_sheet_exists = if_sheet_exists

def _deprecate(self, attr: str):
"""
Deprecate attribute or method for ExcelWriter.
"""
warnings.warn(
f"{attr} is not part of the public API, usage can give in unexpected "
"results and will be removed in a future version",
FutureWarning,
stacklevel=find_stack_level(),
)

@property
def date_format(self) -> str:
"""
Format string for dates written into Excel files (e.g. ‘YYYY-MM-DD’).
"""
return self._date_format

@property
def datetime_format(self) -> str:
"""
Format string for dates written into Excel files (e.g. ‘YYYY-MM-DD’).
"""
return self._datetime_format

@property
def if_sheet_exists(self) -> str:
"""
How to behave when writing to a sheet that already exists in append mode.
"""
return self._if_sheet_exists

@property
def cur_sheet(self):
"""
Current sheet for writing.

.. deprecated:: 1.5.0
"""
self._deprecate("cur_sheet")
return self._cur_sheet

@property
def handles(self):
"""
Handles to Excel sheets.

.. deprecated:: 1.5.0
"""
self._deprecate("handles")
return self._handles

@property
def path(self):
"""
Path to Excel file.

.. deprecated:: 1.5.0
"""
self._deprecate("path")
return self._path

def __fspath__(self):
return getattr(self.handles.handle, "name", "")
return getattr(self._handles.handle, "name", "")

def _get_sheet_name(self, sheet_name: str | None) -> str:
if sheet_name is None:
sheet_name = self.cur_sheet
sheet_name = self._cur_sheet
if sheet_name is None: # pragma: no cover
raise ValueError("Must pass explicit sheet_name or set cur_sheet property")
raise ValueError("Must pass explicit sheet_name or set _cur_sheet property")
return sheet_name

def _value_with_fmt(self, val) -> tuple[object, str | None]:
Expand All @@ -1175,9 +1272,9 @@ def _value_with_fmt(self, val) -> tuple[object, str | None]:
elif is_bool(val):
val = bool(val)
elif isinstance(val, datetime.datetime):
fmt = self.datetime_format
fmt = self._datetime_format
elif isinstance(val, datetime.date):
fmt = self.date_format
fmt = self._date_format
elif isinstance(val, datetime.timedelta):
val = val.total_seconds() / 86400
fmt = "0"
Expand Down Expand Up @@ -1213,8 +1310,8 @@ def __exit__(self, exc_type, exc_value, traceback):

def close(self) -> None:
"""synonym for save, to make it more file-like"""
self.save()
self.handles.close()
self._save()
self._handles.close()


XLS_SIGNATURES = (
Expand Down
23 changes: 16 additions & 7 deletions pandas/io/excel/_odswriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,17 @@ def __init__(

engine_kwargs = combine_kwargs(engine_kwargs, kwargs)

self.book = OpenDocumentSpreadsheet(**engine_kwargs)
self._book = OpenDocumentSpreadsheet(**engine_kwargs)
self._style_dict: dict[str, str] = {}

@property
def book(self):
"""Book instance of class odf.opendocument.OpenDocumentSpreadsheet.

This attribute can be used to access engine-specific features.
"""
return self._book

@property
def sheets(self) -> dict[str, Any]:
"""Mapping of sheet names to sheet objects."""
Expand All @@ -69,15 +77,15 @@ def sheets(self) -> dict[str, Any]:
}
return result

def save(self) -> None:
def _save(self) -> None:
"""
Save workbook to disk.
"""
for sheet in self.sheets.values():
self.book.spreadsheet.addElement(sheet)
self.book.save(self.handles.handle)
self.book.save(self._handles.handle)

def write_cells(
def _write_cells(
self,
cells: list[ExcelCell],
sheet_name: str | None = None,
Expand Down Expand Up @@ -131,9 +139,10 @@ def write_cells(
p = P(text=pvalue)
tc.addElement(p)

# add all rows to the sheet
for row_nr in range(max(rows.keys()) + 1):
wks.addElement(rows[row_nr])
if len(rows) > 0:
# add all rows to the sheet
for row_nr in range(max(rows.keys()) + 1):
wks.addElement(rows[row_nr])

def _make_table_cell_attributes(self, cell) -> dict[str, int | str]:
"""Convert cell attributes to OpenDocument attributes
Expand Down
Loading