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
« 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
6import importlib.metadata
7import shutil
8from pathlib import Path
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
18_FALLBACK_VERSION = "0.1.4"
21def _package_version() -> str:
22 try:
23 return importlib.metadata.version("pyswig")
24 except importlib.metadata.PackageNotFoundError:
25 pass
26 return _FALLBACK_VERSION
29class PySwig:
30 """The class used to configure the Swig preprocessor"""
32 VERSION = _package_version()
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
57 self.set_verbose(verbose)
58 if parse_cli:
59 self.process_param()
61 def set_imports(self, imports: list[str]) -> None:
62 self.m_imports = imports
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)
69 def get_imports(self) -> list[str] | None:
70 return self.m_imports
72 def set_include_libs(self, include_libs: list[str] | None) -> None:
73 self.m_include_libs = include_libs
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)
80 def get_include_libs(self) -> list[str] | None:
81 return self.m_include_libs
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))
87 def get_replace_strings(self) -> list[ReplaceStringPair]:
88 return self.m_replace_strings
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
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
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
105 def set_output_file(self, output_file: str) -> None:
106 """Set the outfile for Swig"""
107 self.m_output_file = output_file
109 def get_output_file(self) -> str | None:
110 """Get the outfile for Swig"""
111 return self.m_output_file
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)
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()
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)
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()
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)
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()
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)
141 def get_language(self) -> str | None:
142 """Get the language that the wrapper Swig should create"""
143 return self.m_swig.get_language()
145 def get_swig(self) -> Swig:
146 """Get the Swig instance"""
147 return self.m_swig
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)
154 def get_verbose(self) -> bool:
155 """Get the verbosity level"""
156 return self.m_verbose
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)
168 def get_version(self) -> str:
169 """Get PySwig version"""
170 return self.m_version
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
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
180 def set_module_name(self, module_name: str) -> None:
181 """Set the Swig module name"""
182 self.m_module_name = module_name
184 def get_module_name(self) -> str | None:
185 """Get the Swig module name"""
186 return self.m_module_name
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
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
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
200 def get_src_output_dir(self) -> str | None:
201 """Get the output directory for source file"""
202 return self.m_src_output_dir
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
208 def get_inc_output_dir(self) -> str | None:
209 """Get the output directory for include files"""
210 return self.m_inc_output_dir
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)
216 def get_typemaps(self) -> list[str]:
217 """Get the list of all typemaps to use"""
218 return self.m_typemaps
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)
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
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))
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
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)
248 def get_file_configs(self) -> list[FileConfig]:
249 """Return the list of all FileConfig objects"""
250 return self.m_file_configs
252 def process_param(self) -> None:
253 """Process the command line parameters."""
254 apply_to_engine(self, build_parser().parse_args())
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
261 def get_input_files(self) -> list[str]:
262 """Return configured CLI input header paths."""
263 return list(self.m_input_files)
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
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)
273 def generate(self) -> None:
274 """Generate all output files"""
275 self._message(f"PySwig {self.get_version()}\n")
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")
283 self.handle_input_file()
285 for file_config in self.get_file_configs():
286 self.do_file_config(file_config)
288 self.generate_main_interface_file()
289 self.generate_include_file()
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()
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)
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
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")
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")
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("")
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")
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")
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()
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")
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())
385 result = self.get_swig().run(cwd=src_output_dir)
386 if result == 0:
387 self._message("Success.")
388 else:
389 self._message("Failed.")
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)
398 return result
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"
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())
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
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")
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")
428 self._message(f"\nReplacing strings in {filename} ...")
429 self._message(f"cwd {src_output_dir}")
431 shutil.move(str(target_path), str(orig_path))
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))