Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ jobs:
shell: Rscript {0}
run: |
reticulate::install_miniconda()
reticulate::py_install(c("numpy", "pandas"))
# pyarrow lets the use_arrow = TRUE conversion path be exercised
reticulate::py_install(c("numpy", "pandas", "pyarrow"))
pak::local_install()
cat(sprintf("RETICULATE_PYTHON=%s\n", reticulate::py_config()$python),
file = Sys.getenv("GITHUB_ENV"), append = TRUE)
Expand Down
57 changes: 57 additions & 0 deletions tests/testthat/test-convert.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,63 @@ test_that("pandas2df recovers 64-bit ids and object columns", {
expect_identical(out$label, c("a", "b"))
})

test_that("pandas2df flattens object columns and normalises datetimes", {
skip_if_no_module("pandas")
# An object column of python ints (flattened to an atomic vector), a genuine
# list-valued object column (a multi-select, left as a list), an all-None
# object column (-> NA), and a datetime column (-> UTC POSIXct).
df <- reticulate::py_eval(
paste0("__import__('pandas').DataFrame({",
"'n': __import__('pandas').Series([1, 2, 3], dtype=object), ",
"'tags': __import__('pandas').Series([['AB'], ['CD'], []], dtype=object), ",
"'blank': __import__('pandas').Series([None, None, None], dtype=object), ",
"'when': __import__('pandas').to_datetime(",
"['2020-01-01', '2020-01-02', '2020-01-03'])})"),
convert = FALSE)
out <- pandas2df(df)
# object ints flattened to a plain atomic vector, not a list of length-1s
expect_false(is.list(out$n))
expect_identical(out$n, c(1L, 2L, 3L))
# a genuine list-valued (multi-select) column is left intact as a list
expect_true(is.list(out$tags))
expect_identical(out$tags[[1]], "AB")
expect_length(out$tags[[3]], 0L)
# an all-None object column collapses to NA
expect_true(all(is.na(out$blank)))
# datetimes come back as UTC POSIXct
expect_s3_class(out$when, "POSIXct")
expect_identical(attr(out$when, "tzone"), "UTC")
expect_identical(as.character(as.Date(out$when)),
c("2020-01-01", "2020-01-02", "2020-01-03"))
})

test_that("pandas2df use_arrow path round-trips via a feather file", {
skip_if_no_module("pandas")
skip_if_no_module("pyarrow") # pandas.to_feather needs pyarrow
skip_if_not_installed("arrow") # R arrow::read_feather
df <- reticulate::py_eval(
"__import__('pandas').DataFrame({'label': ['a', 'b', 'c'], 'n': [1, 2, 3]})",
convert = FALSE)
out <- pandas2df(df, use_arrow = TRUE)
# the arrow path always returns a tibble
expect_s3_class(out, "tbl_df")
expect_identical(out$label, c("a", "b", "c"))
expect_equal(as.integer(out$n), 1:3)
})

test_that("pandas2df use_arrow handles an empty frame", {
skip_if_no_module("pandas")
skip_if_not_installed("arrow")
skip_if_not_installed("tibble")
# the empty-frame branch returns via py_to_r + tibble, without touching arrow
df <- reticulate::py_eval(
"__import__('pandas').DataFrame({'label': [], 'n': []})", convert = FALSE)
out <- pandas2df(df, use_arrow = TRUE)
expect_s3_class(out, "tbl_df")
expect_identical(nrow(out), 0L)
expect_identical(names(out), c("label", "n"))
})

