Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 28 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,69 +61,60 @@ python3 -m pytest

## Usage

`chaste-sbml` has two subcommands:
Generating Chaste C++ code from an SBML file is the default action. Passing
`--copy-base-classes` switches to copying the C++ base classes instead.

```
usage: chaste-sbml [-h] [--version] command ...

positional arguments:
command
generate Generate Chaste C++ code from an SBML file
copy-base-classes Copy the C++ base classes the generated code depends on

options:
-h, --help show this help message and exit
--version show program's version number and exit
```

### `generate`

Generate Chaste C++ code from an SBML file:

```sh
chaste-sbml generate my_model.xml --model-type srn --output-dir src/
```

```
usage: chaste-sbml generate [-h] [--output-dir OUTPUT_DIR]
[--model-type [{generic,srn,cell-cycle}]]
[--tests | --no-tests]
[--test-output-dir TEST_OUTPUT_DIR]
sbml_file
usage: chaste-sbml [-h] [--version] [--copy-base-classes]
[--output-dir OUTPUT_DIR]
[--model-type [{generic,srn,cell-cycle}]]
[--tests | --no-tests] [--timescale {ms,s,m,h}]
[--test-output-dir TEST_OUTPUT_DIR]
[sbml_file]

positional arguments:
sbml_file The SBML file to convert

options:
-h, --help show this help message and exit
--version show program's version number and exit
--copy-base-classes Copy the C++ base classes the generated code depends
on, instead of generating code
--output-dir OUTPUT_DIR
The directory to place output files in
--model-type [{generic,srn,cell-cycle}]
The type of model to generate
--tests, --no-tests Generate placeholder test files (default: on)
--timescale {ms,s,m,h}
The model's native time unit, used to convert
derivatives to Chaste's hours (auto-detected if omitted)
--test-output-dir TEST_OUTPUT_DIR
The directory to place generated test files in
(defaults to --output-dir)
```

By default `generate` also emits a placeholder test (`Test<Model>Sbml.hpp`), a
### Generate code

Generate Chaste C++ code from an SBML file:

```sh
chaste-sbml my_model.xml --model-type srn --output-dir src/
```

By default this also emits a placeholder test (`Test<Model>Sbml.hpp`), a
CxxTest skeleton with a suite for the ODE system, and the SRN/cell-cycle
model where applicable. Pass `--no-tests` to skip it, or `--test-output-dir` to
place the placeholder test somewhere other than `--output-dir`.

### `copy-base-classes`
### Copy the base classes

The generated code `#include`s and subclasses a set of C++ base classes (for example
`AbstractSbmlOdeSystem`). These are shipped with the package; copy them into your project so
they match the installed version of `chaste-sbml`:

```sh
chaste-sbml copy-base-classes --output-dir src/
```

chaste-sbml --copy-base-classes --output-dir src/
```
usage: chaste-sbml copy-base-classes [-h] [--output-dir OUTPUT_DIR]

options:
--output-dir OUTPUT_DIR
The directory to place the base classes in
```
In this mode only `--output-dir` applies; passing an SBML file or a generation option (such as
`--model-type` or `--no-tests`) is a usage error.
81 changes: 53 additions & 28 deletions chaste_sbml/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,94 @@

from ._version import __version__

# Options that apply only when generating, mapped to the default each takes. In copy mode none may
# be supplied (only --output-dir applies), so they use ``argparse.SUPPRESS`` as their default: the
# attribute is absent unless the option was passed, which lets copy mode reject an explicitly-supplied
# option while generation fills in the real default below.
_GENERATION_DEFAULTS = {
"model_type": "generic",
"tests": True,
"timescale": None,
"test_output_dir": None,
}


def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
"""Parse command line arguments.

Generating Chaste C++ code from an SBML file is the default action. Passing ``--copy-base-classes``
switches to copying the C++ base classes the generated code depends on instead; in that mode no
SBML file is taken and the only other option that applies is ``--output-dir``.
"""
parser = argparse.ArgumentParser(
prog="chaste-sbml",
description="Convert SBML models to Chaste C++ code",
)
parser.add_argument("--version", action="version", version="%(prog)s " + __version__)

subparsers = parser.add_subparsers(dest="command", required=True, metavar="command")

