/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/patches.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
 
2
# <aaron.bentley@utoronto.ca>
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
from .errors import (
 
21
    BzrError,
 
22
    )
 
23
 
 
24
import os
 
25
import re
 
26
 
 
27
 
 
28
binary_files_re = b'Binary files (.*) and (.*) differ\n'
 
29
 
 
30
 
 
31
class PatchSyntax(BzrError):
 
32
    """Base class for patch syntax errors."""
 
33
 
 
34
 
 
35
class BinaryFiles(BzrError):
 
36
 
 
37
    _fmt = 'Binary files section encountered.'
 
38
 
 
39
    def __init__(self, orig_name, mod_name):
 
40
        self.orig_name = orig_name
 
41
        self.mod_name = mod_name
 
42
 
 
43
 
 
44
class MalformedPatchHeader(PatchSyntax):
 
45
 
 
46
    _fmt = "Malformed patch header.  %(desc)s\n%(line)r"
 
47
 
 
48
    def __init__(self, desc, line):
 
49
        self.desc = desc
 
50
        self.line = line
 
51
 
 
52
 
 
53
class MalformedLine(PatchSyntax):
 
54
 
 
55
    _fmt = "Malformed line.  %(desc)s\n%(line)r"
 
56
 
 
57
    def __init__(self, desc, line):
 
58
        self.desc = desc
 
59
        self.line = line
 
60
 
 
61
 
 
62
class PatchConflict(BzrError):
 
63
 
 
64
    _fmt = ('Text contents mismatch at line %(line_no)d.  Original has '
 
65
            '"%(orig_line)s", but patch says it should be "%(patch_line)s"')
 
66
 
 
67
    def __init__(self, line_no, orig_line, patch_line):
 
68
        self.line_no = line_no
 
69
        self.orig_line = orig_line.rstrip('\n')
 
70
        self.patch_line = patch_line.rstrip('\n')
 
71
 
 
72
 
 
73
class MalformedHunkHeader(PatchSyntax):
 
74
 
 
75
    _fmt = "Malformed hunk header.  %(desc)s\n%(line)r"
 
76
 
 
77
    def __init__(self, desc, line):
 
78
        self.desc = desc
 
79
        self.line = line
 
80
 
 
81
 
 
82
def get_patch_names(iter_lines):
 
83
    line = next(iter_lines)
 
84
    try:
 
85
        match = re.match(binary_files_re, line)
 
86
        if match is not None:
 
87
            raise BinaryFiles(match.group(1), match.group(2))
 
88
        if not line.startswith(b"--- "):
 
89
            raise MalformedPatchHeader("No orig name", line)
 
90
        else:
 
91
            orig_name = line[4:].rstrip(b"\n")
 
92
    except StopIteration:
 
93
        raise MalformedPatchHeader("No orig line", "")
 
94
    try:
 
95
        line = next(iter_lines)
 
96
        if not line.startswith(b"+++ "):
 
97
            raise PatchSyntax("No mod name")
 
98
        else:
 
99
            mod_name = line[4:].rstrip(b"\n")
 
100
    except StopIteration:
 
101
        raise MalformedPatchHeader("No mod line", "")
 
102
    return (orig_name, mod_name)
 
103
 
 
104
 
 
105
def parse_range(textrange):
 
106
    """Parse a patch range, handling the "1" special-case
 
107
 
 
108
    :param textrange: The text to parse
 
109
    :type textrange: str
 
110
    :return: the position and range, as a tuple
 
111
    :rtype: (int, int)
 
112
    """
 
113
    tmp = textrange.split(b',')
 
114
    if len(tmp) == 1:
 
115
        pos = tmp[0]
 
116
        range = b"1"
 
117
    else:
 
118
        (pos, range) = tmp
 
119
    pos = int(pos)
 
120
    range = int(range)
 
121
    return (pos, range)
 
122
 
 
123
 
 
124
def hunk_from_header(line):
 
125
    import re
 
126
    matches = re.match(br'\@\@ ([^@]*) \@\@( (.*))?\n', line)
 
127
    if matches is None:
 
128
        raise MalformedHunkHeader("Does not match format.", line)
 
129
    try:
 
130
        (orig, mod) = matches.group(1).split(b" ")
 
131
    except (ValueError, IndexError) as e:
 
132
        raise MalformedHunkHeader(str(e), line)
 
133
    if not orig.startswith(b'-') or not mod.startswith(b'+'):
 
134
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
 
135
    try:
 
136
        (orig_pos, orig_range) = parse_range(orig[1:])
 
137
        (mod_pos, mod_range) = parse_range(mod[1:])
 
