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
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ S3method(py_to_r,collections.abc.Mapping)
S3method(r_to_py,AbstractAnnData)
export(AnnData)
export(AnnDataView)
export(HDF5AnnData)
export(InMemoryAnnData)
export(ReticulateAnnData)
export(as_AnnData)
export(generate_dataset)
export(get_generator_types)
export(read_h5ad)
export(register_anndata_coercions)
export(write_h5ad)
importFrom(Matrix,as.matrix)
importFrom(Matrix,sparseMatrix)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Implemented an `AnnDataView` class, which provides a lazy view of an `AnnData` object without copying data (PR #1096)
* Implemented S3 methods for `AbstractAnnData` objects: `dim`, `nrow`, `ncol`, `dimnames`, `rownames`, `colnames`, and `[` (PR #1096)
* Add `ReticulateAnnData` class for seamless Python integration via **{reticulate}** (PR #322)
* Added S4 `as()` coercions linking AnnData implementations with `SingleCellExperiment` and `Seurat` objects (PR #358)

## Major changes

Expand All @@ -29,6 +30,7 @@
- Add checks for type arguments to `generate_dataset()` (PR #354)
- Generalise the layers created by `generate_dataset()` when `format = "Seurat"`
(PR #354)
- Fix inconsistency in `output_class` argument values across conversion functions (PR #358)

## Bug fixes

Expand Down
1 change: 0 additions & 1 deletion R/AnnDataView.R
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@ AnnDataView <- R6::R6Class(
#' @param context_name Name for error messages ("observations" or "variables")
#'
#' @return Integer vector of indices, or NULL if subset is NULL
#' @keywords internal
#' @noRd
convert_to_indices <- function(
subset,
Expand Down
1 change: 1 addition & 0 deletions R/HDF5AnnData.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#' See [AnnData-usage] for details on creating and using `AnnData` objects.
#'
#' @return An `HDF5AnnData` object
#' @export
#'
#' @seealso [AnnData-usage] for details on creating and using `AnnData` objects
#'
Expand Down
1 change: 1 addition & 0 deletions R/InMemoryAnnData.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#' @seealso [AnnData-usage] for details on creating and using `AnnData` objects
#'
#' @family AnnData classes
#' @export
#'
#' @examples
#' ## complete example
Expand Down
1 change: 1 addition & 0 deletions R/ReticulateAnnData.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#' See [AnnData-usage] for details on creating and using `AnnData` objects.
#'
#' @return A `ReticulateAnnData` object
#' @export
#'
#' @seealso [AnnData-usage] for details on creating and using `AnnData` objects
#'
Expand Down
5 changes: 5 additions & 0 deletions R/anndataR-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
## usethis namespace: end
NULL

.onLoad <- function(libname, pkgname) {
# Register S4 coercion methods
.register_as_coercions()
}

.onAttach <- function(libname, pkgname) {
# Check if the R anndata package is loaded and warn about conflicts
if ("anndata" %in% loadedNamespaces()) {
Expand Down
270 changes: 270 additions & 0 deletions R/as-coercions.R
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR but I think we should be more consistent about if/when we use the . prefix for internal functions

Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#' Coercion helpers for `as()`
#'
#' These helper registrations wire up S4-style `as()` conversions so that
#' AnnData implementations (including [`InMemoryAnnData`], [`HDF5AnnData`], and
#' [`ReticulateAnnData`]) as well as [`SingleCellExperiment`] and
#' [`SeuratObject::Seurat`] objects can be coerced
#' between one another without the caller needing to know the underlying helper
#' functions. Because `as()` cannot accept additional arguments, conversions
#' that require them (such as writing HDF5-backed AnnData objects) raise an
#' informative error pointing to the richer interface.
#'
#' @noRd
NULL

# Class compatibility registrations -----------------------------------------

.register_oldclass <- function(class, super = character()) {
if (!methods::isClass(class)) {
methods::setOldClass(c(class, super))
}
}

.as_abort_extra_args <- function(from, to, helper) {
cli::cli_abort(
c(
"Can't coerce {.cls {from}} to {.cls {to}} with {.fun as} as extra arguments are required",
"i" = helper
),
call = rlang::caller_env()
)
}

.warn_as_limited <- function(recommendation) {
cli::cli_warn(
c(
"Using {.fun as} to coerce objects limits control over data mapping",
"i" = recommendation
),
call = rlang::caller_env()
)
}

# Handler constructors -------------------------------------------------------

.make_convert_handler <- function(converter, warn = NULL, pre = NULL) {
force(warn)
force(pre)
Comment on lines +46 to +47
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this (just curious)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It causes these arguments to be evaluated eagerly instead of lazily

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that, I just wasn't sure why that was needed 😸


function(from) {
if (!is.null(pre)) {
pre(from)
}
if (!is.null(warn)) {
.warn_as_limited(warn)
}
converter(from)
}
}

.make_abort_handler <- function(from_class, to_class, helper) {
force(from_class)
force(to_class)
force(helper)

function(from) {
.as_abort_extra_args(from_class, to_class, helper)
}
}

.register_set_as_rules <- function(rules) {
for (rule in rules) {
tryCatch(
methods::setAs(rule$from, rule$to, rule$handler),
error = function(e) {
# Silently skip if environment is locked (e.g., during devtools::document())
if (!grepl("locked", e$message)) {
stop(e) # Re-throw if it's not a locking error
}
}
)
}
}

.format_control_recommendation <- function(call_expr) {
sprintf(
"Prefer {.code %s} for fine-grained control over data mapping",
call_expr
)
}

#' Register S4 coercion methods
#'
#' This function registers all S4 coercion methods for converting between
#' `AnnData` objects and other formats. It's called automatically when
#' \pkg{anndataR} is loaded, but can also be called manually if you load
#' \pkg{SingleCellExperiment}, \pkg{Seurat}, or \pkg{SeuratObject} after loading
#' \pkg{anndataR}.
#'
#' @return NULL (invisibly). Called for its side effect of registering S4 methods.
#' @export
#' @examples
#' \dontrun{
#' # If you load suggested packages after anndataR:
#' library(anndataR)
#' library(SingleCellExperiment)
#' register_anndata_coercions() # Now as() will work
#' }
register_anndata_coercions <- function() {
.register_as_coercions()
invisible(NULL)
}

.register_as_coercions <- function() {
# Register old-style classes for S4 compatibility
.register_oldclass("AbstractAnnData", "R6")
.register_oldclass("InMemoryAnnData", c("AbstractAnnData", "R6"))
.register_oldclass("HDF5AnnData", c("AbstractAnnData", "R6"))
.register_oldclass("ReticulateAnnData", c("AbstractAnnData", "R6"))
.register_oldclass("AnnDataView", c("AbstractAnnData", "R6"))

# AnnData <-> AnnData coercion rules --------------------------------------
warn_ann_inmemory <- .format_control_recommendation(
"adata$as_InMemoryAnnData(...)"
)
warn_ann_reticulate <- .format_control_recommendation(
"adata$as_ReticulateAnnData(...)"
)

anndata_rules <- list(
list(
from = "AbstractAnnData",
to = "InMemoryAnnData",
handler = .make_convert_handler(
converter = as_InMemoryAnnData,
warn = warn_ann_inmemory
)
),
list(
from = "AbstractAnnData",
to = "ReticulateAnnData",
handler = .make_convert_handler(
converter = as_ReticulateAnnData,
warn = warn_ann_reticulate
)
),
list(
from = "AbstractAnnData",
to = "HDF5AnnData",
handler = .make_abort_handler(
from_class = "AbstractAnnData",
to_class = "HDF5AnnData",
helper = "Use {.code adata$as_HDF5AnnData(file = <path>)} to provide the output file"
)
)
)

.register_set_as_rules(anndata_rules)

# SingleCellExperiment coercion rules ---------------------------------------

# Only register coercion methods if SingleCellExperiment is available
# This prevents NOTEs about undefined classes during package load
if (rlang::is_installed("SingleCellExperiment")) {
warn_customise <- .format_control_recommendation("as_AnnData(...)")
warn_sce <- .format_control_recommendation(
"adata$as_SingleCellExperiment(...)"
)

single_cell_rules <- list(
list(
from = "SingleCellExperiment",
to = "InMemoryAnnData",
handler = .make_convert_handler(
converter = function(from) {
as_AnnData(from, output_class = "InMemoryAnnData")
},
warn = warn_customise
)
),
list(
from = "SingleCellExperiment",
to = "ReticulateAnnData",
handler = .make_convert_handler(
converter = function(from) {
as_AnnData(from, output_class = "ReticulateAnnData")
},
warn = warn_customise
)
),
list(
from = "SingleCellExperiment",
to = "HDF5AnnData",
handler = .make_abort_handler(
from_class = "SingleCellExperiment",
to_class = "HDF5AnnData",
helper = paste(
"Use {.code as_AnnData(from, output_class = \"HDF5AnnData\",",
"filename = <path>)} to provide the output file"
)
)
),
list(
from = "AbstractAnnData",
to = "SingleCellExperiment",
handler = .make_convert_handler(
converter = as_SingleCellExperiment,
warn = warn_sce
)
)
)

.register_set_as_rules(single_cell_rules)
}

# SingleCellExperiment coercion rules ---------------------------------------

# Only register coercion methods if SeuratObject is available
# This prevents NOTEs about undefined classes during package load
if (rlang::is_installed("SeuratObject")) {
warn_customise <- .format_control_recommendation("as_AnnData(...)")
warn_seurat <- .format_control_recommendation("adata$as_Seurat(...)")

seurat_rules <- list(
list(
from = "Seurat",
to = "InMemoryAnnData",
handler = .make_convert_handler(
converter = function(from) {
as_AnnData(from, output_class = "InMemoryAnnData")
},
warn = warn_customise
)
),
list(
from = "Seurat",
to = "ReticulateAnnData",
handler = .make_convert_handler(
converter = function(from) {
as_AnnData(from, output_class = "ReticulateAnnData")
},
warn = warn_customise
)
),
list(
from = "Seurat",
to = "HDF5AnnData",
handler = .make_abort_handler(
from_class = "Seurat",
to_class = "HDF5AnnData",
helper = paste(
"Use {.code as_AnnData(from, output_class = \"HDF5AnnData\",",
"filename = <path>)} to provide the output file"
)
)
),
list(
from = "AbstractAnnData",
to = "Seurat",
handler = .make_convert_handler(
converter = as_Seurat,
warn = warn_seurat
)
)
)

.register_set_as_rules(seurat_rules)
}

invisible(NULL)
}
6 changes: 3 additions & 3 deletions R/as_AnnData.R
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ as_AnnData <- function(
varp_mapping = TRUE,
uns_mapping = TRUE,
assay_name = NULL,
output_class = c("InMemory", "HDF5AnnData", "ReticulateAnnData"),
output_class = c("InMemoryAnnData", "HDF5AnnData", "ReticulateAnnData"),
...
) {
UseMethod("as_AnnData", x)
Expand All @@ -182,7 +182,7 @@ as_AnnData.SingleCellExperiment <- function(
varp_mapping = TRUE,
uns_mapping = TRUE,
assay_name = TRUE,
output_class = c("InMemory", "HDF5AnnData", "ReticulateAnnData"),
output_class = c("InMemoryAnnData", "HDF5AnnData", "ReticulateAnnData"),
...
) {
from_SingleCellExperiment(
Expand Down Expand Up @@ -215,7 +215,7 @@ as_AnnData.Seurat <- function(
varp_mapping = TRUE,
uns_mapping = TRUE,
assay_name = NULL,
output_class = c("InMemory", "HDF5AnnData", "ReticulateAnnData"),
output_class = c("InMemoryAnnData", "HDF5AnnData", "ReticulateAnnData"),
...
) {
from_Seurat(
Expand Down
2 changes: 1 addition & 1 deletion R/from_Seurat.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ from_Seurat <- function(
obsp_mapping = TRUE,
varp_mapping = TRUE,
uns_mapping = TRUE,
output_class = c("InMemory", "HDF5AnnData", "ReticulateAnnData"),
output_class = c("InMemoryAnnData", "HDF5AnnData", "ReticulateAnnData"),
...
) {
check_requires("Converting Seurat to AnnData", c("SeuratObject", "Seurat"))
Expand Down
2 changes: 1 addition & 1 deletion R/from_SingleCellExperiment.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ from_SingleCellExperiment <- function(
obsp_mapping = TRUE,
varp_mapping = TRUE,
uns_mapping = TRUE,
output_class = c("InMemory", "HDF5AnnData", "ReticulateAnnData"),
output_class = c("InMemoryAnnData", "HDF5AnnData", "ReticulateAnnData"),
...
) {
check_requires(
Expand Down
Loading