File: treenode.py

package info (click to toggle)
treeline 3.1.5-1.1
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 6,508 kB
  • sloc: python: 20,489; javascript: 998; makefile: 54
file content (793 lines) | stat: -rw-r--r-- 32,270 bytes parent folder | download | duplicates (3)
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
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
#!/usr/bin/env python3

#******************************************************************************
# treenode.py, provides a class to store tree node data
#
# TreeLine, an information storage program
# Copyright (C) 2020, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, either Version 2 or any later
# version.  This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
#******************************************************************************

import re
import uuid
import operator
import itertools
import treespot
import nodeformat

_replaceBackrefRe = (re.compile(r'\\(\d+)'), re.compile(r'\\g<(\d+)>'))
_origBackrefMatch = None


class TreeNode:
    """Class to store tree node data and the tree's linked structure.

    Stores a data dict, lists of children and a format name string.
    Provides methods to get info on the structure and the data.
    """
    def __init__(self, formatRef, fileData=None):
        """Initialize a tree node.

        Arguments:
            formatRef -- a ref to this node's format info
            fileData -- a dict with uid, data, child refs & parent refs
        """
        self.formatRef = formatRef
        if not fileData:
            fileData = {}
        self.uId = fileData.get('uid', uuid.uuid1().hex)
        self.data = fileData.get('data', {})
        self.tmpChildRefs = fileData.get('children', [])
        self.childList = []
        self.spotRefs = set()

    def assignRefs(self, nodeDict):
        """Add actual refs to child nodes from data in self.tmpChildRefs.

        Any bad node refs (corrupt file data) are left in self.tmpChildRefs.
        Arguments:
            nodeDict -- all nodes stored by uid
        """
        try:
            self.childList = [nodeDict[uid] for uid in self.tmpChildRefs]
            self.tmpChildRefs = []
        except KeyError:   # due to corrupt file data
            badChildRefs = []
            for uid in self.tmpChildRefs:
                if uid in nodeDict:
                    self.childList.append(nodeDict[uid])
                else:
                    badChildRefs.append(uid)
            self.tmpChildRefs = badChildRefs

    def generateSpots(self, parentSpot):
        """Recursively generate spot references for this branch.

        Arguments:
            parentSpot -- the parent spot reference
        """
        spot = treespot.TreeSpot(self, parentSpot)
        self.spotRefs.add(spot)
        for child in self.childList:
            child.generateSpots(spot)

    def addSpotRef(self, parentNode, includeChildren=True):
        """Add a spot ref here to the given parent if not already there.

        If changed, propogate to descendant nodes.
        Arguments:
            parentNode -- the parent to ref in the new spot
            includeChildren -- if True, propogate to descendant nodes
        """
        changed = False
        origParentSpots = {spot.parentSpot for spot in self.spotRefs}
        for parentSpot in parentNode.spotRefs:
            if parentSpot not in origParentSpots:
                self.spotRefs.add(treespot.TreeSpot(self, parentSpot))
                changed = True
        if changed and includeChildren:
            for child in self.childList:
                child.addSpotRef(self)

    def removeInvalidSpotRefs(self, includeChildren=True, forceDesend=False):
        """Verify existing spot refs and remove any that aren't valid.

        If changed and includeChilderen, propogate to descendant nodes.
        Arguments:
            includeChildren -- if True, propogate to descendants if changes
            forceDesend -- if True, force propogate to descendant nodes
        """
        goodSpotRefs = {spot for spot in self.spotRefs if
                        (self in spot.parentSpot.nodeRef.childList and
                         spot.parentSpot in spot.parentSpot.nodeRef.spotRefs)}
        changed = len(self.spotRefs) != len(goodSpotRefs)
        self.spotRefs = goodSpotRefs
        if includeChildren and (changed or forceDesend):
            for child in self.childList:
                child.removeInvalidSpotRefs(includeChildren)

    def spotByNumber(self, num):
        """Return the spot at the given rank in the spot sequence.

        Arguments:
            num -- the rank number to return
        """
        spotList = sorted(list(self.spotRefs),
                          key=operator.methodcaller('sortKey'))
        return spotList[num]

    def matchedSpot(self, parentSpot):
        """Return the spot for this node that matches a parent spot.

        Return None if not found.
        Arguments:
            parentSpot -- the parent to match
        """
        for spot in self.spotRefs:
            if spot.parentSpot is parentSpot:
                return spot
        return None

    def setInitDefaultData(self, overwrite=False):
        """Add initial default data from fields into internal data.

        Arguments:
            overwrite -- if true, replace previous data entries
        """
        self.formatRef.setInitDefaultData(self.data, overwrite)

    def parents(self):
        """Return a set of parent nodes for this node.

        Returns an empty set if called from the tree structure.
        """
        try:
            return {spot.parentSpot.nodeRef for spot in self.spotRefs}
        except AttributeError:
            return set()

    def numChildren(self):
        """Return number of children.
        """
        return len(self.childList)

    def descendantGen(self):
        """Return a generator to step through all nodes in this branch.

        Includes self and closed nodes.
        """
        yield self
        for child in self.childList:
            for node in child.descendantGen():
                yield node

    def ancestors(self):
        """Return a set of all ancestor nodes (including self).
        """
        spots = set()
        for spot in self.spotRefs:
            spots.update(spot.spotChain())
        return {spot.nodeRef for spot in spots}

    def treeStructureRef(self):
        """Return the tree structure based on the root spot ref.
        """
        return next(iter(self.spotRefs)).rootSpot().nodeRef

    def fileData(self):
        """Return the file data dict for this node.
        """
        children = [node.uId for node in self.childList]
        fileData = {'format': self.formatRef.name, 'uid': self.uId,
                    'data': self.data, 'children': children}
        return fileData

    def title(self, spotRef=None):
        """Return the title string for this node.

        If spotRef not given, ancestor fields assume first spot.
        Arguments:
            spotRef -- optional, used for ancestor field refs
        """
        return self.formatRef.formatTitle(self, spotRef)

    def setTitle(self, title):
        """Change this node's data based on a new title string.

        Return True if successfully changed.
        """
        if title == self.title():
            return False
        return self.formatRef.extractTitleData(title, self.data)

    def output(self, plainText=False, keepBlanks=False, spotRef=None):
        """Return a list of formatted text output lines.

        If spotRef not given, ancestor fields assume first spot.
        Arguments:
            plainText -- if True, remove HTML markup from fields and formats
            keepBlanks -- if True, keep lines with empty fields
            spotRef -- optional, used for ancestor field refs
        """
        return self.formatRef.formatOutput(self, plainText, keepBlanks,
                                           spotRef)

    def changeDataType(self, formatRef):
        """Change this node's data type to the given name.

        Set init default data and update the title if blank.
        Arguments:
            formatRef -- the new tree format type
        """
        origTitle = self.title()
        self.formatRef = formatRef
        formatRef.setInitDefaultData(self.data)
        if not formatRef.formatTitle(self):
            formatRef.extractTitleData(origTitle, self.data)

    def setConditionalType(self, treeStructure):
        """Set self to type based on auto conditional settings.

        Return True if type is changed.
        Arguments:
            treeStructure -- a ref to the tree structure
        """
        if self.formatRef not in treeStructure.treeFormats.conditionalTypes:
            return False
        if self.formatRef.genericType:
            genericFormat = treeStructure.treeFormats[self.formatRef.
                                                      genericType]
        else:
            genericFormat = self.formatRef
        formatList = [genericFormat] + genericFormat.derivedTypes
        formatList.remove(self.formatRef)
        formatList.insert(0, self.formatRef)   # reorder to give priority
        neutralResult = None
        newType = None
        for typeFormat in formatList:
            if typeFormat.conditional:
                if typeFormat.conditional.evaluate(self):
                    newType = typeFormat
                    break
            elif not neutralResult:
                neutralResult = typeFormat
        if not newType and neutralResult:
            newType = neutralResult
        if newType and newType is not self.formatRef:
            self.changeDataType(newType)
            return True
        return False

    def setDescendantConditionalTypes(self, treeStructure):
        """Set auto conditional types for self and all descendants.

        Return number of changes made.
        Arguments:
            treeStructure -- a ref to the tree structure
        """
        if not treeStructure.treeFormats.conditionalTypes:
            return 0
        changes = 0
        for node in self.descendantGen():
            if node.setConditionalType(treeStructure):
                changes += 1
        return changes

    def setData(self, field, editorText):
        """Set the data entry for the given field to editorText.

        If the data does not match the format, sets to the raw text and
        raises a ValueError.
        Arguments:
            field-- the field object to be set
            editorText -- new text data from an editor
        """
        try:
            self.data[field.name] = field.storedText(editorText)
        except ValueError as err:
            if len(err.args) >= 2:
                self.data[field.name] = err.args[1]
            else:
                self.data[field.name] = editorText
            raise ValueError

    def wordSearch(self, wordList, titleOnly=False, spotRef=None):
        """Return True if all words in wordlist are found in this node's data.

        Arguments:
            wordList -- a list of words or phrases to find
            titleOnly -- search only in the title text if True
            spotRef -- an optional spot reference for ancestor field refs
        """
        dataStr = self.title(spotRef).lower()
        if not titleOnly:
            # join with null char so phrase matches don't cross borders
            dataStr = '{0}\0{1}'.format(dataStr,
                                        '\0'.join(self.data.values()).lower())
        for word in wordList:
            if word not in dataStr:
                return False
        return True

    def regExpSearch(self, regExpList, titleOnly=False, spotRef=None):
        """Return True if the regular expression is found in this node's data.

        Arguments:
            regExpList -- a list of regular expression objects to find
            titleOnly -- search only in the title text if True
            spotRef -- an optional spot reference for ancestor field refs
        """
        dataStr = self.title(spotRef)
        if not titleOnly:
            # join with null char so phrase matches don't cross borders
            dataStr = '{0}\0{1}'.format(dataStr, '\0'.join(self.data.values()))
        for regExpObj in regExpList:
            if not regExpObj.search(dataStr):
                return False
        return True

    def searchReplace(self, searchText='', regExpObj=None, skipMatches=0,
                      typeName='', fieldName='', replaceText=None,
                      replaceAll=False):
        """Find the search text in the field data and optionally replace it.

        Returns a tuple of the fieldName where found (empty string if not
        found), the node match number and the field match number.
        Returns the last match if skipMatches < 0 (not used with replace).
        Arguments:
            searchText -- the text to find if regExpObj is None
            regExpObj -- the regular expression to find if not None
            skipMatches -- number of already found matches to skip in this node
            typeName -- if given, verify that this node matches this type
            fieldName -- if given, only find matches under this type name
            replaceText -- if not None, replace a match with this string
            replaceAll -- if True, replace all matches (returns last fieldName)
        """
        if typeName and typeName != self.formatRef.name:
            return ('', 0, 0)
        try:
            fields = ([self.formatRef.fieldDict[fieldName]] if fieldName
                      else self.formatRef.fields())
        except KeyError:
            return ('', 0, 0)   # field not in this type
        matchedFieldname = ''
        findCount = 0
        prevFieldFindCount = 0
        for field in fields:
            try:
                fieldText = field.editorText(self)
            except ValueError:
                fieldText = self.data.get(field.name, '')
            fieldFindCount = 0
            pos = 0
            while True:
                if pos >= len(fieldText) and pos > 0:
                    break
                if regExpObj:
                    match = regExpObj.search(fieldText, pos)
                    pos = match.start() if match else -1
                else:
                    pos = fieldText.lower().find(searchText, pos)
                    if not searchText and fieldText:
                        pos = -1  # skip invalid find of empty string
                if pos < 0:
                    break
                findCount += 1
                fieldFindCount += 1
                prevFieldFindCount = fieldFindCount
                matchLen = (len(match.group()) if regExpObj
                            else len(searchText))
                if findCount > skipMatches:
                    matchedFieldname = field.name
                    if replaceText is not None:
                        replace = replaceText
                        if regExpObj:
                            global _origBackrefMatch
                            _origBackrefMatch = match
                            for backrefRe in _replaceBackrefRe:
                                replace = backrefRe.sub(self.replaceBackref,
                                                        replace)
                        fieldText = (fieldText[:pos] + replace +
                                     fieldText[pos + matchLen:])
                        try:
                            self.setData(field, fieldText)
                        except ValueError:
                            pass
                    if not replaceAll and skipMatches >= 0:
                        return (field.name, findCount, fieldFindCount)
                pos = pos + matchLen if matchLen else pos + 1
        if not matchedFieldname:
            findCount = prevFieldFindCount = 0
        return (matchedFieldname, findCount, prevFieldFindCount)

    @staticmethod
    def replaceBackref(match):
        """Return the re match group from _origBackrefMatch for replacement.

        Used for reg exp backreference replacement.
        Arguments:
            match -- the backref match in the replacement string
        """
        return _origBackrefMatch.group(int(match.group(1)))

    def addNewChild(self, treeStructure, posRefNode=None, insertBefore=True,
                    newTitle=_('New')):
        """Add a new child node with this node as the parent.

        Insert the new node near the posRefNode or at the end if no ref node.
        Return the new node.
        Arguments:
            treeStructure -- a ref to the tree structure
            posRefNode -- a child reference for the new node's position
            insertBefore -- insert before the ref node if True, after if False
        """
        try:
            newFormat = treeStructure.treeFormats[self.formatRef.childType]
        except (KeyError, AttributeError):
            if posRefNode:
                newFormat = posRefNode.formatRef
            elif self.childList:
                newFormat = self.childList[0].formatRef
            else:
                newFormat = self.formatRef
        newNode = TreeNode(newFormat)
        pos = len(self.childList)
        if posRefNode:
            pos = self.childList.index(posRefNode)
            if not insertBefore:
                pos += 1
        self.childList.insert(pos, newNode)
        newNode.setInitDefaultData()
        newNode.addSpotRef(self)
        if newTitle and not newNode.title():
            newNode.setTitle(newTitle)
        treeStructure.addNodeDictRef(newNode)
        return newNode

    def changeParent(self, oldParentSpot, newParentSpot, newPos=-1):
        """Move this node from oldParent to newParent.

        Used for indent and unindent commands.
        Arguments:
            oldParent -- the original parent spot
            newParent -- the new parent spot
            newPos -- the position in the new childList, -1 for append
        """
        oldParent = oldParentSpot.nodeRef
        oldParent.childList.remove(self)
        newParent = newParentSpot.nodeRef
        if newPos >= 0:
            newParent.childList.insert(newPos, self)
        else:
            newParent.childList.append(self)
        # preserve one spot to maintain tree expand state
        self.matchedSpot(oldParentSpot).parentSpot = newParentSpot
        self.removeInvalidSpotRefs()
        self.addSpotRef(newParent)

    def replaceChildren(self, titleList, treeStructure):
        """Replace child nodes with titles from a text list.

        Nodes with matches in the titleList are kept, others are added or
        deleted as required.
        Arguments:
            titleList -- the list of new child titles
            treeStructure -- a ref to the tree structure
        """
        try:
            newFormat = treeStructure.treeFormats[self.formatRef.childType]
        except (KeyError, AttributeError):
            newFormat = (self.childList[0].formatRef if self.childList
                         else self.formatRef)
        matchList = []
        remainTitles = [child.title() for child in self.childList]
        for title in titleList:
            try:
                match = self.childList.pop(remainTitles.index(title))
                matchList.append((title, match))
                remainTitles = [child.title() for child in self.childList]
            except ValueError:
                matchList.append((title, None))
        newChildList = []
        firstMiss = True
        for title, node in matchList:
            if not node:
                if (firstMiss and remainTitles and
                    remainTitles[0].startswith(title)):
                    # accept partial match on first miss for split tiles
                    node = self.childList.pop(0)
                    node.setTitle(title)
                else:
                    node = TreeNode(newFormat)
                    node.setTitle(title)
                    node.setInitDefaultData()
                    node.addSpotRef(self)
                    treeStructure.addNodeDictRef(node)
                firstMiss = False
            newChildList.append(node)
        for child in self.childList:
            for oldNode in child.descendantGen():
                if len(oldNode.spotRefs) <= 1:
                    treeStructure.removeNodeDictRef(oldNode)
                else:
                    oldNode.removeInvalidSpotRefs(False)
        self.childList = newChildList

    def replaceClonedBranches(self, origStruct):
        """Replace any duplicate IDs with clones from the given structure.

        Recursively search for duplicates.
        Arguments:
            origStruct -- the tree structure with the cloned nodes
        """
        for i in range(len(self.childList)):
            if self.childList[i].uId in origStruct.nodeDict:
                self.childList[i] = origStruct.nodeDict[self.childList[i].uId]
            else:
                self.childList[i].replaceClonedBranches(origStruct)

    def loadChildNodeLevels(self, nodeList, initLevel=-1):
        """Recursively add children from a list of nodes and levels.

        Return True on success, False if data levels are not valid.
        Arguments:
            nodeList -- list of tuples with node and level
            initLevel -- the level of this node in the structure
        """
        while nodeList:
            child, level = nodeList[0]
            if level == initLevel + 1:
                del nodeList[0]
                self.childList.append(child)
                if not child.loadChildNodeLevels(nodeList, level):
                    return False
            else:
                return -1 < level <= initLevel
        return True

    def fieldSortKey(self, level=0):
        """Return a key used to sort by key fields.

        Arguments:
            level -- the sort key depth level for the current sort stage
        """
        if len(self.formatRef.sortFields) > level:
            return self.formatRef.sortFields[level].sortKey(self)
        return ('',)

    def sortChildrenByField(self, recursive=True, forward=True):
        """Sort child nodes by predefined field keys.

        Arguments:
            recursive -- continue to sort recursively if true
            forward -- reverse the sort if false
        """
        formats = set([child.formatRef for child in self.childList])
        maxDepth = 0
        directions = []
        for nodeFormat in formats:
            if not nodeFormat.sortFields:
                nodeFormat.loadSortFields()
            maxDepth = max(maxDepth, len(nodeFormat.sortFields))
            newDirections = [field.sortKeyForward for field in
                             nodeFormat.sortFields]
            directions = [sum(i) for i in itertools.zip_longest(directions,
                                                                newDirections,
                                                                fillvalue=
                                                                False)]
        if forward:
            directions = [bool(direct) for direct in directions]
        else:
            directions = [not bool(direct) for direct in directions]
        for level in range(maxDepth, 0, -1):
            self.childList.sort(key = operator.methodcaller('fieldSortKey',
                                                            level - 1),
                                reverse = not directions[level - 1])
        if recursive:
            for child in self.childList:
                child.sortChildrenByField(True, forward)

    def titleSortKey(self):
        """Return a key used to sort by titles.
        """
        return self.title().lower()

    def sortChildrenByTitle(self, recursive=True, forward=True):
        """Sort child nodes by titles.

        Arguments:
            recursive -- continue to sort recursively if true
            forward -- reverse the sort if false
        """
        self.childList.sort(key = operator.methodcaller('titleSortKey'),
                            reverse = not forward)
        if recursive:
            for child in self.childList:
                child.sortChildrenByTitle(True, forward)

    def updateNodeMathFields(self, treeFormats):
        """Recalculate math fields that depend on this node and so on.

        Return True if any data was changed.
        Arguments:
            treeFormats -- a ref to all of the formats
        """
        changed = False
        for field in self.formatRef.fields():
            for fieldRef in treeFormats.mathFieldRefDict.get(field.name, []):
                for node in fieldRef.dependentEqnNodes(self):
                    if node.recalcMathField(fieldRef.eqnFieldName,
                                            treeFormats):
                        changed = True
        return changed

    def recalcMathField(self, eqnFieldName, treeFormats):
        """Recalculate a math field, if changed, recalc depending math fields.

        Return True if any data was changed.
        Arguments:
            eqnFieldName -- the equation field in this node to update
            treeFormats -- a ref to all of the formats
        """
        changed = False
        oldValue = self.data.get(eqnFieldName, '')
        newValue = self.formatRef.fieldDict[eqnFieldName].equationValue(self)
        if newValue != oldValue:
            self.data[eqnFieldName] = newValue
            changed = True
            for fieldRef in treeFormats.mathFieldRefDict.get(eqnFieldName, []):
                for node in fieldRef.dependentEqnNodes(self):
                    node.recalcMathField(fieldRef.eqnFieldName, treeFormats)
        return changed

    def updateNumbering(self, fieldDict, currentSequence, levelLimit,
                        completedClones, includeRoot=True, reserveNums=True,
                        restartSetting=False):
        """Add auto incremented numbering to fields by type in the dict.

        Arguments:
            fieldDict -- numbering field name lists stored by type name
            currentSequence -- a list of int for the current numbering sequence
            levelLimit -- the number of child levels to include
            completedClones -- set of clone nodes already numbered
            includeRoot -- if Ture, number the current node
            reserveNums -- if true, increment number even without num field
            restartSetting -- if true, restart numbering after a no-field gap
        """
        childSequence = currentSequence[:]
        if includeRoot:
            for fieldName in fieldDict.get(self.formatRef.name, []):
                self.data[fieldName] = '.'.join((repr(num) for num in
                                                 currentSequence))
            if self.formatRef.name in fieldDict or reserveNums:
                childSequence += [1]
                currentSequence[-1] += 1
            if restartSetting and self.formatRef.name not in fieldDict:
                currentSequence[-1] = 1
            if len(self.spotRefs) > 1:
                completedClones.add(self.uId)
        if levelLimit > 0:
            for child in self.childList:
                if len(child.spotRefs) > 1 and child.uId in completedClones:
                    return
                child.updateNumbering(fieldDict, childSequence, levelLimit - 1,
                                      completedClones, True, reserveNums,
                                      restartSetting)

    def isIdentical(self, node, checkParents=True):
        """Return True if node format, data and descendants are identical.

        Also returns False if checkParents & the nodes have parents in common.
        Arguments:
            node -- the node to check
        """
        if (self.formatRef != node.formatRef or
            len(self.childList) != len(node.childList) or
            self.data != node.data or
            (checkParents and not self.parents().isdisjoint(node.parents()))):
            return False
        for thisChild, otherChild in zip(self.childList, node.childList):
            if not thisChild.isIdentical(otherChild, False):
                return False
        return True

    def flatChildCategory(self, origFormats, structure):
        """Collapse descendant nodes by merging fields.

        Overwrites data in any fields with the same name.
        Arguments:
            origFormats -- copy of tree formats before any changes
            structure -- a ref to the tree structure
        """
        thisSpot = self.spotByNumber(0)
        newChildList = []
        for spot in thisSpot.spotDescendantOnlyGen():
            if not spot.nodeRef.childList:
                oldParentSpot = spot.parentSpot
                while oldParentSpot != thisSpot:
                    for field in origFormats[oldParentSpot.nodeRef.formatRef.
                                             name].fields():
                        data = oldParentSpot.nodeRef.data.get(field.name, '')
                        if data:
                            spot.nodeRef.data[field.name] = data
                        spot.nodeRef.formatRef.addFieldIfNew(field.name,
                                                            field.formatData())
                    oldParentSpot = oldParentSpot.parentSpot
                spot.parentSpot = thisSpot
                newChildList.append(spot.nodeRef)
            else:
                structure.removeNodeDictRef(spot.nodeRef)
        self.childList = newChildList

    def addChildCategory(self, catList, structure):
        """Insert category nodes above children.

        Arguments:
            catList -- the field names to add to the new level
            structure -- a ref to the tree structure
        """
        newFormat = None
        catSet = set(catList)
        similarFormats = [nodeFormat for nodeFormat in
                          structure.treeFormats.values() if
                          catSet.issubset(set(nodeFormat.fieldNames()))]
        if similarFormats:
            similarFormat = min(similarFormats, key=lambda f: len(f.fieldDict))
            if len(similarFormat.fieldDict) < len(self.childList[0].
                                                  formatRef.fieldDict):
                newFormat = similarFormat
        if not newFormat:
            newFormatName = '{0}_TYPE'.format(catList[0].upper())
            num = 1
            while newFormatName in structure.treeFormats:
                newFormatName = '{0}_TYPE_{1}'.format(catList[0].upper(), num)
                num += 1
            newFormat = nodeformat.NodeFormat(newFormatName,
                                              structure.treeFormats)
            newFormat.addFieldList(catList, True, True)
            structure.treeFormats[newFormatName] = newFormat
        newParents = []
        for child in self.childList:
            newParent = child.findEqualFields(catList, newParents)
            if not newParent:
                newParent = TreeNode(newFormat)
                for field in catList:
                    data = child.data.get(field, '')
                    if data:
                        newParent.data[field] = data
                structure.addNodeDictRef(newParent)
                newParents.append(newParent)
            newParent.childList.append(child)
        self.childList = newParents
        for child in self.childList:
            child.removeInvalidSpotRefs(True, True)
            child.addSpotRef(self)

    def findEqualFields(self, fieldNames, nodes):
        """Return first node in nodes with same data in fieldNames as self.

        Arguments:
            fieldNames -- the list of fields to check
            nodes -- the nodes to search for a match
        """
        for node in nodes:
            for field in fieldNames:
                if self.data.get(field, '') != node.data.get(field, ''):
                    break
            else:   # this for loop didn't hit break, so we have a match
                return node

    def exportTitleText(self, level=0):
        """Return a list of tabbed title lines for this node and descendants.

        Arguments:
            level -- indicates the indent level needed
        """
        textList = ['\t' * level + self.title()]
        for child in self.childList:
            textList.extend(child.exportTitleText(level + 1))
        return textList