Repository: biopython/biopython

Issue:
KeyError: 'name' when reading KGML from file
### Setup

I am reporting a problem with Biopython version, Python version, and operating
system as follows:

```
1.77

3.7.6 (default, Jan  8 2020, 19:59:22)
[GCC 7.3.0]

CPython
Linux-4.15.0-115-generic-x86_64-with-debian-buster-sid
```

### Expected behaviour

Read the file as a ```Bio.KEGG.KGML.KGML_pathway.Pathway``` object

### Actual behaviour

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jsequeira/anaconda3/lib/python3.7/site-packages/Bio/KEGG/KGML/KGML_parser.py", line 38, in read
    pathway = next(pathways)
  File "/home/jsequeira/anaconda3/lib/python3.7/site-packages/Bio/KEGG/KGML/KGML_parser.py", line 71, in parse
    yield KGMLParser(elem).parse()
  File "/home/jsequeira/anaconda3/lib/python3.7/site-packages/Bio/KEGG/KGML/KGML_parser.py", line 172, in parse
    _parse_relation(element)
  File "/home/jsequeira/anaconda3/lib/python3.7/site-packages/Bio/KEGG/KGML/KGML_parser.py", line 154, in _parse_relation
    name, value = subtype.attrib["name"], subtype.attrib["value"]
KeyError: 'name'
```

### Steps to reproduce

```
kgml = KGML_parser.read(kegg_get("ko00680", "kgml"))
with open('map00680.xml','w') as f: f.write(kgml.get_KGML())
KGML_parser.read(open('map00680.xml','r'))
```



Discussion:
It might take me a while to test and submit PR, but this seems to fix it:
```
diff --git a/Bio/KEGG/KGML/KGML_pathway.py b/Bio/KEGG/KGML/KGML_pathway.py
index 0476f0bde..a75a185af 100644
--- a/Bio/KEGG/KGML/KGML_pathway.py
+++ b/Bio/KEGG/KGML/KGML_pathway.py
@@ -854,6 +854,9 @@ class Relation:
         }
         for (name, value) in self.subtypes:
             subtype = ET.Element("subtype")
-            subtype.attrib[name] = str(value)
+            subtype.attrib = {
+                "name": name,
+                "value": str(value)
+            }
             relation.append(subtype)
         return relation
```
Cross reference #3465 - the timing suggests both issues stem from a KEGG update.
@widdowquinn are you able to look at this and the proposed fix?
Looking at it just now…
I don't know when the change happened, but the new raw XML for a subtype is

```xml
    <relation entry1="35" entry2="71" type="ECrel">
        <subtype name="compound" value="33"/>
    </relation>
```

but `KGMLParser` writes

```xml
  <relation entry1="35" entry2="71" type="ECrel">
    <subtype compound="33"/>
  </relation>
