Skip to content

Commit

Permalink
DEP: Protect some ExcelWriter attributes (pandas-dev#45795)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rhshadrach authored and yehoshuadimarsky committed Jul 13, 2022
1 parent f27e215 commit dd298d9
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 62 deletions.
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

0 comments on commit dd298d9

Please sign in to comment.