Skip to content

Commit b482ff4

Browse files
feat(s3): add pandas_mode to to_csv to pass file open mode (e.g. a) through to pandas, resolving collision with dataset mode parameter
1 parent e32e74e commit b482ff4

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

awswrangler/s3/_write_text.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def _to_text(
6060
file_path = path
6161
else:
6262
raise RuntimeError("path and path_root received at the same time.")
63-
63+
64+
pandas_write_mode = pandas_kwargs.pop("mode", None)
6465
mode, encoding, newline = _get_write_details(path=file_path, pandas_kwargs=pandas_kwargs)
6566
with open_s3_object(
6667
path=file_path,
@@ -71,9 +72,22 @@ def _to_text(
7172
encoding=encoding,
7273
newline=newline,
7374
) as f:
75+
if pandas_write_mode == "a":
76+
# S3 has no native append — read existing content and write it first.
77+
try:
78+
with open_s3_object(
79+
path=file_path,
80+
mode="r" if pandas_kwargs.get("compression") is None else "rb",
81+
s3_client=s3_client,
82+
s3_additional_kwargs=s3_additional_kwargs,
83+
encoding=encoding,
84+
) as existing:
85+
f.write(existing.read())
86+
except Exception:
87+
pass # File does not exist yet — first write, nothing to prepend.
7488
_logger.debug("pandas_kwargs: %s", pandas_kwargs)
7589
if file_format == "csv":
76-
df.to_csv(f, mode=mode, **pandas_kwargs)
90+
df.to_csv(f, **pandas_kwargs)
7791
elif file_format == "json":
7892
df.to_json(f, **pandas_kwargs)
7993
return [file_path]
@@ -99,6 +113,7 @@ def to_csv( # noqa: PLR0912,PLR0915
99113
bucketing_info: BucketingInfoTuple | None = None,
100114
concurrent_partitioning: bool = False,
101115
mode: Literal["append", "overwrite", "overwrite_partitions"] | None = None,
116+
pandas_mode: str | None = None,
102117
catalog_versioning: bool = False,
103118
schema_evolution: bool = False,
104119
dtype: dict[str, str] | None = None,
@@ -251,6 +266,10 @@ def to_csv( # noqa: PLR0912,PLR0915
251266
valid Pandas arguments in the function call and awswrangler will accept it.
252267
e.g. wr.s3.to_csv(df, path, sep='|', na_rep='NULL', decimal=',')
253268
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html
269+
pandas_mode
270+
Pandas file open mode passed to ``df.to_csv()`` (e.g. ``"a"`` to append).
271+
Distinct from the ``mode`` parameter which controls dataset write behaviour.
272+
Only relevant when ``dataset=False``. Defaults to ``None`` (pandas default ``"w"``).
254273
255274
Returns
256275
-------
@@ -516,6 +535,8 @@ def to_csv( # noqa: PLR0912,PLR0915
516535
pandas_kwargs["sep"] = sep
517536
pandas_kwargs["index"] = index
518537
pandas_kwargs["columns"] = columns
538+
if pandas_mode is not None:
539+
pandas_kwargs["mode"] = pandas_mode
519540
_to_text(
520541
df,
521542
file_format="csv",

tests/unit/test_moto.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,3 +760,14 @@ def test_extract_ctas_manifest_paths_cross_bucket_raises(moto_s3_client: "S3Clie
760760

761761
with pytest.raises(InvalidArgumentValue, match="unexpected bucket"):
762762
_extract_ctas_manifest_paths(path=f"s3://bucket/{manifest_key}")
763+
764+
765+
def test_csv_pandas_mode_append(moto_s3_client: "S3Client") -> None:
766+
path = "s3://bucket/test_append.csv"
767+
df1 = pd.DataFrame({"col": [1, 2, 3]})
768+
df2 = pd.DataFrame({"col": [4, 5, 6]})
769+
wr.s3.to_csv(df=df1, path=path, index=False)
770+
wr.s3.to_csv(df=df2, path=path, index=False, pandas_mode="a", header=False)
771+
result = wr.s3.read_csv(path=path)
772+
assert len(result) == 6
773+
assert list(result["col"]) == [1, 2, 3, 4, 5, 6]

0 commit comments

Comments
 (0)