@@ -119,6 +119,63 @@ test_that("pandas2df recovers 64-bit ids and object columns", {
119119 expect_identical(out $ label , c(" a" , " b" ))
120120})
121121
122+ test_that(" pandas2df flattens object columns and normalises datetimes" , {
123+ skip_if_no_module(" pandas" )
124+ # An object column of python ints (flattened to an atomic vector), a genuine
125+ # list-valued object column (a multi-select, left as a list), an all-None
126+ # object column (-> NA), and a datetime column (-> UTC POSIXct).
127+ df <- reticulate :: py_eval(
128+ paste0(" __import__('pandas').DataFrame({" ,
129+ " 'n': __import__('pandas').Series([1, 2, 3], dtype=object), " ,
130+ " 'tags': __import__('pandas').Series([['AB'], ['CD'], []], dtype=object), " ,
131+ " 'blank': __import__('pandas').Series([None, None, None], dtype=object), " ,
132+ " 'when': __import__('pandas').to_datetime(" ,
133+ " ['2020-01-01', '2020-01-02', '2020-01-03'])})" ),
134+ convert = FALSE )
135+ out <- pandas2df(df )
136+ # object ints flattened to a plain atomic vector, not a list of length-1s
137+ expect_false(is.list(out $ n ))
138+ expect_identical(out $ n , c(1L , 2L , 3L ))
139+ # a genuine list-valued (multi-select) column is left intact as a list
140+ expect_true(is.list(out $ tags ))
141+ expect_identical(out $ tags [[1 ]], " AB" )
142+ expect_length(out $ tags [[3 ]], 0L )
143+ # an all-None object column collapses to NA
144+ expect_true(all(is.na(out $ blank )))
145+ # datetimes come back as UTC POSIXct
146+ expect_s3_class(out $ when , " POSIXct" )
147+ expect_identical(attr(out $ when , " tzone" ), " UTC" )
148+ expect_identical(as.character(as.Date(out $ when )),
149+ c(" 2020-01-01" , " 2020-01-02" , " 2020-01-03" ))
150+ })
151+
152+ test_that(" pandas2df use_arrow path round-trips via a feather file" , {
153+ skip_if_no_module(" pandas" )
154+ skip_if_no_module(" pyarrow" ) # pandas.to_feather needs pyarrow
155+ skip_if_not_installed(" arrow" ) # R arrow::read_feather
156+ df <- reticulate :: py_eval(
157+ " __import__('pandas').DataFrame({'label': ['a', 'b', 'c'], 'n': [1, 2, 3]})" ,
158+ convert = FALSE )
159+ out <- pandas2df(df , use_arrow = TRUE )
160+ # the arrow path always returns a tibble
161+ expect_s3_class(out , " tbl_df" )
162+ expect_identical(out $ label , c(" a" , " b" , " c" ))
163+ expect_equal(as.integer(out $ n ), 1 : 3 )
164+ })
165+
166+ test_that(" pandas2df use_arrow handles an empty frame" , {
167+ skip_if_no_module(" pandas" )
168+ skip_if_not_installed(" arrow" )
169+ skip_if_not_installed(" tibble" )
170+ # the empty-frame branch returns via py_to_r + tibble, without touching arrow
171+ df <- reticulate :: py_eval(
172+ " __import__('pandas').DataFrame({'label': [], 'n': []})" , convert = FALSE )
173+ out <- pandas2df(df , use_arrow = TRUE )
174+ expect_s3_class(out , " tbl_df" )
175+ expect_identical(nrow(out ), 0L )
176+ expect_identical(names(out ), c(" label" , " n" ))
177+ })
178+
122179test_that(" pandas2df fast-path preserves column order and nullable ids" , {
123180 skip_if_no_module(" pandas" )
124181 ids <- c(" 720575940621039145" , " 720575940626877799" )
0 commit comments