/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
1
# -*- coding: UTF-8 -*-
2
"""Difference window.
3
4
This module contains the code to manage the diff window which shows
5
the changes made between two revisions on a branch.
6
"""
7
8
__copyright__ = "Copyright © 2005 Canonical Ltd."
9
__author__    = "Scott James Remnant <scott@ubuntu.com>"
10
11
12
from cStringIO import StringIO
13
252 by Aaron Bentley
Fix test suite
14
import pygtk
15
pygtk.require("2.0")
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
16
import gtk
17
import pango
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
18
import os
19
import re
76 by Jelmer Vernooij
Replace non-UTF8 characters rather than generating an exception (fixes #44677).
20
import sys
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
21
22
try:
23
    import gtksourceview
24
    have_gtksourceview = True
25
except ImportError:
26
    have_gtksourceview = False
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
27
try:
28
    import gconf
29
    have_gconf = True
30
except ImportError:
31
    have_gconf = False
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
32
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
33
from bzrlib import osutils
34
from bzrlib.diff import show_diff_trees, internal_diff
59.2.4 by Aaron Bentley
Teach gdiff to accept a single file argument
35
from bzrlib.errors import NoSuchFile
426 by Aaron Bentley
Start support for Merge Directives
36
from bzrlib.patches import parse_patches
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
37
from bzrlib.trace import warning
298.2.1 by Daniel Schierbeck
Refactored the GTK window code, creating a single base window class that handles keyboard events.
38
from bzrlib.plugins.gtk.window import Window
39
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
40
424 by Aaron Bentley
Add ghandle-patch
41
class DiffFileView(gtk.ScrolledWindow):
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
42
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
43
    def __init__(self):
278.1.4 by John Arbash Meinel
Just playing around.
44
        gtk.ScrolledWindow.__init__(self)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
45
        self.construct()
424 by Aaron Bentley
Add ghandle-patch
46
        self._diffs = {}
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
47
48
    def construct(self):
278.1.4 by John Arbash Meinel
Just playing around.
49
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
50
        self.set_shadow_type(gtk.SHADOW_IN)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
51
52
        if have_gtksourceview:
53
            self.buffer = gtksourceview.SourceBuffer()
54
            slm = gtksourceview.SourceLanguagesManager()
55
            gsl = slm.get_language_from_mime_type("text/x-patch")
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
56
            if have_gconf:
57
                self.apply_gedit_colors(gsl)
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
58
            self.apply_colordiff_colors(gsl)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
59
            self.buffer.set_language(gsl)
60
            self.buffer.set_highlight(True)
61
62
            sourceview = gtksourceview.SourceView(self.buffer)
63
        else:
64
            self.buffer = gtk.TextBuffer()
65
            sourceview = gtk.TextView(self.buffer)
66
67
        sourceview.set_editable(False)
68
        sourceview.modify_font(pango.FontDescription("Monospace"))
278.1.4 by John Arbash Meinel
Just playing around.
69
        self.add(sourceview)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
70
        sourceview.show()
71
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
72
    @staticmethod
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
73
    def apply_gedit_colors(lang):
74
        """Set style for lang to that specified in gedit configuration.
75
76
        This method needs the gconf module.
278.1.4 by John Arbash Meinel
Just playing around.
77
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
78
        :param lang: a gtksourceview.SourceLanguage object.
79
        """
80
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
81
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
82
83
        client = gconf.client_get_default()
84
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
85
86
        for tag in lang.get_tags():
87
            tag_id = tag.get_id()
88
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
89
            style_string = client.get_string(gconf_key)
90
91
            if style_string is None:
92
                continue
93
94
            # function to get a bool from a string that's either '0' or '1'
95
            string_bool = lambda x: bool(int(x))
96
97
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
98
            # values are: mask, fg, bg, italic, bold, underline, strike
99
            # this packs them into (str_value, attr_name, conv_func) tuples
100
            items = zip(style_string.split('/'), ['mask', 'foreground',
101
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
102
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
103
                    string_bool, string_bool, string_bool ]
104
            )
105
106
            style = gtksourceview.SourceTagStyle()
107
108
            # XXX The mask attribute controls whether the present values of
109
            # foreground and background color should in fact be used. Ideally
110
            # (and that's what gedit does), one could set all three attributes,
111
            # and let the TagStyle object figure out which colors to use.
112
            # However, in the GtkSourceview python bindings, the mask attribute
113
            # is read-only, and it's derived instead from the colors being
114
            # set or not. This means that we have to sometimes refrain from
115
            # setting fg or bg colors, depending on the value of the mask.
116
            # This code could go away if mask were writable.
117
            mask = int(items[0][0])
118
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
119
                items[2:3] = []
120
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
121
                items[1:2] = []
122
            items[0:1] = [] # skip the mask unconditionally
123
124
            for value, attr, func in items:
125
                try:
126
                    value = func(value)
127
                except ValueError:
128
                    warning('gconf key %s contains an invalid value: %s'
129
                            % gconf_key, value)
130
                else:
131
                    setattr(style, attr, value)
132
133
            lang.set_tag_style(tag_id, style)
134
424 by Aaron Bentley
Add ghandle-patch
135
    @classmethod
136
    def apply_colordiff_colors(klass, lang):
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
137
        """Set style colors for lang using the colordiff configuration file.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
138
139
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
140
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
141
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
142
        """
143
        colors = {}
144
145
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
146
            f = os.path.expanduser(f)
147
            if os.path.exists(f):
148
                try:
149
                    f = file(f)
150
                except IOError, e:
151
                    warning('could not open file %s: %s' % (f, str(e)))
152
                else:
424 by Aaron Bentley
Add ghandle-patch
153
                    colors.update(klass.parse_colordiffrc(f))
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
154
                    f.close()
155
156
        if not colors:
157
            # ~/.colordiffrc does not exist
158
            return
159
160
        mapping = {
161
                # map GtkSourceView tags to colordiff names
162
                # since GSV is richer, accept new names for extra bits,
163
                # defaulting to old names if they're not present
164
                'Added@32@line': ['newtext'],
165
                'Removed@32@line': ['oldtext'],
166
                'Location': ['location', 'diffstuff'],
167
                'Diff@32@file': ['file', 'diffstuff'],
168
                'Special@32@case': ['specialcase', 'diffstuff'],
169
        }
170
171
        for tag in lang.get_tags():
172
            tag_id = tag.get_id()
173
            keys = mapping.get(tag_id, [])
174
            color = None
175
176
            for key in keys:
177
                color = colors.get(key, None)
178
                if color is not None:
179
                    break
180
181
            if color is None:
182
                continue
183
184
            style = gtksourceview.SourceTagStyle()
185
            try:
186
                style.foreground = gtk.gdk.color_parse(color)
187
            except ValueError:
188
                warning('not a valid color: %s' % color)
189
            else:
190
                lang.set_tag_style(tag_id, style)
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
191
192
    @staticmethod
193
    def parse_colordiffrc(fileobj):
194
        """Parse fileobj as a colordiff configuration file.
278.1.4 by John Arbash Meinel
Just playing around.
195
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
196
        :return: A dict with the key -> value pairs.
197
        """
198
        colors = {}
199
        for line in fileobj:
200
            if re.match(r'^\s*#', line):
201
                continue
202
            if '=' not in line:
203
                continue
204
            key, val = line.split('=', 1)
205
            colors[key.strip()] = val.strip()
206
        return colors
207
278.1.4 by John Arbash Meinel
Just playing around.
208
    def set_trees(self, rev_tree, parent_tree):
209
        self.rev_tree = rev_tree
210
        self.parent_tree = parent_tree
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
211
#        self._build_delta()
212
213
#    def _build_delta(self):
214
#        self.parent_tree.lock_read()
215
#        self.rev_tree.lock_read()
216
#        try:
217
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
218
#            self.path_to_status = {}
219
#            self.path_to_diff = {}
220
#            source_inv = self.parent_tree.inventory
221
#            target_inv = self.rev_tree.inventory
222
#            for (file_id, real_path, change_type, display_path) in self.delta:
223
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
224
#                if change_type in ('modified', 'renamed and modified'):
225
#                    source_ie = source_inv[file_id]
226
#                    target_ie = target_inv[file_id]
227
#                    sio = StringIO()
228
#                    source_ie.diff(internal_diff, *old path, *old_tree,
229
#                                   *new_path, target_ie, self.rev_tree,
230
#                                   sio)
231
#                    self.path_to_diff[real_path] = 
232
#
233
#        finally:
234
#            self.rev_tree.unlock()
235
#            self.parent_tree.unlock()
278.1.4 by John Arbash Meinel
Just playing around.
236
237
    def show_diff(self, specific_files):
426 by Aaron Bentley
Start support for Merge Directives
238
        sections = []
239
        if specific_files is None:
240
            self.buffer.set_text(self._diffs[None])
241
        else:
242
            for specific_file in specific_files:
243
                sections.append(self._diffs[specific_file])
244
            self.buffer.set_text(''.join(sections))
424 by Aaron Bentley
Add ghandle-patch
245
246
247
class DiffView(DiffFileView):
248
    """This is the soft and chewy filling for a DiffWindow."""
249
250
    def __init__(self):
251
        DiffFileView.__init__(self)
252
        self.rev_tree = None
253
        self.parent_tree = None
254
255
    def show_diff(self, specific_files):
278.1.4 by John Arbash Meinel
Just playing around.
256
        s = StringIO()
278.1.18 by John Arbash Meinel
Start checking the diff view is correct.
257
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
258
                        old_label='', new_label='',
259
                        # path_encoding=sys.getdefaultencoding()
260
                        # The default is utf-8, but we interpret the file
261
                        # contents as getdefaultencoding(), so we should
262
                        # probably try to make the paths in the same encoding.
263
                        )
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
264
        # str.decode(encoding, 'replace') doesn't do anything. Because if a
