Coverage for src/pyTRLCConverter/docx_converter.py: 95%

64 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 10:59 +0000

1"""Converter to Word docx format. 

2 

3 Author: Norbert Schulz (norbert.schulz@newtec.de) 

4""" 

5 

6# pyTRLCConverter - A tool to convert TRLC files to specific formats. 

7# Copyright (c) 2024 - 2025 NewTec GmbH 

8# 

9# This file is part of pyTRLCConverter program. 

10# 

11# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software Foundation, 

13# either version 3 of the License, or (at your option) any later version. 

14# 

15# The pyTRLCConverter program is distributed in the hope that it will be useful, but 

16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

17# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License along with pyTRLCConverter. 

20# If not, see <https://www.gnu.org/licenses/>. 

21 

22# Imports ********************************************************************** 

23import os 

24from typing import Optional 

25import docx 

26from pyTRLCConverter.base_converter import BaseConverter 

27from pyTRLCConverter.logger import log_verbose 

28from pyTRLCConverter.ret import Ret 

29from pyTRLCConverter.trlc_helper import Record_Object 

30 

31# Variables ******************************************************************** 

32 

33# Classes ********************************************************************** 

34 

35class DocxConverter(BaseConverter): 

36 """ 

37 Converter to docx format. 

38 """ 

39 

40 OUTPUT_FILE_NAME_DEFAULT = "output.docx" 

41 

42 def __init__(self, args: any) -> None: 

43 # lobster-trace: SwRequirements.sw_req_no_prj_spec 

44 # lobster-trace: SwRequirements.sw_req_docx 

45 # lobster-trace: SwRequirements.sw_req_docx_template 

46 """ 

47 Initialize the docx converter. 

48 

49 Args: 

50 args (any): The parsed program arguments. 

51 """ 

52 super().__init__(args) 

53 

54 if args.template is not None: 

55 log_verbose(f"Loading template file {args.template}.") 

56 

57 self._docx = docx.Document(docx=args.template) 

58 

59 # Ensure default table style is present in the document. 

60 if not 'Table Grid' in self._docx.styles: 

61 self._docx.styles.add_style('Table Grid', docx.enum.style.WD_STYLE_TYPE.TABLE, builtin=True) 

62 

63 @staticmethod 

64 def get_subcommand() -> str: 

65 # lobster-trace: SwRequirements.sw_req_docx 

66 """ Return subcommand token for this converter. 

67 

68 Returns: 

69 Ret: Status 

70 """ 

71 return "docx" 

72 

73 @staticmethod 

74 def get_description() -> str: 

75 # lobster-trace: SwRequirements.sw_req_docx 

76 """ Return converter description. 

77  

78 Returns: 

79 Ret: Status 

80 """ 

81 return "Convert into docx format." 

82 

83 @classmethod 

84 def register(cls, args_parser: any) -> None: 

85 # lobster-trace: SwRequirements.sw_req_docx 

86 """Register converter specific argument parser. 

87 

88 Args: 

89 args_parser (any): Argument parser 

90 """ 

91 super().register(args_parser) 

92 

93 BaseConverter._parser.add_argument( 

94 "-t", 

95 "--template", 

96 type=str, 

97 default=None, 

98 required=False, 

99 help="Load the given docx file as a template to append to." 

100 ) 

101 BaseConverter._parser.add_argument( 

102 "-n", 

103 "--name", 

104 type=str, 

105 default=DocxConverter.OUTPUT_FILE_NAME_DEFAULT, 

106 required=False, 

107 help="Name of the generated output file inside the output folder " \ 

108 f"(default = {DocxConverter.OUTPUT_FILE_NAME_DEFAULT})." 

109 ) 

110 

111 def convert_section(self, section: str, level: int) -> Ret: 

112 # lobster-trace: SwRequirements.sw_req_docx_section 

113 """Process the given section item. 

114 

115 Args: 

116 section (str): The section name 

117 level (int): The section indentation level 

118 

119 Returns: 

120 Ret: Status 

121 """ 

122 self._docx.add_heading(section, level) 

123 

124 return Ret.OK 

125 

126 def convert_record_object_generic(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret: 

127 # lobster-trace: SwRequirements.sw_req_docx_record 

128 """ 

129 Process the given record object in a generic way. 

130 

131 The handler is called by the base converter if no specific handler is 

132 defined for the record type. 

133 

134 Args: 

135 record (Record_Object): The record object. 

136 level (int): The record level. 

137 translation (Optional[dict]): Translation dictionary for the record object. 

138 If None, no translation is applied. 

139 

140  

141 Returns: 

142 Ret: Status 

143 """ 

144 return self._convert_record_object(record, level, translation) 

145 

146 def finish(self) -> Ret: 

147 # lobster-trace: SwRequirements.sw_req_docx_file 

148 """Finish the conversion. 

149 

150 Returns: 

151 Ret: Status 

152 """ 

153 result = Ret.ERROR 

154 

155 if self._docx is not None: 

156 output_file_name = self._args.name 

157 if 0 < len(self._args.out): 

158 output_file_name = os.path.join(self._args.out, self._args.name) 

159 

160 log_verbose(f"Writing docx {output_file_name}.") 

161 self._docx.save(output_file_name) 

162 self._docx = None 

163 result = Ret.OK 

164 

165 return result 

166 

167 def _convert_record_object(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret: 

168 # lobster-trace: SwRequirements.sw_req_docx_record 

169 """ 

170 Process the given record object. 

171 

172 Args: 

173 record (Record_Object): The record object. 

174 level (int): The record level. 

175 translation (Optional[dict]): Translation dictionary for the record object. 

176 If None, no translation is applied. 

177 

178 Returns: 

179 Ret: Status 

180 """ 

181 self._docx.add_heading(f"{record.name} ({record.n_typ.name})", level + 1) 

182 attributes = record.to_python_dict() 

183 

184 table = self._docx.add_table(rows=1, cols=2) 

185 table.style = 'Table Grid' 

186 table.autofit = True 

187 

188 # Set table headers 

189 header_cells = table.rows[0].cells 

190 header_cells[0].text = "Element" 

191 header_cells[1].text = "Value" 

192 

193 # Populate table with attribute key-value pairs 

194 for key, value in attributes.items(): 

195 if value is None: 

196 value = self._empty_attribute_value 

197 

198 if translation is not None: 

199 if key in translation: 

200 key = translation[key] 

201 

202 cells = table.add_row().cells 

203 cells[0].text = key 

204 cells[1].text = str(value) 

205 

206 # Add a paragraph with the record object location 

207 p = self._docx.add_paragraph() 

208 p.add_run(f"from {record.location.file_name}:{record.location.line_no}").italic = True 

209 

210 return Ret.OK 

211 

212 

213# Functions ******************************************************************** 

214 

215# Main *************************************************************************