From b31360d07a0d4a679d7e24bf549eeb9b95029575 Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Fri, 23 Oct 2026 22:24:30 +0200 Subject: [PATCH] write_yaml_model: write a single ec-code or smiles annotation as a list, as RAVEN does --- src/raven_toolbox/io/yaml.py | 15 ++++++++------- tests/test_io_yaml.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/raven_toolbox/io/yaml.py b/src/raven_toolbox/io/yaml.py index af880e0..9a218e8 100644 --- a/src/raven_toolbox/io/yaml.py +++ b/src/raven_toolbox/io/yaml.py @@ -621,8 +621,10 @@ def _collapse_singleton_annotations(entries) -> None: Matches writeYAMLmodel.m's MIRIAM handling: every annotation entry collapses to a scalar when it carries a single value, except ``ec-code``/``smiles`` — RAVEN's own writer marks exactly those two - ``forceList`` — which stay a list even for one value, since cobra and - geckopy read them as ``list[str]``. Normalises in place. + ``forceList``, which are always a list, even for one value, since cobra and + geckopy read them as ``list[str]``. cobra stores a single identifier as a + bare string, so a string under one of those keys is wrapped in a list. + Normalises in place. """ for entry in entries: if not isinstance(entry, dict): @@ -631,11 +633,10 @@ def _collapse_singleton_annotations(entries) -> None: if not isinstance(annotation, dict): continue for key, value in annotation.items(): - if ( - key not in _LIST_ONLY_ANNOTATION_KEYS - and isinstance(value, list) - and len(value) == 1 - ): + if key in _LIST_ONLY_ANNOTATION_KEYS: + if isinstance(value, str): + annotation[key] = [value] + elif isinstance(value, list) and len(value) == 1: annotation[key] = value[0] diff --git a/tests/test_io_yaml.py b/tests/test_io_yaml.py index 4521c40..5be224e 100644 --- a/tests/test_io_yaml.py +++ b/tests/test_io_yaml.py @@ -453,3 +453,29 @@ def test_needs_quote_leaves_longer_words_plain(value): from raven_toolbox.io.yaml import _needs_quote assert not _needs_quote(value) + + +def test_single_ec_code_and_smiles_are_written_as_lists(tmp_path): + # cobra stores a one-identifier annotation as a bare string; RAVEN's writer forces + # ec-code and smiles to a list even for one value, and other keys stay scalars. + m = cobra.Model("m") + a = cobra.Metabolite("a_c", compartment="c", name="a") + b = cobra.Metabolite("b_c", compartment="c", name="b") + a.annotation = {"smiles": "C1=NC2", "kegg.compound": "C00002"} + r = cobra.Reaction("R1") + m.add_reactions([r]) + r.add_metabolites({a: -1, b: 1}) + r.annotation = {"ec-code": "1.1.1.1", "kegg.reaction": "R00001"} + + out = tmp_path / "m.yml" + write_yaml_model(m, out) + lines = out.read_text().splitlines() + + assert lines[lines.index(" - ec-code:") + 1] == " - 1.1.1.1" + assert lines[lines.index(" - smiles:") + 1] == " - C1=NC2" + assert " - kegg.compound: C00002" in lines + assert " - kegg.reaction: R00001" in lines + + back = read_yaml_model(out) + assert back.reactions.R1.annotation["ec-code"] == ["1.1.1.1"] + assert back.metabolites.a_c.annotation["smiles"] == ["C1=NC2"]