From 55ce148c75a56841a199b79029ae843adc4667d2 Mon Sep 17 00:00:00 2001 From: Andrey Isaev Date: Fri, 7 Aug 2026 16:20:00 +0300 Subject: [PATCH 1/4] Check unique genes in validator for any organism --- src/cap_upload_validator/upload_validator.py | 9 ++++- test/test_upload_validator.py | 35 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/cap_upload_validator/upload_validator.py b/src/cap_upload_validator/upload_validator.py index 4bc8ed9..b271f1d 100644 --- a/src/cap_upload_validator/upload_validator.py +++ b/src/cap_upload_validator/upload_validator.py @@ -11,6 +11,7 @@ MusMusculus, MultiSpecies, Organism, + UnsupportedOrganism, str_to_organism, ontology_id_to_organism, ) @@ -352,6 +353,12 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: logger.debug(f"Organism(s) in dataset = {dataset_organisms}, known organisms = {known_organisms}") missing_genes_mask = None + if UnsupportedOrganism in dataset_organisms: + organism = (UnsupportedOrganism if len(dataset_organisms) == 1 else MultiSpecies) + self._organism = organism + logger.debug("Unknown organism found, skipping only known-gene validation.") + return missing_genes_mask + # Check ENSEMBL ids for supported organism if len(dataset_organisms) == 1: organism = dataset_organisms[0] @@ -361,7 +368,7 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: logger.debug("Single known organism found, validating gene IDs.") missing_genes_mask = self._validate_gene_ids(clean_index, organism) else: - logger.debug("Unknown organism found, skipping gene validation.") + logger.debug("Organism has no known-gene validation rules, skipping that check.") elif len(dataset_organisms) > 1: logger.debug("There are multiple organisms in dataset") self._organism = MultiSpecies diff --git a/test/test_upload_validator.py b/test/test_upload_validator.py index e00e0aa..5c54079 100644 --- a/test/test_upload_validator.py +++ b/test/test_upload_validator.py @@ -33,6 +33,7 @@ CSCMatrixInX, AnnDataMultipleOntologyIDs, AnnDataInvalidDiseaseOntologyForHuman, + AnnDataGeneIndexIsNotUnique, ) TMP_DIR = Path(tempfile.mkdtemp()) @@ -189,6 +190,40 @@ def check_var_index(): assert False, f"Unpredicted error: {e}" +@pytest.mark.parametrize( + ("var_names", "should_fail"), + [ + (["ENSG000001.1", "ENSG000001.2"], True), # duplicate after removing version suffix + (["TP53", "TP53"], True), # duplicate gene symbol + (["unknown_gene_1", "unknown_gene_2"], False), # unique genes + ], +) +@pytest.mark.parametrize( + "organisms", + [ + "unsupported organism", + [HomoSapiens.name, "unsupported organism"], # mixed with unsupported organism + ], +) +def test_var_requires_unique_genes_for_any_unsupported_organism( + var_names, + should_fail, + organisms, +): + adata = ad.AnnData(X=np.eye(len(var_names))) + adata.var_names = var_names + adata.obs[ORGANISM_COLUMN] = organisms + + validator = UploadValidator(None) + validator._multi_exception.raise_on_append = True + + if should_fail: + with pytest.raises(AnnDataGeneIndexIsNotUnique): + validator._check_var_index(adata) + else: + assert validator._check_var_index(adata) is None + + @pytest.mark.parametrize("set_organism", [False, True, "ont"]) def test_validator(set_organism): x = np.eye(10) + 0.1 # not a counts From e507005a0b55d063fbd2b576aae4e0271678b006 Mon Sep 17 00:00:00 2001 From: Andrey Isaev Date: Mon, 10 Aug 2026 13:10:29 +0300 Subject: [PATCH 2/4] Refactoring --- src/cap_upload_validator/upload_validator.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/cap_upload_validator/upload_validator.py b/src/cap_upload_validator/upload_validator.py index b271f1d..f770b93 100644 --- a/src/cap_upload_validator/upload_validator.py +++ b/src/cap_upload_validator/upload_validator.py @@ -353,12 +353,6 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: logger.debug(f"Organism(s) in dataset = {dataset_organisms}, known organisms = {known_organisms}") missing_genes_mask = None - if UnsupportedOrganism in dataset_organisms: - organism = (UnsupportedOrganism if len(dataset_organisms) == 1 else MultiSpecies) - self._organism = organism - logger.debug("Unknown organism found, skipping only known-gene validation.") - return missing_genes_mask - # Check ENSEMBL ids for supported organism if len(dataset_organisms) == 1: organism = dataset_organisms[0] @@ -372,7 +366,10 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: elif len(dataset_organisms) > 1: logger.debug("There are multiple organisms in dataset") self._organism = MultiSpecies - missing_genes_mask = self._validate_gene_ids(clean_index, MultiSpecies) + if UnsupportedOrganism in dataset_organisms: + logger.debug("Unknown organism found, skipping only known-gene validation.") + else: + missing_genes_mask = self._validate_gene_ids(clean_index, MultiSpecies) logger.debug("Finished checking var index!") return missing_genes_mask From 99fb34b98be623bfa801db1e8fa947ebee02f5bb Mon Sep 17 00:00:00 2001 From: Andrey Isaev Date: Mon, 10 Aug 2026 13:22:13 +0300 Subject: [PATCH 3/4] MultiSpecies requires human genes --- src/cap_upload_validator/upload_validator.py | 6 +-- test/test_upload_validator.py | 48 ++++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/cap_upload_validator/upload_validator.py b/src/cap_upload_validator/upload_validator.py index f770b93..0d3eb86 100644 --- a/src/cap_upload_validator/upload_validator.py +++ b/src/cap_upload_validator/upload_validator.py @@ -11,7 +11,6 @@ MusMusculus, MultiSpecies, Organism, - UnsupportedOrganism, str_to_organism, ontology_id_to_organism, ) @@ -366,10 +365,7 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: elif len(dataset_organisms) > 1: logger.debug("There are multiple organisms in dataset") self._organism = MultiSpecies - if UnsupportedOrganism in dataset_organisms: - logger.debug("Unknown organism found, skipping only known-gene validation.") - else: - missing_genes_mask = self._validate_gene_ids(clean_index, MultiSpecies) + missing_genes_mask = self._validate_gene_ids(clean_index, MultiSpecies) logger.debug("Finished checking var index!") return missing_genes_mask diff --git a/test/test_upload_validator.py b/test/test_upload_validator.py index 5c54079..0b6b361 100644 --- a/test/test_upload_validator.py +++ b/test/test_upload_validator.py @@ -34,6 +34,7 @@ AnnDataMultipleOntologyIDs, AnnDataInvalidDiseaseOntologyForHuman, AnnDataGeneIndexIsNotUnique, + AnnDataUnsupportedGenes, ) TMP_DIR = Path(tempfile.mkdtemp()) @@ -191,25 +192,36 @@ def check_var_index(): @pytest.mark.parametrize( - ("var_names", "should_fail"), + ("organisms", "var_names", "expected_error"), [ - (["ENSG000001.1", "ENSG000001.2"], True), # duplicate after removing version suffix - (["TP53", "TP53"], True), # duplicate gene symbol - (["unknown_gene_1", "unknown_gene_2"], False), # unique genes + ( + "unsupported organism", + ["ENSG000001.1", "ENSG000001.2"], + AnnDataGeneIndexIsNotUnique, + ), # duplicate after removing version suffix + ( + "unsupported organism", + ["TP53", "TP53"], + AnnDataGeneIndexIsNotUnique, + ), # duplicate gene symbol + ( + "unsupported organism", + ["unknown_gene_1", "unknown_gene_2"], + None, + ), # unique genes for a single unsupported organism + ( + [HomoSapiens.name, "unsupported organism"], + ["ENSG00000290825", "ENSG00000223972"], + None, + ), # known human genes for multiple organisms + ( + [HomoSapiens.name, "unsupported organism"], + ["unknown_gene_1", "unknown_gene_2"], + AnnDataUnsupportedGenes, + ), # genes outside the human gene map for multiple organisms ], ) -@pytest.mark.parametrize( - "organisms", - [ - "unsupported organism", - [HomoSapiens.name, "unsupported organism"], # mixed with unsupported organism - ], -) -def test_var_requires_unique_genes_for_any_unsupported_organism( - var_names, - should_fail, - organisms, -): +def test_var_validation_with_unsupported_organisms(organisms, var_names, expected_error): adata = ad.AnnData(X=np.eye(len(var_names))) adata.var_names = var_names adata.obs[ORGANISM_COLUMN] = organisms @@ -217,8 +229,8 @@ def test_var_requires_unique_genes_for_any_unsupported_organism( validator = UploadValidator(None) validator._multi_exception.raise_on_append = True - if should_fail: - with pytest.raises(AnnDataGeneIndexIsNotUnique): + if expected_error: + with pytest.raises(expected_error): validator._check_var_index(adata) else: assert validator._check_var_index(adata) is None From a156f6f13fe01991e1cf171c58d727e9d5aa8fb5 Mon Sep 17 00:00:00 2001 From: Andrey Isaev Date: Mon, 10 Aug 2026 13:23:55 +0300 Subject: [PATCH 4/4] Update upload_validator.py --- src/cap_upload_validator/upload_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cap_upload_validator/upload_validator.py b/src/cap_upload_validator/upload_validator.py index 0d3eb86..4bc8ed9 100644 --- a/src/cap_upload_validator/upload_validator.py +++ b/src/cap_upload_validator/upload_validator.py @@ -361,7 +361,7 @@ def _check_var_index(self, cap_adata: CapAnnData) -> Optional[pd.Series]: logger.debug("Single known organism found, validating gene IDs.") missing_genes_mask = self._validate_gene_ids(clean_index, organism) else: - logger.debug("Organism has no known-gene validation rules, skipping that check.") + logger.debug("Unknown organism found, skipping gene validation.") elif len(dataset_organisms) > 1: logger.debug("There are multiple organisms in dataset") self._organism = MultiSpecies