Repository: levitsky/pyteomics

Issue:
Unparsable modifications are silently ignored in ProForma mass calculation
Hello,

I discovered a quirky "attitude" of ProForma parsing.

If a ProForma object has a modification that cannot be decoded this modification is silently ignored in all further calculations, while the same calculation on the modification object raises. I believe it will be better to propagate the Exception rather than silencing it.

Here is an example (Pyteomics 4.7.2 Python 3.10.2 Win64)

Both raise `KeyError`

```python
proforma.GenericModification('Generic').mass

proforma.UnimodModification('123456').mass
```

However - this works while ignoring the modification

```python
for peptide in ['PEPTIDE', 'PEPTID[Generic]E', 'PEPTID[UNIMOD:123456]E']:
    prof = proforma.ProForma.parse(peptide)
    print(f'{prof}\n{prof.mass}\n')
```
```
PEPTIDE
799.3599640267099

PEPTID[Generic]E
799.3599640267099

PEPTID[UNIMOD:123456]E
799.3599640267099
```


Discussion:

Patch:
diff --git a/pyteomics/proforma.py b/pyteomics/proforma.py
index 2d71c33..188a622 100644
--- a/pyteomics/proforma.py
+++ b/pyteomics/proforma.py
@@ -207,6 +207,16 @@ class TagBase(object):
     def parse(cls, buffer):
         return process_tag_tokens(buffer)
 
+    def has_mass(self):
+        """
+        Check if this tag carries a mass value.
+
+        Returns
+        -------
+        bool
+        """
+        return False
+
 
 class GroupLabelBase(TagBase):
     __slots__ = ()
@@ -742,6 +752,16 @@ class ModificationBase(TagBase):
         '''
         return self.definition['mass']
 
+    def has_mass(self):
+        """
+        Check if this tag carries a mass value.
+
+        Returns
+        -------
+        bool
+        """
+        return True
+
     @property
     def composition(self):
         '''The chemical composition shift this modification applies'''
@@ -850,6 +870,16 @@ class MassModification(TagBase):
     def mass(self):
         return self.value
 
+    def has_mass(self):
+        """
+        Check if this tag carries a mass value.
+
+        Returns
+        -------
+        bool
+        """
+        return True
+
     def __eq__(self, other):
         if isinstance(other, ModificationToken):
             return other == self
@@ -2321,38 +2351,32 @@ class ProForma(object):
             c_term = i == c_term_v
             for rule in fixed_modifications:
                 if rule.is_valid(aa, n_term, c_term):
-                    mass += rule.modification_tag.mass
+                    if rule.modification_tag.has_mass():
+                        mass += rule.modification_tag.mass
             tags = position[1]
             if tags:
                 for tag in tags:
-                    try:
+                    if tag.has_mass():
                         mass += tag.mass
-                    except (AttributeError, KeyError):
-                        continue
         for mod in self.properties['labile_modifications']:
             mass += mod.mass
         for mod in self.properties['unlocalized_modifications']:
             mass += mod.mass
         if self.properties.get('n_term'):
             for mod in self.properties['n_term']:
-                try:
+                if mod.has_mass():
                     mass += mod.mass
-                except (AttributeError, KeyError):
-                    continue
         mass += calculate_mass(formula="H")
         if self.properties.get('c_term'):
             for mod in self.properties['c_term']:
-                try:
+                if mod.has_mass():
                     mass += mod.mass
-                except (AttributeError, KeyError):
-                    continue
 
         mass += calculate_mass(formula="OH")
         for iv in self.properties['intervals']:
-            try:
-                mass += iv.tag.mass
-            except (AttributeError, KeyError):
-                continue
+            for tag in iv.tags:
+                if tag.has_mass():
+                    mass += tag.mass
         return mass
 
     def fragments(self, ion_shift, charge=1, reverse=None, include_labile=True, include_unlocalized=True):
@@ -2419,21 +2443,18 @@ class ProForma(object):
         if not reverse:
             if self.properties.get('n_term'):
                 for mod in self.properties['n_term']:
-                    try:
+                    if mod.has_mass():
                         mass += mod.mass
-                    except (AttributeError, KeyError):
-                        continue
         else:
             if self.properties.get('c_term'):
                 for mod in self.properties['c_term']:
-                    try:
+                    if mod.has_mass():
                         mass += mod.mass
-                    except (AttributeError, KeyError):
-                        continue
 
         if include_unlocalized:
             for mod in self.properties['unlocalized_modifications']:
-                mass += mod.mass
+                if mod.has_mass():
+                    mass += mod.mass
 
         mass += _WATER_MASS
 
@@ -2459,23 +2480,20 @@ class ProForma(object):
             c_term = i == c_term_v
             for rule in fixed_modifications:
                 if rule.is_valid(aa, n_term, c_term):
-                    mass += rule.modification_tag.mass
+                    if rule.modification_tag.has_mass():
+                        mass += rule.modification_tag.mass
 
             tags = position[1]
             if tags:
                 for tag in tags:
-                    try:
+                    if tag.has_mass():
                         mass += tag.mass
-                    except (AttributeError, KeyError):
-                        continue
 
             while intervals and intervals[0].contains(i):
                 iv = intervals.popleft()
-
-                try:
-                    mass += iv.tag.mass
-                except (AttributeError, KeyError):
-                    continue
+                for tag in iv.tags:
+                    if tag.has_mass():
+                        mass += tag.mass
 
             masses.append(mass)
 


Test patch:
diff --git a/tests/test_proforma.py b/tests/test_proforma.py
index 2425f83..3331344 100644
--- a/tests/test_proforma.py
+++ b/tests/test_proforma.py
@@ -26,8 +26,7 @@ class ProFormaTest(unittest.TestCase):
         assert properties['fixed_modifications'][0] == ModificationRule(
             GenericModification('Carbamidomethyl', None, None), ['C'])
         assert to_proforma(tokens, **properties) == complicated_short
-        self.assertAlmostEqual(
-            ProForma(tokens, properties).mass, 1210.5088, 3)
+        self.assertAlmostEqual(ProForma(tokens, properties).mass, 1228.6588, 3)
 
     def test_range(self):
         seq = "PRQT(EQC[Carbamidomethyl]FQRMS)[+19.0523]ISK"