Simple Python library to parse and interact with unified diff data.
$ pip install unidiff>>> import urllib.request
>>> from unidiff import PatchSet
>>> diff = urllib.request.urlopen('https://github.com/matiasb/python-unidiff/pull/3.diff')
>>> encoding = diff.headers.get_charsets()[0]
>>> patch = PatchSet(diff, encoding=encoding)
>>> patch
<PatchSet: [<PatchedFile: .gitignore>, <PatchedFile: unidiff/patch.py>, <PatchedFile: unidiff/utils.py>]>
>>> patch[0]
<PatchedFile: .gitignore>
>>> patch[0].is_added_file
True
>>> patch[0].added
6
>>> patch[1]
<PatchedFile: unidiff/patch.py>
>>> patch[1].added, patch[1].removed
(20, 11)
>>> len(patch[1])
6
>>> patch[1][2]
<Hunk: @@ 109,14 110,21 @@ def __repr__(self):>
>>> patch[2]
<PatchedFile: unidiff/utils.py>
>>> print(patch[2])
diff --git a/unidiff/utils.py b/unidiff/utils.py
index eae63e6..29c896a 100644
--- a/unidiff/utils.py
+++ b/unidiff/utils.py
@@ -37,4 +37,3 @@
# - deleted line
# \ No newline case (ignore)
RE_HUNK_BODY_LINE = re.compile(r'^([- \+\\])')
-Load unified diff data by instantiating PatchSet with a file-like object as
argument, or using the PatchSet.from_filename class method to read the diff
from a file.
A PatchSet is a list of files updated by the given patch. For each
PatchedFile you can get stats (if it is a new, removed or modified file; the
source/target lines; etc), besides having access to each hunk (also like a
list) and its respective info.
At any point you can get the string representation of the current object, and that will return the unified diff data of it.
As a quick example of what can be done, check the unidiff/__main__.py file.
Also, once installed, unidiff provides a command-line program that displays information from diff data (a file, or stdin). For example:
$ git diff | unidiff
Summary
-------
README.md: +6 additions, -0 deletions
1 modified file(s), 0 added file(s), 0 removed file(s)
Total: 6 addition(s), 0 deletion(s)To instantiate PatchSet from a local file, you can use:
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/bzr.diff', encoding='utf-8')
>>> patch
<PatchSet: [<PatchedFile: added_file>, <PatchedFile: modified_file>, <PatchedFile: removed_file>]>Notice the (optional) encoding parameter. If not specified, unicode input
will be expected (or, when the data is bytes, it is decoded using encoding,
defaulting to UTF-8). Or alternatively:
>>> import codecs
>>> from unidiff import PatchSet
>>> with codecs.open('tests/samples/bzr.diff', 'r', encoding='utf-8') as diff:
... patch = PatchSet(diff)
...
>>> patch
<PatchSet: [<PatchedFile: added_file>, <PatchedFile: modified_file>, <PatchedFile: removed_file>]>Finally, you can also instantiate PatchSet passing any iterable (and encoding,
if needed):
>>> from unidiff import PatchSet
>>> with open('tests/samples/bzr.diff', 'r') as diff:
... data = diff.readlines()
...
>>> patch = PatchSet(data)
>>> patch
<PatchSet: [<PatchedFile: added_file>, <PatchedFile: modified_file>, <PatchedFile: removed_file>]>If you don't need to be able to rebuild the original unified diff input, you can
pass metadata_only=True (defaults to False), which should help making the
parsing more efficient:
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/bzr.diff', encoding='utf-8', metadata_only=True)>>> from unidiff import PatchSet
>>> patch = PatchSet.from_string(
... '--- a/story.txt\n'
... '+++ b/story.txt\n'
... '@@ -1,4 +1,4 @@\n'
... ' Once upon a time\n'
... '-there was a bug\n'
... '+there was a fix\n'
... ' the end\n'
... ' really\n')
>>> patched_file = patch[0]
>>> patched_file.path
'story.txt'
>>> patched_file.is_modified_file
True
>>> patched_file.added, patched_file.removed
(1, 1)
>>> hunk = patched_file[0]
>>> hunk.source_start, hunk.target_start
(1, 1)
>>> removed = [line for line in hunk if line.is_removed]
>>> len(removed)
1
>>> removed[0].value
'there was a bug\n'
>>> removed[0].source_line_no
2
>>> added = [line for line in hunk if line.is_added]
>>> added[0].value, added[0].target_line_no
('there was a fix\n', 2)For git diffs, the file mode is exposed through the source_mode and
target_mode attributes (e.g. '100644', '100755', '120000'), or None
when unknown. The is_symlink property is a shortcut to detect symbolic links
(mode 120000):
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/git_symlink.diff')
>>> patched_file = patch[0]
>>> patched_file.path
'bin/check'
>>> patched_file.is_added_file
True
>>> patched_file.target_mode
'120000'
>>> patched_file.is_symlink
TrueEach PatchedFile also exposes diff_line_no, the 1-based line number in the
diff where its entry starts. This is useful to locate files that have no hunks,
such as binary changes:
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/debdiff.diff')
>>> [(f.path, f.is_binary_file, f.diff_line_no) for f in patch]
[('new/added.txt', False, 3), ('/t/p2/a.png', True, 6), ('/t/p2/b.png', True, 7)]PatchSet and PatchSet.from_string also accept bytes, which are decoded
using the given encoding (defaulting to UTF-8):
>>> from unidiff import PatchSet
>>> patch = PatchSet(b'--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n hola\n-mundo\n+world\n')
>>> patch.added, patch.removed
(1, 1)Diff hunk content may include arbitrary bytes, such as a lone carriage return
(\r) or other control characters (for example the output of some editors or
generated patches). By default Python opens text files in universal newlines
mode, which translates a lone \r into a line break and would split such
content across lines, breaking parsing.
To parse these diffs, read the data without universal-newline translation by
passing newline='\n' (so lines are split only on \n):
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/git_cr.diff', newline='\n')Equivalently, open the file yourself with newline='\n' (or in binary mode
passing the encoding argument) before handing it to PatchSet.