Repository: sciunto-org/python-bibtexparser

Issue:
Parsing fails when bibtex contains duplicates
Based on the v2 docs, it seems bibtexparser should be able to process bibtex files with duplicates. However, if a bibtex file contains duplicates, an error is produced:

```python
import bibtexparser
str = """@article{duplicate,
  author = {Duplicate, A.},
  title = {Duplicate article},
  year = {2022},
}
@article{duplicate,
  author = {Duplicate, A.},
  title = {Duplicate article},
  year = {2022},
}"""

bibtexparser.parse_string(str)
```

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "~/.local/lib/python3.10/site-packages/bibtexparser/entrypoint.py", line 93, in parse_string
        library = middleware.transform(library=library)
      File "~/.local/lib/python3.10/site-packages/bibtexparser/middlewares/interpolate.py", line 60, in transform
        for field in entry.fields:
    AttributeError: 'DuplicateEntryKeyBlock' object has no attribute 'fields'

Discussion:

Patch:
diff --git a/bibtexparser/library.py b/bibtexparser/library.py
index e2cfa6e..296b7fc 100644
--- a/bibtexparser/library.py
+++ b/bibtexparser/library.py
@@ -2,7 +2,7 @@ from typing import Dict, List, Union
 
 from .model import (
     Block,
-    DuplicateEntryKeyBlock,
+    DuplicateBlockKeyBlock,
     Entry,
     ExplicitComment,
     ImplicitComment,
@@ -37,6 +37,7 @@ class Library:
             blocks = [blocks]
 
         for block in blocks:
+            # This may replace block with a DuplicateEntryKeyBlock
             block = self._add_to_dicts(block)
             self._blocks.append(block)
 
@@ -77,7 +78,7 @@ class Library:
 
         if (
             new_block is not block_after_add
-            and isinstance(new_block, DuplicateEntryKeyBlock)
+            and isinstance(new_block, DuplicateBlockKeyBlock)
             and fail_on_duplicate_key
         ):
             # Revert changes to old_block
@@ -104,7 +105,7 @@ class Library:
             prev_block_with_same_key.key == duplicate.key
         ), "Internal BibtexParser Error. Duplicate blocks have different keys."
 
-        return DuplicateEntryKeyBlock(
+        return DuplicateBlockKeyBlock(
             start_line=duplicate.start_line,
             raw=duplicate.raw,
             key=duplicate.key,
@@ -116,25 +117,23 @@ class Library:
         """Safely add block references to private dict structures.
 
         :param block: Block to add.
-        :returns: The block that was added to the library, except if a block
-            of same type and with same key already exists, in which case a
-            DuplicateKeyBlock is returned.
+        :returns: The block that was added to the library. If a block
+            of same type and with same key already existed, a
+            DuplicateKeyBlock is returned (not added to dict).
         """
         if isinstance(block, Entry):
             try:
                 prev_block_with_same_key = self._entries_by_key[block.key]
                 block = self._cast_to_duplicate(prev_block_with_same_key, block)
             except KeyError:
-                pass  # No previous entry with same key
-            finally:
+                # No duplicate found
                 self._entries_by_key[block.key] = block
         elif isinstance(block, String):
             try:
                 prev_block_with_same_key = self._strings_by_key[block.key]
                 block = self._cast_to_duplicate(prev_block_with_same_key, block)
             except KeyError:
-                pass  # No previous string with same key
-            finally:
+                # No duplicate found
                 self._strings_by_key[block.key] = block
         return block
 
diff --git a/bibtexparser/model.py b/bibtexparser/model.py
index ba12aec..d3b99be 100644
--- a/bibtexparser/model.py
+++ b/bibtexparser/model.py
@@ -386,7 +386,7 @@ class MiddlewareErrorBlock(ParsingFailedBlock):
         )
 
 
-class DuplicateEntryKeyBlock(ParsingFailedBlock):
+class DuplicateBlockKeyBlock(ParsingFailedBlock):
     """An error-indicating block created for blocks with keys present in the library already.
 
     To get the block that caused this error, call `block.ignore_error_block`."""


Test patch:
diff --git a/tests/splitter_tests/test_splitter_basic.py b/tests/splitter_tests/test_splitter_basic.py
index 1b11a89..c65a601 100644
--- a/tests/splitter_tests/test_splitter_basic.py
+++ b/tests/splitter_tests/test_splitter_basic.py
@@ -263,3 +263,59 @@ def test_failed_block():
         'Was still looking for field-value closing `"`'
         in failed_block.error.abort_reason
     )
+
+
+duplicate_bibtex_entry_keys = """
+@article{duplicate,
+  author = {Duplicate, A.},
+  title = {Duplicate article 1},
+  year = {2021},
+}
+@article{duplicate,
+  author = {Duplicate, B.},
+  title = {Duplicate article 2},
+  year = {2022},
+}"""
+
+
+def test_handles_duplicate_entries():
+    """Makes sure that duplicate keys are handled correctly."""
+    import bibtexparser
+
+    lib = bibtexparser.parse_string(duplicate_bibtex_entry_keys)
+    assert len(lib.blocks) == 2
+    assert len(lib.entries) == 1
+    assert len(lib.entries_dict) == 1
+    assert len(lib.failed_blocks) == 1
+
+    assert lib.entries[0]["title"] == "Duplicate article 1"
+    assert isinstance(lib.failed_blocks[0], bibtexparser.model.DuplicateBlockKeyBlock)
+    assert lib.failed_blocks[0].previous_block["title"] == "Duplicate article 1"
+    assert lib.failed_blocks[0].ignore_error_block["title"] == "{Duplicate article 2}"
+    assert isinstance(lib.failed_blocks[0].ignore_error_block, bibtexparser.model.Entry)
+
+
+duplicate_bibtex_string_keys = """
+@string{duplicate = "Duplicate string 1"}
+@string{duplicate = "Duplicate string 2"}
+@string{duplicate = "Duplicate string 3"}
+"""
+
+
+def test_handles_duplicate_strings():
+    """Makes sure that duplicate string keys are handled correctly."""
+    import bibtexparser
+
+    lib = bibtexparser.parse_string(duplicate_bibtex_string_keys)
+    assert len(lib.blocks) == 3
+    assert len(lib.strings) == 1
+    assert len(lib.strings_dict) == 1
+    assert len(lib.failed_blocks) == 2
+
+    assert lib.strings[0].value == "Duplicate string 1"
+    assert isinstance(lib.failed_blocks[0], bibtexparser.model.DuplicateBlockKeyBlock)
+    assert lib.failed_blocks[0].previous_block.value == "Duplicate string 1"
+    assert lib.failed_blocks[0].ignore_error_block.value == '"Duplicate string 2"'
+    assert isinstance(
+        lib.failed_blocks[0].ignore_error_block, bibtexparser.model.String
+    )