Repository: networkx/networkx

Issue:
Reimplement graphml to use SAX (and not build XML documents in memory) (migrated from Trac #455)
Original ticket https://networkx.lanl.gov/trac/ticket/455
Reported 2010-11-01 by @alexsdutton, assigned to @alexsdutton.

I've reimplemented the readwrite.graphml API to use a SAX content handler and xml.sax.utils.XMLGenerator instead of etree. It passes the tests (so could be assumed to be a functional replacement), but may need tidying.

We'd like to use graphml for serialization but it builds the XML document in memory, which is suboptimal for very large graphs. A SAX-based implementation also seems to be much quicker.

I've attached the patch.



Discussion:
Attachment in Trac by @alexsdutton, 2010-11-01: [graphml-reimplementation.patch](https://networkx.lanl.gov/trac/attachment/ticket/455/graphml-reimplementation.patch)

Comment in Trac by @hagberg, 2010-11-02

Thanks - that is great!  

I don't know much about XML parsing but it would be great if this sped up reading and writing large files.

Here are some comments on quick review:
- There are still some stray print statements to be removed
- To be included in NetworkX it should work with Python3.x (and Python 2.6+).  So it needs to handle unicode/str.  It should be doable - see the type section in the gexf.py for one hacky way.
- I'd like it to at least be able to read the yEd generated graphml files (yfiles extension) like the current DOM version
- It's OK to define a new NetworkX exception - but maybe we should call it NetworkXIOError to match  Python and other NetworkX exceptions
- Nit-picky alert:  try to keep the line length less than 80 columns.

Comment in Trac by @hagberg, 2010-11-02

Comment in Trac by @alexsdutton, 2010-11-02

Hi Aric,

Thanks for the review and feedback. I'll get on to fixing those issues shortly.

I'm glad you're driving Py3k compatibility, and sorry for not considering it. In Py2.6+, am I right in thinking that all strings should be unicode once they've been pulled out of the XML anyway? In which case (to follow the yexf example) something like:

'''

try:
    chr(12345)     # Fails on Py!=3.
    unicode = str  # Py3k's str is our unicode type
except ValueError, e:
    pass
'''

should work if placed near the top.

With regards to yEd versions, would it be worth me adding more tests to lay out the required interoperability?

I'd also like to improve the comments (i.e. add some), as it's possibly a little difficult to understand what it's up to.

Comment in Trac by Aric Hagberg <aric.hagberg, 2010-11-03

In [816ebcc0d1fa09646a7b0d03a90b99bc8aab96d8/networkx]:
'''
#!CommitTicketReference repository="networkx" revision="816ebcc0d1fa09646a7b0d03a90b99bc8aab96d8"
Add yfiles graphml data test.

Addresses #455
'''

Comment in Trac by @hagberg, 2010-11-03

Replying to [comment:3 alexsdutton]:

> I'm glad you're driving Py3k compatibility, and sorry for not considering it. 

Almost all of NetworkX works with Python 3.1 (only the few parts that depend on
external libraries that aren't Python 3 ready). 

> In Py2.6+, am I right in thinking that all strings should be unicode once they've been pulled out of the XML anyway? In which case (to follow the yexf example) something like:
> 
> '''
> 
> try:
>     chr(12345)     # Fails on Py!=3.
>     unicode = str  # Py3k's str is our unicode type
> except ValueError, e:
>     pass
> '''
> 
> should work if placed near the top.

That seems like it will work.  For Python 2.x the strings should be unicode 
(unless we want to allow the user to override that).

> With regards to yEd versions, would it be worth me adding more tests to lay out the required interoperability?

I just added a very basic smoke test for reading a yEd generated graphml file.  The currently DOM implementation reads the file correctly but throws away
any files extension data.  Obviously that isn't a full implementation....

Comment in Trac by @hagberg, 2010-11-24

Comment in Trac by @hagberg, 2010-12-14

Comment in Trac by @hagberg, 2011-01-14

Comment in Trac by @hagberg, 2011-05-14

This is still waiting for updated code.

Comment in Trac by @hagberg, 2011-05-20

Is there any progress on this issue or a new implementation should be proposed?

I was forced to write my own GraphML writer to output very large graphs, and would be glad to send a pull request for this if needed. 

I had no idea about this one. Thanks for bringing attention to it.

Does your writer use SAX? Does it pass all the unittests we have for grapml?

I haven't finished the change, but it would use lxml instead of SAX (http://lxml.de/api.html#incremental-xml-generation), as it allows you to reuse some of the already written pieces using Element Tree.

I will work on this and send you a Pull request then.

I submitted a pull request 4 months ago, and still no comments.


Resolving patch:
diff --git a/networkx/readwrite/graphml.py b/networkx/readwrite/graphml.py
--- a/networkx/readwrite/graphml.py
+++ b/networkx/readwrite/graphml.py
@@ -1,3 +1,12 @@
+#    Copyright (C) 2008-2017 by
+#    Aric Hagberg <hagberg@lanl.gov>
+#    Dan Schult <dschult@colgate.edu>
+#    Pieter Swart <swart@lanl.gov>
+#    All rights reserved.
+#    BSD license.
+#
+# Authors: Salim Fadhley
+#          Aric Hagberg (hagberg@lanl.gov)
 """
 *******
 GraphML
@@ -34,37 +43,41 @@
 http://graphml.graphdrawing.org/primer/graphml-primer.html
 for examples.
 """
-__author__ = """\n""".join(['Salim Fadhley',
-                            'Aric Hagberg (hagberg@lanl.gov)'
-                            ])
-
-__all__ = ['write_graphml', 'read_graphml', 'generate_graphml',
-           'parse_graphml', 'GraphMLWriter', 'GraphMLReader']
-
-from collections import defaultdict
-import networkx as nx
-from networkx.utils import open_file, make_str
 import warnings
+from collections import defaultdict
+
 try:
-    from xml.etree.cElementTree import Element, ElementTree, tostring, fromstring
+    from xml.etree.cElementTree import Element, ElementTree
+    from xml.etree.cElementTree import tostring, fromstring
 except ImportError:
     try:
-        from xml.etree.ElementTree import Element, ElementTree, tostring, fromstring
+        from xml.etree.ElementTree import Element, ElementTree
+        from xml.etree.ElementTree import tostring, fromstring
     except ImportError:
         pass
 
-@open_file(1,mode='wb')
-def write_graphml(G, path, encoding='utf-8', prettyprint=True, infer_numeric_types=False):
+try:
+    import lxml.etree as lxmletree
+except ImportError:
+    lxmletree = None
+
+import networkx as nx
+from networkx.utils import open_file, make_str
+
+__all__ = ['write_graphml', 'read_graphml', 'generate_graphml',
+           'write_graphml_xml', 'write_graphml_lxml',
+           'parse_graphml', 'GraphMLWriter', 'GraphMLReader']
+
+
+@open_file(1, mode='wb')
+def write_graphml_xml(G, path, encoding='utf-8', prettyprint=True,
+                      infer_numeric_types=False):
     """Write G in GraphML XML format to path
 
     Parameters
     ----------
     G : graph
        A networkx graph
-    infer_numeric_types : boolean
-       Determine if numeric types should be generalized despite different python values.
-       For example, if edges have both int and float 'weight' attributes, it will be
-       inferred in GraphML that they are both floats (which translates to double in GraphML).
     path : file or string
        File or filename to write.
        Filenames ending in .gz or .bz2 will be compressed.
@@ -72,38 +85,87 @@ def write_graphml(G, path, encoding='utf-8', prettyprint=True, infer_numeric_typ
        Encoding for text data.
     prettyprint : bool (optional)
        If True use line breaks and indenting in output XML.
+    infer_numeric_types : boolean
+       Determine if numeric types should be generalized.
+       For example, if edges have both int and float 'weight' attributes,
+       we infer in GraphML that both are floats.
 
     Examples
     --------
-    >>> G=nx.path_graph(4)
+    >>> G = nx.path_graph(4)
     >>> nx.write_graphml(G, "test.graphml")
 
     Notes
     -----
-    This implementation does not support mixed graphs (directed and unidirected
-    edges together) hyperedges, nested graphs, or ports.
+    It may be a good idea in Python2 to convert strings to unicode
+    before giving the graph to write_gml. At least the strings with
+    either many characters to escape.
+
+    This implementation does not support mixed graphs (directed
+    and unidirected edges together) hyperedges, nested graphs, or ports.
     """
-    writer = GraphMLWriter(encoding=encoding,prettyprint=prettyprint,infer_numeric_types=infer_numeric_types)
+    writer = GraphMLWriter(encoding=encoding, prettyprint=prettyprint,
+                           infer_numeric_types=infer_numeric_types)
     writer.add_graph_element(G)
     writer.dump(path)
 
-def generate_graphml(G, encoding='utf-8',prettyprint=True):
-    """Generate GraphML lines for G
 
+@open_file(1, mode='wb')
+def write_graphml_lxml(G, path, encoding='utf-8', prettyprint=True,
+                       infer_numeric_types=False):
+    """Write G in GraphML XML format to path
+
+    This function uses the LXML framework and should be faster than
+    the version using the xml library.
     Parameters
     ----------
     G : graph
        A networkx graph
+    path : file or string
+       File or filename to write.
+       Filenames ending in .gz or .bz2 will be compressed.
     encoding : string (optional)
        Encoding for text data.
     prettyprint : bool (optional)
        If True use line breaks and indenting in output XML.
+    infer_numeric_types : boolean
+       Determine if numeric types should be generalized.
+       For example, if edges have both int and float 'weight' attributes,
+       we infer in GraphML that both are floats.
 
     Examples
     --------
     >>> G=nx.path_graph(4)
-    >>> linefeed=chr(10) # linefeed=\n
-    >>> s=linefeed.join(nx.generate_graphml(G))  # doctest: +SKIP
+    >>> nx.write_graphml_lxml(G, "fourpath.graphml") # doctest: +SKIP
+
+    Notes
+    -----
+    This implementation does not support mixed graphs (directed
+    and unidirected edges together) hyperedges, nested graphs, or ports.
+    """
+    writer = GraphMLWriterLxml(path, graph=G, encoding=encoding,
+                               prettyprint=prettyprint,
+                               infer_numeric_types=infer_numeric_types)
+    writer.dump()
+
+
+def generate_graphml(G, encoding='utf-8', prettyprint=True):
+    """Generate GraphML lines for G
+
+    Parameters
+    ----------
+    G : graph
+       A networkx graph
+    encoding : string (optional)
+       Encoding for text data.
+    prettyprint : bool (optional)
+       If True use line breaks and indenting in output XML.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> linefeed = chr(10)  # linefeed = \n
+    >>> s = linefeed.join(nx.generate_graphml(G))  # doctest: +SKIP
     >>> for line in nx.generate_graphml(G):  # doctest: +SKIP
     ...    print(line)
 
@@ -112,13 +174,14 @@ def generate_graphml(G, encoding='utf-8',prettyprint=True):
     This implementation does not support mixed graphs (directed and unidirected
     edges together) hyperedges, nested graphs, or ports.
     """
-    writer = GraphMLWriter(encoding=encoding,prettyprint=prettyprint)
+    writer = GraphMLWriter(encoding=encoding, prettyprint=prettyprint)
     writer.add_graph_element(G)
     for line in str(writer).splitlines():
         yield line
 
-@open_file(0,mode='rb')
-def read_graphml(path,node_type=str):
+
+@open_file(0, mode='rb')
+def read_graphml(path, node_type=str):
     """Read graph in GraphML format from path.
 
     Parameters
@@ -138,6 +201,19 @@ def read_graphml(path,node_type=str):
 
     Notes
     -----
+    Default node and edge attributes are not propagated to each node and edge.
+    They can be obtained from `G.graph` and applied to node and edge attributes
+    if desired using something like this:
+
+    >>> default_color = G.graph['node_default']['color']  # doctest: +SKIP
+    >>> for node, data in G.nodes(data=True):  # doctest: +SKIP
+    ...     if 'color' not in data:
+    ...         data['color']=default_color
+    >>> default_color = G.graph['edge_default']['color']  # doctest: +SKIP
+    >>> for u, v, data in G.edges(data=True):  # doctest: +SKIP
+    ...     if 'color' not in data:
+    ...         data['color']=default_color
+
     This implementation does not support mixed graphs (directed and unidirected
     edges together), hypergraphs, nested graphs, or ports.
 
@@ -155,11 +231,20 @@ def read_graphml(path,node_type=str):
     """
     reader = GraphMLReader(node_type=node_type)
     # need to check for multiple graphs
-    glist=list(reader(path=path))
+    glist = list(reader(path=path))
+    if len(glist) == 0:
+        # If no graph comes back, try looking for an incomplete header
+        header = b'<graphml xmlns="http://graphml.graphdrawing.org/xmlns">'
+        path.seek(0)
+        old_bytes = path.read()
+        new_bytes = old_bytes.replace(b'<graphml>', header)
+        glist = list(reader(string=new_bytes))
+        if len(glist) == 0:
+            raise nx.NetworkXError('file not successfully read as graphml')
     return glist[0]
 
 
-def parse_graphml(graphml_string,node_type=str):
+def parse_graphml(graphml_string, node_type=str):
     """Read graph in GraphML format from string.
 
     Parameters
@@ -179,13 +264,26 @@ def parse_graphml(graphml_string,node_type=str):
 
     Examples
     --------
-    >>> G=nx.path_graph(4)
-    >>> linefeed=chr(10) # linefeed=\n
-    >>> s=linefeed.join(nx.generate_graphml(G))
-    >>> H=nx.parse_graphml(s)
+    >>> G = nx.path_graph(4)
+    >>> linefeed = chr(10)  # linefeed = \n
+    >>> s = linefeed.join(nx.generate_graphml(G))
+    >>> H = nx.parse_graphml(s)
 
     Notes
     -----
+    Default node and edge attributes are not propagated to each node and edge.
+    They can be obtained from `G.graph` and applied to node and edge attributes
+    if desired using something like this:
+
+    >>> default_color = G.graph['node_default']['color']  # doctest: +SKIP
+    >>> for node, data in G.nodes(data=True):  # doctest: +SKIP
+    ...    if 'color' not in data:
+    ...        data['color']=default_color
+    >>> default_color = G.graph['edge_default']['color']  # doctest: +SKIP
+    >>> for u, v, data in G.edges(data=True):  # doctest: +SKIP
+    ...    if 'color' not in data:
+    ...        data['color']=default_color
+
     This implementation does not support mixed graphs (directed and unidirected
     edges together), hypergraphs, nested graphs, or ports.
 
@@ -197,14 +295,21 @@ def parse_graphml(graphml_string,node_type=str):
     """
     reader = GraphMLReader(node_type=node_type)
     # need to check for multiple graphs
-    glist=list(reader(string=graphml_string))
+    glist = list(reader(string=graphml_string))
+    if len(glist) == 0:
+        # If no graph comes back, try looking for an incomplete header
+        header = '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">'
+        new_string = graphml_string.replace('<graphml>', header)
+        glist = list(reader(string=new_string))
+        if len(glist) == 0:
+            raise nx.NetworkXError('file not successfully read as graphml')
     return glist[0]
 
 
 class GraphML(object):
     NS_GRAPHML = "http://graphml.graphdrawing.org/xmlns"
     NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
-    #xmlns:y="http://www.yworks.com/xml/graphml"
+    # xmlns:y="http://www.yworks.com/xml/graphml"
     NS_Y = "http://www.yworks.com/xml/graphml"
     SCHEMALOCATION = \
         ' '.join(['http://graphml.graphdrawing.org/xmlns',
@@ -218,19 +323,19 @@ class GraphML(object):
         # Python 2.x
         pass
 
-    types=[(int,"integer"), # for Gephi GraphML bug
-           (str,"yfiles"),(str,"string"), (unicode,"string"),
-           (int,"int"), (long,"long"),
-           (float,"float"), (float,"double"),
-           (bool, "boolean")]
+    types = [(int, "integer"),  # for Gephi GraphML bug
+             (str, "yfiles"), (str, "string"), (unicode, "string"),
+             (int, "int"), (long, "long"),
+             (float, "float"), (float, "double"),
+             (bool, "boolean")]
 
     xml_type = dict(types)
     python_type = dict(reversed(a) for a in types)
 
     # This page says that data types in GraphML follow Java(TM).
-    #   http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition
+    #  http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition
     # true and false are the only boolean literals:
-    #   http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals
+    #  http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals
     convert_bool = {
         # We use data.lower() in actual use.
         'true': True, 'false': False,
@@ -239,33 +344,35 @@ class GraphML(object):
         '1': True, 1: True
     }
 
+
 class GraphMLWriter(GraphML):
-    def __init__(self, graph=None, encoding="utf-8", prettyprint=True, infer_numeric_types=False):
+    def __init__(self, graph=None, encoding="utf-8", prettyprint=True,
+                 infer_numeric_types=False):
         try:
             import xml.etree.ElementTree
         except ImportError:
-             raise ImportError('GraphML writer requires '
-                               'xml.elementtree.ElementTree')
+            msg = 'GraphML writer requires xml.elementtree.ElementTree'
+            raise ImportError(msg)
+        self.myElement = Element
+
         self.infer_numeric_types = infer_numeric_types
-        self.prettyprint=prettyprint
+        self.prettyprint = prettyprint
         self.encoding = encoding
-        self.xml = Element("graphml",
-                           {'xmlns':self.NS_GRAPHML,
-                            'xmlns:xsi':self.NS_XSI,
-                            'xsi:schemaLocation':self.SCHEMALOCATION}
-                           )
-        self.keys={}
+        self.xml = self.myElement("graphml",
+                                  {'xmlns': self.NS_GRAPHML,
+                                   'xmlns:xsi': self.NS_XSI,
+                                   'xsi:schemaLocation': self.SCHEMALOCATION})
+        self.keys = {}
         self.attributes = defaultdict(list)
         self.attribute_types = defaultdict(set)
 
         if graph is not None:
             self.add_graph_element(graph)
 
-
     def __str__(self):
         if self.prettyprint:
             self.indent(self.xml)
-        s=tostring(self.xml).decode(self.encoding)
+        s = tostring(self.xml).decode(self.encoding)
         return s
 
     def attr_type(self, name, scope, value):
@@ -281,17 +388,23 @@ def attr_type(self, name, scope, value):
             types = self.attribute_types[(name, scope)]
 
             try:
-                chr(12345)     # Fails on Py!=3.
-                long = int     # Py3K's int is our long type
+                chr(12345)     # Fails on Py<3.
+                local_long = int     # Py3's int is Py2's long type
+                local_unicode = str  # Py3's str is Py2's unicode type
             except ValueError:
                 # Python 2.x
-                pass
+                local_long = long
+                local_unicode = unicode
 
             if len(types) > 1:
-                if float in types:
+                if str in types:
+                    return str
+                elif local_unicode in types:
+                    return local_unicode
+                elif float in types:
                     return float
-                elif long in types:
-                    return long
+                elif local_long in types:
+                    return local_long
                 else:
                     return int
             else:
@@ -306,20 +419,19 @@ def get_key(self, name, attr_type, scope, default):
         except KeyError:
             new_id = "d%i" % len(list(self.keys))
             self.keys[keys_key] = new_id
-            key_kwargs = {"id":new_id,
-                          "for":scope,
-                          "attr.name":name,
-                          "attr.type":attr_type}
-            key_element=Element("key",**key_kwargs)
+            key_kwargs = {"id": new_id,
+                          "for": scope,
+                          "attr.name": name,
+                          "attr.type": attr_type}
+            key_element = self.myElement("key", **key_kwargs)
             # add subelement for data default value if present
             if default is not None:
-                default_element=Element("default")
-                default_element.text=make_str(default)
+                default_element = self.myElement("default")
+                default_element.text = make_str(default)
                 key_element.append(default_element)
-            self.xml.insert(0,key_element)
+            self.xml.insert(0, key_element)
         return new_id
 
-
     def add_data(self, name, element_type, value,
                  scope="all",
                  default=None):
@@ -328,10 +440,10 @@ def add_data(self, name, element_type, value,
         type in the keys table.
         """
         if element_type not in self.xml_type:
-            raise nx.NetworkXError('GraphML writer does not support '
-                                   '%s as data values.'%element_type)
-        key_id = self.get_key(name, self.xml_type[element_type], scope, default)
-        data_element = Element("data", key=key_id)
+            msg = 'GraphML writer does not support %s as data values.'
+            raise nx.NetworkXError(msg % element_type)
+        keyid = self.get_key(name, self.xml_type[element_type], scope, default)
+        data_element = self.myElement("data", key=keyid)
         data_element.text = make_str(value)
         return data_element
 
@@ -339,32 +451,33 @@ def add_attributes(self, scope, xml_obj, data, default):
         """Appends attribute data to edges or nodes, and stores type information
         to be added later. See add_graph_element.
         """
-        for k,v in data.items():
+        for k, v in data.items():
             self.attribute_types[(make_str(k), scope)].add(type(v))
             self.attributes[xml_obj].append([k, v, scope, default.get(k)])
 
     def add_nodes(self, G, graph_element):
-        for node,data in G.nodes(data=True):
-            node_element = Element("node", id = make_str(node))
-            default=G.graph.get('node_default',{})
+        default = G.graph.get('node_default', {})
+        for node, data in G.nodes(data=True):
+            node_element = self.myElement("node", id=make_str(node))
             self.add_attributes("node", node_element, data, default)
             graph_element.append(node_element)
 
     def add_edges(self, G, graph_element):
         if G.is_multigraph():
-            for u,v,key,data in G.edges(data=True,keys=True):
-                edge_element = Element("edge",source=make_str(u),
-                                       target=make_str(v))
-                default=G.graph.get('edge_default',{})
+            for u, v, key, data in G.edges(data=True, keys=True):
+                edge_element = self.myElement("edge", source=make_str(u),
+                                              target=make_str(v),
+                                              id=make_str(key))
+                default = G.graph.get('edge_default', {})
                 self.add_attributes("edge", edge_element, data, default)
                 self.add_attributes("edge", edge_element,
-                                    {'key':key}, default)
+                                    {'key': key}, default)
                 graph_element.append(edge_element)
         else:
-            for u,v,data in G.edges(data=True):
-                edge_element = Element("edge",source=make_str(u),
-                                       target=make_str(v))
-                default=G.graph.get('edge_default',{})
+            for u, v, data in G.edges(data=True):
+                edge_element = self.myElement("edge", source=make_str(u),
+                                              target=make_str(v))
+                default = G.graph.get('edge_default', {})
                 self.add_attributes("edge", edge_element, data, default)
                 graph_element.append(edge_element)
 
@@ -373,42 +486,38 @@ def add_graph_element(self, G):
         Serialize graph G in GraphML to the stream.
         """
         if G.is_directed():
-            default_edge_type='directed'
+            default_edge_type = 'directed'
         else:
-            default_edge_type='undirected'
+            default_edge_type = 'undirected'
 
-        graphid=G.graph.pop('id',None)
+        graphid = G.graph.pop('id', None)
         if graphid is None:
-            graph_element = Element("graph",
-                                edgedefault = default_edge_type)
+            graph_element = self.myElement("graph",
+                                           edgedefault=default_edge_type)
         else:
-            graph_element = Element("graph",
-                                edgedefault = default_edge_type,
-                                id=graphid)
-
-        default={}
-        data=dict((k,v) for (k,v) in  G.graph.items()
-                  if k not in ['node_default','edge_default'])
+            graph_element = self.myElement("graph",
+                                           edgedefault=default_edge_type,
+                                           id=graphid)
+        default = {}
+        data = dict((k, v) for k, v in G.graph.items()
+                    if k not in ['node_default', 'edge_default'])
         self.add_attributes("graph", graph_element, data, default)
-        self.add_nodes(G,graph_element)
-        self.add_edges(G,graph_element)
+        self.add_nodes(G, graph_element)
+        self.add_edges(G, graph_element)
 
         # self.attributes contains a mapping from XML Objects to a list of
         # data that needs to be added to them.
-        # We postpone processing of this in order to do type inference/generalization.
+        # We postpone processing in order to do type inference/generalization.
         # See self.attr_type
         for (xml_obj, data) in self.attributes.items():
             for (k, v, scope, default) in data:
-                xml_obj.append(self.add_data(make_str(k), self.attr_type(k, scope, v), make_str(v),
-                                             scope, default))
-
+                xml_obj.append(self.add_data(make_str(k),
+                                             self.attr_type(k, scope, v),
+                                             make_str(v), scope, default))
         self.xml.append(graph_element)
 
-
     def add_graphs(self, graph_list):
-        """
-        Add many graphs to this GraphML document.
-        """
+        """ Add many graphs to this GraphML document. """
         for G in graph_list:
             self.add_graph_element(G)
 
@@ -420,7 +529,7 @@ def dump(self, stream):
 
     def indent(self, elem, level=0):
         # in-place prettyprint formatter
-        i = "\n" + level*"  "
+        i = "\n" + level * "  "
         if len(elem):
             if not elem.text or not elem.text.strip():
                 elem.text = i + "  "
@@ -435,51 +544,197 @@ def indent(self, elem, level=0):
                 elem.tail = i
 
 
-class GraphMLReader(GraphML):
-    """Read a GraphML document.  Produces NetworkX graph objects.
+class IncrementalElement(object):
+    """Wrapper for _IncrementalWriter providing an Element like interface.
+
+    This wrapper does not intend to be a complete implemenation but rather to
+    deal with those calls used in GraphMLWriter.
     """
+
+    def __init__(self, xml, prettyprint):
+        self.xml = xml
+        self.prettyprint = prettyprint
+
+    def append(self, element):
+        self.xml.write(element, pretty_print=self.prettyprint)
+
+
+class GraphMLWriterLxml(GraphMLWriter):
+    def __init__(self, path, graph=None, encoding='utf-8', prettyprint=True,
+                 infer_numeric_types=False):
+        self.myElement = lxmletree.Element
+
+        self._encoding = encoding
+        self._prettyprint = prettyprint
+        self.infer_numeric_types = infer_numeric_types
+
+        self._xml_base = lxmletree.xmlfile(path, encoding=encoding)
+        self._xml = self._xml_base.__enter__()
+        self._xml.write_declaration()
+
+        # We need to have a xml variable that support insertion. This call is
+        # used for adding the keys to the document.
+        # We will store those keys in a plain list, and then after the graph
+        # element is closed we will add them to the main graphml element.
+        self.xml = []
+        self._keys = self.xml
+        self._graphml = self._xml.element(
+            'graphml',
+            {
+                'xmlns': self.NS_GRAPHML,
+                'xmlns:xsi': self.NS_XSI,
+                'xsi:schemaLocation': self.SCHEMALOCATION
+            })
+        self._graphml.__enter__()
+        self.keys = {}
+        self.attribute_types = defaultdict(set)
+
+        if graph is not None:
+            self.add_graph_element(graph)
+
+    def add_graph_element(self, G):
+        """
+        Serialize graph G in GraphML to the stream.
+        """
+        if G.is_directed():
+            default_edge_type = 'directed'
+        else:
+            default_edge_type = 'undirected'
+
+        graphid = G.graph.pop('id', None)
+        if graphid is None:
+            graph_element = self._xml.element('graph',
+                                              edgedefault=default_edge_type)
+        else:
+            graph_element = self._xml.element('graph',
+                                              edgedefault=default_edge_type,
+                                              id=graphid)
+
+        # gather attributes types for the whole graph
+        # to find the most general numeric format needed.
+        # Then pass through attributes to create key_id for each.
+        graphdata = {k: v for k, v in G.graph.items()
+                     if k not in ('node_default', 'edge_default')}
+        node_default = G.graph.get('node_default', {})
+        edge_default = G.graph.get('edge_default', {})
+        # Graph attributes
+        for k, v in graphdata.items():
+            self.attribute_types[(make_str(k), "graph")].add(type(v))
+        for k, v in graphdata.items():
+            element_type = self.xml_type[self.attr_type(k, "graph", v)]
+            self.get_key(make_str(k), element_type, "graph", None)
+        # Nodes and data
+        attributes = {}
+        for node, d in G.nodes(data=True):
+            for k, v in d.items():
+                self.attribute_types[(make_str(k), "node")].add(type(v))
+                if k not in attributes:
+                    attributes[k] = v
+        for k, v in attributes.items():
+            T = self.xml_type[self.attr_type(k, "node", v)]
+            self.get_key(make_str(k), T, "node", node_default.get(k))
+        # Edges and data
+        if G.is_multigraph():
+            attributes = {}
+            for u, v, ekey, d in G.edges(keys=True, data=True):
+                self.attribute_types[("key", "edge")].add(type(ekey))
+                if "key" not in attributes:
+                    attributes["key"] = ekey
+                for k, v in d.items():
+                    self.attribute_types[(make_str(k), "edge")].add(type(v))
+                    if k not in attributes:
+                        attributes[k] = v
+            for k, v in attributes.items():
+                T = self.xml_type[self.attr_type(k, "edge", v)]
+                self.get_key(make_str(k), T, "edge", edge_default.get(k))
+        else:
+            attributes = {}
+            for u, v, d in G.edges(data=True):
+                for k, v in d.items():
+                    self.attribute_types[(make_str(k), "edge")].add(type(v))
+                    if k not in attributes:
+                        attributes[k] = v
+            for k, v in attributes.items():
+                T = self.xml_type[self.attr_type(k, "edge", v)]
+                self.get_key(make_str(k), T, "edge", edge_default.get(k))
+
+        # Now add attribute keys to the xml file
+        for key in self.xml:
+            self._xml.write(key, pretty_print=self._prettyprint)
+
+        # The incremental_writer writes each node/edge as it is created
+        incremental_writer = IncrementalElement(self._xml, self._prettyprint)
+        with graph_element:
+            self.add_attributes('graph', incremental_writer, graphdata, {})
+            self.add_nodes(G, incremental_writer)  # adds attributes too
+            self.add_edges(G, incremental_writer)  # adds attributes too
+
+    def add_attributes(self, scope, xml_obj, data, default):
+        """Appends attribute data."""
+        for k, v in data.items():
+            data_element = self.add_data(make_str(k),
+                                         self.attr_type(make_str(k), scope, v),
+                                         make_str(v), scope, default.get(k))
+            xml_obj.append(data_element)
+
+    def __str__(self):
+        return object.__str__(self)
+
+    def dump(self):
+        self._graphml.__exit__(None, None, None)
+        self._xml_base.__exit__(None, None, None)
+
+# Choose a writer function for default
+if lxmletree is None:
+    write_graphml = write_graphml_xml
+else:
+    write_graphml = write_graphml_lxml
+
+
+class GraphMLReader(GraphML):
+    """Read a GraphML document.  Produces NetworkX graph objects."""
     def __init__(self, node_type=str):
         try:
             import xml.etree.ElementTree
         except ImportError:
-             raise ImportError('GraphML reader requires '
-                               'xml.elementtree.ElementTree')
-        self.node_type=node_type
-        self.multigraph=False # assume multigraph and test for parallel edges
+            msg = 'GraphML reader requires xml.elementtree.ElementTree'
+            raise ImportError(msg)
+        self.node_type = node_type
+        self.multigraph = False  # assume multigraph and test for multiedges
 
     def __call__(self, path=None, string=None):
         if path is not None:
-          self.xml = ElementTree(file=path)
+            self.xml = ElementTree(file=path)
         elif string is not None:
-          self.xml = fromstring(string)
+            self.xml = fromstring(string)
         else:
-          raise ValueError("Must specify either 'path' or 'string' as kwarg.")
-        (keys,defaults) = self.find_graphml_keys(self.xml)
+            raise ValueError("Must specify either 'path' or 'string' as kwarg")
+        (keys, defaults) = self.find_graphml_keys(self.xml)
         for g in self.xml.findall("{%s}graph" % self.NS_GRAPHML):
             yield self.make_graph(g, keys, defaults)
 
     def make_graph(self, graph_xml, graphml_keys, defaults):
         # set default graph type
         edgedefault = graph_xml.get("edgedefault", None)
-        if edgedefault=='directed':
-            G=nx.MultiDiGraph()
+        if edgedefault == 'directed':
+            G = nx.MultiDiGraph()
         else:
-            G=nx.MultiGraph()
+            G = nx.MultiGraph()
         # set defaults for graph attributes
-        G.graph['node_default']={}
-        G.graph['edge_default']={}
-        for key_id,value in defaults.items():
-            key_for=graphml_keys[key_id]['for']
-            name=graphml_keys[key_id]['name']
-            python_type=graphml_keys[key_id]['type']
-            if key_for=='node':
-                G.graph['node_default'].update({name:python_type(value)})
-            if key_for=='edge':
-                G.graph['edge_default'].update({name:python_type(value)})
+        G.graph['node_default'] = {}
+        G.graph['edge_default'] = {}
+        for key_id, value in defaults.items():
+            key_for = graphml_keys[key_id]['for']
+            name = graphml_keys[key_id]['name']
+            python_type = graphml_keys[key_id]['type']
+            if key_for == 'node':
+                G.graph['node_default'].update({name: python_type(value)})
+            if key_for == 'edge':
+                G.graph['edge_default'].update({name: python_type(value)})
         # hyperedges are not supported
-        hyperedge=graph_xml.find("{%s}hyperedge" % self.NS_GRAPHML)
+        hyperedge = graph_xml.find("{%s}hyperedge" % self.NS_GRAPHML)
         if hyperedge is not None:
-            raise nx.NetworkXError("GraphML reader does not support hyperedges")
+            raise nx.NetworkXError("GraphML reader doesn't support hyperedges")
         # add nodes
         for node_xml in graph_xml.findall("{%s}node" % self.NS_GRAPHML):
             self.add_node(G, node_xml, graphml_keys)
@@ -503,7 +758,7 @@ def add_node(self, G, node_xml, graphml_keys):
         """Add a node to the graph.
         """
         # warn on finding unsupported ports tag
-        ports=node_xml.find("{%s}port" % self.NS_GRAPHML)
+        ports = node_xml.find("{%s}port" % self.NS_GRAPHML)
         if ports is not None:
             warnings.warn("GraphML port tag not supported.")
         # find the node by id and cast it to the appropriate type
@@ -516,18 +771,18 @@ def add_edge(self, G, edge_element, graphml_keys):
         """Add an edge to the graph.
         """
         # warn on finding unsupported ports tag
-        ports=edge_element.find("{%s}port" % self.NS_GRAPHML)
+        ports = edge_element.find("{%s}port" % self.NS_GRAPHML)
         if ports is not None:
             warnings.warn("GraphML port tag not supported.")
 
         # raise error if we find mixed directed and undirected edges
         directed = edge_element.get("directed")
-        if G.is_directed() and directed=='false':
-            raise nx.NetworkXError(\
-                "directed=false edge found in directed graph.")
-        if (not G.is_directed()) and directed=='true':
-            raise nx.NetworkXError(\
-                "directed=true edge found in undirected graph.")
+        if G.is_directed() and directed == 'false':
+            msg = "directed=false edge found in directed graph."
+            raise nx.NetworkXError(msg)
+        if (not G.is_directed()) and directed == 'true':
+            msg = "directed=true edge found in undirected graph."
+            raise nx.NetworkXError(msg)
 
         source = self.node_type(edge_element.get("source"))
         target = self.node_type(edge_element.get("target"))
@@ -537,14 +792,19 @@ def add_edge(self, G, edge_element, graphml_keys):
         # attribute is specified
         edge_id = edge_element.get("id")
         if edge_id:
-            data["id"] = edge_id
-        if G.has_edge(source,target):
+            if 'key' in data and make_str(data['key']) == edge_id:
+                # If id there equivalent to 'key' attribute, use key
+                edge_id = data.pop('key')
+            else:
+                data["id"] = edge_id
+        if G.has_edge(source, target):
             # mark this as a multigraph
-            self.multigraph=True
+            self.multigraph = True
         if edge_id is None:
             # no id specified, try using 'key' attribute as id
-            edge_id=data.pop('key',None)
-        G.add_edge(source, target, key=edge_id, **data)
+            edge_id = data.pop('key', None)
+        # Use add_edges_from to avoid error with add_edge when `'key' in data`
+        G.add_edges_from([(source, target, edge_id, data)])
 
     def decode_data_elements(self, graphml_keys, obj_xml):
         """Use the key information to decode the data XML if present."""
@@ -552,16 +812,17 @@ def decode_data_elements(self, graphml_keys, obj_xml):
         for data_element in obj_xml.findall("{%s}data" % self.NS_GRAPHML):
             key = data_element.get("key")
             try:
-                data_name=graphml_keys[key]['name']
-                data_type=graphml_keys[key]['type']
+                data_name = graphml_keys[key]['name']
+                data_type = graphml_keys[key]['type']
             except KeyError:
-                raise nx.NetworkXError("Bad GraphML data: no key %s"%key)
-            text=data_element.text
+                raise nx.NetworkXError("Bad GraphML data: no key %s" % key)
+            text = data_element.text
             # assume anything with subelements is a yfiles extension
-            if text is not None and len(list(data_element))==0:
+            if text is not None and len(list(data_element)) == 0:
                 if data_type == bool:
                     # Ignore cases.
-                    # http://docs.oracle.com/javase/6/docs/api/java/lang/Boolean.html#parseBoolean%28java.lang.String%29
+                    # http://docs.oracle.com/javase/6/docs/api/java/lang/
+                    # Boolean.html#parseBoolean%28java.lang.String%29
                     data[data_name] = self.convert_bool[text.lower()]
                 else:
                     data[data_name] = data_type(text)
@@ -569,23 +830,23 @@ def decode_data_elements(self, graphml_keys, obj_xml):
                 # Assume yfiles as subelements, try to extract node_label
                 node_label = None
                 for node_type in ['ShapeNode', 'SVGNode', 'ImageNode']:
-                    geometry = data_element.find("{%s}%s/{%s}Geometry" %
-                                (self.NS_Y, node_type, self.NS_Y))
+                    pref = "{%s}%s/{%s}" % (self.NS_Y, node_type, self.NS_Y)
+                    geometry = data_element.find("%sGeometry" % pref)
                     if geometry is not None:
                         data['x'] = geometry.get('x')
                         data['y'] = geometry.get('y')
                     if node_label is None:
-                        node_label = data_element.find("{%s}%s/{%s}NodeLabel" %
-                                (self.NS_Y, node_type, self.NS_Y))
+                        node_label = data_element.find("%sNodeLabel" % pref)
                 if node_label is not None:
                     data['label'] = node_label.text
 
                 # check all the diffrent types of edges avaivable in yEd.
-                for e in ['PolyLineEdge', 'SplineEdge', 'QuadCurveEdge', 'BezierEdge', 'ArcEdge']:
-                        edge_label = data_element.find("{%s}%s/{%s}EdgeLabel"%
-                                               (self.NS_Y, e, (self.NS_Y)))
-                        if edge_label is not None:
-                                break
+                for e in ['PolyLineEdge', 'SplineEdge', 'QuadCurveEdge',
+                          'BezierEdge', 'ArcEdge']:
+                    pref = "{%s}%s/{%s}" % (self.NS_Y, e, self.NS_Y)
+                    edge_label = data_element.find("%sEdgeLabel" % pref)
+                    if edge_label is not None:
+                        break
 
                 if edge_label is not None:
                     data['label'] = edge_label.text
@@ -598,26 +859,26 @@ def find_graphml_keys(self, graph_element):
         graphml_key_defaults = {}
         for k in graph_element.findall("{%s}key" % self.NS_GRAPHML):
             attr_id = k.get("id")
-            attr_type=k.get('attr.type')
-            attr_name=k.get("attr.name")
-            yfiles_type=k.get("yfiles.type")
+            attr_type = k.get('attr.type')
+            attr_name = k.get("attr.name")
+            yfiles_type = k.get("yfiles.type")
             if yfiles_type is not None:
                 attr_name = yfiles_type
                 attr_type = 'yfiles'
             if attr_type is None:
                 attr_type = "string"
-                warnings.warn("No key type for id %s. Using string"%attr_id)
+                warnings.warn("No key type for id %s. Using string" % attr_id)
             if attr_name is None:
-                raise nx.NetworkXError("Unknown key for id %s in file."%attr_id)
-            graphml_keys[attr_id] = {
-                "name":attr_name,
-                "type":self.python_type[attr_type],
-                "for":k.get("for")}
+                raise nx.NetworkXError("Unknown key for id %s." % attr_id)
+            graphml_keys[attr_id] = {"name": attr_name,
+                                     "type": self.python_type[attr_type],
+                                     "for": k.get("for")}
             # check for "default" subelement of key element
-            default=k.find("{%s}default" % self.NS_GRAPHML)
+            default = k.find("{%s}default" % self.NS_GRAPHML)
             if default is not None:
-                graphml_key_defaults[attr_id]=default.text
-        return graphml_keys,graphml_key_defaults
+                graphml_key_defaults[attr_id] = default.text
+        return graphml_keys, graphml_key_defaults
+
 
 # fixture for nose tests
 def setup_module(module):
@@ -627,6 +888,7 @@ def setup_module(module):
     except:
         raise SkipTest("xml.etree.ElementTree not available")
 
+
 # fixture for nose tests
 def teardown_module(module):
     import os


Test patch:
diff --git a/networkx/readwrite/tests/test_graphml.py b/networkx/readwrite/tests/test_graphml.py
--- a/networkx/readwrite/tests/test_graphml.py
+++ b/networkx/readwrite/tests/test_graphml.py
@@ -7,19 +7,15 @@
 import tempfile
 import os
 
-class TestGraph(object):
-    @classmethod
-    def setupClass(cls):
-        try:
-            import xml.etree.ElementTree
-        except ImportError:
-            raise SkipTest('xml.etree.ElementTree not available.')
 
+class BaseGraphML(object):
     def setUp(self):
-        self.simple_directed_data="""<?xml version="1.0" encoding="UTF-8"?>
+        self.simple_directed_data = """<?xml version="1.0" encoding="UTF-8"?>
 <!-- This file was written by the JAVA GraphML Library.-->
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <graph id="G" edgedefault="directed">
     <node id="n0"/>
     <node id="n1"/>
@@ -45,26 +41,24 @@ def setUp(self):
     <edge source="n8" target="n9"/>
   </graph>
 </graphml>"""
-        self.simple_directed_graph=nx.DiGraph()
+        self.simple_directed_graph = nx.DiGraph()
         self.simple_directed_graph.add_node('n10')
-        self.simple_directed_graph.add_edge('n0','n2',id='foo')
-        self.simple_directed_graph.add_edges_from([('n1','n2'),
-                                                   ('n2','n3'),
-                                                   ('n3','n5'),
-                                                   ('n3','n4'),
-                                                   ('n4','n6'),
-                                                   ('n6','n5'),
-                                                   ('n5','n7'),
-                                                   ('n6','n8'),
-                                                   ('n8','n7'),
-                                                   ('n8','n9'),
+        self.simple_directed_graph.add_edge('n0', 'n2', id='foo')
+        self.simple_directed_graph.add_edges_from([('n1', 'n2'),
+                                                   ('n2', 'n3'),
+                                                   ('n3', 'n5'),
+                                                   ('n3', 'n4'),
+                                                   ('n4', 'n6'),
+                                                   ('n6', 'n5'),
+                                                   ('n5', 'n7'),
+                                                   ('n6', 'n8'),
+                                                   ('n8', 'n7'),
+                                                   ('n8', 'n9'),
                                                    ])
-
         self.simple_directed_fh = \
             io.BytesIO(self.simple_directed_data.encode('UTF-8'))
 
-
-        self.attribute_data="""<?xml version="1.0" encoding="UTF-8"?>
+        self.attribute_data = """<?xml version="1.0" encoding="UTF-8"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
@@ -106,24 +100,27 @@ def setUp(self):
   </graph>
 </graphml>
 """
-        self.attribute_graph=nx.DiGraph(id='G')
-        self.attribute_graph.graph['node_default']={'color':'yellow'}
-        self.attribute_graph.add_node('n0',color='green')
-        self.attribute_graph.add_node('n2',color='blue')
-        self.attribute_graph.add_node('n3',color='red')
+        self.attribute_graph = nx.DiGraph(id='G')
+        self.attribute_graph.graph['node_default'] = {'color': 'yellow'}
+        self.attribute_graph.add_node('n0', color='green')
+        self.attribute_graph.add_node('n2', color='blue')
+        self.attribute_graph.add_node('n3', color='red')
         self.attribute_graph.add_node('n4')
-        self.attribute_graph.add_node('n5',color='turquoise')
-        self.attribute_graph.add_edge('n0','n2',id='e0',weight=1.0)
-        self.attribute_graph.add_edge('n0','n1',id='e1',weight=1.0)
-        self.attribute_graph.add_edge('n1','n3',id='e2',weight=2.0)
-        self.attribute_graph.add_edge('n3','n2',id='e3')
-        self.attribute_graph.add_edge('n2','n4',id='e4')
-        self.attribute_graph.add_edge('n3','n5',id='e5')
-        self.attribute_graph.add_edge('n5','n4',id='e6',weight=1.1)
+        self.attribute_graph.add_node('n5', color='turquoise')
+        self.attribute_graph.add_edge('n0', 'n2', id='e0', weight=1.0)
+        self.attribute_graph.add_edge('n0', 'n1', id='e1', weight=1.0)
+        self.attribute_graph.add_edge('n1', 'n3', id='e2', weight=2.0)
+        self.attribute_graph.add_edge('n3', 'n2', id='e3')
+        self.attribute_graph.add_edge('n2', 'n4', id='e4')
+        self.attribute_graph.add_edge('n3', 'n5', id='e5')
+        self.attribute_graph.add_edge('n5', 'n4', id='e6', weight=1.1)
         self.attribute_fh = io.BytesIO(self.attribute_data.encode('UTF-8'))
 
         self.attribute_numeric_type_data = """<?xml version='1.0' encoding='utf-8'?>
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <key attr.name="weight" attr.type="double" for="node" id="d1" />
   <key attr.name="weight" attr.type="double" for="edge" id="d0" />
   <graph edgedefault="directed">
@@ -136,23 +133,28 @@ def setUp(self):
     <edge source="n0" target="n1">
       <data key="d0">1</data>
     </edge>
+    <edge source="n1" target="n0">
+      <data key="d0">k</data>
+    </edge>
     <edge source="n1" target="n1">
       <data key="d0">1.0</data>
     </edge>
   </graph>
 </graphml>
 """
-
         self.attribute_numeric_type_graph = nx.DiGraph()
         self.attribute_numeric_type_graph.add_node('n0', weight=1)
         self.attribute_numeric_type_graph.add_node('n1', weight=2.0)
         self.attribute_numeric_type_graph.add_edge('n0', 'n1', weight=1)
         self.attribute_numeric_type_graph.add_edge('n1', 'n1', weight=1.0)
-        self.attribute_numeric_type_fh = io.BytesIO(self.attribute_numeric_type_data.encode('UTF-8'))
+        fh = io.BytesIO(self.attribute_numeric_type_data.encode('UTF-8'))
+        self.attribute_numeric_type_fh = fh
 
-        self.simple_undirected_data="""<?xml version="1.0" encoding="UTF-8"?>
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+        self.simple_undirected_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <graph id="G">
     <node id="n0"/>
     <node id="n1"/>
@@ -164,101 +166,66 @@ def setUp(self):
   </graph>
 </graphml>"""
 #    <edge source="n8" target="n10" directed="false"/>
-        self.simple_undirected_graph=nx.Graph()
+        self.simple_undirected_graph = nx.Graph()
         self.simple_undirected_graph.add_node('n10')
-        self.simple_undirected_graph.add_edge('n0','n2',id='foo')
-        self.simple_undirected_graph.add_edges_from([('n1','n2'),
-                                                   ('n2','n3'),
-                                                   ])
-
-        self.simple_undirected_fh = io.BytesIO(self.simple_undirected_data.encode('UTF-8'))
+        self.simple_undirected_graph.add_edge('n0', 'n2', id='foo')
+        self.simple_undirected_graph.add_edges_from([('n1', 'n2'),
+                                                     ('n2', 'n3'),
+                                                     ])
+        fh = io.BytesIO(self.simple_undirected_data.encode('UTF-8'))
+        self.simple_undirected_fh = fh
 
 
+class TestReadGraphML(BaseGraphML):
     def test_read_simple_directed_graphml(self):
-        G=self.simple_directed_graph
-        H=nx.read_graphml(self.simple_directed_fh)
-        assert_equal(sorted(G.nodes()),sorted(H.nodes()))
-        assert_equal(sorted(G.edges()),sorted(H.edges()))
+        G = self.simple_directed_graph
+        H = nx.read_graphml(self.simple_directed_fh)
+        assert_equal(sorted(G.nodes()), sorted(H.nodes()))
+        assert_equal(sorted(G.edges()), sorted(H.edges()))
         assert_equal(sorted(G.edges(data=True)),
                      sorted(H.edges(data=True)))
         self.simple_directed_fh.seek(0)
 
-        I=nx.parse_graphml(self.simple_directed_data)
-        assert_equal(sorted(G.nodes()),sorted(I.nodes()))
-        assert_equal(sorted(G.edges()),sorted(I.edges()))
+        I = nx.parse_graphml(self.simple_directed_data)
+        assert_equal(sorted(G.nodes()), sorted(I.nodes()))
+        assert_equal(sorted(G.edges()), sorted(I.edges()))
         assert_equal(sorted(G.edges(data=True)),
                      sorted(I.edges(data=True)))
 
-    def test_write_read_simple_directed_graphml(self):
-        G=self.simple_directed_graph
-        fh=io.BytesIO()
-        nx.write_graphml(G,fh)
-        fh.seek(0)
-        H=nx.read_graphml(fh)
-        assert_equal(sorted(G.nodes()),sorted(H.nodes()))
-        assert_equal(sorted(G.edges()),sorted(H.edges()))
-        assert_equal(sorted(G.edges(data=True)),
-                     sorted(H.edges(data=True)))
-        self.simple_directed_fh.seek(0)
-
     def test_read_simple_undirected_graphml(self):
-        G=self.simple_undirected_graph
-        H=nx.read_graphml(self.simple_undirected_fh)
+        G = self.simple_undirected_graph
+        H = nx.read_graphml(self.simple_undirected_fh)
         assert_nodes_equal(G.nodes(), H.nodes())
         assert_edges_equal(G.edges(), H.edges())
         self.simple_undirected_fh.seek(0)
 
-        I=nx.parse_graphml(self.simple_undirected_data)
+        I = nx.parse_graphml(self.simple_undirected_data)
         assert_nodes_equal(G.nodes(), I.nodes())
         assert_edges_equal(G.edges(), I.edges())
 
-    def test_write_read_attribute_numeric_type_graphml(self):
-        from xml.etree.ElementTree import parse
-
-        G = self.attribute_numeric_type_graph
-        fh = io.BytesIO()
-        nx.write_graphml(G, fh, infer_numeric_types=True)
-        fh.seek(0)
-        H = nx.read_graphml(fh)
-        fh.seek(0)
-
-        assert_nodes_equal(G.nodes(), H.nodes())
-        assert_edges_equal(G.edges(), H.edges())
-        assert_edges_equal(G.edges(data=True), H.edges(data=True))
-        self.attribute_numeric_type_fh.seek(0)
-
-        xml = parse(fh)
-        # Children are the key elements, and the graph element
-        children = xml.getroot().getchildren()
-        assert_equal(len(children), 3)
-
-        keys = [child.items() for child in children[:2]]
-
-        assert_equal(len(keys), 2)
-        assert_in(('attr.type', 'double'), keys[0])
-        assert_in(('attr.type', 'double'), keys[1])
-
     def test_read_attribute_graphml(self):
-        G=self.attribute_graph
-        H=nx.read_graphml(self.attribute_fh)
-        assert_nodes_equal(G.nodes(True),sorted(H.nodes(data=True)))
-        ge=sorted(G.edges(data=True))
-        he=sorted(H.edges(data=True))
-        for a,b in zip(ge,he):
-            assert_equal(a,b)
+        G = self.attribute_graph
+        H = nx.read_graphml(self.attribute_fh)
+        assert_nodes_equal(G.nodes(True), sorted(H.nodes(data=True)))
+        ge = sorted(G.edges(data=True))
+        he = sorted(H.edges(data=True))
+        for a, b in zip(ge, he):
+            assert_equal(a, b)
         self.attribute_fh.seek(0)
 
-        I=nx.parse_graphml(self.attribute_data)
-        assert_equal(sorted(G.nodes(True)),sorted(I.nodes(data=True)))
-        ge=sorted(G.edges(data=True))
-        he=sorted(I.edges(data=True))
-        for a,b in zip(ge,he):
-            assert_equal(a,b)
+        I = nx.parse_graphml(self.attribute_data)
+        assert_equal(sorted(G.nodes(True)), sorted(I.nodes(data=True)))
+        ge = sorted(G.edges(data=True))
+        he = sorted(I.edges(data=True))
+        for a, b in zip(ge, he):
+            assert_equal(a, b)
 
     def test_directed_edge_in_undirected(self):
-        s="""<?xml version="1.0" encoding="UTF-8"?>
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+        s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <graph id="G">
     <node id="n0"/>
     <node id="n1"/>
@@ -268,13 +235,15 @@ def test_directed_edge_in_undirected(self):
   </graph>
 </graphml>"""
         fh = io.BytesIO(s.encode('UTF-8'))
-        assert_raises(nx.NetworkXError,nx.read_graphml,fh)
-        assert_raises(nx.NetworkXError,nx.parse_graphml,s)
+        assert_raises(nx.NetworkXError, nx.read_graphml, fh)
+        assert_raises(nx.NetworkXError, nx.parse_graphml, s)
 
     def test_undirected_edge_in_directed(self):
-        s="""<?xml version="1.0" encoding="UTF-8"?>
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+        s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <graph id="G" edgedefault='directed'>
     <node id="n0"/>
     <node id="n1"/>
@@ -284,15 +253,15 @@ def test_undirected_edge_in_directed(self):
   </graph>
 </graphml>"""
         fh = io.BytesIO(s.encode('UTF-8'))
-        assert_raises(nx.NetworkXError,nx.read_graphml,fh)
-        assert_raises(nx.NetworkXError,nx.parse_graphml,s)
+        assert_raises(nx.NetworkXError, nx.read_graphml, fh)
+        assert_raises(nx.NetworkXError, nx.parse_graphml, s)
 
     def test_key_raise(self):
-        s="""<?xml version="1.0" encoding="UTF-8"?>
+        s = """<?xml version="1.0" encoding="UTF-8"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
-      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-      xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
-        http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <key id="d0" for="node" attr.name="color" attr.type="string">
     <default>yellow</default>
   </key>
@@ -312,15 +281,15 @@ def test_key_raise(self):
 </graphml>
 """
         fh = io.BytesIO(s.encode('UTF-8'))
-        assert_raises(nx.NetworkXError,nx.read_graphml,fh)
-        assert_raises(nx.NetworkXError,nx.parse_graphml,s)
+        assert_raises(nx.NetworkXError, nx.read_graphml, fh)
+        assert_raises(nx.NetworkXError, nx.parse_graphml, s)
 
     def test_hyperedge_raise(self):
-        s="""<?xml version="1.0" encoding="UTF-8"?>
+        s = """<?xml version="1.0" encoding="UTF-8"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
-      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-      xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
-        http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <key id="d0" for="node" attr.name="color" attr.type="string">
     <default>yellow</default>
   </key>
@@ -342,37 +311,40 @@ def test_hyperedge_raise(self):
 </graphml>
 """
         fh = io.BytesIO(s.encode('UTF-8'))
-        assert_raises(nx.NetworkXError,nx.read_graphml,fh)
-        assert_raises(nx.NetworkXError,nx.parse_graphml,s)
-
-    # remove test until we get the "name" issue sorted
-    # https://networkx.lanl.gov/trac/ticket/544
-    def test_default_attribute(self):
-        G=nx.Graph()
-        G.add_node(1,label=1,color='green')
-        nx.add_path(G, [0, 1, 2, 3])
-        G.add_edge(1,2,weight=3)
-        G.graph['node_default']={'color':'yellow'}
-        G.graph['edge_default']={'weight':7}
-        fh = io.BytesIO()
-        nx.write_graphml(G,fh)
-        fh.seek(0)
-        H=nx.read_graphml(fh,node_type=int)
-        assert_nodes_equal(G.nodes(), H.nodes())
-        assert_edges_equal(G.edges(), H.edges())
-        assert_equal(G.graph,H.graph)
+        assert_raises(nx.NetworkXError, nx.read_graphml, fh)
+        assert_raises(nx.NetworkXError, nx.parse_graphml, s)
 
     def test_multigraph_keys(self):
-        # test that multigraphs use edge id attributes as key
-        pass
-
-    def test_multigraph_to_graph(self):
-        # test converting multigraph to graph if no parallel edges are found
-        pass
+        # Test that reading multigraphs uses edge id attributes as keys
+        s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+  <graph id="G" edgedefault="directed">
+    <node id="n0"/>
+    <node id="n1"/>
+    <edge id="e0" source="n0" target="n1"/>
+    <edge id="e1" source="n0" target="n1"/>
+  </graph>
+</graphml>
+"""
+        fh = io.BytesIO(s.encode('UTF-8'))
+        G = nx.read_graphml(fh)
+        expected = [("n0", "n1", "e0"), ("n0", "n1", "e1")]
+        assert_equal(sorted(G.edges(keys=True)), expected)
+        fh.seek(0)
+        H = nx.parse_graphml(s)
+        assert_equal(sorted(H.edges(keys=True)), expected)
 
     def test_yfiles_extension(self):
-        data="""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
+        data = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns:y="http://www.yworks.com/xml/graphml"
+         xmlns:yed="http://www.yworks.com/xml/yed/3"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <!--Created by yFiles for Java 2.7-->
   <key for="graphml" id="d0" yfiles.type="resources"/>
   <key attr.name="url" attr.type="string" for="node" id="d1"/>
@@ -391,7 +363,12 @@ def test_yfiles_extension(self):
           <y:Geometry height="30.0" width="30.0" x="125.0" y="100.0"/>
           <y:Fill color="#FFCC00" transparent="false"/>
           <y:BorderStyle color="#000000" type="line" width="1.0"/>
-          <y:NodeLabel alignment="center" autoSizePolicy="content" borderDistance="0.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" modelName="internal" modelPosition="c" textColor="#000000" visible="true" width="12.27099609375" x="8.864501953125" y="5.43359375">1</y:NodeLabel>
+          <y:NodeLabel alignment="center" autoSizePolicy="content"
+           borderDistance="0.0" fontFamily="Dialog" fontSize="13"
+           fontStyle="plain" hasBackgroundColor="false" hasLineColor="false"
+           height="19.1328125" modelName="internal" modelPosition="c"
+           textColor="#000000" visible="true" width="12.27099609375"
+           x="8.864501953125" y="5.43359375">1</y:NodeLabel>
           <y:Shape type="rectangle"/>
         </y:ShapeNode>
       </data>
@@ -402,7 +379,12 @@ def test_yfiles_extension(self):
           <y:Geometry height="30.0" width="30.0" x="183.0" y="205.0"/>
           <y:Fill color="#FFCC00" transparent="false"/>
           <y:BorderStyle color="#000000" type="line" width="1.0"/>
-          <y:NodeLabel alignment="center" autoSizePolicy="content" borderDistance="0.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" modelName="internal" modelPosition="c" textColor="#000000" visible="true" width="12.27099609375" x="8.864501953125" y="5.43359375">2</y:NodeLabel>
+          <y:NodeLabel alignment="center" autoSizePolicy="content"
+          borderDistance="0.0" fontFamily="Dialog" fontSize="13"
+          fontStyle="plain" hasBackgroundColor="false" hasLineColor="false"
+          height="19.1328125" modelName="internal" modelPosition="c"
+          textColor="#000000" visible="true" width="12.27099609375"
+          x="8.864501953125" y="5.43359375">2</y:NodeLabel>
           <y:Shape type="rectangle"/>
         </y:ShapeNode>
       </data>
@@ -424,43 +406,24 @@ def test_yfiles_extension(self):
 </graphml>
 """
         fh = io.BytesIO(data.encode('UTF-8'))
-        G=nx.read_graphml(fh)
-        assert_equal(list(G.edges()),[('n0','n1')])
-        assert_equal(G['n0']['n1']['id'],'e0')
-        assert_equal(G.node['n0']['label'],'1')
-        assert_equal(G.node['n1']['label'],'2')
-
-        H=nx.parse_graphml(data)
-        assert_equal(list(H.edges()),[('n0','n1')])
-        assert_equal(H['n0']['n1']['id'],'e0')
-        assert_equal(H.node['n0']['label'],'1')
-        assert_equal(H.node['n1']['label'],'2')
-
-    def test_unicode(self):
-        G = nx.Graph()
-        try: # Python 3.x
-            name1 = chr(2344) + chr(123) + chr(6543)
-            name2 = chr(5543) + chr(1543) + chr(324)
-            node_type=str
-        except ValueError: # Python 2.6+
-            name1 = unichr(2344) + unichr(123) + unichr(6543)
-            name2 = unichr(5543) + unichr(1543) + unichr(324)
-            node_type=unicode
-        G.add_edge(name1, 'Radiohead', foo=name2)
-        fd, fname = tempfile.mkstemp()
-        nx.write_graphml(G, fname)
-        H = nx.read_graphml(fname,node_type=node_type)
-        assert_equal(G.adj, H.adj)
-        os.close(fd)
-        os.unlink(fname)
+        G = nx.read_graphml(fh)
+        assert_equal(list(G.edges()), [('n0', 'n1')])
+        assert_equal(G['n0']['n1']['id'], 'e0')
+        assert_equal(G.node['n0']['label'], '1')
+        assert_equal(G.node['n1']['label'], '2')
 
+        H = nx.parse_graphml(data)
+        assert_equal(list(H.edges()), [('n0', 'n1')])
+        assert_equal(H['n0']['n1']['id'], 'e0')
+        assert_equal(H.node['n0']['label'], '1')
+        assert_equal(H.node['n1']['label'], '2')
 
     def test_bool(self):
         s = """<?xml version="1.0" encoding="UTF-8"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
-      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-      xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
-        http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
   <key id="d0" for="node" attr.name="test" attr.type="boolean">
     <default>false</default>
   </key>
@@ -497,3 +460,199 @@ def test_bool(self):
             assert_equal(graph.node['n4']['test'], True)
             assert_equal(graph.node['n5']['test'], False)
             assert_equal(graph.node['n6']['test'], True)
+
+    def test_graphml_header_line(self):
+        good = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+  <key id="d0" for="node" attr.name="test" attr.type="boolean">
+    <default>false</default>
+  </key>
+  <graph id="G">
+    <node id="n0">
+      <data key="d0">true</data>
+    </node>
+  </graph>
+</graphml>
+"""
+        bad = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml>
+  <key id="d0" for="node" attr.name="test" attr.type="boolean">
+    <default>false</default>
+  </key>
+  <graph id="G">
+    <node id="n0">
+      <data key="d0">true</data>
+    </node>
+  </graph>
+</graphml>
+"""
+        ugly = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="https://ghghgh">
+  <key id="d0" for="node" attr.name="test" attr.type="boolean">
+    <default>false</default>
+  </key>
+  <graph id="G">
+    <node id="n0">
+      <data key="d0">true</data>
+    </node>
+  </graph>
+</graphml>
+"""
+        for s in (good, bad):
+            fh = io.BytesIO(s.encode('UTF-8'))
+            G = nx.read_graphml(fh)
+            H = nx.parse_graphml(s)
+            for graph in [G, H]:
+                assert_equal(graph.node['n0']['test'], True)
+
+        fh = io.BytesIO(ugly.encode('UTF-8'))
+        assert_raises(nx.NetworkXError, nx.read_graphml, fh)
+        assert_raises(nx.NetworkXError, nx.parse_graphml, ugly)
+
+
+class TestWriteGraphML(BaseGraphML):
+    writer = staticmethod(nx.write_graphml_lxml)
+
+    @classmethod
+    def setupClass(cls):
+        try:
+            import lxml.etree
+        except ImportError:
+            raise SkipTest('lxml.etree not available.')
+
+    def test_write_interface(self):
+        try:
+            import lxml.etree
+            assert_equal(nx.write_graphml, nx.write_graphml_lxml)
+        except ImportError:
+            assert_equal(nx.write_graphml, nx.write_graphml_xml)
+
+    def test_write_read_simple_directed_graphml(self):
+        G = self.simple_directed_graph
+        G.graph['hi'] = 'there'
+        fh = io.BytesIO()
+        self.writer(G, fh)
+        fh.seek(0)
+        H = nx.read_graphml(fh)
+        assert_equal(sorted(G.nodes()), sorted(H.nodes()))
+        assert_equal(sorted(G.edges()), sorted(H.edges()))
+        assert_equal(sorted(G.edges(data=True)), sorted(H.edges(data=True)))
+        self.simple_directed_fh.seek(0)
+
+    def test_write_read_attribute_numeric_type_graphml(self):
+        from xml.etree.ElementTree import parse
+
+        G = self.attribute_numeric_type_graph
+        fh = io.BytesIO()
+        self.writer(G, fh, infer_numeric_types=True)
+        fh.seek(0)
+        H = nx.read_graphml(fh)
+        fh.seek(0)
+
+        assert_nodes_equal(G.nodes(), H.nodes())
+        assert_edges_equal(G.edges(), H.edges())
+        assert_edges_equal(G.edges(data=True), H.edges(data=True))
+        self.attribute_numeric_type_fh.seek(0)
+
+        xml = parse(fh)
+        # Children are the key elements, and the graph element
+        children = xml.getroot().getchildren()
+        assert_equal(len(children), 3)
+
+        keys = [child.items() for child in children[:2]]
+
+        assert_equal(len(keys), 2)
+        assert_in(('attr.type', 'double'), keys[0])
+        assert_in(('attr.type', 'double'), keys[1])
+
+    def test_more_multigraph_keys(self):
+        """Writing keys as edge id attributes means keys become strings.
+        The original keys are stored as data, so read them back in
+        if `make_str(key) == edge_id`
+        This allows the adjacency to remain the same.
+        """
+        G = nx.MultiGraph()
+        G.add_edges_from([('a', 'b', 2), ('a', 'b', 3)])
+        fd, fname = tempfile.mkstemp()
+        self.writer(G, fname)
+        H = nx.read_graphml(fname)
+        assert_true(H.is_multigraph())
+        assert_edges_equal(G.edges(keys=True), H.edges(keys=True))
+        assert_equal(G._adj, H._adj)
+        os.close(fd)
+        os.unlink(fname)
+
+    def test_default_attribute(self):
+        G = nx.Graph(name="Fred")
+        G.add_node(1, label=1, color='green')
+        nx.add_path(G, [0, 1, 2, 3])
+        G.add_edge(1, 2, weight=3)
+        G.graph['node_default'] = {'color': 'yellow'}
+        G.graph['edge_default'] = {'weight': 7}
+        fh = io.BytesIO()
+        self.writer(G, fh)
+        fh.seek(0)
+        H = nx.read_graphml(fh, node_type=int)
+        assert_nodes_equal(G.nodes(), H.nodes())
+        assert_edges_equal(G.edges(), H.edges())
+        assert_equal(G.graph, H.graph)
+
+    def test_multigraph_to_graph(self):
+        # test converting multigraph to graph if no parallel edges found
+        G = nx.MultiGraph()
+        G.add_edges_from([('a', 'b', 2), ('b', 'c', 3)])  # no multiedges
+        fd, fname = tempfile.mkstemp()
+        self.writer(G, fname)
+        H = nx.read_graphml(fname)
+        assert_false(H.is_multigraph())
+        os.close(fd)
+        os.unlink(fname)
+
+    def test_unicode_attributes(self):
+        G = nx.Graph()
+        try:  # Python 3.x
+            name1 = chr(2344) + chr(123) + chr(6543)
+            name2 = chr(5543) + chr(1543) + chr(324)
+            node_type = str
+        except ValueError:  # Python 2.6+
+            name1 = unichr(2344) + unichr(123) + unichr(6543)
+            name2 = unichr(5543) + unichr(1543) + unichr(324)
+            node_type = unicode
+        G.add_edge(name1, 'Radiohead', foo=name2)
+        fd, fname = tempfile.mkstemp()
+        self.writer(G, fname)
+        H = nx.read_graphml(fname, node_type=node_type)
+        assert_equal(G._adj, H._adj)
+        os.close(fd)
+        os.unlink(fname)
+
+    def test_unicode_escape(self):
+        # test for handling json escaped stings in python 2 Issue #1880
+        import json
+        a = dict(a='{"a": "123"}')  # an object with many chars to escape
+        try:  # Python 3.x
+            chr(2344)
+            sa = json.dumps(a)
+        except ValueError:  # Python 2.6+
+            sa = unicode(json.dumps(a))
+        G = nx.Graph()
+        G.graph['test'] = sa
+        fh = io.BytesIO()
+        self.writer(G, fh)
+        fh.seek(0)
+        H = nx.read_graphml(fh)
+        assert_equal(G.graph['test'], H.graph['test'])
+
+
+class TestXMLGraphML(TestWriteGraphML):
+    writer = staticmethod(nx.write_graphml_xml)
+
+    @classmethod
+    def setupClass(cls):
+        try:
+            import xml.etree.ElementTree
+        except ImportError:
+            raise SkipTest('xml.etree.ElementTree not available.')