diff --git a/DESCRIPTION b/DESCRIPTION index cf2131d6..fb6a1862 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -82,7 +82,8 @@ Suggests: SummarizedExperiment, testthat (>= 3.0.0), vctrs, - withr + withr, + yaml VignetteBuilder: knitr Config/Needs/website: pkgdown, tibble, knitr, rprojroot, stringr, readr, diff --git a/NAMESPACE b/NAMESPACE index 72fce24f..6ac4485a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,5 +16,6 @@ importFrom(cli,cli_inform) importFrom(cli,cli_warn) importFrom(methods,as) importFrom(methods,new) +importFrom(purrr,map_dfr) importFrom(purrr,map_lgl) importFrom(rlang,caller_env) diff --git a/R/anndataR-package.R b/R/anndataR-package.R index 04762c83..44ae4943 100644 --- a/R/anndataR-package.R +++ b/R/anndataR-package.R @@ -61,7 +61,7 @@ ## usethis namespace: start #' @importFrom cli cli_abort cli_warn cli_inform -#' @importFrom purrr map_lgl +#' @importFrom purrr map_lgl map_dfr #' @importFrom methods as new ## usethis namespace: end NULL diff --git a/R/known_issues.R b/R/known_issues.R new file mode 100644 index 00000000..36935b42 --- /dev/null +++ b/R/known_issues.R @@ -0,0 +1,71 @@ +# This file contains the known issues that are currently present in the package. +# It can be used to generate documentation, but also throw warnings instead of errors +# in tests. +read_known_issues <- function() { + check_requires("Reading known issues", "yaml") + + data <- yaml::read_yaml(system.file("known_issues.yaml", package = "anndataR")) + + map_dfr( + data$known_issues, + function(row) { + expected_names <- c( + "backend", "slot", "dtype", "process", "error_message", + "description", "proposed_solution", "to_investigate", + "to_fix" + ) + if (!all(expected_names %in% names(row))) { + stop( + "Expected columns ", paste0("'", expected_names, "'", collapse = ", "), + " in known_issues.yaml, but got ", paste0("'", names(row), "'", collapse = ", ") + ) + } + + expand.grid(row) + } + ) +} + +is_known <- function(backend, slot, dtype, process, known_issues = NULL) { + if (is.null(known_issues)) { + known_issues <- read_known_issues() + } + + filt <- rep(TRUE, nrow(known_issues)) + + if (!is.null(backend)) { + filt <- filt & known_issues$backend %in% backend + } + if (!is.null(slot)) { + filt <- filt & known_issues$slot %in% slot + } + if (!is.null(dtype)) { + filt <- filt & known_issues$dtype %in% dtype + } + if (!is.null(process)) { + filt <- filt & known_issues$process %in% process + } + + filt +} + +message_if_known <- function(backend, slot, dtype, process, known_issues = NULL) { + if (is.null(known_issues)) { + known_issues <- read_known_issues() + } + + filt <- is_known(backend, slot, dtype, process, known_issues) + + if (any(filt)) { + # take first + row <- known_issues[which(filt)[[1]], ] + + paste0( + "Known issue for backend '", row$backend, "', slot '", row$slot, + "', dtype '", row$dtype, "', process '", row$process, "': ", + row$description + ) + } else { + NULL + } +} diff --git a/inst/known_issues.yaml b/inst/known_issues.yaml new file mode 100644 index 00000000..0d7c6e8a --- /dev/null +++ b/inst/known_issues.yaml @@ -0,0 +1,74 @@ +known_issues: + - backend: HDF5AnnData + slot: + - X + - layers + - obsp + - varp + - obsm + - varm + dtype: + - integer_csparse + - integer_rsparse + - integer_matrix + process: [read] + error_message: | + Failure (test-roundtrip-obspvarp.R:111:5): Writing an AnnData with obsp and varp 'integer_csparse' works + a$dtype (`actual`) not equal to b$dtype (`expected`). + + `class(actual)`: "numpy.dtypes.Float64DType" "numpy.dtype" "python.builtin.object" + `class(expected)`: "numpy.dtypes.Int64DType" "numpy.dtype" "python.builtin.object" + description: Integers are being converted to floats. + proposed_solution: Debug and fix + to_investigate: True + to_fix: True + - backend: HDF5AnnData + slot: + - obsm + - varm + dtype: + - boolean_array + - categorical + - categorical_missing_values + - categorical_ordered + - categorical_ordered_missing_values + - dense_array + - integer_array + - nullable_boolean_array + - nullable_integer_array + - string_array + process: [reticulate] + error_message: | + adata_r$varm[[name]] (`actual`) not equal to py_to_r(py_get_item(adata_py$varm, name)) (`expected`). + + `dim(actual)` is absent + `dim(expected)` is an integer vector (20) + description: Python nd.arrays have a dimension while R vectors do not. + proposed_solution: Debug and fix + to_investigate: True + to_fix: True + - backend: HDF5AnnData + slot: + - obsm + - varm + dtype: + - boolean_array + - categorical + - categorical_missing_values + - categorical_ordered + - categorical_ordered_missing_values + - dense_array + - integer_array + - nullable_boolean_array + - nullable_integer_array + - string_array + process: [write] + error_message: | + Error in `if (found_dim != expected_dim) { + stop("dim(", label, ")[", i, "] should have shape: ", expected_dim, + ", found: ", found_dim, ".") + }`: argument is of length zero + description: R vectors don't have a dimension. + proposed_solution: The input checking function for obsm and varm should allow the object to be a vector of the correct length instead of only a matrix or a data frame. + to_investigate: True + to_fix: True diff --git a/man/anndataR-package.Rd b/man/anndataR-package.Rd index 47093117..671099fc 100644 --- a/man/anndataR-package.Rd +++ b/man/anndataR-package.Rd @@ -92,7 +92,6 @@ Authors: \item Luke Zappia \email{luke@lazappi.id.au} (\href{https://orcid.org/0000-0001-7744-8565}{ORCID}) (lazappi) \item Martin Morgan \email{mtmorgan.bioc@gmail.com} (\href{https://orcid.org/0000-0002-5874-8148}{ORCID}) (mtmorgan) \item Louise Deconinck \email{louise.deconinck@gmail.com} (\href{https://orcid.org/0000-0001-8100-6823}{ORCID}) (LouiseDck) - \item Data Intuitive \email{info@data-intuitive.com} } Other contributors: @@ -101,6 +100,8 @@ Other contributors: \item Isaac Virshup (\href{https://orcid.org/0000-0002-1710-8945}{ORCID}) (ivirshup) [contributor] \item Brian Schilder \email{brian_schilder@alumni.brown.edu} (\href{https://orcid.org/0000-0001-5949-2191}{ORCID}) (bschilder) [contributor] \item Chananchida Sang-aram (\href{https://orcid.org/0000-0002-0922-0822}{ORCID}) (csangara) [contributor] + \item Data Intuitive \email{info@data-intuitive.com} [funder, copyright holder] + \item Chan Zuckerberg Initiative [funder] } } diff --git a/tests/testthat/helper-expect_equal_py.R b/tests/testthat/helper-expect_equal_py.R new file mode 100644 index 00000000..aabe829e --- /dev/null +++ b/tests/testthat/helper-expect_equal_py.R @@ -0,0 +1,56 @@ +expect_equal_py <- function(a, b) { + requireNamespace("testthat") + requireNamespace("reticulate") + + bi <- reticulate::import_builtins() + + testthat::expect_equal(bi$type(a), bi$type(b)) # does this always work? + + if (inherits(a, "pandas.core.frame.DataFrame")) { + pd <- reticulate::import("pandas") + testthat::expect_null( + pd$testing$assert_frame_equal( + a, + b, + check_dtype = FALSE, + check_exact = FALSE + ) + ) + } else if (inherits(a, "np.ndarray") || inherits(a, "scipy.sparse.base.spmatrix")) { + scipy <- reticulate::import("scipy") + np <- reticulate::import("numpy") + + testthat::expect_equal(a$dtype, b$dtype) + + testthat::expect_equal( + py_to_r_ifneedbe(a$shape), + py_to_r_ifneedbe(b$shape) + ) + + a_dense <- + if (scipy$sparse$issparse(a)) { + a$toarray() + } else { + a + } + b_dense <- + if (scipy$sparse$issparse(b)) { + b$toarray() + } else { + b + } + + testthat::expect_null( + np$testing$assert_allclose(a_dense, b_dense) + ) + } +} + +py_to_r_ifneedbe <- function(x) { + if (inherits(x, "python.builtin.object")) { + requireNamespace("reticulate") + reticulate::py_to_r(x) + } else { + x + } +} diff --git a/tests/testthat/test-InMemoryAnnData.R b/tests/testthat/test-InMemoryAnnData.R index 186d5edd..e3d4e0fd 100644 --- a/tests/testthat/test-InMemoryAnnData.R +++ b/tests/testthat/test-InMemoryAnnData.R @@ -40,7 +40,7 @@ test_that("with empty var", { expect_identical(ad$shape(), c(10L, 0L)) }) -test_that("with only X, no obs or var", function() { +test_that("Creating AnnData works with only X, no obs or var", { X <- dummy$X dimnames(X) <- list( rownames(dummy$obs), diff --git a/tests/testthat/test-roundtrip-X.R b/tests/testthat/test-roundtrip-X.R index ca491b87..8aeb420b 100644 --- a/tests/testthat/test-roundtrip-X.R +++ b/tests/testthat/test-roundtrip-X.R @@ -1,109 +1,104 @@ skip_if_no_anndata() -skip_if_not_installed("hdf5r") +skip_if_not_installed("reticulate") -data <- generate_dataset(10L, 20L) +library(reticulate) +testthat::skip_if_not( + reticulate::py_module_available("dummy_anndata"), + message = "Python dummy_anndata module not available for testing" +) -test_names <- names(data$layers) +ad <- reticulate::import("anndata", convert = FALSE) +da <- reticulate::import("dummy_anndata", convert = FALSE) +bi <- reticulate::import_builtins() -# TODO: Add denseMatrix support to anndata and anndataR -test_names <- test_names[!grepl("_dense", test_names)] +known_issues <- read_known_issues() + +test_names <- names(da$matrix_generators) for (name in test_names) { - test_that(paste0("roundtrip with X '", name, "'"), { - # create anndata - ad <- AnnData( - X = data$layers[[name]], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + # first generate a python h5ad + adata_py <- da$generate_dataset( + x_type = name, + obs_types = list(), + var_types = list(), + layer_types = list(), + obsm_types = list(), + varm_types = list(), + obsp_types = list(), + varp_types = list(), + uns_types = list(), + nested_uns_types = list() + ) + + # create a couple of paths + file_py <- withr::local_file(tempfile(paste0("anndata_py_", name), fileext = ".h5ad")) + file_r <- withr::local_file(tempfile(paste0("anndata_r_", name), fileext = ".h5ad")) + + # write to file + adata_py$write_h5ad(file_py) + + test_that(paste0("Reading an AnnData with X '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("X"), + dtype = name, + process = "read", + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - write_h5ad(ad, filename) - - # read from file - ad_new <- read_h5ad(filename, to = "HDF5AnnData") - - # expect slots are unchanged + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") expect_equal( - ad_new$X, - data$layers[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$shape(), + unlist(reticulate::py_to_r(adata_py$shape)) ) + + # check that the print output is the same + str_r <- capture.output(print(adata_r)) + str_py <- capture.output(print(adata_py)) + expect_equal(str_r, str_py) }) -} -for (name in test_names) { - test_that(paste0("reticulate->hdf5 with X '", name, "'"), { - # add rownames - X <- data$layers[[name]] - obs <- data.frame(row.names = rownames(data$obs)) - var <- data.frame(row.names = rownames(data$var)) - - # TODO: remove this? - if (is.matrix(X) && any(is.na(X))) { - na_indices <- is.na(X) - X[na_indices] <- NaN - } - - # create anndata - ad <- anndata::AnnData( - X = X, - obs = obs, - var = var + # maybe this test simply shouldn't be run if there is a known issue with reticulate + test_that(paste0("Comparing an anndata with X '", name, "' with reticulate works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("X"), + dtype = name, + process = c("read", "reticulate"), + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - ad$write_h5ad(filename) - - # read from file - ad_new <- HDF5AnnData$new(filename) + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") - # expect slots are unchanged expect_equal( - ad_new$X, - data$layers[[name]], + adata_r$X, + py_to_r(adata_py$X), tolerance = 1e-6 ) }) -} - -r2py_names <- test_names -# TODO: re-enable -- rsparse gets converted to csparse by anndata -r2py_names <- r2py_names[!grepl("rsparse", r2py_names)] - -for (name in r2py_names) { - test_that(paste0("hdf5->reticulate with X '", name, "'"), { - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - - # make anndata - ad <- AnnData( - X = data$layers[[name]], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + test_that(paste0("Writing an AnnData with X '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("X"), + dtype = name, + process = c("read", "write"), + known_issues = known_issues ) - write_h5ad(ad, filename) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "InMemoryAnnData") + write_h5ad(adata_r, file_r) # read from file - ad_new <- anndata::read_h5ad(filename) - - # expect slots are unchanged - layer_ <- ad_new$X - # anndata returns these layers as CsparseMatrix - if (grepl("rsparse", name)) { - layer_ <- as(layer_, "RsparseMatrix") - } - # strip rownames - dimnames(layer_) <- list(NULL, NULL) - expect_equal( - layer_, - data$layers[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_py2 <- ad$read_h5ad(file_r) + + # expect that the objects are the same + expect_equal_py( + adata_py2$X, + adata_py$X ) }) } diff --git a/tests/testthat/test-roundtrip-layers.R b/tests/testthat/test-roundtrip-layers.R index 0e72961f..9ba0aa25 100644 --- a/tests/testthat/test-roundtrip-layers.R +++ b/tests/testthat/test-roundtrip-layers.R @@ -1,103 +1,108 @@ skip_if_no_anndata() -skip_if_not_installed("hdf5r") +skip_if_not_installed("reticulate") -data <- generate_dataset(10L, 20L) +library(reticulate) +testthat::skip_if_not( + reticulate::py_module_available("dummy_anndata"), + message = "Python dummy_anndata module not available for testing" +) -test_names <- names(data$layers) +ad <- reticulate::import("anndata", convert = FALSE) +da <- reticulate::import("dummy_anndata", convert = FALSE) +bi <- reticulate::import_builtins() -# TODO: Add denseMatrix support to anndata and anndataR -test_names <- test_names[!grepl("_dense", test_names)] +known_issues <- read_known_issues() + +test_names <- names(da$matrix_generators) for (name in test_names) { - test_that(paste0("roundtrip with layer '", name, "'"), { - # create anndata - ad <- AnnData( - layers = data$layers[name], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + # first generate a python h5ad + adata_py <- da$generate_dataset( + x_type = NULL, + obs_types = list(), + var_types = list(), + layer_types = list(name), + obsm_types = list(), + varm_types = list(), + obsp_types = list(), + varp_types = list(), + uns_types = list(), + nested_uns_types = list() + ) + + # create a couple of paths + file_py <- withr::local_file(tempfile(paste0("anndata_py_", name), fileext = ".h5ad")) + file_r <- withr::local_file(tempfile(paste0("anndata_r_", name), fileext = ".h5ad")) + + # write to file + adata_py$write_h5ad(file_py) + + test_that(paste0("Reading an AnnData with layer '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("layers"), + dtype = name, + process = "read", + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - write_h5ad(ad, filename) - - # read from file - ad_new <- read_h5ad(filename, to = "HDF5AnnData") - - # expect slots are unchanged + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") expect_equal( - ad_new$layers[[name]], - data$layers[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$shape(), + unlist(reticulate::py_to_r(adata_py$shape)) ) + expect_equal( + adata_r$layers_keys(), + bi$list(adata_py$layers$keys()) + ) + + # check that the print output is the same + str_r <- capture.output(print(adata_r)) + str_py <- capture.output(print(adata_py)) + expect_equal(str_r, str_py) }) -} -for (name in test_names) { - test_that(paste0("reticulate->hdf5 with layer '", name, "'"), { - # add rownames - layers <- data$layers[name] - obs <- data.frame(row.names = rownames(data$obs)) - var <- data.frame(row.names = rownames(data$var)) - - # create anndata - ad <- anndata::AnnData( - layers = layers, - shape = dim(data$X), - obs = obs, - var = var + # maybe this test simply shouldn't be run if there is a known issue with reticulate + test_that(paste0("Comparing an anndata with layer '", name, "' with reticulate works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("layers"), + dtype = name, + process = c("read", "reticulate"), + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - ad$write_h5ad(filename) + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") - # read from file - ad_new <- HDF5AnnData$new(filename) - - # expect slots are unchanged expect_equal( - ad_new$layers[[name]], - data$layers[[name]], + adata_r$layers[[name]], + py_to_r(py_get_item(adata_py$layers, name)), tolerance = 1e-6 ) }) -} - -r2py_names <- test_names -# TODO: rsparse gets converted to csparse by anndata -r2py_names <- r2py_names[!grepl("rsparse", r2py_names)] - -for (name in r2py_names) { - test_that(paste0("hdf5->reticulate with layer '", name, "'"), { - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - # make anndata - ad <- AnnData( - layers = data$layers[name], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + test_that(paste0("Writing an AnnData with layer '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("layers"), + dtype = name, + process = c("read", "write"), + known_issues = known_issues ) - write_h5ad(ad, filename) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "InMemoryAnnData") + write_h5ad(adata_r, file_r) # read from file - ad_new <- anndata::read_h5ad(filename) - - # expect slots are unchanged - layer_ <- ad_new$layers[[name]] - # anndata returns these layers as CsparseMatrix - if (grepl("rsparse", name)) { - layer_ <- as(layer_, "RsparseMatrix") - } - # strip rownames - dimnames(layer_) <- list(NULL, NULL) - expect_equal( - layer_, - data$layers[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_py2 <- ad$read_h5ad(file_r) + + # expect that the objects are the same + expect_equal_py( + py_get_item(adata_py2$layers, name), + py_get_item(adata_py$layers, name) ) }) } diff --git a/tests/testthat/test-roundtrip-obsmvarm.R b/tests/testthat/test-roundtrip-obsmvarm.R index d5a3277f..372a72b0 100644 --- a/tests/testthat/test-roundtrip-obsmvarm.R +++ b/tests/testthat/test-roundtrip-obsmvarm.R @@ -1,114 +1,132 @@ skip_if_no_anndata() -skip_if_not_installed("hdf5r") +skip_if_not_installed("reticulate") -data <- generate_dataset(10L, 20L) +library(reticulate) +testthat::skip_if_not( + reticulate::py_module_available("dummy_anndata"), + message = "Python dummy_anndata module not available for testing" +) -test_names <- names(data$obsm) +ad <- reticulate::import("anndata", convert = FALSE) +da <- reticulate::import("dummy_anndata", convert = FALSE) +bi <- reticulate::import_builtins() -# TODO: re-enable this -test_names <- test_names[test_names != "character_with_nas"] +known_issues <- read_known_issues() -# TODO: Add denseMatrix support to anndata and anndataR -test_names <- test_names[!grepl("_dense", test_names)] +test_names <- c( + names(da$matrix_generators), + names(da$vector_generators) +) + +# temporary workaround for +# https://github.com/data-intuitive/dummy-anndata/issues/12 +test_names <- setdiff(test_names, c( + "categorical", "categorical_missing_values", + "categorical_ordered", "categorical_ordered_missing_values", + "nullable_boolean_array", "nullable_integer_array" +)) for (name in test_names) { - test_that(paste0("roundtrip with obsm and varm '", name, "'"), { - # create anndata - ad <- AnnData( - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE], - obsm = data$obsm[name], - varm = data$varm[name] + # first generate a python h5ad + adata_py <- da$generate_dataset( + x_type = NULL, + obs_types = list(), + var_types = list(), + layer_types = list(), + obsm_types = list(name), + varm_types = list(name), + obsp_types = list(), + varp_types = list(), + uns_types = list(), + nested_uns_types = list() + ) + + # create a couple of paths + file_py <- withr::local_file(tempfile(paste0("anndata_py_", name), fileext = ".h5ad")) + file_r <- withr::local_file(tempfile(paste0("anndata_r_", name), fileext = ".h5ad")) + + # write to file + adata_py$write_h5ad(file_py) + + test_that(paste0("Reading an AnnData with obsm and varm '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsm", "varm"), + dtype = name, + process = "read", + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - write_h5ad(ad, filename) - - # read from file - ad_new <- read_h5ad(filename, to = "HDF5AnnData") - - # expect slots are unchanged + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") expect_equal( - ad_new$obsm[[name]], - data$obsm[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$shape(), + unlist(reticulate::py_to_r(adata_py$shape)) ) expect_equal( - ad_new$varm[[name]], - data$varm[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$obsm_keys(), + bi$list(adata_py$obsm$keys()) + ) + expect_equal( + adata_r$varm_keys(), + bi$list(adata_py$varm$keys()) ) - }) -} -# TODO: re-enable these tests -# it seemed like there is a difference in the dimnames of the -# obsm and varm between anndata and anndataR -test_names <- c() + # check that the print output is the same + str_r <- capture.output(print(adata_r)) + str_py <- capture.output(print(adata_py)) + expect_equal(str_r, str_py) + }) -for (name in test_names) { - test_that(paste0("reticulate->hdf5 with obsm and varm '", name, "'"), { - # create anndata - ad <- anndata::AnnData( - obs = data.frame(row.names = data$obs_names), - var = data.frame(row.names = data$var_names), - obsm = data$obsm[name], - varm = data$varm[name] + # maybe this test simply shouldn't be run if there is a known issue with reticulate + test_that(paste0("Comparing an anndata with obsm and varm '", name, "' with reticulate works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsm", "varm"), + dtype = name, + process = c("read", "reticulate"), + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - ad$write_h5ad(filename) - - # read from file - ad_new <- HDF5AnnData$new(filename) + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") - # expect slots are unchanged expect_equal( - ad_new$obsm[[name]], - data$obsm[[name]], + adata_r$obsm[[name]], + py_to_r(py_get_item(adata_py$obsm, name)), tolerance = 1e-6 ) expect_equal( - ad_new$varm[[name]], - data$varm[[name]], + adata_r$varm[[name]], + py_to_r(py_get_item(adata_py$varm, name)), tolerance = 1e-6 ) }) -} -for (name in test_names) { - test_that(paste0("hdf5->reticulate with obsm and varm '", name, "'"), { - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - - # create anndata - ad <- AnnData( - obsm = data$obsm[name], - varm = data$varm[name], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + test_that(paste0("Writing an AnnData with obsm and varm '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsm", "varm"), + dtype = name, + process = c("read", "write"), + known_issues = known_issues ) - write_h5ad(ad, filename) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "InMemoryAnnData") + write_h5ad(adata_r, file_r) # read from file - ad_new <- anndata::read_h5ad(filename) + adata_py2 <- ad$read_h5ad(file_r) - # expect slots are unchanged - expect_equal( - ad_new$obsm[[name]], - data$obsm[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + # expect that the objects are the same + expect_equal_py( + py_get_item(adata_py2$obsm, name), + py_get_item(adata_py$obsm, name) ) - expect_equal( - ad_new$varm[[name]], - data$varm[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + expect_equal_py( + py_get_item(adata_py2$varm, name), + py_get_item(adata_py$varm, name) ) }) } diff --git a/tests/testthat/test-roundtrip-obspvarp.R b/tests/testthat/test-roundtrip-obspvarp.R index 00cb7c17..25b3e9ba 100644 --- a/tests/testthat/test-roundtrip-obspvarp.R +++ b/tests/testthat/test-roundtrip-obspvarp.R @@ -1,106 +1,121 @@ skip_if_no_anndata() -skip_if_not_installed("hdf5r") +skip_if_not_installed("reticulate") -data <- generate_dataset(10L, 20L) +library(reticulate) +testthat::skip_if_not( + reticulate::py_module_available("dummy_anndata"), + message = "Python dummy_anndata module not available for testing" +) -test_names <- names(data$obsp) +ad <- reticulate::import("anndata", convert = FALSE) +da <- reticulate::import("dummy_anndata", convert = FALSE) +bi <- reticulate::import_builtins() -# TODO: re-enable test -test_names <- c() +known_issues <- read_known_issues() + +test_names <- names(da$matrix_generators) for (name in test_names) { - test_that(paste0("roundtrip with obsp and varp '", name, "'"), { - # create anndata - ad <- AnnData( - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE], - obsp = data$obsp[name], - varp = data$varp[name] - ) + # first generate a python h5ad + adata_py <- da$generate_dataset( + x_type = NULL, + obs_types = list(), + var_types = list(), + layer_types = list(), + obsm_types = list(), + varm_types = list(), + obsp_types = list(name), + varp_types = list(name), + uns_types = list(), + nested_uns_types = list() + ) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - write_h5ad(ad, filename) + # create a couple of paths + file_py <- withr::local_file(tempfile(paste0("anndata_py_", name), fileext = ".h5ad")) + file_r <- withr::local_file(tempfile(paste0("anndata_r_", name), fileext = ".h5ad")) - # read from file - ad_new <- read_h5ad(filename, to = "HDF5AnnData") + # write to file + adata_py$write_h5ad(file_py) - # expect slots are unchanged + test_that(paste0("Reading an AnnData with obsp and varp '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsp", "varp"), + dtype = name, + process = "read", + known_issues = known_issues + ) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") expect_equal( - ad_new$obsp[[name]], - data$obsp[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$shape(), + unlist(reticulate::py_to_r(adata_py$shape)) ) expect_equal( - ad_new$varp[[name]], - data$varp[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + adata_r$obsp_keys(), + bi$list(adata_py$obsp$keys()) + ) + expect_equal( + adata_r$varp_keys(), + bi$list(adata_py$varp$keys()) ) + + # check that the print output is the same + str_r <- capture.output(print(adata_r)) + str_py <- capture.output(print(adata_py)) + expect_equal(str_r, str_py) }) -} -for (name in test_names) { - test_that(paste0("reticulate->hdf5 with obsp and varp '", name, "'"), { - # create anndata - ad <- anndata::AnnData( - obs = data.frame(row.names = data$obs_names), - var = data.frame(row.names = data$var_names), - obsp = data$obsp[name], - varp = data$varp[name] + # maybe this test simply shouldn't be run if there is a known issue with reticulate + test_that(paste0("Comparing an anndata with obsp and varp '", name, "' with reticulate works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsp", "varp"), + dtype = name, + process = c("read", "reticulate"), + known_issues = known_issues ) + skip_if(!is.null(msg), message = msg) - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - ad$write_h5ad(filename) - - # read from file - ad_new <- HDF5AnnData$new(filename) + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") - # expect slots are unchanged expect_equal( - ad_new$obsp[[name]], - data$obsp[[name]], + adata_r$obsp[[name]], + py_to_r(py_get_item(adata_py$obsp, name)), tolerance = 1e-6 ) expect_equal( - ad_new$varp[[name]], - data$varp[[name]], + adata_r$varp[[name]], + py_to_r(py_get_item(adata_py$varp, name)), tolerance = 1e-6 ) }) -} -for (name in test_names) { - test_that(paste0("hdf5->reticulate with obsp and varp '", name, "'"), { - # write to file - filename <- withr::local_file(tempfile(fileext = ".h5ad")) - - # create anndata - ad <- AnnData( - obsp = data$obsp[name], - varp = data$varp[name], - obs = data$obs[, c(), drop = FALSE], - var = data$var[, c(), drop = FALSE] + test_that(paste0("Writing an AnnData with obsp and varp '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsp", "varp"), + dtype = name, + process = c("read", "write"), + known_issues = known_issues ) - write_h5ad(ad, filename) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "InMemoryAnnData") + write_h5ad(adata_r, file_r) # read from file - ad_new <- anndata::read_h5ad(filename) + adata_py2 <- ad$read_h5ad(file_r) - # expect slots are unchanged - expect_equal( - ad_new$obsp[[name]], - data$obsp[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + # expect that the objects are the same + expect_equal_py( + py_get_item(adata_py2$obsp, name), + py_get_item(adata_py$obsp, name) ) - expect_equal( - ad_new$varp[[name]], - data$varp[[name]], - ignore_attr = TRUE, - tolerance = 1e-6 + expect_equal_py( + py_get_item(adata_py2$varp, name), + py_get_item(adata_py$varp, name) ) }) } diff --git a/tests/testthat/test-roundtrip-obsvar.R b/tests/testthat/test-roundtrip-obsvar.R index 0479845f..b38e3ec4 100644 --- a/tests/testthat/test-roundtrip-obsvar.R +++ b/tests/testthat/test-roundtrip-obsvar.R @@ -9,9 +9,10 @@ testthat::skip_if_not( ad <- reticulate::import("anndata", convert = FALSE) da <- reticulate::import("dummy_anndata", convert = FALSE) -pd <- reticulate::import("pandas", convert = FALSE) bi <- reticulate::import_builtins() +known_issues <- read_known_issues() + test_names <- names(da$vector_generators) for (name in test_names) { @@ -25,11 +26,9 @@ for (name in test_names) { varm_types = list(), obsp_types = list(), varp_types = list(), - uns_types = list() + uns_types = list(), + nested_uns_types = list() ) - # remove uns - workaround for https://github.com/data-intuitive/dummy-anndata/issues/2 - adata_py$uns <- bi$dict() - # TODO: remove X # create a couple of paths file_py <- withr::local_file(tempfile(paste0("anndata_py_", name), fileext = ".h5ad")) @@ -39,6 +38,15 @@ for (name in test_names) { adata_py$write_h5ad(file_py) test_that(paste0("reading an AnnData with obs and var '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obs", "var"), + dtype = name, + process = "read", + known_issues = known_issues + ) + skip_if(!is.null(msg), message = msg) + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") expect_equal( adata_r$shape(), @@ -46,35 +54,54 @@ for (name in test_names) { ) expect_equal( adata_r$obs_keys(), - py_to_r(adata_py$obs_keys()) + bi$list(adata_py$obs_keys()) ) expect_equal( adata_r$var_keys(), - py_to_r(adata_py$var_keys()) + bi$list(adata_py$var_keys()) ) # check that the print output is the same str_r <- capture.output(print(adata_r)) str_py <- capture.output(print(adata_py)) expect_equal(str_r, str_py) + }) + + # maybe this test simply shouldn't be run if there is a known issue with reticulate + test_that(paste0("Comparing an anndata with obs and var '", name, "' with reticulate works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obs", "var"), + dtype = name, + process = c("read", "reticulate"), + known_issues = known_issues + ) + skip_if(!is.null(msg), message = msg) + + adata_r <- read_h5ad(file_py, to = "HDF5AnnData") - # if we would test the objects at this stage, - # we're also testing reticulate's conversion - # nolint start - # expect_equal( - # adata_r$obs[[name]], - # py_to_r(adata_py$obs[[name]]), - # tolerance = 1e-6 - # ) - # expect_equal( - # adata_r$var[[name]], - # py_to_r(adata_py$var[[name]]), - # tolerance = 1e-6 - # ) - # nolint end + expect_equal( + adata_r$obs[[name]], + py_to_r(adata_py$obs)[[name]], + tolerance = 1e-6 + ) + expect_equal( + adata_r$var[[name]], + py_to_r(adata_py$var)[[name]], + tolerance = 1e-6 + ) }) test_that(paste0("Writing an AnnData with obs and var '", name, "' works"), { + msg <- message_if_known( + backend = "HDF5AnnData", + slot = c("obsp", "varp"), + dtype = name, + process = c("read", "write"), + known_issues = known_issues + ) + skip_if(!is.null(msg), message = msg) + adata_r <- read_h5ad(file_py, to = "InMemoryAnnData") write_h5ad(adata_r, file_r) @@ -82,19 +109,7 @@ for (name in test_names) { adata_py2 <- ad$read_h5ad(file_r) # expect that the objects are the same - zz <- pd$testing$assert_frame_equal( - adata_py2$obs, - adata_py$obs, - check_dtype = FALSE, - check_exact = FALSE - ) - expect_null(reticulate::py_to_r(zz)) - zz <- pd$testing$assert_frame_equal( - adata_py2$var, - adata_py$var, - check_dtype = FALSE, - check_exact = FALSE - ) - expect_null(reticulate::py_to_r(zz)) + expect_equal_py(adata_py2$obs, adata_py$obs) + expect_equal_py(adata_py2$var, adata_py$var) }) }