Coverage for src / pyTRLCConverter / __main__.py: 84%
130 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 12:20 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 12:20 +0000
1"""The main module with the program entry point.
2 The main task is to convert requirements, diagrams and etc. which are defined
3 by TRLC into markdown format.
5 Author: Andreas Merkle (andreas.merkle@newtec.de)
6"""
8# pyTRLCConverter - A tool to convert TRLC files to specific formats.
9# Copyright (c) 2024 - 2026 NewTec GmbH
10#
11# This file is part of pyTRLCConverter program.
12#
13# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under
14# the terms of the GNU General Public License as published by the Free Software Foundation,
15# either version 3 of the License, or (at your option) any later version.
16#
17# The pyTRLCConverter program is distributed in the hope that it will be useful, but
18# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along with pyTRLCConverter.
22# If not, see <https://www.gnu.org/licenses/>.
24# Imports **********************************************************************
25import importlib
26import inspect
27import os
28import sys
29import argparse
30from typing import Optional
31from pyTRLCConverter.abstract_converter import AbstractConverter
32from pyTRLCConverter.dump_converter import DumpConverter
33from pyTRLCConverter.item_walker import ItemWalker
34from pyTRLCConverter.ret import Ret
35from pyTRLCConverter.version import __license__, __repository__, __version__
36from pyTRLCConverter.trlc_helper import get_trlc_symbols
37from pyTRLCConverter.markdown_converter import MarkdownConverter
38from pyTRLCConverter.docx_converter import DocxConverter
39from pyTRLCConverter.logger import enable_verbose, log_verbose, is_verbose_enabled, log_error
40from pyTRLCConverter.rst_converter import RstConverter
41from pyTRLCConverter.render_config import RenderConfig
43# Variables ********************************************************************
45PROG_NAME = "pyTRLCConverter"
46PROG_DESC = "A CLI tool to convert TRLC into different formats."
47PROG_COPYRIGHT = "Copyright (c) 2024 - 2026 NewTec GmbH - " + __license__
48PROG_GITHUB = "Find the project on GitHub: " + __repository__
49PROG_EPILOG = PROG_COPYRIGHT + " - " + PROG_GITHUB
51# List of built-in converters to use or subclass by a project converter.
52BUILD_IN_CONVERTER_LIST = [
53 MarkdownConverter,
54 DocxConverter,
55 DumpConverter,
56 RstConverter
57]
59# Classes **********************************************************************
61# Functions ********************************************************************
63def _create_args_parser() -> argparse.ArgumentParser:
64 # lobster-trace: SwRequirements.sw_req_cli_help
65 """ Creates parser for command line arguments.
67 Returns:
68 argparse.ArgumentParser: The parser object for command line arguments.
69 """
70 parser = argparse.ArgumentParser(prog=PROG_NAME,
71 description=PROG_DESC,
72 epilog=PROG_EPILOG)
74 # lobster-trace: SwRequirements.sw_req_cli_version
75 parser.add_argument(
76 "--version",
77 action="version",
78 version="%(prog)s " + __version__
79 )
81 # lobster-trace: SwRequirements.sw_req_cli_verbose
82 parser.add_argument(
83 "-v",
84 "--verbose",
85 action="store_true",
86 help="Print full command details before executing the command." \
87 "Enables logs of type INFO and WARNING."
88 )
90 # lobster-trace: SwRequirements.sw_req_cli_include
91 parser.add_argument(
92 "-i",
93 "--include",
94 type=str,
95 default=None,
96 required=False,
97 action="append",
98 help="Add additional directory which to include on demand. Can be specified several times."
99 )
101 # lobster-trace: SwRequirements.sw_req_cli_source
102 parser.add_argument(
103 "-s",
104 "--source",
105 type=str,
106 required=True,
107 action="append",
108 help="The path to the TRLC files folder or a single TRLC file."
109 )
111 # lobster-trace: SwRequirements.sw_req_cli_exclude
112 parser.add_argument(
113 "-ex",
114 "--exclude",
115 type=str,
116 default=None,
117 required=False,
118 action="append",
119 help="Add source directory which shall not be considered for conversion. Can be specified several times."
120 )
122 # lobster-trace: SwRequirements.sw_req_cli_out
123 parser.add_argument(
124 "-o",
125 "--out",
126 type=str,
127 default="",
128 required=False,
129 help="Output path, e.g. /out/markdown."
130 )
132 # lobster-trace: SwRequirements.sw_req_prj_spec_file
133 parser.add_argument(
134 "-p",
135 "--project",
136 type=str,
137 default=None,
138 required=False,
139 help="Python module with project specific conversion functions."
140 )
142 # lobster-trace: SwRequirements.sw_req_cli_render_cfg
143 parser.add_argument(
144 "-rc",
145 "--renderCfg",
146 type=str,
147 default=None,
148 required=False,
149 help="Render configuration JSON file."
150 )
152 # lobster-trace: SwRequirements.sw_req_cli_translation
153 parser.add_argument(
154 "-tr",
155 "--translation",
156 type=str,
157 default=None,
158 required=False,
159 help="Requirement attribute translation JSON file."
160 )
162 return parser
164def _setup_converters(args_sub_parser: argparse._SubParsersAction) -> Ret:
165 """Setup the converters.
167 Returns:
168 Ret: Status of the setup.
169 """
170 ret_status = Ret.OK
172 # Check if a project specific converter is given and load it.
173 project_converter = None
175 try:
176 project_converter = _get_project_converter()
177 except ValueError as exc:
178 log_error(str(exc))
179 ret_status = Ret.ERROR
181 if ret_status == Ret.OK:
183 project_converter_cmd = None
185 if project_converter is not None:
186 project_converter.register(args_sub_parser)
187 project_converter_cmd = project_converter.get_subcommand()
189 # Load the built-in converters unless a project converter is replacing built-in.
190 # lobster-trace: SwRequirements.sw_req_no_prj_spec
191 for converter in BUILD_IN_CONVERTER_LIST:
192 if converter.get_subcommand() != project_converter_cmd:
193 converter.register(args_sub_parser)
195 return ret_status
197def _show_program_arguments(args: argparse.Namespace) -> None:
198 # lobster-trace: SwRequirements.sw_req_verbose_mode
199 """Show program arguments in verbose mode to the user.
201 Args:
202 args (argparse.Namespace): Program arguments
203 """
204 if is_verbose_enabled() is True:
205 log_verbose("Program arguments: ")
207 for arg in vars(args):
208 log_verbose(f"* {arg} = {vars(args)[arg]}")
209 log_verbose("\n")
211def _setup_render_configuration(file_name: Optional[str]) -> Optional[RenderConfig]:
212 # lobster-trace: SwRequirements.sw_req_render_configuration
213 """Setup render configuration.
215 Args:
216 file_name (str|None): File name of the render configuration file.
218 Returns:
219 RenderConfig|None: Render configuration or None if render configuration file could not be loaded.
220 """
221 # Load render configuration
222 render_cfg = RenderConfig()
224 if file_name is not None:
225 if render_cfg.load(file_name) is False:
226 render_cfg = None
228 return render_cfg
230def _get_project_converter() -> Optional[AbstractConverter]:
231 # lobster-trace: SwRequirements.sw_req_prj_spec
232 # lobster-trace: SwRequirements.sw_req_prj_spec_file
233 """Get the project specific converter class from a --project or -p argument.
235 Returns:
236 AbstractConverter: The project specific converter or None if not found.
237 """
238 project_module_name = None
240 # Check for project option (-p or --project).
241 arglist = sys.argv[1:]
242 for index, argval in enumerate(arglist):
243 if argval.startswith("-p="):
244 project_module_name = argval[3:]
245 elif argval.startswith("--project="):
246 project_module_name = argval[10:]
247 elif argval in ('-p', '--project') and (index + 1) < len(arglist):
248 project_module_name = arglist[index + 1]
250 if project_module_name is not None:
251 break
253 if project_module_name is not None:
254 # Dynamically load the module and search for an AbstractConverter class definition
255 sys.path.append(os.path.dirname(project_module_name))
256 project_module_name_basename = os.path.basename(project_module_name).replace('.py', '')
258 try:
259 module = importlib.import_module(project_module_name_basename)
260 except ImportError as exc:
261 raise ValueError(f"Failed to import module {project_module_name}: {exc}") from exc
263 #Filter classes that are defined in the module directly.
264 classes = inspect.getmembers(module, inspect.isclass)
265 classes = {name: cls for name, cls in classes if cls.__module__ == project_module_name_basename}
267 # lobster-trace: SwRequirements.sw_req_prj_spec_interface
268 for _, class_def in classes.items():
269 if issubclass(class_def, AbstractConverter):
270 return class_def
272 raise ValueError(f"No AbstractConverter derived class found in {project_module_name_basename}")
274 return None
276def _create_out_folder(path: str) -> None:
277 # lobster-trace: SwRequirements.sw_req_markdown_out_folder
278 """Create output folder if it doesn't exist.
280 Args:
281 path (str): The output folder path which to create.
282 """
283 if 0 < len(path):
284 if not os.path.exists(path):
285 try:
286 os.makedirs(path)
287 except OSError as e:
288 log_error(f"Failed to create folder {path}: {e}")
289 raise
291def main() -> int:
292 # lobster-trace: SwRequirements.sw_req_cli
293 # lobster-trace: SwRequirements.sw_req_destination_format
294 """Main program entry point.
296 Returns:
297 int: Program status
298 """
299 ret_status = Ret.OK
301 # Create program arguments parser.
302 args_parser = _create_args_parser()
303 args_sub_parser = args_parser.add_subparsers(required=True)
305 ret_status = _setup_converters(args_sub_parser)
307 if ret_status == Ret.OK:
309 args = args_parser.parse_args()
311 if args is None:
312 ret_status = Ret.ERROR
314 else:
315 enable_verbose(args.verbose)
316 _show_program_arguments(args)
318 render_cfg = _setup_render_configuration(args.renderCfg)
320 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols
321 symbols = get_trlc_symbols(args.source, args.include)
323 if render_cfg is None:
324 log_error(f"Failed to load render configuration file {args.renderCfg}.")
325 ret_status = Ret.ERROR
326 if symbols is None:
327 log_error(f"No items found at {args.source}.")
328 ret_status = Ret.ERROR
329 else:
330 try:
331 _create_out_folder(args.out)
333 # Feed the items into the given converter.
334 log_verbose(
335 f"Using converter {args.converter_class.__name__}: {args.converter_class.get_description()}")
337 converter = args.converter_class(args)
338 converter.set_render_cfg(render_cfg)
340 walker = ItemWalker(args, converter)
341 ret_status = walker.walk_symbols(symbols)
343 except (FileNotFoundError, OSError) as exc:
344 log_error(str(exc))
345 ret_status = Ret.ERROR
347 return ret_status
349# Main *************************************************************************
351if __name__ == "__main__":
352 sys.exit(main())