From c17bfd8af643e7959ef255562e577001c0184a37 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Sun, 13 Sep 2026 12:11:28 -0300 Subject: [PATCH] Raise UnidiffParseError on target header without source (#148) --- tests/test_parser.py | 10 ++++++++++ unidiff/patch.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/test_parser.py b/tests/test_parser.py index 432d9fb..e2b3a33 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -270,6 +270,16 @@ def test_parse_malformed_diff_shorter_than_expected(self): with open(utf8_file, 'r') as diff_file: self.assertRaises(UnidiffParseError, PatchSet, diff_file) + def test_parse_target_without_source(self): + """Parse a target file header with no preceding source header.""" + # regression test for issue #148: this used to raise UnboundLocalError + for diff in ('+++ b/file\n', + '+++ b/file\n@@ -0,0 +1 @@\n+a\n', + 'some preamble\n+++ b/file\n'): + with self.assertRaises(UnidiffParseError) as cm: + PatchSet(diff) + self.assertIn('Target without source', str(cm.exception)) + def test_from_filename_with_cr_in_diff_text_files(self): """Parse git diff text files that contain CR""" utf8_file = os.path.join(self.samples_dir, 'samples/git_cr.diff') diff --git a/unidiff/patch.py b/unidiff/patch.py index 3cb87f0..e0af12b 100644 --- a/unidiff/patch.py +++ b/unidiff/patch.py @@ -462,6 +462,9 @@ def _parse(self, diff: Iterable, encoding: Optional[str], metadata_only: bool) -> None: current_file = None patch_info = None + # pending source file header details, consumed by the target header + source_file = None + source_timestamp = None diff_lines = enumerate(diff, 1) for diff_line_no, line in diff_lines: @@ -552,6 +555,10 @@ def _parse(self, diff: Iterable, encoding: Optional[str], if current_file is not None and not (current_file.target_file == target_file): raise UnidiffParseError('Target without source: %s' % line) if current_file is None: + if source_file is None: + # a target header requires a preceding source header + raise UnidiffParseError( + 'Target without source: %s' % line) # add current file to PatchSet current_file = PatchedFile( patch_info, source_file, target_file, @@ -559,6 +566,9 @@ def _parse(self, diff: Iterable, encoding: Optional[str], diff_line_no=diff_line_no) self.append(current_file) patch_info = None + # the source header has been consumed by this file + source_file = None + source_timestamp = None else: current_file.target_timestamp = target_timestamp continue