Coverage for packages/pyswig/src/pyswig/pyswig.py: 90%

281 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 

6import importlib.metadata 

7import shutil 

8from pathlib import Path 

9 

10from pyswig.cli_args import apply_to_engine, build_parser 

11from pyswig.exceptions import PySwigError 

12from pyswig.fileconfig import FileConfig 

13from pyswig.maininterfacefile import MainInterfaceFile 

14from pyswig.processsrcfile import ProcessSrcFile 

15from pyswig.swig import Swig 

16from pyswig.types import DefineRecord, ReplaceStringPair, normalize_source_entry 

17 

18_FALLBACK_VERSION = "0.1.4" 

19 

20 

21def _package_version() -> str: 

22 try: 

23 return importlib.metadata.version("pyswig") 

24 except importlib.metadata.PackageNotFoundError: 

25 pass 

26 return _FALLBACK_VERSION 

27 

28 

29class PySwig: 

30 """The class used to configure the Swig preprocessor""" 

31 

32 VERSION = _package_version() 

33 

34 def __init__(self, verbose: bool = True, *, parse_cli: bool = True) -> None: 

35 """Constructor""" 

36 self.m_file_configs: list[FileConfig] = [] 

37 self.m_input_files: list[str] = [] 

38 self.m_resolved_input_file: str | None = None 

39 self.m_module_name: str | None = None 

40 self.m_use_director = False 

41 self.m_src_output_dir: str | None = None 

42 self.m_inc_output_dir: str | None = None 

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

44 self.m_global_include_files: list[str] = [] 

45 self.m_local_include_files: list[str] = [] 

46 self.m_defines: list[DefineRecord] = [] 

47 self.m_output_file_ext: str | None = None 

48 self.m_version = PySwig.VERSION 

49 self.m_swig = Swig() 

50 self.m_output_file: str | None = None 

51 self.m_copy_shadow_dir: str | None = None 

52 self.m_replace_strings: list[ReplaceStringPair] = [] 

53 self.m_include_libs: list[str] | None = None 

54 self.m_imports: list[str] | None = None 

55 self.m_verbose = verbose 

56 

57 self.set_verbose(verbose) 

58 if parse_cli: 

59 self.process_param() 

60 

61 def set_imports(self, imports: list[str]) -> None: 

62 self.m_imports = imports 

63 

64 def add_import(self, import_lib: str) -> None: 

65 if self.m_imports is None: 

66 self.m_imports = [] 

67 self.m_imports.append(import_lib) 

68 

69 def get_imports(self) -> list[str] | None: 

70 return self.m_imports 

71 

72 def set_include_libs(self, include_libs: list[str] | None) -> None: 

73 self.m_include_libs = include_libs 

74 

75 def add_include_lib(self, include_lib: str) -> None: 

76 if self.m_include_libs is None: 

77 self.m_include_libs = [] 

78 self.m_include_libs.append(include_lib) 

79 

80 def get_include_libs(self) -> list[str] | None: 

81 return self.m_include_libs 

82 

83 def add_replace_string(self, orig: str, new: str) -> None: 

84 """Add string to be replace in the wrapper source code gerenated by Swig""" 

85 self.m_replace_strings.append(ReplaceStringPair(orig, new)) 

86 

87 def get_replace_strings(self) -> list[ReplaceStringPair]: 

88 return self.m_replace_strings 

89 

90 def get_shadow_file(self) -> str | None: 

91 """Get the shadow file name if any is build""" 

92 module_name = self.get_module_name() 

93 if self.get_swig().get_language() == "python" and module_name is not None: 

94 return module_name + ".py" 

95 return None 

96 

97 def set_copy_shadow_dir(self, copy_shadow_dir: str) -> None: 

98 """Set the folder where the shadow class is copied to if any""" 

99 self.m_copy_shadow_dir = copy_shadow_dir 

100 

101 def get_copy_shadow_dir(self) -> str | None: 

102 """Get the folder where the shadow class is copied to if any""" 

103 return self.m_copy_shadow_dir 

104 

105 def set_output_file(self, output_file: str) -> None: 

106 """Set the outfile for Swig""" 

107 self.m_output_file = output_file 

108 

109 def get_output_file(self) -> str | None: 

