Coverage for packages/pyswig/src/pyswig/processsrcfile.py: 91%

282 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-26 21:05 +0000

1# Copyright (c) 2015-2020 Michel Gillet 

2# SPDX-License-Identifier: MIT 

3 

4from __future__ import annotations 

5 

6from enum import IntEnum 

7from pathlib import Path 

8from typing import TYPE_CHECKING, TextIO 

9 

10from pyswig.exceptions import PySwigError 

11 

12if TYPE_CHECKING: 

13 from pyswig.fileconfig import FileConfig 

14 

15 

16class ParseState(IntEnum): 

17 NORMAL = 0 

18 SWIG_OUT = 1 

19 SWIG = 2 

20 TAGS = 3 

21 

22 

23class ProcessSrcFile: 

24 """Process one FileConfig""" 

25 

26 def __init__( 

27 self, 

28 name: str, 

29 verbose: bool = False, 

30 file_config: FileConfig | None = None, 

31 wrap_filename: bool = False, 

32 ) -> None: 

33 """Constructor""" 

34 self.m_name = name 

35 self.m_state = ParseState.NORMAL 

36 self.m_line_nbr = 0 

37 self.m_file_config = file_config 

38 self.m_file_in: str | None = None 

39 self.m_fin: TextIO | None = None 

40 self.m_fout: TextIO | None = None 

41 self.m_verbose = verbose 

42 self.m_found_tags = False 

43 self.m_tags: list[str] = [] 

44 self.m_line: str | None = None 

45 self.m_cmd_i = -1 

46 self.m_cmdname: str | None = None 

47 self.m_pyswig_version: str | None = None 

48 self.m_wrap_filename = wrap_filename 

49 

50 def get_wrap_filename(self) -> bool: 

51 return self.m_wrap_filename 

52 

53 def set_pyswig_version(self, pyswig_version: str) -> None: 

54 self.m_pyswig_version = pyswig_version 

55 

56 def get_pyswig_version(self) -> str | None: 

57 return self.m_pyswig_version 

58 

59 def add_include(self) -> None: 

60 """Add a C/C++ include file at the place of <swig_inc/> annotation""" 

61 assert self.m_line is not None 

62 assert self.m_fout is not None 

63 assert self.m_file_config is not None 

64 base_inc_dir = self.m_file_config.get_base_inc_dir() 

65 assert base_inc_dir is not None 

66 line = self.m_line.replace("//<swig_inc/>", "//<swig_inc>") 

67 self.m_line = self.m_line.replace("//<swig_inc/>", "//</swig_inc>") 

68 self.m_fout.write(line) 

69 self.m_fout.write("%{\n") 

70 self.m_fout.write(f'#include "{base_inc_dir}/{self.m_name}"\n') 

71 self.m_fout.write("%}\n") 

72 

73 def write(self, text: str) -> None: 

74 """Write some text to the ouput file""" 

75 assert self.m_fout is not None 

76 self.m_fout.write(text) 

77 

78 def _resolve_input_path(self) -> Path: 

79 input_path = Path(self.m_name) 

80 if self.m_file_config is not None: 

81 input_dir = self.m_file_config.get_input_dir() 

82 if input_dir is not None: 

83 input_path = Path(input_dir) / input_path 

84 base_abs_path = self.m_file_config.get_base_abs_path() 

85 if base_abs_path is not None and not input_path.is_absolute(): 

86 input_path = Path(base_abs_path) / input_path 

87 return input_path.resolve() 

88 

89 def _resolve_output_path(self) -> Path | None: 

90 if self.m_file_config is None or not self.m_file_config.is_swig_enabled(): 

91 return None 

92 

93 i = self.m_name.rfind(".") 

94 name = self.m_name[:i] 

95 ext = self.m_file_config.get_output_file_ext(False) or "i" 

96 if self.get_wrap_filename(): 

97 name = name + "_wrap" 

98 output_path = Path(f"{name}.{ext}") 

99 

100 src_output_dir = self.m_file_config.get_src_output_dir(False) 

101 if src_output_dir is not None: 

102 output_path = Path(src_output_dir) / output_path 

103 base_abs_path = self.m_file_config.get_base_abs_path() 

104 if base_abs_path is not None and not output_path.is_absolute(): 

105 output_path = Path(base_abs_path) / output_path 

106 return output_path.resolve() 

107 

108 def _write_output_header(self) -> None: 

109 assert self.m_fout is not None 

110 if self.get_pyswig_version() is not None: 

111 self.m_fout.write(f"// PySwig {self.get_pyswig_version()}\n\n") 

112 else: 

113 self.m_fout.write("// PySwig unknown version\n\n") 

114 

115 def _parse_lines(self) -> None: 

116 assert self.m_fin is not None 

117 self.m_line = self.m_fin.readline() 

118 self.m_line_nbr = self.m_line_nbr + 1 

119 self.m_state = ParseState.NORMAL 

120 while self.m_line: 

121 self.m_cmd_i = -1 

122 assert self.m_line is not None 

123 if self.m_line.find("//<swig_inc/>") != -1: 

124 self.add_include() 

