Completed
Pull Request — master (#2077)
by Lasse
02:11
created

Diff.rename()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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