Skip to content

Commit 9b15cab

Browse files
committed
Prevent failed block annotations from accumulating
* Recognize an existing annotation attached to the next failed block. * Avoid adding a second terminal newline to retained raw source. * Keep repeated opt-in annotation writes idempotent.
1 parent a1e68d1 commit 9b15cab

2 files changed

Lines changed: 47 additions & 11 deletions

File tree

bibtexparser/writer.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,27 @@ def _treat_failed_block(block: ParsingFailedBlock, bibtex_format: "BibtexFormat"
7272
raise ValueError(_failed_blocks_forbidden_error([block]))
7373
lines = len(block.raw.splitlines())
7474
parsing_failed_comment = bibtex_format.parsing_failed_comment.format(n=lines)
75-
return [parsing_failed_comment, "\n", block.raw, "\n"]
75+
rendered = [parsing_failed_comment, "\n", block.raw]
76+
if not block.raw.endswith(("\n", "\r")):
77+
rendered.append("\n")
78+
return rendered
79+
80+
81+
def _is_existing_failed_block_annotation(
82+
block: object,
83+
next_block: object,
84+
bibtex_format: "BibtexFormat",
85+
) -> bool:
86+
"""Recognize an annotation emitted for the immediately following failure."""
87+
if bibtex_format.failed_block_policy != "annotate":
88+
return False
89+
if not isinstance(block, ImplicitComment) or not isinstance(next_block, ParsingFailedBlock):
90+
return False
91+
if next_block.raw is None:
92+
return False
93+
source_lines = len(next_block.raw.splitlines())
94+
expected = bibtex_format.parsing_failed_comment.format(n=source_lines)
95+
return block.comment == expected
7696

7797

7898
def _failed_blocks_without_raw_error(blocks: list[ParsingFailedBlock]) -> str:
@@ -145,17 +165,16 @@ def write(library: Library, bibtex_format: Optional["BibtexFormat"] = None) -> s
145165
bibtex_format = deepcopy(bibtex_format)
146166
bibtex_format.value_column = auto_val
147167

148-
string_pieces = []
168+
rendered_blocks: list[str] = []
169+
for index, block in enumerate(library.blocks):
170+
next_block = library.blocks[index + 1] if index + 1 < len(library.blocks) else None
171+
if _is_existing_failed_block_annotation(block, next_block, bibtex_format):
172+
# The following failed block recreates this exact annotation. Omitting
173+
# the parsed copy prevents one new comment from appearing per cycle.
174+
continue
175+
rendered_blocks.append("".join(_treat_block(bibtex_format, block)))
149176

150-
for i, block in enumerate(library.blocks):
151-
# Get string representation (as list of strings) of block
152-
string_block_pieces = _treat_block(bibtex_format, block)
153-
string_pieces.extend(string_block_pieces)
154-
# Separate Blocks
155-
if i < len(library.blocks) - 1:
156-
string_pieces.append(bibtex_format.block_separator)
157-
158-
return "".join(string_pieces)
177+
return bibtex_format.block_separator.join(rendered_blocks)
159178

160179

161180
def _treat_block(bibtex_format, block) -> list[str]:

tests/test_entrypoint.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from bibtexparser import BibtexFormat
910
from bibtexparser import parse_file
1011
from bibtexparser import parse_string
1112
from bibtexparser import write_file
@@ -40,6 +41,22 @@ def test_duplicate_block_roundtrip_retains_both_sources_and_stabilizes():
4041
assert second_write == first_write
4142

4243

44+
def test_failed_block_annotation_does_not_accumulate_across_roundtrips():
45+
"""Opt-in annotations remain a single diagnostic instead of growing on every save."""
46+
source = "@article{broken,\n title = {Still retained}\n"
47+
bibtex_format = BibtexFormat()
48+
bibtex_format.failed_block_policy = "annotate"
49+
50+
first_write = write_string(parse_string(source), bibtex_format=bibtex_format)
51+
second_write = write_string(parse_string(first_write), bibtex_format=bibtex_format)
52+
third_write = write_string(parse_string(second_write), bibtex_format=bibtex_format)
53+
54+
warning = "% WARNING Parsing failed for the following 2 lines."
55+
assert first_write.count(warning) == 1
56+
assert second_write == first_write
57+
assert third_write == first_write
58+
59+
4360
def test_gbk():
4461
library = parse_file("tests/resources/gbk_test.bib", encoding="gbk")
4562
assert library.entries[0]["author"] == "凯撒"

0 commit comments

Comments
 (0)