```

and the proposed fix looks like it should work, to me.
I think this might be fixed to be up to date with 0.7.2, now.

```python
>>> from Bio.KEGG.REST import kegg_get
>>> from Bio.KEGG.KGML import KGML_parser
>>> kgml = KGML_parser.read(kegg_get("ko00680", "kgml"))
>>> with open("map00680.xml", "w") as ofh:
...     ofh.write(kgml.get_KGML())
...     
... 
104280
>>> with open("map00680.xml", "r") as ifh:
...     roundtrip = KGML_parser.read(ifh)
...     
... 
>>> kgml.name
'path:ko00680'
>>> roundtrip.name
'path:ko00680'
>>> len(kgml.relations)
267
>>> len(roundtrip.relations)
267
```

Resolving patch:
diff --git a/Bio/KEGG/KGML/KGML_pathway.py b/Bio/KEGG/KGML/KGML_pathway.py
--- a/Bio/KEGG/KGML/KGML_pathway.py
+++ b/Bio/KEGG/KGML/KGML_pathway.py
@@ -7,7 +7,7 @@
 
 """Classes to represent a KGML Pathway Map.
 
-The KGML definition is as of release KGML v0.7.1
+The KGML definition is as of release KGML v0.7.2
 (http://www.kegg.jp/kegg/xml/docs/)
 
 Classes:
@@ -33,7 +33,7 @@ class Pathway:
     """Represents a KGML pathway from KEGG.
 
     Specifies graph information for the pathway map, as described in
-    release KGML v0.7.1 (http://www.kegg.jp/kegg/xml/docs/)
+    release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
 
     Attributes:
      - name - KEGGID of the pathway map
@@ -81,7 +81,7 @@ def get_KGML(self):
                 '<?xml version="1.0"?>',
                 "<!DOCTYPE pathway SYSTEM "
                 '"http://www.genome.jp/kegg/xml/'
-                'KGML_v0.7.1_.dtd">',
+                'KGML_v0.7.2_.dtd">',
                 "<!-- Created by KGML_Pathway.py %s -->" % time.asctime(),
             ]
         )
@@ -262,7 +262,7 @@ class Entry:
     """Represent an Entry from KGML.
 
     Each Entry element is a node in the pathway graph, as described in
-    release KGML v0.7.1 (http://www.kegg.jp/kegg/xml/docs/)
+    release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
 
     Attributes:
      - id - The ID of the entry in the pathway map (integer)
@@ -422,7 +422,7 @@ class Component:
     """An Entry subelement used to represents a complex node.
 
     A subelement of the Entry element, used when the Entry is a complex
-    node, as described in release KGML v0.7.1
+    node, as described in release KGML v0.7.2
     (http://www.kegg.jp/kegg/xml/docs/)
 
     The Component acts as a collection (with type 'group', and typically
@@ -460,7 +460,7 @@ class Graphics:
     """An Entry subelement used to represents the visual representation.
 
     A subelement of Entry, specifying its visual representation, as
-    described in release KGML v0.7.1 (http://www.kegg.jp/kegg/xml/docs/)
+    described in release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
 
     Attributes:
      - name         Label for the graphics object
@@ -780,7 +780,7 @@ class Relation:
     """A relationship between to products, KOs, or protein and compound.
 
     This describes a relationship between two products, KOs, or protein
-    and compound, as described in release KGML v0.7.1
+    and compound, as described in release KGML v0.7.2
     (http://www.kegg.jp/kegg/xml/docs/)
 
     Attributes:
@@ -854,6 +854,6 @@ def element(self):
         }
         for (name, value) in self.subtypes:
             subtype = ET.Element("subtype")
-            subtype.attrib[name] = str(value)
+            subtype.attrib = {"name": name, "value": str(value)}
             relation.append(subtype)
         return relation


Test patch:
diff --git a/Tests/test_KEGG_online.py b/Tests/test_KEGG_online.py
--- a/Tests/test_KEGG_online.py
+++ b/Tests/test_KEGG_online.py
@@ -8,6 +8,7 @@
 """Tests for online functionality of the KEGG module."""
 
 # Builtins
+import io
 import unittest
 
 from Bio.KEGG.KGML import KGML_parser
@@ -251,6 +252,17 @@ def test_parse_remote_pathway(self):
             pathway = KGML_parser.read(handle)
         self.assertEqual(pathway.name, "path:ko03070")
 
+    def test_parser_roundtrip(self):
+        """Download a KEGG pathway, write local KGML and check roundtrip."""
+        with kegg_get("ko00680", "kgml") as remote_handle:
+            pathway = KGML_parser.read(remote_handle)
+
+        with io.StringIO(pathway.get_KGML()) as local_handle:
+            roundtrip = KGML_parser.read(local_handle)
+
+        self.assertEqual(pathway.name, roundtrip.name)
+        self.assertEqual(len(pathway.relations), len(roundtrip.relations))
+
 
 if __name__ == "__main__":
     runner = unittest.TextTestRunner(verbosity=2)