Skip to content
Draft
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
18 changes: 14 additions & 4 deletions chatspatial/tools/cnv_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,20 @@ def _infer_cnv_numbat(
# Lazy import and check for Numbat availability
# Note: Numbat requires rpy2 + R + Numbat R package - cannot use centralized manager
try:
import anndata2ri
import rpy2.robjects as ro
from rpy2.rinterface_lib import openrlib
from rpy2.robjects import conversion, default_converter, numpy2ri, pandas2ri
import importlib
import sys

anndata2ri = importlib.import_module("anndata2ri")
ro = sys.modules.get("rpy2.robjects") or importlib.import_module("rpy2.robjects")
rinterface_lib = sys.modules.get("rpy2.rinterface_lib") or importlib.import_module(
"rpy2.rinterface_lib"
)
openrlib = rinterface_lib.openrlib

conversion = ro.conversion
default_converter = ro.default_converter
numpy2ri = ro.numpy2ri
pandas2ri = ro.pandas2ri

# Test if Numbat R package is available
ro.r("suppressPackageStartupMessages(library(numbat))")
Expand Down
11 changes: 6 additions & 5 deletions chatspatial/tools/differential.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,11 +472,6 @@ async def _run_pydeseq2(
"sample_key='sample')"
)

# Import pydeseq2 (require() raises ImportError if not available)
require("pydeseq2", ctx, feature="DESeq2 differential expression")
from pydeseq2.dds import DeseqDataSet
from pydeseq2.ds import DeseqStats

# Get data
adata = await ctx.get_adata(data_id)

Expand Down Expand Up @@ -617,6 +612,12 @@ async def _run_pydeseq2(
f"Current counts: {condition_counts.to_dict()}"
)

# Import pydeseq2 only after precondition checks so users get actionable
# validation errors even when optional dependency is unavailable.
require("pydeseq2", ctx, feature="DESeq2 differential expression")
from pydeseq2.dds import DeseqDataSet
from pydeseq2.ds import DeseqStats

# Run PyDESeq2
try:
# Design: ~condition (standard for pseudobulk DE).
Expand Down
17 changes: 12 additions & 5 deletions chatspatial/tools/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,6 @@ async def preprocess_data(
vst_flavor = "v2" if params.sct_method == "fix-slope" else "v1"

try:
# Import rpy2 modules
import rpy2.robjects as ro
from rpy2.robjects import numpy2ri
from rpy2.robjects.conversion import localconverter

# Note: counts layer is already created earlier in this preprocessing workflow.
# It will be properly subsetted if SCT filters genes
# Convert to sparse CSC matrix (genes × cells) for R's dgCMatrix
Expand All @@ -380,6 +375,18 @@ async def preprocess_data(
else:
counts_sparse = scipy.sparse.csc_matrix(adata.X.T)

# Import rpy2 modules
try:
import rpy2.robjects as ro
from rpy2.robjects import numpy2ri
from rpy2.robjects.conversion import localconverter
except ImportError as e:
raise DependencyError(
"SCTransform requires rpy2. "
"Install with: pip install 'rpy2>=3.5.0' "
"or use normalization='pearson_residuals'/'log'."
) from e

# Transfer sparse matrix components to R
with localconverter(ro.default_converter + numpy2ri.converter):
ro.globalenv["sp_data"] = counts_sparse.data.astype(np.float64)
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_cnv_analysis_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def test_infer_cnv_numbat_requires_allele_dataframe(
):
adata = minimal_spatial_adata.copy()
adata.obs["cell_type"] = ["A"] * 30 + ["B"] * 30
monkeypatch.setitem(__import__("sys").modules, "rpy2", ModuleType("rpy2"))

fake_ro = ModuleType("rpy2.robjects")
fake_ro.r = lambda *_a, **_k: None
Expand Down