diff --git a/lobster/core/data_manager_v2.py b/lobster/core/data_manager_v2.py index 0b2643c..f7f7445 100644 --- a/lobster/core/data_manager_v2.py +++ b/lobster/core/data_manager_v2.py @@ -9,6 +9,7 @@ import json import logging import os +import re import tempfile import threading import time @@ -754,6 +755,32 @@ def register_adapter( self.adapters[name] = adapter logger.debug(f"Registered adapter: {name} ({adapter.__class__.__name__})") + def _normalize_adapter_name(self, adapter: str) -> str: + """Normalize adapter names for tolerant registry lookup.""" + normalized = adapter.lower().strip() + normalized = normalized.replace(" ", "_").replace("-", "_") + normalized = re.sub(r"_+", "_", normalized) + return normalized.strip("_") + + def _resolve_adapter_name(self, adapter: str) -> str: + """Resolve aliases and normalized names to a registered adapter key.""" + if adapter in self.adapters: + return adapter + + normalized_adapter = self._normalize_adapter_name(adapter) + alias_map = { + "single_cell_rna_seq": "transcriptomics_single_cell", + "singlecell_rnaseq": "transcriptomics_single_cell", + "scrna": "transcriptomics_single_cell", + "scrna_seq": "transcriptomics_single_cell", + } + + resolved_adapter = alias_map.get(normalized_adapter, normalized_adapter) + if resolved_adapter in self.adapters: + return resolved_adapter + + raise ValueError(f"Adapter '{adapter}' not registered") + def load_modality( self, name: str, @@ -778,10 +805,8 @@ def load_modality( Raises: ValueError: If adapter is not registered or validation fails """ - if adapter not in self.adapters: - raise ValueError(f"Adapter '{adapter}' not registered") - - adapter_instance = self.adapters[adapter] + resolved_adapter = self._resolve_adapter_name(adapter) + adapter_instance = self.adapters[resolved_adapter] # Load data using adapter if not _is_anndata_instance(source): @@ -815,7 +840,7 @@ def load_modality( entity_type="modality_data", metadata={ "modality_name": name, - "adapter": adapter, + "adapter": resolved_adapter, "shape": adata.shape, }, ) @@ -835,10 +860,10 @@ def load_modality( parameters={ "name": name, "source": source_path, - "adapter": adapter, + "adapter": resolved_adapter, **kwargs, }, - description=f"Loaded modality '{name}' using {adapter} adapter", + description=f"Loaded modality '{name}' using {resolved_adapter} adapter", ir=loading_ir, ) @@ -846,7 +871,7 @@ def load_modality( adata = self.provenance.add_to_anndata(adata) logger.info( - f"Loaded modality '{name}': {adata.shape} using adapter '{adapter}'" + f"Loaded modality '{name}': {adata.shape} using adapter '{resolved_adapter}'" ) return adata diff --git a/tests/unit/core/test_adapter_aliases_issue8.py b/tests/unit/core/test_adapter_aliases_issue8.py new file mode 100644 index 0000000..55a2119 --- /dev/null +++ b/tests/unit/core/test_adapter_aliases_issue8.py @@ -0,0 +1,40 @@ +import pytest + +from lobster.core.data_manager_v2 import DataManagerV2 + + +@pytest.fixture +def lightweight_data_manager(): + dm = DataManagerV2.__new__(DataManagerV2) + dm.adapters = {"transcriptomics_single_cell": object()} + return dm + + +@pytest.mark.unit +@pytest.mark.parametrize( + "adapter_input", + [ + "single_cell_rna_seq", + "single-cell rna-seq", + "scrna", + "scrna_seq", + "scRNA-seq", + ], +) +def test_single_cell_aliases_resolve_to_canonical( + lightweight_data_manager, adapter_input +): + resolved = lightweight_data_manager._resolve_adapter_name(adapter_input) + assert resolved == "transcriptomics_single_cell" + + +@pytest.mark.unit +def test_canonical_adapter_resolves_as_is(lightweight_data_manager): + resolved = lightweight_data_manager._resolve_adapter_name("transcriptomics_single_cell") + assert resolved == "transcriptomics_single_cell" + + +@pytest.mark.unit +def test_invalid_adapter_still_raises_value_error(lightweight_data_manager): + with pytest.raises(ValueError, match="Adapter 'not_a_real_adapter' not registered"): + lightweight_data_manager._resolve_adapter_name("not_a_real_adapter")