110 """Get the outfile for Swig""" 

111 return self.m_output_file 

112 

113 def set_all_warnings(self, all_warnings: bool = True) -> None: 

114 """Turn on or off all warnings""" 

115 self.m_swig.set_all_warnings(all_warnings) 

116 

117 def get_all_warnings(self) -> bool: 

118 """Get if all warnings are turned on or off""" 

119 return self.m_swig.get_all_warnings() 

120 

121 def set_threads_enabled(self, are_threads_enabled: bool = True) -> None: 

122 """Turn on or off thread support""" 

123 self.m_swig.set_threads_enabled(are_threads_enabled) 

124 

125 def get_threads_enabled(self) -> bool: 

126 """Get if swig will generate theaded wrapper ot not""" 

127 return self.m_swig.get_threads_enabled() 

128 

129 def set_process_cpp(self, process_cpp: bool = True) -> None: 

130 """Turn on of off the processing of C++""" 

131 self.m_swig.set_process_cpp(process_cpp) 

132 

133 def get_process_cpp(self) -> bool: 

134 """Get if Swig will be processing of C++ is turned on or off""" 

135 return self.m_swig.get_process_cpp() 

136 

137 def set_language(self, language: str) -> None: 

138 """Set the language that the wrapper Swig should create""" 

139 self.m_swig.set_language(language) 

140 

141 def get_language(self) -> str | None: 

142 """Get the language that the wrapper Swig should create""" 

143 return self.m_swig.get_language() 

144 

145 def get_swig(self) -> Swig: 

146 """Get the Swig instance""" 

147 return self.m_swig 

148 

149 def set_verbose(self, verbose: bool) -> None: 

150 """Set the verbosity level""" 

151 self.m_verbose = verbose 

152 self.m_swig.set_verbose(verbose) 

153 

154 def get_verbose(self) -> bool: 

155 """Get the verbosity level""" 

156 return self.m_verbose 

157 

158 def _message( 

159 self, 

160 *args: object, 

161 sep: str = " ", 

162 end: str = "\n", 

163 flush: bool = False, 

164 ) -> None: 

165 if self.get_verbose(): 

166 print(*args, sep=sep, end=end, flush=flush) 

167 

168 def get_version(self) -> str: 

169 """Get PySwig version""" 

170 return self.m_version 

171 

172 def set_output_file_ext(self, ext: str) -> None: 

173 """Set the extension that the generated files input to Swig should have""" 

174 self.m_output_file_ext = ext 

175 

176 def get_output_file_ext(self) -> str | None: 

177 """get the extension that the generated files input to Swig should have""" 

178 return self.m_output_file_ext 

179 

180 def set_module_name(self, module_name: str) -> None: 

181 """Set the Swig module name""" 

182 self.m_module_name = module_name 

183 

184 def get_module_name(self) -> str | None: 

185 """Get the Swig module name""" 

186 return self.m_module_name 

187 

188 def set_use_director(self, use_director: bool) -> None: 

189 """Set if the director feature of Swig should be used or not""" 

190 self.m_use_director = use_director 

191 

192 def get_use_director(self) -> bool: 

193 """Get if the director feature of Swig should be used or not""" 

194 return self.m_use_director 

195 

196 def set_src_output_dir(self, src_output_dir: str) -> None: 

197 """Set the output directory for source file""" 

198 self.m_src_output_dir = src_output_dir 

199 

200 def get_src_output_dir(self) -> str | None: 

201 """Get the output directory for source file""" 

202 return self.m_src_output_dir 

203 

204 def set_inc_output_dir(self, inc_output_dir: str) -> None: 

205 """Set the output directory for include files""" 

206 self.m_inc_output_dir = inc_output_dir 

207 

208 def get_inc_output_dir(self) -> str | None: 

209 """Get the output directory for include files""" 

210 return self.m_inc_output_dir 

211 

212 def add_typemap(self, type_map: str) -> None: 

213 """Add a typemap which should be included in the generated files""" 

214 self.m_typemaps.append(type_map) 

215 

216 def get_typemaps(self) -> list[str]: 

217 """Get the list of all typemaps to use""" 

218 return self.m_typemaps 

219 

220 def add_include(self, include: str, local: bool = True) -> None: 

221 """Add a C/C++ include file to be added""" 