138
    except (ValueError, IndexError) as e:
 
139
        raise MalformedHunkHeader(str(e), line)
 
140
    if mod_range < 0 or orig_range < 0:
 
141
        raise MalformedHunkHeader("Hunk range is negative", line)
 
142
    tail = matches.group(3)
 
143
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
144
 
 
145
 
 
146
class HunkLine(object):
 
147
 
 
148
    def __init__(self, contents):
 
149
        self.contents = contents
 
150
 
 
151
    def get_str(self, leadchar):
 
152
        if self.contents == b"\n" and leadchar == b" " and False:
 
153
            return b"\n"
 
154
        if not self.contents.endswith(b'\n'):
 
155
            terminator = b'\n' + NO_NL
 
156
        else:
 
157
            terminator = b''
 
158
        return leadchar + self.contents + terminator
 
159
 
 
160
    def as_bytes(self):
 
161
        raise NotImplementedError
 
162
 
 
163
 
 
164
class ContextLine(HunkLine):
 
165
 
 
166
    def __init__(self, contents):
 
167
        HunkLine.__init__(self, contents)
 
168
 
 
169
    def as_bytes(self):
 
170
        return self.get_str(b" ")
 
171
 
 
172
 
 
173
class InsertLine(HunkLine):
 
174
    def __init__(self, contents):
 
175
        HunkLine.__init__(self, contents)
 
176
 
 
177
    def as_bytes(self):
 
178
        return self.get_str(b"+")
 
179
 
 
180
 
 
181
class RemoveLine(HunkLine):
 
182
    def __init__(self, contents):
 
183
        HunkLine.__init__(self, contents)
 
184
 
 
185
    def as_bytes(self):
 
186
        return self.get_str(b"-")
 
187
 
 
188
 
 
189
NO_NL = b'\\ No newline at end of file\n'
 
190
__pychecker__ = "no-returnvalues"
 
191
 
 
192
 
 
193
def parse_line(line):
 
194
    if line.startswith(b"\n"):
 
195
        return ContextLine(line)
 
196
    elif line.startswith(b" "):
 
197
        return ContextLine(line[1:])
 
198
    elif line.startswith(b"+"):
 
199
        return InsertLine(line[1:])
 
200
    elif line.startswith(b"-"):
 
201
        return RemoveLine(line[1:])
 
202
    else:
 
203
        raise MalformedLine("Unknown line type", line)
 
204
 
 
205
 
 
206
__pychecker__ = ""
 
207
 
 
208
 
 
209
class Hunk(object):
 
210
 
 
211
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
 
212
        self.orig_pos = orig_pos
 
213
        self.orig_range = orig_range
 
214
        self.mod_pos = mod_pos
 
215
        self.mod_range = mod_range
 
216
        self.tail = tail
 
217
        self.lines = []
 
218
 
 
219
    def get_header(self):
 
220
        if self.tail is None:
 
221
            tail_str = b''
 
222
        else:
 
223
            tail_str = b' ' + self.tail
 
224
        return b"@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
 
225
                                                      self.orig_range),
 
226
                                       self.range_str(self.mod_pos,
 
227
                                                      self.mod_range),
 
228
                                       tail_str)
 
229
 
 
230
    def range_str(self, pos, range):
 
231
        """Return a file range, special-casing for 1-line files.
 
232
 
 
233
        :param pos: The position in the file
 
234
        :type pos: int
 
235
        :range: The range in the file
 
236
        :type range: int
 
237
        :return: a string in the format 1,4 except when range == pos == 1
 
238
        """
 
239
        if range == 1:
 
240
            return b"%i" % pos
 
241
        else:
 
242
            return b"%i,%i" % (pos, range)
 
243
 
 
244
    def as_bytes(self):
 
245
        lines = [self.get_header()]
 
246
        for line in self.lines:
 
247
            lines.append(line.as_bytes())
 
248
        return b"".join(lines)
 
249
 
 
250
    __bytes__ = as_bytes
 
251
 
 
252
    def shift_to_mod(self, pos):
 
253
        if pos < self.orig_pos - 1:
 
254
            return 0
 
255
        elif pos > self.orig_pos + self.orig_range:
 
256
            return self.mod_range - self.orig_range
 
257
        else:
 
258
            return self.shift_to_mod_lines(pos)
 
259
 
 
260
    def shift_to_mod_lines(self, pos):
 
261
        position = self.orig_pos - 1
 
262
        shift = 0
 
263
        for line in self.lines:
 
264
            if isinstance(line, InsertLine):
 
265
                shift += 1
 
266
            elif isinstance(line, RemoveLine):
 
267
                if position == pos:
 
268
                    return None
 
