Skip to content

Commit 3a8f4e6

Browse files
committed
Add support for path-like file_name in to_csv and to_parquet
1 parent 3da4ec5 commit 3a8f4e6

8 files changed

Lines changed: 70 additions & 19 deletions

File tree

_duckdb-stubs/__init__.pyi

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ class DuckDBPyRelation:
707707
def tf(self) -> dict[str, typing.Any]: ...
708708
def to_csv(
709709
self,
710-
file_name: str,
710+
file_name: str | os.PathLike[str],
711711
*,
712712
sep: str | None = None,
713713
na_rep: str | None = None,
@@ -728,7 +728,7 @@ class DuckDBPyRelation:
728728
def to_df(self, *, date_as_object: bool = False) -> pandas.DataFrame: ...
729729
def to_parquet(
730730
self,
731-
file_name: str,
731+
file_name: str | os.PathLike[str],
732732
*,
733733
compression: ParquetCompression | None = None,
734734
field_ids: ParquetFieldsOptions | None = None,
@@ -764,7 +764,7 @@ class DuckDBPyRelation:
764764
) -> DuckDBPyRelation: ...
765765
def write_csv(
766766
self,
767-
file_name: str,
767+
file_name: str | os.PathLike[str],
768768
*,
769769
sep: str | None = None,
770770
na_rep: str | None = None,
@@ -784,7 +784,7 @@ class DuckDBPyRelation:
784784
) -> None: ...
785785
def write_parquet(
786786
self,
787-
file_name: str,
787+
file_name: str | os.PathLike[str],
788788
*,
789789
compression: ParquetCompression | None = None,
790790
field_ids: ParquetFieldsOptions | None = None,
@@ -1272,7 +1272,7 @@ def values(*args: IntoValues, connection: DuckDBPyConnection | None = None) -> D
12721272
def view(view_name: str, *, connection: DuckDBPyConnection | None = None) -> DuckDBPyRelation: ...
12731273
def write_csv(
12741274
df: pandas.DataFrame,
1275-
filename: str,
1275+
filename: str | os.PathLike[str],
12761276
*,
12771277
sep: str | None = None,
12781278
na_rep: str | None = None,

src/duckdb_python.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ static void InitializeConnectionMethods(nb::module_ &m) {
866866
nb::arg("connection").none() = nb::none());
867867
m.def(
868868
"write_csv",
869-
[](const PandasDataFrame &df, const string &filename, const nb::object &sep = nb::none(),
869+
[](const PandasDataFrame &df, const nb::object &filename, const nb::object &sep = nb::none(),
870870
const nb::object &na_rep = nb::none(), const nb::object &header = nb::none(),
871871
const nb::object &quotechar = nb::none(), const nb::object &escapechar = nb::none(),
872872
const nb::object &date_format = nb::none(), const nb::object &timestamp_format = nb::none(),

src/include/duckdb_python/path_like.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ namespace duckdb {
99

1010
struct DuckDBPyConnection;
1111

12+
bool TryDecodePath(const nb::object &object, string &result);
13+
string PathToString(const nb::object &object);
14+
1215
struct PathLike {
1316
static PathLike Create(const nb::object &object, DuckDBPyConnection &connection);
1417
// The file(s) extracted from object

src/include/duckdb_python/pyrelation.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,15 @@ struct DuckDBPyRelation {
211211
std::unique_ptr<DuckDBPyRelation> Join(DuckDBPyRelation *other, const nb::object &condition, const string &type);
212212
std::unique_ptr<DuckDBPyRelation> Cross(DuckDBPyRelation *other);
213213

214-
void ToParquet(const string &filename, const nb::object &compression = nb::none(),
214+
void ToParquet(const nb::object &file_name, const nb::object &compression = nb::none(),
215215
const nb::object &field_ids = nb::none(), const nb::object &row_group_size_bytes = nb::none(),
216216
const nb::object &row_group_size = nb::none(), const nb::object &overwrite = nb::none(),
217217
const nb::object &per_thread_output = nb::none(), const nb::object &use_tmp_file = nb::none(),
218218
const nb::object &partition_by = nb::none(), const nb::object &write_partition_columns = nb::none(),
219219
const nb::object &append = nb::none(), const nb::object &filename_pattern = nb::none(),
220220
const nb::object &file_size_bytes = nb::none());
221221

222-
void ToCSV(const string &filename, const nb::object &sep = nb::none(), const nb::object &na_rep = nb::none(),
222+
void ToCSV(const nb::object &file_name, const nb::object &sep = nb::none(), const nb::object &na_rep = nb::none(),
223223
const nb::object &header = nb::none(), const nb::object &quotechar = nb::none(),
224224
const nb::object &escapechar = nb::none(), const nb::object &date_format = nb::none(),
225225
const nb::object &timestamp_format = nb::none(), const nb::object &quoting = nb::none(),

src/path_like.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,33 @@ struct PathLikeProcessor {
3434
vector<string> fs_files;
3535
};
3636

37-
void PathLikeProcessor::AddFile(const nb::object &object) {
37+
bool TryDecodePath(const nb::object &object, string &result) {
3838
if (nb::isinstance<nb::str>(object)) {
39-
all_files.push_back(nb::cast<std::string>(nb::str(object)));
40-
return;
39+
result = nb::cast<string>(object);
40+
return true;
4141
}
4242
if (nb::isinstance<nb::bytes>(object) || nb::hasattr(object, "__fspath__")) {
4343
// A bytes path or an os.PathLike object (e.g. pathlib.Path) - decode it to a string
4444
auto fsdecode = nb::module_::import_("os").attr("fsdecode");
45-
all_files.push_back(nb::cast<std::string>(nb::str(fsdecode(object))));
45+
result = nb::cast<string>(fsdecode(object));
46+
return true;
47+
}
48+
return false;
49+
}
50+
51+
string PathToString(const nb::object &object) {
52+
string result;
53+
if (!TryDecodePath(object, result)) {
54+
throw InvalidInputException("Expected a str, bytes, or os.PathLike object for the file path, not '%s'",
55+
Py_TYPE(object.ptr())->tp_name);
56+
}
57+
return result;
58+
}
59+
60+
void PathLikeProcessor::AddFile(const nb::object &object) {
61+
string decoded;
62+
if (TryDecodePath(object, decoded)) {
63+
all_files.push_back(std::move(decoded));
4664
return;
4765
}
4866
// This is (assumed to be) a file-like object

src/pyrelation.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "duckdb_python/nb/casters.hpp"
22
#include "duckdb_python/pyrelation.hpp"
3+
#include "duckdb_python/path_like.hpp"
34
#include "duckdb_python/pyconnection/pyconnection.hpp"
45
#include "duckdb_python/pytype.hpp"
56
#include "duckdb_python/pyresult.hpp"
@@ -1253,12 +1254,14 @@ static Value NestedDictToStruct(const nb::object &dictionary) {
12531254
return Value::STRUCT(std::move(children));
12541255
}
12551256

1256-
void DuckDBPyRelation::ToParquet(const string &filename, const nb::object &compression, const nb::object &field_ids,
1257-
const nb::object &row_group_size_bytes, const nb::object &row_group_size,
1258-
const nb::object &overwrite, const nb::object &per_thread_output,
1259-
const nb::object &use_tmp_file, const nb::object &partition_by,
1260-
const nb::object &write_partition_columns, const nb::object &append,
1261-
const nb::object &filename_pattern, const nb::object &file_size_bytes) {
1257+
void DuckDBPyRelation::ToParquet(const nb::object &file_name, const nb::object &compression,
1258+
const nb::object &field_ids, const nb::object &row_group_size_bytes,
1259+
const nb::object &row_group_size, const nb::object &overwrite,
1260+
const nb::object &per_thread_output, const nb::object &use_tmp_file,
1261+
const nb::object &partition_by, const nb::object &write_partition_columns,
1262+
const nb::object &append, const nb::object &filename_pattern,
1263+
const nb::object &file_size_bytes) {
1264+
auto filename = PathToString(file_name);
12621265
case_insensitive_map_t<vector<Value>> options;
12631266

12641267
if (!nb::none().is(compression)) {
@@ -1371,13 +1374,14 @@ void DuckDBPyRelation::ToParquet(const string &filename, const nb::object &compr
13711374
PyExecuteRelation(write_parquet);
13721375
}
13731376

1374-
void DuckDBPyRelation::ToCSV(const string &filename, const nb::object &sep, const nb::object &na_rep,
1377+
void DuckDBPyRelation::ToCSV(const nb::object &file_name, const nb::object &sep, const nb::object &na_rep,
13751378
const nb::object &header, const nb::object &quotechar, const nb::object &escapechar,
13761379
const nb::object &date_format, const nb::object &timestamp_format,
13771380
const nb::object &quoting, const nb::object &encoding, const nb::object &compression,
13781381
const nb::object &overwrite, const nb::object &per_thread_output,
13791382
const nb::object &use_tmp_file, const nb::object &partition_by,
13801383
const nb::object &write_partition_columns) {
1384+
auto filename = PathToString(file_name);
13811385
case_insensitive_map_t<vector<Value>> options;
13821386

13831387
if (!nb::none().is(sep)) {

tests/fast/api/test_to_csv.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,16 @@ def test_to_csv_use_tmp_file(self):
299299
rel.to_csv(temp_file_name, header=True, use_tmp_file=True)
300300
csv_rel = duckdb.read_csv(temp_file_name, header=True)
301301
assert rel.execute().fetchall() == csv_rel.execute().fetchall()
302+
303+
def test_to_csv_pathlib(self, tmp_path):
304+
file_path = tmp_path / "test.csv" # pathlib.Path
305+
df = pd.DataFrame({"a": [5, 3, 23, 2], "b": [45, 234, 234, 2]})
306+
rel = duckdb.from_df(df)
307+
rel.to_csv(file_path)
308+
assert rel.execute().fetchall() == duckdb.read_csv(file_path).execute().fetchall()
309+
310+
def test_to_csv_rejects_non_path(self):
311+
df = pd.DataFrame({"a": [5, 3, 23, 2], "b": [45, 234, 234, 2]})
312+
rel = duckdb.from_df(df)
313+
with pytest.raises(duckdb.InvalidInputException):
314+
rel.to_csv(123)

tests/fast/api/test_to_parquet.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ def test_use_tmp_file(self):
117117
result = duckdb.read_parquet(temp_file_name)
118118
assert rel.execute().fetchall() == result.execute().fetchall()
119119

120+
def test_to_parquet_pathlib(self, tmp_path):
121+
file_name = tmp_path / "test.parquet" # pathlib.Path
122+
df = pd.DataFrame({"a": [5, 3, 23, 2], "b": [45, 234, 234, 2]})
123+
rel = duckdb.from_df(df)
124+
rel.to_parquet(file_name)
125+
assert rel.execute().fetchall() == duckdb.read_parquet(file_name).execute().fetchall()
126+
127+
def test_to_parquet_rejects_non_path(self):
128+
df = pd.DataFrame({"a": [5, 3, 23, 2], "b": [45, 234, 234, 2]})
129+
rel = duckdb.from_df(df)
130+
with pytest.raises(duckdb.InvalidInputException):
131+
rel.to_parquet(123)
132+
120133
def test_per_thread_output(self):
121134
temp_file_name = os.path.join(tempfile.mkdtemp(), next(tempfile._get_candidate_names())) # noqa: PTH118
122135
num_threads = duckdb.sql("select current_setting('threads')").fetchone()[0]

0 commit comments

Comments
 (0)