import logging from dataclasses import dataclass, field from typing import Any, Dict, List, Set from .cxxparser import Enum, Function, LiteralVariable, Variable from .preprocessor import PreprocessedClass, PreprocessedMethod from .textholder import Indent, IndentLater, TextHolder from .type import array_base, is_array_type, is_pointer_type, pointer_base, remove_cvref logger = logging.getLogger(__file__) def _read_file(name: str): with open(name, "rt") as f: return f.read() def render_template(template: str, **kwargs): for key, replacement in kwargs.items(): template = template.replace(f"${key}", str(replacement)) return template @dataclass class GeneratorOptions: typedefs: Dict[str, str] = field(default_factory=dict) constants: Dict[str, Variable] = field(default_factory=dict) # to global value functions: Dict[str, Function] = field(default_factory=dict) # to def classes: Dict[str, PreprocessedClass] = field(default_factory=dict) # to class dict_classes: Set[str] = field(default_factory=set) # to dict enums: Dict[str, Enum] = field(default_factory=dict) includes: List[str] = field(default_factory=list) split_in_files: bool = True module_name: str = 'unknown_module' max_classes_in_one_file: int = 50 cpp_str_bases = {'char', 'wchar_t', 'char8_t', 'char16_t', 'char32_t'} cpp_base_type_to_python_map = { 'char8_t': "int", 'char16_t': "int", 'char32_t': "int", 'wchar_t': "int", 'char': 'int', 'short': 'int', 'int': 'int', 'long': 'int', 'long long': 'int', 'unsigned char': 'int', 'unsigned short': 'int', 'unsigned int': 'int', 'unsigned long': 'int', 'unsigned long long': 'int', 'float': 'float', 'double': 'float', 'bool': 'bool', 'void': 'None', } python_type_to_pybind11 = { 'int': 'int_', 'float': 'float_', 'str': 'str', 'None': 'none', } def cpp_base_type_to_python(t: str): if is_pointer_type(t): if pointer_base(t) in cpp_str_bases: return 'str' if is_array_type(t): if array_base(t) in cpp_str_bases: return 'str' if t in cpp_base_type_to_python_map: return cpp_base_type_to_python_map[t] return None def cpp_base_type_to_pybind11(t: str): t = remove_cvref(t) return python_type_to_pybind11[cpp_base_type_to_python(t)] def python_value_to_cpp_literal(val: Any): t = type(val) if t is str: return f'"({val})"' if t is int: return f'({val})' if t is float: return f'(double({val}))' class Generator: def __init__(self, options: GeneratorOptions): self.options = options self.template_dir = "templates" self.saved_files: Dict[str, str] = {} def generate(self): # all classes self._output_wrappers() self._output_module() self._output_class_generator_declarations() self._output_ide_hints() self._save_template("dispatcher.hpp") self._save_template("property_helper.hpp") self._save_template("wrapper_helper.hpp") return self.saved_files def cpp_variable_to_py_with_hint( self, v: Variable, append='', append_unknown: bool = True ): cpp_type = self._cpp_type_to_python(v.type) default_value = '' if v.default: default_value = ' = ' + str(v.default) if cpp_type: return f"{v.name}: {cpp_type}{default_value}{append}" if append_unknown: return f"{v.name}: {v.type}{default_value}{append} # unknown what to wrap in py" else: return f"{v.name}: {v.type}{default_value}{append}" def _cpp_type_to_python(self, t: str): t = remove_cvref(t) base_type = cpp_base_type_to_python(t) if base_type: return base_type if is_pointer_type(t): t = pointer_base(t) while t in self.options.typedefs: t = self.options.typedefs[t] if t in self.options.classes: c = self.options.classes[t] if self._should_wrap_as_dict(c): return 'dict' else: return t if t in self.options.enums: return t return cpp_base_type_to_python(t) def _should_wrap_as_dict(self, c: PreprocessedClass): return c.name in self.options.dict_classes def _output_ide_hints(self): hint_code = TextHolder() for c in self.options.classes.values(): if self._should_output_class_generator(c): class_code = TextHolder() class_code += f"class {c.name}:" + Indent() for ms in c.functions.values(): for m in ms: class_code += '\n' if m.is_static: class_code += "@staticmethod" class_code += f"def {m.name}(" + Indent() else: class_code += f"def {m.name}(self, " + Indent() for arg in m.args: class_code += Indent( self.cpp_variable_to_py_with_hint( arg, append=',' ) ) cpp_ret_type = self._cpp_type_to_python(m.ret_type) class_code += f") -> {cpp_ret_type if cpp_ret_type else m.ret_type}:" class_code += "\n" class_code += "..." - IndentLater() for v in c.variables.values(): description = self.cpp_variable_to_py_with_hint(v) class_code += f"{description}" class_code += "\n" class_code += "..." class_code += "..." - IndentLater() hint_code += class_code for v in self.options.constants.values(): description = self.cpp_variable_to_py_with_hint(v) if description: hint_code += f"{description}" for e in self.options.enums.values(): enum_code = TextHolder() enum_code += f"class {e.name}:" + Indent() for v in e.values.values(): description = self.cpp_variable_to_py_with_hint(v) enum_code += f"{description}" enum_code += "..." - IndentLater() hint_code += enum_code self._save_template( template_filename="hint.py", output_filename=f"{self.options.module_name}.pyi", hint_code=hint_code ) def _output_wrappers(self): pyclass_template = _read_file(f'{self.template_dir}/wrapper_class.h') wrappers = '' # generate callback wrappers for c in self.options.classes.values(): if self._has_wrapper(c): wrapper_code = TextHolder() for ms in c.functions.values(): for m in ms: # filter all arguments can convert as dict dict_types = self._method_dict_types(m) if m.is_virtual and not m.is_final: function_code = self._generate_callback_wrapper( m, dict_types=dict_types ) wrapper_code += Indent(function_code) if dict_types: wrapper_code += self._generate_calling_wrapper( c, m, dict_types=dict_types ) py_class_code = render_template( pyclass_template, class_name=c.name, body=wrapper_code ) wrappers += py_class_code self._save_template(f'wrappers.hpp', wrappers=wrappers) def _output_class_generator_declarations(self): class_generator_declarations = TextHolder() for c in self.options.classes.values(): class_name = c.name if not self._should_wrap_as_dict(c): class_generator_function_name = self._generate_class_generator_function_name( class_name ) class_generator_declarations += f"void {class_generator_function_name}(pybind11::module &m);" self._save_template( f'class_generators.h', class_generator_declarations=class_generator_declarations, ) def _should_output_class_generator(self, c: PreprocessedClass): return not self._should_wrap_as_dict(c) def _output_module(self): call_to_generator_code, combined_class_generator_definitions = self._output_class_generator_definitions() constants_code = TextHolder() for name, value in self.options.constants.items(): pybind11_type = cpp_base_type_to_pybind11(value.type) literal = python_value_to_cpp_literal(value.default) if isinstance(value, LiteralVariable): if value.literal_valid: literal = value.literal constants_code += Indent( f"""m.add_object("{name}", pybind11::{pybind11_type}({literal}));""" ) enums_code = TextHolder() for name, e in self.options.enums.items(): enums_code += 1 enums_code += f"""pybind11::enum_<{e.full_name}>(m, "{e.name}")""" + Indent( ) for v in e.values.values(): enums_code += f""".value("{v.name}", {e.full_name_of(v)})""" enums_code += ".export_values()" enums_code += ";" - Indent() enums_code -= 1 self._save_template( template_filename="module.cpp", output_filename=f'{self.options.module_name}.cpp', module_name=self.options.module_name, classes_code=call_to_generator_code, combined_class_generator_definitions= combined_class_generator_definitions, constants_code=constants_code, enums_code=enums_code, ) def _output_class_generator_definitions(self): class_template = _read_file(f'{self.template_dir}/class.cpp') call_to_generator_code = TextHolder() combined_class_generator_definitions = TextHolder() file_index = 1 classes_in_this_file = 0 # generate class call_to_generator_code class_generator_code = TextHolder() for c in self.options.classes.values(): class_name = c.name if self._should_output_class_generator(c): # header first class_generator_function_name = self._generate_class_generator_function_name( class_name ) class_generator_code += f"void {class_generator_function_name}(pybind11::module &m)" class_generator_code += "{" + Indent() if self._has_wrapper(c): wrapper_class_name = "Py" + c.name if c.destructor is not None and c.destructor.access == 'public': class_generator_code += f"""pybind11::class_<{wrapper_class_name}>(m, "{class_name}")\n""" else: class_generator_code += f"pybind11::class_<" + Indent() class_generator_code += f"{class_name}," class_generator_code += f"std::unique_ptr<{class_name}, pybind11::nodelete>," class_generator_code += f"{wrapper_class_name}" class_generator_code += f"""> c(m, "{class_name}");\n""" - Indent( ) else: class_generator_code += f"""pybind11::class_<{class_name}> c(m, "{class_name}");\n""" # constructor if not c.is_pure_virtual: if c.constructors: for con in c.constructors: arg_list = ",".join([arg.type for arg in con.args]) class_generator_code += f"""c.def(pybind11::init<{arg_list}>());\n""" else: class_generator_code += f"""c.def(pybind11::init<>());\n""" # functions for ms in c.functions.values(): has_overwrite: bool = False if len(ms) > 1: has_overwrite = True for m in ms: if m.is_static: class_generator_code += f"""c.def_static("{m.name}",""" + Indent( ) else: class_generator_code += f"""c.def("{m.name}",""" + Indent( ) class_generator_code += f"""autocxxpy::calling_wrapper<""" if has_overwrite: class_generator_code += f"""static_cast<{m.type}>(""" + Indent( ) class_generator_code += f"""&{class_name}::{m.name}""" if has_overwrite: class_generator_code += f""")""" - IndentLater() class_generator_code += f""">::value""" class_generator_code += f""");\n""" - Indent() for name, value in c.variables.items(): class_generator_code += f"""c.AUTOCXXPY_DEF_PROPERTY({class_name}, {name});\n""" class_generator_code += "}" - Indent() if self.options.split_in_files: if self.options.max_classes_in_one_file <= 1: self._save_file( f'{class_name}.cpp', self.render_template( class_template, class_generator_definition=class_generator_code ) ) class_generator_code = TextHolder() else: classes_in_this_file += 1 if classes_in_this_file == self.options.max_classes_in_one_file: self._save_file( f"classes_{file_index}.cpp", self.render_template( class_template, class_generator_definition= class_generator_code ) ) file_index += 1 classes_in_this_file = 0 class_generator_code = TextHolder() else: combined_class_generator_definitions += class_generator_code class_code = TextHolder() class_code += f"{class_generator_function_name}(m);" call_to_generator_code += Indent(class_code) if class_generator_code: self._save_file( f"classes_{file_index}.cpp", self.render_template( class_template, class_generator_definition=class_generator_code ) ) return call_to_generator_code, combined_class_generator_definitions def _generate_class_generator_function_name(self, class_name): class_generator_function_name = f"generate_class_{class_name}" return class_generator_function_name def _has_wrapper(self, c: PreprocessedClass): return not self._should_wrap_as_dict(c) and c.is_polymorphic def _method_dict_types(self, m): # filter all arguments can convert as dict arg_base_types = set(remove_cvref(i.type) for i in m.args) return set( i for i in (arg_base_types & self.options.dict_classes) if self._should_wrap_as_dict(i) ) def _generate_callback_wrapper( self, m: PreprocessedMethod, dict_types: set = None ): # calling_back_code ret_type = m.ret_type args = m.args function_name = m.name arguments_signature = ",".join([f"{i.type} {i.name}" for i in args]) arg_list = ",".join( ['this', f'"{function_name}"', *[f"{i.name}" for i in args]] ) if m.has_overload: cast_expression = f"static_cast<{m.type}>(&{m.full_name})" else: cast_expression = f"&{m.full_name}" function_code = TextHolder() function_code += f"{ret_type} {function_name}({arguments_signature}) override\n" function_code += "{\n" + Indent() function_code += f"return autocxxpy::callback_wrapper<{cast_expression}>::call(" + Indent( ) function_code += f"{arg_list}" - IndentLater() function_code += f");" function_code += "}\n" - Indent() return function_code def _generate_calling_wrapper(self, c, m, dict_types: set = None): return "" pass def _save_template( self, template_filename: str, output_filename: str = None, **kwargs ): template = _read_file(f'{self.template_dir}/{template_filename}') if output_filename is None: output_filename = template_filename return self._save_file( output_filename, self.render_template(template, **kwargs) ) def _save_file(self, filename, data): self.saved_files[filename] = data def render_template(self, templates, **kwargs): kwargs['includes'] = self._generate_includes() return render_template(templates, **kwargs) def _generate_includes(self): code = "" for i in self.options.includes: code += f"""#include "{i}"\n""" return code