Skip to content

Commit 3c23e6e

Browse files
authored
ENH: storage_options for to_excel (#37818)
1 parent b34dbe8 commit 3c23e6e

File tree

8 files changed

+89
-22
lines changed

8 files changed

+89
-22
lines changed

pandas/core/generic.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -2028,9 +2028,9 @@ def _repr_data_resource_(self):
20282028
def to_excel(
20292029
self,
20302030
excel_writer,
2031-
sheet_name="Sheet1",
2032-
na_rep="",
2033-
float_format=None,
2031+
sheet_name: str = "Sheet1",
2032+
na_rep: str = "",
2033+
float_format: Optional[str] = None,
20342034
columns=None,
20352035
header=True,
20362036
index=True,
@@ -2043,6 +2043,7 @@ def to_excel(
20432043
inf_rep="inf",
20442044
verbose=True,
20452045
freeze_panes=None,
2046+
storage_options: StorageOptions = None,
20462047
) -> None:
20472048
"""
20482049
Write {klass} to an Excel sheet.
@@ -2059,7 +2060,7 @@ def to_excel(
20592060
20602061
Parameters
20612062
----------
2062-
excel_writer : str or ExcelWriter object
2063+
excel_writer : path-like, file-like, or ExcelWriter object
20632064
File path or existing ExcelWriter.
20642065
sheet_name : str, default 'Sheet1'
20652066
Name of sheet which will contain DataFrame.
@@ -2100,6 +2101,12 @@ def to_excel(
21002101
freeze_panes : tuple of int (length 2), optional
21012102
Specifies the one-based bottommost row and rightmost column that
21022103
is to be frozen.
2104+
storage_options : dict, optional
2105+
Extra options that make sense for a particular storage connection, e.g.
2106+
host, port, username, password, etc., if using a URL that will
2107+
be parsed by ``fsspec``, e.g., starting "s3://", "gcs://".
2108+
2109+
.. versionadded:: 1.2.0
21032110
21042111
See Also
21052112
--------
@@ -2174,6 +2181,7 @@ def to_excel(
21742181
startcol=startcol,
21752182
freeze_panes=freeze_panes,
21762183
engine=engine,
2184+
storage_options=storage_options,
21772185
)
21782186

21792187
@final

pandas/io/excel/_base.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
from io import BufferedIOBase, BytesIO, RawIOBase
44
import os
55
from textwrap import fill
6-
from typing import Any, Mapping, Union
6+
from typing import Any, Dict, Mapping, Union, cast
77

88
from pandas._config import config
99

1010
from pandas._libs.parsers import STR_NA_VALUES
11-
from pandas._typing import StorageOptions
11+
from pandas._typing import Buffer, FilePathOrBuffer, StorageOptions
1212
from pandas.errors import EmptyDataError
1313
from pandas.util._decorators import Appender, deprecate_nonkeyword_arguments
1414

@@ -567,6 +567,12 @@ class ExcelWriter(metaclass=abc.ABCMeta):
567567
File mode to use (write or append). Append does not work with fsspec URLs.
568568
569569
.. versionadded:: 0.24.0
570+
storage_options : dict, optional
571+
Extra options that make sense for a particular storage connection, e.g.
572+
host, port, username, password, etc., if using a URL that will
573+
be parsed by ``fsspec``, e.g., starting "s3://", "gcs://".
574+
575+
.. versionadded:: 1.2.0
570576
571577
Attributes
572578
----------
@@ -710,11 +716,12 @@ def save(self):
710716

711717
def __init__(
712718
self,
713-
path,
719+
path: Union[FilePathOrBuffer, "ExcelWriter"],
714720
engine=None,
715721
date_format=None,
716722
datetime_format=None,
717-
mode="w",
723+
mode: str = "w",
724+
storage_options: StorageOptions = None,
718725
**engine_kwargs,
719726
):
720727
# validate that this engine can handle the extension
@@ -729,10 +736,13 @@ def __init__(
729736
# the excel backend first read the existing file and then write any data to it
730737
mode = mode.replace("a", "r+")
731738

732-
self.handles = IOHandles(path, compression={"copression": None})
739+
# cast ExcelWriter to avoid adding 'if self.handles is not None'
740+
self.handles = IOHandles(cast(Buffer, path), compression={"copression": None})
733741
if not isinstance(path, ExcelWriter):
734-
self.handles = get_handle(path, mode, is_text=False)
735-
self.sheets = {}
742+
self.handles = get_handle(
743+
path, mode, storage_options=storage_options, is_text=False
744+
)
745+
self.sheets: Dict[str, Any] = {}
736746
self.cur_sheet = None
737747

738748
if date_format is None:

pandas/io/excel/_odswriter.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union
44

55
import pandas._libs.json as json
6+
from pandas._typing import StorageOptions
67

78
from pandas.io.excel._base import ExcelWriter
89
from pandas.io.excel._util import validate_freeze_panes
@@ -14,7 +15,12 @@ class ODSWriter(ExcelWriter):
1415
supported_extensions = (".ods",)
1516

1617
def __init__(
17-
self, path: str, engine: Optional[str] = None, mode: str = "w", **engine_kwargs
18+
self,
19+
path: str,
20+
engine: Optional[str] = None,
21+
mode: str = "w",
22+
storage_options: StorageOptions = None,
23+
**engine_kwargs,
1824
):
1925
from odf.opendocument import OpenDocumentSpreadsheet
2026

@@ -23,7 +29,9 @@ def __init__(
2329
if mode == "a":
2430
raise ValueError("Append mode is not supported with odf!")
2531

26-
super().__init__(path, mode=mode, **engine_kwargs)
32+
super().__init__(
33+
path, mode=mode, storage_options=storage_options, **engine_kwargs
34+
)
2735

2836
self.book = OpenDocumentSpreadsheet()
2937
self._style_dict: Dict[str, str] = {}

pandas/io/excel/_openpyxl.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,20 @@ class OpenpyxlWriter(ExcelWriter):
1616
engine = "openpyxl"
1717
supported_extensions = (".xlsx", ".xlsm")
1818

19-
def __init__(self, path, engine=None, mode="w", **engine_kwargs):
19+
def __init__(
20+
self,
21+
path,
22+
engine=None,
23+
mode: str = "w",
24+
storage_options: StorageOptions = None,
25+
**engine_kwargs,
26+
):
2027
# Use the openpyxl module as the Excel writer.
2128
from openpyxl.workbook import Workbook
2229

23-
super().__init__(path, mode=mode, **engine_kwargs)
30+
super().__init__(
31+
path, mode=mode, storage_options=storage_options, **engine_kwargs
32+
)
2433

2534
# ExcelWriter replaced "a" by "r+" to allow us to first read the excel file from
2635
# the file and later write to it

pandas/io/excel/_xlsxwriter.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Dict, List, Tuple
22

33
import pandas._libs.json as json
4+
from pandas._typing import StorageOptions
45

56
from pandas.io.excel._base import ExcelWriter
67
from pandas.io.excel._util import validate_freeze_panes
@@ -168,7 +169,8 @@ def __init__(
168169
engine=None,
169170
date_format=None,
170171
datetime_format=None,
171-
mode="w",
172+
mode: str = "w",
173+
storage_options: StorageOptions = None,
172174
**engine_kwargs,
173175
):
174176
# Use the xlsxwriter module as the Excel writer.
@@ -183,6 +185,7 @@ def __init__(
183185
date_format=date_format,
184186
datetime_format=datetime_format,
185187
mode=mode,
188+
storage_options=storage_options,
186189
**engine_kwargs,
187190
)
188191

pandas/io/excel/_xlwt.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING, Dict
22

33
import pandas._libs.json as json
4+
from pandas._typing import StorageOptions
45

56
from pandas.io.excel._base import ExcelWriter
67
from pandas.io.excel._util import validate_freeze_panes
@@ -13,7 +14,15 @@ class XlwtWriter(ExcelWriter):
1314
engine = "xlwt"
1415
supported_extensions = (".xls",)
1516

16-
def __init__(self, path, engine=None, encoding=None, mode="w", **engine_kwargs):
17+
def __init__(
18+
self,
19+
path,
20+
engine=None,
21+
encoding=None,
22+
mode: str = "w",
23+
storage_options: StorageOptions = None,
24+
**engine_kwargs,
25+
):
1726
# Use the xlwt module as the Excel writer.
1827
import xlwt
1928

@@ -22,7 +31,9 @@ def __init__(self, path, engine=None, encoding=None, mode="w", **engine_kwargs):
2231
if mode == "a":
2332
raise ValueError("Append mode is not supported with xlwt!")
2433

25-
super().__init__(path, mode=mode, **engine_kwargs)
34+
super().__init__(
35+
path, mode=mode, storage_options=storage_options, **engine_kwargs
36+
)
2637

2738
if encoding is None:
2839
encoding = "ascii"

pandas/io/formats/excel.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import numpy as np
1212

13-
from pandas._typing import Label
13+
from pandas._typing import Label, StorageOptions
1414

1515
from pandas.core.dtypes import missing
1616
from pandas.core.dtypes.common import is_float, is_scalar
@@ -19,7 +19,6 @@
1919
from pandas import DataFrame, Index, MultiIndex, PeriodIndex
2020
import pandas.core.common as com
2121

22-
from pandas.io.common import stringify_path
2322
from pandas.io.formats.css import CSSResolver, CSSWarning
2423
from pandas.io.formats.format import get_level_lengths
2524
from pandas.io.formats.printing import pprint_thing
@@ -785,9 +784,10 @@ def write(
785784
startcol=0,
786785
freeze_panes=None,
787786
engine=None,
787+
storage_options: StorageOptions = None,
788788
):
789789
"""
790-
writer : string or ExcelWriter object
790+
writer : path-like, file-like, or ExcelWriter object
791791
File path or existing ExcelWriter
792792
sheet_name : string, default 'Sheet1'
793793
Name of sheet which will contain DataFrame
@@ -802,6 +802,12 @@ def write(
802802
write engine to use if writer is a path - you can also set this
803803
via the options ``io.excel.xlsx.writer``, ``io.excel.xls.writer``,
804804
and ``io.excel.xlsm.writer``.
805+
storage_options : dict, optional
806+
Extra options that make sense for a particular storage connection, e.g.
807+
host, port, username, password, etc., if using a URL that will
808+
be parsed by ``fsspec``, e.g., starting "s3://", "gcs://".
809+
810+
.. versionadded:: 1.2.0
805811
"""
806812
from pandas.io.excel import ExcelWriter
807813

@@ -819,7 +825,7 @@ def write(
819825
# abstract class 'ExcelWriter' with abstract attributes 'engine',
820826
# 'save', 'supported_extensions' and 'write_cells' [abstract]
821827
writer = ExcelWriter( # type: ignore[abstract]
822-
stringify_path(writer), engine=engine
828+
writer, engine=engine, storage_options=storage_options
823829
)
824830
need_save = True
825831

pandas/tests/io/test_fsspec.py

+12
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ def test_csv_options(fsspectest):
124124
assert fsspectest.test[0] == "csv_read"
125125

126126

127+
@pytest.mark.parametrize("extension", ["xlsx", "xls"])
128+
def test_excel_options(fsspectest, extension):
129+
df = DataFrame({"a": [0]})
130+
131+
path = f"testmem://test/test.{extension}"
132+
133+
df.to_excel(path, storage_options={"test": "write"}, index=False)
134+
assert fsspectest.test[0] == "write"
135+
read_excel(path, storage_options={"test": "read"})
136+
assert fsspectest.test[0] == "read"
137+
138+
127139
@td.skip_if_no("fastparquet")
128140
def test_to_parquet_new_file(monkeypatch, cleared_fs):
129141
"""Regression test for writing to a not-yet-existent GCS Parquet file."""

0 commit comments

Comments
 (0)