265
        # character is not valid in 'encoding' there is nothing to replace, the
266
        # 'replace' is for 'str.encode()'
267
        try:
268
            decoded = s.getvalue().decode(sys.getdefaultencoding())
269
        except UnicodeDecodeError:
270
            try:
271
                decoded = s.getvalue().decode('UTF-8')
272
            except UnicodeDecodeError:
273
                decoded = s.getvalue().decode('iso-8859-1')
274
                # This always works, because every byte has a valid
275
                # mapping from iso-8859-1 to Unicode
276
        # TextBuffer must contain pure UTF-8 data
277
        self.buffer.set_text(decoded.encode('UTF-8'))
278.1.4 by John Arbash Meinel
Just playing around.
278
279
322.1.1 by Jelmer Vernooij
Merge Johns' gcommit improvements and fix conflicts against trunk.
280
class DiffWindow(Window):
278.1.4 by John Arbash Meinel
Just playing around.
281
    """Diff window.
282
283
    This object represents and manages a single window containing the
284
    differences between two revisions on a branch.
285
    """
286
322.1.1 by Jelmer Vernooij
Merge Johns' gcommit improvements and fix conflicts against trunk.
287
    def __init__(self, parent=None):
288
        Window.__init__(self, parent)
278.1.4 by John Arbash Meinel
Just playing around.
289
        self.set_border_width(0)
