"""Functionality for ModelCIF data/ categories when represented as
:class:`dict`.
"""
# Copyright (c) 2026, SIB - Swiss Institute of Bioinformatics and
# Biozentrum - University of Basel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
[docs]
def add_row_to_category_dict(
cat_dict, row_dict, ordinal_item=None, null_value=False
):
"""Add a row to a category dict or create it if empty.
Appends a new row to ``cat_dict``, which holds mmCIF category data as
a column-oriented dict of lists, as obtained from
``gemmi.cif.Block.find_mmcif_category(raw=False)`` or an empty dict. If
``cat_dict`` is empty, it is populated from ``row_dict`` directly.
Args:
cat_dict (dict[str]): Column-oriented category dict to update. Each key
maps to a list of values, one entry per row.
row_dict (dict[str]): Dict with keys and single values representing the
new row. Keys not present in ``cat_dict`` are added as new
columns, back-filled with ``null_value``. Keys only in
``cat_dict`` are filled with ``null_value``.
ordinal_item (:class:`str` or ``None``): Key whose value is
auto-incremented on each call. Set to ``"1"`` when ``cat_dict``
is empty. If set but not present in a non-empty ``cat_dict``,
it is silently ignored. Defaults to ``None``.
null_value: Value used to fill missing keys. ``False`` for
inapplicable (``'.'``), ``None`` for unknown (``'?'``).
Defaults to ``False``.
Returns:
:class:`str` or ``None``: The new ordinal value as a string, or
``None`` if ``ordinal_item`` is not set.
Raises:
ValueError: If column lengths in ``cat_dict`` are inconsistent
after appending.
"""
new_ordinal = None
if cat_dict:
# add row to cat_dict
num_rows = None
for key in cat_dict:
if key == ordinal_item:
new_ordinal = str(int(cat_dict[key][-1]) + 1)
cat_dict[key].append(new_ordinal)
else:
cat_dict[key].append(row_dict.get(key, null_value))
# check all lengths equal
if num_rows is None:
num_rows = len(cat_dict[key])
else:
if num_rows != len(cat_dict[key]):
raise ValueError(
f"Column length mismatch for key '{key}': expected "
f"{num_rows}, got {len(cat_dict[key])} values"
)
# add extra columns if needed
for key in row_dict:
if key not in cat_dict:
cat_dict[key] = [null_value] * (num_rows - 1)
cat_dict[key].append(row_dict[key])
else:
# fill new cat_dict
for key, val in row_dict.items():
cat_dict[key] = [val]
if ordinal_item is not None:
new_ordinal = "1"
cat_dict[ordinal_item] = [new_ordinal]
return new_ordinal