269
                shift -= 1
 
270
                position += 1
 
271
            elif isinstance(line, ContextLine):
 
272
                position += 1
 
273
            if position > pos:
 
274
                break
 
275
        return shift
 
276
 
 
277
 
 
278
def iter_hunks(iter_lines, allow_dirty=False):
 
279
    '''
 
280
    :arg iter_lines: iterable of lines to parse for hunks
 
281
    :kwarg allow_dirty: If True, when we encounter something that is not
 
282
        a hunk header when we're looking for one, assume the rest of the lines
 
283
        are not part of the patch (comments or other junk).  Default False
 
284
    '''
 
285
    hunk = None
 
286
    for line in iter_lines:
 
287
        if line == b"\n":
 
288
            if hunk is not None:
 
289
                yield hunk
 
290
                hunk = None
 
291
            continue
 
292
        if hunk is not None:
 
293
            yield hunk
 
294
        try:
 
295
            hunk = hunk_from_header(line)
 
296
        except MalformedHunkHeader:
 
297
            if allow_dirty:
 
298
                # If the line isn't a hunk header, then we've reached the end
 
299
                # of this patch and there's "junk" at the end.  Ignore the
 
300
                # rest of this patch.
 
301
                return
 
302
            raise
 
303
        orig_size = 0
 
304
        mod_size = 0
 
305
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
 
306
            hunk_line = parse_line(next(iter_lines))
 
307
            hunk.lines.append(hunk_line)
 
308
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
 
309
                orig_size += 1
 
310
            if isinstance(hunk_line, (InsertLine, ContextLine)):
 
311
                mod_size += 1
 
312
    if hunk is not None:
 
313
        yield hunk
 
314
 
 
315
 
 
316
class BinaryPatch(object):
 
317
 
 
318
    def __init__(self, oldname, newname):
 
319
        self.oldname = oldname
 
320
        self.newname = newname
 
321
 
 
322
    def as_bytes(self):
 
323
        return b'Binary files %s and %s differ\n' % (self.oldname, self.newname)
 
324
 
 
325
 
 
326
class Patch(BinaryPatch):
 
327
 
 
328
    def __init__(self, oldname, newname):
 
329
        BinaryPatch.__init__(self, oldname, newname)
 
330
        self.hunks = []
 
331
 
 
332
    def as_bytes(self):
 
333
        ret = self.get_header()
 
334
        ret += b"".join([h.as_bytes() for h in self.hunks])
 
335
        return ret
 
336
 
 
337
    def get_header(self):
 
338
        return b"--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
339
 
 
340
    def stats_values(self):
 
341
        """Calculate the number of inserts and removes."""
 
342
        removes = 0
 
343
        inserts = 0
 
344
        for hunk in self.hunks:
 
345
            for line in hunk.lines:
 
346
                if isinstance(line, InsertLine):
 
347
                    inserts += 1
 
348
                elif isinstance(line, RemoveLine):
 
349
                    removes += 1
 
350
        return (inserts, removes, len(self.hunks))
 
351
 
 
352
    def stats_str(self):
 
353
        """Return a string of patch statistics"""
 
354
        return "%i inserts, %i removes in %i hunks" % \
 
355
            self.stats_values()
 
356
 
 
357
    def pos_in_mod(self, position):
 
358
        newpos = position
 
359
        for hunk in self.hunks:
 
360
            shift = hunk.shift_to_mod(position)
 
361
            if shift is None:
 
362
                return None
 
363
            newpos += shift
 
364
        return newpos
 
365
 
 
366
    def iter_inserted(self):
 
367
        """Iteraties through inserted lines
 
368
 
 
369
        :return: Pair of line number, line
 
370
        :rtype: iterator of (int, InsertLine)
 
371
        """
 
372
        for hunk in self.hunks:
 
373
            pos = hunk.mod_pos - 1
 
374
            for line in hunk.lines:
 
375
                if isinstance(line, InsertLine):
 
376
                    yield (pos, line)
 
377
                    pos += 1
 
378
                if isinstance(line, ContextLine):
 
379
                    pos += 1
 
380
 
 
381
 
 
382
def parse_patch(iter_lines, allow_dirty=False):
 
383
    '''
 
384
    :arg iter_lines: iterable of lines to parse
 
385
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
 
386
        Default False
 
387
    '''
 
388
    iter_lines = iter_lines_handle_nl(iter_lines)
 
389
    try:
 
390
        (orig_name, mod_name) = get_patch_names(iter_lines)
 
391
    except BinaryFiles as e:
 
392
        return BinaryPatch(e.orig_name, e.mod_name)
 
393
    else:
 
