Repository: sciunto-org/python-bibtexparser
Pull request #334 (2022-10-11 15:47:22)

:bug: `customization.getnames`: respect protected names
Fixes https://github.com/sciunto-org/python-bibtexparser/issues/327

I worked on https://github.com/sciunto-org/python-bibtexparser/issues/327 all while not noticing https://github.com/sciunto-org/python-bibtexparser/pull/331 .

Not surprisingly the solution is the same, with the difference that the current PR only breaks at spaces outside a sequence protected by `"{...}"` in line with the rest of the implementation of `getnames`. This reduces some of the worries discussed there I believe

Update: I was reading https://github.com/sciunto-org/python-bibtexparser/pull/331 and realised that I could do things better (safer). It does account for escaped brackets too. I think that this PR should be merged in favour of https://github.com/sciunto-org/python-bibtexparser/pull/331 . BTW, it seems to fix #9 as well that I happen to find in the tests . Note that there is a panic-switch: in case of unmatched brackets the split is entirely not applied. Instead there could be an error (or warning) in my opinion. 

For reference, I took the implementation from my own code ( https://github.com/tdegeus/texplain/blob/21d0d50aa10a6a9a3ee437e0c374d088142da709/texplain/__init__.py#L80 ) licensed MIT 

@QuantumNovice @MiWeiss 



MiWeiss: Hi @tdegeus 

Thanks a lot for this PR! I guess we're very close to merging it. 

P.S. Next time you work on any issues you identify (which would be highly appreciated), please leave a corresponding remark in the issue. I'll then assign it to you, making sure that no one else works on the issue. Sorry to @QuantumNovice - I'll reply to you in your PR.
tdegeus: Indeed, I'm very sorry  @QuantumNovice . I started working on it even after your latests commit. So I was frustrated with myself and wanted to close my PR. However, the discussion you guys had over there made me realise that I actually had something to contribute on top of https://github.com/sciunto-org/python-bibtexparser/pull/331 in terms of escaped brackets, as that is something that I had solved in the passed. 

Patch:
diff --git a/bibtexparser/customization.py b/bibtexparser/customization.py
index 99c48fb..169f1ba 100644
--- a/bibtexparser/customization.py
+++ b/bibtexparser/customization.py
@@ -299,6 +299,61 @@ def splitname(name, strict_mode=True):
     return parts
 
 
+def find_matching(
+    text: str,
+    opening: str,
+    closing: str,
+    ignore_escaped: bool = True,
+) -> dict:
+    r"""
+    Find matching 'brackets'.
+
+    :param text: The string to consider.
+    :param opening: The opening bracket (e.g. "(", "[", "{").
+    :param closing: The closing bracket (e.g. ")", "]", "}").
+    :param ignore_escaped: Ignore escaped bracket (e.g. "\(", "\[", "\{", "\)", "\]", "\}").
+    :return: Dictionary with ``{index_opening: index_closing}``
+    """
+
+    a = []
+    b = []
+
+    if ignore_escaped:
+        opening = r"(?<!\\)" + opening
+        closing = r"(?<!\\)" + closing
+
+    for i in re.finditer(opening, text):
+        a.append(i.span()[0])
+
+    for i in re.finditer(closing, text):
+        b.append(-1 * i.span()[0])
+
+    if len(a) == 0 and len(b) == 0:
+        return {}
+
+    if len(a) != len(b):
+        raise IndexError(f"Unmatching {opening}...{closing} found")
+
+    brackets = sorted(a + b, key=lambda i: abs(i))
+
+    ret = {}
+    stack = []
+
+    for i in brackets:
+        if i >= 0:
+            stack.append(i)
+        else:
+            if len(stack) == 0:
+                raise IndexError(f"No closing {closing} at: {i:d}")
+            j = stack.pop()
+            ret[j] = -1 * i
+
+    if len(stack) > 0:
+        raise IndexError(f"No opening {opening} at {stack.pop():d}")
+
+    return ret
+
+
 def getnames(names):
     """Convert people names as surname, firstnames
     or surname, initials.
@@ -322,7 +377,29 @@ def getnames(names):
             last = namesplit[0].strip()
             firsts = [i.strip() for i in namesplit[1].split()]
         else:
-            namesplit = namestring.split()
+            if "{" in namestring and "}" in namestring:
+                try:
+                    brackets = find_matching(namestring, "{", "}")
+                except IndexError:
+                    tidynames.append(namestring)
+                    continue
+                namesplit = []
+                start = 0
+                i = 0
+                while True:
+                    if i in brackets:
+                        i = brackets[i]
+                    else:
+                        i += 1
+                    if i >= len(namestring):
+                        break
+                    if namestring[i] == " ":
+                        namesplit.append(namestring[start:i])
+                        start = i + 1
+                    elif i == len(namestring) - 1:
+                        namesplit.append(namestring[start:])
+            else:
+                namesplit = namestring.split()
             last = namesplit.pop()
             firsts = [i.replace('.', '. ').strip() for i in namesplit]
         if last in ['jnr', 'jr', 'junior']:


Test patch:
diff --git a/bibtexparser/tests/test_customization.py b/bibtexparser/tests/test_customization.py
index 77450e7..f43e77d 100644
--- a/bibtexparser/tests/test_customization.py
+++ b/bibtexparser/tests/test_customization.py
@@ -22,6 +22,15 @@ class TestBibtexParserMethod(unittest.TestCase):
                  'Jean la Tour',
                  'Jean le Tour',
                  'Mike ben Akar',
+                 'A. {Delgado de Molina}',
+                 r'M. Vign{\'e}',
+                 'Tom {de Geus}',
+                 'Tom {de \{Geus}',
+                 'Tom \{de Geus\}',
+                 'Tom de {G\{eus}',
+                 'Foo B{\'a}r',
+                 r'{G{\'{e}}rard} {Ben Arous}',
+                 'Incorrect {{name}',
                  #'Jean de la Tour',
                  #'Johannes Diderik van der Waals',
                  ]
@@ -35,18 +44,20 @@ class TestBibtexParserMethod(unittest.TestCase):
                     'la Tour, Jean',
                     'le Tour, Jean',
                     'ben Akar, Mike',
+                    '{Delgado de Molina}, A.',
+                    r'Vign{\'e}, M.',
+                    '{de Geus}, Tom',
+                    '{de \{Geus}, Tom',
+                    'Geus\}, Tom \{de',
+                    'de {G\{eus}, Tom',
+                    'B{\'a}r, Foo',
+                    r'{Ben Arous}, {G{\'{e}}rard}',
+                    'Incorrect {{name}',
                     #'de la Tour, Jean',
                     #'van der Waals, Johannes Diderik',
                     ]
         self.assertEqual(result, expected)
 
-    @unittest.skip('Bug #9')
-    def test_getnames_braces(self):
-        names = ['A. {Delgado de Molina}', r'M. Vign{\'e}']
-        result = getnames(names)
-        expected = ['Delgado de Molina, A.', 'Vigné, M.']
-        self.assertEqual(result, expected)
-
     ###########
     # page_double_hyphen
     ###########