222 if local: 

223 self.m_local_include_files.append(include) 

224 else: 

225 self.m_global_include_files.append(include) 

226 

227 def get_includes(self, local: bool = True) -> list[str]: 

228 """Get the list of include files to be addded to the generated files""" 

229 if local: 

230 return self.m_local_include_files 

231 return self.m_global_include_files 

232 

233 def add_define(self, name: str, value: str | None = "") -> None: 

234 """Adds a define to be included in the generated files""" 

235 if value == "": 

236 value = None 

237 self.m_defines.append(DefineRecord(name, value)) 

238 

239 def get_defines(self) -> list[DefineRecord]: 

240 """Get the list of all defines to be included in the generated files""" 

241 return self.m_defines 

242 

243 def add_file_config(self, file_config: FileConfig) -> None: 

244 """Add a FileConfig object""" 

245 self.m_file_configs.append(file_config) 

246 file_config.set_pyswig(self) 

247 

248 def get_file_configs(self) -> list[FileConfig]: 

249 """Return the list of all FileConfig objects""" 

250 return self.m_file_configs 

251 

252 def process_param(self) -> None: 

253 """Process the command line parameters.""" 

254 apply_to_engine(self, build_parser().parse_args()) 

255 

256 def set_input_files(self, input_files: list[str]) -> None: 

257 """Set CLI-style input header paths.""" 

258 self.m_input_files = list(input_files) 

259 self.m_resolved_input_file = None 

260 

261 def get_input_files(self) -> list[str]: 

262 """Return configured CLI input header paths.""" 

263 return list(self.m_input_files) 

264 

265 def get_resolved_input_file(self) -> str | None: 

266 """Return the absolute path resolved from a single CLI input header.""" 

267 return self.m_resolved_input_file 

268 

269 def add_include_dir(self, include_dir: str) -> None: 

270 """Add a Swig include search directory (-I).""" 

271 self.add_include_lib(include_dir) 

272 

273 def generate(self) -> None: 

274 """Generate all output files""" 

275 self._message(f"PySwig {self.get_version()}\n") 

276 

277 detected = self.m_swig.detect() 

278 if detected: 

279 self._message(f"Swig {self.m_swig.get_version()} detected.\n") 

280 else: 

281 self._message("Couldn't find Swig.\n") 

282 

283 self.handle_input_file() 

284 

285 for file_config in self.get_file_configs(): 

286 self.do_file_config(file_config) 

287 

288 self.generate_main_interface_file() 

289 self.generate_include_file() 

290 

291 def _parse_source( 

292 self, 

293 path: str, 

294 file_config: FileConfig | None = None, 

295 wrap_filename: bool = False, 

296 ) -> None: 

297 processor = ProcessSrcFile(path, self.get_verbose(), file_config, wrap_filename) 

298 processor.set_pyswig_version(self.get_version()) 

299 processor.parse() 

300 

301 def do_file_config(self, file_config: FileConfig) -> None: 

302 """Generate the output files related to one given FileConfig""" 

303 temp_folder = file_config.get_src_output_dir(False) or "temp_pyswig" 

304 Path(temp_folder).mkdir(parents=True, exist_ok=True) 

305 

306 source_files = file_config.get_source_files() 

307 if source_files is not None: 

308 input_dir = file_config.get_input_dir() 

309 for entry in source_files: 

310 spec = normalize_source_entry(entry) 

311 if input_dir is not None and Path(input_dir, spec.name).is_dir(): 

312 continue 

313 self._parse_source(spec.name, file_config, spec.wrap) 

314 return 

315 

316 resolved_input = self.get_resolved_input_file() 

317 if resolved_input is not None: 

318 self._parse_source(resolved_input) 

319 return 

320 input_files = self.get_input_files() 

321 if input_files: 

322 self._parse_source(input_files[0]) 

323 return 

324 raise PySwigError("must provide a file") 

325 

326 def handle_input_file(self) -> None: 

327 """Resolve CLI input paths without changing the process working directory.""" 

328 input_files = self.get_input_files() 

329 if not input_files: 

330 return 

331 if len(input_files) != 1: 

332 raise PySwigError("more than one input file was given") 

333 

334 input_path = Path(input_files[0]).resolve() 

