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 all 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
37 changes: 37 additions & 0 deletions doc/source/whatsnew/v1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,43 @@ use ``series.loc[i:j]``.

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

.. _whatsnew_150.deprecations.excel_writer_attributes:

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

All attributes of :class:`ExcelWriter` were previously documented as not
public. However some third party Excel engines documented accessing
``ExcelWriter.book`` or ``ExcelWriter.sheets``, and users were utilizing these
and possibly other attributes. Previously these attributes were not safe to use;
e.g. modifications to ``ExcelWriter.book`` would not update ``ExcelWriter.sheets``
and conversely. In order to support this, pandas has made some attributes public
and improved their implementations so that they may now be safely used. (:issue:`45572`)

The following attributes are now public and considered safe to access.

- ``book``
- ``check_extension``
- ``close``
- ``date_format``
- ``datetime_format``
- ``engine``
- ``if_sheet_exists``
- ``sheets``
- ``supported_extensions``

The following attributes have been deprecated. They now raise a ``FutureWarning``
when accessed and will removed in a future version. Users should be aware
that their usage is considered unsafe, and can lead to unexpected results.

- ``cur_sheet``
- ``handles``
- ``path``
- ``save``
- ``write_cells``

See the documentation of :class:`ExcelWriter` for further details.

.. _whatsnew_150.deprecations.other:

Other Deprecations
Expand Down
154 changes: 126 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,16 @@ 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 +1065,8 @@ def write_cells(
"""
Write given formatted cells into Excel an excel sheet
.. deprecated:: 1.5.0
Parameters
----------
cells : generator
Expand All @@ -1077,12 +1078,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 +1147,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 +1176,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 +1273,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 +1311,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
17 changes: 13 additions & 4 deletions pandas/io/excel/_odswriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,18 @@ 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 +78,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
Loading