From 197354bfbf5cd9bb2e489de5922fca442892d399 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Wed, 24 Jun 2026 14:47:26 +0100 Subject: [PATCH] More internal fixes/tidying --- docs/sphinx/source/api/Contributors.md | 2 +- examples/sbml/SBML.md | 89 -------------------------- examples/sbml/SBML.rst | 33 ---------- src/modelspec/__init__.py | 2 +- src/modelspec/base_types.py | 41 +++++++++--- src/modelspec/utils.py | 16 +++-- 6 files changed, 44 insertions(+), 139 deletions(-) diff --git a/docs/sphinx/source/api/Contributors.md b/docs/sphinx/source/api/Contributors.md index a23950b0..71238940 100644 --- a/docs/sphinx/source/api/Contributors.md +++ b/docs/sphinx/source/api/Contributors.md @@ -3,7 +3,7 @@ # Modelspec contributors This page list names and Github profiles of contributors to Modelspec, listed in no particular order. -This page is generated periodically, most recently on 2026-06-10. +This page is generated periodically, most recently on 2026-06-24. - Padraig Gleeson ([@pgleeson](https://github.com/pgleeson)) - Manifest Chakalov ([@mqnifestkelvin](https://github.com/mqnifestkelvin)) diff --git a/examples/sbml/SBML.md b/examples/sbml/SBML.md index 664e42e0..3a36b45b 100644 --- a/examples/sbml/SBML.md +++ b/examples/sbml/SBML.md @@ -1670,95 +1670,6 @@ XHTML field of SBase - - -## SpeciesReference -### Allowed parameters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sidstr SId optional
namestr string optional
metaidstr XML ID optional
sboTermstrSBOTerm optional
notesNotes XHTML 1.0 optional
annotationstrXML content optional
speciesstrSIdRef
stoichiometryfloatdouble optional
constantboolboolean
- -## Notes -XHTML field of SBase - -### Allowed parameters - - - - - - - - - - - - - - -
xmlnsstrstr fixed "http://www.w3.org/1999/xhtml"
contentstrstr valid XHTML
## ModifierSpeciesReference diff --git a/examples/sbml/SBML.rst b/examples/sbml/SBML.rst index 083cfc0f..98c2accf 100644 --- a/examples/sbml/SBML.rst +++ b/examples/sbml/SBML.rst @@ -650,39 +650,6 @@ Allowed field Data Type Description **content** str str valid XHTML =============== =========== ======================================== -================ -SpeciesReference -================ -**Allowed parameters** - -================= ======================================= ==================== -Allowed field Data Type Description -================= ======================================= ==================== -**sid** str SId optional -**name** str string optional -**metaid** str XML ID optional -**sboTerm** str SBOTerm optional -**notes** ` <#notes>`__ XHTML 1.0 optional -**annotation** str XML content optional -**species** str SIdRef -**stoichiometry** float double optional -**constant** bool boolean -================= ======================================= ==================== - -===== -Notes -===== -XHTML field of SBase - -**Allowed parameters** - -=============== =========== ======================================== -Allowed field Data Type Description -=============== =========== ======================================== -**xmlns** str str fixed "http://www.w3.org/1999/xhtml" -**content** str str valid XHTML -=============== =========== ======================================== - ======================== ModifierSpeciesReference ======================== diff --git a/src/modelspec/__init__.py b/src/modelspec/__init__.py index 16d48162..87b6d854 100644 --- a/src/modelspec/__init__.py +++ b/src/modelspec/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.10" +__version__ = "0.4.0" from .base_types import Base, define, has, field, fields, optional, instance_of, in_ diff --git a/src/modelspec/base_types.py b/src/modelspec/base_types.py index 7241375a..a7e119dc 100644 --- a/src/modelspec/base_types.py +++ b/src/modelspec/base_types.py @@ -44,8 +44,10 @@ class EvaluableExpression(str): str, so it can be used as a string. """ - def __init__(self, expr): - self.expr = expr + @property + def expr(self): + """The expression string. Always reflects the underlying str value.""" + return str(self) # Union of types that are allowed as value expressions for parameters. @@ -62,7 +64,7 @@ def print_(text: str, print_it: bool = False): prefix = "modelspec >>> " if not isinstance(text, str): - text = ("%s" % text).decode("ascii") + text = "%s" % text print("{}{}".format(prefix, text.replace("\n", "\n" + prefix))) @@ -111,9 +113,9 @@ def to_json(self) -> str: """ return json.dumps(self.to_dict(), indent=4) - def to_bson(self) -> str: + def to_bson(self) -> bytes: """ - Convert the Base object to a BSON string representation. + Convert the Base object to a BSON (bytes) representation. """ return bson.encode(self.to_dict()) @@ -238,7 +240,9 @@ def to_json_file( return filename - def to_bson_file(self, filename: str, include_metadata: bool = True) -> str: + def to_bson_file( + self, filename: Optional[str] = None, include_metadata: bool = True + ) -> str: """Convert modelspec format to bson format Args: @@ -321,7 +325,8 @@ def to_xml_file( def from_file(cls, filename: str) -> "Base": """ Create a :class:`.Base` from its representation stored in a file. Auto-detect the correct deserialization code - based on file extension. Currently supported formats are; JSON(.json) and YAML (.yaml or .yml) + based on file extension. Currently supported formats are: JSON (.json), YAML (.yaml or .yml), + BSON (.bson) and XML (.xml). Args: filename: The name of the file to load. @@ -340,7 +345,7 @@ def from_file(cls, filename: str) -> "Base": else: raise ValueError( f"Cannot auto-detect modelspec serialization format from filename ({filename}). The filename " - f"must have one of the following extensions: .json, .yml, or .yaml." + f"must have one of the following extensions: .json, .yaml, .yml, .bson, or .xml." ) @classmethod @@ -945,10 +950,19 @@ def insert_links(text, format=MARKDOWN_FORMAT): ) ) + # De-duplicate while preserving order, so a type referenced by more than + # one field/child doesn't get its documentation section emitted twice. + seen = set() + unique_referenced = [] for r in referenced: + if r not in seen: + seen.add(r) + unique_referenced.append(r) + + for r in unique_referenced: if format in (MARKDOWN_FORMAT, RST_FORMAT): doc_string += r._cls_generate_documentation(format=format) - if format in (DICT_FORMAT): + if format == DICT_FORMAT: doc_dict.update(r._cls_generate_documentation(format=format)) if format in (MARKDOWN_FORMAT, RST_FORMAT): @@ -1090,7 +1104,14 @@ def _is_list_base(cl): Check if a class is a list of Base objects. These will be serialized as dicts if the underlying class has an id attribute. """ - return get_origin(cl) is list and issubclass(get_args(cl)[0], Base) + if get_origin(cl) is not list: + return False + + args = get_args(cl) + # Guard against a bare ``list`` annotation (no args) and against element + # types that aren't classes (e.g. List[Union[A, B]]), which would make + # issubclass() raise TypeError. + return len(args) > 0 and isinstance(args[0], type) and issubclass(args[0], Base) converter.register_unstructure_hook_factory(_is_list_base, _unstructure_list_base) diff --git a/src/modelspec/utils.py b/src/modelspec/utils.py index b4cab168..e51d0f9d 100644 --- a/src/modelspec/utils.py +++ b/src/modelspec/utils.py @@ -336,7 +336,7 @@ def _parse_attributes(dict_format, to_build): ff = type_to_use() print_(f" Type for {key}: {type_to_use} ({ff})", verbose) ff = _parse_element({v: value[v]}, ff) - exec("to_build.%s.append(ff)" % key) + getattr(to_build, key).append(ff) else: if ( isinstance(value, str) @@ -361,7 +361,7 @@ def _parse_attributes(dict_format, to_build): else: ff = type_to_use() ff = _parse_attributes(value, ff) - exec("to_build.%s = ff" % key) + setattr(to_build, key, ff) else: if isinstance(to_build, dict): @@ -378,12 +378,12 @@ def _parse_attributes(dict_format, to_build): for vl in value: ff = type_to_use() ff = _parse_element(vl, ff) - exec("to_build.%s.append(ff)" % key) + getattr(to_build, key).append(ff) else: type_to_use = to_build.allowed_fields[key][1] ff = type_to_use() ff = _parse_attributes(value, ff) - exec("to_build.%s = ff" % key) + setattr(to_build, key, ff) return to_build @@ -445,7 +445,7 @@ def _params_info(parameters, multiline=False): def evaluate( expr: Union[int, float, str, list, dict], - parameters: dict = {}, + parameters: dict = None, rng: Random = None, array_format: str = FORMAT_NUMPY, verbose: bool = False, @@ -465,6 +465,10 @@ def evaluate( cast_to_int: return an int for float/string values if castable """ + # Work on a private copy so we never mutate the caller's dict (or a shared + # default) when injecting rng/math/numpy below or when eval() adds __builtins__. + parameters = dict(parameters) if parameters is not None else {} + if array_format == FORMAT_TENSORFLOW: import tensorflow as tf @@ -591,3 +595,5 @@ def parse_list_like(list_str): pass if "[" in list_str: return eval(list_str) + + raise ValueError(f"Cannot parse {list_str!r} ({type(list_str)}) as a list")