Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 88 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Quick start
--- 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'^([- \+\\])')
# - deleted line
# \ No newline case (ignore)
RE_HUNK_BODY_LINE = re.compile(r'^([- \+\\])')
-


Expand All @@ -62,11 +62,6 @@ 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.

For git diffs, the file mode is exposed through the :code:`source_mode` and
:code:`target_mode` attributes (e.g. :code:`'100644'`, :code:`'100755'`,
:code:`'120000'`), or :code:`None` when unknown. The :code:`is_symlink`
property is a shortcut to detect symbolic links (mode :code:`120000`).

At any point you can get the string representation of the current object, and
that will return the unified diff data of it.

Expand Down Expand Up @@ -132,6 +127,91 @@ parsing more efficient:
>>> patch = PatchSet.from_filename('tests/samples/bzr.diff', encoding='utf-8', metadata_only=True)


Inspecting files, hunks and lines
---------------------------------

.. code-block:: python

>>> 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 :code:`source_mode` and
:code:`target_mode` attributes (e.g. :code:`'100644'`, :code:`'100755'`,
:code:`'120000'`), or :code:`None` when unknown. The :code:`is_symlink`
property is a shortcut to detect symbolic links (mode :code:`120000`):

.. code-block:: python

>>> 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 :code:`PatchedFile` also exposes :code:`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:

.. code-block:: python

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

:code:`PatchSet` and :code:`PatchSet.from_string` also accept :code:`bytes`,
which are decoded using the given :code:`encoding` (defaulting to UTF-8):

.. code-block:: python

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

Expand Down
15 changes: 15 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,21 @@ def test_patchset_from_string(self):

self.assertEqual(ps1, ps2)

def test_metadata_only_via_convenience_constructors(self):
# from_filename and from_string should forward metadata_only (the
# from_filename usage is documented in the README)
ps_file = PatchSet.from_filename(
self.sample_file, encoding='utf-8', metadata_only=True)
with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file:
ps_string = PatchSet.from_string(diff_file.read(), metadata_only=True)

# counts are still computed under metadata_only
self.assertEqual((ps_file.added, ps_file.removed), (21, 17))
self.assertEqual((ps_string.added, ps_string.removed), (21, 17))
# metadata_only skips storing the line content
self.assertEqual(len(ps_file[0][0]), 0)
self.assertEqual(len(ps_string[0][0]), 0)

def test_patchset_from_bytes_string(self):
with codecs.open(self.sample_file, 'rb') as diff_file:
diff_data = diff_file.read()
Expand Down
10 changes: 6 additions & 4 deletions unidiff/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,10 +617,11 @@ def _parse(self, diff: Iterable, encoding: Optional[str],
@classmethod
def from_filename(cls, filename: str, encoding: str = DEFAULT_ENCODING,
errors: Optional[str] = None,
newline: Optional[str] = None) -> PatchSet:
newline: Optional[str] = None,
metadata_only: bool = False) -> PatchSet:
"""Return a PatchSet instance given a diff filename."""
with open(filename, 'r', encoding=encoding, errors=errors, newline=newline) as f:
instance = cls(f)
instance = cls(f, metadata_only=metadata_only)
return instance

@staticmethod
Expand All @@ -633,9 +634,10 @@ def _convert_string(data: Union[str, bytes], encoding: Optional[str] = None,

@classmethod
def from_string(cls, data: Union[str, bytes], encoding: Optional[str] = None,
errors: str = 'strict') -> PatchSet:
errors: str = 'strict', metadata_only: bool = False) -> PatchSet:
"""Return a PatchSet instance given a diff string."""
return cls(cls._convert_string(data, encoding, errors))
return cls(cls._convert_string(data, encoding, errors),
metadata_only=metadata_only)

@property
def added_files(self) -> list[PatchedFile]:
Expand Down
Loading