Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/networkx/readwrite/graphml.py @ 1:56ad4e20f292 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author | guerler |
---|---|
date | Fri, 31 Jul 2020 00:32:28 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
0:d30785e31577 | 1:56ad4e20f292 |
---|---|
1 # Copyright (C) 2008-2019 by | |
2 # Aric Hagberg <hagberg@lanl.gov> | |
3 # Dan Schult <dschult@colgate.edu> | |
4 # Pieter Swart <swart@lanl.gov> | |
5 # All rights reserved. | |
6 # BSD license. | |
7 # | |
8 # Authors: Salim Fadhley | |
9 # Aric Hagberg (hagberg@lanl.gov) | |
10 """ | |
11 ******* | |
12 GraphML | |
13 ******* | |
14 Read and write graphs in GraphML format. | |
15 | |
16 This implementation does not support mixed graphs (directed and unidirected | |
17 edges together), hyperedges, nested graphs, or ports. | |
18 | |
19 "GraphML is a comprehensive and easy-to-use file format for graphs. It | |
20 consists of a language core to describe the structural properties of a | |
21 graph and a flexible extension mechanism to add application-specific | |
22 data. Its main features include support of | |
23 | |
24 * directed, undirected, and mixed graphs, | |
25 * hypergraphs, | |
26 * hierarchical graphs, | |
27 * graphical representations, | |
28 * references to external data, | |
29 * application-specific attribute data, and | |
30 * light-weight parsers. | |
31 | |
32 Unlike many other file formats for graphs, GraphML does not use a | |
33 custom syntax. Instead, it is based on XML and hence ideally suited as | |
34 a common denominator for all kinds of services generating, archiving, | |
35 or processing graphs." | |
36 | |
37 http://graphml.graphdrawing.org/ | |
38 | |
39 Format | |
40 ------ | |
41 GraphML is an XML format. See | |
42 http://graphml.graphdrawing.org/specification.html for the specification and | |
43 http://graphml.graphdrawing.org/primer/graphml-primer.html | |
44 for examples. | |
45 """ | |
46 import warnings | |
47 from collections import defaultdict | |
48 | |
49 try: | |
50 from xml.etree.cElementTree import Element, ElementTree | |
51 from xml.etree.cElementTree import tostring, fromstring | |
52 except ImportError: | |
53 try: | |
54 from xml.etree.ElementTree import Element, ElementTree | |
55 from xml.etree.ElementTree import tostring, fromstring | |
56 except ImportError: | |
57 pass | |
58 | |
59 try: | |
60 import lxml.etree as lxmletree | |
61 except ImportError: | |
62 lxmletree = None | |
63 | |
64 import networkx as nx | |
65 from networkx.utils import open_file, make_str | |
66 | |
67 __all__ = ['write_graphml', 'read_graphml', 'generate_graphml', | |
68 'write_graphml_xml', 'write_graphml_lxml', | |
69 'parse_graphml', 'GraphMLWriter', 'GraphMLReader'] | |
70 | |
71 | |
72 @open_file(1, mode='wb') | |
73 def write_graphml_xml(G, path, encoding='utf-8', prettyprint=True, | |
74 infer_numeric_types=False): | |
75 """Write G in GraphML XML format to path | |
76 | |
77 Parameters | |
78 ---------- | |
79 G : graph | |
80 A networkx graph | |
81 path : file or string | |
82 File or filename to write. | |
83 Filenames ending in .gz or .bz2 will be compressed. | |
84 encoding : string (optional) | |
85 Encoding for text data. | |
86 prettyprint : bool (optional) | |
87 If True use line breaks and indenting in output XML. | |
88 infer_numeric_types : boolean | |
89 Determine if numeric types should be generalized. | |
90 For example, if edges have both int and float 'weight' attributes, | |
91 we infer in GraphML that both are floats. | |
92 | |
93 Examples | |
94 -------- | |
95 >>> G = nx.path_graph(4) | |
96 >>> nx.write_graphml(G, "test.graphml") | |
97 | |
98 Notes | |
99 ----- | |
100 It may be a good idea in Python2 to convert strings to unicode | |
101 before giving the graph to write_gml. At least the strings with | |
102 either many characters to escape. | |
103 | |
104 This implementation does not support mixed graphs (directed | |
105 and unidirected edges together) hyperedges, nested graphs, or ports. | |
106 """ | |
107 writer = GraphMLWriter(encoding=encoding, prettyprint=prettyprint, | |
108 infer_numeric_types=infer_numeric_types) | |
109 writer.add_graph_element(G) | |
110 writer.dump(path) | |
111 | |
112 | |
113 @open_file(1, mode='wb') | |
114 def write_graphml_lxml(G, path, encoding='utf-8', prettyprint=True, | |
115 infer_numeric_types=False): | |
116 """Write G in GraphML XML format to path | |
117 | |
118 This function uses the LXML framework and should be faster than | |
119 the version using the xml library. | |
120 | |
121 Parameters | |
122 ---------- | |
123 G : graph | |
124 A networkx graph | |
125 path : file or string | |
126 File or filename to write. | |
127 Filenames ending in .gz or .bz2 will be compressed. | |
128 encoding : string (optional) | |
129 Encoding for text data. | |
130 prettyprint : bool (optional) | |
131 If True use line breaks and indenting in output XML. | |
132 infer_numeric_types : boolean | |
133 Determine if numeric types should be generalized. | |
134 For example, if edges have both int and float 'weight' attributes, | |
135 we infer in GraphML that both are floats. | |
136 | |
137 Examples | |
138 -------- | |
139 >>> G = nx.path_graph(4) | |
140 >>> nx.write_graphml_lxml(G, "fourpath.graphml") # doctest: +SKIP | |
141 | |
142 Notes | |
143 ----- | |
144 This implementation does not support mixed graphs (directed | |
145 and unidirected edges together) hyperedges, nested graphs, or ports. | |
146 """ | |
147 writer = GraphMLWriterLxml(path, graph=G, encoding=encoding, | |
148 prettyprint=prettyprint, | |
149 infer_numeric_types=infer_numeric_types) | |
150 writer.dump() | |
151 | |
152 | |
153 def generate_graphml(G, encoding='utf-8', prettyprint=True): | |
154 """Generate GraphML lines for G | |
155 | |
156 Parameters | |
157 ---------- | |
158 G : graph | |
159 A networkx graph | |
160 encoding : string (optional) | |
161 Encoding for text data. | |
162 prettyprint : bool (optional) | |
163 If True use line breaks and indenting in output XML. | |
164 | |
165 Examples | |
166 -------- | |
167 >>> G = nx.path_graph(4) | |
168 >>> linefeed = chr(10) # linefeed = \n | |
169 >>> s = linefeed.join(nx.generate_graphml(G)) # doctest: +SKIP | |
170 >>> for line in nx.generate_graphml(G): # doctest: +SKIP | |
171 ... print(line) | |
172 | |
173 Notes | |
174 ----- | |
175 This implementation does not support mixed graphs (directed and unidirected | |
176 edges together) hyperedges, nested graphs, or ports. | |
177 """ | |
178 writer = GraphMLWriter(encoding=encoding, prettyprint=prettyprint) | |
179 writer.add_graph_element(G) | |
180 for line in str(writer).splitlines(): | |
181 yield line | |
182 | |
183 | |
184 @open_file(0, mode='rb') | |
185 def read_graphml(path, node_type=str, edge_key_type=int): | |
186 """Read graph in GraphML format from path. | |
187 | |
188 Parameters | |
189 ---------- | |
190 path : file or string | |
191 File or filename to write. | |
192 Filenames ending in .gz or .bz2 will be compressed. | |
193 | |
194 node_type: Python type (default: str) | |
195 Convert node ids to this type | |
196 | |
197 edge_key_type: Python type (default: int) | |
198 Convert graphml edge ids to this type as key of multi-edges | |
199 | |
200 | |
201 Returns | |
202 ------- | |
203 graph: NetworkX graph | |
204 If no parallel edges are found a Graph or DiGraph is returned. | |
205 Otherwise a MultiGraph or MultiDiGraph is returned. | |
206 | |
207 Notes | |
208 ----- | |
209 Default node and edge attributes are not propagated to each node and edge. | |
210 They can be obtained from `G.graph` and applied to node and edge attributes | |
211 if desired using something like this: | |
212 | |
213 >>> default_color = G.graph['node_default']['color'] # doctest: +SKIP | |
214 >>> for node, data in G.nodes(data=True): # doctest: +SKIP | |
215 ... if 'color' not in data: | |
216 ... data['color']=default_color | |
217 >>> default_color = G.graph['edge_default']['color'] # doctest: +SKIP | |
218 >>> for u, v, data in G.edges(data=True): # doctest: +SKIP | |
219 ... if 'color' not in data: | |
220 ... data['color']=default_color | |
221 | |
222 This implementation does not support mixed graphs (directed and unidirected | |
223 edges together), hypergraphs, nested graphs, or ports. | |
224 | |
225 For multigraphs the GraphML edge "id" will be used as the edge | |
226 key. If not specified then they "key" attribute will be used. If | |
227 there is no "key" attribute a default NetworkX multigraph edge key | |
228 will be provided. | |
229 | |
230 Files with the yEd "yfiles" extension will can be read but the graphics | |
231 information is discarded. | |
232 | |
233 yEd compressed files ("file.graphmlz" extension) can be read by renaming | |
234 the file to "file.graphml.gz". | |
235 | |
236 """ | |
237 reader = GraphMLReader(node_type=node_type, edge_key_type=edge_key_type) | |
238 # need to check for multiple graphs | |
239 glist = list(reader(path=path)) | |
240 if len(glist) == 0: | |
241 # If no graph comes back, try looking for an incomplete header | |
242 header = b'<graphml xmlns="http://graphml.graphdrawing.org/xmlns">' | |
243 path.seek(0) | |
244 old_bytes = path.read() | |
245 new_bytes = old_bytes.replace(b'<graphml>', header) | |
246 glist = list(reader(string=new_bytes)) | |
247 if len(glist) == 0: | |
248 raise nx.NetworkXError('file not successfully read as graphml') | |
249 return glist[0] | |
250 | |
251 | |
252 def parse_graphml(graphml_string, node_type=str): | |
253 """Read graph in GraphML format from string. | |
254 | |
255 Parameters | |
256 ---------- | |
257 graphml_string : string | |
258 String containing graphml information | |
259 (e.g., contents of a graphml file). | |
260 | |
261 node_type: Python type (default: str) | |
262 Convert node ids to this type | |
263 | |
264 Returns | |
265 ------- | |
266 graph: NetworkX graph | |
267 If no parallel edges are found a Graph or DiGraph is returned. | |
268 Otherwise a MultiGraph or MultiDiGraph is returned. | |
269 | |
270 Examples | |
271 -------- | |
272 >>> G = nx.path_graph(4) | |
273 >>> linefeed = chr(10) # linefeed = \n | |
274 >>> s = linefeed.join(nx.generate_graphml(G)) | |
275 >>> H = nx.parse_graphml(s) | |
276 | |
277 Notes | |
278 ----- | |
279 Default node and edge attributes are not propagated to each node and edge. | |
280 They can be obtained from `G.graph` and applied to node and edge attributes | |
281 if desired using something like this: | |
282 | |
283 >>> default_color = G.graph['node_default']['color'] # doctest: +SKIP | |
284 >>> for node, data in G.nodes(data=True): # doctest: +SKIP | |
285 ... if 'color' not in data: | |
286 ... data['color']=default_color | |
287 >>> default_color = G.graph['edge_default']['color'] # doctest: +SKIP | |
288 >>> for u, v, data in G.edges(data=True): # doctest: +SKIP | |
289 ... if 'color' not in data: | |
290 ... data['color']=default_color | |
291 | |
292 This implementation does not support mixed graphs (directed and unidirected | |
293 edges together), hypergraphs, nested graphs, or ports. | |
294 | |
295 For multigraphs the GraphML edge "id" will be used as the edge | |
296 key. If not specified then they "key" attribute will be used. If | |
297 there is no "key" attribute a default NetworkX multigraph edge key | |
298 will be provided. | |
299 | |
300 """ | |
301 reader = GraphMLReader(node_type=node_type) | |
302 # need to check for multiple graphs | |
303 glist = list(reader(string=graphml_string)) | |
304 if len(glist) == 0: | |
305 # If no graph comes back, try looking for an incomplete header | |
306 header = '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">' | |
307 new_string = graphml_string.replace('<graphml>', header) | |
308 glist = list(reader(string=new_string)) | |
309 if len(glist) == 0: | |
310 raise nx.NetworkXError('file not successfully read as graphml') | |
311 return glist[0] | |
312 | |
313 | |
314 class GraphML(object): | |
315 NS_GRAPHML = "http://graphml.graphdrawing.org/xmlns" | |
316 NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" | |
317 # xmlns:y="http://www.yworks.com/xml/graphml" | |
318 NS_Y = "http://www.yworks.com/xml/graphml" | |
319 SCHEMALOCATION = \ | |
320 ' '.join(['http://graphml.graphdrawing.org/xmlns', | |
321 'http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd']) | |
322 | |
323 try: | |
324 chr(12345) # Fails on Py!=3. | |
325 unicode = str # Py3k's str is our unicode type | |
326 long = int # Py3K's int is our long type | |
327 except ValueError: | |
328 # Python 2.x | |
329 pass | |
330 | |
331 types = [(int, "integer"), # for Gephi GraphML bug | |
332 (str, "yfiles"), (str, "string"), (unicode, "string"), | |
333 (int, "int"), (long, "long"), | |
334 (float, "float"), (float, "double"), | |
335 (bool, "boolean")] | |
336 | |
337 # These additions to types allow writing numpy types | |
338 try: | |
339 import numpy as np | |
340 except: | |
341 pass | |
342 else: | |
343 # prepend so that python types are created upon read (last entry wins) | |
344 types = [(np.float64, "float"), (np.float32, "float"), | |
345 (np.float16, "float"), (np.float_, "float"), | |
346 (np.int, "int"), (np.int8, "int"), | |
347 (np.int16, "int"), (np.int32, "int"), | |
348 (np.int64, "int"), (np.uint8, "int"), | |
349 (np.uint16, "int"), (np.uint32, "int"), | |
350 (np.uint64, "int"), (np.int_, "int"), | |
351 (np.intc, "int"), (np.intp, "int"), | |
352 ] + types | |
353 | |
354 xml_type = dict(types) | |
355 python_type = dict(reversed(a) for a in types) | |
356 | |
357 # This page says that data types in GraphML follow Java(TM). | |
358 # http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition | |
359 # true and false are the only boolean literals: | |
360 # http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals | |
361 convert_bool = { | |
362 # We use data.lower() in actual use. | |
363 'true': True, 'false': False, | |
364 # Include integer strings for convenience. | |
365 '0': False, 0: False, | |
366 '1': True, 1: True | |
367 } | |
368 | |
369 | |
370 class GraphMLWriter(GraphML): | |
371 def __init__(self, graph=None, encoding="utf-8", prettyprint=True, | |
372 infer_numeric_types=False): | |
373 try: | |
374 import xml.etree.ElementTree | |
375 except ImportError: | |
376 msg = 'GraphML writer requires xml.elementtree.ElementTree' | |
377 raise ImportError(msg) | |
378 self.myElement = Element | |
379 | |
380 self.infer_numeric_types = infer_numeric_types | |
381 self.prettyprint = prettyprint | |
382 self.encoding = encoding | |
383 self.xml = self.myElement("graphml", | |
384 {'xmlns': self.NS_GRAPHML, | |
385 'xmlns:xsi': self.NS_XSI, | |
386 'xsi:schemaLocation': self.SCHEMALOCATION}) | |
387 self.keys = {} | |
388 self.attributes = defaultdict(list) | |
389 self.attribute_types = defaultdict(set) | |
390 | |
391 if graph is not None: | |
392 self.add_graph_element(graph) | |
393 | |
394 def __str__(self): | |
395 if self.prettyprint: | |
396 self.indent(self.xml) | |
397 s = tostring(self.xml).decode(self.encoding) | |
398 return s | |
399 | |
400 def attr_type(self, name, scope, value): | |
401 """Infer the attribute type of data named name. Currently this only | |
402 supports inference of numeric types. | |
403 | |
404 If self.infer_numeric_types is false, type is used. Otherwise, pick the | |
405 most general of types found across all values with name and scope. This | |
406 means edges with data named 'weight' are treated separately from nodes | |
407 with data named 'weight'. | |
408 """ | |
409 if self.infer_numeric_types: | |
410 types = self.attribute_types[(name, scope)] | |
411 | |
412 try: | |
413 chr(12345) # Fails on Py<3. | |
414 local_long = int # Py3's int is Py2's long type | |
415 local_unicode = str # Py3's str is Py2's unicode type | |
416 except ValueError: | |
417 # Python 2.x | |
418 local_long = long | |
419 local_unicode = unicode | |
420 | |
421 if len(types) > 1: | |
422 if str in types: | |
423 return str | |
424 elif local_unicode in types: | |
425 return local_unicode | |
426 elif float in types: | |
427 return float | |
428 elif local_long in types: | |
429 return local_long | |
430 else: | |
431 return int | |
432 else: | |
433 return list(types)[0] | |
434 else: | |
435 return type(value) | |
436 | |
437 def get_key(self, name, attr_type, scope, default): | |
438 keys_key = (name, attr_type, scope) | |
439 try: | |
440 return self.keys[keys_key] | |
441 except KeyError: | |
442 new_id = "d%i" % len(list(self.keys)) | |
443 self.keys[keys_key] = new_id | |
444 key_kwargs = {"id": new_id, | |
445 "for": scope, | |
446 "attr.name": name, | |
447 "attr.type": attr_type} | |
448 key_element = self.myElement("key", **key_kwargs) | |
449 # add subelement for data default value if present | |
450 if default is not None: | |
451 default_element = self.myElement("default") | |
452 default_element.text = make_str(default) | |
453 key_element.append(default_element) | |
454 self.xml.insert(0, key_element) | |
455 return new_id | |
456 | |
457 def add_data(self, name, element_type, value, | |
458 scope="all", | |
459 default=None): | |
460 """ | |
461 Make a data element for an edge or a node. Keep a log of the | |
462 type in the keys table. | |
463 """ | |
464 if element_type not in self.xml_type: | |
465 msg = 'GraphML writer does not support %s as data values.' | |
466 raise nx.NetworkXError(msg % element_type) | |
467 keyid = self.get_key(name, self.xml_type[element_type], scope, default) | |
468 data_element = self.myElement("data", key=keyid) | |
469 data_element.text = make_str(value) | |
470 return data_element | |
471 | |
472 def add_attributes(self, scope, xml_obj, data, default): | |
473 """Appends attribute data to edges or nodes, and stores type information | |
474 to be added later. See add_graph_element. | |
475 """ | |
476 for k, v in data.items(): | |
477 self.attribute_types[(make_str(k), scope)].add(type(v)) | |
478 self.attributes[xml_obj].append([k, v, scope, default.get(k)]) | |
479 | |
480 def add_nodes(self, G, graph_element): | |
481 default = G.graph.get('node_default', {}) | |
482 for node, data in G.nodes(data=True): | |
483 node_element = self.myElement("node", id=make_str(node)) | |
484 self.add_attributes("node", node_element, data, default) | |
485 graph_element.append(node_element) | |
486 | |
487 def add_edges(self, G, graph_element): | |
488 if G.is_multigraph(): | |
489 for u, v, key, data in G.edges(data=True, keys=True): | |
490 edge_element = self.myElement("edge", source=make_str(u), | |
491 target=make_str(v), | |
492 id=make_str(key)) | |
493 default = G.graph.get('edge_default', {}) | |
494 self.add_attributes("edge", edge_element, data, default) | |
495 graph_element.append(edge_element) | |
496 else: | |
497 for u, v, data in G.edges(data=True): | |
498 edge_element = self.myElement("edge", source=make_str(u), | |
499 target=make_str(v)) | |
500 default = G.graph.get('edge_default', {}) | |
501 self.add_attributes("edge", edge_element, data, default) | |
502 graph_element.append(edge_element) | |
503 | |
504 def add_graph_element(self, G): | |
505 """ | |
506 Serialize graph G in GraphML to the stream. | |
507 """ | |
508 if G.is_directed(): | |
509 default_edge_type = 'directed' | |
510 else: | |
511 default_edge_type = 'undirected' | |
512 | |
513 graphid = G.graph.pop('id', None) | |
514 if graphid is None: | |
515 graph_element = self.myElement("graph", | |
516 edgedefault=default_edge_type) | |
517 else: | |
518 graph_element = self.myElement("graph", | |
519 edgedefault=default_edge_type, | |
520 id=graphid) | |
521 default = {} | |
522 data = {k: v for (k, v) in G.graph.items() | |
523 if k not in ['node_default', 'edge_default']} | |
524 self.add_attributes("graph", graph_element, data, default) | |
525 self.add_nodes(G, graph_element) | |
526 self.add_edges(G, graph_element) | |
527 | |
528 # self.attributes contains a mapping from XML Objects to a list of | |
529 # data that needs to be added to them. | |
530 # We postpone processing in order to do type inference/generalization. | |
531 # See self.attr_type | |
532 for (xml_obj, data) in self.attributes.items(): | |
533 for (k, v, scope, default) in data: | |
534 xml_obj.append(self.add_data(make_str(k), | |
535 self.attr_type(k, scope, v), | |
536 make_str(v), scope, default)) | |
537 self.xml.append(graph_element) | |
538 | |
539 def add_graphs(self, graph_list): | |
540 """ Add many graphs to this GraphML document. """ | |
541 for G in graph_list: | |
542 self.add_graph_element(G) | |
543 | |
544 def dump(self, stream): | |
545 if self.prettyprint: | |
546 self.indent(self.xml) | |
547 document = ElementTree(self.xml) | |
548 document.write(stream, encoding=self.encoding, xml_declaration=True) | |
549 | |
550 def indent(self, elem, level=0): | |
551 # in-place prettyprint formatter | |
552 i = "\n" + level * " " | |
553 if len(elem): | |
554 if not elem.text or not elem.text.strip(): | |
555 elem.text = i + " " | |
556 if not elem.tail or not elem.tail.strip(): | |
557 elem.tail = i | |
558 for elem in elem: | |
559 self.indent(elem, level + 1) | |
560 if not elem.tail or not elem.tail.strip(): | |
561 elem.tail = i | |
562 else: | |
563 if level and (not elem.tail or not elem.tail.strip()): | |
564 elem.tail = i | |
565 | |
566 | |
567 class IncrementalElement(object): | |
568 """Wrapper for _IncrementalWriter providing an Element like interface. | |
569 | |
570 This wrapper does not intend to be a complete implementation but rather to | |
571 deal with those calls used in GraphMLWriter. | |
572 """ | |
573 | |
574 def __init__(self, xml, prettyprint): | |
575 self.xml = xml | |
576 self.prettyprint = prettyprint | |
577 | |
578 def append(self, element): | |
579 self.xml.write(element, pretty_print=self.prettyprint) | |
580 | |
581 | |
582 class GraphMLWriterLxml(GraphMLWriter): | |
583 def __init__(self, path, graph=None, encoding='utf-8', prettyprint=True, | |
584 infer_numeric_types=False): | |
585 self.myElement = lxmletree.Element | |
586 | |
587 self._encoding = encoding | |
588 self._prettyprint = prettyprint | |
589 self.infer_numeric_types = infer_numeric_types | |
590 | |
591 self._xml_base = lxmletree.xmlfile(path, encoding=encoding) | |
592 self._xml = self._xml_base.__enter__() | |
593 self._xml.write_declaration() | |
594 | |
595 # We need to have a xml variable that support insertion. This call is | |
596 # used for adding the keys to the document. | |
597 # We will store those keys in a plain list, and then after the graph | |
598 # element is closed we will add them to the main graphml element. | |
599 self.xml = [] | |
600 self._keys = self.xml | |
601 self._graphml = self._xml.element( | |
602 'graphml', | |
603 { | |
604 'xmlns': self.NS_GRAPHML, | |
605 'xmlns:xsi': self.NS_XSI, | |
606 'xsi:schemaLocation': self.SCHEMALOCATION | |
607 }) | |
608 self._graphml.__enter__() | |
609 self.keys = {} | |
610 self.attribute_types = defaultdict(set) | |
611 | |
612 if graph is not None: | |
613 self.add_graph_element(graph) | |
614 | |
615 def add_graph_element(self, G): | |
616 """ | |
617 Serialize graph G in GraphML to the stream. | |
618 """ | |
619 if G.is_directed(): | |
620 default_edge_type = 'directed' | |
621 else: | |
622 default_edge_type = 'undirected' | |
623 | |
624 graphid = G.graph.pop('id', None) | |
625 if graphid is None: | |
626 graph_element = self._xml.element('graph', | |
627 edgedefault=default_edge_type) | |
628 else: | |
629 graph_element = self._xml.element('graph', | |
630 edgedefault=default_edge_type, | |
631 id=graphid) | |
632 | |
633 # gather attributes types for the whole graph | |
634 # to find the most general numeric format needed. | |
635 # Then pass through attributes to create key_id for each. | |
636 graphdata = {k: v for k, v in G.graph.items() | |
637 if k not in ('node_default', 'edge_default')} | |
638 node_default = G.graph.get('node_default', {}) | |
639 edge_default = G.graph.get('edge_default', {}) | |
640 # Graph attributes | |
641 for k, v in graphdata.items(): | |
642 self.attribute_types[(make_str(k), "graph")].add(type(v)) | |
643 for k, v in graphdata.items(): | |
644 element_type = self.xml_type[self.attr_type(k, "graph", v)] | |
645 self.get_key(make_str(k), element_type, "graph", None) | |
646 # Nodes and data | |
647 for node, d in G.nodes(data=True): | |
648 for k, v in d.items(): | |
649 self.attribute_types[(make_str(k), "node")].add(type(v)) | |
650 for node, d in G.nodes(data=True): | |
651 for k, v in d.items(): | |
652 T = self.xml_type[self.attr_type(k, "node", v)] | |
653 self.get_key(make_str(k), T, "node", node_default.get(k)) | |
654 # Edges and data | |
655 if G.is_multigraph(): | |
656 for u, v, ekey, d in G.edges(keys=True, data=True): | |
657 for k, v in d.items(): | |
658 self.attribute_types[(make_str(k), "edge")].add(type(v)) | |
659 for u, v, ekey, d in G.edges(keys=True, data=True): | |
660 for k, v in d.items(): | |
661 T = self.xml_type[self.attr_type(k, "edge", v)] | |
662 self.get_key(make_str(k), T, "edge", edge_default.get(k)) | |
663 else: | |
664 for u, v, d in G.edges(data=True): | |
665 for k, v in d.items(): | |
666 self.attribute_types[(make_str(k), "edge")].add(type(v)) | |
667 for u, v, d in G.edges(data=True): | |
668 for k, v in d.items(): | |
669 T = self.xml_type[self.attr_type(k, "edge", v)] | |
670 self.get_key(make_str(k), T, "edge", edge_default.get(k)) | |
671 | |
672 # Now add attribute keys to the xml file | |
673 for key in self.xml: | |
674 self._xml.write(key, pretty_print=self._prettyprint) | |
675 | |
676 # The incremental_writer writes each node/edge as it is created | |
677 incremental_writer = IncrementalElement(self._xml, self._prettyprint) | |
678 with graph_element: | |
679 self.add_attributes('graph', incremental_writer, graphdata, {}) | |
680 self.add_nodes(G, incremental_writer) # adds attributes too | |
681 self.add_edges(G, incremental_writer) # adds attributes too | |
682 | |
683 def add_attributes(self, scope, xml_obj, data, default): | |
684 """Appends attribute data.""" | |
685 for k, v in data.items(): | |
686 data_element = self.add_data(make_str(k), | |
687 self.attr_type(make_str(k), scope, v), | |
688 make_str(v), scope, default.get(k)) | |
689 xml_obj.append(data_element) | |
690 | |
691 def __str__(self): | |
692 return object.__str__(self) | |
693 | |
694 def dump(self): | |
695 self._graphml.__exit__(None, None, None) | |
696 self._xml_base.__exit__(None, None, None) | |
697 | |
698 | |
699 # Choose a writer function for default | |
700 if lxmletree is None: | |
701 write_graphml = write_graphml_xml | |
702 else: | |
703 write_graphml = write_graphml_lxml | |
704 | |
705 | |
706 class GraphMLReader(GraphML): | |
707 """Read a GraphML document. Produces NetworkX graph objects.""" | |
708 | |
709 def __init__(self, node_type=str, edge_key_type=int): | |
710 try: | |
711 import xml.etree.ElementTree | |
712 except ImportError: | |
713 msg = 'GraphML reader requires xml.elementtree.ElementTree' | |
714 raise ImportError(msg) | |
715 self.node_type = node_type | |
716 self.edge_key_type = edge_key_type | |
717 self.multigraph = False # assume multigraph and test for multiedges | |
718 self.edge_ids = {} # dict mapping (u,v) tuples to id edge attributes | |
719 | |
720 def __call__(self, path=None, string=None): | |
721 if path is not None: | |
722 self.xml = ElementTree(file=path) | |
723 elif string is not None: | |
724 self.xml = fromstring(string) | |
725 else: | |
726 raise ValueError("Must specify either 'path' or 'string' as kwarg") | |
727 (keys, defaults) = self.find_graphml_keys(self.xml) | |
728 for g in self.xml.findall("{%s}graph" % self.NS_GRAPHML): | |
729 yield self.make_graph(g, keys, defaults) | |
730 | |
731 def make_graph(self, graph_xml, graphml_keys, defaults, G=None): | |
732 # set default graph type | |
733 edgedefault = graph_xml.get("edgedefault", None) | |
734 if G is None: | |
735 if edgedefault == 'directed': | |
736 G = nx.MultiDiGraph() | |
737 else: | |
738 G = nx.MultiGraph() | |
739 # set defaults for graph attributes | |
740 G.graph['node_default'] = {} | |
741 G.graph['edge_default'] = {} | |
742 for key_id, value in defaults.items(): | |
743 key_for = graphml_keys[key_id]['for'] | |
744 name = graphml_keys[key_id]['name'] | |
745 python_type = graphml_keys[key_id]['type'] | |
746 if key_for == 'node': | |
747 G.graph['node_default'].update({name: python_type(value)}) | |
748 if key_for == 'edge': | |
749 G.graph['edge_default'].update({name: python_type(value)}) | |
750 # hyperedges are not supported | |
751 hyperedge = graph_xml.find("{%s}hyperedge" % self.NS_GRAPHML) | |
752 if hyperedge is not None: | |
753 raise nx.NetworkXError("GraphML reader doesn't support hyperedges") | |
754 # add nodes | |
755 for node_xml in graph_xml.findall("{%s}node" % self.NS_GRAPHML): | |
756 self.add_node(G, node_xml, graphml_keys, defaults) | |
757 # add edges | |
758 for edge_xml in graph_xml.findall("{%s}edge" % self.NS_GRAPHML): | |
759 self.add_edge(G, edge_xml, graphml_keys) | |
760 # add graph data | |
761 data = self.decode_data_elements(graphml_keys, graph_xml) | |
762 G.graph.update(data) | |
763 | |
764 # switch to Graph or DiGraph if no parallel edges were found. | |
765 if not self.multigraph: | |
766 if G.is_directed(): | |
767 G = nx.DiGraph(G) | |
768 else: | |
769 G = nx.Graph(G) | |
770 nx.set_edge_attributes(G, values=self.edge_ids, name='id') | |
771 | |
772 return G | |
773 | |
774 def add_node(self, G, node_xml, graphml_keys, defaults): | |
775 """Add a node to the graph. | |
776 """ | |
777 # warn on finding unsupported ports tag | |
778 ports = node_xml.find("{%s}port" % self.NS_GRAPHML) | |
779 if ports is not None: | |
780 warnings.warn("GraphML port tag not supported.") | |
781 # find the node by id and cast it to the appropriate type | |
782 node_id = self.node_type(node_xml.get("id")) | |
783 # get data/attributes for node | |
784 data = self.decode_data_elements(graphml_keys, node_xml) | |
785 G.add_node(node_id, **data) | |
786 # get child nodes | |
787 if node_xml.attrib.get('yfiles.foldertype') == 'group': | |
788 graph_xml = node_xml.find("{%s}graph" % self.NS_GRAPHML) | |
789 self.make_graph(graph_xml, graphml_keys, defaults, G) | |
790 | |
791 def add_edge(self, G, edge_element, graphml_keys): | |
792 """Add an edge to the graph. | |
793 """ | |
794 # warn on finding unsupported ports tag | |
795 ports = edge_element.find("{%s}port" % self.NS_GRAPHML) | |
796 if ports is not None: | |
797 warnings.warn("GraphML port tag not supported.") | |
798 | |
799 # raise error if we find mixed directed and undirected edges | |
800 directed = edge_element.get("directed") | |
801 if G.is_directed() and directed == 'false': | |
802 msg = "directed=false edge found in directed graph." | |
803 raise nx.NetworkXError(msg) | |
804 if (not G.is_directed()) and directed == 'true': | |
805 msg = "directed=true edge found in undirected graph." | |
806 raise nx.NetworkXError(msg) | |
807 | |
808 source = self.node_type(edge_element.get("source")) | |
809 target = self.node_type(edge_element.get("target")) | |
810 data = self.decode_data_elements(graphml_keys, edge_element) | |
811 # GraphML stores edge ids as an attribute | |
812 # NetworkX uses them as keys in multigraphs too if no key | |
813 # attribute is specified | |
814 edge_id = edge_element.get("id") | |
815 if edge_id: | |
816 # self.edge_ids is used by `make_graph` method for non-multigraphs | |
817 self.edge_ids[source, target] = edge_id | |
818 try: | |
819 edge_id = self.edge_key_type(edge_id) | |
820 except ValueError: # Could not convert. | |
821 pass | |
822 else: | |
823 edge_id = data.get('key') | |
824 | |
825 if G.has_edge(source, target): | |
826 # mark this as a multigraph | |
827 self.multigraph = True | |
828 | |
829 # Use add_edges_from to avoid error with add_edge when `'key' in data` | |
830 G.add_edges_from([(source, target, edge_id, data)]) | |
831 | |
832 def decode_data_elements(self, graphml_keys, obj_xml): | |
833 """Use the key information to decode the data XML if present.""" | |
834 data = {} | |
835 for data_element in obj_xml.findall("{%s}data" % self.NS_GRAPHML): | |
836 key = data_element.get("key") | |
837 try: | |
838 data_name = graphml_keys[key]['name'] | |
839 data_type = graphml_keys[key]['type'] | |
840 except KeyError: | |
841 raise nx.NetworkXError("Bad GraphML data: no key %s" % key) | |
842 text = data_element.text | |
843 # assume anything with subelements is a yfiles extension | |
844 if text is not None and len(list(data_element)) == 0: | |
845 if data_type == bool: | |
846 # Ignore cases. | |
847 # http://docs.oracle.com/javase/6/docs/api/java/lang/ | |
848 # Boolean.html#parseBoolean%28java.lang.String%29 | |
849 data[data_name] = self.convert_bool[text.lower()] | |
850 else: | |
851 data[data_name] = data_type(text) | |
852 elif len(list(data_element)) > 0: | |
853 # Assume yfiles as subelements, try to extract node_label | |
854 node_label = None | |
855 for node_type in ['ShapeNode', 'SVGNode', 'ImageNode']: | |
856 pref = "{%s}%s/{%s}" % (self.NS_Y, node_type, self.NS_Y) | |
857 geometry = data_element.find("%sGeometry" % pref) | |
858 if geometry is not None: | |
859 data['x'] = geometry.get('x') | |
860 data['y'] = geometry.get('y') | |
861 if node_label is None: | |
862 node_label = data_element.find("%sNodeLabel" % pref) | |
863 if node_label is not None: | |
864 data['label'] = node_label.text | |
865 | |
866 # check all the different types of edges avaivable in yEd. | |
867 for e in ['PolyLineEdge', 'SplineEdge', 'QuadCurveEdge', | |
868 'BezierEdge', 'ArcEdge']: | |
869 pref = "{%s}%s/{%s}" % (self.NS_Y, e, self.NS_Y) | |
870 edge_label = data_element.find("%sEdgeLabel" % pref) | |
871 if edge_label is not None: | |
872 break | |
873 | |
874 if edge_label is not None: | |
875 data['label'] = edge_label.text | |
876 return data | |
877 | |
878 def find_graphml_keys(self, graph_element): | |
879 """Extracts all the keys and key defaults from the xml. | |
880 """ | |
881 graphml_keys = {} | |
882 graphml_key_defaults = {} | |
883 for k in graph_element.findall("{%s}key" % self.NS_GRAPHML): | |
884 attr_id = k.get("id") | |
885 attr_type = k.get('attr.type') | |
886 attr_name = k.get("attr.name") | |
887 yfiles_type = k.get("yfiles.type") | |
888 if yfiles_type is not None: | |
889 attr_name = yfiles_type | |
890 attr_type = 'yfiles' | |
891 if attr_type is None: | |
892 attr_type = "string" | |
893 warnings.warn("No key type for id %s. Using string" % attr_id) | |
894 if attr_name is None: | |
895 raise nx.NetworkXError("Unknown key for id %s." % attr_id) | |
896 graphml_keys[attr_id] = {"name": attr_name, | |
897 "type": self.python_type[attr_type], | |
898 "for": k.get("for")} | |
899 # check for "default" subelement of key element | |
900 default = k.find("{%s}default" % self.NS_GRAPHML) | |
901 if default is not None: | |
902 graphml_key_defaults[attr_id] = default.text | |
903 return graphml_keys, graphml_key_defaults | |
904 | |
905 | |
906 # fixture for pytest | |
907 def setup_module(module): | |
908 import pytest | |
909 xml.etree.ElementTree = pytest.importorskip('xml.etree.ElementTree') | |
910 | |
911 | |
912 # fixture for pytest | |
913 def teardown_module(module): | |
914 import os | |
915 try: | |
916 os.unlink('test.graphml') | |
917 except: | |
918 pass |