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
25 changes: 24 additions & 1 deletion bibtexparser/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,30 @@ def _treat_expl_comment(block: ExplicitComment, bibtex_format: "BibtexFormat") -


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


def _failed_blocks_without_raw_error(blocks: List[ParsingFailedBlock]) -> str:
descriptions = "\n".join(f" - {type(b).__name__}: {b.error}" for b in blocks)
return (
"Cannot write library: it contains failed blocks without raw bibtex "
"(typically created programmatically, not by parsing):\n"
f"{descriptions}\n"
"Inspect `library.failed_blocks` to resolve this, e.g. by removing these blocks "
"or by fixing and re-adding their `block.ignore_error_block`."
)


def _raise_on_unwritable_blocks(library: Library) -> None:
unwritable = [b for b in library.blocks if isinstance(b, ParsingFailedBlock) and b.raw is None]
if unwritable:
raise ValueError(_failed_blocks_without_raw_error(unwritable))


def _calculate_auto_value_align(library: Library) -> int:
max_key_len = 0
for entry in library.entries:
Expand All @@ -82,7 +101,11 @@ def write(library: Library, bibtex_format: Optional["BibtexFormat"] = None) -> s
The exposed entrypoint is `bibtexparser.write_string` (in entrypoint.py).

:param library: BibTeX database to serialize.
:param bibtex_format: Customized BibTeX format to use (optional)."""
:param bibtex_format: Customized BibTeX format to use (optional).
:raises ValueError: If the library contains failed blocks without raw bibtex
(e.g. duplicate-key blocks resulting from programmatically created entries)."""
_raise_on_unwritable_blocks(library)

if bibtex_format is None:
bibtex_format = BibtexFormat()

Expand Down
25 changes: 25 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,28 @@ def test_write_failed_block():
assert lines[2] == "except = {that-there-need-to-be},"
assert lines[3] == "other = {multiple-lines}"
assert lines[4] == "}"


def test_write_failed_block_without_raw_raises():
"""A failed block with no raw bibtex cannot be written (issue #400)."""
block = ParsingFailedBlock(error=ValueError("Some error"), raw=None, ignore_error_block=None)
library = Library(blocks=[block])
with pytest.raises(ValueError, match="failed blocks without raw bibtex"):
writer.write(library)


def test_write_manually_created_duplicate_entries_raises():
"""Reproduces issue #400: duplicate keys on programmatically created entries."""
library = Library()
library.add(_dummy_entry())
library.add(_dummy_entry())
with pytest.raises(ValueError, match="failed_blocks"):
writer.write(library)


def test_write_raises_listing_all_unwritable_blocks():
first = ParsingFailedBlock(error=ValueError("first error"), raw=None)
second = ParsingFailedBlock(error=ValueError("second error"), raw=None)
library = Library(blocks=[first, _dummy_entry(), second])
with pytest.raises(ValueError, match="(?s)first error.*second error"):
writer.write(library)
Loading