Coverage for src/coursebook/cli.py: 100%
61 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
1"""Command line interface for `coursebook`.
3The commands are:
5* `coursebook build [folder]` (or just `coursebook [folder]`): creates the
6 files of a project.
7* `coursebook new folder`: creates a new project.
8* `coursebook template style destination`: writes the Word or PowerPoint
9 template of a style so that it can be changed.
11Contents:
12 main: runs a command from the command line.
13 _COMMANDS: the names of the commands.
15"""
17from __future__ import annotations
19import argparse
20import logging
21import pathlib
22import sys
23from typing import TYPE_CHECKING
25from . import __version__, project, scaffold, settings
27if TYPE_CHECKING:
28 from collections.abc import Sequence
30_COMMANDS: tuple[str, ...] = ('build', 'new', 'template')
33""" Public Functions """
36def main(arguments: Sequence[str] | None = None) -> int:
37 """Runs a command from the command line.
39 If the first argument is not a command, the command is 'build'.
41 Args:
42 arguments: the command line arguments, without the program name.
43 Defaults to `None`, which uses `sys.argv`.
45 Returns:
46 The exit status: 0 if the command succeeded and 1 otherwise.
48 """
49 values = list(sys.argv[1:] if arguments is None else arguments)
50 if not values or values[0] not in {*_COMMANDS, '-h', '--help', '--version'}:
51 values.insert(0, 'build')
52 options = _create_parser().parse_args(values)
53 logging.basicConfig(
54 format = '%(levelname)s: %(message)s',
55 level = (
56 logging.INFO if getattr(options, 'verbose', False)
57 else logging.WARNING))
58 try:
59 if options.command == 'new':
60 paths = _new(options)
61 elif options.command == 'template':
62 paths = [scaffold.write_template(
63 options.style, options.destination)]
64 else:
65 paths = _build(options)
66 except (ImportError, OSError, RuntimeError, ValueError) as error:
67 sys.stderr.write(f'coursebook: error: {error}\n')
68 return 1
69 for path in paths:
70 sys.stdout.write(f'Created {path}\n')
71 return 0
74""" Private Functions """
77def _build(options: argparse.Namespace) -> list[pathlib.Path]:
78 """Builds a project from the parsed command line options.
80 Args:
81 options: the parsed options of the 'build' command.
83 Returns:
84 The paths to the created files.
86 """
87 project_ = project.Project.from_folder(options.folder)
88 if options.verbose:
89 for source in project_.unused:
90 logging.info('Source file "%s" is not in the outline', (
91 source.path.name if source.path else source.name))
92 return project_.build(
93 include = options.include,
94 output_folder = options.output_folder)
97def _create_parser() -> argparse.ArgumentParser:
98 """Returns the parser for the command line arguments.
100 Returns:
101 The argument parser.
103 """
104 parser = argparse.ArgumentParser(
105 prog = 'coursebook',
106 description = (
107 'Creates Word, PowerPoint, PDF, and HTML files from a project '
108 'file and markdown source files.'))
109 parser.add_argument(
110 '--version',
111 action = 'version',
112 version = f'%(prog)s {__version__}')
113 commands = parser.add_subparsers(dest = 'command', required = True)
114 build = commands.add_parser(
115 'build',
116 help = 'create the files of a project (the default command)',
117 description = 'Creates the files of a project.')
118 build.add_argument(
119 'folder',
120 nargs = '?',
121 default = '.',
122 help = 'the project folder (defaults to the current folder)')
123 build.add_argument(
124 '-i', '--include',
125 nargs = '+',
126 metavar = 'SECTION',
127 help = (
128 'the sections to create files for, overriding the project file '
129 f'(choices: {", ".join(settings.SECTIONS)})'))
130 build.add_argument(
131 '-o', '--output-folder',
132 help = 'folder for the created files, overriding the project file')
133 build.add_argument(
134 '-v', '--verbose',
135 action = 'store_true',
136 help = 'report source files that are not in the outline')
137 new = commands.add_parser(
138 'new',
139 help = 'create a new project',
140 description = (
141 'Creates a project folder with a project file, a source file for '
142 'each outline entry, and a visuals folder.'))
143 new.add_argument('folder', help = 'the folder for the new project')
144 new.add_argument(
145 '-n', '--name',
146 help = 'the title of the project (defaults to the folder name)')
147 new.add_argument(
148 '-s', '--style',
149 default = 'coursebook',
150 help = 'the style of the project (defaults to coursebook)')
151 new.add_argument(
152 '-f', '--format',
153 choices = scaffold.PROJECT_FILE_FORMATS,
154 default = 'ini',
155 help = 'the format of the project file (defaults to ini)')
156 new.add_argument(
157 '--outline',
158 metavar = 'FILE',
159 help = 'a text file with one outline line (such as "I: Introduction") '
160 'per line')
161 new.add_argument(
162 '-e', '--entry',
163 action = 'append',
164 metavar = 'LINE',
165 help = 'an outline line (repeat for each entry)')
166 template = commands.add_parser(
167 'template',
168 help = "write a style's Word or PowerPoint template",
169 description = (
170 'Writes the Word (.docx) or PowerPoint (.pptx) template of a '
171 'style so that it can be changed and named in a project file.'))
172 template.add_argument('style', help = 'the name of the style')
173 template.add_argument(
174 'destination',
175 help = 'the path for the template, ending in .docx or .pptx')
176 return parser
179def _new(options: argparse.Namespace) -> list[pathlib.Path]:
180 """Creates a project from the parsed command line options.
182 Args:
183 options: the parsed options of the 'new' command.
185 Returns:
186 The paths to the created files and folders.
188 """
189 lines: list[str] = []
190 if options.outline:
191 text = pathlib.Path(options.outline).read_text(encoding = 'utf-8')
192 lines.extend(
193 line for line in text.splitlines()
194 if line.strip() and not line.lstrip().startswith('#'))
195 lines.extend(options.entry or [])
196 return scaffold.create_project(
197 options.folder,
198 name = options.name,
199 style = options.style,
200 outline_lines = lines or None,
201 file_format = options.format)