Skip to content

Commit 1009d5f

Browse files
Improved docs to flesh out descriptions of various model/manager methods
1 parent 36cb662 commit 1009d5f

File tree

3 files changed

+322
-40
lines changed

3 files changed

+322
-40
lines changed

django_postgresql_dag/models.py

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""
2-
A class to model hierarchies of objects following Directed Acyclic Graph structure.
2+
A set of model classes to model hierarchies of objects following Directed Acyclic Graph structure.
33
4-
The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree
5-
of related node ids in a single query. These queries also topologically sort the ids
6-
by generation.
4+
The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree of related node ids in a single
5+
query. These queries also topologically sort the ids by generation.
76
"""
87

98
from django.apps import apps
@@ -18,13 +17,19 @@
1817

1918
class NodeManager(models.Manager):
2019
def roots(self, node=None):
21-
"""Returns a Queryset of all root Nodes, or optionally, the roots of a select node"""
20+
"""
21+
Returns a Queryset of all root nodes (nodes with no parents) in the Node model. If a node instance is specified,
22+
returns only the roots for that node.
23+
"""
2224
if node is not None:
2325
return node.roots()
2426
return self.filter(parents__isnull=True)
2527

2628
def leaves(self, node=None):
27-
"""Returns a Queryset of all leaf Nodes, or optionally, the leaves of a select node"""
29+
"""
30+
Returns a Queryset of all leaf nodes (nodes with no children) in the Node model. If a node instance is
31+
specified, returns only the leaves for that node.
32+
"""
2833
if node is not None:
2934
return node.leaves()
3035
return self.filter(children__isnull=True)
@@ -72,6 +77,7 @@ def ordered_queryset_from_pks(self, pks):
7277
return _ordered_filter(self.__class__.objects, "pk", pks)
7378

7479
def add_child(self, child, **kwargs):
80+
"""Provided with a Node instance, attaches that instance as a child to the current Node instance"""
7581
kwargs.update({"parent": self, "child": child})
7682

7783
disable_circular_check = kwargs.pop("disable_circular_check", False)
@@ -81,7 +87,10 @@ def add_child(self, child, **kwargs):
8187
return cls.save(disable_circular_check=disable_circular_check, allow_duplicate_edges=allow_duplicate_edges)
8288

8389
def remove_child(self, child, delete_node=False):
84-
"""Removes the edge connecting this node to child, and optionally deletes the child node as well"""
90+
"""
91+
Removes the edge connecting this node to the provided child Node instance, and optionally deletes the child
92+
node as well
93+
"""
8594
if child in self.children.all():
8695
self.children.through.objects.filter(parent=self, child=child).delete()
8796
if delete_node:
@@ -92,6 +101,7 @@ def remove_child(self, child, delete_node=False):
92101
child.delete()
93102

94103
def add_parent(self, parent, *args, **kwargs):
104+
"""Provided with a Node instance, attaches the current instance as a child to the provided Node instance"""
95105
return parent.add_child(self, **kwargs)
96106

97107
def remove_parent(self, parent, delete_node=False):
@@ -106,44 +116,54 @@ def remove_parent(self, parent, delete_node=False):
106116
parent.delete()
107117

108118
def ancestors_raw(self, **kwargs):
119+
"""Returns a raw QuerySet of all nodes in connected paths in a rootward direction"""
109120
return AncestorQuery(instance=self, **kwargs).raw_queryset()
110121

111122
def ancestors(self, **kwargs):
123+
"""Returns a QuerySet of all nodes in connected paths in a rootward direction"""
112124
pks = [item.pk for item in self.ancestors_raw(**kwargs)]
113125
return self.ordered_queryset_from_pks(pks)
114126

115127
def ancestors_count(self):
128+
"""Returns an integer number representing the total number of ancestor nodes"""
116129
return self.ancestors().count()
117130

118131
def self_and_ancestors(self, **kwargs):
132+
"""Returns a QuerySet of all nodes in connected paths in a rootward direction, prepending with self"""
119133
pks = [self.pk] + [item.pk for item in self.ancestors_raw(**kwargs)][::-1]
120134
return self.ordered_queryset_from_pks(pks)
121135

122136
def ancestors_and_self(self, **kwargs):
137+
"""Returns a QuerySet of all nodes in connected paths in a rootward direction, appending with self"""
123138
pks = [item.pk for item in self.ancestors_raw(**kwargs)] + [self.pk]
124139
return self.ordered_queryset_from_pks(pks)
125140

126141
def descendants_raw(self, **kwargs):
142+
"""Returns a raw QuerySet of all nodes in connected paths in a leafward direction"""
127143
return DescendantQuery(instance=self, **kwargs).raw_queryset()
128144

129145
def descendants(self, **kwargs):
146+
"""Returns a QuerySet of all nodes in connected paths in a leafward direction"""
130147
pks = [item.pk for item in self.descendants_raw(**kwargs)]
131148
return self.ordered_queryset_from_pks(pks)
132149

133150
def descendants_count(self):
151+
"""Returns an integer number representing the total number of descendant nodes"""
134152
return self.descendants().count()
135153

136154
def self_and_descendants(self, **kwargs):
155+
"""Returns a QuerySet of all nodes in connected paths in a leafward direction, prepending with self"""
137156
pks = [self.pk] + [item.pk for item in self.descendants_raw(**kwargs)]
138157
return self.ordered_queryset_from_pks(pks)
139158

140159
def descendants_and_self(self, **kwargs):
160+
"""Returns a QuerySet of all nodes in connected paths in a leafward direction, appending with self"""
141161
pks = [item.pk for item in self.descendants_raw(**kwargs)] + [self.pk]
142162
return self.ordered_queryset_from_pks(pks)
143163

144164
def clan(self, **kwargs):
145165
"""
146-
Returns a queryset with all ancestors, self, and all descendants
166+
Returns a QuerySet with all ancestors nodes, self, and all descendant nodes
147167
"""
148168
pks = (
149169
[item.pk for item in self.ancestors_raw(**kwargs)]
@@ -153,22 +173,23 @@ def clan(self, **kwargs):
153173
return self.ordered_queryset_from_pks(pks)
154174

155175
def clan_count(self):
176+
"""Returns an integer number representing the total number of clan nodes"""
156177
return self.clan().count()
157178

158179
def siblings(self):
159-
# Returns all nodes that share a parent with this node
180+
"""Returns a QuerySet of all nodes that share a parent with this node, excluding self"""
160181
return self.siblings_with_self().exclude(pk=self.pk)
161182

162183
def siblings_count(self):
163-
# Returns count of all nodes that share a parent with this node
184+
"""Returns count of all nodes that share a parent with this node"""
164185
return self.siblings().count()
165186

166187
def siblings_with_self(self):
167-
# Returns all nodes that share a parent with this node and self
188+
"""Returns a QuerySet of all nodes that share a parent with this node and self"""
168189
return self.__class__.objects.filter(parents__in=self.parents.all()).distinct()
169190

170191
def partners(self):
171-
# Returns all nodes that share a child with this node
192+
"""Returns a QuerySet of all nodes that share a child with this node"""
172193
return self.partners_with_self().exclude(pk=self.pk)
173194

174195
def partners_count(self):
@@ -200,12 +221,21 @@ def path_raw(self, ending_node, directional=True, **kwargs):
200221
return path
201222

202223
def path_exists(self, ending_node, **kwargs):
224+
"""
225+
Given an ending Node instance, returns a boolean value determining whether there is a path from the current
226+
Node instance to the ending Node instance
227+
"""
203228
try:
204229
return len(list(self.path_raw(ending_node, **kwargs))) >= 1
205230
except NodeNotReachableException:
206231
return False
207232

208233
def path(self, ending_node, **kwargs):
234+
"""
235+
Returns a QuerySet of the shortest path from self to ending node, optionally in either direction.
236+
The resulting Queryset is sorted from root-side, toward leaf-side, regardless of the relative position of
237+
starting and ending nodes.
238+
"""
209239
pks = [item.pk for item in self.path_raw(ending_node, **kwargs)]
210240
return self.ordered_queryset_from_pks(pks)
211241

@@ -220,68 +250,85 @@ def distance(self, ending_node, **kwargs):
220250

221251
def is_root(self):
222252
"""
223-
Check if has children and not ancestors
253+
Returns True if the current Node instance has children, but no parents
224254
"""
225255
return bool(self.children.exists() and not self.parents.exists())
226256

227257
def is_leaf(self):
228258
"""
229-
Check if has ancestors and not children
259+
Returns True if the current Node instance has parents, but no children
230260
"""
231261
return bool(self.parents.exists() and not self.children.exists())
232262

233263
def is_island(self):
234264
"""
235-
Check if has no ancestors nor children
265+
Returns True if the current Node instance has no parents nor children
236266
"""
237267
return bool(not self.children.exists() and not self.parents.exists())
238268

239269
def is_ancestor_of(self, ending_node, **kwargs):
270+
"""
271+
Provided an ending_node Node instance, returns True if the current Node instance and is an ancestor of the
272+
provided Node instance
273+
"""
240274
try:
241275
return len(self.path_raw(ending_node, **kwargs)) >= 1
242276
except NodeNotReachableException:
243277
return False
244278

245279
def is_descendant_of(self, ending_node, **kwargs):
280+
"""
281+
Provided an ending_node Node instance, returns True if the current Node instance and is a descendant of the
282+
provided Node instance
283+
"""
246284
return (
247285
not self.is_ancestor_of(ending_node, **kwargs)
248286
and len(self.path_raw(ending_node, directional=False, **kwargs)) >= 1
249287
)
250288

251289
def is_sibling_of(self, ending_node):
290+
"""
291+
Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
292+
instance share a parent Node
293+
"""
252294
return ending_node in self.siblings()
253295

254296
def is_partner_of(self, ending_node):
297+
"""
298+
Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
299+
instance share a child Node
300+
"""
255301
return ending_node in self.partners()
256302

257303
def node_depth(self):
258-
# Depth from furthest root
304+
"""Returns an integer representing the depth of this Node instance from furthest root"""
259305
# ToDo: Implement
260306
pass
261307

262308
def connected_graph_raw(self, **kwargs):
263-
# Gets all nodes connected in any way to this node
309+
"""Returns a raw QuerySet of all nodes connected in any way to the current Node instance"""
264310
return ConnectedGraphQuery(instance=self, **kwargs).raw_queryset()
265311

266312
def connected_graph(self, **kwargs):
313+
"""Returns a QuerySet of all nodes connected in any way to the current Node instance"""
267314
pks = [item.pk for item in self.connected_graph_raw(**kwargs)]
268315
return self.ordered_queryset_from_pks(pks)
269316

270317
def descendants_tree(self):
271318
"""
272-
Returns a tree-like structure with descendants
273-
# ToDo: Modify to use CTE
319+
Returns a tree-like structure with descendants for the current Node
274320
"""
321+
# ToDo: Modify to use CTE
275322
tree = {}
276323
for child in self.children.all():
277324
tree[child] = child.descendants_tree()
278325
return tree
279326

280327
def ancestors_tree(self):
281328
"""
282-
Returns a tree-like structure with ancestors
283-
# ToDo: Modify to use CTE
329+
Returns a tree-like structure with ancestors for the current Node
284330
"""
331+
# ToDo: Modify to use CTE
285332
tree = {}
286333
for parent in self.parents.all():
287334
tree[parent] = parent.ancestors_tree()
@@ -300,9 +347,9 @@ def _roots(self, ancestors_tree):
300347

301348
def roots(self):
302349
"""
303-
Returns roots nodes, if any
304-
# ToDo: Modify to use CTE
350+
Returns a QuerySet of all root nodes, if any, for the current Node
305351
"""
352+
# ToDo: Modify to use CTE
306353
ancestors_tree = self.ancestors_tree()
307354
roots = set()
308355
for ancestor in ancestors_tree:
@@ -324,9 +371,9 @@ def _leaves(self, descendants_tree):
324371

325372
def leaves(self):
326373
"""
327-
Returns leaves nodes, if any
328-
# ToDo: Modify to use CTE
374+
Returns a QuerySet of all leaf nodes, if any, for the current Node
329375
"""
376+
# ToDo: Modify to use CTE
330377
descendants_tree = self.descendants_tree()
331378
leaves = set()
332379
for descendant in descendants_tree:
@@ -337,29 +384,27 @@ def leaves(self):
337384

338385
def descendants_edges(self):
339386
"""
340-
Returns a queryset of descendants edges
341-
342-
ToDo: Perform topological sort
387+
Returns a QuerySet of descendant Edge instances for the current Node
343388
"""
389+
# ToDo: Perform topological sort
344390
return edge_model.objects.filter(
345391
parent__in=self.self_and_descendants(),
346392
child__in=self.self_and_descendants(),
347393
)
348394

349395
def ancestors_edges(self):
350396
"""
351-
Returns a queryset of ancestors edges
352-
353-
ToDo: Perform topological sort
397+
Returns a QuerySet of ancestor Edge instances for the current Node
354398
"""
399+
# ToDo: Perform topological sort
355400
return edge_model.objects.filter(
356401
parent__in=self.self_and_ancestors(),
357402
child__in=self.self_and_ancestors(),
358403
)
359404

360405
def clan_edges(self):
361406
"""
362-
Returns a queryset of all edges associated with a given node
407+
Returns a QuerySet of all Edge instances associated with a given node
363408
"""
364409
return self.ancestors_edges() | self.descendants_edges()
365410

@@ -378,44 +423,46 @@ def duplicate_edge_checker(parent, child):
378423

379424
class EdgeManager(models.Manager):
380425
def from_nodes_queryset(self, nodes_queryset):
381-
"""Provided a queryset of nodes, returns all edges where a parent and child
382-
node are within the queryset of nodes."""
426+
"""
427+
Provided a QuerySet of nodes, returns a QuerySet of all Edge instances where a parent and child Node are within
428+
the QuerySet of nodes
429+
"""
383430
return _ordered_filter(self.model.objects, ["parent", "child"], nodes_queryset)
384431

385432
def descendants(self, node, **kwargs):
386433
"""
387-
Returns a queryset of all edges descended from the given node
434+
Returns a QuerySet of all Edge instances descended from the given Node instance
388435
"""
389436
return _ordered_filter(self.model.objects, "parent", node.self_and_descendants(**kwargs))
390437

391438
def ancestors(self, node, **kwargs):
392439
"""
393-
Returns a queryset of all edges which are ancestors of the given node
440+
Returns a QuerySet of all Edge instances which are ancestors of the given Node instance
394441
"""
395442
return _ordered_filter(self.model.objects, "child", node.ancestors_and_self(**kwargs))
396443

397444
def clan(self, node, **kwargs):
398445
"""
399-
Returns a queryset of all edges for ancestors, self, and descendants
446+
Returns a QuerySet of all Edge instances for ancestors, self, and descendants
400447
"""
401448
return self.from_nodes_queryset(node.clan(**kwargs))
402449

403450
def path(self, start_node, end_node, **kwargs):
404451
"""
405-
Returns a queryset of all edges for the shortest path from start_node to end_node
452+
Returns a QuerySet of all Edge instances for the shortest path from start_node to end_node
406453
"""
407454
return self.from_nodes_queryset(start_node.path(end_node, **kwargs))
408455

409456
def validate_route(self, edges, **kwargs):
410457
"""
411-
Given a list or set of edges, verify that they result in a contiguous route
458+
Given a list or set of Edge instances, verify that they result in a contiguous route
412459
"""
413460
# ToDo: Implement
414461
pass
415462

416463
def sort(self, edges, **kwargs):
417464
"""
418-
Given a list or set of edges, sort them from root-side to leaf-side
465+
Given a list or set of Edge instances, sort them from root-side to leaf-side
419466
"""
420467
# ToDo: Implement
421468
pass

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Contents
1010
:maxdepth: 2
1111

1212
quickstart
13+
methods
1314
transformations
1415

1516

0 commit comments

Comments
 (0)