doorstop.core.tree   F
last analyzed

Complexity

Total Complexity 116

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Test Coverage

Coverage 99.34%

Importance

Changes 0
Metric Value
wmc 116
eloc 337
dl 0
loc 655
ccs 301
cts 303
cp 0.9934
rs 2
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
C Tree.find_item() 0 46 11
A Tree._get_prefix_of_children() 0 8 3
A Tree.add_item() 0 18 1
C Tree.from_list() 0 47 11
A Tree.delete() 0 6 2
A Tree.__str__() 0 2 1
A Tree.load() 0 17 4
B Tree.get_traceability() 0 31 8
B Tree.get_issues() 0 26 7
A Tree.documents() 0 4 1
A Tree.link_items() 0 24 2
A Tree.__len__() 0 5 2
B Tree.find_document() 0 39 8
B Tree._draw_lines() 0 27 7
A Tree.__getitem__() 0 2 1
C Tree._place() 0 50 10
A Tree.check_for_cycle() 0 18 3
C Tree._iter_rows() 0 50 11
A Tree.__iter__() 0 4 2
A Tree._symbol() 0 6 2
A Tree._draw_line() 0 13 2
A Tree.draw() 0 13 2
A Tree.remove_item() 0 23 4
A Tree.__init__() 0 10 1
A Tree.__bool__() 0 3 1
A Tree.create_document() 0 32 3
A Tree.unlink_items() 0 21 1
A Tree.edit_item() 0 20 2
A Tree.__repr__() 0 2 1
A Tree.vcs() 0 6 2

How to fix   Complexity   

Complexity

Complex classes like doorstop.core.tree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# SPDX-License-Identifier: LGPL-3.0-only
4 1
5
"""Representation of a hierarchy of documents."""
6 1
7 1
import sys
8
from itertools import chain
9 1
from typing import Dict, List, Optional
10 1
11 1
from doorstop import common, settings
12 1
from doorstop.common import DoorstopError, DoorstopWarning
13 1
from doorstop.core import vcs
14 1
from doorstop.core.base import BaseValidatable
15 1
from doorstop.core.document import Document
16
from doorstop.core.item import Item
17 1
from doorstop.core.types import UID, Prefix
18 1
19 1
UTF8 = 'utf-8'
20
CP437 = 'cp437'
21 1
ASCII = 'ascii'
22
23
BOX = {
24
    'end': {UTF8: '│   ', CP437: '┬   ', ASCII: '|   '},
25
    'tee': {UTF8: '├── ', CP437: '├── ', ASCII: '+-- '},
26
    'bend': {UTF8: '└── ', CP437: '└── ', ASCII: '+-- '},
27
    'pipe': {UTF8: '│   ', CP437: '│   ', ASCII: '|   '},
28
    'space': {UTF8: '    ', CP437: '    ', ASCII: '    '},
29
}
30
31
log = common.logger(__name__)
32
33
34
class Tree(BaseValidatable):  # pylint: disable=R0902
35
    """A bidirectional tree structure to store a hierarchy of documents.
36
37 1
    Although requirements link "upwards", bidirectionality simplifies
38
    document processing and validation.
39
40 1
    """
41
42
    @staticmethod
43
    def from_list(documents, root=None):
44
        """Initialize a new tree from a list of documents.
45
46
        :param documents: list of :class:`~doorstop.core.document.Document`
47
        :param root: path to root of the project
48 1
49 1
        :raises: :class:`~doorstop.common.DoorstopError` when the tree
50
            cannot be built
51
52
        :return: new :class:`~doorstop.core.tree.Tree`
53
54
        """
55
        if not documents:
56
            return Tree(document=None, root=root)
57
        unplaced = list(documents)
58
        for document in list(unplaced):
59
            if document.parent is None:
60
                log.info("root of the tree: {}".format(document))
61 1
                tree = Tree(document)
62 1
                document.tree = tree
63 1
                unplaced.remove(document)
64 1
                break
65 1
        else:
66 1
            raise DoorstopError("no root document")
67 1
68 1
        while unplaced:
69 1
            count = len(unplaced)
70 1
            for document in list(unplaced):
71
                if document.parent is None:
72 1
                    log.info("root of the tree: {}".format(document))
73
                    raise DoorstopError("multiple root documents")
