-
-
Notifications
You must be signed in to change notification settings - Fork 14
Implement coercion using as()
#358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Changes from all commits
9b7c25b
10cc0fb
9f898eb
fb5aafe
a506f48
50f3024
6df423b
4f2723d
b789912
1640938
1c7b0c9
266f5f5
5265dbd
b5509bd
e7d856a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)) | ||
} | ||
} | ||
rcannood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need this (just curious)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It causes these arguments to be evaluated eagerly instead of lazily There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
rcannood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} |
There was a problem hiding this comment.
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