# generate: create Chaste C++ code from an SBML file.
generate = subparsers.add_parser(
"generate",
help="Generate Chaste C++ code from an SBML file",
description="Generate Chaste C++ code from an SBML file",
parser.add_argument("sbml_file", nargs="?", help="The SBML file to convert")
parser.add_argument(
"--copy-base-classes",
action="store_true",
help="Copy the C++ base classes the generated code depends on, instead of generating code",
)
generate.add_argument("sbml_file", help="The SBML file to convert")
generate.add_argument("--output-dir", default=None, help="The directory to place output files in")
generate.add_argument(
parser.add_argument("--output-dir", default=None, help="The directory to place output files in")
parser.add_argument(
"--model-type",
help="The type of model to generate",
help="The type of model to generate (default: generic)",
choices=["generic", "srn", "cell-cycle"],
default="generic",
default=argparse.SUPPRESS,
const="generic",
nargs="?",
)
generate.add_argument(
parser.add_argument(
"--tests",
action=argparse.BooleanOptionalAction,
default=True,
default=argparse.SUPPRESS,
help="Generate placeholder test files (default: on). Use --no-tests to disable.",
)
generate.add_argument(
parser.add_argument(
"--timescale",
choices=["ms", "s", "m", "h"],
default=None,
default=argparse.SUPPRESS,
help="The model's native time unit (milliseconds/seconds/minutes/hours), used to convert "
"derivatives to Chaste's hours. Overrides auto-detection from the SBML; omit to auto-detect.",
)
generate.add_argument(
parser.add_argument(
"--test-output-dir",
default=None,
default=argparse.SUPPRESS,
help="The directory to place generated test files in (defaults to --output-dir)",
)

# copy-base-classes: copy the C++ base classes the generated code depends on.
copy_parser = subparsers.add_parser(
"copy-base-classes",
help="Copy the C++ base classes the generated code depends on",
description="Copy the C++ base classes the generated code depends on",
)
copy_parser.add_argument("--output-dir", default=None, help="The directory to place the base classes in")
args = parser.parse_args()

# Options are stored only when supplied (default=SUPPRESS), so a missing attribute means unset.
supplied_generation_opts = [name for name in _GENERATION_DEFAULTS if hasattr(args, name)]

if args.copy_base_classes:
# Copy mode takes only --output-dir; reject the SBML file and any generation-only option
# rather than silently ignoring them.
rejected = ["an SBML file"] if args.sbml_file is not None else []
rejected += [f"--{name.replace('_', '-')}" for name in supplied_generation_opts]
if rejected:
parser.error("--copy-base-classes takes only --output-dir, not: " + ", ".join(rejected))
else:
if args.sbml_file is None:
parser.error("an SBML file is required (or pass --copy-base-classes to copy the base classes)")
# Fill in the real default for every generation option that was not supplied.
for name, default in _GENERATION_DEFAULTS.items():
if not hasattr(args, name):
setattr(args, name, default)

return parser.parse_args()
return args


def process_command_line(args: "argparse.Namespace"):
"""Run the command line interface.

:args: The parsed command line arguments.
"""
if args.command == "copy-base-classes":
if args.copy_base_classes:
copy_base_classes(args.output_dir)
return

Expand Down
43 changes: 35 additions & 8 deletions chaste_sbml/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,30 @@ def test_version_exits_zero(monkeypatch):

@pytest.mark.parametrize("model_type", ["generic", "srn", "cell-cycle"])
def test_generate_each_model_type(monkeypatch, tmp_path, model_type):
"""generate handles every --model-type and emits the model plus placeholder test."""
_run(monkeypatch, "generate", str(GOLDBETER), "--model-type", model_type, "--output-dir", str(tmp_path))
"""Generation (the default action) handles every --model-type and emits the model plus test."""
_run(monkeypatch, str(GOLDBETER), "--model-type", model_type, "--output-dir", str(tmp_path))

assert (tmp_path / "Goldbeter1991SbmlOdeSystem.hpp").is_file()
assert (tmp_path / "TestGoldbeter1991Sbml.hpp").is_file()


def test_generate_no_tests(monkeypatch, tmp_path):
"""generate --no-tests writes the model but no placeholder test."""
_run(monkeypatch, "generate", str(GOLDBETER), "--output-dir", str(tmp_path), "--no-tests")
"""--no-tests writes the model but no placeholder test."""
_run(monkeypatch, str(GOLDBETER), "--output-dir", str(tmp_path), "--no-tests")

assert (tmp_path / "Goldbeter1991SbmlOdeSystem.hpp").is_file()
assert not (tmp_path / "TestGoldbeter1991Sbml.hpp").exists()


def test_generate_test_output_dir(monkeypatch, tmp_path):
"""generate --test-output-dir routes the placeholder test to its own directory."""
"""--test-output-dir routes the placeholder test to its own directory."""
src_dir = tmp_path / "src"
test_dir = tmp_path / "test"
src_dir.mkdir()
test_dir.mkdir()

_run(
monkeypatch,
"generate",
str(GOLDBETER),
"--output-dir",
str(src_dir),
Expand All @@ -65,7 +64,35 @@ def test_generate_test_output_dir(monkeypatch, tmp_path):


def test_copy_base_classes(monkeypatch, tmp_path):
"""copy-base-classes copies the C++ base classes into --output-dir."""
_run(monkeypatch, "copy-base-classes", "--output-dir", str(tmp_path))
"""--copy-base-classes copies the C++ base classes into --output-dir."""
_run(monkeypatch, "--copy-base-classes", "--output-dir", str(tmp_path))

assert (tmp_path / "AbstractSbmlOdeSystem.hpp").is_file()


def test_missing_sbml_file_is_usage_error(monkeypatch):
"""No SBML file and no --copy-base-classes exits with a usage error (code 2)."""
with pytest.raises(SystemExit) as exc:
_run(monkeypatch)

assert exc.value.code == 2


def test_copy_base_classes_with_sbml_file_is_usage_error(monkeypatch):
"""--copy-base-classes takes no SBML file; supplying one exits with a usage error (code 2)."""
with pytest.raises(SystemExit) as exc:
_run(monkeypatch, "--copy-base-classes", str(GOLDBETER))

assert exc.value.code == 2


@pytest.mark.parametrize(
"extra",
[["--model-type", "srn"], ["--no-tests"], ["--timescale", "s"], ["--test-output-dir", "test/"]],
)
def test_copy_base_classes_rejects_generation_options(monkeypatch, extra):
"""A generation-only option in copy mode exits with a usage error (code 2), not silent ignore."""
with pytest.raises(SystemExit) as exc:
_run(monkeypatch, "--copy-base-classes", *extra)

assert exc.value.code == 2
Loading