from copy import deepcopy
from typing import Optional
from .library import Library
from .model import Entry
from .model import ExplicitComment
from .model import Field
from .model import ImplicitComment
from .model import ParsingFailedBlock
from .model import Preamble
from .model import String
VAL_SEP = " = "
PARSING_FAILED_COMMENT = "% WARNING Parsing failed for the following {n} lines."
def _treat_entry(block: Entry, bibtex_format) -> list[str]:
res = ["@", block.entry_type, "{", block.key, ",\n"]
field: Field
for i, field in enumerate(block.fields):
res.append(bibtex_format.indent)
res.append(field.key)
res.append(_val_indent_string(bibtex_format, field.key))
res.append(VAL_SEP)
res.append(field.value)
if bibtex_format.trailing_comma or i < len(block.fields) - 1:
res.append(",")
res.append("\n")
res.append("}\n")
return res
def _val_indent_string(bibtex_format: "BibtexFormat", key: str) -> str:
"""The spaces which have to be added after the ` = `."""
length = bibtex_format.value_column - len(key) - len(VAL_SEP)
return "" if length <= 0 else " " * length
def _treat_string(block: String, bibtex_format) -> list[str]:
return [
"@string{",
block.key,
_val_indent_string(bibtex_format, block.key),
VAL_SEP,
block.value,
"}\n",
]
def _treat_preamble(block: Preamble, bibtex_format: "BibtexFormat") -> list[str]:
return [f"@preamble{{{block.value}}}\n"]
def _treat_impl_comment(block: ImplicitComment, bibtex_format: "BibtexFormat") -> list[str]:
# Note: No explicit escaping is done here - that should be done in middleware
return [block.comment, "\n"]
def _treat_expl_comment(block: ExplicitComment, bibtex_format: "BibtexFormat") -> list[str]:
return ["@comment{", block.comment, "}\n"]
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 = bibtex_format.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:
for key in entry.fields_dict:
max_key_len = max(max_key_len, len(key))
for string in library.strings:
max_key_len = max(max_key_len, len(string.key))
return max_key_len + len(VAL_SEP)
def write(library: Library, bibtex_format: Optional["BibtexFormat"] = None) -> str:
"""Serialize a BibTeX database to a string.
Note: This is not the exposed writing entrypoint.
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).
: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()
if bibtex_format.value_column == "auto":
auto_val: int = _calculate_auto_value_align(library)
# Copy the format instance to avoid modifying the original
# (which would be bad if the format is used for multiple libraries)
bibtex_format = deepcopy(bibtex_format)
bibtex_format.value_column = auto_val
string_pieces = []
for i, block in enumerate(library.blocks):
# Get string representation (as list of strings) of block
string_block_pieces = _treat_block(bibtex_format, block)
string_pieces.extend(string_block_pieces)
# Separate Blocks
if i < len(library.blocks) - 1:
string_pieces.append(bibtex_format.block_separator)
return "".join(string_pieces)
def _treat_block(bibtex_format, block) -> list[str]:
if isinstance(block, Entry):
string_block_pieces = _treat_entry(block, bibtex_format)
elif isinstance(block, String):
string_block_pieces = _treat_string(block, bibtex_format)
elif isinstance(block, Preamble):
string_block_pieces = _treat_preamble(block, bibtex_format)
elif isinstance(block, ExplicitComment):
string_block_pieces = _treat_expl_comment(block, bibtex_format)
elif isinstance(block, ImplicitComment):
string_block_pieces = _treat_impl_comment(block, bibtex_format)
elif isinstance(block, ParsingFailedBlock):
string_block_pieces = _treat_failed_block(block, bibtex_format)
else:
raise ValueError(f"Unknown block type: {type(block)}")
return string_block_pieces