125 elif self.m_line.find("//<") != -1: 

126 self.start_of_single_line_comment() 

127 elif self.m_line.find("/*<") != -1: 

128 self.start_of_multi_lines_comment() 

129 self.handle_comment() 

130 

131 self.m_line = self.m_fin.readline() 

132 self.m_line_nbr = self.m_line_nbr + 1 

133 

134 def start_of_single_line_comment(self) -> None: 

135 """Handle the start of a single line comment""" 

136 assert self.m_line is not None 

137 i = self.m_line.find("//<") 

138 el_str = self.m_line[i + 3 :] 

139 

140 j = el_str.find(">") 

141 if j != -1: 

142 self.m_cmdname = el_str[:j] 

143 self.m_cmd_i = i + 3 

144 

145 def start_of_multi_lines_comment(self) -> None: 

146 """Handle the start of a multi lines comment""" 

147 assert self.m_line is not None 

148 i = self.m_line.find("/*<") 

149 el_str = self.m_line[i + 3 :] 

150 

151 j = el_str.find(">") 

152 if j != -1: 

153 self.m_cmdname = el_str[:j] 

154 self.m_cmd_i = i + 3 

155 

156 def handle_comment(self) -> None: 

157 """Handle one comment""" 

158 assert self.m_file_config is not None 

159 if self.m_state == ParseState.NORMAL: 

160 if self.m_cmd_i != -1: 

161 assert self.m_cmdname is not None 

162 self.do_command(self.m_cmdname) 

163 elif self.m_file_config.is_swig_enabled(): 

164 assert self.m_line is not None 

165 self.write(self.m_line) 

166 elif self.m_state == ParseState.SWIG_OUT: 

167 result = True 

168 if self.m_cmd_i != -1: 

169 assert self.m_cmdname is not None 

170 result = self.do_command(self.m_cmdname) 

171 if self.m_cmd_i == -1 or not result: 

172 assert self.m_line is not None 

173 self.m_line = self.m_line.replace("/*", " *") 

174 self.m_line = self.m_line.replace("*/", "* ") 

175 if self.m_file_config.is_swig_enabled(): 

176 self.write(self.m_line) 

177 elif self.m_state == ParseState.SWIG: 

178 result = True 

179 if self.m_cmd_i != -1: 

180 assert self.m_cmdname is not None 

181 result = self.do_command(self.m_cmdname) 

182 if self.m_cmd_i == -1 or not result: 

183 assert self.m_line is not None 

184 if self.m_file_config.is_swig_enabled(): 

185 self.write(self.m_line) 

186 elif self.m_state == ParseState.TAGS: 

187 if self.m_cmd_i != -1: 

188 assert self.m_cmdname is not None 

189 self.do_command(self.m_cmdname) 

190 else: 

191 assert self.m_line is not None 

192 if self.m_file_config.is_swig_enabled(): 

193 self.write(self.m_line) 

194 self.m_tags.extend(part for part in self.m_line.strip(",").split(",") if part) 

195 

196 def parse(self) -> int: 

197 """Parse this input file and generate the corresponding output file.""" 

198 input_path = self._resolve_input_path() 

199 self.m_file_in = str(input_path) 

200 output_path = self._resolve_output_path() 

201 

202 try: 

203 with open(input_path, encoding="utf-8") as fin: 

204 self.m_fin = fin 

205 if output_path is not None: 

206 output_path.parent.mkdir(parents=True, exist_ok=True) 

207 if self.m_verbose: 

208 print(f"Creating {output_path}") 

209 with open(output_path, "w", encoding="utf-8") as fout: 

210 self.m_fout = fout 

211 self._write_output_header() 

212 self._parse_lines() 

213 else: 

214 self.m_fout = None 

215 self._parse_lines() 

216 except OSError as err: 

217 raise PySwigError(f"failed to open the input file {self.m_name!r}: {err}") from err 

218 finally: 

219 self.m_fin = None 

220 self.m_fout = None 

221 

222 if self.m_file_config is not None and not self.m_found_tags: 

223 self.m_file_config.add_tags(self.m_file_in, None) 

224 return 0 

225 

226 def print_error(self, msg: str) -> None: 

227 """Raise a parse error with file and line context.""" 

228 header = f"file {self.m_file_in} line {self.m_line_nbr}: " 

229 raise PySwigError(header + msg) 

230 

231 def do_swig_out_start(self, cmd: str) -> None: 

232 """Process a <swig_out>""" 

233 if self.m_state != ParseState.NORMAL: 

234 self.print_error(f"<{cmd}> can't be nested inside another pyswig construct") 

235 

236 assert self.m_file_config is not None 

237 assert self.m_line is not None 

238 if self.m_file_config.is_swig_enabled(): 

239 assert self.m_fout is not None 

240 self.m_fout.write("/* ") 

241 self.m_fout.write(self.m_line) 

242 self.m_state = ParseState.SWIG_OUT 

243 

244 def do_swig_out_close(self, cmd: str) -> bool: 

245 """Process a </swig_out>""" 

246 assert self.m_file_config is not None 

247 assert self.m_line is not None 

