/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 bzrlib/_annotator_pyx.pyx

  • Committer: John Arbash Meinel
  • Date: 2009-07-06 20:21:34 UTC
  • mto: This revision was merged to the branch mainline in revision 4522.
  • Revision ID: john@arbash-meinel.com-20090706202134-19iakgxrs3yxi7k7
Tests that VF implementations support .get_annotator()
This also meant supporting no-graph style VF implementations, which
wasn't too bad.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2009 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
55
55
        PyObject *, PyObject *, int opid)
56
56
 
57
57
 
58
 
from bzrlib import _annotator_py
 
58
from bzrlib import errors, graph as _mod_graph, osutils, patiencediff, ui
 
59
 
 
60
 
 
61
cdef class _NeededTextIterator:
 
62
 
 
63
    cdef object counter
 
64
    cdef object text_cache
 
65
    cdef object stream
 
66
    cdef object ann_keys
 
67
    cdef object stream_len
 
68
    cdef object pb
 
69
    cdef int stream_is_consumed
 
70
    cdef int ann_key_pos
 
71
 
 
72
    def __init__(self, stream, text_cache, stream_len, ann_keys, pb=None):
 
73
        self.counter = 0
 
74
        self.stream = stream
 
75
        self.stream_len = stream_len
 
76
        self.text_cache = text_cache
 
77
        self.stream_len = stream_len
 
78
        self.ann_keys = list(ann_keys)
 
79
        self.pb = pb
 
80
        self.stream_is_consumed = 0
 
81
        self.ann_key_pos = 0
 
82
 
 
83
    def __iter__(self):
 
84
        return self
 
85
 
 
86
    cdef _get_ann_text(self):
 
87
        if self.ann_key_pos >= len(self.ann_keys):
 
88
            raise StopIteration
 
89
        key = self.ann_keys[self.ann_key_pos]
 
90
        self.ann_key_pos = self.ann_key_pos + 1
 
91
        lines = self.text_cache[key]
 
92
        num_lines = len(lines)
 
93
        return key, lines, num_lines
 
94
 
 
95
    def __next__(self):
 
96
        if self.stream_is_consumed:
 
97
            return self._get_ann_text()
 
98
        try:
 
99
            record = self.stream.next()
 
100
        except StopIteration:
 
101
            self.stream_is_consumed = 1
 
102
            return self._get_ann_text()
 
103
        if self.pb is not None:
 
104
            self.pb.update('extracting', self.counter, self.stream_len)
 
105
        if record.storage_kind == 'absent':
 
106
            raise errors.RevisionNotPresent(record.key, None)
 
107
        self.counter = self.counter + 1
 
108
        lines = osutils.chunks_to_lines(record.get_bytes_as('chunked'))
 
109
        num_lines = len(lines)
 
110
        self.text_cache[record.key] = lines
 
111
        return record.key, lines, num_lines
59
112
 
60
113
 
