-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdna_chain.py
433 lines (345 loc) · 12.7 KB
/
dna_chain.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
"""
11-11-15
The DNA structure is a doubly-linked list modified to support hierarchical
information. It's intended to be a simple balance between memory efficiency
and computational efficiency.
Below is a visual of the DNA structure. Each node has links to the next and
previous sibling. If it has a child, it has a link to the FIRST child in the
chain. The first child in a chain has a link back to its parent.
...
|
DNANode -- DNANode
| |
| DNANode
| |
| DNANode -- DNANode
| | |
| DNANode DNANode
|
DNANode
|
DNANode
|
DNANode -- DNANode
| |
| DNANode
|
DNANode
|
DNANode -- DNANode
|
DNANode
|
DNANode
|
...
"""
from collections import deque
__globals__ = ('DNANode',
'DNACrawlerException',
'DNACrawler')
class DNANode(object):
"""
The base unit of our DNA data structure.
"""
def __init__(self):
self._dna_node_child = None
self._dna_node_parent = None
self._dna_node_next_sib = None
self._dna_node_prev_sib = None
@property
def dna_node_child(self):
return self._dna_node_child
@property
def dna_node_parent(self):
return self._dna_node_parent
@property
def dna_node_next_sib(self):
return self._dna_node_next_sib
@property
def dna_node_prev_sib(self):
return self._dna_node_prev_sib
class DNACrawlerException(Exception):
pass
class DNACrawler(object):
"""
An object for traversing, reading, and editing the DNA structure.
Functionality and terminology:
"crawls" a DNA chain
"emits" events when editing the chain
"""
def __init__(self, dna):
self.dna = dna
self.__node = None
self.__parent_node_stack = deque()
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
# traversing/reading
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
def attach_to(self, node):
"""
The crawler will not be able to traverse any level higher than the node
it's originally attached to.
"""
self.__node = node
self.__parent_node_stack.clear()
def reset(self):
self.__node = self.dna.head
self.__parent_node_stack.clear()
@property
def current_node(self):
return self.__node
def next_node(self):
"""
Move to and return the next node in the DNA. Returns None if there is
no next.
"""
cur_node = self.__node
if cur_node is None:
return None
stack = self.__parent_node_stack
if cur_node.dna_node_child is not None:
stack.append(cur_node)
self.__node = cur_node.dna_node_child
elif cur_node.dna_node_next_sib is not None:
self.__node = cur_node.dna_node_next_sib
else:
# We may be at the end of the entire chain,
# or just the local chain.
if stack:
# At the end of a local chain.
# We may simply need to jump back up to the parent to continue,
# as in this case:
#
# JUMP FROM 3. TO 1.
#
# |----<---<---<---<---<-|
# \/ |
# 1. DNANode -- 2. DNANode |
# | | |
# | 3. DNANode ->--|
# 4. DNANode
# |
# ...
#
# ...or we may need to jump back up several parents, as in this
# case:
#
# JUMP FROM 5. TO 2., AND THEN FINALLY TO 1.
#
# |----<---<----|
# | |-----------|
# \/ \/ |
# 1. DNANode -- 2. DNANode |--<----<--|
# | | |
# | 3. DNANode -- 4. DNANode |
# 6. DNANode | |
# | 5. DNANode ->--|
# ...
#
# The loop construct used below can handle any number of jumps
# necessary to get back up the chain.
cur_parent = stack.pop()
while cur_parent.dna_node_next_sib is None and stack:
cur_parent = stack.pop()
self.__node = cur_parent.dna_node_next_sib
else:
# Nothing in the stack, at the end of the entire chain.
self.__node = None
return self.__node
def next_sib(self):
"""
Move to and return the next sibling in the DNA. Returns None if there
is no next.
"""
cur_node = self.__node
if cur_node is None:
return None
self.__node = cur_node.dna_node_next_sib
return self.__node
def crawl(self):
"""
Traverse the entire structure from the current node to the end.
"""
node = self.__node
while node is not None:
yield node
node = self.next_node()
def crawl_indents(self):
"""
Same as crawl but returns a tuple. The first item is the node and the
second item is the indentation of the node relative to the previous
node returned.
"""
stack = self.__parent_node_stack
node = self.__node
indent = len(stack)
while node is not None:
yield node, len(stack) - indent
indent = len(stack)
node = self.next_node()
def crawl_sibs(self):
"""
Traverse all the siblings coming after
(and including) the current node.
"""
node = self.__node
while node is not None:
yield node
node = self.next_sib()
def get_origin(self, node):
"""
Starts at the given node and climbs backwards and up the chain as far
as possible.
"""
while True:
if node.dna_node_prev_sib is not None:
node = node.dna_node_prev_sib
elif node.dna_node_parent is not None:
node = node.dna_node_parent
else:
break
return node
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
# editing "backend"
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
def __insert_before(self, node, ref_node=None):
"""
Insert node before ref_node. Use the current node if ref_node is not
specified.
"""
ref_node = self.__node if ref_node is None else ref_node
if ref_node is None:
raise DNACrawlerException(
"Cannot insert, no node specified and current node is None.")
if ref_node is self.dna.head:
self.dna.head = node
prev_n = ref_node._dna_node_prev_sib
parent = ref_node._dna_node_parent
# handle next/prev
ref_node._dna_node_prev_sib = node
node._dna_node_next_sib = ref_node
if prev_n is not None:
prev_n._dna_node_next_sib = node
node._dna_node_prev_sib = prev_n
# handle parent/child
if parent is not None:
ref_node._dna_node_parent = None
node._dna_node_parent = parent
parent._dna_node_child = node
return ref_node
def __insert_after(self, node, ref_node=None):
"""
Insert node after ref_node. User current node if ref_node is not
specified.
"""
ref_node = self.__node if ref_node is None else ref_node
if ref_node is None:
raise DNACrawlerException(
"Cannot insert, no node specified and current node is None.")
next_n = ref_node.dna_node_next_sib
ref_node._dna_node_next_sib = node
node._dna_node_prev_sib = ref_node
if next_n is not None:
next_n._dna_node_prev_sib = node
node._dna_node_next_sib = next_n
return ref_node
def __insert_child(self, node, ref_node=None):
"""
Insert node as child of ref_node. Use current node if ref_node is not
specified.
"""
ref_node = self.__node if ref_node is None else ref_node
if ref_node is None:
raise DNACrawlerException(
"Cannot insert, no node specified and current node is None.")
# If the node we are inserting as a child is the DNA head, then we
# need to update the head.
if ref_node is self.dna.head:
self.dna.head = self.get_origin(ref_node)
child = ref_node._dna_node_child
if child is None:
# Easy, there is no child.
ref_node._dna_node_child = node
node._dna_node_parent = ref_node
else:
# There is already a child
# TODO: this inserts node at the head of the list of children.
# TODO: we probably want to insert it at the tail.
self.__insert_before(node, child)
return ref_node
def __remove(self, node=None):
"""
Removes node from the chain. Use current node if node is not
specified.
"""
node = self.__node if node is None else node
if node is None:
raise DNACrawlerException(
"Cannot remove, no node specified and current node is None.")
prev_n = node.dna_node_prev_sib
next_n = node.dna_node_next_sib
# Update the DNA head if we are removing it.
if node is self.dna.head:
self.dna.head = next_n
node._dna_node_next_sib = None
node._dna_node_prev_sib = None
# Does this node have a parent?
if node.dna_node_parent is not None:
# There should not be a prev in this case, check if there is a
# next to link the parent to.
node.dna_node_parent._dna_node_child = next_n
if next_n is not None:
next_n._dna_node_parent = node.dna_node_parent
node._dna_node_parent = None
# Do we have a previous node?
if prev_n is not None:
prev_n._dna_node_next_sib = next_n
# Do we have a next node?
if next_n is not None:
next_n._dna_node_prev_sib = prev_n
def __create_node(self, node):
"""
Creates a new node if the given node is None.
"""
if node is None:
return self.dna.node_factory()
return node
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
# editing "fronted"
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
# TODO: if someone accidentally uses add_* when they should use move_*,
# that will be bad. The node will still be connected to other parts of
# the DNA chain. Need to do something about it. It's currently easy to
# make the mistake.
def add_before(self, node=None, ref_node=None):
node = self.__create_node(node)
ref_node = self.__insert_before(node, ref_node)
self.emit(('c', '+', node, 'b', ref_node))
def add_after(self, node=None, ref_node=None):
node = self.__create_node(node)
ref_node = self.__insert_after(node, ref_node)
self.emit(('c', '+', node, 'a', ref_node))
def add_child(self, node=None, ref_node=None):
node = self.__create_node(node)
ref_node = self.__insert_child(node, ref_node)
self.emit(('c', '+', node, 'c', ref_node))
def move_before(self, node, ref_node=None):
self.__remove(node)
ref_node = self.__insert_before(node, ref_node)
self.emit(('c', '^', node, 'b', ref_node))
def move_after(self, node, ref_node=None):
self.__remove(node)
ref_node = self.__insert_after(node, ref_node)
self.emit(('c', '^', node, 'a', ref_node))
def move_child(self, node, ref_node=None):
self.__remove(node)
ref_node = self.__insert_child(node, ref_node)
self.emit(('c', '^', node, 'c', ref_node))
def remove(self, node=None):
self.__remove(node)
self.emit(('c', '-', node))
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
# event emitting
# - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ - ~ -
def emit(self, event):
print(event)