Coverage for src / pyTRLCConverter / __main__.py: 84%
131 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 12:06 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 12:06 +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 - 2025 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 - 2025 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 """ Creater 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 configuriation 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 """Show program arguments in verbose mode to the user.
200 Args:
201 args (argparse.Namespace): Program arguments
202 """
203 if is_verbose_enabled() is True:
204 log_verbose("Program arguments: ")
206 for arg in vars(args):
207 log_verbose(f"* {arg} = {vars(args)[arg]}")
208 log_verbose("\n")
210def _setup_render_configuration(file_name: Optional[str]) -> Optional[RenderConfig]:
211 """Setup render configuration.
213 Args:
214 file_name (str|None): File name of the render configuration file.
216 Returns:
217 RenderConfig|None: Render configuration or None if render configuration file could not be loaded.
218 """
219 # Load render configuration
220 render_cfg = RenderConfig()
222 if file_name is not None:
223 if render_cfg.load(file_name) is False:
224 render_cfg = None
226 return render_cfg
228def _get_project_converter() -> Optional[AbstractConverter]:
229 # lobster-trace: SwRequirements.sw_req_prj_spec
230 # lobster-trace: SwRequirements.sw_req_prj_spec_file
231 """Get the project specific converter class from a --project or -p argument.
233 Returns:
234 AbstractConverter: The project specific converter or None if not found.
235 """
236 project_module_name = None
238 # Check for project option (-p or --project).
239 arglist = sys.argv[1:]
240 for index, argval in enumerate(arglist):
241 if argval.startswith("-p="):
242 project_module_name = argval[3:]
243 elif argval.startswith("--project="):
244 project_module_name = argval[10:]
245 elif argval in ('-p', '--project') and (index + 1) < len(arglist):
246 project_module_name = arglist[index + 1]
248 if project_module_name is not None:
249 break
251 if project_module_name is not None:
252 # Dynamically load the module and search for an AbstractConverter class definition
253 sys.path.append(os.path.dirname(project_module_name))
254 project_module_name_basename = os.path.basename(project_module_name).replace('.py', '')
256 try:
257 module = importlib.import_module(project_module_name_basename)
258 except ImportError as exc:
259 raise ValueError(f"Failed to import module {project_module_name}: {exc}") from exc
261 #Filter classes that are defined in the module directly.
262 classes = inspect.getmembers(module, inspect.isclass)
263 classes = {name: cls for name, cls in classes if cls.__module__ == project_module_name_basename}
265 # lobster-trace: SwRequirements.sw_req_prj_spec_interface
266 for class_name, class_def in classes.items():
267 if issubclass(class_def, AbstractConverter):
268 log_verbose(f"Found project specific converter type: {class_name}")
269 return class_def
271 raise ValueError(f"No AbstractConverter derived class found in {project_module_name_basename}")
273 return None
275def _create_out_folder(path: str) -> None:
276 # lobster-trace: SwRequirements.sw_req_markdown_out_folder
277 """Create output folder if it doesn't exist.
279 Args:
280 path (str): The output folder path which to create.
281 """
282 if 0 < len(path):
283 if not os.path.exists(path):
284 try:
285 os.makedirs(path)
286 except OSError as e:
287 log_error(f"Failed to create folder {path}: {e}")
288 raise
290def main() -> int:
291 # lobster-trace: SwRequirements.sw_req_cli
292 # lobster-trace: SwRequirements.sw_req_destination_format
293 """Main program entry point.
295 Returns:
296 int: Program status
297 """
298 ret_status = Ret.OK
300 # Create program arguments parser.
301 args_parser = _create_args_parser()
302 args_sub_parser = args_parser.add_subparsers(required=True)
304 ret_status = _setup_converters(args_sub_parser)
306 if ret_status == Ret.OK:
308 args = args_parser.parse_args()
310 if args is None:
311 ret_status = Ret.ERROR
313 else:
314 enable_verbose(args.verbose)
315 _show_program_arguments(args)
317 # lobster-trace: SwRequirements.sw_req_trlc_type_attr_md
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())