Skip to main content

Unified diff parsing/metadata extraction library.

Project description

Unidiff

Simple Python library to parse and interact with unified diff data.

CI

Installing unidiff

$ pip install unidiff

Quick start

>>> 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)

Load a local diff file

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)

Inspecting files, hunks and lines

>>> 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)

Git file modes, symlinks and line numbers

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
True

Each 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)]

Parsing from bytes

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)

Diffs with embedded carriage returns or control characters

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.

References

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unidiff-1.0.0.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unidiff-1.0.0-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file unidiff-1.0.0.tar.gz.

File metadata

  • Download URL: unidiff-1.0.0.tar.gz
  • Upload date:
  • Size: 29.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for unidiff-1.0.0.tar.gz
Algorithm Hash digest
SHA256 5e5d5cfab2dc98be819b74747ab7d9f5af8695369ec8710b93f9ab0f0ae6a449
MD5 22a472a2102cf5b2d243ecad215342d6
BLAKE2b-256 98486ebfbda867e1a07bab3bbffe820e980bff8262c97ff77d1496a4fa15e711

See more details on using hashes here.

File details

Details for the file unidiff-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: unidiff-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 18.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for unidiff-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2e1fb4eebe2354a26a1f3d51efe2e5d504cae5764b98ed8bdbb4e7a000baff28
MD5 88d5c43dc585ef956aa5d599234ffdd3
BLAKE2b-256 65ca860142913b2fee25c78b3af733054e248c488bd83cf6cfb97969e98e3bcf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page