Repository: biomass-dev/biomass

Issue:
Raise `DetectionError` instead of creating wrong model
### Discussed in https://github.com/biomass-dev/biomass/discussions/223

Prepare `test_ubiquitination.txt` including:
```
A ubiquitinates B --> uB
```

Since there is no rule like 'ubiquitinate', we expect
```
# This is what I expect.
DetectionError: Unregistered words in line1: A ubiquitinates B --> uB
```

However, when you run the following code:
```python
testmodel = Text2Model("test_ubiquitination.txt")
testmodel.convert(overwrite=True)
```

This does NOT raise errors but creates model.
When you look into the resulting model, you will soon find the resulting model is wrong.
For example, in `name2idx/species.py`:
```python
from dataclasses import make_dataclass
from typing import Dict, List

NAMES: List[str] = [
    "AubiquitinatesB", # <-- this is not desired :(
    "uB",
]

...
```

It seems that this unregistered word is recognized as [`state transition rule`](https://github.com/biomass-dev/biomass/pull/169).

Comments:

Patch:
diff --git a/biomass/construction/reaction_rules.py b/biomass/construction/reaction_rules.py
index f780835..41921c8 100644
--- a/biomass/construction/reaction_rules.py
+++ b/biomass/construction/reaction_rules.py
@@ -1709,11 +1709,21 @@ class ReactionRules(ThermodynamicRestrictions):
             if arrow in description[0]:
                 is_unidirectional = True if arrow in self.fwd_arrows else False
                 two_species = description[0].split(arrow)
-                reactants = two_species[0].replace(" ", "").split("+")
-                products = two_species[1].replace(" ", "").split("+")
+                reactants = (
+                    None
+                    if len(two_species[0].split(" ")) > 1 and "+" not in two_species[0]
+                    else two_species[0].replace(" ", "").split("+")
+                )
+                products = (
+                    None
+                    if len(two_species[1].split(" ")) > 1 and "+" not in two_species[1]
+                    else two_species[1].replace(" ", "").split("+")
+                )
                 break
         else:
             raise ArrowError(self._get_arrow_error_message(line_num) + ".")
+        if reactants is None or products is None:
+            raise DetectionError(f"Unregistered words in line{line_num:d}: {line}")
         if list(set(reactants) & set(products)):
             modifiers = list(set(reactants) & set(products))
             for modifier in modifiers:


Test patch:
diff --git a/tests/test_text2model/test_error_message.py b/tests/test_text2model/test_error_message.py
index f21705b..5a90608 100644
--- a/tests/test_text2model/test_error_message.py
+++ b/tests/test_text2model/test_error_message.py
@@ -39,6 +39,19 @@ def test_typo_detection():
         assert "Maybe: 'binds'." in str(e.value)
 
 
+def test_unregistered_rule():
+    ubiquitination = Text2Model(
+        os.path.join(
+            os.path.dirname(__file__),
+            "text_files",
+            "unregistered_rule.txt",
+        )
+    )
+    with pytest.raises(DetectionError) as e:
+        ubiquitination.convert()
+    assert "Unregistered words in line1: A ubiquitinates B --> uB" in str(e.value)
+
+
 def test_cleanup():
     for i in ["1", "2"]:
         assert os.path.isdir(
@@ -55,3 +68,17 @@ def test_cleanup():
                 f"typo_{i}",
             )
         )
+    assert os.path.isdir(
+        os.path.join(
+            os.path.dirname(__file__),
+            "text_files",
+            "unregistered_rule",
+        )
+    )
+    shutil.rmtree(
+        os.path.join(
+            os.path.dirname(__file__),
+            "text_files",
+            "unregistered_rule",
+        )
+    )
diff --git a/tests/test_text2model/test_graph.py b/tests/test_text2model/test_graph.py
index 54ba13c..933c5d8 100644
--- a/tests/test_text2model/test_graph.py
+++ b/tests/test_text2model/test_graph.py
@@ -1,13 +1,13 @@
 import os
 import shutil
 
-import pytest
-
 from biomass import Text2Model
 
 file_dir = os.path.join(os.path.dirname(__file__), "text_files")
 txt_files = [name for name in os.listdir(file_dir) if "__" not in name]
 
+skipped_files = ["duplicate_binding.txt", "typo_1.txt", "typo_2.txt", "unregistered_rule.txt"]
+
 
 def test_preprocessing():
     for model in txt_files:
@@ -30,7 +30,7 @@ def test_preprocessing():
 
 def test_graph():
     for model_file in txt_files:
-        if model_file in ["duplicate_binding.txt", "typo_1.txt", "typo_2.txt"]:
+        if model_file in skipped_files:
             continue
         model = Text2Model(os.path.join(os.path.dirname(__file__), "text_files", model_file))
         model_path = os.path.join(file_dir, model_file.split(".")[0])
@@ -44,7 +44,7 @@ def test_graph():
 
 def test_cleanup():
     for model_file in txt_files:
-        if model_file in ["duplicate_binding.txt", "typo_1.txt", "typo_2.txt"]:
+        if model_file in skipped_files:
             continue
         model = model_file.split(".")[0]
         assert os.path.isdir(
diff --git a/tests/test_text2model/text_files/unregistered_rule.txt b/tests/test_text2model/text_files/unregistered_rule.txt
new file mode 100644
index 0000000..3566be5
--- /dev/null
+++ b/tests/test_text2model/text_files/unregistered_rule.txt
@@ -0,0 +1,1 @@
+A ubiquitinates B --> uB
\ No newline at end of file