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
« 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
4from __future__ import annotations
6from enum import IntEnum
7from pathlib import Path
8from typing import TYPE_CHECKING, TextIO
10from pyswig.exceptions import PySwigError
12if TYPE_CHECKING:
13 from pyswig.fileconfig import FileConfig
16class ParseState(IntEnum):
17 NORMAL = 0
18 SWIG_OUT = 1
19 SWIG = 2
20 TAGS = 3
23class ProcessSrcFile:
24 """Process one FileConfig"""
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
50 def get_wrap_filename(self) -> bool:
51 return self.m_wrap_filename
53 def set_pyswig_version(self, pyswig_version: str) -> None:
54 self.m_pyswig_version = pyswig_version
56 def get_pyswig_version(self) -> str | None:
57 return self.m_pyswig_version
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")
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)
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()
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
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}")
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()
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")
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()
131 self.m_line = self.m_fin.readline()
132 self.m_line_nbr = self.m_line_nbr + 1
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 :]
140 j = el_str.find(">")
141 if j != -1:
142 self.m_cmdname = el_str[:j]
143 self.m_cmd_i = i + 3
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 :]
151 j = el_str.find(">")
152 if j != -1:
153 self.m_cmdname = el_str[:j]
154 self.m_cmd_i = i + 3
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)
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()
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
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
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)
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")
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
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
256 self.print_error(f"<{cmd}> without previous opening statement <{cmd[1:]}>")
257 return False
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)
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
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
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
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
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")
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
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)
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
344 if cmd in ["/commentout", "/swig_out"]:
345 return self.do_swig_out_close(cmd)
347 if cmd in ["swig_out/", "commentout/"]:
348 self.do_swig_out_closed()
349 return True
351 if cmd == "swig":
352 return self.do_swig_start(cmd)
354 if cmd == "/swig":
355 return self.do_swig_close(cmd)
357 if cmd == "swig/":
358 return self.do_swig_closed(cmd)
360 if cmd == "tags":
361 self.do_tags_start(cmd)
363 elif cmd == "/tags":
364 self.do_tags_close(cmd)
366 else:
367 self.print_error(f"unknown command <{cmd}> was found")
368 return False