Coverage for src/pyTRLCConverter/__main__.py: 84%
111 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 10:59 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 10:59 +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
42# Variables ********************************************************************
44PROG_NAME = "pyTRLCConverter"
45PROG_DESC = "A CLI tool to convert TRLC into different formats."
46PROG_COPYRIGHT = "Copyright (c) 2024 - 2025 NewTec GmbH - " + __license__
47PROG_GITHUB = "Find the project on GitHub: " + __repository__
48PROG_EPILOG = PROG_COPYRIGHT + " - " + PROG_GITHUB
50# List of built-in converters to use or subclass by a project converter.
51BUILD_IN_CONVERTER_LIST = [
52 MarkdownConverter,
53 DocxConverter,
54 DumpConverter,
55 RstConverter
56]
58# Classes **********************************************************************
60# Functions ********************************************************************
62def _create_args_parser() -> argparse.ArgumentParser:
63 # lobster-trace: SwRequirements.sw_req_cli_help
64 """ Creater parser for command line arguments.
66 Returns:
67 argparse.ArgumentParser: The parser object for command line arguments.
68 """
69 parser = argparse.ArgumentParser(prog=PROG_NAME,
70 description=PROG_DESC,
71 epilog=PROG_EPILOG)
73 # lobster-trace: SwRequirements.sw_req_cli_version
74 parser.add_argument(
75 "--version",
76 action="version",
77 version="%(prog)s " + __version__
78 )
80 parser.add_argument(
81 "-v",
82 "--verbose",
83 action="store_true",
84 help="Print full command details before executing the command." \
85 "Enables logs of type INFO and WARNING."
86 )
88 # lobster-trace: SwRequirements.sw_req_cli_include
89 parser.add_argument(
90 "-i",
91 "--include",
92 type=str,
93 default=None,
94 required=False,
95 action="append",
96 help="Add additional directory which to include on demand. Can be specified several times."
97 )
99 # lobster-trace: SwRequirements.sw_req_cli_source
100 parser.add_argument(
101 "-s",
102 "--source",
103 type=str,
104 required=True,
105 action="append",
106 help="The path to the TRLC files folder or a single TRLC file."
107 )
109 # lobster-trace: SwRequirements.sw_req_cli_exclude
110 parser.add_argument(
111 "-ex",
112 "--exclude",
113 type=str,
114 default=None,
115 required=False,
116 action="append",
117 help="Add source directory which shall not be considered for conversion. Can be specified several times."
118 )
120 # lobster-trace: SwRequirements.sw_req_cli_out
121 parser.add_argument(
122 "-o",
123 "--out",
124 type=str,
125 default="",
126 required=False,
127 help="Output path, e.g. /out/markdown."
128 )
130 # lobster-trace: SwRequirements.sw_req_prj_spec_file
131 parser.add_argument(
132 "-p",
133 "--project",
134 type=str,
135 default=None,
136 required=False,
137 help="Python module with project specific conversion functions."
138 )
140 # lobster-trace: SwRequirements.sw_req_cli_translation
141 parser.add_argument(
142 "-tr",
143 "--translation",
144 type=str,
145 default=None,
146 required=False,
147 help="Requirement attribute translation JSON file."
148 )
150 return parser
152def main() -> int:
153 # lobster-trace: SwRequirements.sw_req_cli
154 # lobster-trace: SwRequirements.sw_req_destination_format
155 """Main program entry point.
157 Returns:
158 int: Program status
159 """
160 ret_status = Ret.OK
162 # Create program arguments parser.
163 args_parser = _create_args_parser()
164 args_sub_parser = args_parser.add_subparsers(required='True')
166 # Check if a project specific converter is given and load it.
167 project_converter = None
169 try:
170 project_converter = _get_project_converter()
171 except ValueError as exc:
172 log_error(exc)
173 ret_status = Ret.ERROR
175 if ret_status == Ret.OK:
177 project_converter_cmd = None
179 if project_converter is not None:
180 project_converter.register(args_sub_parser)
181 project_converter_cmd = project_converter.get_subcommand()
183 # Load the built-in converters unless a project converter is replacing built-in.
184 # lobster-trace: SwRequirements.sw_req_no_prj_spec
185 for converter in BUILD_IN_CONVERTER_LIST:
186 if converter.get_subcommand() != project_converter_cmd:
187 converter.register(args_sub_parser)
189 args = args_parser.parse_args()
191 if args is None:
192 ret_status = Ret.ERROR
194 else:
195 enable_verbose(args.verbose)
197 # In verbose mode print all program arguments.
198 if is_verbose_enabled() is True:
199 log_verbose("Program arguments: ")
201 for arg in vars(args):
202 log_verbose(f"* {arg} = {vars(args)[arg]}")
203 log_verbose("\n")
205 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols
206 symbols = get_trlc_symbols(args.source, args.include)
208 if symbols is None:
209 log_error(f"No items found at {args.source}.")
210 ret_status = Ret.ERROR
211 else:
212 try:
213 _create_out_folder(args.out)
215 # Feed the items into the given converter.
216 log_verbose(
217 f"Using converter {args.converter_class.__name__}: {args.converter_class.get_description()}")
218 converter = args.converter_class(args)
220 walker = ItemWalker(args, converter)
221 ret_status = walker.walk_symbols(symbols)
223 except (FileNotFoundError, OSError) as exc:
224 log_error(exc)
225 ret_status = Ret.ERROR
227 return ret_status
229def _get_project_converter() -> Optional[AbstractConverter]:
230 # lobster-trace: SwRequirements.sw_req_prj_spec
231 # lobster-trace: SwRequirements.sw_req_prj_spec_file
232 """Get the project specific converter class from a --project or -p argument.
234 Returns:
235 AbstractConverter: The project specific converter or None if not found.
236 """
237 project_module_name = None
239 # Check for project option (-p or --project).
240 arglist = sys.argv[1:]
241 for index, argval in enumerate(arglist):
242 if argval.startswith("-p="):
243 project_module_name = argval[3:]
244 elif argval.startswith("--project="):
245 project_module_name = argval[10:]
246 elif argval in ('-p', '--project') and (index + 1) < len(arglist):
247 project_module_name = arglist[index + 1]
249 if project_module_name is not None:
250 break
252 if project_module_name is not None:
253 # Dynamically load the module and search for an AbstractConverter class definition
254 sys.path.append(os.path.dirname(project_module_name))
255 project_module_name_basename = os.path.basename(project_module_name).replace('.py', '')
257 try:
258 module = importlib.import_module(project_module_name_basename)
259 except ImportError as exc:
260 raise ValueError(f"Failed to import module {project_module_name}: {exc}") from exc
262 #Filter classes that are defined in the module directly.
263 classes = inspect.getmembers(module, inspect.isclass)
264 classes = {name: cls for name, cls in classes if cls.__module__ == project_module_name_basename}
266 # lobster-trace: SwRequirements.sw_req_prj_spec_interface
267 for class_name, class_def in classes.items():
268 if issubclass(class_def, AbstractConverter):
269 log_verbose(f"Found project specific converter type: {class_name}")
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
291# Main *************************************************************************
293if __name__ == "__main__":
294 sys.exit(main())