Coverage for src/pyTRLCConverter/__main__.py: 84%
131 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-26 12:41 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-26 12:41 +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.reqif_converter import ReqifConverter
42from pyTRLCConverter.render_config import RenderConfig
44# Variables ********************************************************************
46PROG_NAME = "pyTRLCConverter"
47PROG_DESC = "A CLI tool to convert TRLC into different formats."
48PROG_COPYRIGHT = "Copyright (c) 2024 - 2026 NewTec GmbH - " + __license__
49PROG_GITHUB = "Find the project on GitHub: " + __repository__
50PROG_EPILOG = PROG_COPYRIGHT + " - " + PROG_GITHUB
52# List of built-in converters to use or subclass by a project converter.
53BUILD_IN_CONVERTER_LIST = [
54 MarkdownConverter,
55 DocxConverter,
56 DumpConverter,
57 RstConverter,
58 ReqifConverter
59]
61# Classes **********************************************************************
63# Functions ********************************************************************
65def _create_args_parser() -> argparse.ArgumentParser:
66 # lobster-trace: SwRequirements.sw_req_cli_help
67 """ Creates parser for command line arguments.
69 Returns:
70 argparse.ArgumentParser: The parser object for command line arguments.
71 """
72 parser = argparse.ArgumentParser(prog=PROG_NAME,
73 description=PROG_DESC,
74 epilog=PROG_EPILOG)
76 # lobster-trace: SwRequirements.sw_req_cli_version
77 parser.add_argument(
78 "--version",
79 action="version",
80 version="%(prog)s " + __version__
81 )
83 # lobster-trace: SwRequirements.sw_req_cli_verbose
84 parser.add_argument(
85 "-v",
86 "--verbose",
87 action="store_true",
88 help="Print full command details before executing the command." \
89 "Enables logs of type INFO and WARNING."
90 )
92 # lobster-trace: SwRequirements.sw_req_cli_include
93 parser.add_argument(
94 "-i",
95 "--include",
96 type=str,
97 default=None,
98 required=False,
99 action="append",
100 help="Add additional directory which to include on demand. Can be specified several times."
101 )
103 # lobster-trace: SwRequirements.sw_req_cli_source
104 parser.add_argument(
105 "-s",
106 "--source",
107 type=str,
108 required=True,
109 action="append",
110 help="The path to the TRLC files folder or a single TRLC file."
111 )
113 # lobster-trace: SwRequirements.sw_req_cli_exclude
114 parser.add_argument(
115 "-ex",
116 "--exclude",
117 type=str,
118 default=None,
119 required=False,
120 action="append",
121 help="Add source directory which shall not be considered for conversion. Can be specified several times."
122 )
124 # lobster-trace: SwRequirements.sw_req_cli_out
125 parser.add_argument(
126 "-o",
127 "--out",
128 type=str,
129 default="",
130 required=False,
131 help="Output path, e.g. /out/markdown."
132 )
134 # lobster-trace: SwRequirements.sw_req_prj_spec_file
135 parser.add_argument(
136 "-p",
137 "--project",
138 type=str,
139 default=None,
140 required=False,
141 help="Python module with project specific conversion functions."
142 )
144 # lobster-trace: SwRequirements.sw_req_cli_render_cfg
145 parser.add_argument(
146 "-rc",
147 "--renderCfg",
148 type=str,
149 default=None,
150 required=False,
151 help="Render configuration JSON file."
152 )
154 # lobster-trace: SwRequirements.sw_req_cli_translation
155 parser.add_argument(
156 "-tr",
157 "--translation",
158 type=str,
159 default=None,
160 required=False,
161 help="Requirement attribute translation JSON file."
162 )
164 return parser
166def _setup_converters(args_sub_parser: argparse._SubParsersAction) -> Ret:
167 """Setup the converters.
169 Returns:
170 Ret: Status of the setup.
171 """
172 ret_status = Ret.OK
174 # Check if a project specific converter is given and load it.
175 project_converter = None
177 try:
178 project_converter = _get_project_converter()
179 except ValueError as exc:
180 log_error(str(exc))
181 ret_status = Ret.ERROR
183 if ret_status == Ret.OK:
185 project_converter_cmd = None
187 if project_converter is not None:
188 project_converter.register(args_sub_parser)
189 project_converter_cmd = project_converter.get_subcommand()
191 # Load the built-in converters unless a project converter is replacing built-in.
192 # lobster-trace: SwRequirements.sw_req_no_prj_spec
193 for converter in BUILD_IN_CONVERTER_LIST:
194 if converter.get_subcommand() != project_converter_cmd:
195 converter.register(args_sub_parser)
197 return ret_status
199def _show_program_arguments(args: argparse.Namespace) -> None:
200 # lobster-trace: SwRequirements.sw_req_verbose_mode
201 """Show program arguments in verbose mode to the user.
203 Args:
204 args (argparse.Namespace): Program arguments
205 """
206 if is_verbose_enabled() is True:
207 log_verbose("Program arguments: ")
209 for arg in vars(args):
210 log_verbose(f"* {arg} = {vars(args)[arg]}")
211 log_verbose("\n")
213def _setup_render_configuration(file_name: Optional[str]) -> Optional[RenderConfig]:
214 # lobster-trace: SwRequirements.sw_req_render_configuration
215 """Setup render configuration.
217 Args:
218 file_name (str|None): File name of the render configuration file.
220 Returns:
221 RenderConfig|None: Render configuration or None if render configuration file could not be loaded.
222 """
223 # Load render configuration
224 render_cfg = RenderConfig()
226 if file_name is not None:
227 if render_cfg.load(file_name) is False:
228 render_cfg = None
230 return render_cfg
232def _get_project_converter() -> Optional[AbstractConverter]:
233 # lobster-trace: SwRequirements.sw_req_prj_spec
234 # lobster-trace: SwRequirements.sw_req_prj_spec_file
235 """Get the project specific converter class from a --project or -p argument.
237 Returns:
238 AbstractConverter: The project specific converter or None if not found.
239 """
240 project_module_name = None
242 # Check for project option (-p or --project).
243 arglist = sys.argv[1:]
244 for index, argval in enumerate(arglist):
245 if argval.startswith("-p="):
246 project_module_name = argval[3:]
247 elif argval.startswith("--project="):
248 project_module_name = argval[10:]
249 elif argval in ('-p', '--project') and (index + 1) < len(arglist):
250 project_module_name = arglist[index + 1]
252 if project_module_name is not None:
253 break
255 if project_module_name is not None:
256 # Dynamically load the module and search for an AbstractConverter class definition
257 sys.path.append(os.path.dirname(project_module_name))
258 project_module_name_basename = os.path.basename(project_module_name).replace('.py', '')
260 try:
261 module = importlib.import_module(project_module_name_basename)
262 except ImportError as exc:
263 raise ValueError(f"Failed to import module {project_module_name}: {exc}") from exc
265 #Filter classes that are defined in the module directly.
266 classes = inspect.getmembers(module, inspect.isclass)
267 classes = {name: cls for name, cls in classes if cls.__module__ == project_module_name_basename}
269 # lobster-trace: SwRequirements.sw_req_prj_spec_interface
270 for _, class_def in classes.items():
271 if issubclass(class_def, AbstractConverter):
272 return class_def
274 raise ValueError(f"No AbstractConverter derived class found in {project_module_name_basename}")
276 return None
278def _create_out_folder(path: str) -> None:
279 # lobster-trace: SwRequirements.sw_req_markdown_out_folder
280 """Create output folder if it doesn't exist.
282 Args:
283 path (str): The output folder path which to create.
284 """
285 if 0 < len(path):
286 if not os.path.exists(path):
287 try:
288 os.makedirs(path)
289 except OSError as e:
290 log_error(f"Failed to create folder {path}: {e}")
291 raise
293def main() -> int:
294 # lobster-trace: SwRequirements.sw_req_cli
295 # lobster-trace: SwRequirements.sw_req_destination_format
296 """Main program entry point.
298 Returns:
299 int: Program status
300 """
301 ret_status = Ret.OK
303 # Create program arguments parser.
304 args_parser = _create_args_parser()
305 args_sub_parser = args_parser.add_subparsers(required=True)
307 ret_status = _setup_converters(args_sub_parser)
309 if ret_status == Ret.OK:
311 args = args_parser.parse_args()
313 if args is None:
314 ret_status = Ret.ERROR
316 else:
317 enable_verbose(args.verbose)
318 _show_program_arguments(args)
320 render_cfg = _setup_render_configuration(args.renderCfg)
322 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols
323 symbols = get_trlc_symbols(args.source, args.include)
325 if render_cfg is None:
326 log_error(f"Failed to load render configuration file {args.renderCfg}.")
327 ret_status = Ret.ERROR
328 if symbols is None:
329 log_error(f"No items found at {args.source}.")
330 ret_status = Ret.ERROR
331 else:
332 try:
333 _create_out_folder(args.out)
335 # Feed the items into the given converter.
336 log_verbose(
337 f"Using converter {args.converter_class.__name__}: {args.converter_class.get_description()}")
339 converter = args.converter_class(args)
340 converter.set_render_cfg(render_cfg)
342 walker = ItemWalker(args, converter)
343 ret_status = walker.walk_symbols(symbols)
345 except (FileNotFoundError, OSError) as exc:
346 log_error(str(exc))
347 ret_status = Ret.ERROR
349 return ret_status
351# Main *************************************************************************
353if __name__ == "__main__":
354 sys.exit(main())