From 05c15cccabce9757c05c638f082f0006e01c99b4 Mon Sep 17 00:00:00 2001 From: Bill Hlavacek Date: Wed, 10 Jun 2026 11:44:58 -0600 Subject: [PATCH] Defer BioNetGen app setup in library imports --- bionetgen/modelapi/bngfile.py | 12 ++--- bionetgen/modelapi/bngparser.py | 13 ++--- bionetgen/modelapi/model.py | 16 ++----- bionetgen/network/network.py | 9 +--- bionetgen/network/networkparser.py | 6 --- bionetgen/simulator/csimulator.py | 8 +--- tests/test_csimulator_errors.py | 16 +++++-- tests/test_deferred_app_setup.py | 71 ++++++++++++++++++++++++++++ tests/test_modelapi_export_errors.py | 42 ++++++++++++++++ 9 files changed, 141 insertions(+), 52 deletions(-) create mode 100644 tests/test_deferred_app_setup.py diff --git a/bionetgen/modelapi/bngfile.py b/bionetgen/modelapi/bngfile.py index 3d735a3a..5513257a 100644 --- a/bionetgen/modelapi/bngfile.py +++ b/bionetgen/modelapi/bngfile.py @@ -4,17 +4,11 @@ import tempfile from typing import NoReturn -from bionetgen.main import BioNetGen +from bionetgen.main import get_default_bng_path from bionetgen.core.exc import BNGFileError from bionetgen.core.utils.logging import BNGLogger from bionetgen.core.utils.utils import find_BNG_path, run_command, ActionList -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] - class BNGFile: """ @@ -45,7 +39,7 @@ class BNGFile: """ def __init__( - self, path, BNGPATH=def_bng_path, generate_network=False, suppress=True + self, path, BNGPATH=None, generate_network=False, suppress=True ) -> None: self.path = path self.logger = BNGLogger() @@ -53,6 +47,8 @@ def __init__( self.suppress = suppress AList = ActionList() self._action_list = [i + "(" for i in AList.possible_types] + if BNGPATH is None: + BNGPATH = get_default_bng_path() BNGPATH, bngexec = find_BNG_path(BNGPATH) self.BNGPATH = BNGPATH self.bngexec = bngexec diff --git a/bionetgen/modelapi/bngparser.py b/bionetgen/modelapi/bngparser.py index cd540904..3ca16035 100644 --- a/bionetgen/modelapi/bngparser.py +++ b/bionetgen/modelapi/bngparser.py @@ -1,6 +1,5 @@ import xmltodict -from bionetgen.main import BioNetGen from bionetgen.core.exc import BNGFileError, BNGParseError, BNGModelError from tempfile import TemporaryFile @@ -11,12 +10,6 @@ from .blocks import ActionBlock, ProtocolBlock from bionetgen.core.utils.utils import ActionList -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] - def _normalize_action_text(action: str) -> str: """Strip BNGL comments and unquoted whitespace, keep quoted spans intact. @@ -188,13 +181,15 @@ class BNGParser: def __init__( self, path, - BNGPATH=def_bng_path, + BNGPATH=None, parse_actions=True, generate_network=False, suppress=True, ) -> None: self.to_parse_actions = parse_actions - self.bngfile = BNGFile(path, generate_network=generate_network, suppress=True) + self.bngfile = BNGFile( + path, BNGPATH=BNGPATH, generate_network=generate_network, suppress=suppress + ) self.alist = ActionList() self.alist.define_parser() diff --git a/bionetgen/modelapi/model.py b/bionetgen/modelapi/model.py index 9581d363..5c679148 100644 --- a/bionetgen/modelapi/model.py +++ b/bionetgen/modelapi/model.py @@ -1,6 +1,5 @@ import copy, tempfile, shutil -from bionetgen.main import BioNetGen from bionetgen.core.exc import BNGFileError, BNGModelError from .bngparser import BNGParser @@ -18,12 +17,6 @@ PopulationMapBlock, ) -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] - ###### CORE OBJECT AND PARSING FRONT-END ###### class bngmodel: @@ -73,9 +66,7 @@ class bngmodel: "population_maps", "rules", "reaction_rules", "actions". """ - def __init__( - self, bngl_model, BNGPATH=def_bng_path, generate_network=False, suppress=True - ): + def __init__(self, bngl_model, BNGPATH=None, generate_network=False, suppress=True): self.active_blocks = [] # We want blocks to be printed in the same order every time self._block_order = [ @@ -94,7 +85,10 @@ def __init__( self.model_name = "" self.model_path = bngl_model self.bngparser = BNGParser( - bngl_model, generate_network=generate_network, suppress=True + bngl_model, + BNGPATH=BNGPATH, + generate_network=generate_network, + suppress=suppress, ) self.bngparser.parse_model(self) for block in self._block_order: diff --git a/bionetgen/network/network.py b/bionetgen/network/network.py index 616fd6eb..c089afdb 100644 --- a/bionetgen/network/network.py +++ b/bionetgen/network/network.py @@ -1,4 +1,3 @@ -from bionetgen.main import BioNetGen from bionetgen.network.networkparser import BNGNetworkParser from bionetgen.network.blocks import ( NetworkGroupBlock, @@ -11,12 +10,6 @@ NetworkPopulationMapBlock, ) -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] - ###### CORE OBJECT AND PARSING FRONT-END ###### class Network: @@ -46,7 +39,7 @@ class Network: type of simulator is libRR for libRoadRunner simulator. """ - def __init__(self, bngl_model, BNGPATH=def_bng_path): + def __init__(self, bngl_model, BNGPATH=None): self.active_blocks = [] # We want blocks to be printed in the same order every time self.block_order = [ diff --git a/bionetgen/network/networkparser.py b/bionetgen/network/networkparser.py index bf53d4e5..f5c7a9c6 100644 --- a/bionetgen/network/networkparser.py +++ b/bionetgen/network/networkparser.py @@ -1,7 +1,6 @@ import re, os from bionetgen.core.exc import BNGParseError from bionetgen.core.utils.logging import BNGLogger -from bionetgen.main import BioNetGen from bionetgen.network.blocks import ( NetworkGroupBlock, NetworkParameterBlock, @@ -13,11 +12,6 @@ NetworkPopulationMapBlock, ) -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] logger = BNGLogger() diff --git a/bionetgen/simulator/csimulator.py b/bionetgen/simulator/csimulator.py index 05313a65..ec328f59 100644 --- a/bionetgen/simulator/csimulator.py +++ b/bionetgen/simulator/csimulator.py @@ -2,7 +2,7 @@ import numpy as np from .bngsimulator import BNGSimulator -from bionetgen.main import BioNetGen +from bionetgen.main import get_conf from bionetgen.core.exc import BNGCompileError, BNGFormatError, BNGSimError from bionetgen.core.utils.logging import BNGLogger @@ -27,11 +27,6 @@ def _new_ccompiler(): return ccompiler.new_compiler() -# This allows access to the CLIs config setup -app = BioNetGen() -app.setup() -conf = app.config["bionetgen"] -def_bng_path = conf["bngpath"] logger = BNGLogger() @@ -161,6 +156,7 @@ class CSimulator(BNGSimulator): """ def __init__(self, model_file, generate_network=False): + conf = get_conf() # check cvode library paths if (conf.get("cvode_include") is None) or (conf.get("cvode_lib") is None): logger.warning( diff --git a/tests/test_csimulator_errors.py b/tests/test_csimulator_errors.py index 3351e1c7..46a9f14e 100644 --- a/tests/test_csimulator_errors.py +++ b/tests/test_csimulator_errors.py @@ -12,11 +12,15 @@ def test_csimulator_init_logs_missing_cvode_paths(): fake_model.species = {} fake_compiler = mock.MagicMock() mock_conf_get = mock.MagicMock(side_effect=lambda key: None) + fake_conf = mock.MagicMock() + fake_conf.get = mock_conf_get def fake_compile(self): self.lib_file = "/tmp/fake/libcsim.so" - with mock.patch.object(csim_module.conf, "get", mock_conf_get), mock.patch.object( + with mock.patch.object( + csim_module, "get_conf", return_value=fake_conf + ) as mock_get_conf, mock.patch.object( csim_module, "logger" ) as mock_logger, mock.patch.object( csim_module.bionetgen, "bngmodel", return_value=fake_model @@ -29,6 +33,7 @@ def fake_compile(self): ) as mock_wrapper: csim_module.CSimulator("/fake/model.bngl") + mock_get_conf.assert_called_once_with() mock_logger.warning.assert_called_once() warning_args, warning_kwargs = mock_logger.warning.call_args assert "CVODE include and library paths are not set" in warning_args[0] @@ -55,16 +60,19 @@ def test_csimulator_init_invalid_model_type_raises_bng_format_error(): "cvode_lib": "/tmp/lib", }[key] ) + fake_conf = mock.MagicMock() + fake_conf.get = mock_conf_get - with mock.patch.object(csim_module.conf, "get", mock_conf_get), mock.patch.object( - csim_module, "logger" - ) as mock_logger: + with mock.patch.object( + csim_module, "get_conf", return_value=fake_conf + ) as mock_get_conf, mock.patch.object(csim_module, "logger") as mock_logger: with pytest.raises( BNGFormatError, match="CSimulator model input must be a BNGL path or bngmodel instance", ): csim_module.CSimulator(123) + mock_get_conf.assert_called_once_with() mock_logger.error.assert_called_once() error_args, error_kwargs = mock_logger.error.call_args assert "got int" in error_args[0] diff --git a/tests/test_deferred_app_setup.py b/tests/test_deferred_app_setup.py new file mode 100644 index 00000000..e46fe81b --- /dev/null +++ b/tests/test_deferred_app_setup.py @@ -0,0 +1,71 @@ +import subprocess +import sys +import textwrap +from pathlib import Path +from unittest.mock import patch + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def test_library_imports_do_not_setup_default_app(): + script = textwrap.dedent(""" + import importlib + from unittest.mock import patch + + modules = [ + "bionetgen.modelapi.bngfile", + "bionetgen.modelapi.bngparser", + "bionetgen.modelapi.model", + "bionetgen.network.network", + "bionetgen.network.networkparser", + "bionetgen.simulator.csimulator", + ] + + with patch("cement.core.foundation.App.setup") as setup: + for module in modules: + importlib.import_module(module) + + if setup.called: + raise SystemExit(f"App.setup() called during import: {setup.call_args!r}") + """) + result = subprocess.run( + [sys.executable, "-c", script], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + +def test_bngfile_resolves_default_bngpath_lazily(): + from bionetgen.modelapi import bngfile as bngfile_module + + with patch.object( + bngfile_module, "get_default_bng_path", return_value="/default/bng" + ) as mock_default, patch.object( + bngfile_module, + "find_BNG_path", + return_value=("/resolved/bng", "/resolved/bng/BNG2.pl"), + ) as mock_find: + bngfile_module.BNGFile("/some/model.bngl") + + mock_default.assert_called_once_with() + mock_find.assert_called_once_with("/default/bng") + + +def test_bngfile_explicit_bngpath_skips_default_lookup(): + from bionetgen.modelapi import bngfile as bngfile_module + + with patch.object( + bngfile_module, "get_default_bng_path" + ) as mock_default, patch.object( + bngfile_module, + "find_BNG_path", + return_value=("/custom/bng", "/custom/bng/BNG2.pl"), + ) as mock_find: + bngfile_module.BNGFile("/some/model.bngl", BNGPATH="/custom/bng") + + mock_default.assert_not_called() + mock_find.assert_called_once_with("/custom/bng") diff --git a/tests/test_modelapi_export_errors.py b/tests/test_modelapi_export_errors.py index d9a78600..8c9a08d8 100644 --- a/tests/test_modelapi_export_errors.py +++ b/tests/test_modelapi_export_errors.py @@ -202,6 +202,48 @@ def test_parse_model_xml_generation_failure_wraps_bngfile_error(mock_bngfile_cls parser.parse_model(MagicMock()) +@patch("bionetgen.modelapi.bngparser.BNGFile") +def test_bngparser_passes_path_options_to_bngfile(mock_bngfile_cls): + from bionetgen.modelapi.bngparser import BNGParser + + BNGParser( + "/some/model.bngl", + BNGPATH="/custom/bng", + generate_network=True, + suppress=False, + ) + + mock_bngfile_cls.assert_called_once_with( + "/some/model.bngl", + BNGPATH="/custom/bng", + generate_network=True, + suppress=False, + ) + + +@patch("bionetgen.modelapi.model.BNGParser") +def test_bngmodel_passes_path_options_to_bngparser(mock_parser_cls): + from bionetgen.modelapi.model import bngmodel + + parser = MagicMock() + mock_parser_cls.return_value = parser + + bngmodel( + "/some/model.bngl", + BNGPATH="/custom/bng", + generate_network=True, + suppress=False, + ) + + mock_parser_cls.assert_called_once_with( + "/some/model.bngl", + BNGPATH="/custom/bng", + generate_network=True, + suppress=False, + ) + parser.parse_model.assert_called_once() + + def test_setup_simulator_write_xml_failure_raises_bngmodel_error_and_restores_actions( tmp_path, monkeypatch ):