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

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 

41 

42# Variables ******************************************************************** 

43 

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 

49 

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] 

57 

58# Classes ********************************************************************** 

59 

60# Functions ******************************************************************** 

61 

62def _create_args_parser() -> argparse.ArgumentParser: 

63 # lobster-trace: SwRequirements.sw_req_cli_help 

64 """ Creater parser for command line arguments. 

65 

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) 

72 

73 # lobster-trace: SwRequirements.sw_req_cli_version 

74 parser.add_argument( 

75 "--version", 

76 action="version", 

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

78 ) 

79 

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 ) 

87 

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 ) 

98 

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 ) 

108 

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 ) 

119 

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 ) 

129 

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 ) 

139 

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 ) 

149 

150 return parser 

151 

152def main() -> int: 

153 # lobster-trace: SwRequirements.sw_req_cli 

154 # lobster-trace: SwRequirements.sw_req_destination_format 

155 """Main program entry point. 

156 

157 Returns: 

158 int: Program status 

159 """ 

160 ret_status = Ret.OK 

161 

162 # Create program arguments parser. 

163 args_parser = _create_args_parser() 

164 args_sub_parser = args_parser.add_subparsers(required='True') 

165 

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

167 project_converter = None 

168 

169 try: 

170 project_converter = _get_project_converter() 

171 except ValueError as exc: 

172 log_error(exc) 

173 ret_status = Ret.ERROR 

174 

175 if ret_status == Ret.OK: 

176 

177 project_converter_cmd = None 

178 

179 if project_converter is not None: 

180 project_converter.register(args_sub_parser) 

181 project_converter_cmd = project_converter.get_subcommand() 

182 

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) 

188 

189 args = args_parser.parse_args() 

190 

191 if args is None: 

192 ret_status = Ret.ERROR 

193 

194 else: 

195 enable_verbose(args.verbose) 

196 

197 # In verbose mode print all program arguments. 

198 if is_verbose_enabled() is True: 

199 log_verbose("Program arguments: ") 

200 

201 for arg in vars(args): 

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

203 log_verbose("\n") 

204 

205 # lobster-trace: SwRequirements.sw_req_process_trlc_symbols 

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

207 

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) 

214 

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) 

219 

220 walker = ItemWalker(args, converter) 

221 ret_status = walker.walk_symbols(symbols) 

222 

223 except (FileNotFoundError, OSError) as exc: 

224 log_error(exc) 

225 ret_status = Ret.ERROR 

226 

227 return ret_status 

228 

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. 

233 

234 Returns: 

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

236 """ 

237 project_module_name = None 

238 

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] 

248 

249 if project_module_name is not None: 

250 break 

251 

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

256 

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 

261 

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} 

265 

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 

271 

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

273 

274 return None 

275 

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. 

279 

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 

290 

291# Main ************************************************************************* 

292 

293if __name__ == "__main__": 

294 sys.exit(main())