74 1
                try:
75 1
                    tree._place(document)  # pylint: disable=W0212
76 1
                except DoorstopError as error:
77 1
                    log.debug(error)
78 1
                else:
79 1
                    log.info("added to tree: {}".format(document))
80 1
                    document.tree = tree
81 1
                    unplaced.remove(document)
82 1
83 1
            if len(unplaced) == count:  # no more documents could be placed
84
                log.debug("unplaced documents: {}".format(unplaced))
85 1
                msg = "unplaced document: {}".format(unplaced[0])
86 1
                raise DoorstopError(msg)
87 1
88
        return tree
89 1
90 1
    def __init__(self, document, parent=None, root=None):
91 1
        self.document = document
92 1
        self.root = root or document.root  # enables mock testing
93
        self.parent = parent
94 1
        self.children: List[Tree] = []
95
        self._vcs = None  # working copy reference loaded in a property
96 1
        self.request_next_number = None  # server method injected by clients
97 1
        self._loaded = False
98 1
        self._item_cache: Dict[str, Item] = {}
99 1
        self._document_cache: Dict[str, Optional[Document]] = {}
100 1
101 1
    def __repr__(self):
102 1
        return "<Tree {}>".format(self._draw_line())
103 1
104 1
    def __str__(self):
105 1
        return self._draw_line()
106
107 1
    def __len__(self):
108 1
        if self.document:
109
            return 1 + sum(len(child) for child in self.children)
110 1
        else:
111 1
            return 0
112
113 1
    def __bool__(self):
114 1
        """Even empty trees should be considered truthy."""
115 1
        return True
116
117 1
    def __getitem__(self, key):
118
        raise IndexError("{} cannot be indexed by key".format(self.__class__))
119 1
120 1
    def __iter__(self):
121
        if self.document:
122 1
            yield self.document
123 1
        yield from chain(*(iter(c) for c in self.children))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable c does not seem to be defined.
Loading history...
124
125 1
    def _place(self, document):
126 1
        """Attempt to place the document in the current tree.
127 1
128 1
        :param document: :class:`doorstop.core.document.Document` to add
129
130 1
        :raises: :class:`~doorstop.common.DoorstopError` if the document
131
            cannot yet be placed
132
133
        """
134
        log.debug("trying to add {}...".format(document))
135
        if not self.document:  # tree is empty
136
137
            if document.parent:
138
                msg = "unknown parent for {}: {}".format(document, document.parent)
139 1
                raise DoorstopError(msg)
140 1
            self.document = document
141
142 1
        elif document.parent:  # tree has documents, document has parent
143 1
144
            if document.parent.lower() == self.document.prefix.lower():
145 1
146 1
                # Current document is the parent
147
                node = Tree(document, self)
148 1
                self.children.append(node)
149
150 1
            else:
151
152
                # Search for the parent
153 1
                for child in self.children:
154 1
                    try:
155
                        child._place(document)  # pylint: disable=W0212
156
                    except DoorstopError:
157
                        pass  # the error is raised later
158
                    else:
159 1
                        break
160 1
                else:
161 1
                    msg = "unknown parent for {}: {}".format(document, document.parent)
162 1
                    raise DoorstopError(msg)
163 1
164
        else:  # tree has documents, but no parent specified for document
165 1
166
            msg = "no parent specified for {}".format(document)
167 1
            log.info(msg)
168
            prefixes = ', '.join(document.prefix for document in self)
169 1
            log.info("parent options: {}".format(prefixes))
170
            raise DoorstopError(msg)
171
172
        for document2 in self:
173 1
            children = self._get_prefix_of_children(document2)
174 1
            document2.children = children
175 1
176 1
    # attributes #############################################################
177 1
178
    @property
179 1
    def documents(self):
180 1
        """Get an list of documents in the tree."""
181 1
        return list(self)
182
183
    @property
184 1
    def vcs(self):
185
        """Get the working copy."""
186
        if self._vcs is None:
187 1
            self._vcs = vcs.load(self.root)
188
        return self._vcs
189 1
190
    # actions ################################################################
191
192 1
    # decorators are applied to methods in the associated classes
193 1
    def create_document(
194 1
        self, path, value, sep=None, digits=None, parent=None
195
    ):  # pylint: disable=R0913
