Coverage for structured_tutorials / cli.py: 100%
61 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 13:17 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 13:17 +0200
1# Copyright (c) 2025 Mathias Ertl
2# Licensed under the MIT License. See LICENSE file for details.
4"""Main CLI entrypoint."""
6import argparse
7import sys
8from collections.abc import Sequence
9from importlib import import_module
10from pathlib import Path
12import yaml
14from structured_tutorials import __version__
15from structured_tutorials.errors import InvalidAlternativesSelectedError, RunTutorialException
16from structured_tutorials.models import TutorialModel
17from structured_tutorials.models.tutorial import RunnerConfig
18from structured_tutorials.output import error, setup_logging
19from structured_tutorials.runners.base import RunnerBase
22def get_runner(config: RunnerConfig) -> type[RunnerBase]:
23 """Get runner class."""
24 mod_path, cls_name = config.path.rsplit(".", 1)
25 mod = import_module(mod_path)
26 cls = getattr(mod, cls_name)
27 assert issubclass(cls, RunnerBase)
28 return cls # type: ignore[no-any-return]
31def main(argv: Sequence[str] | None = None) -> int:
32 """Main entry function for the command-line."""
33 parser = argparse.ArgumentParser()
34 parser.add_argument("path", type=Path)
35 parser.add_argument("--version", action="version", version=__version__)
36 parser.add_argument("-a", "--alternative", dest="alternatives", action="append", default=[])
37 parser.add_argument("--no-colors", action="store_true", default=False)
38 parser.add_argument(
39 "-n",
40 "--non-interactive",
41 dest="interactive",
42 action="store_false",
43 default=True,
44 help="Never prompt for any user input.",
45 )
46 parser.add_argument(
47 "--log-level",
48 choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
49 default="INFO",
50 help="Override root log level",
51 )
52 parser.add_argument(
53 "--hide-commands",
54 dest="show_commands",
55 action="store_false",
56 default=True,
57 help="Do not show commands that are run by the tutorial.",
58 )
59 parser.add_argument(
60 "--hide-command-output",
61 dest="show_command_output",
62 action="store_false",
63 default=True,
64 help="Do not show the output of commands that are run on the terminal.",
65 )
66 parser.add_argument(
67 "-D", "--define", action="append", default=[], nargs=2, help="Define custom variables in context."
68 )
69 args = parser.parse_args(argv)
71 setup_logging(level=args.log_level, no_colors=args.no_colors, show_commands=args.show_commands)
72 context = {k: v for k, v in args.define}
74 try:
75 tutorial = TutorialModel.from_file(args.path)
76 except yaml.YAMLError as exc: # an invalid YAML file
77 error(f"{args.path}: Invalid YAML file:")
78 print(exc, file=sys.stderr)
79 return 1
80 except ValueError as ex: # thrown by Pydantic model loading
81 error(f"{args.path}: File is not a valid Tutorial:")
82 print(ex, file=sys.stderr)
83 return 1
85 try:
86 runner_cls = get_runner(tutorial.configuration.run.runner)
87 runner = runner_cls(
88 path=args.path,
89 tutorial=tutorial,
90 alternatives=tuple(args.alternatives),
91 show_command_output=args.show_command_output,
92 interactive=args.interactive,
93 context=context,
94 )
95 except Exception as ex:
96 error(str(ex))
97 return 1
99 try:
100 runner.validate_alternatives()
101 except InvalidAlternativesSelectedError as ex:
102 error(str(ex))
103 return 1
105 try:
106 runner.prepare_tutorial()
107 runner.run()
108 except RunTutorialException as ex:
109 error(str(ex))
110 return 1 # ignored, already handled by cleanup
111 finally:
112 runner.cleanup_tutorial()
113 return 0