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

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. 

4 

5 Author: Andreas Merkle (andreas.merkle@newtec.de) 

6""" 

7 

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/>. 

23 

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 

43 

44# Variables ******************************************************************** 

45 

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 

51 

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] 

60 

61# Classes ********************************************************************** 

62 

63# Functions ******************************************************************** 

64 

65def _create_args_parser() -> argparse.ArgumentParser: 

66 # lobster-trace: SwRequirements.sw_req_cli_help 

67 """ Creates parser for command line arguments. 

68 

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) 

75 

76 # lobster-trace: SwRequirements.sw_req_cli_version 

77 parser.add_argument( 

78 "--version", 

79 action="version", 

80 version="%(prog)s " + __version__ 

81 ) 

82 

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 ) 

91 

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 ) 

102 

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 ) 

112 

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 ) 

123 

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 ) 

133 

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 ) 

143 

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 ) 

153 

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 ) 

163 

164 return parser 

165 

166def _setup_converters(args_sub_parser: argparse._SubParsersAction) -> Ret: 

167 """Setup the converters. 

168 

169 Returns: 

170 Ret: Status of the setup. 

171 """ 

172 ret_status = Ret.OK 

173 

174 # Check if a project specific converter is given and load it. 

175 project_converter = None 

176 

177 try: 

178 project_converter = _get_project_converter() 

179 except ValueError as exc: 

180 log_error(str(exc)) 

181 ret_status = Ret.ERROR 

182 

183 if ret_status == Ret.OK: 

184 

185 project_converter_cmd = None 

186 

187 if project_converter is not None: 

188 project_converter.register(args_sub_parser) 

189 project_converter_cmd = project_converter.get_subcommand() 

190 

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) 

196 

197 return ret_status 

198 

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. 

202 

203 Args: 

204 args (argparse.Namespace): Program arguments 

205 """ 

206 if is_verbose_enabled() is True: 

207 log_verbose("Program arguments: ") 

208 

209 for arg in vars(args): 

210 log_verbose(f"* {arg} = {vars(args)[arg]}") 

211 log_verbose("\n") 

212 

213def _setup_render_configuration(file_name: Optional[str]) -> Optional[RenderConfig]: 

214 # lobster-trace: SwRequirements.sw_req_render_configuration 

215 """Setup render configuration. 

216 

217 Args: 

218 file_name (str|None): File name of the render configuration file. 

219 

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() 

225 

226 if file_name is not None: 

227 if render_cfg.load(file_name) is False: 

228 render_cfg = None 

229 

230 return render_cfg 

231 

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. 

236 

237 Returns: 

238 AbstractConverter: The project specific converter or None if not found. 

239 """ 

240 project_module_name = None 

241 

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] 

251 

252 if project_module_name is not None: 

253 break 

254 

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', '') 

259 

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 

264 

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} 

268 

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 

273 

274 raise ValueError(f"No AbstractConverter derived class found in {project_module_name_basename}") 

275 

276 return None 

277 

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. 

281 

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 

292 

293def main() -> int: 

294 # lobster-trace: SwRequirements.sw_req_cli 

295 # lobster-trace: SwRequirements.sw_req_destination_format 

296 """Main program entry point. 

297 

298 Returns: 

299 int: Program status 

300 """ 

301 ret_status = Ret.OK 

302 

303 # Create program arguments parser. 

304 args_parser = _create_args_parser() 

305 args_sub_parser = args_parser.add_subparsers(required=True) 

306 

307 ret_status = _setup_converters(args_sub_parser) 

308 

309 if ret_status == Ret.OK: 

310 

311 args = args_parser.parse_args() 

312 

313 if args is None: 

314 ret_status = Ret.ERROR 

315 

316 else: 

317 enable_verbose(args.verbose) 

318 _show_program_arguments(args) 

319 

320 render_cfg = _setup_render_configuration(args.renderCfg) 

321 

322 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols 

323 symbols = get_trlc_symbols(args.source, args.include) 

324 

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) 

334 

335 # Feed the items into the given converter. 

336 log_verbose( 

337 f"Using converter {args.converter_class.__name__}: {args.converter_class.get_description()}") 

338 

339 converter = args.converter_class(args) 

340 converter.set_render_cfg(render_cfg) 

341 

342 walker = ItemWalker(args, converter) 

343 ret_status = walker.walk_symbols(symbols) 

344 

345 except (FileNotFoundError, OSError) as exc: 

346 log_error(str(exc)) 

347 ret_status = Ret.ERROR 

348 

349 return ret_status 

350 

351# Main ************************************************************************* 

352 

353if __name__ == "__main__": 

354 sys.exit(main())