From 8152ff77ffee76e4feea3553d4321bbcbc9c50d3 Mon Sep 17 00:00:00 2001 From: Claudius Ellsel Date: Thu, 16 Jul 2026 15:00:34 +0200 Subject: [PATCH] Preserve trailing backslashes in name round trips * Avoid duplicating a terminal backslash while splitting names. * Escape comma-adjacent name sections without changing the final section. * Enable the strict and non-strict inverse regressions. --- bibtexparser/middlewares/names.py | 11 +++++++++-- tests/middleware_tests/test_names.py | 18 ------------------ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/bibtexparser/middlewares/names.py b/bibtexparser/middlewares/names.py index 45713a9..6ef88c2 100644 --- a/bibtexparser/middlewares/names.py +++ b/bibtexparser/middlewares/names.py @@ -143,8 +143,14 @@ def escape_last_slash(string: str) -> str: last = " ".join(self.last) if self.last else None jr = " ".join(self.jr) if self.jr else None - von_last = " ".join(name for name in [von, last] if name) - return ", ".join(escape_last_slash(name) for name in [von_last, jr, first] if name) + sections = [ + name for name in [" ".join(name for name in [von, last] if name), jr, first] if name + ] + + # Only sections followed by a comma need protection from a trailing backslash. + # Escaping the final section changes its value even though no delimiter follows it. + escaped_sections = [escape_last_slash(section) for section in sections[:-1]] + return ", ".join([*escaped_sections, sections[-1]]) class SplitNameParts(_NameTransformerMiddleware): @@ -311,6 +317,7 @@ def parse_single_name_into_parts(name: str, strict: bool = True) -> NameParts: # If we're at the end of the string, then the \ is just a \. except StopIteration: word.append(char) + continue # Start of a braced expression. if char == "{": diff --git a/tests/middleware_tests/test_names.py b/tests/middleware_tests/test_names.py index e11133b..5078876 100644 --- a/tests/middleware_tests/test_names.py +++ b/tests/middleware_tests/test_names.py @@ -875,26 +875,8 @@ def test_split_name_into_parts(name, expected_as_dict, strict): def test_merge_last_name_first_inverse(name, expected_as_dict, strict): """Tests that merging name parts using the last-name-first method maintains the "semantics" of the name. - - This property does not hold for certain values that contain '\\'. """ - # cases where either the last name or "von" part ends with an odd number of `\` cannot be handled, - # since in those cases the `,` is escaped when the name parts are put back together - def ends_with_odd_slash(names: list[str]) -> bool: - if len(names) == 0: - return False - name = names[-1] - count = 0 - i = len(name) - 1 - while i >= 0 and name[i] == "\\": - count += 1 - i -= 1 - return count % 2 == 1 - - if any(ends_with_odd_slash(name) for name in expected_as_dict.values()): - pytest.skip("Inverse property does not hold for names ending with '\\'") - nameparts = _dict_to_nameparts(expected_as_dict) merged = nameparts.merge_last_name_first resplit = parse_single_name_into_parts(merged, strict=strict)