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
6 changes: 3 additions & 3 deletions bibtexparser/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __deepcopy__(self, memo):


class BlockAbortedException(ParsingException):
"""Exception where a invalid bibtex file led to an aborted block."""
"""Exception where an invalid bibtex file led to an aborted block."""

def __init__(
self,
Expand Down Expand Up @@ -50,9 +50,9 @@ def __init__(self, first_match, expected_match, second_match):
self.expected_match = expected_match
self.second_match = second_match
super().__init__(
f"Regex mismatch: {first_match} followed by {second_match},"
f"Regex mismatch: {first_match} followed by {second_match}, "
f"but expected {expected_match}.\n"
"This is an python-bibtexparser internal error. "
"This is a python-bibtexparser internal error. "
"Please report this issue at our issue tracker."
)

Expand Down
2 changes: 1 addition & 1 deletion bibtexparser/middlewares/enclosing.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __init__(

if default_enclosing not in ("{", '"'):
raise ValueError(
"default_enclosing must be either '{' or '\"'" f"not '{default_enclosing}'"
"default_enclosing must be either '{' or '\"', " f"not '{default_enclosing}'"
)
self._default_enclosing = default_enclosing
self._reuse_previous_enclosing = reuse_previous_enclosing
Expand Down
4 changes: 2 additions & 2 deletions bibtexparser/middlewares/interpolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def transform(self, library: Library) -> Library:
warnings.warn(
(
"The RemoveEnclosingMiddleware must not run before "
"the ResolveStringReferencesMiddleware."
"We continue, but string interpolation is likely to fail,"
"the ResolveStringReferencesMiddleware. "
"We continue, but string interpolation is likely to fail, "
"or to be too aggressive (i.e., replace too many strings)."
),
UserWarning,
Expand Down
34 changes: 17 additions & 17 deletions bibtexparser/middlewares/sorting_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@


@dataclass
class _BlockJunk:
class _BlockChunk:
"""Data-Structure reflecting zero or more comments together with a block."""

# The blocks (comments and the main block) are stored in the order they were parsed.
blocks: List[Block] = field(default_factory=list)

@property
def main_block(self) -> Block:
"""Returns the main (i.e., last, non-comment) block of this junk."""
"""Returns the main (i.e., last, non-comment) block of this chunk."""
try:
return self.blocks[-1]
except IndexError:
raise RuntimeError(
"Block junk must contain at least one block. "
"Block chunk must contain at least one block. "
"This is a bug in bibtexparser, please report it."
)

Expand Down Expand Up @@ -104,31 +104,31 @@ def __init__(
super().__init__(allow_inplace_modification=False)

@staticmethod
def _block_junks(blocks: List[Block]) -> List[_BlockJunk]:
block_junks = []
current_junk = _BlockJunk()
def _block_chunks(blocks: List[Block]) -> List[_BlockChunk]:
block_chunks = []
current_chunk = _BlockChunk()
for block in blocks:
current_junk.blocks.append(block)
current_chunk.blocks.append(block)
if not (isinstance(block, ExplicitComment) or isinstance(block, ImplicitComment)):
# We added a non-comment block, hence we finish the junk and
# We added a non-comment block, hence we finish the chunk and
# start a new one
block_junks.append(current_junk)
current_junk = _BlockJunk()
block_chunks.append(current_chunk)
current_chunk = _BlockChunk()

if current_junk.blocks:
# That would be a junk with only comments, but we add it at the end for completeness
block_junks.append(current_junk)
if current_chunk.blocks:
# That would be a chunk with only comments, but we add it at the end for completeness
block_chunks.append(current_chunk)

return block_junks
return block_chunks

# docstr-coverage: inherited
def transform(self, library: Library) -> Library:
blocks = deepcopy(library.blocks)
if self._preserve_comments_on_top:
block_junks = self._block_junks(blocks)
block_junks.sort(key=lambda junk: self._key(junk.main_block), reverse=self._reverse)
block_chunks = self._block_chunks(blocks)
block_chunks.sort(key=lambda chunk: self._key(chunk.main_block), reverse=self._reverse)
return Library(
blocks=[block for block_junk in block_junks for block in block_junk.blocks],
blocks=[block for block_chunk in block_chunks for block in block_chunk.blocks],
fail_on_duplicate_key=False,
)
else:
Expand Down
4 changes: 2 additions & 2 deletions bibtexparser/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ def __init__(
):
self._start_line_in_file = start_line
self._raw = raw
self._parser_metadata: Dict[str, Any] = parser_metadata
if parser_metadata is None:
self._parser_metadata: Dict[str, Any] = {}
parser_metadata = {}
self._parser_metadata: Dict[str, Any] = parser_metadata

@property
def start_line(self) -> Optional[int]:
Expand Down
18 changes: 12 additions & 6 deletions bibtexparser/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _treat_entry(block: Entry, bibtex_format) -> List[str]:
for i, field in enumerate(block.fields):
res.append(bibtex_format.indent)
res.append(field.key)
res.append(_val_intent_string(bibtex_format, 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:
Expand All @@ -32,7 +32,7 @@ def _treat_entry(block: Entry, bibtex_format) -> List[str]:
return res


def _val_intent_string(bibtex_format: "BibtexFormat", key: str) -> str:
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
Expand All @@ -42,6 +42,7 @@ 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",
Expand Down Expand Up @@ -91,6 +92,8 @@ def _calculate_auto_value_align(library: Library) -> int:
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)


Expand Down Expand Up @@ -163,7 +166,7 @@ def __init__(self):

@property
def indent(self) -> str:
"""Character(s) for indenting BibTeX field-value pairs. Default: single space."""
"""Character(s) for indenting BibTeX field-value pairs. Default: single tab."""
return self._indent

@indent.setter
Expand All @@ -177,13 +180,16 @@ def value_column(self) -> Union[int, str]:
This impacts String and Entry blocks.

An integer value x specifies that spaces should be added before the " = ",
such that, if possible, the value is written at column `len(self.indent) + x`.
such that, if possible, the value starts x characters after the line prefix
(the ``indent`` for entry fields, ``@string{`` for string values).
Entry and string values are thus each aligned among themselves.
Note that for long keys, the value may be written at a later column.

Thus, a value of 0 means that the value is written directly after the " = ".

The special value "auto" specifies that the bibtex field value should be aligned
based on the longest key in the library.
The special value "auto" specifies that values should be aligned
based on the longest key in the library
(considering both entry field keys and string keys).
"""
return self._align_field_values

Expand Down
34 changes: 34 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,40 @@ def test_entry_value_column(value_column):
assert f"{bib_format.indent}veryverylongkeyfield = 2020" in string


@pytest.mark.parametrize("value_column", [None, 10, "auto"])
def test_string_value_column(value_column):
library = Library(
blocks=[
String(key="me", value='"myValue"'),
String(key="veryverylongkey", value='"otherValue"'),
]
)
bib_format = BibtexFormat()
if value_column is not None:
bib_format.value_column = value_column
string = writer.write(library, bib_format)
if value_column is None:
# Make sure there are no unneeded spaces
assert '@string{me = "myValue"}' in string
assert '@string{veryverylongkey = "otherValue"}' in string
elif value_column == 10:
assert '@string{me = "myValue"}' in string
assert '@string{veryverylongkey = "otherValue"}' in string
if value_column == "auto":
assert '@string{me = "myValue"}' in string
assert '@string{veryverylongkey = "otherValue"}' in string


def test_auto_value_column_considers_string_keys():
library = Library(blocks=[String(key="averyverylongstringkey", value='"v"'), _dummy_entry()])
bib_format = BibtexFormat()
bib_format.value_column = "auto"
string = writer.write(library, bib_format)
# The 22-char string key drives the alignment column for entry fields, too
pad = " " * (22 - len("title"))
assert f'{bib_format.indent}title{pad} = "myTitle"' in string


@pytest.mark.parametrize("block_separator", [None, "\n\n", "\n-----\n"])
def test_block_separator(block_separator):
library = Library(blocks=[_DUMMY_STRING, _DUMMY_PREAMBLE])
Expand Down
Loading