1"""
2*************************
3Multi-line Adjacency List
4*************************
5Read and write NetworkX graphs as multi-line adjacency lists.
6
7The multi-line adjacency list format is useful for graphs with
8nodes that can be meaningfully represented as strings. With this format
9simple edge data can be stored but node or graph data is not.
10
11Format
12------
13The first label in a line is the source node label followed by the node degree
14d. The next d lines are target node labels and optional edge data.
15That pattern repeats for all nodes in the graph.
16
17The graph with edges a-b, a-c, d-e can be represented as the following
18adjacency list (anything following the # in a line is a comment)::
19
20 # example.multiline-adjlist
21 a 2
22 b
23 c
24 d 1
25 e
26"""
27
28__all__ = [
29 "generate_multiline_adjlist",
30 "write_multiline_adjlist",
31 "parse_multiline_adjlist",
32 "read_multiline_adjlist",
33]
34
35import networkx as nx
36from networkx.utils import open_file
37
38
39def generate_multiline_adjlist(G, delimiter=" "):
40 """Generate a single line of the graph G in multiline adjacency list format.
41
42 Parameters
43 ----------
44 G : NetworkX graph
45
46 delimiter : string, optional
47 Separator for node labels
48
49 Returns
50 -------
51 lines : string
52 Lines of data in multiline adjlist format.
53
54 Examples
55 --------
56 >>> G = nx.lollipop_graph(4, 3)
57 >>> for line in nx.generate_multiline_adjlist(G):
58 ... print(line)
59 0 3
60 1 {}
61 2 {}
62 3 {}
63 1 2
64 2 {}
65 3 {}
66 2 1
67 3 {}
68 3 1
69 4 {}
70 4 1
71 5 {}
72 5 1
73 6 {}
74 6 0
75
76 See Also
77 --------
78 write_multiline_adjlist, read_multiline_adjlist
79 """
80 if G.is_directed():
81 if G.is_multigraph():
82 for s, nbrs in G.adjacency():
83 nbr_edges = [
84 (u, data)
85 for u, datadict in nbrs.items()
86 for key, data in datadict.items()
87 ]
88 deg = len(nbr_edges)
89 yield str(s) + delimiter + str(deg)
90 for u, d in nbr_edges:
91 if d is None:
92 yield str(u)
93 else:
94 yield str(u) + delimiter + str(d)
95 else: # directed single edges
96 for s, nbrs in G.adjacency():
97 deg = len(nbrs)
98 yield str(s) + delimiter + str(deg)
99 for u, d in nbrs.items():
100 if d is None:
101 yield str(u)
102 else:
103 yield str(u) + delimiter + str(d)
104 else: # undirected
105 if G.is_multigraph():
106 seen = set() # helper dict used to avoid duplicate edges
107 for s, nbrs in G.adjacency():
108 nbr_edges = [
109 (u, data)
110 for u, datadict in nbrs.items()
111 if u not in seen
112 for key, data in datadict.items()
113 ]
114 deg = len(nbr_edges)
115 yield str(s) + delimiter + str(deg)
116 for u, d in nbr_edges:
117 if d is None:
118 yield str(u)
119 else:
120 yield str(u) + delimiter + str(d)
121 seen.add(s)
122 else: # undirected single edges
123 seen = set() # helper dict used to avoid duplicate edges
124 for s, nbrs in G.adjacency():
125 nbr_edges = [(u, d) for u, d in nbrs.items() if u not in seen]
126 deg = len(nbr_edges)
127 yield str(s) + delimiter + str(deg)
128 for u, d in nbr_edges:
129 if d is None:
130 yield str(u)
131 else:
132 yield str(u) + delimiter + str(d)
133 seen.add(s)
134
135
136@open_file(1, mode="wb")
137def write_multiline_adjlist(G, path, delimiter=" ", comments="#", encoding="utf-8"):
138 """Write the graph G in multiline adjacency list format to path
139
140 Parameters
141 ----------
142 G : NetworkX graph
143
144 path : string or file
145 Filename or file handle to write to.
146 Filenames ending in .gz or .bz2 will be compressed.
147
148 comments : string, optional
149 Marker for comment lines
150
151 delimiter : string, optional
152 Separator for node labels
153
154 encoding : string, optional
155 Text encoding.
156
157 Examples
158 --------
159 >>> G = nx.path_graph(4)
160 >>> nx.write_multiline_adjlist(G, "test.multi_adjlist")
161
162 The path can be a file handle or a string with the name of the file. If a
163 file handle is provided, it has to be opened in 'wb' mode.
164
165 >>> fh = open("test.multi_adjlist2", "wb")
166 >>> nx.write_multiline_adjlist(G, fh)
167
168 Filenames ending in .gz or .bz2 will be compressed.
169
170 >>> nx.write_multiline_adjlist(G, "test.multi_adjlist.gz")
171
172 See Also
173 --------
174 read_multiline_adjlist
175 """
176 import sys
177 import time
178
179 pargs = comments + " ".join(sys.argv)
180 header = (
181 f"{pargs}\n"
182 + comments
183 + f" GMT {time.asctime(time.gmtime())}\n"
184 + comments
185 + f" {G.name}\n"
186 )
187 path.write(header.encode(encoding))
188
189 for multiline in generate_multiline_adjlist(G, delimiter):
190 multiline += "\n"
191 path.write(multiline.encode(encoding))
192
193
194@nx._dispatchable(graphs=None, returns_graph=True)
195def parse_multiline_adjlist(
196 lines, comments="#", delimiter=None, create_using=None, nodetype=None, edgetype=None
197):
198 """Parse lines of a multiline adjacency list representation of a graph.
199
200 Parameters
201 ----------
202 lines : list or iterator of strings
203 Input data in multiline adjlist format
204
205 create_using : NetworkX graph constructor, optional (default=nx.Graph)
206 Graph type to create. If graph instance, then cleared before populated.
207
208 nodetype : Python type, optional
209 Convert nodes to this type.
210
211 edgetype : Python type, optional
212 Convert edges to this type.
213
214 comments : string, optional
215 Marker for comment lines
216
217 delimiter : string, optional
218 Separator for node labels. The default is whitespace.
219
220 Returns
221 -------
222 G: NetworkX graph
223 The graph corresponding to the lines in multiline adjacency list format.
224
225 Examples
226 --------
227 >>> lines = [
228 ... "1 2",
229 ... "2 {'weight':3, 'name': 'Frodo'}",
230 ... "3 {}",
231 ... "2 1",
232 ... "5 {'weight':6, 'name': 'Saruman'}",
233 ... ]
234 >>> G = nx.parse_multiline_adjlist(iter(lines), nodetype=int)
235 >>> list(G)
236 [1, 2, 3, 5]
237
238 """
239 from ast import literal_eval
240
241 G = nx.empty_graph(0, create_using)
242 for line in lines:
243 p = line.find(comments)
244 if p >= 0:
245 line = line[:p]
246 if not line:
247 continue
248 try:
249 (u, deg) = line.rstrip("\n").split(delimiter)
250 deg = int(deg)
251 except BaseException as err:
252 raise TypeError(f"Failed to read node and degree on line ({line})") from err
253 if nodetype is not None:
254 try:
255 u = nodetype(u)
256 except BaseException as err:
257 raise TypeError(
258 f"Failed to convert node ({u}) to type {nodetype}"
259 ) from err
260 G.add_node(u)
261 for i in range(deg):
262 while True:
263 try:
264 line = next(lines)
265 except StopIteration as err:
266 msg = f"Failed to find neighbor for node ({u})"
267 raise TypeError(msg) from err
268 p = line.find(comments)
269 if p >= 0:
270 line = line[:p]
271 if line:
272 break
273 vlist = line.rstrip("\n").split(delimiter)
274 numb = len(vlist)
275 if numb < 1:
276 continue # isolated node
277 v = vlist.pop(0)
278 data = "".join(vlist)
279 if nodetype is not None:
280 try:
281 v = nodetype(v)
282 except BaseException as err:
283 raise TypeError(
284 f"Failed to convert node ({v}) to type {nodetype}"
285 ) from err
286 if edgetype is not None:
287 try:
288 edgedata = {"weight": edgetype(data)}
289 except BaseException as err:
290 raise TypeError(
291 f"Failed to convert edge data ({data}) to type {edgetype}"
292 ) from err
293 else:
294 try: # try to evaluate
295 edgedata = literal_eval(data)
296 except:
297 edgedata = {}
298 G.add_edge(u, v, **edgedata)
299
300 return G
301
302
303@open_file(0, mode="rb")
304@nx._dispatchable(graphs=None, returns_graph=True)
305def read_multiline_adjlist(
306 path,
307 comments="#",
308 delimiter=None,
309 create_using=None,
310 nodetype=None,
311 edgetype=None,
312 encoding="utf-8",
313):
314 """Read graph in multi-line adjacency list format from path.
315
316 Parameters
317 ----------
318 path : string or file
319 Filename or file handle to read.
320 Filenames ending in .gz or .bz2 will be decompressed.
321
322 create_using : NetworkX graph constructor, optional (default=nx.Graph)
323 Graph type to create. If graph instance, then cleared before populated.
324
325 nodetype : Python type, optional
326 Convert nodes to this type.
327
328 edgetype : Python type, optional
329 Convert edge data to this type.
330
331 comments : string, optional
332 Marker for comment lines
333
334 delimiter : string, optional
335 Separator for node labels. The default is whitespace.
336
337 Returns
338 -------
339 G: NetworkX graph
340
341 Examples
342 --------
343 >>> G = nx.path_graph(4)
344 >>> nx.write_multiline_adjlist(G, "test.multi_adjlistP4")
345 >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4")
346
347 The path can be a file or a string with the name of the file. If a
348 file s provided, it has to be opened in 'rb' mode.
349
350 >>> fh = open("test.multi_adjlistP4", "rb")
351 >>> G = nx.read_multiline_adjlist(fh)
352
353 Filenames ending in .gz or .bz2 will be compressed.
354
355 >>> nx.write_multiline_adjlist(G, "test.multi_adjlistP4.gz")
356 >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4.gz")
357
358 The optional nodetype is a function to convert node strings to nodetype.
359
360 For example
361
362 >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4", nodetype=int)
363
364 will attempt to convert all nodes to integer type.
365
366 The optional edgetype is a function to convert edge data strings to
367 edgetype.
368
369 >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4")
370
371 The optional create_using parameter is a NetworkX graph container.
372 The default is Graph(), an undirected graph. To read the data as
373 a directed graph use
374
375 >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4", create_using=nx.DiGraph)
376
377 Notes
378 -----
379 This format does not store graph, node, or edge data.
380
381 See Also
382 --------
383 write_multiline_adjlist
384 """
385 lines = (line.decode(encoding) for line in path)
386 return parse_multiline_adjlist(
387 lines,
388 comments=comments,
389 delimiter=delimiter,
390 create_using=create_using,
391 nodetype=nodetype,
392 edgetype=edgetype,
393 )