From 06a9ec12223c8b2406d3334df24c4dfbd168436a Mon Sep 17 00:00:00 2001 From: Michael Weiss Date: Fri, 12 Jun 2026 13:13:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Make=20Entry=20dict=20access=20s?= =?UTF-8?q?ymmetric=20for=20ENTRYTYPE=20and=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `entry[ENTRYTYPE]` / `entry[ID]` returned `entry_type` / `key`, but the corresponding assignment created a literal field that was shadowed on read and serialized as a regular field (e.g. `ENTRYTYPE = book`) on write. Particularly dangerous for v1 migrators, where these dict keys were the documented way to change an entry type or key. `__setitem__` now mirrors `__getitem__` and sets `entry_type` / `key`; `__contains__` now reports the two keys as contained, consistent with `__getitem__` and `items()`. Fixes #532 Co-Authored-By: Claude Fable 5 --- bibtexparser/model.py | 17 +++++++++++++++-- tests/test_model.py | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/bibtexparser/model.py b/bibtexparser/model.py index 3e93f63..421aef9 100644 --- a/bibtexparser/model.py +++ b/bibtexparser/model.py @@ -378,7 +378,12 @@ def get(self, key: str, default=None) -> Optional[Field]: return self.fields_dict.get(key, default) def __contains__(self, key: str) -> bool: - """Dict-mimicking ``in`` operator.""" + """Dict-mimicking ``in`` operator. + + As in ``__getitem__``, ``ENTRYTYPE`` and ``ID`` are always contained. + """ + if key in ("ENTRYTYPE", "ID"): + return True return key in self.fields_dict def __getitem__(self, key: str) -> Any: @@ -400,8 +405,16 @@ def __setitem__(self, key: str, value: Any): This serves for partial v1.x backwards compatibility, as well as for a shorthand for `set_field`. + + Mirroring ``__getitem__``, the keys ``ENTRYTYPE`` and ``ID`` + set ``entry_type`` and ``key`` instead of a field. """ - self.set_field(Field(key, value)) + if key == "ENTRYTYPE": + self.entry_type = value + elif key == "ID": + self.key = value + else: + self.set_field(Field(key, value)) def __delitem__(self, key: str) -> None: """Dict-mimicking index. diff --git a/tests/test_model.py b/tests/test_model.py index 497fce4..c00929e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -96,6 +96,30 @@ def test_entry_contains(): entry = Entry("article", "key", [Field("field", "value", 1)], 1, "raw") assert "field" in entry assert "other" not in entry + # ENTRYTYPE and ID are exposed by `__getitem__`, hence always contained + assert "ENTRYTYPE" in entry + assert "ID" in entry + + +def test_entry_setitem_field(): + entry = Entry("article", "key", [Field("field", "value", 1)], 1, "raw") + entry["field"] = "new_value" + entry["other"] = "other_value" + assert entry["field"] == "new_value" + assert entry["other"] == "other_value" + assert [f.key for f in entry.fields] == ["field", "other"] + + +def test_entry_setitem_entrytype_and_id(): + entry = Entry("article", "key", [Field("field", "value", 1)], 1, "raw") + entry["ENTRYTYPE"] = "book" + entry["ID"] = "new_key" + assert entry.entry_type == "book" + assert entry.key == "new_key" + assert entry["ENTRYTYPE"] == "book" + assert entry["ID"] == "new_key" + # No literal fields must be created for these keys + assert [f.key for f in entry.fields] == ["field"] def test_string_equality():