290
        self.set_title("bzrk diff")
291
292
        # Use two thirds of the screen by default
293
        screen = self.get_screen()
294
        monitor = screen.get_monitor_geometry(0)
295
        width = int(monitor.width * 0.66)
296
        height = int(monitor.height * 0.66)
297
        self.set_default_size(width, height)
298
299
        self.construct()
300
427 by Aaron Bentley
Add merge button when displaying merge directives
301
    def _get_button_bar(self):
302
        return None
303
278.1.4 by John Arbash Meinel
Just playing around.
304
    def construct(self):
305
        """Construct the window contents."""
306
        # The   window  consists  of   a  pane   containing:  the
307
        # hierarchical list  of files on  the left, and  the diff
308
        # for the currently selected file on the right.
427 by Aaron Bentley
Add merge button when displaying merge directives
309
        self.vbox = gtk.VBox()
310
        self.add(self.vbox)
311
        self.vbox.show()
424 by Aaron Bentley
Add ghandle-patch
312
        self.pane = gtk.HPaned()
427 by Aaron Bentley
Add merge button when displaying merge directives
313
        self.vbox.pack_end(self.pane, expand=True, fill=True)
314
        hbox = self._get_button_bar()
315
        if hbox is not None:
316
            self.vbox.pack_start(hbox, expand=False, fill=True)