test_that("pandas2df fast-path preserves column order and nullable ids", {
skip_if_no_module("pandas")
ids <- c("720575940621039145", "720575940626877799")
Expand Down
45 changes: 45 additions & 0 deletions tests/testthat/test-env.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Pure-R helpers in env.R. The provisioning machinery itself (install_miniconda,
# conda_install, ...) has real side effects and is not unit-tested here; these
# cover the option/env plumbing and the non-destructive "print the command"
# branches of simple_python_base().

test_that("np_condaenv honours the option and defaults to r-reticulate", {
withr::local_options(nat.python.condaenv = NULL)
expect_identical(np_condaenv(), "r-reticulate")
withr::local_options(nat.python.condaenv = "my-env")
expect_identical(np_condaenv(), "my-env")
})

test_that("ownpythonrequested reflects RETICULATE_PYTHON", {
withr::local_envvar(RETICULATE_PYTHON = "")
expect_false(ownpythonrequested())
withr::local_envvar(RETICULATE_PYTHON = "/opt/python/bin/python")
expect_true(ownpythonrequested())
})

test_that("checkownpython aborts for a non-standard Python", {
withr::local_envvar(RETICULATE_PYTHON = "")
# miniconda = FALSE means the user asked for their own Python
expect_error(checkownpython(miniconda = FALSE), "on your own")
withr::local_envvar(RETICULATE_PYTHON = "/opt/python/bin/python")
expect_error(checkownpython(miniconda = TRUE), "on your own")
# standard managed setup: no abort
withr::local_envvar(RETICULATE_PYTHON = "")
expect_silent(checkownpython(miniconda = TRUE))
})

test_that("check_reticulate is a no-op when check_python = FALSE", {
expect_true(check_reticulate(check_python = FALSE))
expect_invisible(check_reticulate(check_python = FALSE))
})

test_that("simple_python_base blast only prints and deletes nothing", {
withr::local_envvar(RETICULATE_PYTHON = "")
# the blast branch just prints the (destructive) unlink command; it must
# never touch the filesystem itself, and returns invisibly
existed <- dir.exists(reticulate::miniconda_path())
expect_invisible(res <- simple_python_base("blast", miniconda = TRUE))
expect_null(res)
# the miniconda directory is left exactly as it was
expect_identical(dir.exists(reticulate::miniconda_path()), existed)
})
50 changes: 50 additions & 0 deletions tests/testthat/test-int64.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,53 @@ test_that("rids2raw honours endianness", {
expect_false(identical(rids2raw(1:3, endian = "little"),
rids2raw(1:3, endian = "big")))
})

test_that("pyids2bit64 round-trips a numpy int64 array", {
skip_if_no_module("numpy")
np <- reticulate::import("numpy", convert = FALSE)
ids <- c("123", "720575940621039145", "9223372036854775807")
arr <- np$array(ids, dtype = "int64")
# default: character out
expect_identical(pyids2bit64(arr), ids)
# as_character = FALSE: an exact integer64 vector
i64 <- pyids2bit64(arr, as_character = FALSE)
expect_s3_class(i64, "integer64")
expect_identical(as.character(i64), ids)
})

test_that("pyids2bit64 accepts uint64 within signed range and rejects overflow", {
skip_if_no_module("numpy")
np <- reticulate::import("numpy", convert = FALSE)
ok <- np$array(c("123", "9223372036854775807"), dtype = "uint64")
expect_identical(pyids2bit64(ok), c("123", "9223372036854775807"))
# a uint64 value beyond the signed range cannot be represented as int64
ov <- np$array(c("18446744073709551615"), dtype = "uint64")
expect_error(pyids2bit64(ov), "int64 overflow")
# unsupported dtype is rejected
f <- np$array(c("1.5"), dtype = "float64")
expect_error(pyids2bit64(f), "dtype=int64 or uint64")
})

test_that("pyids2bit64 handles an empty array", {
skip_if_no_module("numpy")
np <- reticulate::import("numpy", convert = FALSE)
empty <- np$array(list(), dtype = "int64")
expect_identical(pyids2bit64(empty), character())
expect_identical(pyids2bit64(empty, as_character = FALSE), bit64::integer64())
})

test_that("rids2pyint round-trips R ids back through pyids2bit64", {
skip_if_no_module("numpy")
ids <- c("123", "720575940621039145", "9223372036854775807")
# in-memory string path (default for short vectors)
arr <- rids2pyint(ids, numpyarray = TRUE)
expect_identical(pyids2bit64(arr), ids)
# a Python list of ints is the non-numpyarray default
lst <- rids2pyint(ids)
expect_s3_class(lst, "python.builtin.list")
# the file-marshalling path (usefile = TRUE) yields the same ids
arr2 <- rids2pyint(ids, numpyarray = TRUE, usefile = TRUE)
expect_identical(pyids2bit64(arr2), ids)
# a numpy array passed in is returned as-is
expect_identical(pyids2bit64(rids2pyint(arr, numpyarray = TRUE)), ids)
})
20 changes: 20 additions & 0 deletions tests/testthat/test-time.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ts2pydatetime: the early-return for an already-converted datetime is pure R;
# the conversion path needs the Python datetime module.

test_that("ts2pydatetime returns an existing python datetime unchanged", {
# a value already carrying the datetime.datetime class is passed straight
# back, no Python needed
fake <- structure(list(), class = "datetime.datetime")
expect_identical(ts2pydatetime(fake), fake)
})

test_that("ts2pydatetime converts an R time to a UTC python datetime", {
skip_if_no_module("datetime")
t <- as.POSIXct("2020-01-02 03:04:05", tz = "UTC")
py <- ts2pydatetime(t)
expect_s3_class(py, "datetime.datetime")
# tzinfo is made explicit (UTC), and the instant round-trips exactly
expect_true(reticulate::py_to_r(py$tzinfo == reticulate::import("datetime")$timezone$utc))
back <- as.numeric(reticulate::py_to_r(py$timestamp()))
expect_equal(back, as.numeric(t))
})
31 changes: 31 additions & 0 deletions tests/testthat/test-utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Internal utility helpers in utils.R. check_suggested is pure R; py_np needs
# numpy and is skipped when Python is unavailable.

test_that("check_suggested passes for an installed package", {
# reticulate is a hard dependency, so it is always present
expect_true(check_suggested("reticulate"))
expect_invisible(check_suggested("reticulate"))
})

test_that("check_suggested errors for a missing package with instructions", {
nope <- "a.package.that.does.not.exist"
expect_error(check_suggested(nope), "is required")
expect_error(check_suggested(nope), "install.packages")
# the purpose string is woven into the message when supplied
expect_error(check_suggested(nope, "for the widget path"),
"for the widget path")
})

test_that("py_np imports and caches the numpy module", {
skip_if_no_module("numpy")
# clear any cached handle so we exercise the import branch
rm(list = ls(.nat_python_cache), envir = .nat_python_cache)
np1 <- py_np()
expect_s3_class(np1, "python.builtin.module")
# second call is served from the session cache -- same object handle
np2 <- py_np()
expect_identical(np1, np2)
# convert = TRUE is cached under a separate key
np3 <- py_np(convert = TRUE)
expect_s3_class(np3, "python.builtin.module")
})
Loading