335 self._message(f"Input file : {input_path}") 

336 self.m_resolved_input_file = str(input_path) 

337 self._message("") 

338 

339 def generate_include_file(self) -> None: 

340 """Write the include file included in the wrapper if generated""" 

341 inc_output_dir = self.get_inc_output_dir() 

342 module_name = self.get_module_name() 

343 if inc_output_dir is None or module_name is None: 

344 return 

345 output_path = Path(inc_output_dir) / f"{module_name}_inc.h" 

346 self._message(f"Creating {output_path}") 

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

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

349 fout.write(f"// PySwig {self.get_version()}\n\n") 

350 fout.write("#pragma once\n") 

351 fout.write("\n") 

352 

353 for file_config in self.get_file_configs(): 

354 base_dir = file_config.get_base_inc_dir() 

355 source_files = file_config.get_source_files() 

356 if source_files is None: 

357 continue 

358 for entry in source_files: 

359 header_name = normalize_source_entry(entry).name 

360 if base_dir is not None: 

361 fout.write(f'#include "{base_dir}/{header_name}"\n') 

362 else: 

363 fout.write(f'#include "{header_name}"\n') 

364 fout.write("\n\n") 

365 

366 def generate_main_interface_file(self) -> None: 

367 """Write the Swig interface file, the main input file for Swig""" 

368 main_if_file = MainInterfaceFile(self) 

369 main_if_file.generate() 

370 

371 def run_swig(self) -> int: 

372 """Run swig on the newly generated files""" 

373 src_output_dir = self.get_src_output_dir() 

374 output_file = self.get_output_file() 

375 if src_output_dir is None: 

376 raise PySwigError("source output directory must be configured before running Swig") 

377 if output_file is None: 

378 raise PySwigError("main interface file must be generated before running Swig") 

379 

380 self._message("\nRunning Swig ...") 

381 self._message(f"cwd {src_output_dir}") 

382 self.get_swig().set_input_file(output_file) 

383 self.get_swig().set_include_libs(self.get_include_libs()) 

384 

385 result = self.get_swig().run(cwd=src_output_dir) 

386 if result == 0: 

387 self._message("Success.") 

388 else: 

389 self._message("Failed.") 

390 

391 shadow_file = self.get_shadow_file() 

392 copy_shadow_dir = self.get_copy_shadow_dir() 

393 if copy_shadow_dir is not None and shadow_file is not None: 

394 shadow_path = Path(src_output_dir) / shadow_file 

395 self._message(f"copy file {shadow_file} to {copy_shadow_dir}") 

396 shutil.copy(str(shadow_path), copy_shadow_dir) 

397 

398 return result 

399 

400 def get_output_cxx_filename(self) -> str: 

401 module_name = self.get_module_name() 

402 if module_name is None: 

403 raise PySwigError("module name must be configured") 

404 return module_name + "_wrap.cxx" 

405 

406 def get_output_cxx_file_path(self) -> str: 

407 src_output_dir = self.get_src_output_dir() 

408 if src_output_dir is None: 

409 raise PySwigError("source output directory must be configured") 

410 return str(Path(src_output_dir) / self.get_output_cxx_filename()) 

411 

412 def replace_strings_in_line(self, line: str) -> str: 

413 for rep in self.get_replace_strings(): 

414 line = line.replace(rep.orig, rep.new) 

415 return line 

416 

417 def replace_all_strings(self) -> None: 

418 """Replace all strings in Swig generated wrapper file""" 

419 src_output_dir = self.get_src_output_dir() 

420 if src_output_dir is None: 

421 raise PySwigError("source output directory must be configured") 

422 

423 filename = self.get_output_cxx_filename() 

424 src_dir = Path(src_output_dir) 

425 target_path = src_dir / filename 

426 orig_path = src_dir / (filename + ".orig") 

427 

428 self._message(f"\nReplacing strings in {filename} ...") 

429 self._message(f"cwd {src_output_dir}") 

430 

431 shutil.move(str(target_path), str(orig_path)) 

432 

433 with ( 

434 open(orig_path, encoding="utf-8") as fin, 

435 open(target_path, "w", encoding="utf-8") as fout, 

436 ): 

437 for line in fin: 

438 fout.write(self.replace_strings_in_line(line))