424 by Aaron Bentley
Add ghandle-patch
317
        self.pane.show()
278.1.4 by John Arbash Meinel
Just playing around.
318
319
        # The file hierarchy: a scrollable treeview
320
        scrollwin = gtk.ScrolledWindow()
321
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
322
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
424 by Aaron Bentley
Add ghandle-patch
323
        self.pane.pack1(scrollwin)
278.1.4 by John Arbash Meinel
Just playing around.
324
        scrollwin.show()
325
326
        self.model = gtk.TreeStore(str, str)
327
        self.treeview = gtk.TreeView(self.model)
328
        self.treeview.set_headers_visible(False)
329
        self.treeview.set_search_column(1)
330
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
331
        scrollwin.add(self.treeview)
332
        self.treeview.show()
333
334
        cell = gtk.CellRendererText()
335
        cell.set_property("width-chars", 20)
336
        column = gtk.TreeViewColumn()
337
        column.pack_start(cell, expand=True)
338
        column.add_attribute(cell, "text", 0)
339
        self.treeview.append_column(column)
340
426 by Aaron Bentley
Start support for Merge Directives
341
    def set_diff_text(self, description, lines):
278.1.4 by John Arbash Meinel
Just playing around.
342
        # The diffs of the  selected file: a scrollable source or
343
        # text view
424 by Aaron Bentley
Add ghandle-patch
344
        self.diff_view = DiffFileView()
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
345
        self.diff_view.show()
424 by Aaron Bentley
Add ghandle-patch
346
        self.pane.pack2(self.diff_view)
347
        self.model.append(None, [ "Complete Diff", "" ])
426 by Aaron Bentley
Start support for Merge Directives
348
        self.diff_view._diffs[None] = ''.join(lines)
349
        for patch in parse_patches(lines):
350
            oldname = patch.oldname.split('\t')[0]
351
            newname = patch.newname.split('\t')[0]
352
            self.model.append(None, [oldname, newname])
353
            self.diff_view._diffs[newname] = str(patch)
427 by Aaron Bentley
Add merge button when displaying merge directives
354
        self.diff_view.show_diff(None)
278.1.4 by John Arbash Meinel
Just playing around.
355
356
    def set_diff(self, description, rev_tree, parent_tree):
357
        """Set the differences showed by this window.
358
359
        Compares the two trees and populates the window with the
360
        differences.
361
        """
424 by Aaron Bentley
Add ghandle-patch
362
        # The diffs of the  selected file: a scrollable source or
363
        # text view
364
        self.diff_view = DiffView()
365
        self.pane.pack2(self.diff_view)
366
        self.diff_view.show()
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
367
        self.diff_view.set_trees(rev_tree, parent_tree)
278.1.4 by John Arbash Meinel
Just playing around.
368
        self.rev_tree = rev_tree
369
        self.parent_tree = parent_tree
370
371
        self.model.clear()
372
        delta = self.rev_tree.changes_from(self.parent_tree)
373
374
        self.model.append(None, [ "Complete Diff", "" ])
375
376
        if len(delta.added):
377
            titer = self.model.append(None, [ "Added", None ])
378
            for path, id, kind in delta.added:
379
                self.model.append(titer, [ path, path ])
380
381
        if len(delta.removed):
382
            titer = self.model.append(None, [ "Removed", None ])
383
            for path, id, kind in delta.removed:
384
                self.model.append(titer, [ path, path ])
385
386
        if len(delta.renamed):
387
            titer = self.model.append(None, [ "Renamed", None ])
388
            for oldpath, newpath, id, kind, text_modified, meta_modified \
389
                    in delta.renamed:
390
                self.model.append(titer, [ oldpath, newpath ])
391
392
        if len(delta.modified):
393
            titer = self.model.append(None, [ "Modified", None ])
394
            for path, id, kind, text_modified, meta_modified in delta.modified:
395
                self.model.append(titer, [ path, path ])
396
397
        self.treeview.expand_all()
398
        self.set_title(description + " - bzrk diff")