61
114
cdef int _check_annotations_are_lists(annotations,
83
136
    return 0
84
137
 
85
138
 
86
 
cdef PyObject *_next_tuple_entry(object tpl, Py_ssize_t *pos): # cannot_raise
87
 
    """Return the next entry from this tuple.
88
 
 
89
 
    :param tpl: The tuple we are investigating, *must* be a PyTuple
90
 
    :param pos: The last item we found. Will be updated to the new position.
91
 
    
92
 
    This cannot raise an exception, as it does no error checking.
93
 
    """
 
139
cdef PyObject *_next_tuple_entry(object tpl, Py_ssize_t *pos):
94
140
    pos[0] = pos[0] + 1
95
141
    if pos[0] >= PyTuple_GET_SIZE(tpl):
96
142
        return NULL
123
169
    new_ann = PyTuple_New(PyTuple_GET_SIZE(ann_one)
124
170
                          + PyTuple_GET_SIZE(ann_two))
125
171
    while left != NULL and right != NULL:
126
 
        # left == right is done by PyObject_RichCompareBool_ptr, however it
127
 
        # avoids a function call for a very common case. Drops 'time bzr
128
 
        # annotate NEWS' from 7.25s to 7.16s, so it *is* a visible impact.
129
 
        if (left == right
130
 
            or PyObject_RichCompareBool_ptr(left, right, Py_EQ)):
 
172
        if (PyObject_RichCompareBool_ptr(left, right, Py_EQ)):
131
173
            # Identical values, step both
132
174
            Py_INCREF_ptr(left)
133
175
            PyTuple_SET_ITEM_ptr(new_ann, out_pos, left)
161
203
    return new_ann
162
204
 
163
205
 
164
 
cdef int _apply_parent_annotations(annotations, parent_annotations,
165
 
                                   matching_blocks) except -1:
 
206
cdef _apply_parent_annotations(annotations, parent_annotations,
 
207
                               matching_blocks):
166
208
    """Apply the annotations from parent_annotations into annotations.
167
209
 
168
210
    matching_blocks defines the ranges that match.
187
229
            Py_INCREF_ptr(par_temp[idx])
188
230
            Py_DECREF_ptr(ann_temp[idx])
189
231
            ann_temp[idx] = par_temp[idx]
190
 
    return 0
191
 
 
192
 
 
193
 
cdef int _merge_annotations(this_annotation, annotations, parent_annotations,
194
 
                            matching_blocks, ann_cache) except -1:
195
 
    cdef Py_ssize_t parent_idx, ann_idx, lines_idx, match_len, idx
196
 
    cdef Py_ssize_t pos
197
 
    cdef PyObject *ann_temp, *par_temp
198
 
 
199
 
    _check_annotations_are_lists(annotations, parent_annotations)
200
 
    last_ann = None
201
 
    last_parent = None
202
 
    last_res = None
203
 
    for parent_idx, lines_idx, match_len in matching_blocks:
204
 
        _check_match_ranges(parent_annotations, annotations,
205
 
                            parent_idx, lines_idx, match_len)
206
 
        # For lines which match this parent, we will now resolve whether
207
 
        # this parent wins over the current annotation
208
 
        for idx from 0 <= idx < match_len:
209
 
            ann_idx = lines_idx + idx
210
 
            ann_temp = PyList_GET_ITEM(annotations, ann_idx)
211
 
            par_temp = PyList_GET_ITEM(parent_annotations, parent_idx + idx)
212
 
            if (ann_temp == par_temp):
213
 
                # This is parent, do nothing
214
 
                # Pointer comparison is fine here. Value comparison would
215
 
                # be ok, but it will be handled in the final if clause by
216
 
                # merging the two tuples into the same tuple
217
 
                # Avoiding the Py_INCREF and function call to
218
 
                # PyObject_RichCompareBool using pointer comparison drops
219
 
                # timing from 215ms => 125ms
220
 
                continue
221
 
            par_ann = <object>par_temp
222
 
            ann = <object>ann_temp
223
 
            if (ann is this_annotation):
224
 
                # Originally claimed 'this', but it was really in this
225
 
                # parent
226
 
                Py_INCREF(par_ann)
227
 
                PyList_SetItem(annotations, ann_idx, par_ann)
228
 
                continue
229
 
            # Resolve the fact that both sides have a different value for
230
 
            # last modified
231
 
            if (ann is last_ann and par_ann is last_parent):
232
 
                Py_INCREF(last_res)
233
 
                PyList_SetItem(annotations, ann_idx, last_res)
234
 
            else:
235
 
                new_ann = _combine_annotations(ann, par_ann, ann_cache)
236
 
                Py_INCREF(new_ann)
237
 
                PyList_SetItem(annotations, ann_idx, new_ann)
238
 
                last_ann = ann
239
 
                last_parent = par_ann
240
 
                last_res = new_ann
241
 
    return 0
242
 
 
243
 
 
244
 
class Annotator(_annotator_py.Annotator):
 
232
 
 
233
 
 
234
class Annotator:
245
235
    """Class that drives performing annotations."""
246
236
 
247
 
    def _update_from_first_parent(self, key, annotations, lines, parent_key):
 
237
    def __init__(self, vf):
 
238
        """Create a new Annotator from a VersionedFile."""
 
239
        self._vf = vf
 
240
        self._parent_map = {}
 
241
        self._text_cache = {}
 
242
        # Map from key => number of nexts that will be built from this key
 
243
        self._num_needed_children = {}
 
244
        self._annotations_cache = {}
 
245
        self._heads_provider = None
 
246
        self._ann_tuple_cache = {}
 
247
 
 
248
 
 
249
    def _update_needed_children(self, key, parent_keys):
 
250
        for parent_key in parent_keys:
 
251
            if parent_key in self._num_needed_children:
 
252
                self._num_needed_children[parent_key] += 1
 
253
            else:
 
254
                self._num_needed_children[parent_key] = 1
 
255
 
 
256
    def _get_needed_keys(self, key):
 
257
        """Determine the texts we need to get from the backing vf.
 
258
 
 
259
        :return: (vf_keys_needed, ann_keys_needed)
 
260
            vf_keys_needed  These are keys that we need to get from the vf
 
261
            ann_keys_needed Texts which we have in self._text_cache but we
 
262
                            don't have annotations for. We need to yield these
 
263
                            in the proper order so that we can get proper
 
264
                            annotations.
 
265
        """
 
266
        parent_map = self._parent_map
 
267
        # We need 1 extra copy of the node we will be looking at when we are
 
268
        # done
 
269
        self._num_needed_children[key] = 1
 
270
        vf_keys_needed = set()
 
271
        ann_keys_needed = set()
 
272
        needed_keys = set([key])
 
273
        while needed_keys:
 
274
            parent_lookup = []
 
275
            next_parent_map = {}
 
276
            for key in needed_keys:
 
277
                if key in self._parent_map:
 
278
                    # We don't need to lookup this key in the vf
 
279
                    if key not in self._text_cache:
 
280
                        # Extract this text from the vf
 
281
                        vf_keys_needed.add(key)
 
282
                    elif key not in self._annotations_cache:
 
283
                        # We do need to annotate
 
284
                        ann_keys_needed.add(key)
 
285
                        next_parent_map[key] = self._parent_map[key]
 
286
                else:
 
287
                    parent_lookup.append(key)
 
288
                    vf_keys_needed.add(key)
 
289
            needed_keys = set()
 
290
            next_parent_map.update(self._vf.get_parent_map(parent_lookup))
 
291
            for key, parent_keys in next_parent_map.iteritems():
 
292
                if parent_keys is None:
 
293
                    parent_keys = ()
 
294
                    next_parent_map[key] = ()
 
295
                self._update_needed_children(key, parent_keys)
 
296
                for key in parent_keys:
 
297
                    if key not in parent_map:
 
298
                        needed_keys.add(key)
 
299
            parent_map.update(next_parent_map)
 
300
            # _heads_provider does some graph caching, so it is only valid while
 
301
            # self._parent_map hasn't changed
 
302
            self._heads_provider = None
 
303
        return vf_keys_needed, ann_keys_needed
 
304
 
 
305
    def _get_needed_texts(self, key, pb=None):
 
306
        """Get the texts we need to properly annotate key.
 
307
 
 
308
        :param key: A Key that is present in self._vf
 
309
        :return: Yield (this_key, text, num_lines)
 
310
            'text' is an opaque object that just has to work with whatever
 
311
            matcher object we are using. Currently it is always 'lines' but
 
312
            future improvements may change this to a simple text string.
 
313
        """
 
314
        keys, ann_keys = self._get_needed_keys(key)
 
315
        if pb is not None:
 
316
            pb.update('getting stream', 0, len(keys))
 
317
        stream  = self._vf.get_record_stream(keys, 'topological', True)
 
318
        iterator = _NeededTextIterator(stream, self._text_cache, len(keys),
 
319
                                       ann_keys, pb)
 
320
        return iterator
 
321
 
 
322
    def _get_parent_annotations_and_matches(self, key, text, parent_key):
 
323
        """Get the list of annotations for the parent, and the matching lines.
 
324
 
 
325
        :param text: The opaque value given by _get_needed_texts
 
326
        :param parent_key: The key for the parent text
 
327
        :return: (parent_annotations, matching_blocks)
 
328
            parent_annotations is a list as long as the number of lines in
 
329
                parent
 
330
            matching_blocks is a list of (parent_idx, text_idx, len) tuples
 
331
                indicating which lines match between the two texts
 
332
        """
 
333
        parent_lines = self._text_cache[parent_key]
 
334
        parent_annotations = self._annotations_cache[parent_key]
 
335
        # PatienceSequenceMatcher should probably be part of Policy
 
336
        matcher = patiencediff.PatienceSequenceMatcher(None,
 
337
            parent_lines, text)
 
338
        matching_blocks = matcher.get_matching_blocks()
 
339
        return parent_annotations, matching_blocks
 
340
 
 
341
    def _update_from_one_parent(self, key, annotations, lines, parent_key):
248
342
        """Reannotate this text relative to its first parent."""
249
 
        (parent_annotations,
250
 
         matching_blocks) = self._get_parent_annotations_and_matches(
251
 
                                key, lines, parent_key)
 
343
        parent_annotations, matching_blocks = self._get_parent_annotations_and_matches(
 
344
            key, lines, parent_key)
252
345
 
253
346
        _apply_parent_annotations(annotations, parent_annotations,
254
347
                                  matching_blocks)
256
349
    def _update_from_other_parents(self, key, annotations, lines,
257
350
                                   this_annotation, parent_key):
258
351
        """Reannotate this text relative to a second (or more) parent."""
259
 
        (parent_annotations,
260
 
         matching_blocks) = self._get_parent_annotations_and_matches(
261
 
                                key, lines, parent_key)
262
 
        _merge_annotations(this_annotation, annotations, parent_annotations,
263
 
                           matching_blocks, self._ann_tuple_cache)
 
352
        cdef Py_ssize_t parent_idx, ann_idx, lines_idx, match_len, idx
 
353
        cdef Py_ssize_t pos
 
354
        cdef PyObject *ann_temp, *par_temp
 
355
        parent_annotations, matching_blocks = self._get_parent_annotations_and_matches(
 
356
            key, lines, parent_key)
 
357
        _check_annotations_are_lists(annotations, parent_annotations)
 
358
        last_ann = None
 
359
        last_parent = None
 
360
        last_res = None
 
361
        cache = self._ann_tuple_cache
 
362
        for parent_idx, lines_idx, match_len in matching_blocks:
 
363
            _check_match_ranges(parent_annotations, annotations,
 
364
                                parent_idx, lines_idx, match_len)
 
365
            # For lines which match this parent, we will now resolve whether
 
366
            # this parent wins over the current annotation
 
367
            for idx from 0 <= idx < match_len:
 
368
                ann_idx = lines_idx + idx
 
369
                ann_temp = PyList_GET_ITEM(annotations, ann_idx)
 
370
                par_temp = PyList_GET_ITEM(parent_annotations, parent_idx + idx)
 
371
                if (ann_temp == par_temp):
 
372
                    # This is parent, do nothing
 
373
                    # Pointer comparison is fine here. Value comparison would
 
374
                    # be ok, but it will be handled in the final if clause by
 
375
                    # merging the two tuples into the same tuple
 
376
                    # Avoiding the Py_INCREF by using pointer comparison drops
 
377
                    # timing from 215ms => 125ms
 
378
                    continue
 
379
                par_ann = <object>par_temp
 
380
                ann = <object>ann_temp
 
381
                if (ann is this_annotation):
 
382
                    # Originally claimed 'this', but it was really in this
 
383
                    # parent
 
384
                    Py_INCREF(par_ann)
 
385
                    PyList_SetItem(annotations, ann_idx, par_ann)
 
386
                    continue
 
387
                # Resolve the fact that both sides have a different value for
 
388
                # last modified
 
389
                if (ann is last_ann and par_ann is last_parent):
 
390
                    Py_INCREF(last_res)
 
391
                    PyList_SetItem(annotations, ann_idx, last_res)
 
392
                else:
 
393
                    new_ann = _combine_annotations(ann, par_ann, cache)
 
394
                    Py_INCREF(new_ann)
 
395
                    PyList_SetItem(annotations, ann_idx, new_ann)
 
396
                    last_ann = ann
 
397
                    last_parent = par_ann
 
398
                    last_res = new_ann
 
399
 
 
400
    def _record_annotation(self, key, parent_keys, annotations):
 
401
        self._annotations_cache[key] = annotations
 
402
        for parent_key in parent_keys:
 
403
            num = self._num_needed_children[parent_key]
 
404
            num = num - 1
 
405
            if num == 0:
 
406
                del self._text_cache[parent_key]
 
407
                del self._annotations_cache[parent_key]
 
408
                # Do we want to clean up _num_needed_children at this point as
 
409
                # well?
 
410
            self._num_needed_children[parent_key] = num
 
411
 
 
412
    def _annotate_one(self, key, text, num_lines):
 
413
        this_annotation = (key,)
 
414
        # Note: annotations will be mutated by calls to _update_from*
 
415
        annotations = [this_annotation] * num_lines
 
416
        parent_keys = self._parent_map[key]
 
417
        if parent_keys:
 
418
            self._update_from_one_parent(key, annotations, text, parent_keys[0])
 
419
            for parent in parent_keys[1:]:
 
420
                self._update_from_other_parents(key, annotations, text,
 
421
                                                this_annotation, parent)
 
422
        self._record_annotation(key, parent_keys, annotations)
 
423
 
 
424
    def add_special_text(self, key, parent_keys, text):
 
425
        """Add a specific text to the graph."""
 
426
        self._parent_map[key] = parent_keys
 
427
        self._text_cache[key] = osutils.split_lines(text)
 
428
        self._heads_provider = None
 
429
 
 
430
    def annotate(self, key):
 
431
        """Return annotated fulltext for the given key."""
 
432
        pb = ui.ui_factory.nested_progress_bar()
 
433
        try:
 
434
            for text_key, text, num_lines in self._get_needed_texts(key, pb=pb):
 
435
                self._annotate_one(text_key, text, num_lines)
 
436
        finally:
 
437
            pb.finished()
 
438
        try:
 
439
            annotations = self._annotations_cache[key]
 
440
        except KeyError:
 
441
            raise errors.RevisionNotPresent(key, self._vf)
 
442
        return annotations, self._text_cache[key]
 
443
 
 
444
    def _get_heads_provider(self):
 
445
        if self._heads_provider is None:
 
446
            self._heads_provider = _mod_graph.KnownGraph(self._parent_map)
 
447
        return self._heads_provider
264
448
 
265
449
    def annotate_flat(self, key):
266
450
        """Determine the single-best-revision to source for each line.
268
452
        This is meant as a compatibility thunk to how annotate() used to work.
269
453
        """
270
454
        cdef Py_ssize_t pos, num_lines
271
 
 
272
 
        from bzrlib import annotate
273
 
 
274
 
        custom_tiebreaker = annotate._break_annotation_tie
275
455
        annotations, lines = self.annotate(key)
 
456
        assert len(annotations) == len(lines)
276
457
        num_lines = len(lines)
277
458
        out = []
278
459
        heads = self._get_heads_provider().heads
284
465
            else:
285
466
                the_heads = heads(annotation)
286
467
                if len(the_heads) == 1:
287
 
                    for head in the_heads: break # get the item out of the set
 
468
                    for head in the_heads:
 
469
                        break
288
470
                else:
289
471
                    # We need to resolve the ambiguity, for now just pick the
290
472
                    # sorted smallest
291
 
                    head = self._resolve_annotation_tie(the_heads, line,
292
 
                                                        custom_tiebreaker)
 
473
                    head = sorted(the_heads)[0]
293
474
            PyList_Append(out, (head, line))
294
475
        return out