196
        """Create a new document and add it to the tree.
197
198
        :param path: directory path for the new document
199 1
        :param value: document or prefix
200
        :param sep: separator between prefix and numbers
201
        :param digits: number of digits for the document's numbers
202
        :param parent: parent document's prefix
203
204
        :raises: :class:`~doorstop.common.DoorstopError` if the
205
            document cannot be created
206
207
        :return: newly created and placed document
208
            :class:`~doorstop.core.document.Document`
209
210
        """
211
        prefix = Prefix(value)
212
        document = Document.new(
213
            self, path, self.root, prefix, sep=sep, digits=digits, parent=parent
214
        )
215 1
        try:
216 1
            self._place(document)
217
        except DoorstopError:
218
            msg = "deleting unplaced directory {}...".format(document.path)
219 1
            log.debug(msg)
220 1
            document.delete()
221 1
            raise
222 1
        else:
223 1
            log.info("added to tree: {}".format(document))
224 1
        return document
225 1
226
    # decorators are applied to methods in the associated classes
227 1
    def add_item(self, value, number=None, level=None, reorder=True):
228 1
        """Add a new item to an existing document by prefix.
229
230
        :param value: document or prefix
231 1
        :param number: desired item number
232
        :param level: desired item level
233
        :param reorder: update levels of document items
234
235
        :raises: :class:`~doorstop.common.DoorstopError` if the item
236
            cannot be created
237
238
        :return: newly created :class:`~doorstop.core.item.Item`
239
240
        """
241
        prefix = Prefix(value)
242
        document = self.find_document(prefix)
243
        item = document.add_item(number=number, level=level, reorder=reorder)
244
        return item
245 1
246 1
    # decorators are applied to methods in the associated classes
247 1
    def remove_item(self, value, reorder=True):
248 1
        """Remove an item from a document by UID.
249
250
        :param value: item or UID
251 1
        :param reorder: update levels of document items
252
253
        :raises: :class:`~doorstop.common.DoorstopError` if the item
254
            cannot be removed
255
256
        :return: removed :class:`~doorstop.core.item.Item`
257
258
        """
259
        uid = UID(value)
260
        for document in self:
261
            try:
262
                document.find_item(uid)
263 1
            except DoorstopError:
264 1
                pass  # item not found in that document
265 1
            else:
266 1
                item = document.remove_item(uid, reorder=reorder)
267 1
                return item
268 1
269
        raise DoorstopError(UID.UNKNOWN_MESSAGE.format(k='', u=uid))
270 1
271 1
    def check_for_cycle(self, item, cid, path):
272
        """Check if a cyclic dependency would be created.
273 1
274
        :param item: an item on the dependency path
275
        :param cid: the child item's UID
276 1
        :param path: the path of UIDs from the child item to the item
277
278
        :raises: :class:`~doorstop.common.DoorstopError` if the link
279
            would create a cyclic dependency
280
        """
281
        for did in item.links:
282
            path2 = path + [did]
283
            if did in path:
284
                s = " -> ".join(list(map(str, path2)))
285
                msg = "link would create a cyclic dependency: {}".format(s)
286
                raise DoorstopError(msg)
287
            dep = self.find_item(did, _kind='dependency')
288
            self.check_for_cycle(dep, cid, path2)
289 1
290
    # decorators are applied to methods in the associated classes
291 1
    def link_items(self, cid, pid):
292
        """Add a new link between two items by UIDs.
293 1
294
        :param cid: child item's UID (or child item)
295 1
        :param pid: parent item's UID (or parent item)
296 1
297
        :raises: :class:`~doorstop.common.DoorstopError` if the link
298
            cannot be created
299 1
300
        :return: child :class:`~doorstop.core.item.Item`,
301
                 parent :class:`~doorstop.core.item.Item`
302
303
        """
304
        log.info("linking {} to {}...".format(cid, pid))
305
        # Find child item
306
        child = self.find_item(cid, _kind='child')
307
        # Find parent item
308
        parent = self.find_item(pid, _kind='parent')
309
        # Add link if it is not a self reference or cyclic dependency
310
        if child is parent:
311
            raise DoorstopError("link would be self reference")
312 1
        self.check_for_cycle(parent, child.uid, [child.uid, parent.uid])