427 by Aaron Bentley
Add merge button when displaying merge directives
399
        self.diff_view.show_diff(None)
278.1.4 by John Arbash Meinel
Just playing around.
400
401
    def set_file(self, file_path):
402
        tv_path = None
403
        for data in self.model:
404
            for child in data.iterchildren():
405
                if child[0] == file_path or child[1] == file_path:
406
                    tv_path = child.path
407
                    break
408
        if tv_path is None:
409
            raise NoSuchFile(file_path)
410
        self.treeview.set_cursor(tv_path)
411
        self.treeview.scroll_to_cell(tv_path)
412
413
    def _treeview_cursor_cb(self, *args):
414
        """Callback for when the treeview cursor changes."""
415
        (path, col) = self.treeview.get_cursor()
416
        specific_files = [ self.model[path][1] ]
417
        if specific_files == [ None ]:
418
            return
419
        elif specific_files == [ "" ]:
420
            specific_files = None
421
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
422
        self.diff_view.show_diff(specific_files)
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
423
424
427 by Aaron Bentley
Add merge button when displaying merge directives
425
class MergeDirectiveWindow(DiffWindow):
426
427
    def _get_button_bar(self):
428
        merge_button = gtk.Button('Merge')
429
        merge_button.show()
430
        merge_button.set_relief(gtk.RELIEF_NONE)
431
        hbox = gtk.HButtonBox()
432
        hbox.set_layout(gtk.BUTTONBOX_START)
433
        hbox.pack_start(merge_button, expand=False, fill=True)
434
        hbox.show()
435
        return hbox
436
437
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
438
def _iter_changes_to_status(source, target):
439
    """Determine the differences between trees.
440
441
    This is a wrapper around _iter_changes which just yields more
442
    understandable results.
443
444
    :param source: The source tree (basis tree)
445
    :param target: The target tree
446
    :return: A list of (file_id, real_path, change_type, display_path)
447
    """
448
    added = 'added'
449
    removed = 'removed'
450
    renamed = 'renamed'
451
    renamed_and_modified = 'renamed and modified'
452
    modified = 'modified'
453
    kind_changed = 'kind changed'
454
455
    # TODO: Handle metadata changes
456
457
    status = []
458
    target.lock_read()
459
    try:
460
        source.lock_read()
461
        try:
462
            for (file_id, paths, changed_content, versioned, parent_ids, names,
463
                 kinds, executables) in target._iter_changes(source):
464
465
                # Skip the root entry if it isn't very interesting
466
                if parent_ids == (None, None):
467
                    continue
468
469
                change_type = None
470
                if kinds[0] is None:
471
                    source_marker = ''
472
                else:
473
                    source_marker = osutils.kind_marker(kinds[0])
474
                if kinds[1] is None:
475
                    assert kinds[0] is not None
476
                    marker = osutils.kind_marker(kinds[0])
477
                else:
478
                    marker = osutils.kind_marker(kinds[1])
479
480
                real_path = paths[1]
481
                if real_path is None:
482
                    real_path = paths[0]
483
                assert real_path is not None
484
                display_path = real_path + marker
485
486
                present_source = versioned[0] and kinds[0] is not None
487
                present_target = versioned[1] and kinds[1] is not None
488
489
                if present_source != present_target:
490
                    if present_target:
491
                        change_type = added
492
                    else:
493
                        assert present_source
494
                        change_type = removed
495
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
496
                    # Renamed
497
                    if changed_content or executables[0] != executables[1]:
498
                        # and modified
499
                        change_type = renamed_and_modified
500
                    else:
501
                        change_type = renamed
502
                    display_path = (paths[0] + source_marker
503
                                    + ' => ' + paths[1] + marker)
504
                elif kinds[0] != kinds[1]:
505
                    change_type = kind_changed
506
                    display_path = (paths[0] + source_marker
507
                                    + ' => ' + paths[1] + marker)
508
                elif changed_content is True or executables[0] != executables[1]:
509
                    change_type = modified
510
                else:
511
                    assert False, "How did we get here?"
512
513
                status.append((file_id, real_path, change_type, display_path))
514
        finally:
515
            source.unlock()
516
    finally:
517
        target.unlock()
518
519
    return status