Coverage for structured_tutorials / cli.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-26 12:41 +0100

1# Copyright (c) 2025 Mathias Ertl 

2# Licensed under the MIT License. See LICENSE file for details. 

3 

4"""Main CLI entrypoint.""" 

5 

6import argparse 

7import sys 

8from collections.abc import Sequence 

9from pathlib import Path 

10 

11import yaml 

12 

13from structured_tutorials import __version__ 

14from structured_tutorials.errors import InvalidAlternativesSelectedError, RunTutorialException 

15from structured_tutorials.models import TutorialModel 

16from structured_tutorials.output import error, setup_logging 

17from structured_tutorials.runners.local import LocalTutorialRunner 

18 

19 

20def main(argv: Sequence[str] | None = None) -> int: 

21 """Main entry function for the command-line.""" 

22 parser = argparse.ArgumentParser() 

23 parser.add_argument("path", type=Path) 

24 parser.add_argument("--version", action="version", version=__version__) 

25 parser.add_argument("-a", "--alternative", dest="alternatives", action="append", default=[]) 

26 parser.add_argument("--no-colors", action="store_true", default=False) 

27 parser.add_argument( 

28 "-n", 

29 "--non-interactive", 

30 dest="interactive", 

31 action="store_false", 

32 default=True, 

33 help="Never prompt for any user input.", 

34 ) 

35 parser.add_argument( 

36 "--log-level", 

37 choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 

38 default="INFO", 

39 help="Override root log level", 

40 ) 

41 parser.add_argument( 

42 "--hide-commands", 

43 dest="show_commands", 

44 action="store_false", 

45 default=True, 

46 help="Do not show commands that are run by the tutorial.", 

47 ) 

48 parser.add_argument( 

49 "--hide-command-output", 

50 dest="show_command_output", 

51 action="store_false", 

52 default=True, 

53 help="Do not show the output of commands that are run on the terminal.", 

54 ) 

55 parser.add_argument( 

56 "-D", "--define", action="append", default=[], nargs=2, help="Define custom variables in context." 

57 ) 

58 args = parser.parse_args(argv) 

59 

60 setup_logging(level=args.log_level, no_colors=args.no_colors, show_commands=args.show_commands) 

61 context = {k: v for k, v in args.define} 

62 

63 try: 

64 tutorial = TutorialModel.from_file(args.path) 

65 except yaml.YAMLError as exc: # an invalid YAML file 

66 error(f"{args.path}: Invalid YAML file:") 

67 print(exc, file=sys.stderr) 

68 return 1 

69 except ValueError as ex: # thrown by Pydantic model loading 

70 error(f"{args.path}: File is not a valid Tutorial:") 

71 print(ex, file=sys.stderr) 

72 return 1 

73 

74 runner = LocalTutorialRunner( 

75 tutorial, 

76 alternatives=tuple(args.alternatives), 

77 show_command_output=args.show_command_output, 

78 interactive=args.interactive, 

79 context=context, 

80 ) 

81 

82 try: 

83 runner.validate_alternatives() 

84 except InvalidAlternativesSelectedError as ex: 

85 error(str(ex)) 

86 return 1 

87 

88 try: 

89 runner.run() 

90 except RunTutorialException: 

91 return 1 # ignored, already handled by cleanup 

92 return 0