313
        child.link(parent.uid)
314 1
        return child, parent
315
316 1
    # decorators are applied to methods in the associated classes`
317
    def unlink_items(self, cid, pid):
318 1
        """Remove a link between two items by UIDs.
319 1
320
        :param cid: child item's UID (or child item)
321
        :param pid: parent item's UID (or parent item)
322 1
323
        :raises: :class:`~doorstop.common.DoorstopError` if the link
324
            cannot be removed
325
326
        :return: child :class:`~doorstop.core.item.Item`,
327
                 parent :class:`~doorstop.core.item.Item`
328
329
        """
330
        log.info("unlinking '{}' from '{}'...".format(cid, pid))
331
        # Find child item
332
        child = self.find_item(cid, _kind='child')
333
        # Find parent item
334
        parent = self.find_item(pid, _kind='parent')
335
        # Remove link
336 1
        child.unlink(parent.uid)
337
        return child, parent
338 1
339 1
    # decorators are applied to methods in the associated classes
340
    def edit_item(self, uid, tool=None, launch=False):
341 1
        """Open an item for editing by UID.
342
343 1
        :param uid: item's UID (or item)
344
        :param tool: alternative text editor to open the item
345
        :param launch: open the text editor
346
347
        :raises: :class:`~doorstop.common.DoorstopError` if the item
348
            cannot be found
349
350
        :return: edited :class:`~doorstop.core.item.Item`
351
352
        """
353
        # Find the item
354 1
        item = self.find_item(uid)
355 1
        # Edit the item
356 1
        if launch:
357 1
            item.edit(tool=tool)
358 1
        # Return the item
359 1
        return item
360 1
361
    def find_document(self, value) -> Document:
362 1
        """Get a document by its prefix.
363 1
364 1
        :param value: document or prefix
365 1
366 1
        :raises: :class:`~doorstop.common.DoorstopError` if the document
367 1
            cannot be found
368 1
369 1
        :return: matching :class:`~doorstop.core.document.Document`
370 1
371 1
        """
372 1
        prefix = Prefix(value)
373 1
        log.debug("looking for document '{}'...".format(prefix))
374 1
        try:
375
            document = self._document_cache[prefix]
376 1
            if document:
377
                log.trace("found cached document: {}".format(document))  # type: ignore
378 1
                return document
379
            else:
380
                log.trace("found cached unknown: {}".format(prefix))  # type: ignore
381
        except KeyError:
382
            for document in self:
383
                if not document:
384
                    # TODO: mypy seems to think document can be None here, but that shouldn't be possible
385
                    continue
386
                if document.prefix == prefix:
387
                    log.trace("found document: {}".format(document))  # type: ignore
388
                    if settings.CACHE_DOCUMENTS:
389 1
                        self._document_cache[prefix] = document
390 1
                        log.trace(  # type: ignore
391 1
                            "cached document: {}".format(document)
392 1
                        )
393 1
                    return document
394 1
            log.debug("could not find document: {}".format(prefix))
395 1
            if settings.CACHE_DOCUMENTS:
396 1
                self._document_cache[prefix] = None
397 1
                log.trace("cached unknown: {}".format(prefix))  # type: ignore
398
399
        raise DoorstopError(Prefix.UNKNOWN_MESSAGE.format(prefix))
400
401 1
    def find_item(self, value, _kind=''):
402 1
        """Get an item by its UID.
403 1
404 1
        :param value: item or UID
405 1
406 1
        :raises: :class:`~doorstop.common.DoorstopError` if the item
407 1
            cannot be found
408
409 1
        :return: matching :class:`~doorstop.core.item.Item`
410 1
411 1
        """
412 1
        uid = UID(value)
413 1
        _kind = (' ' + _kind) if _kind else _kind  # for logging messages
414 1
        log.debug("looking for{} item '{}'...".format(_kind, uid))
415
        try:
416
            item = self._item_cache[uid]
417
            if item:
418 1
                log.trace("found cached item: {}".format(item))  # type: ignore
419 1
                if item.active:
420 1
                    return item
421 1
                else:
422
                    log.trace("item is inactive: {}".format(item))  # type: ignore
423 1
            else:
424
                log.trace("found cached unknown: {}".format(uid))  # type: ignore
425 1
        except KeyError:
