Repository: sciunto-org/python-bibtexparser
Pull request #459 (2024-02-06 14:00:04)

Adding missing default `allow_inplace_modification`. Simplifying examples


Patch:
diff --git a/README.md b/README.md
index f4e85e3..2781ae6 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ Writes the content of a bibtex library to a `.bib` file. Optional formatting par
 
 ## About
 
-Since 2022, `bibtexparser` is primarily written and maintained by Michael Weiss ([@MiWeiss](https://github.com/MiWeiss/)). 
+Since 2022, `bibtexparser` is primarily written and maintained by Michael Weiss ([@MiWeiss](https://github.com/MiWeiss/)). In 2024, Tom de Geus ([@tdegeus](https://github.com/tdegeus)) joined as co-maintainer. 
 
 Credits and thanks to the many contributors who helped creating this library, including
 François Boulogne ([@sciunto](https://github.com/sciunto/), creator of the first version) and Olivier Mangin ([@omangin](https://github.com/omangin/), long-term contributor).
diff --git a/bibtexparser/entrypoint.py b/bibtexparser/entrypoint.py
index 5ecf871..0d77607 100644
--- a/bibtexparser/entrypoint.py
+++ b/bibtexparser/entrypoint.py
@@ -76,14 +76,19 @@ def parse_string(
 ):
     """Parse a BibTeX string.
 
-    Args:
-        bibtex_str (str): BibTeX string to parse
-        parse_stack (Optional[Iterable[Middleware]], optional): List of middleware to apply to the database after splitting. If None (default), a default stack will be used providing simple standard functionality will be used.
-        append_middleware (Optional[Iterable[Middleware]], optional): List of middleware to append to the default stack (ignored if a not-None parse_stack is passed).
-        library (Optional[Library], optional): Library to add entries to. If None (default), a new library will be created.
-
-    Returns:
-        Library: Parsed BibTeX database
+    :param bibtex_str: BibTeX string to parse
+    :param parse_stack:
+        List of middleware to apply to the database after splitting.
+        If ``None`` (default), a default stack will be used providing simple standard functionality.
+
+    :param append_middleware:
+        List of middleware to append to the default stack
+        (ignored if a not-``None`` parse_stack is passed).
+
+    :param library:
+        Library to add entries to. If ``None`` (default), a new library will be created.
+
+    :return: Library: Parsed BibTeX database
     """
     splitter = Splitter(bibstr=bibtex_str)
     library = splitter.split(library=library)
@@ -103,14 +108,17 @@ def parse_file(
 ) -> Library:
     """Parse a BibTeX file
 
-    Args:
-        path (str): Path to BibTeX file
-        parse_stack (Optional[Iterable[Middleware]], optional): List of middleware to apply to the database after splitting. If None (default), a default stack will be used providing simple standard functionality will be used.
-        append_middleware (Optional[Iterable[Middleware]], optional): List of middleware to append to the default stack (ignored if a not-None parse_stack is passed).
-        encoding: Encoding of the .bib file. Default encoding is "UTF-8".
+    :param path: Path to BibTeX file
+    :param parse_stack:
+        List of middleware to apply to the database after splitting.
+        If ``None`` (default), a default stack will be used providing simple standard functionality.
+
+    :param append_middleware:
+        List of middleware to append to the default stack
+        (ignored if a not-``None`` parse_stack is passed).
 
-    Returns:
-        Library: Parsed BibTeX library
+    :param encoding: Encoding of the .bib file. Default encoding is ``"UTF-8"``.
+    :return: Library: Parsed BibTeX library
     """
     with open(path, encoding=encoding) as f:
         bibtex_str = f.read()
diff --git a/bibtexparser/exceptions.py b/bibtexparser/exceptions.py
index cfbcd34..161c78d 100644
--- a/bibtexparser/exceptions.py
+++ b/bibtexparser/exceptions.py
@@ -40,8 +40,8 @@ class ParserStateException(ParsingException):
 class RegexMismatchException(ParserStateException):
     """Raised when regex matches are inconsistent, implying a bug in the parser.
 
-    For example, raised when first match `@string{`
-    is not followed by an overlapping match `}`.
+    For example, raised when first match ``@string{``
+    is not followed by an overlapping match ``}``.
     """
 
     def __init__(self, first_match, expected_match, second_match):
diff --git a/bibtexparser/middlewares/month.py b/bibtexparser/middlewares/month.py
index 0dbd621..31c9ec2 100644
--- a/bibtexparser/middlewares/month.py
+++ b/bibtexparser/middlewares/month.py
@@ -12,7 +12,7 @@ class _MonthInterpolator(BlockMiddleware, abc.ABC):
     """Abstract class to handle month-conversions."""
 
     # docstr-coverage: inherited
-    def __init__(self, allow_inplace_modification: bool):
+    def __init__(self, allow_inplace_modification: bool = True):
         super().__init__(
             allow_inplace_modification=allow_inplace_modification,
             allow_parallel_execution=True,
diff --git a/bibtexparser/middlewares/names.py b/bibtexparser/middlewares/names.py
index 11d4d42..7152870 100644
--- a/bibtexparser/middlewares/names.py
+++ b/bibtexparser/middlewares/names.py
@@ -574,6 +574,9 @@ def split_multiple_persons_names(names):
 
         # Change in brace level.
         if char == "{":
+            if step == NEXT_WORD:
+                spans[-1].append(possible_end)
+                spans.append([pos - 1])
             bracelevel += 1
             step = START_WHITESPACE
             continue
diff --git a/bibtexparser/model.py b/bibtexparser/model.py
index 2e37b00..71283dd 100644
--- a/bibtexparser/model.py
+++ b/bibtexparser/model.py
@@ -5,7 +5,8 @@ from typing import Any, Dict, List, Optional, Set
 class Block(abc.ABC):
     """A abstract superclass of all top-level building blocks of a bibtex file.
 
-    E.g. a `@string` block, a `@preamble` block, an `@entry` block, a comment, etc."""
+    E.g. a ``@string`` block, a ``@preamble`` block, an ``@entry`` block, a comment, etc.
+    """
 
     def __init__(
         self,
@@ -51,15 +52,15 @@ class Block(abc.ABC):
         return self._parser_metadata
 
     def get_parser_metadata(self, key: str) -> Optional[Any]:
-        """EXPERIMENTAL: get auxiliary information stored in `parser_metadata`.
+        """EXPERIMENTAL: get auxiliary information stored in ``parser_metadata``.
 
-        See attribute `parser_metadata` for more information."""
+        See attribute ``parser_metadata`` for more information."""
         return self._parser_metadata.get(key, None)
 
     def set_parser_metadata(self, key: str, value: Any):
-        """EXPERIMENTAL: set auxiliary information stored in `parser_metadata`.
+        """EXPERIMENTAL: set auxiliary information stored in ``parser_metadata``.
 
-        See attribute `parser_metadata` for more information."""
+        See attribute ``parser_metadata`` for more information."""
         self._parser_metadata[key] = value
 
     def __eq__(self, other):
@@ -72,7 +73,7 @@ class Block(abc.ABC):
 
 
 class String(Block):
-    """Bibtex Blocks of the `@string` type, e.g. @string{me = "My Name"}."""
+    """Bibtex Blocks of the ``@string`` type, e.g. ``@string{me = "My Name"}``."""
 
     def __init__(
         self,
@@ -87,7 +88,7 @@ class String(Block):
 
     @property
     def key(self) -> str:
-        """The key of the string, e.g. `me` in `@string{me = "My Name"}`."""
+        """The key of the string, e.g. ``me`` in ``@string{me = "My Name"}``."""
         return self._key
 
     @key.setter
@@ -96,7 +97,7 @@ class String(Block):
 
     @property
     def value(self) -> str:
-        """The value of the string, e.g. `"My Name"` in `@string{me = "My Name"}`."""
+        """The value of the string, e.g. ``"My Name"`` in ``@string{me = "My Name"}``."""
         return self._value
 
     @value.setter
@@ -114,7 +115,7 @@ class String(Block):
 
 
 class Preamble(Block):
-    """Bibtex Blocks of the `@preamble` type, e.g. @preamble{This is a preamble}."""
+    """Bibtex Blocks of the ``@preamble`` type, e.g. ``@preamble{This is a preamble}``."""
 
     def __init__(
         self, value: str, start_line: Optional[int] = None, raw: Optional[str] = None
@@ -124,7 +125,7 @@ class Preamble(Block):
 
     @property
     def value(self) -> str:
-        """The value of the preamble, e.g. `blabla` in `@preamble{blabla}`."""
+        """The value of the preamble, e.g. ``blabla`` in ``@preamble{blabla}``."""
         return self._value
 
     @value.setter
@@ -142,7 +143,7 @@ class Preamble(Block):
 
 
 class ExplicitComment(Block):
-    """Bibtex Blocks of the `@comment` type, e.g. @comment{This is a comment}."""
+    """Bibtex Blocks of the ``@comment`` type, e.g. ``@comment{This is a comment}``."""
 
     def __init__(
         self, comment: str, start_line: Optional[int] = None, raw: Optional[str] = None
@@ -152,7 +153,7 @@ class ExplicitComment(Block):
 
     @property
     def comment(self) -> str:
-        """The value of the comment, e.g. `blabla` in `@comment{blabla}`."""
+        """The value of the comment, e.g. ``blabla`` in ``@comment{blabla}``."""
         return self._comment
 
     @comment.setter
@@ -170,7 +171,7 @@ class ExplicitComment(Block):
 
 
 class ImplicitComment(Block):
-    """Bibtex outside of an @{...} block, which is treated as a comment."""
+    """Bibtex outside of an ``@{...}`` block, which is treated as a comment."""
 
     def __init__(
         self, comment: str, start_line: Optional[int] = None, raw: Optional[str] = None
@@ -198,7 +199,7 @@ class ImplicitComment(Block):
 
 
 class Field:
-    """A field of a Bibtex entry, e.g. `author = {John Doe}`."""
+    """A field of a Bibtex entry, e.g. ``author = {John Doe}``."""
 
     def __init__(self, key: str, value: Any, start_line: Optional[int] = None):
         self._start_line = start_line
@@ -207,7 +208,7 @@ class Field:
 
     @property
     def key(self) -> str:
-        """The key of the field, e.g. `author` in `author = {John Doe}`."""
+        """The key of the field, e.g. ``author`` in ``author = {John Doe}``."""
         return self._key
 
     @key.setter
@@ -216,7 +217,7 @@ class Field:
 
     @property
     def value(self) -> Any:
-        """The value of the field, e.g. `{John Doe}` in `author = {John Doe}`."""
+        """The value of the field, e.g. ``{John Doe}`` in ``author = {John Doe}``."""
         return self._value
 
     @value.setter
@@ -247,7 +248,7 @@ class Field:
 
 
 class Entry(Block):
-    """Bibtex Blocks of the `@entry` type, e.g. @article{Cesar2013, ...}."""
+    """Bibtex Blocks of the ``@entry`` type, e.g. ``@article{Cesar2013, ...}``."""
 
     def __init__(
         self,
@@ -264,7 +265,7 @@ class Entry(Block):
 
     @property
     def entry_type(self):
-        """The type of the entry, e.g. `article` in `@article{Cesar2013, ...}`."""
+        """The type of the entry, e.g. ``article`` in ``@article{Cesar2013, ...}``."""
         return self._entry_type
 
     @entry_type.setter
@@ -273,7 +274,7 @@ class Entry(Block):
 
     @property
     def key(self):
-        """The key of the entry, e.g. `Cesar2013` in `@article{Cesar2013, ...}`."""
+        """The key of the entry, e.g. ``Cesar2013`` in ``@article{Cesar2013, ...}``."""
         return self._key
 
     @key.setter
@@ -282,7 +283,7 @@ class Entry(Block):
 
     @property
     def fields(self) -> List[Field]:
-        """The key-value attributes of an entry, as `Field` instances."""
+        """The key-value attributes of an entry, as ``Field`` instances."""
         return self._fields
 
     @fields.setter
@@ -304,16 +305,30 @@ class Entry(Block):
         else:
             self._fields.append(field)
 
-    def pop_field(self, key: str) -> Optional[Field]:
-        """Removes and returns the field with the given key, if present."""
+    def pop(self, key: str, default=None) -> Optional[Field]:
+        """Removes and returns the field with the given key.
+
+        :param key: The key of the field to remove.
+        :param default: The value to return if the field does not exist."""
         try:
             field = self.fields_dict.pop(key)
         except KeyError:
-            return None
+            return default
 
         self._fields = [f for f in self._fields if f.key != key]
         return field
 
+    def get(self, key: str, default=None) -> Optional[Field]:
+        """Returns the field with the given key, or the default value if it does not exist.
+
+        :param key: The key of the field.
+        :param default: The value to return if the field does not exist."""
+        return self.fields_dict.get(key, default)
+
+    def __contains__(self, key: str) -> bool:
+        """Dict-mimicking ``in`` operator."""
+        return key in self.fields_dict
+
     def __getitem__(self, key: str) -> Any:
         """Dict-mimicking index.
 
@@ -340,9 +355,9 @@ class Entry(Block):
         """Dict-mimicking index.
 
         This serves for partial v1.x backwards compatibility,
-        as well as for a shorthand for `pop_field`.
+        as well as for a shorthand for `pop`.
         """
-        self.pop_field(key)
+        self.pop(key)
 
     def items(self):
         """Dict-mimicking, for partial v1.x backwards compatibility.
@@ -436,7 +451,7 @@ class DuplicateBlockKeyBlock(ParsingFailedBlock):
 
     @property
     def key(self) -> str:
-        """The key of the entry, e.g. `Cesar2013` in `@article{Cesar2013, ...}`."""
+        """The key of the entry, e.g. ``Cesar2013`` in ``@article{Cesar2013, ...}``."""
         return self._key
 
     @key.setter
diff --git a/docs/source/customize.rst b/docs/source/customize.rst
index 3ff81ce..ca193f1 100644
--- a/docs/source/customize.rst
+++ b/docs/source/customize.rst
@@ -16,7 +16,7 @@ The core functionality of bibtexparser is deliberately kept simple:
 * Upon writing, the splitting is reversed and the blocks are joined together again, with few formatting options.
 
 Advanced transformations of blocks, such as sorting, encoding, cross-referencing, etc. are not part of the core functionality,
-but can be optionally added to the parse stack by using the corresponging middleware layers:
+but can be optionally added to the parse stack by using the corresponding middleware layers:
 Middleware layers helper classes providing the functionality take a library object and return a new, transformed version of said library.
 
 Middleware Layers
@@ -28,9 +28,9 @@ Middleware Layers
 
     # We want to add three new middleware layers to our parse stack:
     layers = [
-        m.MonthIntMiddleware(True), # Months should be represented as int (0-12)
-        m.SeparateCoAuthors(True), # Co-authors should be separated as list of strings
-        m.SplitNameParts(True) # Individual Names should be split into first, von, last, jr parts
+        m.MonthIntMiddleware(), # Months should be represented as int (0-12)
+        m.SeparateCoAuthors(), # Co-authors should be separated as list of strings
+        m.SplitNameParts() # Individual Names should be split into first, von, last, jr parts
     ]
     library = bibtexparser.parse_file('bibtex.bib', append_middleware=layers)
 
@@ -147,4 +147,4 @@ Specifically, a user may pass a :class:`bibtexparser.BibtexFormatter` object to
     bib_str = bibtexparser.write_string(library, bibtex_format=bibtex_format)
 
 A few more options are provided and we refer to the docstrings of :class:`bibtexparser.BibtexFormat` for details.
-Note: Sorting of blocks and fields is done with the corresponding middleware, as described above.
\ No newline at end of file
+Note: Sorting of blocks and fields is done with the corresponding middleware, as described above.
diff --git a/examples/middelware.ipynb b/examples/middelware.ipynb
index 750e326..a466c54 100644
--- a/examples/middelware.ipynb
+++ b/examples/middelware.ipynb
@@ -101,9 +101,9 @@
     "\n",
     "# We want to add three new middleware layers to our parse stack:\n",
     "layers = [\n",
-    "    m.MonthIntMiddleware(True), # Months should be represented as int (0-12)\n",
-    "    m.SeparateCoAuthors(True), # Co-authors should be separated\n",
-    "    m.SplitNameParts(True) # Names should be split into first, von, last, jr parts\n",
+    "    m.MonthIntMiddleware(), # Months should be represented as int (0-12)\n",
+    "    m.SeparateCoAuthors(), # Co-authors should be separated\n",
+    "    m.SplitNameParts() # Names should be split into first, von, last, jr parts\n",
     "]\n",
     "library_append_pstack = bibtexparser.parse_file('bibtex.bib', append_middleware=layers)\n",
     "\n",


Test patch:
diff --git a/tests/middleware_tests/test_names.py b/tests/middleware_tests/test_names.py
index 8dde269..d2f3ff1 100644
--- a/tests/middleware_tests/test_names.py
+++ b/tests/middleware_tests/test_names.py
@@ -47,6 +47,18 @@ from tests.middleware_tests.middleware_test_util import (
         ("P. M. Sutherland And\nSmith, J.", ["P. M. Sutherland", "Smith, J."]),
         ("P. M. Sutherland aNd\tSmith, J.", ["P. M. Sutherland", "Smith, J."]),
         ("P. M. Sutherland aNd\nSmith, J.", ["P. M. Sutherland", "Smith, J."]),
+        (
+            r"{Delgado de Molina}, A. and Vign{\'e}, M. and {de Geus}, Tom",
+            ["{Delgado de Molina}, A.", r"Vign{\'e}, M.", "{de Geus}, Tom"],
+        ),
+        (
+            r"{Delgado de Molina}, A. and {de Geus}, Tom and Vign{\'e}, M.",
+            ["{Delgado de Molina}, A.", "{de Geus}, Tom", r"Vign{\'e}, M."],
+        ),
+        (
+            r"{Delgado de Molina}, A. and {de Geus}, Tom and {Vign{\'e}, M.}",
+            ["{Delgado de Molina}, A.", "{de Geus}, Tom", r"{Vign{\'e}, M.}"],
+        ),
         (
             "Fake Name an{d brace in an}d and Somebody Else",
             ["Fake Name an{d brace in an}d", "Somebody Else"],
@@ -238,6 +250,10 @@ REGULAR_NAME_PARTS_PARSING_TEST_CASES = (
         {"first": ["A."], "von": [], "last": ["{Delgado de Molina}"], "jr": []},
     ),
     (r"M. Vign{\'e}", {"first": ["M."], "von": [], "last": [r"Vign{\'e}"], "jr": []}),
+    (
+        r"Tom {de Geus}",
+        {"first": ["Tom"], "von": [], "last": ["{de Geus}"], "jr": []},
+    ),
     ###############################################################################
     #
     # Test cases from
diff --git a/tests/test_model.py b/tests/test_model.py
index 4807bc8..198e8a0 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -77,6 +77,34 @@ def test_entry_deepcopy():
     assert entry_1.fields_dict["field"] == entry_2.fields_dict["field"]
 
 
+def test_entry_get():
+    entry1 = Entry(
+        "article", "key", [Field("field", "value", 1), Field("foo", "bar", 2)], 1, "raw"
+    )
+    entry2 = Entry(
+        "article", "key", [Field("field", "value", 1), Field("foo", "bar", 2)], 1, "raw"
+    )
+    assert entry1.get("other", "default") == "default"
+    assert entry1.get("foo") == Field("foo", "bar", 2)
+    assert entry1 == entry2
+
+
+def test_entry_pop():
+    entry1 = Entry(
+        "article", "key", [Field("field", "value", 1), Field("foo", "bar", 2)], 1, "raw"
+    )
+    entry2 = Entry("article", "key", [Field("field", "value", 1)], 1, "raw")
+    assert entry1.pop("other", "default") == "default"
+    assert entry1.pop("foo") == Field("foo", "bar", 2)
+    assert entry1 == entry2
+
+
+def test_entry_contains():
+    entry = Entry("article", "key", [Field("field", "value", 1)], 1, "raw")
+    assert "field" in entry
+    assert "other" not in entry
+
+
 def test_string_equality():
     # Equal to itself
     string_1 = String(