1"""Views of core data structures such as nested Mappings (e.g. dict-of-dicts).
2These ``Views`` often restrict element access, with either the entire view or
3layers of nested mappings being read-only.
4"""
5
6from collections.abc import Mapping
7
8__all__ = [
9 "AtlasView",
10 "AdjacencyView",
11 "MultiAdjacencyView",
12 "UnionAtlas",
13 "UnionAdjacency",
14 "UnionMultiInner",
15 "UnionMultiAdjacency",
16 "FilterAtlas",
17 "FilterAdjacency",
18 "FilterMultiInner",
19 "FilterMultiAdjacency",
20]
21
22
23class AtlasView(Mapping):
24 """An AtlasView is a Read-only Mapping of Mappings.
25
26 It is a View into a dict-of-dict data structure.
27 The inner level of dict is read-write. But the
28 outer level is read-only.
29
30 See Also
31 ========
32 AdjacencyView: View into dict-of-dict-of-dict
33 MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict
34 """
35
36 __slots__ = ("_atlas",)
37
38 def __getstate__(self):
39 return {"_atlas": self._atlas}
40
41 def __setstate__(self, state):
42 self._atlas = state["_atlas"]
43
44 def __init__(self, d):
45 self._atlas = d
46
47 def __len__(self):
48 return len(self._atlas)
49
50 def __iter__(self):
51 return iter(self._atlas)
52
53 def __getitem__(self, key):
54 return self._atlas[key]
55
56 def copy(self):
57 return {n: self[n].copy() for n in self._atlas}
58
59 def __str__(self):
60 return str(self._atlas) # {nbr: self[nbr] for nbr in self})
61
62 def __repr__(self):
63 return f"{self.__class__.__name__}({self._atlas!r})"
64
65
66class AdjacencyView(AtlasView):
67 """An AdjacencyView is a Read-only Map of Maps of Maps.
68
69 It is a View into a dict-of-dict-of-dict data structure.
70 The inner level of dict is read-write. But the
71 outer levels are read-only.
72
73 See Also
74 ========
75 AtlasView: View into dict-of-dict
76 MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict
77 """
78
79 __slots__ = () # Still uses AtlasView slots names _atlas
80
81 def __getitem__(self, name):
82 return AtlasView(self._atlas[name])
83
84 def copy(self):
85 return {n: self[n].copy() for n in self._atlas}
86
87
88class MultiAdjacencyView(AdjacencyView):
89 """An MultiAdjacencyView is a Read-only Map of Maps of Maps of Maps.
90
91 It is a View into a dict-of-dict-of-dict-of-dict data structure.
92 The inner level of dict is read-write. But the
93 outer levels are read-only.
94
95 See Also
96 ========
97 AtlasView: View into dict-of-dict
98 AdjacencyView: View into dict-of-dict-of-dict
99 """
100
101 __slots__ = () # Still uses AtlasView slots names _atlas
102
103 def __getitem__(self, name):
104 return AdjacencyView(self._atlas[name])
105
106 def copy(self):
107 return {n: self[n].copy() for n in self._atlas}
108
109
110class UnionAtlas(Mapping):
111 """A read-only union of two atlases (dict-of-dict).
112
113 The two dict-of-dicts represent the inner dict of
114 an Adjacency: `G.succ[node]` and `G.pred[node]`.
115 The inner level of dict of both hold attribute key:value
116 pairs and is read-write. But the outer level is read-only.
117
118 See Also
119 ========
120 UnionAdjacency: View into dict-of-dict-of-dict
121 UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
122 """
123
124 __slots__ = ("_succ", "_pred")
125
126 def __getstate__(self):
127 return {"_succ": self._succ, "_pred": self._pred}
128
129 def __setstate__(self, state):
130 self._succ = state["_succ"]
131 self._pred = state["_pred"]
132
133 def __init__(self, succ, pred):
134 self._succ = succ
135 self._pred = pred
136
137 def __len__(self):
138 return len(self._succ.keys() | self._pred.keys())
139
140 def __iter__(self):
141 return iter(set(self._succ.keys()) | set(self._pred.keys()))
142
143 def __getitem__(self, key):
144 try:
145 return self._succ[key]
146 except KeyError:
147 return self._pred[key]
148
149 def copy(self):
150 result = {nbr: dd.copy() for nbr, dd in self._succ.items()}
151 for nbr, dd in self._pred.items():
152 if nbr in result:
153 result[nbr].update(dd)
154 else:
155 result[nbr] = dd.copy()
156 return result
157
158 def __str__(self):
159 return str({nbr: self[nbr] for nbr in self})
160
161 def __repr__(self):
162 return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})"
163
164
165class UnionAdjacency(Mapping):
166 """A read-only union of dict Adjacencies as a Map of Maps of Maps.
167
168 The two input dict-of-dict-of-dicts represent the union of
169 `G.succ` and `G.pred`. Return values are UnionAtlas
170 The inner level of dict is read-write. But the
171 middle and outer levels are read-only.
172
173 succ : a dict-of-dict-of-dict {node: nbrdict}
174 pred : a dict-of-dict-of-dict {node: nbrdict}
175 The keys for the two dicts should be the same
176
177 See Also
178 ========
179 UnionAtlas: View into dict-of-dict
180 UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
181 """
182
183 __slots__ = ("_succ", "_pred")
184
185 def __getstate__(self):
186 return {"_succ": self._succ, "_pred": self._pred}
187
188 def __setstate__(self, state):
189 self._succ = state["_succ"]
190 self._pred = state["_pred"]
191
192 def __init__(self, succ, pred):
193 # keys must be the same for two input dicts
194 assert len(set(succ.keys()) ^ set(pred.keys())) == 0
195 self._succ = succ
196 self._pred = pred
197
198 def __len__(self):
199 return len(self._succ) # length of each dict should be the same
200
201 def __iter__(self):
202 return iter(self._succ)
203
204 def __getitem__(self, nbr):
205 return UnionAtlas(self._succ[nbr], self._pred[nbr])
206
207 def copy(self):
208 return {n: self[n].copy() for n in self._succ}
209
210 def __str__(self):
211 return str({nbr: self[nbr] for nbr in self})
212
213 def __repr__(self):
214 return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})"
215
216
217class UnionMultiInner(UnionAtlas):
218 """A read-only union of two inner dicts of MultiAdjacencies.
219
220 The two input dict-of-dict-of-dicts represent the union of
221 `G.succ[node]` and `G.pred[node]` for MultiDiGraphs.
222 Return values are UnionAtlas.
223 The inner level of dict is read-write. But the outer levels are read-only.
224
225 See Also
226 ========
227 UnionAtlas: View into dict-of-dict
228 UnionAdjacency: View into dict-of-dict-of-dict
229 UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
230 """
231
232 __slots__ = () # Still uses UnionAtlas slots names _succ, _pred
233
234 def __getitem__(self, node):
235 in_succ = node in self._succ
236 in_pred = node in self._pred
237 if in_succ:
238 if in_pred:
239 return UnionAtlas(self._succ[node], self._pred[node])
240 return UnionAtlas(self._succ[node], {})
241 return UnionAtlas({}, self._pred[node])
242
243 def copy(self):
244 nodes = set(self._succ.keys()) | set(self._pred.keys())
245 return {n: self[n].copy() for n in nodes}
246
247
248class UnionMultiAdjacency(UnionAdjacency):
249 """A read-only union of two dict MultiAdjacencies.
250
251 The two input dict-of-dict-of-dict-of-dicts represent the union of
252 `G.succ` and `G.pred` for MultiDiGraphs. Return values are UnionAdjacency.
253 The inner level of dict is read-write. But the outer levels are read-only.
254
255 See Also
256 ========
257 UnionAtlas: View into dict-of-dict
258 UnionMultiInner: View into dict-of-dict-of-dict
259 """
260
261 __slots__ = () # Still uses UnionAdjacency slots names _succ, _pred
262
263 def __getitem__(self, node):
264 return UnionMultiInner(self._succ[node], self._pred[node])
265
266
267class FilterAtlas(Mapping): # nodedict, nbrdict, keydict
268 """A read-only Mapping of Mappings with filtering criteria for nodes.
269
270 It is a view into a dict-of-dict data structure, and it selects only
271 nodes that meet the criteria defined by ``NODE_OK``.
272
273 See Also
274 ========
275 FilterAdjacency
276 FilterMultiInner
277 FilterMultiAdjacency
278 """
279
280 def __init__(self, d, NODE_OK):
281 self._atlas = d
282 self.NODE_OK = NODE_OK
283
284 def __len__(self):
285 # check whether NODE_OK stores the number of nodes as `length`
286 # or the nodes themselves as a set `nodes`. If not, count the nodes.
287 if hasattr(self.NODE_OK, "length"):
288 return self.NODE_OK.length
289 if hasattr(self.NODE_OK, "nodes"):
290 return len(self.NODE_OK.nodes & self._atlas.keys())
291 return sum(1 for n in self._atlas if self.NODE_OK(n))
292
293 def __iter__(self):
294 try: # check that NODE_OK has attr 'nodes'
295 node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
296 except AttributeError:
297 node_ok_shorter = False
298 if node_ok_shorter:
299 return (n for n in self.NODE_OK.nodes if n in self._atlas)
300 return (n for n in self._atlas if self.NODE_OK(n))
301
302 def __getitem__(self, key):
303 if key in self._atlas and self.NODE_OK(key):
304 return self._atlas[key]
305 raise KeyError(f"Key {key} not found")
306
307 def __str__(self):
308 return str({nbr: self[nbr] for nbr in self})
309
310 def __repr__(self):
311 return f"{self.__class__.__name__}({self._atlas!r}, {self.NODE_OK!r})"
312
313
314class FilterAdjacency(Mapping): # edgedict
315 """A read-only Mapping of Mappings with filtering criteria for nodes and edges.
316
317 It is a view into a dict-of-dict-of-dict data structure, and it selects nodes
318 and edges that satisfy specific criteria defined by ``NODE_OK`` and ``EDGE_OK``,
319 respectively.
320
321 See Also
322 ========
323 FilterAtlas
324 FilterMultiInner
325 FilterMultiAdjacency
326 """
327
328 def __init__(self, d, NODE_OK, EDGE_OK):
329 self._atlas = d
330 self.NODE_OK = NODE_OK
331 self.EDGE_OK = EDGE_OK
332
333 def __len__(self):
334 # check whether NODE_OK stores the number of nodes as `length`
335 # or the nodes themselves as a set `nodes`. If not, count the nodes.
336 if hasattr(self.NODE_OK, "length"):
337 return self.NODE_OK.length
338 if hasattr(self.NODE_OK, "nodes"):
339 return len(self.NODE_OK.nodes & self._atlas.keys())
340 return sum(1 for n in self._atlas if self.NODE_OK(n))
341
342 def __iter__(self):
343 try: # check that NODE_OK has attr 'nodes'
344 node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
345 except AttributeError:
346 node_ok_shorter = False
347 if node_ok_shorter:
348 return (n for n in self.NODE_OK.nodes if n in self._atlas)
349 return (n for n in self._atlas if self.NODE_OK(n))
350
351 def __getitem__(self, node):
352 if node in self._atlas and self.NODE_OK(node):
353
354 def new_node_ok(nbr):
355 return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr)
356
357 return FilterAtlas(self._atlas[node], new_node_ok)
358 raise KeyError(f"Key {node} not found")
359
360 def __str__(self):
361 return str({nbr: self[nbr] for nbr in self})
362
363 def __repr__(self):
364 name = self.__class__.__name__
365 return f"{name}({self._atlas!r}, {self.NODE_OK!r}, {self.EDGE_OK!r})"
366
367
368class FilterMultiInner(FilterAdjacency): # muliedge_seconddict
369 """A read-only Mapping of Mappings with filtering criteria for nodes and edges.
370
371 It is a view into a dict-of-dict-of-dict-of-dict data structure, and it selects nodes
372 and edges that meet specific criteria defined by ``NODE_OK`` and ``EDGE_OK``.
373
374 See Also
375 ========
376 FilterAtlas
377 FilterAdjacency
378 FilterMultiAdjacency
379 """
380
381 def __iter__(self):
382 try: # check that NODE_OK has attr 'nodes'
383 node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
384 except AttributeError:
385 node_ok_shorter = False
386 if node_ok_shorter:
387 my_nodes = (n for n in self.NODE_OK.nodes if n in self._atlas)
388 else:
389 my_nodes = (n for n in self._atlas if self.NODE_OK(n))
390 for n in my_nodes:
391 some_keys_ok = False
392 for key in self._atlas[n]:
393 if self.EDGE_OK(n, key):
394 some_keys_ok = True
395 break
396 if some_keys_ok is True:
397 yield n
398
399 def __getitem__(self, nbr):
400 if (
401 nbr in self._atlas
402 and self.NODE_OK(nbr)
403 and any(self.EDGE_OK(nbr, key) for key in self._atlas[nbr])
404 ):
405
406 def new_node_ok(key):
407 return self.EDGE_OK(nbr, key)
408
409 return FilterAtlas(self._atlas[nbr], new_node_ok)
410 raise KeyError(f"Key {nbr} not found")
411
412
413class FilterMultiAdjacency(FilterAdjacency): # multiedgedict
414 """A read-only Mapping of Mappings with filtering criteria
415 for nodes and edges.
416
417 It is a view into a dict-of-dict-of-dict-of-dict data structure,
418 and it selects nodes and edges that satisfy specific criteria
419 defined by ``NODE_OK`` and ``EDGE_OK``, respectively.
420
421 See Also
422 ========
423 FilterAtlas
424 FilterAdjacency
425 FilterMultiInner
426 """
427
428 def __getitem__(self, node):
429 if node in self._atlas and self.NODE_OK(node):
430
431 def edge_ok(nbr, key):
432 return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr, key)
433
434 return FilterMultiInner(self._atlas[node], self.NODE_OK, edge_ok)
435 raise KeyError(f"Key {node} not found")