426
            for document in self:
427
                try:
428
                    item = document.find_item(uid, _kind=_kind)
429
                except DoorstopError:
430
                    pass  # item not found in that document
431
                else:
432
                    log.trace("found item: {}".format(item))  # type: ignore
433
                    if settings.CACHE_ITEMS:
434
                        self._item_cache[uid] = item
435
                        log.trace("cached item: {}".format(item))  # type: ignore
436
                    if item.active:
437 1
                        return item
438 1
                    else:
439
                        log.trace("item is inactive: {}".format(item))  # type: ignore
440 1
441 1
            log.debug("could not find item: {}".format(uid))
442
            if settings.CACHE_ITEMS:
443 1
                self._item_cache[uid] = None
444 1
                log.trace("cached unknown: {}".format(uid))  # type: ignore
445
446
        raise DoorstopError(UID.UNKNOWN_MESSAGE.format(k=_kind, u=uid))
447
448 1
    def get_issues(self, skip=None, document_hook=None, item_hook=None):
449 1
        """Yield all the tree's issues.
450
451 1
        :param skip: list of document prefixes to skip
452
        :param document_hook: function to call for custom document validation
453
        :param item_hook: function to call for custom item validation
454
455
        :return: generator of :class:`~doorstop.common.DoorstopError`,
456
                              :class:`~doorstop.common.DoorstopWarning`,
457 1
                              :class:`~doorstop.common.DoorstopInfo`
458
459 1
        """
460 1
        hook = document_hook if document_hook else lambda **kwargs: []
461 1
        documents = list(self)
462 1
        # Check for documents
463
        if not documents:
464 1
            yield DoorstopWarning("no documents")
465 1
        # Check each document
466
        for document in documents:
467
            for issue in chain(
468 1
                hook(document=document, tree=self),
469 1
                document.get_issues(skip=skip, item_hook=item_hook),
470 1
            ):
471
                # Prepend the document's prefix to yielded exceptions
472
                if isinstance(issue, Exception):
473 1
                    yield type(issue)("{}: {}".format(document.prefix, issue))
474 1
475 1
    def get_traceability(self):
476 1
        """Return sorted rows of traceability slices.
477 1
478 1
        :return: list of list of :class:`~doorstop.core.item.Item` or `None`
479
480
        """
481 1
482
        def by_uid(row):
483 1
            row2 = []
484
            for item in row:
485 1
                if item:
486 1
                    row2.append('0' + str(item.uid))
487 1
                else:
488 1
                    row2.append('1')  # force `None` to sort after items
489 1
            return row2
490 1
491
        # Create mapping of document prefix to slice index
492 1
        mapping = {}
493
        for index, document in enumerate(self.documents):
494
            mapping[document.prefix] = index
495
496
        # Collect all rows
497
        rows = set()
498
        for index, document in enumerate(self.documents):
499
            for item in document:
500
                if item.active:
501
                    for row in self._iter_rows(item, mapping):
502 1
                        rows.add(row)
503
504
        # Sort rows
505 1
        return sorted(rows, key=by_uid)
506 1
507
    def _get_prefix_of_children(self, document):
508 1
        """Return the prefixes of the children of this document."""
509 1
        for child in self.children:
510
            if child.document == document:
511 1
                children = [c.document.prefix for c in child.children]
512
                return children
513
        children = [c.document.prefix for c in self.children]
514 1
        return children
515 1
516
    def _iter_rows(
517 1
        self, item, mapping, parent=True, child=True, row=None
518
    ):  # pylint: disable=R0913
519
        """Generate all traceability row slices.
520 1
521
        :param item: base :class:`~doorstop.core.item.Item` for slicing
522
        :param mapping: `dict` of document prefix to slice index
523 1
        :param parent: indicate recursion is in the parent direction
524 1
        :param child: indicates recursion is in the child direction
525 1
        :param row: currently generated row
526 1
527
        """
528 1
529 1
        class Row(list):
530 1
            """List type that tracks upper and lower boundaries."""
531 1
532 1
            def __init__(self, *args, parent=False, child=False, **kwargs):
533 1
                super().__init__(*args, **kwargs)
534
                # Flags to indicate upper and lower bounds have been hit
535 1
                self.parent = parent
536 1
                self.child = child
