Failed Conditions
Pull Request — master (#2077)
by Lasse
02:14
created

coalib/results/Diff.py (27 issues)

1
import copy
2
import difflib
3
4
from coalib.results.LineDiff import LineDiff, ConflictError
5
from coalib.results.SourceRange import SourceRange
6
from coalib.misc.Decorators import enforce_signature, generate_eq
7
8
9
@generate_eq("_file", "modified", "rename", "delete")
10
class Diff:
11
    """
12
    A Diff result represents a difference for one file.
13
    """
14
15
    def __init__(self, file_list, rename=False, delete=False):
16
        """
17
        Creates an empty diff for the given file.
18
19
        :param file_list: The original (unmodified) file as a list of its
20
                          lines.
21
        :param rename:    False or str containing new name of file.
22
        :param delete:    True if file is set to be deleted.
23
        """
24
        self._changes = {}
25
        self._file = file_list
26
        self.rename = rename
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable rename does not seem to be defined.
Loading history...
27
        self.delete = delete
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable delete does not seem to be defined.
Loading history...
28
29
    @classmethod
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable classmethod does not seem to be defined.
Loading history...
30
    def from_string_arrays(cls, file_array_1, file_array_2):
31
        """
32
        Creates a Diff object from two arrays containing strings.
33
34
        If this Diff is applied to the original array, the second array will be
35
        created.
36
37
        :param file_array_1: Original array
38
        :param file_array_2: Array to compare
39
        """
40
        result = cls(file_array_1)
41
42
        matcher = difflib.SequenceMatcher(None, file_array_1, file_array_2)
43
        # We use this because its faster (generator) and doesnt yield as much
44
        # useless information as get_opcodes.
45
        for change_group in matcher.get_grouped_opcodes(1):
46
            for (tag,
47
                 a_index_1,
48
                 a_index_2,
49
                 b_index_1,
50
                 b_index_2) in change_group:
51
                if tag == "delete":
52
                    for index in range(a_index_1+1, a_index_2+1):
53
                        result.delete_line(index)
54
                elif tag == "insert":
55
                    # We add after line, they add before, so dont add 1 here
56
                    result.add_lines(a_index_1,
57
                                     file_array_2[b_index_1:b_index_2])
58
                elif tag == "replace":
59
                    result.change_line(a_index_1+1,
60
                                       file_array_1[a_index_1],
61
                                       file_array_2[b_index_1])
62
                    result.add_lines(a_index_1+1,
63
                                     file_array_2[b_index_1+1:b_index_2])
64
                    for index in range(a_index_1+2, a_index_2+1):
65
                        result.delete_line(index)
66
67
        return result
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable result does not seem to be defined.
Loading history...
68
69
    @classmethod
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable classmethod does not seem to be defined.
Loading history...
70
    def from_clang_fixit(cls, fixit, file):
71
        """
72
        Creates a Diff object from a given clang fixit and the file contents.
73
74
        :param fixit: A cindex.Fixit object.
75
        :param file:  A list of lines in the file to apply the fixit to.
76
        :return:      The corresponding Diff object.
77
        """
78
        assert isinstance(file, (list, tuple))
79
80
        oldvalue = '\n'.join(file[fixit.range.start.line-1:
81
                                  fixit.range.end.line])
82
        endindex = fixit.range.end.column - len(file[fixit.range.end.line-1])-1
83
84
        newvalue = (oldvalue[:fixit.range.start.column-1] +
85
                    fixit.value +
86
                    oldvalue[endindex:])
87
        new_file = (file[:fixit.range.start.line-1] +
88
                    type(file)(newvalue.splitlines(True)) +
89
                    file[fixit.range.end.line:])
90
91
        return cls.from_string_arrays(file, new_file)
92
93
    def _get_change(self, line_nr, min_line=1):
94
        if not isinstance(line_nr, int):
95
            raise TypeError("line_nr needs to be an integer.")
96
        if line_nr < min_line:
97
            raise ValueError("The given line number is not allowed.")
98
99
        return self._changes.get(line_nr, LineDiff())
100
101
    def __len__(self):
102
        return len(self._changes)
103
104
    @property
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable property does not seem to be defined.
Loading history...
105
    def rename(self):
106
        """
107
        :return: string containing new name of the file.
108
        """
109
        return self._rename
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
110
111
    @rename.setter
112
    @enforce_signature
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable enforce_signature does not seem to be defined.
Loading history...
113
    def rename(self, rename: (str, False)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable str does not seem to be defined.
Loading history...
114
        """
115
        :param rename: False or string containing new name of file.
116
        """
117
        self._rename = rename
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable rename does not seem to be defined.
Loading history...
118
119
    @property
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable property does not seem to be defined.
Loading history...
120
    def delete(self):
121
        """
122
        :return: True if file is set to be deleted.
123
        """
124
        return self._delete
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
125
126
    @delete.setter
127
    @enforce_signature
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable enforce_signature does not seem to be defined.
Loading history...
128
    def delete(self, delete: bool):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable bool does not seem to be defined.
Loading history...
129
        """
130
        :param delete: True if file is set to be deleted, False otherwise.
131
        """
132
        self._delete = delete
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable delete does not seem to be defined.
Loading history...
133
134
    @property
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable property does not seem to be defined.
Loading history...
135
    def original(self):
136
        """
137
        Retrieves the original file.
138
        """
139
        return self._file
140
141
    @property
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable property does not seem to be defined.
Loading history...
142
    def modified(self):
143
        """
144
        Calculates the modified file, after applying the Diff to the original.
145
        """
146
        result = []
147
148
        if self.delete:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
149
            return result
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable result does not seem to be defined.
Loading history...
150
151
        current_line = 0
152
153
        # Note that line_nr counts from _1_ although 0 is possible when
154
        # inserting lines before everything
155
        for line_nr in sorted(self._changes):
156
            result.extend(self._file[current_line:max(line_nr-1, 0)])
157
            linediff = self._changes[line_nr]
158
            if not linediff.delete and not linediff.change and line_nr > 0:
159
                result.append(self._file[line_nr-1])
160
            elif linediff.change:
161
                result.append(linediff.change[1])
162
163
            if linediff.add_after:
164
                result.extend(linediff.add_after)
165
166
            current_line = line_nr
167
168
        result.extend(self._file[current_line:])
169
170
        return result
171
172
    @property
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable property does not seem to be defined.
Loading history...
173
    def unified_diff(self):
174
        """
175
        Generates a unified diff corresponding to this patch.
176
177
        Note that the unified diff is not deterministic and thus not suitable
178
        for equality comparison.
179
        """
180
        return ''.join(difflib.unified_diff(
181
            self.original,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
182
            self.modified if not self.delete else [],
183
            tofile=self.rename if isinstance(self.rename, str) else ''))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable str does not seem to be defined.
Loading history...
184
185
    def __json__(self):
186
        """
187
        Override JSON export, using the unified diff is the easiest thing for
188
        the users.
189
        """
190
        return self.unified_diff
191
192
    def affected_code(self, filename):
193
        """
194
        Creates a list of SourceRange objects which point to the related code.
195
        Changes on continuous lines will be put into one SourceRange.
196
197
        :param filename: The filename to associate the SourceRange's to.
198
        :return:         A list of all related SourceRange objects.
199
        """
200
        return list(diff.range(filename) for diff in self.split_diff())
201
202
    def split_diff(self):
203
        """
204
        Splits this diff into small pieces, such that several continuously
205
        altered lines are still together in one diff. All subdiffs will be
206
        yielded.
207
        """
208
        last_line = -1
209
        this_diff = Diff(self._file, rename=self.rename, delete=self.delete)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
210
        for line in sorted(self._changes.keys()):
211
            if line != last_line + 1 and len(this_diff._changes) > 0:
212
                yield this_diff
213
                this_diff = Diff(self._file, rename=self.rename,
214
                                 delete=self.delete)
215
216
            last_line = line
217
            this_diff._changes[line] = self._changes[line]
218
219
        if len(this_diff._changes) > 0:
220
            yield this_diff
221
222
    def range(self, filename):
223
        """
224
        Calculates a SourceRange spanning over the whole Diff. If something is
225
        added after the 0th line (i.e. before the first line) the first line
226
        will be included in the SourceRange.
227
228
        :param filename: The filename to associate the SourceRange with.
229
        :return:         A SourceRange object.
230
        """
231
        start = min(self._changes.keys())
232
        end = max(self._changes.keys())
233
        return SourceRange.from_values(filename,
234
                                       start_line=max(1, start),
235
                                       end_line=max(1, end))
236
237
    def __add__(self, other):
238
        """
239
        Adds another diff to this one. Will throw an exception if this is not
240
        possible. (This will *not* be done in place.)
241
        """
242
        if not isinstance(other, Diff):
243
            raise TypeError("Only diffs can be added to a diff.")
244
245
        if self.rename != other.rename and False not in (self.rename,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
246
                                                         other.rename):
247
            raise ConflictError("Diffs contain conflicting renamings.")
248
249
        result = copy.deepcopy(self)
250
        result.rename = self.rename or other.rename
251
        result.delete = self.delete or other.delete
252
253
        for line_nr in other._changes:
254
            change = other._changes[line_nr]
255
            if change.delete is True:
256
                result.delete_line(line_nr)
257
            if change.add_after is not False:
258
                result.add_lines(line_nr, change.add_after)
259
            if change.change is not False:
260
                result.change_line(line_nr, change.change[0], change.change[1])
261
262
        return result
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable result does not seem to be defined.
Loading history...
263
264
    def delete_line(self, line_nr):
265
        """
266
        Mark the given line nr as deleted. The first line is line number 1.
267
        """
268
        linediff = self._get_change(line_nr)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable line_nr does not seem to be defined.
Loading history...
269
        linediff.delete = True
270
        self._changes[line_nr] = linediff
271
272
    def add_lines(self, line_nr_before, lines):
273
        """
274
        Adds lines after the given line number.
275
276
        :param line_nr_before: Line number of the line before the additions.
277
                               Use 0 for insert lines before everything.
278
        :param lines:          A list of lines to add.
279
        """
280
        if lines == []:
281
            return  # No action
282
283
        linediff = self._get_change(line_nr_before, min_line=0)
284
        if linediff.add_after is not False:
285
            raise ConflictError("Cannot add lines after the given line since "
286
                                "there are already lines.")
287
288
        linediff.add_after = lines
289
        self._changes[line_nr_before] = linediff
290
291
    def change_line(self, line_nr, original_line, replacement):
292
        """
293
        Changes the given line with the given line number. The replacement will
294
        be there instead.
295
        """
296
        linediff = self._get_change(line_nr)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable line_nr does not seem to be defined.
Loading history...
297
        if linediff.change is not False:
298
            raise ConflictError("An already changed line cannot be changed.")
299
300
        linediff.change = (original_line, replacement)
301
        self._changes[line_nr] = linediff
302