248 if self.m_state == ParseState.SWIG_OUT: 

249 if self.m_file_config.is_swig_enabled(): 

250 assert self.m_fout is not None 

251 self.m_fout.write(self.m_line.strip("\n") + " */\n") 

252 if self.m_state in (ParseState.SWIG_OUT, ParseState.SWIG): 

253 self.m_state = ParseState.NORMAL 

254 return True 

255 

256 self.print_error(f"<{cmd}> without previous opening statement <{cmd[1:]}>") 

257 return False 

258 

259 def do_swig_out_closed(self) -> None: 

260 """Process a <swig_out/>""" 

261 assert self.m_fout is not None 

262 assert self.m_line is not None 

263 if self.m_file_config is not None and self.m_file_config.is_swig_enabled(): 

264 self.m_fout.write("// ") 

265 self.m_fout.write(self.m_line) 

266 

267 def do_swig_start(self, cmd: str) -> bool: 

268 """Process a <swig>""" 

269 assert self.m_line is not None 

270 assert self.m_file_config is not None 

271 if self.m_state != ParseState.NORMAL: 

272 self.print_error(f"<{cmd}> can't be nested inside another pyswig construct") 

273 if self.m_line.find("</swig>") != -1: 

274 end = self.m_line.find("</swig>") 

275 begin = self.m_cmd_i + 5 

276 if self.m_file_config.is_swig_enabled(): 

277 assert self.m_fout is not None 

278 self.m_fout.write(self.m_line[begin:end] + " " + self.m_line) 

279 self.m_state = ParseState.NORMAL 

280 return True 

281 

282 if self.m_file_config.is_swig_enabled(): 

283 assert self.m_fout is not None 

284 self.m_fout.write(self.m_line.replace("/*", "//")) 

285 self.m_state = ParseState.SWIG 

286 return True 

287 

288 def do_swig_close(self, cmd: str) -> bool: 

289 """Process a </swig>""" 

290 if self.m_state != ParseState.SWIG: 

291 self.print_error(f"<{cmd}> was found without previous <{cmd[1:]}>") 

292 assert self.m_file_config is not None 

293 if self.m_file_config.is_swig_enabled(): 

294 assert self.m_line is not None 

295 self.write(self.m_line.replace("*/", "")) 

296 self.m_state = ParseState.NORMAL 

297 return True 

298 

299 def do_swig_closed(self, cmd: str) -> bool: 

300 """Process a <swig/>""" 

301 assert self.m_line is not None 

302 assert self.m_fout is not None 

303 if self.m_file_config is not None and self.m_file_config.is_swig_enabled(): 

304 line = self.m_line.replace("//<swig/>", "") 

305 self.m_fout.write(line) 

306 else: 

307 self.m_fout.write(self.m_line) 

308 return True 

309 

310 def do_tags_start(self, cmd: str) -> None: 

311 """Process a <tags>""" 

312 assert self.m_line is not None 

313 assert self.m_file_config is not None 

314 self.m_found_tags = True 

315 if self.m_state != ParseState.NORMAL: 

316 self.print_error("<tags> can't be nested inside another pyswig construct") 

317 

318 if self.m_line.find("</tags>") != -1: 

319 end = self.m_line.find("</tags>") 

320 begin = self.m_cmd_i + 5 

321 self.m_tags = self.m_tags + self.m_line[begin:end].replace(" ", "").split(",") 

322 self.m_file_config.add_tags(self.m_file_in, self.m_tags) 

323 else: 

324 begin = self.m_cmd_i + 5 

325 self.m_tags = self.m_tags + self.m_line[begin:].split(",") 

326 self.m_state = ParseState.TAGS 

327 

328 def do_tags_close(self, cmd: str) -> None: 

329 """Process a </tags>""" 

330 assert self.m_line is not None 

331 assert self.m_file_config is not None 

332 self.m_state = ParseState.NORMAL 

333 end = self.m_line.find("</tags>") 

334 begin = self.m_cmd_i + 5 

335 self.m_tags = self.m_tags + self.m_line[begin:end].replace(" ", "").split(",") 

336 self.m_file_config.add_tags(self.m_file_in, self.m_tags) 

337 

338 def do_command(self, cmd: str) -> bool: 

339 """Process one given command""" 

340 if cmd in ["commentout", "swig_out"]: 

341 self.do_swig_out_start(cmd) 

342 return False 

343 

344 if cmd in ["/commentout", "/swig_out"]: 

345 return self.do_swig_out_close(cmd) 

346 

347 if cmd in ["swig_out/", "commentout/"]: 

348 self.do_swig_out_closed() 

349 return True 

350 

351 if cmd == "swig": 

352 return self.do_swig_start(cmd) 

353 

354 if cmd == "/swig": 

355 return self.do_swig_close(cmd) 

356 

357 if cmd == "swig/": 

358 return self.do_swig_closed(cmd) 

359 

360 if cmd == "tags": 

361 self.do_tags_start(cmd) 

362 

363 elif cmd == "/tags": 

364 self.do_tags_close(cmd) 

365 

366 else: 

367 self.print_error(f"unknown command <{cmd}> was found") 

368 return False