Skip to content

Commit 1569968

Browse files
MiWeissclaude
andauthored
🐛 Raise descriptive error when writing failed blocks without raw bibtex (#526)
* 🐛 Raise descriptive error when writing failed blocks without raw bibtex (#400) Failed blocks (e.g. DuplicateBlockKeyBlock) created programmatically have no raw bibtex, which previously crashed the writer with an opaque AttributeError. The writer now pre-scans the library and raises a single ValueError listing all such blocks, pointing users to library.failed_blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🎨 Apply black formatting Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9fe14b0 commit 1569968

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

bibtexparser/writer.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,30 @@ def _treat_expl_comment(block: ExplicitComment, bibtex_format: "BibtexFormat") -
6262

6363

6464
def _treat_failed_block(block: ParsingFailedBlock, bibtex_format: "BibtexFormat") -> List[str]:
65+
if block.raw is None:
66+
raise ValueError(_failed_blocks_without_raw_error([block]))
6567
lines = len(block.raw.splitlines())
6668
parsing_failed_comment = PARSING_FAILED_COMMENT.format(n=lines)
6769
return [parsing_failed_comment, "\n", block.raw, "\n"]
6870

6971

72+
def _failed_blocks_without_raw_error(blocks: List[ParsingFailedBlock]) -> str:
73+
descriptions = "\n".join(f" - {type(b).__name__}: {b.error}" for b in blocks)
74+
return (
75+
"Cannot write library: it contains failed blocks without raw bibtex "
76+
"(typically created programmatically, not by parsing):\n"
77+
f"{descriptions}\n"
78+
"Inspect `library.failed_blocks` to resolve this, e.g. by removing these blocks "
79+
"or by fixing and re-adding their `block.ignore_error_block`."
80+
)
81+
82+
83+
def _raise_on_unwritable_blocks(library: Library) -> None:
84+
unwritable = [b for b in library.blocks if isinstance(b, ParsingFailedBlock) and b.raw is None]
85+
if unwritable:
86+
raise ValueError(_failed_blocks_without_raw_error(unwritable))
87+
88+
7089
def _calculate_auto_value_align(library: Library) -> int:
7190
max_key_len = 0
7291
for entry in library.entries:
@@ -82,7 +101,11 @@ def write(library: Library, bibtex_format: Optional["BibtexFormat"] = None) -> s
82101
The exposed entrypoint is `bibtexparser.write_string` (in entrypoint.py).
83102
84103
:param library: BibTeX database to serialize.
85-
:param bibtex_format: Customized BibTeX format to use (optional)."""
104+
:param bibtex_format: Customized BibTeX format to use (optional).
105+
:raises ValueError: If the library contains failed blocks without raw bibtex
106+
(e.g. duplicate-key blocks resulting from programmatically created entries)."""
107+
_raise_on_unwritable_blocks(library)
108+
86109
if bibtex_format is None:
87110
bibtex_format = BibtexFormat()
88111

tests/test_writer.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,28 @@ def test_write_failed_block():
143143
assert lines[2] == "except = {that-there-need-to-be},"
144144
assert lines[3] == "other = {multiple-lines}"
145145
assert lines[4] == "}"
146+
147+
148+
def test_write_failed_block_without_raw_raises():
149+
"""A failed block with no raw bibtex cannot be written (issue #400)."""
150+
block = ParsingFailedBlock(error=ValueError("Some error"), raw=None, ignore_error_block=None)
151+
library = Library(blocks=[block])
152+
with pytest.raises(ValueError, match="failed blocks without raw bibtex"):
153+
writer.write(library)
154+
155+
156+
def test_write_manually_created_duplicate_entries_raises():
157+
"""Reproduces issue #400: duplicate keys on programmatically created entries."""
158+
library = Library()
159+
library.add(_dummy_entry())
160+
library.add(_dummy_entry())
161+
with pytest.raises(ValueError, match="failed_blocks"):
162+
writer.write(library)
163+
164+
165+
def test_write_raises_listing_all_unwritable_blocks():
166+
first = ParsingFailedBlock(error=ValueError("first error"), raw=None)
167+
second = ParsingFailedBlock(error=ValueError("second error"), raw=None)
168+
library = Library(blocks=[first, _dummy_entry(), second])
169+
with pytest.raises(ValueError, match="(?s)first error.*second error"):
170+
writer.write(library)

0 commit comments

Comments
 (0)