diff --git a/.github/actions/setup-pybnf/action.yml b/.github/actions/setup-pybnf/action.yml new file mode 100644 index 00000000..68292aef --- /dev/null +++ b/.github/actions/setup-pybnf/action.yml @@ -0,0 +1,75 @@ +name: Set up pybnf (bngsim-less) +description: >- + Install BioNetGen (BNG2.pl) plus pybnf and its public runtime/test + dependencies into a uv venv, deliberately omitting the private, macOS-only + bngsim wheel. Tests run under PYBNF_NO_BNGSIM=1 so bngsim-marked tests skip + (see tests/conftest.py). BNG2.pl is still required: Configuration validation + execs `BNG2.pl -v` for any BNGLModel regardless of bngsim, and the analytical + tests aside, most of the suite builds such configs. + +inputs: + python-version: + description: Python version for the venv (e.g. "3.10", "3.12"). + required: true + +runs: + using: composite + steps: + # Provision the interpreter with setup-python rather than uv's managed + # (python-build-standalone) builds: libroadrunner is a C++ extension that + # dlopen's libpython, and uv's standalone Python 3.10 does not expose + # libpython3.10.so.1.0 on the loader path (3.12's does). setup-python's + # Pythons are built --enable-shared with libpython discoverable, so + # roadrunner imports on every matrix leg. + - uses: actions/setup-python@v5 + id: python + with: + python-version: ${{ inputs.python-version }} + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + # No uv.lock in this repo; key the cache on pyproject.toml so it can + # invalidate when deps change. + cache-dependency-glob: pyproject.toml + + - name: Install BioNetGen 2.9.3 + shell: bash + run: | + # BioNetGen is free/open-source and provides BNG2.pl (Perl is + # preinstalled on ubuntu runners). It is NOT vendored into the repo or + # the pybnf wheel -- it is fetched onto the ephemeral runner each run, + # mirroring the developer prerequisite (BNGPATH on a local machine). + curl -fsSL \ + https://github.com/RuleWorld/bionetgen/releases/download/BioNetGen-2.9.3/BioNetGen-2.9.3-linux.tar.gz \ + -o "$RUNNER_TEMP/bionetgen.tar.gz" + tar -xzf "$RUNNER_TEMP/bionetgen.tar.gz" -C "$RUNNER_TEMP" + echo "BNGPATH=$RUNNER_TEMP/BioNetGen-2.9.3" >> "$GITHUB_ENV" + + - name: Create venv and install pybnf without bngsim + shell: bash + run: | + # Build the venv from the setup-python interpreter (see note above), not + # a uv-managed one. + uv venv --python "${{ steps.python.outputs.python-path }}" + # Mirror pyproject.toml's runtime deps MINUS bngsim (private, mac-only + # wheel, not on PyPI), plus the test stack and pytest-xdist. Keep these + # version bounds in sync with pyproject.toml; bngsim is the only + # intentional omission. + uv pip install \ + 'dask>=2021.5.0,<2023.0' \ + 'distributed>=2021.6.2,<2023.0' \ + 'libroadrunner>=1.6.0,<3' \ + 'msgpack>=0.6.2,<2' \ + 'numpy>=1.24,<3' \ + 'paramiko>=2.7,<5' \ + 'pyparsing>=2.4,<4' \ + 'scipy>=1.10,<2' \ + 'tornado>=6.1,<7' \ + 'pytest>=7,<10' \ + 'hypothesis>=6,<7' \ + 'pytest-xdist' \ + 'tomli>=2,<3; python_version < "3.11"' + # Install pybnf itself without re-resolving deps (which would pull + # bngsim and fail). + uv pip install --no-deps . diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..0e26c230 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,30 @@ +name: integration + +# Manual only. The slow tier is the 4 statistical-recovery tests against +# analytical targets (AnalyticalModel) -- bngsim-free and BNG2.pl-free, ~8 min. +# +# NOTE: the bngsim *simulation* suites (true NFsim/RuleMonkey/SBML runs) are +# deliberately NOT in hosted CI -- they need the private, macOS-only bngsim +# wheel. They remain covered by the local pre-push hook (.pre-commit-config.yaml) +# on a dev machine until bngsim ships Linux wheels. +# +# A nightly `schedule:` cron could be added here, but is intentionally omitted +# pending a maintainer decision. +on: + workflow_dispatch: + +jobs: + slow: + runs-on: ubuntu-latest + name: slow recovery tests + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-pybnf + with: + python-version: '3.12' + + - name: Run slow analytical recovery tests + env: + PYBNF_NO_BNGSIM: '1' + run: uv run --no-sync pytest -m slow -n auto diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..bf08734c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: lint + +on: + push: + branches: [master] + pull_request: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: ruff check + # Pin ruff so the rule set is reproducible: the [tool.ruff] config in + # pyproject.toml pins which rules run, but a newer ruff could add rules + # to the default selection and turn the gate red unexpectedly. + run: uvx ruff@0.15.14 check . + + # NOTE: `ruff format --check` is intentionally NOT enforced in v1. The + # codebase predates ruff formatting; a repo-wide reformat is a large, + # behavior-adjacent diff best done in its own dedicated PR. Add a format + # check here once that lands. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..0a24ea98 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +name: tests + +on: + push: + branches: [master] + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Min and max of the supported range (requires-python >= 3.10; + # classifiers list through 3.12). + python-version: ['3.10', '3.12'] + name: pytest (py${{ matrix.python-version }}) + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-pybnf + with: + python-version: ${{ matrix.python-version }} + + - name: Run unit tests (bngsim-less, parallel) + env: + # bngsim is a private mac-only wheel; with it absent, conftest.py + # auto-skips bngsim/nfsim/sbml/etc.-marked tests. The remaining suite + # runs against the bngsim-less code paths (and BNG2.pl from the + # composite action). -n auto parallelizes across the runner's cores. + PYBNF_NO_BNGSIM: '1' + # --no-sync: do NOT let `uv run` re-resolve the project env from + # pyproject.toml (that would pull the private bngsim and fail). The + # composite action already populated .venv. + run: uv run --no-sync pytest -m "not slow" -n auto diff --git a/examples/sampler_benchmarking/run_all.py b/examples/sampler_benchmarking/run_all.py index 4551b69a..1baabf38 100755 --- a/examples/sampler_benchmarking/run_all.py +++ b/examples/sampler_benchmarking/run_all.py @@ -159,7 +159,7 @@ def main(): print(f"{submitted} jobs completed, {failed} failed.") else: print(f"{submitted} jobs submitted, {failed} failed.") - print(f"Monitor with: squeue -u $USER") + print("Monitor with: squeue -u $USER") if __name__ == "__main__": diff --git a/pybnf/algorithms.py b/pybnf/algorithms.py index 2bc1f431..931a7cc3 100644 --- a/pybnf/algorithms.py +++ b/pybnf/algorithms.py @@ -8,7 +8,6 @@ from subprocess import STDOUT from .pset import run_subprocess -from numpy import mean from .bngsim_model import ( BngsimModel, @@ -24,9 +23,8 @@ from .data import Data from .pset import PSet from .pset import Trajectory -from .pset import TimeCourse from .pset import BNGLModel -from .pset import NetModel, BNGLModel, SbmlModelNoTimeout +from .pset import NetModel from .pset import OutOfBoundsException from .pset import FailedSimulationError from .printing import print0, print1, print2, PybnfError diff --git a/pybnf/bngsim_model.py b/pybnf/bngsim_model.py index 3d6a1419..333e85d6 100644 --- a/pybnf/bngsim_model.py +++ b/pybnf/bngsim_model.py @@ -14,13 +14,10 @@ from ._bngsim_caps import ( BNGSIM_AVAILABLE, BNGSIM_ERROR, - BNGSIM_FEATURES, BNGSIM_HAS_NFSIM, BNGSIM_HAS_RULEMONKEY, - BNGSIM_MISSING, BNGSIM_VERSION, bngsim, - feature_missing_reason, ) from .data import Data from .pset import FreeParameter, Model, NetModel, PSet, _stage_and_rewrite_tfun_files @@ -501,7 +498,6 @@ def _create_nf_session(session_backend, xml_path, molecule_limit=None): if molecule_limit is None: return session_cls(xml_path) return session_cls(xml_path, molecule_limit=molecule_limit) - return session def _destroy_nf_session(session): diff --git a/pybnf/bngsim_sbml_model.py b/pybnf/bngsim_sbml_model.py index d2293d4f..a7f429c8 100644 --- a/pybnf/bngsim_sbml_model.py +++ b/pybnf/bngsim_sbml_model.py @@ -29,8 +29,6 @@ from ._bngsim_caps import ( - BNGSIM_AVAILABLE, - BNGSIM_HAS_LIBSBML as LIBSBML_AVAILABLE, BNGSIM_HAS_SBML, BNGSIM_SBML_ERROR, bngsim, diff --git a/pybnf/config.py b/pybnf/config.py index 8afb3c56..211defaa 100644 --- a/pybnf/config.py +++ b/pybnf/config.py @@ -2,7 +2,7 @@ from .data import Data, DuplicateColumnError -from .objective import ChiSquareObjective, ChiSquareObjective_Dynamic, NegBinLikelihood_Dynamic, NegBinLikelihood, SumOfSquaresObjective, NormSumOfSquaresObjective, \ +from .objective import ChiSquareObjective, ChiSquareObjective_Dynamic, NegBinLikelihood_Dynamic, SumOfSquaresObjective, NormSumOfSquaresObjective, \ AveNormSumOfSquaresObjective, SumOfDiffsObjective, NegBinLikelihood, KLLikelihood, DirectPassObjective from .pset import BNGLModel, ModelError, SbmlModel, SbmlModelNoTimeout, FreeParameter, TimeCourse, ParamScan, \ @@ -10,13 +10,11 @@ from .bngsim_sbml_model import ( BNGSIM_HAS_SBML, BNGSIM_SBML_ERROR, - BngsimSbmlModel, BngsimSbmlModelNoTimeout, ) from .bngsim_antimony_model import ( BNGSIM_HAS_ANTIMONY, BNGSIM_ANTIMONY_ERROR, - BngsimAntimonyModel, BngsimAntimonyModelNoTimeout, ) from .printing import verbosity, print1, PybnfError diff --git a/pybnf/pset.py b/pybnf/pset.py index 82d4e3e8..8ea983c7 100644 --- a/pybnf/pset.py +++ b/pybnf/pset.py @@ -9,13 +9,12 @@ import re import copy import signal -from subprocess import run, Popen, STDOUT, PIPE, DEVNULL, CalledProcessError, TimeoutExpired +from subprocess import Popen, STDOUT, PIPE, CalledProcessError, TimeoutExpired from .data import Data import heapq import traceback import roadrunner as rr import pickle -from os.path import join import os import shutil import tempfile diff --git a/pyproject.toml b/pyproject.toml index 10e46009..98685219 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,35 @@ packages = ["pybnf"] [tool.setuptools.dynamic] version = { attr = "pybnf.__version__" } +[tool.ruff] +target-version = "py310" +# Pins ruff's default rule set (pycodestyle E4/E7/E9 + pyflakes F) so local runs +# and CI agree. Line length (E501) and the broader style/lint families are +# intentionally left off for v1; revisit once the codebase is reformatted. + +[tool.ruff.lint] +# Pre-existing legacy style that PyBNF does not enforce. Each is either purely +# cosmetic or a behavioral change to "fix", so it is out of scope for the lint +# gate (which must stay mechanical). Items tied to dev/PUNCHLIST.md are tracked +# and fixed there, not here. +ignore = [ + "E402", # module-import-not-at-top: intentional (lazy backend guards, test bootstrap) + "E712", # ==True/False: legacy; rewriting can change truthiness semantics (e.g. numpy bools) + "E721", # type(x) == T comparisons: legacy, semantically fine in context + "E722", # bare except: tracked as ROB-4 in dev/PUNCHLIST.md (behavioral fix) + "E741", # ambiguous variable names (l/I/O): legacy + "F841", # unused locals: several are CQ-5 dead code (tracked); common bind-then-assert in tests +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "E702", # multiple-statements-on-one-line (semicolons): established test-file style + "F402", # import shadowed by loop variable +] +# context.py is a re-export shim: test modules do `from .context import data`, +# so its imports are "unused" only within the file itself. +"tests/context.py" = ["F401"] + [tool.pytest.ini_options] minversion = "7.0" testpaths = ["tests"] diff --git a/tests/test_bayes_mcmc.py b/tests/test_bayes_mcmc.py index 478db831..35de5a66 100644 --- a/tests/test_bayes_mcmc.py +++ b/tests/test_bayes_mcmc.py @@ -1,4 +1,4 @@ -from .context import data, algorithms, pset, objective, config +from .context import data, algorithms, pset, config import os import shutil import numpy as np diff --git a/tests/test_bngsim_bridge.py b/tests/test_bngsim_bridge.py index e136dade..93498ef9 100644 --- a/tests/test_bngsim_bridge.py +++ b/tests/test_bngsim_bridge.py @@ -394,6 +394,7 @@ def _stub_normalize_method_without_rulemonkey(method): return bngsim_model.bngsim.normalize_method(method) +@pytest.mark.bngsim def test_classify_actions_for_bngsim_routes_nf_aliases(): for method in ('nf', 'nf_reject', 'nfsim'): assert bngsim_model.classify_actions_for_bngsim([ @@ -409,6 +410,7 @@ def test_classify_actions_for_bngsim_routes_rulemonkey_public_aliases(): ]) == bngsim_model.BNGSIM_BACKEND_NF +@pytest.mark.bngsim def test_normalize_nf_action_method_normalizes_nfsim_aliases(): for method in ('nf', 'nf_reject', 'nfsim'): assert bngsim_model._normalize_nf_action_method(method) == 'nf_reject' @@ -420,6 +422,7 @@ def test_normalize_nf_action_method_normalizes_rulemonkey_public_aliases(): assert bngsim_model._normalize_nf_action_method(method) == 'nf_exact' +@pytest.mark.bngsim @pytest.mark.parametrize('method', ['rm', 'rulemonkey']) def test_normalize_nf_action_method_rejects_rulemonkey_when_unavailable(monkeypatch, method): monkeypatch.setattr( @@ -439,6 +442,7 @@ def test_normalize_nf_action_method_rejects_unavailable_canonical_aliases(method bngsim_model._normalize_nf_action_method(method) +@pytest.mark.bngsim def test_normalize_nf_action_method_rejects_non_nf_method(): with pytest.raises(ValueError, match="method=>'ode' is not supported"): bngsim_model._normalize_nf_action_method('ode') @@ -450,9 +454,11 @@ def test_normalize_nf_action_method_rejects_non_nf_method(): ('ode', bngsim_model.BNGSIM_BACKEND_NET), ('ssa', bngsim_model.BNGSIM_BACKEND_NET), ('psa', bngsim_model.BNGSIM_BACKEND_NET), - ('nf', bngsim_model.BNGSIM_BACKEND_NF), - ('nf_reject', bngsim_model.BNGSIM_BACKEND_NF), - ('nfsim', bngsim_model.BNGSIM_BACKEND_NF), + # The NF tokens route through bngsim.normalize_method, so they need + # bngsim present; the net/pla/unknown cases short-circuit before that. + pytest.param('nf', bngsim_model.BNGSIM_BACKEND_NF, marks=pytest.mark.bngsim), + pytest.param('nf_reject', bngsim_model.BNGSIM_BACKEND_NF, marks=pytest.mark.bngsim), + pytest.param('nfsim', bngsim_model.BNGSIM_BACKEND_NF, marks=pytest.mark.bngsim), ('pla', None), ('unknown_method', None), ], @@ -461,6 +467,7 @@ def test_classify_action_method_backend_maps_methods(method, expected_backend): assert bngsim_model._classify_action_method_backend(method) == expected_backend +@pytest.mark.bngsim @pytest.mark.parametrize('method', ['rm', 'rulemonkey']) def test_classify_action_method_backend_returns_none_when_rulemonkey_unavailable(monkeypatch, method): monkeypatch.setattr( @@ -483,6 +490,7 @@ def test_classify_actions_for_bngsim_defaults_methodless_simulate_to_net(): ]) == bngsim_model.BNGSIM_BACKEND_NET +@pytest.mark.bngsim def test_classify_actions_for_bngsim_routes_nf_parameter_scan_aliases(): for method in ('nf', 'nf_reject', 'nfsim'): assert bngsim_model.classify_actions_for_bngsim([ @@ -560,6 +568,7 @@ def test_subprocess_env_uses_bng_command_root(monkeypatch): assert env['BioNetGenRoot'] == '/Users/wish/Code/bionetgen/bng2' +@pytest.mark.bngsim def test_initialize_models_uses_bngsim_when_available(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path) output_dir = tmp_path / 'pybnf_output' @@ -675,6 +684,7 @@ def test_initialize_models_no_bngsim_env_disables_hybrid_auto(monkeypatch, tmp_p assert not isinstance(models[0], bngsim_model.BngsimNfModel) +@pytest.mark.bngsim def test_initialize_models_bngsim_backend_rejects_unavailable_bngsim(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path) output_dir = tmp_path / 'pybnf_output' @@ -738,6 +748,7 @@ def test_initialize_models_auto_falls_back_for_unsupported_actions(monkeypatch, assert isinstance(models[0], pset.NetModel) +@pytest.mark.bngsim def test_initialize_models_bngsim_backend_rejects_missing_nfsim(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='nf') output_dir = tmp_path / 'pybnf_output' @@ -772,6 +783,7 @@ def __init__(self, *args, **kwargs): assert isinstance(models[0], pset.NetModel) +@pytest.mark.bngsim def test_initialize_models_bngsim_backend_errors_when_bridge_init_fails(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path) output_dir = tmp_path / 'pybnf_output' @@ -790,6 +802,7 @@ def __init__(self, *args, **kwargs): algorithms.Algorithm._initialize_models(algo) +@pytest.mark.bngsim def test_initialize_models_uses_bngsim_nf_when_supported(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='nf') output_dir = tmp_path / 'pybnf_output' @@ -821,6 +834,7 @@ def __init__(self, name, acts, suffs, mutants, xml_path, **kwargs): assert tuple(models[0].kwargs['param_names']) == model.param_names +@pytest.mark.bngsim def test_initialize_models_uses_bngsim_rulemonkey_when_supported(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='rm') output_dir = tmp_path / 'pybnf_output' @@ -851,6 +865,7 @@ def __init__(self, name, acts, suffs, mutants, xml_path, **kwargs): assert any('method=>"rm"' in action for action in models[0].actions) +@pytest.mark.bngsim def test_initialize_models_nf_xml_generation_stages_relative_tfun_files(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='nf') output_dir = tmp_path / 'pybnf_output' @@ -993,6 +1008,7 @@ def test_evaluate_bngl_params_raises_on_unresolved_name(): ) +@pytest.mark.bngsim def test_bngsim_nf_model_save_preserves_protocol_block(monkeypatch, tmp_path): """Regression for #383: protocol blocks must round-trip through the saved debug .bngl, positioned between the model body and the actions block.""" @@ -1044,6 +1060,7 @@ def test_bngsim_nf_model_save_preserves_protocol_block(monkeypatch, tmp_path): assert proto_end < actions_start +@pytest.mark.bngsim def test_bngsim_nf_model_save_omits_protocol_block_when_absent(monkeypatch, tmp_path): """Models constructed without a protocol must not emit empty protocol blocks.""" monkeypatch.setattr(bngsim_model, 'BNGSIM_AVAILABLE', True) @@ -1277,6 +1294,7 @@ def test_bngsim_nf_model_execute_writes_scan_for_parameter_scan(monkeypatch, tmp assert list(roundtrip.cols.keys())[0] == 'k' +@pytest.mark.bngsim def test_initialize_models_propagates_save_files_when_delete_old_files_zero(monkeypatch, tmp_path): """delete_old_files=0 must reach BngsimNfModel(save_files=True) at construction time.""" model = _make_tfun_bngl_model(tmp_path, method='nf') @@ -1302,6 +1320,7 @@ def __init__(self, *args, **kwargs): assert captured.get('save_files') is True +@pytest.mark.bngsim def test_initialize_models_save_files_defaults_false_when_delete_old_files_positive(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='nf') output_dir = tmp_path / 'pybnf_output' @@ -2236,6 +2255,7 @@ def save_concentrations(self): # Hybrid backend (generate_network + NF simulate) — classification tests # --------------------------------------------------------------------------- +@pytest.mark.bngsim def test_classify_actions_for_bngsim_returns_hybrid_for_gennet_plus_nf(): """When classify sees both generate_network and NF simulate, returns hybrid.""" assert bngsim_model.classify_actions_for_bngsim([ @@ -2244,6 +2264,7 @@ def test_classify_actions_for_bngsim_returns_hybrid_for_gennet_plus_nf(): ]) == bngsim_model.BNGSIM_BACKEND_HYBRID +@pytest.mark.bngsim def test_classify_actions_for_bngsim_returns_hybrid_with_nfsim_alias(): assert bngsim_model.classify_actions_for_bngsim([ 'generate_network({overwrite=>1})', @@ -2251,6 +2272,7 @@ def test_classify_actions_for_bngsim_returns_hybrid_with_nfsim_alias(): ]) == bngsim_model.BNGSIM_BACKEND_HYBRID +@pytest.mark.bngsim def test_classify_actions_for_bngsim_returns_hybrid_with_rulemonkey_alias(monkeypatch): monkeypatch.setattr(bngsim_model, 'BNGSIM_HAS_RULEMONKEY', True) assert bngsim_model.classify_actions_for_bngsim([ @@ -2275,6 +2297,7 @@ def test_classify_actions_for_bngsim_returns_none_for_gennet_plus_pla(): ]) is None +@pytest.mark.bngsim def test_hybrid_detected_via_generates_network_flag(): """BNGLModel strips generate_network from actions; hybrid is detected by the combination of generates_network=True and bridge_backend=NF @@ -2289,6 +2312,7 @@ def test_hybrid_detected_via_generates_network_flag(): # Hybrid backend — _initialize_models tests # --------------------------------------------------------------------------- +@pytest.mark.bngsim def test_initialize_models_hybrid_uses_bngsim_nf(monkeypatch, tmp_path): model = _make_tfun_bngl_model(tmp_path, method='nf', force_generate_network=True) output_dir = tmp_path / 'pybnf_output' @@ -2321,6 +2345,7 @@ def __init__(self, name, acts, suffs, mutants, xml_path, **kwargs): assert 'writeXML()' in _fake_hybrid_generation.last_bngl_text +@pytest.mark.bngsim def test_initialize_models_hybrid_explicit_bngsim_backend_uses_nf_bridge(monkeypatch, tmp_path): """Explicit `bngl_backend = bngsim` for hybrid models must take the same happy path as the auto case (XML generation + BngsimNfModel).""" @@ -2352,6 +2377,7 @@ def __init__(self, name, acts, suffs, mutants, xml_path, **kwargs): assert any('simulate' in a for a in models[0].actions) +@pytest.mark.bngsim def test_initialize_models_hybrid_explicit_bngsim_backend_errors_on_xml_failure(monkeypatch, tmp_path): """Explicit `bngl_backend = bngsim` must raise when hybrid XML generation fails — silent fallback to BNG2.pl is the auto behavior, not the explicit diff --git a/tests/test_constraint_satisfaction.py b/tests/test_constraint_satisfaction.py index 8c1a513e..08c4458e 100644 --- a/tests/test_constraint_satisfaction.py +++ b/tests/test_constraint_satisfaction.py @@ -1,9 +1,8 @@ """Tests for the constraint satisfaction tracking feature (#324).""" -from .context import constraint, algorithms, config, data, pset +from .context import constraint, algorithms, config, data import os import shutil import tempfile -import numpy as np from unittest.mock import patch diff --git a/tests/test_diff_evolution.py b/tests/test_diff_evolution.py index 0b745678..104468ec 100644 --- a/tests/test_diff_evolution.py +++ b/tests/test_diff_evolution.py @@ -1,4 +1,4 @@ -from .context import data, algorithms, pset, objective, config, printing +from .context import data, algorithms, pset, config, printing import shutil import numpy as np diff --git a/tests/test_distributions.py b/tests/test_distributions.py index 024f6668..184c2e56 100644 --- a/tests/test_distributions.py +++ b/tests/test_distributions.py @@ -9,7 +9,6 @@ """ import numpy as np -import pytest from scipy import stats import os diff --git a/tests/test_job_class.py b/tests/test_job_class.py index 5706c396..a050027b 100644 --- a/tests/test_job_class.py +++ b/tests/test_job_class.py @@ -34,12 +34,13 @@ def setup_class(cls): @classmethod def teardown_class(cls): - rmtree('pybnf_output') - rmtree('sim_net') - rmtree('sim_x') - rmtree('sim_1') - rmtree('sim_to') - rmtree('sim_to_rerun1') + # These dirs are created by individual tests in this class, relative to + # the cwd. ignore_errors keeps teardown from raising FileNotFoundError + # when a dir was never created -- e.g. under pytest-xdist, where the + # worker running this class need not have produced every dir, or when + # only a subset of the class is run. + for d in ('pybnf_output', 'sim_net', 'sim_x', 'sim_1', 'sim_to', 'sim_to_rerun1'): + rmtree(d, ignore_errors=True) def test_job_components(self): mkdir('sim_x') diff --git a/tests/test_job_execution.py b/tests/test_job_execution.py index 5269fa39..3fd056f6 100644 --- a/tests/test_job_execution.py +++ b/tests/test_job_execution.py @@ -26,7 +26,6 @@ import os import numpy as np -import pytest from .context import algorithms, data, pset diff --git a/tests/test_model_check.py b/tests/test_model_check.py index f6d53d83..12c188cd 100644 --- a/tests/test_model_check.py +++ b/tests/test_model_check.py @@ -35,7 +35,6 @@ import logging import os -import pytest from .context import algorithms, pset diff --git a/tests/test_model_class.py b/tests/test_model_class.py index 2163d15e..4acea766 100644 --- a/tests/test_model_class.py +++ b/tests/test_model_class.py @@ -195,7 +195,8 @@ def test_has_observables(self): def test_no_observables(self): """Model with empty observables block should have has_observables=False""" - import tempfile, os + import tempfile + import os bngl_content = """begin model begin parameters v1 v1__FREE diff --git a/tests/test_parse_class.py b/tests/test_parse_class.py index c8556a5b..2d459697 100644 --- a/tests/test_parse_class.py +++ b/tests/test_parse_class.py @@ -1,4 +1,3 @@ -import os from .context import parse diff --git a/tests/test_particle_swarm.py b/tests/test_particle_swarm.py index f817f3af..966a19c7 100644 --- a/tests/test_particle_swarm.py +++ b/tests/test_particle_swarm.py @@ -1,7 +1,7 @@ -from .context import data, algorithms, pset, objective, config, parse +from .context import data, algorithms, pset, objective, config import numpy as np import numpy.testing as npt -from os import mkdir, path +from os import path from shutil import rmtree from copy import deepcopy diff --git a/tests/test_run_loop.py b/tests/test_run_loop.py index 960346c9..bebfb189 100644 --- a/tests/test_run_loop.py +++ b/tests/test_run_loop.py @@ -32,7 +32,7 @@ import numpy as np import pytest -from .context import algorithms, config, pset, printing +from .context import algorithms, pset, printing from pybnf.pset import Trajectory, FailedSimulationError diff --git a/tests/test_sbml_model.py b/tests/test_sbml_model.py index ac535c90..e3e9e7f2 100644 --- a/tests/test_sbml_model.py +++ b/tests/test_sbml_model.py @@ -1,6 +1,5 @@ from .context import pset, printing, raises import os -import numpy as np import shutil diff --git a/tests/test_scatter.py b/tests/test_scatter.py index 938a35b5..8237a933 100644 --- a/tests/test_scatter.py +++ b/tests/test_scatter.py @@ -1,4 +1,4 @@ -from .context import data, algorithms, pset, objective, config, parse, printing, raises +from .context import data, algorithms, pset, config, printing, raises from os import mkdir from shutil import rmtree from copy import deepcopy diff --git a/tests/test_seed_determinism.py b/tests/test_seed_determinism.py index c189db62..2bed4104 100644 --- a/tests/test_seed_determinism.py +++ b/tests/test_seed_determinism.py @@ -8,7 +8,7 @@ regardless of the order results are fed to got_result) vs order-dependent. """ -from .context import data, algorithms, pset, config +from .context import data, algorithms, config from unittest.mock import patch import numpy as np import os diff --git a/tests/test_simplex.py b/tests/test_simplex.py index 71d173d2..2d7e8320 100644 --- a/tests/test_simplex.py +++ b/tests/test_simplex.py @@ -1,4 +1,4 @@ -from .context import data, algorithms, pset, objective, config +from .context import data, algorithms, config import numpy as np from copy import deepcopy diff --git a/tests/test_simplex_collinearity.py b/tests/test_simplex_collinearity.py index da55f64d..b54c3618 100644 --- a/tests/test_simplex_collinearity.py +++ b/tests/test_simplex_collinearity.py @@ -1,10 +1,8 @@ """Tests for simplex collinearity fix (#207).""" -from .context import algorithms, config, data, pset -import numpy as np +from .context import algorithms, config, pset import os import shutil import tempfile -from copy import deepcopy from unittest.mock import patch diff --git a/tests/test_subprocess_handling.py b/tests/test_subprocess_handling.py index 41cae36f..1b10a55b 100644 --- a/tests/test_subprocess_handling.py +++ b/tests/test_subprocess_handling.py @@ -12,7 +12,6 @@ import signal import tempfile import time -import shutil from subprocess import run, Popen, STDOUT, PIPE, CalledProcessError, TimeoutExpired import pytest