From 20eb0956e703aa9967efa7b09366e6e468dc9e51 Mon Sep 17 00:00:00 2001 From: Michael Weiss Date: Fri, 12 Jun 2026 08:32:21 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20and=20do?= =?UTF-8?q?cstring=20mismatches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- bibtexparser/exceptions.py | 6 ++-- bibtexparser/middlewares/enclosing.py | 2 +- bibtexparser/middlewares/interpolate.py | 4 +-- bibtexparser/middlewares/sorting_blocks.py | 34 +++++++++++----------- bibtexparser/model.py | 4 +-- bibtexparser/writer.py | 8 ++--- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/bibtexparser/exceptions.py b/bibtexparser/exceptions.py index f010768..772203e 100644 --- a/bibtexparser/exceptions.py +++ b/bibtexparser/exceptions.py @@ -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, @@ -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." ) diff --git a/bibtexparser/middlewares/enclosing.py b/bibtexparser/middlewares/enclosing.py index 542914f..a9d9c7c 100644 --- a/bibtexparser/middlewares/enclosing.py +++ b/bibtexparser/middlewares/enclosing.py @@ -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 diff --git a/bibtexparser/middlewares/interpolate.py b/bibtexparser/middlewares/interpolate.py index baf0a63..de171bd 100644 --- a/bibtexparser/middlewares/interpolate.py +++ b/bibtexparser/middlewares/interpolate.py @@ -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, diff --git a/bibtexparser/middlewares/sorting_blocks.py b/bibtexparser/middlewares/sorting_blocks.py index 2062ded..a5300a7 100644 --- a/bibtexparser/middlewares/sorting_blocks.py +++ b/bibtexparser/middlewares/sorting_blocks.py @@ -21,7 +21,7 @@ @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. @@ -29,12 +29,12 @@ class _BlockJunk: @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." ) @@ -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: diff --git a/bibtexparser/model.py b/bibtexparser/model.py index 7dcd72d..3e93f63 100644 --- a/bibtexparser/model.py +++ b/bibtexparser/model.py @@ -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]: diff --git a/bibtexparser/writer.py b/bibtexparser/writer.py index bee53b6..cf5f1a2 100644 --- a/bibtexparser/writer.py +++ b/bibtexparser/writer.py @@ -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: @@ -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 @@ -163,7 +163,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 @@ -174,7 +174,7 @@ def indent(self, indent: str): def value_column(self) -> Union[int, str]: """Controls the alignment of field- and string-values. Default: no alignment. - This impacts String and Entry blocks. + This impacts Entry blocks (String blocks are not aligned). 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`. From 57645b671660f5bf167a5c04b4184504fc6f5571 Mon Sep 17 00:00:00 2001 From: Michael Weiss Date: Fri, 12 Jun 2026 08:44:27 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=8F=AA=20Keep=20value=5Fcolumn=20docstri?= =?UTF-8?q?ng;=20string=20alignment=20to=20be=20implemented=20separately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- bibtexparser/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bibtexparser/writer.py b/bibtexparser/writer.py index cf5f1a2..0683ff3 100644 --- a/bibtexparser/writer.py +++ b/bibtexparser/writer.py @@ -174,7 +174,7 @@ def indent(self, indent: str): def value_column(self) -> Union[int, str]: """Controls the alignment of field- and string-values. Default: no alignment. - This impacts Entry blocks (String blocks are not aligned). + 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`. From 9c112fc98a998c99235b40007cd49c95daba6c08 Mon Sep 17 00:00:00 2001 From: Michael Weiss Date: Fri, 12 Jun 2026 08:49:20 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Align=20@string=20values=20acco?= =?UTF-8?q?rding=20to=20BibtexFormat.value=5Fcolumn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- bibtexparser/writer.py | 12 +++++++++--- tests/test_writer.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/bibtexparser/writer.py b/bibtexparser/writer.py index 0683ff3..932671c 100644 --- a/bibtexparser/writer.py +++ b/bibtexparser/writer.py @@ -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", @@ -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) @@ -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 diff --git a/tests/test_writer.py b/tests/test_writer.py index 459093d..5e36cac 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -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])