Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/networkx/readwrite/gml.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """ | |
2 Read graphs in GML format. | |
3 | |
4 "GML, the Graph Modelling Language, is our proposal for a portable | |
5 file format for graphs. GML's key features are portability, simple | |
6 syntax, extensibility and flexibility. A GML file consists of a | |
7 hierarchical key-value lists. Graphs can be annotated with arbitrary | |
8 data structures. The idea for a common file format was born at the | |
9 GD'95; this proposal is the outcome of many discussions. GML is the | |
10 standard file format in the Graphlet graph editor system. It has been | |
11 overtaken and adapted by several other systems for drawing graphs." | |
12 | |
13 GML files are stored using a 7-bit ASCII encoding with any extended | |
14 ASCII characters (iso8859-1) appearing as HTML character entities. | |
15 You will need to give some thought into how the exported data should | |
16 interact with different languages and even different Python versions. | |
17 Re-importing from gml is also a concern. | |
18 | |
19 Without specifying a `stringizer`/`destringizer`, the code is capable of | |
20 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML | |
21 specification. For other data types, you need to explicitly supply a | |
22 `stringizer`/`destringizer`. | |
23 | |
24 For additional documentation on the GML file format, please see the | |
25 `GML website <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_. | |
26 | |
27 Several example graphs in GML format may be found on Mark Newman's | |
28 `Network data page <http://www-personal.umich.edu/~mejn/netdata/>`_. | |
29 """ | |
30 from io import StringIO | |
31 from ast import literal_eval | |
32 from collections import defaultdict | |
33 from enum import Enum | |
34 from typing import Any, NamedTuple | |
35 import networkx as nx | |
36 from networkx.exception import NetworkXError | |
37 from networkx.utils import open_file | |
38 | |
39 import warnings | |
40 import re | |
41 import html.entities as htmlentitydefs | |
42 | |
43 __all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"] | |
44 | |
45 | |
46 def escape(text): | |
47 """Use XML character references to escape characters. | |
48 | |
49 Use XML character references for unprintable or non-ASCII | |
50 characters, double quotes and ampersands in a string | |
51 """ | |
52 | |
53 def fixup(m): | |
54 ch = m.group(0) | |
55 return "&#" + str(ord(ch)) + ";" | |
56 | |
57 text = re.sub('[^ -~]|[&"]', fixup, text) | |
58 return text if isinstance(text, str) else str(text) | |
59 | |
60 | |
61 def unescape(text): | |
62 """Replace XML character references with the referenced characters""" | |
63 | |
64 def fixup(m): | |
65 text = m.group(0) | |
66 if text[1] == "#": | |
67 # Character reference | |
68 if text[2] == "x": | |
69 code = int(text[3:-1], 16) | |
70 else: | |
71 code = int(text[2:-1]) | |
72 else: | |
73 # Named entity | |
74 try: | |
75 code = htmlentitydefs.name2codepoint[text[1:-1]] | |
76 except KeyError: | |
77 return text # leave unchanged | |
78 try: | |
79 return chr(code) | |
80 except (ValueError, OverflowError): | |
81 return text # leave unchanged | |
82 | |
83 return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text) | |
84 | |
85 | |
86 def literal_destringizer(rep): | |
87 """Convert a Python literal to the value it represents. | |
88 | |
89 Parameters | |
90 ---------- | |
91 rep : string | |
92 A Python literal. | |
93 | |
94 Returns | |
95 ------- | |
96 value : object | |
97 The value of the Python literal. | |
98 | |
99 Raises | |
100 ------ | |
101 ValueError | |
102 If `rep` is not a Python literal. | |
103 """ | |
104 msg = "literal_destringizer is deprecated and will be removed in 3.0." | |
105 warnings.warn(msg, DeprecationWarning) | |
106 if isinstance(rep, str): | |
107 orig_rep = rep | |
108 try: | |
109 return literal_eval(rep) | |
110 except SyntaxError as e: | |
111 raise ValueError(f"{orig_rep!r} is not a valid Python literal") from e | |
112 else: | |
113 raise ValueError(f"{rep!r} is not a string") | |
114 | |
115 | |
116 @open_file(0, mode="rb") | |
117 def read_gml(path, label="label", destringizer=None): | |
118 """Read graph in GML format from `path`. | |
119 | |
120 Parameters | |
121 ---------- | |
122 path : filename or filehandle | |
123 The filename or filehandle to read from. | |
124 | |
125 label : string, optional | |
126 If not None, the parsed nodes will be renamed according to node | |
127 attributes indicated by `label`. Default value: 'label'. | |
128 | |
129 destringizer : callable, optional | |
130 A `destringizer` that recovers values stored as strings in GML. If it | |
131 cannot convert a string to a value, a `ValueError` is raised. Default | |
132 value : None. | |
133 | |
134 Returns | |
135 ------- | |
136 G : NetworkX graph | |
137 The parsed graph. | |
138 | |
139 Raises | |
140 ------ | |
141 NetworkXError | |
142 If the input cannot be parsed. | |
143 | |
144 See Also | |
145 -------- | |
146 write_gml, parse_gml | |
147 | |
148 Notes | |
149 ----- | |
150 GML files are stored using a 7-bit ASCII encoding with any extended | |
151 ASCII characters (iso8859-1) appearing as HTML character entities. | |
152 Without specifying a `stringizer`/`destringizer`, the code is capable of | |
153 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML | |
154 specification. For other data types, you need to explicitly supply a | |
155 `stringizer`/`destringizer`. | |
156 | |
157 For additional documentation on the GML file format, please see the | |
158 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_. | |
159 | |
160 See the module docstring :mod:`networkx.readwrite.gml` for more details. | |
161 | |
162 Examples | |
163 -------- | |
164 >>> G = nx.path_graph(4) | |
165 >>> nx.write_gml(G, "test.gml") | |
166 >>> H = nx.read_gml("test.gml") | |
167 """ | |
168 | |
169 def filter_lines(lines): | |
170 for line in lines: | |
171 try: | |
172 line = line.decode("ascii") | |
173 except UnicodeDecodeError as e: | |
174 raise NetworkXError("input is not ASCII-encoded") from e | |
175 if not isinstance(line, str): | |
176 lines = str(lines) | |
177 if line and line[-1] == "\n": | |
178 line = line[:-1] | |
179 yield line | |
180 | |
181 G = parse_gml_lines(filter_lines(path), label, destringizer) | |
182 return G | |
183 | |
184 | |
185 def parse_gml(lines, label="label", destringizer=None): | |
186 """Parse GML graph from a string or iterable. | |
187 | |
188 Parameters | |
189 ---------- | |
190 lines : string or iterable of strings | |
191 Data in GML format. | |
192 | |
193 label : string, optional | |
194 If not None, the parsed nodes will be renamed according to node | |
195 attributes indicated by `label`. Default value: 'label'. | |
196 | |
197 destringizer : callable, optional | |
198 A `destringizer` that recovers values stored as strings in GML. If it | |
199 cannot convert a string to a value, a `ValueError` is raised. Default | |
200 value : None. | |
201 | |
202 Returns | |
203 ------- | |
204 G : NetworkX graph | |
205 The parsed graph. | |
206 | |
207 Raises | |
208 ------ | |
209 NetworkXError | |
210 If the input cannot be parsed. | |
211 | |
212 See Also | |
213 -------- | |
214 write_gml, read_gml | |
215 | |
216 Notes | |
217 ----- | |
218 This stores nested GML attributes as dictionaries in the NetworkX graph, | |
219 node, and edge attribute structures. | |
220 | |
221 GML files are stored using a 7-bit ASCII encoding with any extended | |
222 ASCII characters (iso8859-1) appearing as HTML character entities. | |
223 Without specifying a `stringizer`/`destringizer`, the code is capable of | |
224 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML | |
225 specification. For other data types, you need to explicitly supply a | |
226 `stringizer`/`destringizer`. | |
227 | |
228 For additional documentation on the GML file format, please see the | |
229 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_. | |
230 | |
231 See the module docstring :mod:`networkx.readwrite.gml` for more details. | |
232 """ | |
233 | |
234 def decode_line(line): | |
235 if isinstance(line, bytes): | |
236 try: | |
237 line.decode("ascii") | |
238 except UnicodeDecodeError as e: | |
239 raise NetworkXError("input is not ASCII-encoded") from e | |
240 if not isinstance(line, str): | |
241 line = str(line) | |
242 return line | |
243 | |
244 def filter_lines(lines): | |
245 if isinstance(lines, str): | |
246 lines = decode_line(lines) | |
247 lines = lines.splitlines() | |
248 yield from lines | |
249 else: | |
250 for line in lines: | |
251 line = decode_line(line) | |
252 if line and line[-1] == "\n": | |
253 line = line[:-1] | |
254 if line.find("\n") != -1: | |
255 raise NetworkXError("input line contains newline") | |
256 yield line | |
257 | |
258 G = parse_gml_lines(filter_lines(lines), label, destringizer) | |
259 return G | |
260 | |
261 | |
262 class Pattern(Enum): | |
263 """ encodes the index of each token-matching pattern in `tokenize`. """ | |
264 | |
265 KEYS = 0 | |
266 REALS = 1 | |
267 INTS = 2 | |
268 STRINGS = 3 | |
269 DICT_START = 4 | |
270 DICT_END = 5 | |
271 COMMENT_WHITESPACE = 6 | |
272 | |
273 | |
274 class Token(NamedTuple): | |
275 category: Pattern | |
276 value: Any | |
277 line: int | |
278 position: int | |
279 | |
280 | |
281 LIST_START_VALUE = "_networkx_list_start" | |
282 | |
283 | |
284 def parse_gml_lines(lines, label, destringizer): | |
285 """Parse GML `lines` into a graph. | |
286 """ | |
287 | |
288 def tokenize(): | |
289 patterns = [ | |
290 r"[A-Za-z][0-9A-Za-z_]*\b", # keys | |
291 # reals | |
292 r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*)(?:[Ee][+-]?[0-9]+)?", | |
293 r"[+-]?[0-9]+", # ints | |
294 r'".*?"', # strings | |
295 r"\[", # dict start | |
296 r"\]", # dict end | |
297 r"#.*$|\s+", # comments and whitespaces | |
298 ] | |
299 tokens = re.compile("|".join(f"({pattern})" for pattern in patterns)) | |
300 lineno = 0 | |
301 for line in lines: | |
302 length = len(line) | |
303 pos = 0 | |
304 while pos < length: | |
305 match = tokens.match(line, pos) | |
306 if match is None: | |
307 m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})" | |
308 raise NetworkXError(m) | |
309 for i in range(len(patterns)): | |
310 group = match.group(i + 1) | |
311 if group is not None: | |
312 if i == 0: # keys | |
313 value = group.rstrip() | |
314 elif i == 1: # reals | |
315 value = float(group) | |
316 elif i == 2: # ints | |
317 value = int(group) | |
318 else: | |
319 value = group | |
320 if i != 6: # comments and whitespaces | |
321 yield Token(Pattern(i), value, lineno + 1, pos + 1) | |
322 pos += len(group) | |
323 break | |
324 lineno += 1 | |
325 yield Token(None, None, lineno + 1, 1) # EOF | |
326 | |
327 def unexpected(curr_token, expected): | |
328 category, value, lineno, pos = curr_token | |
329 value = repr(value) if value is not None else "EOF" | |
330 raise NetworkXError(f"expected {expected}, found {value} at ({lineno}, {pos})") | |
331 | |
332 def consume(curr_token, category, expected): | |
333 if curr_token.category == category: | |
334 return next(tokens) | |
335 unexpected(curr_token, expected) | |
336 | |
337 def parse_kv(curr_token): | |
338 dct = defaultdict(list) | |
339 while curr_token.category == Pattern.KEYS: | |
340 key = curr_token.value | |
341 curr_token = next(tokens) | |
342 category = curr_token.category | |
343 if category == Pattern.REALS or category == Pattern.INTS: | |
344 value = curr_token.value | |
345 curr_token = next(tokens) | |
346 elif category == Pattern.STRINGS: | |
347 value = unescape(curr_token.value[1:-1]) | |
348 if destringizer: | |
349 try: | |
350 value = destringizer(value) | |
351 except ValueError: | |
352 pass | |
353 curr_token = next(tokens) | |
354 elif category == Pattern.DICT_START: | |
355 curr_token, value = parse_dict(curr_token) | |
356 else: | |
357 # Allow for string convertible id and label values | |
358 if key in ("id", "label", "source", "target"): | |
359 try: | |
360 # String convert the token value | |
361 value = unescape(str(curr_token.value)) | |
362 if destringizer: | |
363 try: | |
364 value = destringizer(value) | |
365 except ValueError: | |
366 pass | |
367 curr_token = next(tokens) | |
368 except Exception: | |
369 msg = ( | |
370 "an int, float, string, '[' or string" | |
371 + " convertable ASCII value for node id or label" | |
372 ) | |
373 unexpected(curr_token, msg) | |
374 else: # Otherwise error out | |
375 unexpected(curr_token, "an int, float, string or '['") | |
376 dct[key].append(value) | |
377 | |
378 def clean_dict_value(value): | |
379 if not isinstance(value, list): | |
380 return value | |
381 if len(value) == 1: | |
382 return value[0] | |
383 if value[0] == LIST_START_VALUE: | |
384 return value[1:] | |
385 return value | |
386 | |
387 dct = {key: clean_dict_value(value) for key, value in dct.items()} | |
388 return curr_token, dct | |
389 | |
390 def parse_dict(curr_token): | |
391 # dict start | |
392 curr_token = consume(curr_token, Pattern.DICT_START, "'['") | |
393 # dict contents | |
394 curr_token, dct = parse_kv(curr_token) | |
395 # dict end | |
396 curr_token = consume(curr_token, Pattern.DICT_END, "']'") | |
397 return curr_token, dct | |
398 | |
399 def parse_graph(): | |
400 curr_token, dct = parse_kv(next(tokens)) | |
401 if curr_token.category is not None: # EOF | |
402 unexpected(curr_token, "EOF") | |
403 if "graph" not in dct: | |
404 raise NetworkXError("input contains no graph") | |
405 graph = dct["graph"] | |
406 if isinstance(graph, list): | |
407 raise NetworkXError("input contains more than one graph") | |
408 return graph | |
409 | |
410 tokens = tokenize() | |
411 graph = parse_graph() | |
412 | |
413 directed = graph.pop("directed", False) | |
414 multigraph = graph.pop("multigraph", False) | |
415 if not multigraph: | |
416 G = nx.DiGraph() if directed else nx.Graph() | |
417 else: | |
418 G = nx.MultiDiGraph() if directed else nx.MultiGraph() | |
419 graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")} | |
420 G.graph.update(graph_attr) | |
421 | |
422 def pop_attr(dct, category, attr, i): | |
423 try: | |
424 return dct.pop(attr) | |
425 except KeyError as e: | |
426 raise NetworkXError(f"{category} #{i} has no '{attr}' attribute") from e | |
427 | |
428 nodes = graph.get("node", []) | |
429 mapping = {} | |
430 node_labels = set() | |
431 for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]): | |
432 id = pop_attr(node, "node", "id", i) | |
433 if id in G: | |
434 raise NetworkXError(f"node id {id!r} is duplicated") | |
435 if label is not None and label != "id": | |
436 node_label = pop_attr(node, "node", label, i) | |
437 if node_label in node_labels: | |
438 raise NetworkXError(f"node label {node_label!r} is duplicated") | |
439 node_labels.add(node_label) | |
440 mapping[id] = node_label | |
441 G.add_node(id, **node) | |
442 | |
443 edges = graph.get("edge", []) | |
444 for i, edge in enumerate(edges if isinstance(edges, list) else [edges]): | |
445 source = pop_attr(edge, "edge", "source", i) | |
446 target = pop_attr(edge, "edge", "target", i) | |
447 if source not in G: | |
448 raise NetworkXError(f"edge #{i} has undefined source {source!r}") | |
449 if target not in G: | |
450 raise NetworkXError(f"edge #{i} has undefined target {target!r}") | |
451 if not multigraph: | |
452 if not G.has_edge(source, target): | |
453 G.add_edge(source, target, **edge) | |
454 else: | |
455 arrow = "->" if directed else "--" | |
456 msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated" | |
457 raise nx.NetworkXError(msg) | |
458 else: | |
459 key = edge.pop("key", None) | |
460 if key is not None and G.has_edge(source, target, key): | |
461 arrow = "->" if directed else "--" | |
462 msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})" | |
463 msg2 = 'Hint: If multigraph add "multigraph 1" to file header.' | |
464 raise nx.NetworkXError(msg + " is duplicated\n" + msg2) | |
465 G.add_edge(source, target, key, **edge) | |
466 | |
467 if label is not None and label != "id": | |
468 G = nx.relabel_nodes(G, mapping) | |
469 return G | |
470 | |
471 | |
472 def literal_stringizer(value): | |
473 """Convert a `value` to a Python literal in GML representation. | |
474 | |
475 Parameters | |
476 ---------- | |
477 value : object | |
478 The `value` to be converted to GML representation. | |
479 | |
480 Returns | |
481 ------- | |
482 rep : string | |
483 A double-quoted Python literal representing value. Unprintable | |
484 characters are replaced by XML character references. | |
485 | |
486 Raises | |
487 ------ | |
488 ValueError | |
489 If `value` cannot be converted to GML. | |
490 | |
491 Notes | |
492 ----- | |
493 `literal_stringizer` is largely the same as `repr` in terms of | |
494 functionality but attempts prefix `unicode` and `bytes` literals with | |
495 `u` and `b` to provide better interoperability of data generated by | |
496 Python 2 and Python 3. | |
497 | |
498 The original value can be recovered using the | |
499 :func:`networkx.readwrite.gml.literal_destringizer` function. | |
500 """ | |
501 msg = "literal_stringizer is deprecated and will be removed in 3.0." | |
502 warnings.warn(msg, DeprecationWarning) | |
503 | |
504 def stringize(value): | |
505 if isinstance(value, (int, bool)) or value is None: | |
506 if value is True: # GML uses 1/0 for boolean values. | |
507 buf.write(str(1)) | |
508 elif value is False: | |
509 buf.write(str(0)) | |
510 else: | |
511 buf.write(str(value)) | |
512 elif isinstance(value, str): | |
513 text = repr(value) | |
514 if text[0] != "u": | |
515 try: | |
516 value.encode("latin1") | |
517 except UnicodeEncodeError: | |
518 text = "u" + text | |
519 buf.write(text) | |
520 elif isinstance(value, (float, complex, str, bytes)): | |
521 buf.write(repr(value)) | |
522 elif isinstance(value, list): | |
523 buf.write("[") | |
524 first = True | |
525 for item in value: | |
526 if not first: | |
527 buf.write(",") | |
528 else: | |
529 first = False | |
530 stringize(item) | |
531 buf.write("]") | |
532 elif isinstance(value, tuple): | |
533 if len(value) > 1: | |
534 buf.write("(") | |
535 first = True | |
536 for item in value: | |
537 if not first: | |
538 buf.write(",") | |
539 else: | |
540 first = False | |
541 stringize(item) | |
542 buf.write(")") | |
543 elif value: | |
544 buf.write("(") | |
545 stringize(value[0]) | |
546 buf.write(",)") | |
547 else: | |
548 buf.write("()") | |
549 elif isinstance(value, dict): | |
550 buf.write("{") | |
551 first = True | |
552 for key, value in value.items(): | |
553 if not first: | |
554 buf.write(",") | |
555 else: | |
556 first = False | |
557 stringize(key) | |
558 buf.write(":") | |
559 stringize(value) | |
560 buf.write("}") | |
561 elif isinstance(value, set): | |
562 buf.write("{") | |
563 first = True | |
564 for item in value: | |
565 if not first: | |
566 buf.write(",") | |
567 else: | |
568 first = False | |
569 stringize(item) | |
570 buf.write("}") | |
571 else: | |
572 msg = "{value!r} cannot be converted into a Python literal" | |
573 raise ValueError(msg) | |
574 | |
575 buf = StringIO() | |
576 stringize(value) | |
577 return buf.getvalue() | |
578 | |
579 | |
580 def generate_gml(G, stringizer=None): | |
581 r"""Generate a single entry of the graph `G` in GML format. | |
582 | |
583 Parameters | |
584 ---------- | |
585 G : NetworkX graph | |
586 The graph to be converted to GML. | |
587 | |
588 stringizer : callable, optional | |
589 A `stringizer` which converts non-int/non-float/non-dict values into | |
590 strings. If it cannot convert a value into a string, it should raise a | |
591 `ValueError` to indicate that. Default value: None. | |
592 | |
593 Returns | |
594 ------- | |
595 lines: generator of strings | |
596 Lines of GML data. Newlines are not appended. | |
597 | |
598 Raises | |
599 ------ | |
600 NetworkXError | |
601 If `stringizer` cannot convert a value into a string, or the value to | |
602 convert is not a string while `stringizer` is None. | |
603 | |
604 Notes | |
605 ----- | |
606 Graph attributes named 'directed', 'multigraph', 'node' or | |
607 'edge', node attributes named 'id' or 'label', edge attributes | |
608 named 'source' or 'target' (or 'key' if `G` is a multigraph) | |
609 are ignored because these attribute names are used to encode the graph | |
610 structure. | |
611 | |
612 GML files are stored using a 7-bit ASCII encoding with any extended | |
613 ASCII characters (iso8859-1) appearing as HTML character entities. | |
614 Without specifying a `stringizer`/`destringizer`, the code is capable of | |
615 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML | |
616 specification. For other data types, you need to explicitly supply a | |
617 `stringizer`/`destringizer`. | |
618 | |
619 For additional documentation on the GML file format, please see the | |
620 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_. | |
621 | |
622 See the module docstring :mod:`networkx.readwrite.gml` for more details. | |
623 | |
624 Examples | |
625 -------- | |
626 >>> G = nx.Graph() | |
627 >>> G.add_node("1") | |
628 >>> print("\n".join(nx.generate_gml(G))) | |
629 graph [ | |
630 node [ | |
631 id 0 | |
632 label "1" | |
633 ] | |
634 ] | |
635 >>> G = nx.OrderedMultiGraph([("a", "b"), ("a", "b")]) | |
636 >>> print("\n".join(nx.generate_gml(G))) | |
637 graph [ | |
638 multigraph 1 | |
639 node [ | |
640 id 0 | |
641 label "a" | |
642 ] | |
643 node [ | |
644 id 1 | |
645 label "b" | |
646 ] | |
647 edge [ | |
648 source 0 | |
649 target 1 | |
650 key 0 | |
651 ] | |
652 edge [ | |
653 source 0 | |
654 target 1 | |
655 key 1 | |
656 ] | |
657 ] | |
658 """ | |
659 valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$") | |
660 | |
661 def stringize(key, value, ignored_keys, indent, in_list=False): | |
662 if not isinstance(key, str): | |
663 raise NetworkXError(f"{key!r} is not a string") | |
664 if not valid_keys.match(key): | |
665 raise NetworkXError(f"{key!r} is not a valid key") | |
666 if not isinstance(key, str): | |
667 key = str(key) | |
668 if key not in ignored_keys: | |
669 if isinstance(value, (int, bool)): | |
670 if key == "label": | |
671 yield indent + key + ' "' + str(value) + '"' | |
672 elif value is True: | |
673 # python bool is an instance of int | |
674 yield indent + key + " 1" | |
675 elif value is False: | |
676 yield indent + key + " 0" | |
677 # GML only supports signed 32-bit integers | |
678 elif value < -(2 ** 31) or value >= 2 ** 31: | |
679 yield indent + key + ' "' + str(value) + '"' | |
680 else: | |
681 yield indent + key + " " + str(value) | |
682 elif isinstance(value, float): | |
683 text = repr(value).upper() | |
684 # GML requires that a real literal contain a decimal point, but | |
685 # repr may not output a decimal point when the mantissa is | |
686 # integral and hence needs fixing. | |
687 epos = text.rfind("E") | |
688 if epos != -1 and text.find(".", 0, epos) == -1: | |
689 text = text[:epos] + "." + text[epos:] | |
690 if key == "label": | |
691 yield indent + key + ' "' + text + '"' | |
692 else: | |
693 yield indent + key + " " + text | |
694 elif isinstance(value, dict): | |
695 yield indent + key + " [" | |
696 next_indent = indent + " " | |
697 for key, value in value.items(): | |
698 yield from stringize(key, value, (), next_indent) | |
699 yield indent + "]" | |
700 elif ( | |
701 isinstance(value, (list, tuple)) | |
702 and key != "label" | |
703 and value | |
704 and not in_list | |
705 ): | |
706 if len(value) == 1: | |
707 yield indent + key + " " + f'"{LIST_START_VALUE}"' | |
708 for val in value: | |
709 yield from stringize(key, val, (), indent, True) | |
710 else: | |
711 if stringizer: | |
712 try: | |
713 value = stringizer(value) | |
714 except ValueError as e: | |
715 raise NetworkXError( | |
716 f"{value!r} cannot be converted into a string" | |
717 ) from e | |
718 if not isinstance(value, str): | |
719 raise NetworkXError(f"{value!r} is not a string") | |
720 yield indent + key + ' "' + escape(value) + '"' | |
721 | |
722 multigraph = G.is_multigraph() | |
723 yield "graph [" | |
724 | |
725 # Output graph attributes | |
726 if G.is_directed(): | |
727 yield " directed 1" | |
728 if multigraph: | |
729 yield " multigraph 1" | |
730 ignored_keys = {"directed", "multigraph", "node", "edge"} | |
731 for attr, value in G.graph.items(): | |
732 yield from stringize(attr, value, ignored_keys, " ") | |
733 | |
734 # Output node data | |
735 node_id = dict(zip(G, range(len(G)))) | |
736 ignored_keys = {"id", "label"} | |
737 for node, attrs in G.nodes.items(): | |
738 yield " node [" | |
739 yield " id " + str(node_id[node]) | |
740 yield from stringize("label", node, (), " ") | |
741 for attr, value in attrs.items(): | |
742 yield from stringize(attr, value, ignored_keys, " ") | |
743 yield " ]" | |
744 | |
745 # Output edge data | |
746 ignored_keys = {"source", "target"} | |
747 kwargs = {"data": True} | |
748 if multigraph: | |
749 ignored_keys.add("key") | |
750 kwargs["keys"] = True | |
751 for e in G.edges(**kwargs): | |
752 yield " edge [" | |
753 yield " source " + str(node_id[e[0]]) | |
754 yield " target " + str(node_id[e[1]]) | |
755 if multigraph: | |
756 yield from stringize("key", e[2], (), " ") | |
757 for attr, value in e[-1].items(): | |
758 yield from stringize(attr, value, ignored_keys, " ") | |
759 yield " ]" | |
760 yield "]" | |
761 | |
762 | |
763 @open_file(1, mode="wb") | |
764 def write_gml(G, path, stringizer=None): | |
765 """Write a graph `G` in GML format to the file or file handle `path`. | |
766 | |
767 Parameters | |
768 ---------- | |
769 G : NetworkX graph | |
770 The graph to be converted to GML. | |
771 | |
772 path : filename or filehandle | |
773 The filename or filehandle to write. Files whose names end with .gz or | |
774 .bz2 will be compressed. | |
775 | |
776 stringizer : callable, optional | |
777 A `stringizer` which converts non-int/non-float/non-dict values into | |
778 strings. If it cannot convert a value into a string, it should raise a | |
779 `ValueError` to indicate that. Default value: None. | |
780 | |
781 Raises | |
782 ------ | |
783 NetworkXError | |
784 If `stringizer` cannot convert a value into a string, or the value to | |
785 convert is not a string while `stringizer` is None. | |
786 | |
787 See Also | |
788 -------- | |
789 read_gml, generate_gml | |
790 | |
791 Notes | |
792 ----- | |
793 Graph attributes named 'directed', 'multigraph', 'node' or | |
794 'edge', node attributes named 'id' or 'label', edge attributes | |
795 named 'source' or 'target' (or 'key' if `G` is a multigraph) | |
796 are ignored because these attribute names are used to encode the graph | |
797 structure. | |
798 | |
799 GML files are stored using a 7-bit ASCII encoding with any extended | |
800 ASCII characters (iso8859-1) appearing as HTML character entities. | |
801 Without specifying a `stringizer`/`destringizer`, the code is capable of | |
802 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML | |
803 specification. For other data types, you need to explicitly supply a | |
804 `stringizer`/`destringizer`. | |
805 | |
806 Note that while we allow non-standard GML to be read from a file, we make | |
807 sure to write GML format. In particular, underscores are not allowed in | |
808 attribute names. | |
809 For additional documentation on the GML file format, please see the | |
810 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_. | |
811 | |
812 See the module docstring :mod:`networkx.readwrite.gml` for more details. | |
813 | |
814 Examples | |
815 -------- | |
816 >>> G = nx.path_graph(4) | |
817 >>> nx.write_gml(G, "test.gml") | |
818 | |
819 Filenames ending in .gz or .bz2 will be compressed. | |
820 | |
821 >>> nx.write_gml(G, "test.gml.gz") | |
822 """ | |
823 for line in generate_gml(G, stringizer): | |
824 path.write((line + "\n").encode("ascii")) |