Repository: networkx/networkx

Issue:
Cannot parse GML files from Cytoscape, possible regression?
This does not appear to be a duplicate of #321 -- I cannot parse .GML files produced from Cytoscape, including the "simple.gml" [file](https://networkx.lanl.gov/trac/attachment/ticket/324/simple.gml) as reported in #321. Below is the output I receive if parsing the file provided in that issue. 

From reading the comments, it looks like the suggestion is to either modify the GML parser directly in networkx (allowing slightly out of format GML files), or to modify the output from Cytoscape. If I issue a PR for the GML parser modification, would there be support for it?

``` python
10:20:28 (mcdonadt@8086):~> nx.__version__
Out[5]: '1.10'

10:20:32 (mcdonadt@8086):~> g = nx.read_gml('Downloads/simple.gml')
---------------------------------------------------------------------------
NetworkXError                             Traceback (most recent call last)
<ipython-input-6-e05f5232526f> in <module>()
----> 1 g = nx.read_gml('Downloads/simple.gml')

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in read_gml(path, label, destringizer)

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/utils/decorators.py in _open_file(func, *args, **kwargs)
    218         # Finally, we call the original function, making sure to close the fobj.
    219         try:
--> 220             result = func(*new_args, **kwargs)
    221         finally:
    222             if close_fobj:

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in read_gml(path, label, destringizer)
    208             yield line
    209
--> 210     G = parse_gml_lines(filter_lines(path), label, destringizer)
    211     return G
    212

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_gml_lines(lines, label, destringizer)
    381
    382     tokens = tokenize()
--> 383     graph = parse_graph()
    384
    385     directed = graph.pop('directed', False)

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_graph()
    370
    371     def parse_graph():
--> 372         curr_token, dct = parse_kv(next(tokens))
    373         if curr_token[0] is not None:  # EOF
    374             unexpected(curr_token, 'EOF')

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_kv(curr_token)
    355                 curr_token = next(tokens)
    356             elif type == 4:  # dict start
--> 357                 curr_token, value = parse_dict(curr_token)
    358             else:
    359                 unexpected(curr_token, "an int, float, string or '['")

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_dict(curr_token)
    365     def parse_dict(curr_token):
    366         curr_token = consume(curr_token, 4, "'['")    # dict start
--> 367         curr_token, dct = parse_kv(curr_token)
    368         curr_token = consume(curr_token, 5, "']'")  # dict end
    369         return curr_token, dct

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_kv(curr_token)
    355                 curr_token = next(tokens)
    356             elif type == 4:  # dict start
--> 357                 curr_token, value = parse_dict(curr_token)
    358             else:
    359                 unexpected(curr_token, "an int, float, string or '['")

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in parse_dict(curr_token)
    364
    365     def parse_dict(curr_token):
--> 366         curr_token = consume(curr_token, 4, "'['")    # dict start
    367         curr_token, dct = parse_kv(curr_token)
    368         curr_token = consume(curr_token, 5, "']'")  # dict end

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in consume(curr_token, type, expected)
    334     def consume(curr_token, type, expected):
    335         if curr_token[0] == type:
--> 336             return next(tokens)
    337         unexpected(curr_token, expected)
    338

/Users/mcdonadt/miniconda3/envs/unifrac-network/lib/python3.4/site-packages/networkx/readwrite/gml.py in tokenize()
    321                 else:
    322                     raise NetworkXError('cannot tokenize %r at (%d, %d)' %
--> 323                                         (line[pos:], lineno + 1, pos + 1))
    324             lineno += 1
    325         yield (None, None, lineno + 1, 1)  # EOF

NetworkXError: cannot tokenize 'root_index\t-3' at (5, 3)
```



Discussion:
Maybe the problem is that the parser doesn't recognize tabs as whitespace separators?

Yes, there would be support for such a PR. Check out the suggestion from @hagberg... 

I thought the underlying issue was that the spec doesn't include support for "_" characters? I did a terrible `sed` to replace "root_index" with "rootindex" and parsing was able to proceed but hit another wall on a another token that had an underscore. 

Oh, right.  That isn't part of the spec.  We'd need to add something like 
            r'[A-Za-z][0-9A-Za-z_]*\s+',  # keys
to the parser to accept that.


Resolving patch:
diff --git a/networkx/readwrite/gml.py b/networkx/readwrite/gml.py
--- a/networkx/readwrite/gml.py
+++ b/networkx/readwrite/gml.py
@@ -286,7 +286,7 @@ def parse_gml_lines(lines, label, destringizer):
     """
     def tokenize():
         patterns = [
-            r'[A-Za-z][0-9A-Za-z]*\s+',  # keys
+            r'[A-Za-z][0-9A-Za-z_]*\s+',  # keys
             r'[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*)(?:[Ee][+-]?[0-9]+)?',  # reals
             r'[+-]?[0-9]+',   # ints
             r'".*?"',         # strings


Test patch:
diff --git a/networkx/readwrite/tests/test_gml.py b/networkx/readwrite/tests/test_gml.py
--- a/networkx/readwrite/tests/test_gml.py
+++ b/networkx/readwrite/tests/test_gml.py
@@ -65,6 +65,90 @@ def setUp(self):
   ]
 ]
 """
+    def test_parse_gml_cytoscape_bug(self):
+        # example from issue #321, originally #324 in trac
+        cytoscape_example = """
+Creator "Cytoscape"
+Version 1.0
+graph   [
+    node    [
+        root_index  -3
+        id  -3
+        graphics    [
+            x   -96.0
+            y   -67.0
+            w   40.0
+            h   40.0
+            fill    "#ff9999"
+            type    "ellipse"
+            outline "#666666"
+            outline_width   1.5
+        ]
+        label   "node2"
+    ]
+    node    [
+        root_index  -2
+        id  -2
+        graphics    [
+            x   63.0
+            y   37.0
+            w   40.0
+            h   40.0
+            fill    "#ff9999"
+            type    "ellipse"
+            outline "#666666"
+            outline_width   1.5
+        ]
+        label   "node1"
+    ]
+    node    [
+        root_index  -1
+        id  -1
+        graphics    [
+            x   -31.0
+            y   -17.0
+            w   40.0
+            h   40.0
+            fill    "#ff9999"
+            type    "ellipse"
+            outline "#666666"
+            outline_width   1.5
+        ]
+        label   "node0"
+    ]
+    edge    [
+        root_index  -2
+        target  -2
+        source  -1
+        graphics    [
+            width   1.5
+            fill    "#0000ff"
+            type    "line"
+            Line    [
+            ]
+            source_arrow    0
+            target_arrow    3
+        ]
+        label   "DirectedEdge"
+    ]
+    edge    [
+        root_index  -1
+        target  -1
+        source  -3
+        graphics    [
+            width   1.5
+            fill    "#0000ff"
+            type    "line"
+            Line    [
+            ]
+            source_arrow    0
+            target_arrow    3
+        ]
+        label   "DirectedEdge"
+    ]
+]
+"""
+        nx.parse_gml(cytoscape_example)
 
     def test_parse_gml(self):
         G = nx.parse_gml(self.simple_data, label='label')