537
538
        if item.normative:
539 1
540 1
            # Start the next row or copy from recursion
541
            if row is None:
542 1
                row = Row([None] * len(mapping))
543
            else:
544
                row = Row(row, parent=row.parent, child=row.child)
545
546
            # Add the current item to the row
547
            row[mapping[item.document.prefix]] = item
548
549
            # Recurse to the next parent/child item
550
            if parent:
551
                items = item.parent_items
552 1
                for item2 in items:
553 1
                    yield from self._iter_rows(item2, mapping, child=False, row=row)
554 1
                if not items:
555 1
                    row.parent = True
556 1
            if child:
557
                items = item.child_items
558 1
                for item2 in items:
559
                    yield from self._iter_rows(item2, mapping, parent=False, row=row)
560 1
                if not items:
561
                    row.child = True
562
563
            # Yield the row if both boundaries have been hit
564
            if row.parent and row.child:
565
                yield tuple(row)
566
567
    def load(self, reload=False):
568
        """Load the tree's documents and items.
569
570 1
        Unlike the :class:`~doorstop.core.document.Document` and
571 1
        :class:`~doorstop.core.item.Item` class, this load method is not
572 1
        used internally. Its purpose is to force the loading of
573
        content in large trees where lazy loading may cause long delays
574 1
        late in processing.
575
576
        """
577 1
        if self._loaded and not reload:
578
            return
579 1
        log.info("loading the tree...")
580
        for document in self:
581 1
            document.load(reload=True)
582 1
        # Set meta attributes
583
        self._loaded = True
584 1
585
    def draw(self, encoding=None, html_links=False):
586 1
        """Get the tree structure as text.
587
588
        :param encoding: limit character set to:
589 1
590 1
            - `'utf-8'` - all characters
591
            - `'cp437'` - Code Page 437 characters
592 1
            - (other) - ASCII characters
593 1
594 1
        """
595
        encoding = encoding or getattr(sys.stdout, 'encoding', None)
596 1
        encoding = encoding.lower() if encoding else None
597 1
        return '\n'.join(self._draw_lines(encoding, html_links))
598 1
599 1
    def _draw_line(self):
600
        """Get the tree structure in one line."""
601 1
        # Build parent prefix string (`getattr` to enable mock testing)
602 1
        prefix = getattr(self.document, 'prefix', '') or str(self.document)
603 1
        # Build children prefix strings
604 1
        children = ", ".join(
605 1
            c._draw_line() for c in self.children  # pylint: disable=protected-access
606
        )
607 1
        # Format the tree
608
        if children:
609 1
            return "{} <- [ {} ]".format(prefix, children)
610
        else:
611
            return "{}".format(prefix)
612 1
613 1
    def _draw_lines(self, encoding, html_links=False):
614 1
        """Generate lines of the tree structure."""
615
        # Build parent prefix string (`getattr` to enable mock testing)
616
        prefix = getattr(self.document, 'prefix', '') or str(self.document)
617 1
        if html_links:
618
            prefix = '<a href="documents/{0}">{0}</a>'.format(prefix)
619 1
        yield prefix
620 1
        # Build child prefix strings
621 1
        for count, child in enumerate(self.children, start=1):
622 1
            if count == 1:
623
                yield self._symbol('end', encoding)
624
            else:
625
                yield self._symbol('pipe', encoding)
626
            if count < len(self.children):
627
                base = self._symbol('pipe', encoding)
628
                indent = self._symbol('tee', encoding)
629
            else:
630
                base = self._symbol('space', encoding)
631
                indent = self._symbol('bend', encoding)
632
            for index, line in enumerate(
633
                # pylint: disable=protected-access
634
                child._draw_lines(encoding, html_links)
635
            ):
636
                if index == 0:
637
                    yield indent + line
638
                else:
639
                    yield base + line
640
641
    @staticmethod
642
    def _symbol(name, encoding):
643
        """Get a drawing symbol based on encoding."""
644
        if encoding not in (UTF8, CP437):
645
            encoding = ASCII
646
        return BOX[name][encoding]
647
648
    # decorators are applied to methods in the associated classes
649
    def delete(self):
650
        """Delete the tree and its documents and items."""
651
        for document in self:
652
            document.delete()
653
        self.document = None
654
        self.children = []
655