From f7f4b3eeda1d916265e0689ab074e4e221de21f9 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Fri, 24 Jul 2026 19:33:43 -0300 Subject: [PATCH] Update README --- README.rst | 96 ++++++++++++++++++++++++++++++++++++++++---- tests/test_parser.py | 15 +++++++ unidiff/patch.py | 10 +++-- 3 files changed, 109 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 72a3284..ddd5e88 100644 --- a/README.rst +++ b/README.rst @@ -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'^([- \+\\])') - @@ -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. @@ -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 ---------------------------------------------------------- diff --git a/tests/test_parser.py b/tests/test_parser.py index d8096bd..9571ed0 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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() diff --git a/unidiff/patch.py b/unidiff/patch.py index ede7699..32f0835 100644 --- a/unidiff/patch.py +++ b/unidiff/patch.py @@ -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 @@ -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]: