napistu 0.2.5.dev6__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napistu/__main__.py +126 -96
- napistu/constants.py +35 -41
- napistu/context/__init__.py +10 -0
- napistu/context/discretize.py +462 -0
- napistu/context/filtering.py +387 -0
- napistu/gcs/__init__.py +1 -1
- napistu/identifiers.py +74 -15
- napistu/indices.py +68 -0
- napistu/ingestion/__init__.py +1 -1
- napistu/ingestion/bigg.py +47 -62
- napistu/ingestion/constants.py +18 -133
- napistu/ingestion/gtex.py +113 -0
- napistu/ingestion/hpa.py +147 -0
- napistu/ingestion/sbml.py +0 -97
- napistu/ingestion/string.py +2 -2
- napistu/matching/__init__.py +10 -0
- napistu/matching/constants.py +18 -0
- napistu/matching/interactions.py +518 -0
- napistu/matching/mount.py +529 -0
- napistu/matching/species.py +510 -0
- napistu/mcp/__init__.py +7 -4
- napistu/mcp/__main__.py +128 -72
- napistu/mcp/client.py +16 -25
- napistu/mcp/codebase.py +201 -153
- napistu/mcp/component_base.py +170 -0
- napistu/mcp/config.py +223 -0
- napistu/mcp/constants.py +45 -2
- napistu/mcp/documentation.py +253 -136
- napistu/mcp/documentation_utils.py +13 -48
- napistu/mcp/execution.py +372 -305
- napistu/mcp/health.py +49 -67
- napistu/mcp/profiles.py +10 -6
- napistu/mcp/server.py +161 -80
- napistu/mcp/tutorials.py +139 -87
- napistu/modify/__init__.py +1 -1
- napistu/modify/gaps.py +1 -1
- napistu/network/__init__.py +1 -1
- napistu/network/constants.py +101 -34
- napistu/network/data_handling.py +388 -0
- napistu/network/ig_utils.py +351 -0
- napistu/network/napistu_graph_core.py +354 -0
- napistu/network/neighborhoods.py +40 -40
- napistu/network/net_create.py +373 -309
- napistu/network/net_propagation.py +47 -19
- napistu/network/{net_utils.py → ng_utils.py} +124 -272
- napistu/network/paths.py +67 -51
- napistu/network/precompute.py +11 -11
- napistu/ontologies/__init__.py +10 -0
- napistu/ontologies/constants.py +129 -0
- napistu/ontologies/dogma.py +243 -0
- napistu/ontologies/genodexito.py +649 -0
- napistu/ontologies/mygene.py +369 -0
- napistu/ontologies/renaming.py +198 -0
- napistu/rpy2/__init__.py +229 -86
- napistu/rpy2/callr.py +47 -77
- napistu/rpy2/constants.py +24 -23
- napistu/rpy2/rids.py +61 -648
- napistu/sbml_dfs_core.py +587 -222
- napistu/scverse/__init__.py +15 -0
- napistu/scverse/constants.py +28 -0
- napistu/scverse/loading.py +727 -0
- napistu/utils.py +118 -10
- {napistu-0.2.5.dev6.dist-info → napistu-0.3.1.dist-info}/METADATA +8 -3
- napistu-0.3.1.dist-info/RECORD +133 -0
- tests/conftest.py +22 -0
- tests/test_context_discretize.py +56 -0
- tests/test_context_filtering.py +267 -0
- tests/test_identifiers.py +100 -0
- tests/test_indices.py +65 -0
- tests/{test_edgelist.py → test_ingestion_napistu_edgelist.py} +2 -2
- tests/test_matching_interactions.py +108 -0
- tests/test_matching_mount.py +305 -0
- tests/test_matching_species.py +394 -0
- tests/test_mcp_config.py +193 -0
- tests/test_mcp_documentation_utils.py +12 -3
- tests/test_mcp_server.py +356 -0
- tests/test_network_data_handling.py +397 -0
- tests/test_network_ig_utils.py +23 -0
- tests/test_network_neighborhoods.py +19 -0
- tests/test_network_net_create.py +459 -0
- tests/test_network_ng_utils.py +30 -0
- tests/test_network_paths.py +56 -0
- tests/{test_precomputed_distances.py → test_network_precompute.py} +8 -6
- tests/test_ontologies_genodexito.py +58 -0
- tests/test_ontologies_mygene.py +39 -0
- tests/test_ontologies_renaming.py +110 -0
- tests/test_rpy2_callr.py +79 -0
- tests/test_rpy2_init.py +151 -0
- tests/test_sbml.py +0 -31
- tests/test_sbml_dfs_core.py +134 -10
- tests/test_scverse_loading.py +778 -0
- tests/test_set_coverage.py +2 -2
- tests/test_utils.py +121 -1
- napistu/mechanism_matching.py +0 -1353
- napistu/rpy2/netcontextr.py +0 -467
- napistu-0.2.5.dev6.dist-info/RECORD +0 -97
- tests/test_igraph.py +0 -367
- tests/test_mechanism_matching.py +0 -784
- tests/test_net_utils.py +0 -149
- tests/test_netcontextr.py +0 -105
- tests/test_rpy2.py +0 -61
- /napistu/ingestion/{cpr_edgelist.py → napistu_edgelist.py} +0 -0
- {napistu-0.2.5.dev6.dist-info → napistu-0.3.1.dist-info}/WHEEL +0 -0
- {napistu-0.2.5.dev6.dist-info → napistu-0.3.1.dist-info}/entry_points.txt +0 -0
- {napistu-0.2.5.dev6.dist-info → napistu-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {napistu-0.2.5.dev6.dist-info → napistu-0.3.1.dist-info}/top_level.txt +0 -0
- /tests/{test_obo.py → test_ingestion_obo.py} +0 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
from napistu.ontologies.mygene import create_python_mapping_tables
|
2
|
+
from napistu.ontologies.constants import INTERCONVERTIBLE_GENIC_ONTOLOGIES
|
3
|
+
|
4
|
+
|
5
|
+
def test_create_python_mapping_tables_yeast():
|
6
|
+
"""Test create_python_mapping_tables with yeast species."""
|
7
|
+
# Test with a subset of mappings to keep test runtime reasonable
|
8
|
+
test_mappings = {"ensembl_gene", "symbol", "uniprot"}
|
9
|
+
|
10
|
+
# Verify test mappings are valid
|
11
|
+
assert test_mappings.issubset(
|
12
|
+
INTERCONVERTIBLE_GENIC_ONTOLOGIES
|
13
|
+
), "Test mappings must be valid ontologies"
|
14
|
+
|
15
|
+
# Call function with yeast species
|
16
|
+
mapping_tables = create_python_mapping_tables(
|
17
|
+
mappings=test_mappings,
|
18
|
+
species="Saccharomyces cerevisiae",
|
19
|
+
test_mode=True, # Limit to 1000 genes for faster testing
|
20
|
+
)
|
21
|
+
|
22
|
+
# Basic validation of results
|
23
|
+
assert isinstance(mapping_tables, dict), "Should return a dictionary"
|
24
|
+
|
25
|
+
# Check that all requested mappings are present (ignoring extras like ncbi_entrez_gene)
|
26
|
+
assert test_mappings.issubset(
|
27
|
+
set(mapping_tables.keys())
|
28
|
+
), "All requested mappings should be present"
|
29
|
+
|
30
|
+
# Check each mapping table
|
31
|
+
for ontology in test_mappings:
|
32
|
+
df = mapping_tables[ontology]
|
33
|
+
assert not df.empty, f"Mapping table for {ontology} should not be empty"
|
34
|
+
assert (
|
35
|
+
df.index.name == "ncbi_entrez_gene"
|
36
|
+
), f"Index should be entrez gene IDs for {ontology}"
|
37
|
+
assert (
|
38
|
+
not df.index.duplicated().any()
|
39
|
+
), f"Should not have duplicate indices in {ontology}"
|
@@ -0,0 +1,110 @@
|
|
1
|
+
"""Tests for the ontology aliases module."""
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
import pandas as pd
|
5
|
+
from napistu import identifiers
|
6
|
+
from napistu.constants import IDENTIFIERS, SBML_DFS
|
7
|
+
from napistu.ontologies import renaming
|
8
|
+
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def mock_sbml_dfs():
|
12
|
+
"""Create a mock SBML_dfs object for testing."""
|
13
|
+
# Create a simple species DataFrame with identifiers
|
14
|
+
s1_ids = identifiers.Identifiers(
|
15
|
+
[
|
16
|
+
{
|
17
|
+
IDENTIFIERS.ONTOLOGY: "ncbigene",
|
18
|
+
IDENTIFIERS.IDENTIFIER: "123",
|
19
|
+
IDENTIFIERS.URL: "http://ncbi/123",
|
20
|
+
IDENTIFIERS.BQB: "is",
|
21
|
+
},
|
22
|
+
{
|
23
|
+
IDENTIFIERS.ONTOLOGY: "uniprot_id",
|
24
|
+
IDENTIFIERS.IDENTIFIER: "P12345",
|
25
|
+
IDENTIFIERS.URL: "http://uniprot/P12345",
|
26
|
+
IDENTIFIERS.BQB: "is",
|
27
|
+
},
|
28
|
+
]
|
29
|
+
)
|
30
|
+
|
31
|
+
s2_ids = identifiers.Identifiers(
|
32
|
+
[
|
33
|
+
{
|
34
|
+
IDENTIFIERS.ONTOLOGY: "ncbigene",
|
35
|
+
IDENTIFIERS.IDENTIFIER: "456",
|
36
|
+
IDENTIFIERS.URL: "http://ncbi/456",
|
37
|
+
IDENTIFIERS.BQB: "is",
|
38
|
+
}
|
39
|
+
]
|
40
|
+
)
|
41
|
+
|
42
|
+
species_df = pd.DataFrame(
|
43
|
+
{"s_name": ["gene1", "gene2"], SBML_DFS.S_IDENTIFIERS: [s1_ids, s2_ids]}
|
44
|
+
)
|
45
|
+
|
46
|
+
# Create mock SBML_dfs object
|
47
|
+
class MockSBMLDfs:
|
48
|
+
def __init__(self):
|
49
|
+
self.species = species_df
|
50
|
+
self.schema = {"species": {"pk": "s_id", "id": SBML_DFS.S_IDENTIFIERS}}
|
51
|
+
|
52
|
+
def get_identifiers(self, table_name):
|
53
|
+
if table_name == SBML_DFS.SPECIES:
|
54
|
+
all_ids = []
|
55
|
+
for idx, row in self.species.iterrows():
|
56
|
+
for id_dict in row[SBML_DFS.S_IDENTIFIERS].ids:
|
57
|
+
all_ids.append({"s_id": idx, **id_dict})
|
58
|
+
return pd.DataFrame(all_ids)
|
59
|
+
return pd.DataFrame()
|
60
|
+
|
61
|
+
return MockSBMLDfs()
|
62
|
+
|
63
|
+
|
64
|
+
def test_rename_species_ontologies_basic(mock_sbml_dfs):
|
65
|
+
"""Test basic alias updating functionality."""
|
66
|
+
# Define test aliases
|
67
|
+
test_aliases = {"ncbi_entrez_gene": {"ncbigene"}, "uniprot": {"uniprot_id"}}
|
68
|
+
|
69
|
+
# Update aliases
|
70
|
+
renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
|
71
|
+
|
72
|
+
# Get updated identifiers
|
73
|
+
updated_ids = mock_sbml_dfs.get_identifiers(SBML_DFS.SPECIES)
|
74
|
+
|
75
|
+
# Check that ontologies were updated correctly
|
76
|
+
assert "ncbi_entrez_gene" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
|
77
|
+
assert "uniprot" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
|
78
|
+
assert "ncbigene" not in set(updated_ids[IDENTIFIERS.ONTOLOGY])
|
79
|
+
assert "uniprot_id" not in set(updated_ids[IDENTIFIERS.ONTOLOGY])
|
80
|
+
|
81
|
+
|
82
|
+
def test_rename_species_ontologies_no_overlap(mock_sbml_dfs):
|
83
|
+
"""Test that error is raised when no aliases overlap with data."""
|
84
|
+
# Define aliases that don't match any existing ontologies
|
85
|
+
test_aliases = {"ensembl_gene": {"ensembl"}}
|
86
|
+
|
87
|
+
# Should raise ValueError due to no overlap
|
88
|
+
with pytest.raises(ValueError, match="do not overlap"):
|
89
|
+
renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
|
90
|
+
|
91
|
+
|
92
|
+
def test_rename_species_ontologies_partial_update(mock_sbml_dfs):
|
93
|
+
"""Test that partial updates work correctly."""
|
94
|
+
# Define aliases that only update some ontologies
|
95
|
+
test_aliases = {
|
96
|
+
"ncbi_entrez_gene": {"ncbigene"}
|
97
|
+
# Don't include uniprot_id mapping
|
98
|
+
}
|
99
|
+
|
100
|
+
# Update aliases
|
101
|
+
renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
|
102
|
+
|
103
|
+
# Get updated identifiers
|
104
|
+
updated_ids = mock_sbml_dfs.get_identifiers(SBML_DFS.SPECIES)
|
105
|
+
|
106
|
+
# Check that only ncbigene was updated
|
107
|
+
assert "ncbi_entrez_gene" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
|
108
|
+
assert "uniprot_id" in set(
|
109
|
+
updated_ids[IDENTIFIERS.ONTOLOGY]
|
110
|
+
) # Should remain unchanged
|
tests/test_rpy2_callr.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from unittest.mock import Mock, patch
|
5
|
+
import pandas as pd
|
6
|
+
import pytest
|
7
|
+
|
8
|
+
|
9
|
+
# Mock all rpy2 dependencies before any imports to prevent ImportErrors
|
10
|
+
sys.modules["rpy2"] = Mock()
|
11
|
+
sys.modules["rpy2.robjects"] = Mock()
|
12
|
+
sys.modules["rpy2.robjects.conversion"] = Mock()
|
13
|
+
sys.modules["rpy2.robjects.default_converter"] = Mock()
|
14
|
+
sys.modules["rpy2.robjects.packages"] = Mock()
|
15
|
+
sys.modules["rpy2.robjects.pandas2ri"] = Mock()
|
16
|
+
sys.modules["rpy2.rinterface"] = Mock()
|
17
|
+
sys.modules["rpy2_arrow"] = Mock()
|
18
|
+
sys.modules["rpy2_arrow.arrow"] = Mock()
|
19
|
+
sys.modules["pyarrow"] = Mock()
|
20
|
+
|
21
|
+
import napistu.rpy2.callr # noqa: E402
|
22
|
+
|
23
|
+
|
24
|
+
def test_get_napistu_r_without_rpy2():
|
25
|
+
"""Test get_napistu_r when rpy2 not available."""
|
26
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
27
|
+
with pytest.raises(ImportError, match="requires `rpy2`"):
|
28
|
+
napistu.rpy2.callr.get_napistu_r()
|
29
|
+
|
30
|
+
|
31
|
+
def test_bioconductor_org_r_function_without_rpy2():
|
32
|
+
"""Test bioconductor function when rpy2 not available."""
|
33
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
34
|
+
with pytest.raises(ImportError):
|
35
|
+
napistu.rpy2.callr.bioconductor_org_r_function("test", "Homo sapiens")
|
36
|
+
|
37
|
+
|
38
|
+
def test_get_rbase_without_rpy2():
|
39
|
+
"""Test get_rbase when rpy2 not available."""
|
40
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
41
|
+
with pytest.raises(ImportError):
|
42
|
+
napistu.rpy2.callr.get_rbase()
|
43
|
+
|
44
|
+
|
45
|
+
def test_pandas_conversion_functions_without_rpy2():
|
46
|
+
"""Test pandas conversion functions when rpy2 not available."""
|
47
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
48
|
+
# Test pandas to R conversion
|
49
|
+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
|
50
|
+
with pytest.raises(ImportError):
|
51
|
+
napistu.rpy2.callr.pandas_to_r_dataframe(df)
|
52
|
+
|
53
|
+
# Test R to pandas conversion
|
54
|
+
mock_rdf = Mock()
|
55
|
+
with pytest.raises(ImportError):
|
56
|
+
napistu.rpy2.callr.r_dataframe_to_pandas(mock_rdf)
|
57
|
+
|
58
|
+
# Test internal converter function
|
59
|
+
with pytest.raises(ImportError):
|
60
|
+
napistu.rpy2.callr._get_py2rpy_pandas_conv()
|
61
|
+
|
62
|
+
|
63
|
+
def test_callr_module_import_safety():
|
64
|
+
"""Test that callr module can be imported safely without triggering R initialization."""
|
65
|
+
import importlib
|
66
|
+
|
67
|
+
# Test module can be imported without rpy2
|
68
|
+
with patch.dict("sys.modules", {"rpy2": None}):
|
69
|
+
# This should not raise ImportError during import
|
70
|
+
importlib.reload(napistu.rpy2.callr)
|
71
|
+
|
72
|
+
# Test that function calls fail appropriately but imports don't
|
73
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
74
|
+
# Functions should fail with ImportError, not other errors
|
75
|
+
with pytest.raises(ImportError):
|
76
|
+
napistu.rpy2.callr.get_napistu_r()
|
77
|
+
|
78
|
+
with pytest.raises(ImportError):
|
79
|
+
napistu.rpy2.callr.get_rbase()
|
tests/test_rpy2_init.py
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from unittest.mock import Mock, patch
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
|
8
|
+
# Mock all rpy2 dependencies before any imports to prevent ImportErrors
|
9
|
+
sys.modules["rpy2"] = Mock()
|
10
|
+
sys.modules["rpy2.robjects"] = Mock()
|
11
|
+
sys.modules["rpy2.robjects.conversion"] = Mock()
|
12
|
+
sys.modules["rpy2.robjects.default_converter"] = Mock()
|
13
|
+
sys.modules["rpy2.robjects.packages"] = Mock()
|
14
|
+
sys.modules["rpy2.robjects.pandas2ri"] = Mock()
|
15
|
+
sys.modules["rpy2.rinterface"] = Mock()
|
16
|
+
sys.modules["rpy2_arrow"] = Mock()
|
17
|
+
sys.modules["rpy2_arrow.arrow"] = Mock()
|
18
|
+
sys.modules["pyarrow"] = Mock()
|
19
|
+
|
20
|
+
import napistu.rpy2 # noqa: E402
|
21
|
+
|
22
|
+
|
23
|
+
def test_rpy2_availability_detection():
|
24
|
+
"""Test rpy2 availability detection in various scenarios."""
|
25
|
+
# Test ImportError case
|
26
|
+
with patch.dict("sys.modules", {"rpy2": None}):
|
27
|
+
if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
|
28
|
+
napistu.rpy2.get_rpy2_availability.cache_clear()
|
29
|
+
assert napistu.rpy2.get_rpy2_availability() is False
|
30
|
+
|
31
|
+
# Test other exception case during import
|
32
|
+
with patch("builtins.__import__") as mock_import:
|
33
|
+
mock_import.side_effect = RuntimeError("R installation broken")
|
34
|
+
if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
|
35
|
+
napistu.rpy2.get_rpy2_availability.cache_clear()
|
36
|
+
assert napistu.rpy2.get_rpy2_availability() is False
|
37
|
+
|
38
|
+
# Test success case
|
39
|
+
mock_rpy2 = Mock()
|
40
|
+
with patch.dict("sys.modules", {"rpy2": mock_rpy2}):
|
41
|
+
if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
|
42
|
+
napistu.rpy2.get_rpy2_availability.cache_clear()
|
43
|
+
assert napistu.rpy2.get_rpy2_availability() is True
|
44
|
+
|
45
|
+
|
46
|
+
def test_caching_behavior():
|
47
|
+
"""Test that lazy loading functions are properly cached."""
|
48
|
+
# Test availability caching
|
49
|
+
with patch("builtins.__import__") as mock_import:
|
50
|
+
mock_import.return_value = Mock()
|
51
|
+
if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
|
52
|
+
napistu.rpy2.get_rpy2_availability.cache_clear()
|
53
|
+
|
54
|
+
result1 = napistu.rpy2.get_rpy2_availability()
|
55
|
+
result2 = napistu.rpy2.get_rpy2_availability()
|
56
|
+
|
57
|
+
assert result1 == result2
|
58
|
+
# Only test call count if caching is enabled
|
59
|
+
if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
|
60
|
+
assert mock_import.call_count == 1 # Should only be called once
|
61
|
+
|
62
|
+
# Test core modules caching
|
63
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
|
64
|
+
with patch("rpy2.robjects.conversion"), patch(
|
65
|
+
"rpy2.robjects.default_converter"
|
66
|
+
), patch("rpy2.robjects.packages.importr"):
|
67
|
+
if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
|
68
|
+
napistu.rpy2.get_rpy2_core_modules.cache_clear()
|
69
|
+
|
70
|
+
result1 = napistu.rpy2.get_rpy2_core_modules()
|
71
|
+
result2 = napistu.rpy2.get_rpy2_core_modules()
|
72
|
+
|
73
|
+
# Only test object identity if caching is enabled
|
74
|
+
if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
|
75
|
+
assert result1 is result2 # Same object due to caching
|
76
|
+
|
77
|
+
|
78
|
+
def test_lazy_import_functions_without_rpy2():
|
79
|
+
"""Test that lazy import functions fail appropriately when rpy2 unavailable."""
|
80
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
81
|
+
# Clear all caches if they exist
|
82
|
+
if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
|
83
|
+
napistu.rpy2.get_rpy2_core_modules.cache_clear()
|
84
|
+
if hasattr(napistu.rpy2.get_rpy2_extended_modules, "cache_clear"):
|
85
|
+
napistu.rpy2.get_rpy2_extended_modules.cache_clear()
|
86
|
+
if hasattr(napistu.rpy2.get_napistu_r_package, "cache_clear"):
|
87
|
+
napistu.rpy2.get_napistu_r_package.cache_clear()
|
88
|
+
|
89
|
+
# All should raise ImportError
|
90
|
+
with pytest.raises(ImportError, match="requires `rpy2`"):
|
91
|
+
napistu.rpy2.get_rpy2_core_modules()
|
92
|
+
|
93
|
+
with pytest.raises(ImportError, match="requires `rpy2`"):
|
94
|
+
napistu.rpy2.get_rpy2_extended_modules()
|
95
|
+
|
96
|
+
with pytest.raises(ImportError, match="requires `rpy2`"):
|
97
|
+
napistu.rpy2.get_napistu_r_package()
|
98
|
+
|
99
|
+
|
100
|
+
def test_decorators():
|
101
|
+
"""Test require_rpy2 and report_r_exceptions decorators."""
|
102
|
+
# Test require_rpy2 with rpy2 available
|
103
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
|
104
|
+
|
105
|
+
@napistu.rpy2.require_rpy2
|
106
|
+
def test_func_success():
|
107
|
+
return "success"
|
108
|
+
|
109
|
+
assert test_func_success() == "success"
|
110
|
+
|
111
|
+
# Test require_rpy2 without rpy2
|
112
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
|
113
|
+
|
114
|
+
@napistu.rpy2.require_rpy2
|
115
|
+
def test_func_fail():
|
116
|
+
return "success"
|
117
|
+
|
118
|
+
with pytest.raises(ImportError, match="test_func_fail.*requires `rpy2`"):
|
119
|
+
test_func_fail()
|
120
|
+
|
121
|
+
# Test report_r_exceptions with success
|
122
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
|
123
|
+
|
124
|
+
@napistu.rpy2.report_r_exceptions
|
125
|
+
def test_func_report_success():
|
126
|
+
return "success"
|
127
|
+
|
128
|
+
assert test_func_report_success() == "success"
|
129
|
+
|
130
|
+
# Test report_r_exceptions with failure
|
131
|
+
with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
|
132
|
+
with patch("napistu.rpy2.rsession_info") as mock_rsession:
|
133
|
+
|
134
|
+
@napistu.rpy2.report_r_exceptions
|
135
|
+
def test_func_report_fail():
|
136
|
+
raise ValueError("R function failed")
|
137
|
+
|
138
|
+
with pytest.raises(ValueError, match="R function failed"):
|
139
|
+
test_func_report_fail()
|
140
|
+
|
141
|
+
mock_rsession.assert_called_once()
|
142
|
+
|
143
|
+
|
144
|
+
def test_module_import_safety():
|
145
|
+
"""Test that modules can be imported safely without triggering R initialization."""
|
146
|
+
import importlib
|
147
|
+
|
148
|
+
# Test modules can be imported without rpy2
|
149
|
+
with patch.dict("sys.modules", {"rpy2": None}):
|
150
|
+
# These should not raise ImportError during import
|
151
|
+
importlib.reload(napistu.rpy2)
|
tests/test_sbml.py
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import os
|
4
|
-
import pytest
|
5
|
-
|
6
3
|
import pandas as pd
|
7
4
|
from napistu import sbml_dfs_core
|
8
5
|
from napistu.ingestion import sbml
|
@@ -18,31 +15,3 @@ def test_sbml_dfs(sbml_path):
|
|
18
15
|
assert type(dfs.get_cspecies_features()) is pd.DataFrame
|
19
16
|
assert type(dfs.get_species_features()) is pd.DataFrame
|
20
17
|
assert type(dfs.get_identifiers("species")) is pd.DataFrame
|
21
|
-
|
22
|
-
|
23
|
-
@pytest.mark.skip_on_windows
|
24
|
-
def test_adding_sbml_annotations(sbml_model):
|
25
|
-
annotations = pd.DataFrame(
|
26
|
-
[
|
27
|
-
{
|
28
|
-
"id": "compartment_12045",
|
29
|
-
"type": "compartment",
|
30
|
-
"uri": "http://identifiers.org/chebi/CHEBI:00000",
|
31
|
-
},
|
32
|
-
{
|
33
|
-
"id": "species_9033251",
|
34
|
-
"type": "species",
|
35
|
-
"uri": "http://identifiers.org/bigg.metabolite/fakemet",
|
36
|
-
},
|
37
|
-
{
|
38
|
-
"id": "species_9033251",
|
39
|
-
"type": "species",
|
40
|
-
"uri": "http://identifiers.org/chebi/CHEBI:00000",
|
41
|
-
},
|
42
|
-
]
|
43
|
-
)
|
44
|
-
|
45
|
-
outpath = "/tmp/tmp_write_test.sbml"
|
46
|
-
|
47
|
-
sbml.add_sbml_annotations(sbml_model, annotations, save_path=outpath)
|
48
|
-
assert os.path.isfile(outpath) is True
|
tests/test_sbml_dfs_core.py
CHANGED
@@ -10,7 +10,7 @@ from napistu.ingestion import sbml
|
|
10
10
|
from napistu.modify import pathwayannot
|
11
11
|
|
12
12
|
from napistu import identifiers as napistu_identifiers
|
13
|
-
from napistu.constants import SBML_DFS
|
13
|
+
from napistu.constants import SBML_DFS, SBOTERM_NAMES
|
14
14
|
from napistu.sbml_dfs_core import SBML_dfs
|
15
15
|
|
16
16
|
|
@@ -196,17 +196,13 @@ def test_sbml_dfs_remove_reactions_check_species(sbml_dfs):
|
|
196
196
|
# find all r_ids for a species and check if
|
197
197
|
# removing all these reactions also removes the species
|
198
198
|
s_id = sbml_dfs.species.index[0]
|
199
|
-
dat = (
|
200
|
-
sbml_dfs.
|
201
|
-
|
202
|
-
|
203
|
-
)[["r_id", "sc_id"]]
|
204
|
-
r_ids = dat["r_id"]
|
205
|
-
sc_ids = dat["sc_id"]
|
199
|
+
dat = sbml_dfs.compartmentalized_species.query("s_id == @s_id").merge(
|
200
|
+
sbml_dfs.reaction_species, left_index=True, right_on="sc_id"
|
201
|
+
)
|
202
|
+
r_ids = dat["r_id"].unique()
|
206
203
|
sbml_dfs.remove_reactions(r_ids, remove_species=True)
|
207
|
-
for sc_id in sc_ids:
|
208
|
-
assert sc_id not in sbml_dfs.compartmentalized_species.index
|
209
204
|
assert s_id not in sbml_dfs.species.index
|
205
|
+
sbml_dfs.validate()
|
210
206
|
|
211
207
|
|
212
208
|
def test_formula(sbml_dfs):
|
@@ -369,3 +365,131 @@ def test_get_identifiers_handles_missing_values():
|
|
369
365
|
assert result.shape[0] == 0 or all(
|
370
366
|
result[SBML_DFS.S_ID] == "s1"
|
371
367
|
), "Only Identifiers objects should be returned."
|
368
|
+
|
369
|
+
|
370
|
+
def test_find_underspecified_reactions():
|
371
|
+
|
372
|
+
reaction_w_regulators = pd.DataFrame(
|
373
|
+
{
|
374
|
+
SBML_DFS.SC_ID: ["A", "B", "C", "D", "E", "F", "G"],
|
375
|
+
SBML_DFS.STOICHIOMETRY: [-1, -1, 1, 1, 0, 0, 0],
|
376
|
+
SBML_DFS.SBO_TERM: [
|
377
|
+
SBOTERM_NAMES.REACTANT,
|
378
|
+
SBOTERM_NAMES.REACTANT,
|
379
|
+
SBOTERM_NAMES.PRODUCT,
|
380
|
+
SBOTERM_NAMES.PRODUCT,
|
381
|
+
SBOTERM_NAMES.CATALYST,
|
382
|
+
SBOTERM_NAMES.CATALYST,
|
383
|
+
SBOTERM_NAMES.STIMULATOR,
|
384
|
+
],
|
385
|
+
}
|
386
|
+
).assign(r_id="bar")
|
387
|
+
reaction_w_regulators[SBML_DFS.RSC_ID] = [
|
388
|
+
f"rsc_{i}" for i in range(len(reaction_w_regulators))
|
389
|
+
]
|
390
|
+
reaction_w_regulators.set_index(SBML_DFS.RSC_ID, inplace=True)
|
391
|
+
reaction_w_regulators = sbml_dfs_core.add_sbo_role(reaction_w_regulators)
|
392
|
+
|
393
|
+
reaction_w_interactors = pd.DataFrame(
|
394
|
+
{
|
395
|
+
SBML_DFS.SC_ID: ["A", "B"],
|
396
|
+
SBML_DFS.STOICHIOMETRY: [-1, 1],
|
397
|
+
SBML_DFS.SBO_TERM: [SBOTERM_NAMES.REACTANT, SBOTERM_NAMES.REACTANT],
|
398
|
+
}
|
399
|
+
).assign(r_id="baz")
|
400
|
+
reaction_w_interactors[SBML_DFS.RSC_ID] = [
|
401
|
+
f"rsc_{i}" for i in range(len(reaction_w_interactors))
|
402
|
+
]
|
403
|
+
reaction_w_interactors.set_index(SBML_DFS.RSC_ID, inplace=True)
|
404
|
+
reaction_w_interactors = sbml_dfs_core.add_sbo_role(reaction_w_interactors)
|
405
|
+
|
406
|
+
working_reactions = reaction_w_regulators.copy()
|
407
|
+
working_reactions["new"] = True
|
408
|
+
working_reactions.loc["rsc_0", "new"] = False
|
409
|
+
working_reactions
|
410
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
411
|
+
assert result == {"bar"}
|
412
|
+
|
413
|
+
# missing one enzyme -> operable
|
414
|
+
working_reactions = reaction_w_regulators.copy()
|
415
|
+
working_reactions["new"] = True
|
416
|
+
working_reactions.loc["rsc_4", "new"] = False
|
417
|
+
working_reactions
|
418
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
419
|
+
assert result == set()
|
420
|
+
|
421
|
+
# missing one product -> inoperable
|
422
|
+
working_reactions = reaction_w_regulators.copy()
|
423
|
+
working_reactions["new"] = True
|
424
|
+
working_reactions.loc["rsc_2", "new"] = False
|
425
|
+
working_reactions
|
426
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
427
|
+
assert result == {"bar"}
|
428
|
+
|
429
|
+
# missing all enzymes -> inoperable
|
430
|
+
working_reactions = reaction_w_regulators.copy()
|
431
|
+
working_reactions["new"] = True
|
432
|
+
working_reactions.loc["rsc_4", "new"] = False
|
433
|
+
working_reactions.loc["rsc_5", "new"] = False
|
434
|
+
working_reactions
|
435
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
436
|
+
assert result == {"bar"}
|
437
|
+
|
438
|
+
# missing regulators -> operable
|
439
|
+
working_reactions = reaction_w_regulators.copy()
|
440
|
+
working_reactions["new"] = True
|
441
|
+
working_reactions.loc["rsc_6", "new"] = False
|
442
|
+
working_reactions
|
443
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
444
|
+
assert result == set()
|
445
|
+
|
446
|
+
# remove an interactor
|
447
|
+
working_reactions = reaction_w_interactors.copy()
|
448
|
+
working_reactions["new"] = True
|
449
|
+
working_reactions.loc["rsc_0", "new"] = False
|
450
|
+
working_reactions
|
451
|
+
result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
|
452
|
+
assert result == {"baz"}
|
453
|
+
|
454
|
+
|
455
|
+
def test_remove_entity_data_success(sbml_dfs_w_data):
|
456
|
+
"""Test successful removal of entity data."""
|
457
|
+
# Get initial data
|
458
|
+
initial_species_data_keys = set(sbml_dfs_w_data.species_data.keys())
|
459
|
+
initial_reactions_data_keys = set(sbml_dfs_w_data.reactions_data.keys())
|
460
|
+
|
461
|
+
# Remove species data
|
462
|
+
sbml_dfs_w_data._remove_entity_data(SBML_DFS.SPECIES, "test_species")
|
463
|
+
assert "test_species" not in sbml_dfs_w_data.species_data
|
464
|
+
assert set(sbml_dfs_w_data.species_data.keys()) == initial_species_data_keys - {
|
465
|
+
"test_species"
|
466
|
+
}
|
467
|
+
|
468
|
+
# Remove reactions data
|
469
|
+
sbml_dfs_w_data._remove_entity_data(SBML_DFS.REACTIONS, "test_reactions")
|
470
|
+
assert "test_reactions" not in sbml_dfs_w_data.reactions_data
|
471
|
+
assert set(sbml_dfs_w_data.reactions_data.keys()) == initial_reactions_data_keys - {
|
472
|
+
"test_reactions"
|
473
|
+
}
|
474
|
+
|
475
|
+
# Validate the model is still valid after removals
|
476
|
+
sbml_dfs_w_data.validate()
|
477
|
+
|
478
|
+
|
479
|
+
def test_remove_entity_data_nonexistent(sbml_dfs_w_data, caplog):
|
480
|
+
"""Test warning when trying to remove nonexistent entity data."""
|
481
|
+
# Try to remove nonexistent species data
|
482
|
+
sbml_dfs_w_data._remove_entity_data(SBML_DFS.SPECIES, "nonexistent_label")
|
483
|
+
assert "Label 'nonexistent_label' not found in species_data" in caplog.text
|
484
|
+
assert set(sbml_dfs_w_data.species_data.keys()) == {"test_species"}
|
485
|
+
|
486
|
+
# Clear the log
|
487
|
+
caplog.clear()
|
488
|
+
|
489
|
+
# Try to remove nonexistent reactions data
|
490
|
+
sbml_dfs_w_data._remove_entity_data(SBML_DFS.REACTIONS, "nonexistent_label")
|
491
|
+
assert "Label 'nonexistent_label' not found in reactions_data" in caplog.text
|
492
|
+
assert set(sbml_dfs_w_data.reactions_data.keys()) == {"test_reactions"}
|
493
|
+
|
494
|
+
# Validate the model is still valid
|
495
|
+
sbml_dfs_w_data.validate()
|