Source code for bmi_tester.main

from __future__ import annotations

import argparse
import os
import pathlib
import sys
import tempfile
from collections.abc import Iterator
from collections.abc import Sequence
from functools import partial
from typing import Any

from model_metadata._utils import as_cwd
from model_metadata._utils import load_component
from model_metadata._utils import parse_entry_point
from model_metadata.api import query
from model_metadata.api import stage
from model_metadata.errors import BadEntryPointError
from pytest import ExitCode

if sys.version_info >= (3, 12):  # pragma: no cover (PY12+)
    import importlib.resources as importlib_resources
else:  # pragma: no cover (<PY312)
    import importlib_resources

from bmi_tester._version import __version__
from bmi_tester.api import check_bmi

out = partial(print, file=sys.stderr)
err = partial(print, file=sys.stderr)


[docs] def main(argv: tuple[str, ...] | None = None) -> int: """Validate a BMI implementation. \b Examples: Test a BMI for the class *Hydrotrend* in module *pymt_hydrotrend*, $ bmi-test pymt_hydrotrend:Hydrotrend This will test the BMI with a default set of input files as obtained through the model metadata associated with the component. If the component you would like to test does not have model metadata that bmi-tester recognizes, or you would like to test with a non-default set of input files, use the *--root-dir* and *--config-file* options. $ bmi-tests pymt_hydrotrend:Hydrotrend --root-dir=my_files/ --config-file=config.txt where *my_files* is a folder that contains the input files to test with and *config.txt* is the configuration file, which will be passed to the *initialize* method. """ parser = argparse.ArgumentParser(prog="bmi-test") parser.add_argument( "--version", action="version", version=f"bmi-test {__version__}" ) parser.add_argument("entry_point", action=ValidateEntryPoint) parser.add_argument( "--quiet", action="store_true", help=( "Don't emit non-error messages to stderr. Errors are still emitted, " "silence those with 2>/dev/null." ), ) parser.add_argument( "-v", "--verbose", action="store_true", help="Also emit status messages to stderr.", ) parser.add_argument( "--help-pytest", action="store_true", help="Print help about pytest." ) parser.add_argument( "--root-dir", action=ValidatePathExists, help="Define root directory for BMI tests", ) parser.add_argument( "--config-file", action=ValidatePathExists, help="Name of model configuration file", ) parser.add_argument( "--manifest", action=ValidatePathExists, help="Path to manifest file of staged model input files.", ) parser.add_argument( "--bmi-version", default="2.0", help="BMI version to test against" ) args = parser.parse_args(argv) if args.root_dir: if not args.config_file: err("using --root-dir but no config file specified (use --config-file)") return -1 stage = Stage( args.root_dir, config_file=args.config_file, manifest=args.manifest, ) else: stage = Stage.from_entry_point(":".join(args.entry_point)) with as_cwd(stage.dir): status = run_the_tests( ":".join(args.entry_point), stage.config_file, stage.manifest, bmi_version=args.bmi_version, ) if not args.quiet: if status == ExitCode.OK: out("🎉 All tests passed!") else: err("😞 There were errors") return status
[docs] class Stage: def __init__( self, stage_dir: str, config_file: str, manifest: str | Iterator[str] | None = None, ): self._stage_dir = stage_dir self._config_file = config_file if manifest is None: manifest = stage_dir if isinstance(manifest, str): self._manifest = tuple(os.listdir(manifest)) else: self._manifest = tuple(manifest) @property def dir(self) -> str: return self._stage_dir @property def manifest(self) -> tuple[str, ...]: return self._manifest @property def config_file(self) -> str: return self._config_file
[docs] @classmethod def from_entry_point(cls, entry_point: str) -> Stage: module_name, class_name = parse_entry_point(entry_point) try: Bmi = load_component(module_name, class_name) except ImportError: err( f"unable to import BMI implementation, {class_name}," f" from {module_name}" ) raise stage_dir = tempfile.mkdtemp() manifest = stage(Bmi, str(stage_dir)) config_file = query(Bmi, "run.config_file.path") return cls(stage_dir, config_file=config_file, manifest=manifest)
[docs] def run_the_tests( entry_point: str, config_file: str, manifest: tuple[str, ...], bmi_version: str = "2.0", pytest_help: bool = False, ) -> int: path_to_tests = pathlib.Path(str(importlib_resources.files(__name__))).resolve() stages = [ str(p) for p in [path_to_tests / "_bootstrap"] + sorted((path_to_tests / "_tests").glob("stage_*")) ] status = 0 for stage_dir in stages: status = check_bmi( entry_point, tests_dir=stage_dir, input_file=config_file, manifest=manifest, bmi_version=bmi_version, # extra_args=pytest_args + ("-vvv",), help_pytest=pytest_help, ) if status != ExitCode.OK: break return status
[docs] class ValidateEntryPoint(argparse.Action): def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, ) -> None: if not isinstance(values, str): parser.error(f"{values}: invalid entry-point: not a string") entry_point = values try: module_name, class_name = parse_entry_point(entry_point) except BadEntryPointError as error: parser.error(f"{entry_point}: invalid entry-point: {str(error)}") else: setattr(namespace, self.dest, (module_name, class_name))
[docs] class ValidatePathExists(argparse.Action): def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, ) -> None: if not isinstance(values, str): parser.error(f"{values}: invalid path: not a string") path = values # if not os.path.isdir(path): if not os.path.exists(path): parser.error(f"{path}: path does not exist") else: setattr(namespace, self.dest, path)
def _tree(files): tree = [] prefix = ["|--"] * (len(files) - 1) + ["`--"] for p, fname in zip(prefix, files): tree.append(f"{p} {fname}") return os.linesep.join(tree) if __name__ == "__main__": SystemExit(main())