394
        patch = Patch(orig_name, mod_name)
 
395
        for hunk in iter_hunks(iter_lines, allow_dirty):
 
396
            patch.hunks.append(hunk)
 
397
        return patch
 
398
 
 
399
 
 
400
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
 
401
    '''
 
402
    :arg iter_lines: iterable of lines to parse for patches
 
403
    :kwarg allow_dirty: If True, allow comments and other non-patch text
 
404
        before the first patch.  Note that the algorithm here can only find
 
405
        such text before any patches have been found.  Comments after the
 
406
        first patch are stripped away in iter_hunks() if it is also passed
 
407
        allow_dirty=True.  Default False.
 
408
    '''
 
409
    # FIXME: Docstring is not quite true.  We allow certain comments no
 
410
    # matter what, If they startwith '===', '***', or '#' Someone should
 
411
    # reexamine this logic and decide if we should include those in
 
412
    # allow_dirty or restrict those to only being before the patch is found
 
413
    # (as allow_dirty does).
 
414
    regex = re.compile(binary_files_re)
 
415
    saved_lines = []
 
416
    dirty_head = []
 
417
    orig_range = 0
 
418
    beginning = True
 
419
 
 
420
    for line in iter_lines:
 
421
        if line.startswith(b'=== '):
 
422
            if len(saved_lines) > 0:
 
423
                if keep_dirty and len(dirty_head) > 0:
 
424
                    yield {'saved_lines': saved_lines,
 
425
                           'dirty_head': dirty_head}
 
426
                    dirty_head = []
 
427
                else:
 
428
                    yield saved_lines
 
429
                saved_lines = []
 
430
            dirty_head.append(line)
 
431
            continue
 
432
        if line.startswith(b'*** '):
 
433
            continue
 
434
        if line.startswith(b'#'):
 
435
            continue
 
436
        elif orig_range > 0:
 
437
            if line.startswith(b'-') or line.startswith(b' '):
 
438
                orig_range -= 1
 
439
        elif line.startswith(b'--- ') or regex.match(line):
 
440
            if allow_dirty and beginning:
 
441
                # Patches can have "junk" at the beginning
 
442
                # Stripping junk from the end of patches is handled when we
 
443
                # parse the patch
 
444
                beginning = False
 
445
            elif len(saved_lines) > 0:
 
446
                if keep_dirty and len(dirty_head) > 0:
 
447
                    yield {'saved_lines': saved_lines,
 
448
                           'dirty_head': dirty_head}
 
449
                    dirty_head = []
 
450
                else:
 
451
                    yield saved_lines
 
452
            saved_lines = []
 
453
        elif line.startswith(b'@@'):
 
454
            hunk = hunk_from_header(line)
 
455
            orig_range = hunk.orig_range
 
456
        saved_lines.append(line)
 
457
    if len(saved_lines) > 0:
 
458
        if keep_dirty and len(dirty_head) > 0:
 
459
            yield {'saved_lines': saved_lines,
 
460
                   'dirty_head': dirty_head}
 
461
        else:
 
462
            yield saved_lines
 
463
 
 
464
 
 
465
def iter_lines_handle_nl(iter_lines):
 
466
    """
 
467
    Iterates through lines, ensuring that lines that originally had no
 
468
    terminating \n are produced without one.  This transformation may be
 
469
    applied at any point up until hunk line parsing, and is safe to apply
 
470
    repeatedly.
 
471
    """
 
472
    last_line = None
 
473
    for line in iter_lines:
 
474
        if line == NO_NL:
 
475
            if not last_line.endswith(b'\n'):
 
476
                raise AssertionError()
 
477
            last_line = last_line[:-1]
 
478
            line = None
 
479
        if last_line is not None:
 
480
            yield last_line
 
481
        last_line = line
 
482
    if last_line is not None:
 
483
        yield last_line
 
484
 
 
485
 
 
486
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
 
487
    '''
 
488
    :arg iter_lines: iterable of lines to parse for patches
 
489
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
 
490
        selected places.  This includes comments before and after a patch
 
491
        for instance.  Default False.
 
492
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
 
493
        Default False.
 
494
    '''
 
495
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
 
496
        if 'dirty_head' in patch_lines:
 
497
            yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
 
498
                    'dirty_head': patch_lines['dirty_head']})
 
499
        else:
 
500
            yield parse_patch(patch_lines, allow_dirty)
 
501
 
 
502
 
 
503
def difference_index(atext, btext):
 
504
    """Find the indext of the first character that differs between two texts
 
505
 
 
506
    :param atext: The first text
 
507
    :type atext: str
 
508
    :param btext: The second text
 
509
    :type str: str
 
510
    :return: The index, or None if there are no differences within the range
 
511
    :rtype: int or NoneType
 
512
    """
 
