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

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

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.render_config import RenderConfig 

42 

43# Variables ******************************************************************** 

44 

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 

50 

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] 

58 

59# Classes ********************************************************************** 

60 

61# Functions ******************************************************************** 

62 

63def _create_args_parser() -> argparse.ArgumentParser: 

64 # lobster-trace: SwRequirements.sw_req_cli_help 

65 """ Creater parser for command line arguments. 

66 

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) 

73 

74 # lobster-trace: SwRequirements.sw_req_cli_version 

75 parser.add_argument( 

76 "--version", 

77 action="version", 

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

79 ) 

80 

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 ) 

89 

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 ) 

100 

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 ) 

110 

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 ) 

121 

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 ) 

131 

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 ) 

141 

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 ) 

151 

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 ) 

161 

162 return parser 

163 

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

165 """Setup the converters. 

166 

167 Returns: 

168 Ret: Status of the setup. 

169 """ 

170 ret_status = Ret.OK 

171 

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

173 project_converter = None 

174 

175 try: 

176 project_converter = _get_project_converter() 

177 except ValueError as exc: 

178 log_error(str(exc)) 

179 ret_status = Ret.ERROR 

180 

181 if ret_status == Ret.OK: 

182 

183 project_converter_cmd = None 

184 

185 if project_converter is not None: 

186 project_converter.register(args_sub_parser) 

187 project_converter_cmd = project_converter.get_subcommand() 

188 

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) 

194 

195 return ret_status 

196 

197def _show_program_arguments(args: argparse.Namespace) -> None: 

198 """Show program arguments in verbose mode to the user. 

199 

200 Args: 

201 args (argparse.Namespace): Program arguments 

202 """ 

203 if is_verbose_enabled() is True: 

204 log_verbose("Program arguments: ") 

205 

206 for arg in vars(args): 

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

208 log_verbose("\n") 

209 

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

211 """Setup render configuration. 

212 

213 Args: 

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

215 

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

221 

222 if file_name is not None: 

223 if render_cfg.load(file_name) is False: 

224 render_cfg = None 

225 

226 return render_cfg 

227 

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. 

232 

233 Returns: 

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

235 """ 

236 project_module_name = None 

237 

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] 

247 

248 if project_module_name is not None: 

249 break 

250 

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

255 

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 

260 

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} 

264 

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 

270 

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

272 

273 return None 

274 

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. 

278 

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 

289 

290def main() -> int: 

291 # lobster-trace: SwRequirements.sw_req_cli 

292 # lobster-trace: SwRequirements.sw_req_destination_format 

293 """Main program entry point. 

294 

295 Returns: 

296 int: Program status 

297 """ 

298 ret_status = Ret.OK 

299 

300 # Create program arguments parser. 

301 args_parser = _create_args_parser() 

302 args_sub_parser = args_parser.add_subparsers(required=True) 

303 

304 ret_status = _setup_converters(args_sub_parser) 

305 

306 if ret_status == Ret.OK: 

307 

308 args = args_parser.parse_args() 

309 

310 if args is None: 

311 ret_status = Ret.ERROR 

312 

313 else: 

314 enable_verbose(args.verbose) 

315 _show_program_arguments(args) 

316 

317 # lobster-trace: SwRequirements.sw_req_trlc_type_attr_md 

318 render_cfg = _setup_render_configuration(args.renderCfg) 

319 

320 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols 

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

322 

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) 

332 

333 # Feed the items into the given converter. 

334 log_verbose( 

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

336 

337 converter = args.converter_class(args) 

338 converter.set_render_cfg(render_cfg) 

339 

340 walker = ItemWalker(args, converter) 

341 ret_status = walker.walk_symbols(symbols) 

342 

343 except (FileNotFoundError, OSError) as exc: 

344 log_error(str(exc)) 

345 ret_status = Ret.ERROR 

346 

347 return ret_status 

348 

349# Main ************************************************************************* 

350 

351if __name__ == "__main__": 

352 sys.exit(main())