513
    length = len(atext)
 
514
    if len(btext) < length:
 
515
        length = len(btext)
 
516
    for i in range(length):
 
517
        if atext[i] != btext[i]:
 
518
            return i
 
519
    return None
 
520
 
 
521
 
 
522
def iter_patched(orig_lines, patch_lines):
 
523
    """Iterate through a series of lines with a patch applied.
 
524
    This handles a single file, and does exact, not fuzzy patching.
 
525
    """
 
526
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
527
    get_patch_names(patch_lines)
 
528
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
 
529
 
 
530
 
 
531
def iter_patched_from_hunks(orig_lines, hunks):
 
532
    """Iterate through a series of lines with a patch applied.
 
533
    This handles a single file, and does exact, not fuzzy patching.
 
534
 
 
535
    :param orig_lines: The unpatched lines.
 
536
    :param hunks: An iterable of Hunk instances.
 
537
    """
 
538
    seen_patch = []
 
539
    line_no = 1
 
540
    if orig_lines is not None:
 
541
        orig_lines = iter(orig_lines)
 
542
    for hunk in hunks:
 
543
        while line_no < hunk.orig_pos:
 
544
            orig_line = next(orig_lines)
 
545
            yield orig_line
 
546
            line_no += 1
 
547
        for hunk_line in hunk.lines:
 
548
            seen_patch.append(hunk_line.contents)
 
549
            if isinstance(hunk_line, InsertLine):
 
550
                yield hunk_line.contents
 
551
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
 
552
                orig_line = next(orig_lines)
 
553
                if orig_line != hunk_line.contents:
 
554
                    raise PatchConflict(line_no, orig_line,
 
555
                                        b''.join(seen_patch))
 
556
                if isinstance(hunk_line, ContextLine):
 
557
                    yield orig_line
 
558
                else:
 
559
                    if not isinstance(hunk_line, RemoveLine):
 
560
                        raise AssertionError(hunk_line)
 
561
                line_no += 1
 
562
    if orig_lines is not None:
 
563
        for line in orig_lines:
 
564
            yield line
 
565
 
 
566
 
 
567
def apply_patches(tt, patches, prefix=1):
 
568
    """Apply patches to a TreeTransform.
 
569
 
 
570
    :param tt: TreeTransform instance
 
571
    :param patches: List of patches
 
572
    :param prefix: Number leading path segments to strip
 
573
    """
 
574
    def strip_prefix(p):
 
575
        return '/'.join(p.split('/')[1:])
 
576
 
 
577
    from breezy.bzr.generate_ids import gen_file_id
 
578
    # TODO(jelmer): Extract and set mode
 
579
    for patch in patches:
 
580
        if patch.oldname == b'/dev/null':
 
581
            trans_id = None
 
582
            orig_contents = b''
 
583
        else:
 
584
            oldname = strip_prefix(patch.oldname.decode())
 
585
            trans_id = tt.trans_id_tree_path(oldname)
 
586
            orig_contents = tt._tree.get_file_text(oldname)
 
587
            tt.delete_contents(trans_id)
 
588
 
 
589
        if patch.newname != b'/dev/null':
 
590
            newname = strip_prefix(patch.newname.decode())
 
591
            new_contents = iter_patched_from_hunks(
 
592
                orig_contents.splitlines(True), patch.hunks)
 
593
            if trans_id is None:
 
594
                parts = os.path.split(newname)
 
595
                trans_id = tt.root
 
596
                for part in parts[1:-1]:
 
597
                    trans_id = tt.new_directory(part, trans_id)
 
598
                tt.new_file(
 
599
                    parts[-1], trans_id, new_contents,
 
600
                    file_id=gen_file_id(newname))
 
601
            else:
 
602
                tt.create_file(new_contents, trans_id)
 
603
 
 
604
 
 
605
class AppliedPatches(object):
 
606
    """Context that provides access to a tree with patches applied.
 
607
    """
 
608
 
 
609
    def __init__(self, tree, patches, prefix=1):
 
610
        self.tree = tree
 
611
        self.patches = patches
 
612
        self.prefix = prefix
 
613
 
 
614
    def __enter__(self):
 
615
        from .transform import TransformPreview
 
616
        self._tt = TransformPreview(self.tree)
 
617
        apply_patches(self._tt, self.patches, prefix=self.prefix)
 
618
        return self._tt.get_preview_tree()
 
619
 
 
620
    def __exit__(self, exc_type, exc_value, exc_tb):
 
621
        self._tt.finalize()
 
622
        return False