diff --git a/cppwg/generators.py b/cppwg/generators.py index 3a1b421..c6aef3d 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -10,7 +10,7 @@ import pygccxml -from cppwg.input.package_info import PackageInfo +from cppwg.info.package_info import PackageInfo from cppwg.parsers.package_info_parser import PackageInfoParser from cppwg.parsers.source_parser import CppSourceParser from cppwg.templates import pybind11_default as wrapper_templates @@ -174,8 +174,8 @@ def log_unknown_classes(self) -> None: all_class_decls = self.source_ns.classes(allow_empty=True) seen_class_names = set() - for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: + for module_info in self.package_info.module_collection: + for class_info in module_info.class_collection: seen_class_names.add(class_info.name) if class_info.decls: seen_class_names.update(decl.name for decl in class_info.decls) @@ -259,11 +259,8 @@ def generate(self) -> None: # Parse the input yaml for package, module, and class information self.parse_package_info() - # Collect header files, skipping wrappers to avoid pollution - self.package_info.collect_source_headers(restricted_paths=[self.wrapper_root]) - - # Update info objects with data from the source headers - self.package_info.update_from_source() + # Collect header files (skip wrappers), and update info + self.package_info.init(restricted_paths=[self.wrapper_root]) # Write the header collection file self.write_header_collection() diff --git a/cppwg/input/__init__.py b/cppwg/info/__init__.py similarity index 100% rename from cppwg/input/__init__.py rename to cppwg/info/__init__.py diff --git a/cppwg/info/base_info.py b/cppwg/info/base_info.py new file mode 100644 index 0000000..dac9197 --- /dev/null +++ b/cppwg/info/base_info.py @@ -0,0 +1,268 @@ +"""Generic information structure.""" + +import importlib.util +import logging +import os +import sys +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional + +import cppwg.templates.custom as cppwg_custom +from cppwg.utils.constants import CPPWG_SOURCEROOT_STRING + + +class BaseInfo(ABC): + """ + A generic information structure for features. + + Features include packages, modules, classes, free functions, etc. + Information structures are used to store information about the features. + BaseInfo is the base information structure, and set up attributes that are + common to all features. + + Attributes + ---------- + arg_type_excludes : List[str] + List of exclude patterns for arg types in methods. + calldef_excludes : List[str] + Do not include calldefs matching these patterns. + constructor_arg_type_excludes : List[str] + List of exclude patterns for arg types in constructors. + constructor_signature_excludes : List[List[str]] + List of exclude patterns for constructor signatures. + custom_generator : str + A custom generator for the feature. + excluded: bool + Exclude this feature. + excluded_methods : List[str] + Do not include these methods. + excluded_variables : List[str] + Do not include these variables. + extra_code : List[str] + Any extra wrapper code for the feature. + name : str + The name of the package, module, class etc. represented by this object. + name_replacements : Dict[str, str] + A dictionary of name replacements e.g. {"double":"Double"} + pointer_call_policy : str + The default pointer call policy. + prefix_code : List[str] + Any wrapper code that precedes the feature. + prefix_text : str + Text to add at the top of all wrappers. + reference_call_policy : str + The default reference call policy. + return_type_excludes : List[str] + List of exclude patterns for return types. + smart_ptr_type : str + Handle classes with this smart pointer type. + source_includes : List[str] + A list of source files to be included with the feature. + source_root : str + The root directory of the C++ source code. + template_substitutions : Dict[str, List[Any]] + A list of template substitution sequences. + + custom_generator_instance : cppwg_custom.Custom + An instance of the custom generator class. + """ + + def __init__(self, name: str, info_config: Optional[Dict[str, Any]] = None) -> None: + """ + Create a base info object from a config dict. + + Parameters + ---------- + name : str + The name of the package, module, class, etc. represented by this object. + info_config : Dict[str, Any] + A dictionary of configuration settings + """ + self.name: str = name + + # Paths + self.source_includes: List[str] = [] + self.source_root: str = "" + + # Exclusions + self.arg_type_excludes: List[str] = [] + self.calldef_excludes: List[str] = [] + self.constructor_arg_type_excludes: List[str] = [] + self.constructor_signature_excludes: List[List[str]] = [] + self.excluded: bool = False + self.excluded_methods: List[str] = [] + self.excluded_variables: List[str] = [] + self.return_type_excludes: List[str] = [] + + # Pointers + self.pointer_call_policy: str = "" + self.reference_call_policy: str = "" + self.smart_ptr_type: str = "" + + # Substitutions + self.template_substitutions: Dict[str, List[Any]] = [] + self.name_replacements: Dict[str, str] = { + "double": "Double", + "unsigned int": "Unsigned", + "Unsigned int": "Unsigned", + "unsigned": "Unsigned", + "double": "Double", + "std::vector": "Vector", + "std::pair": "Pair", + "std::map": "Map", + "std::string": "String", + "boost::shared_ptr": "SharedPtr", + "*": "Ptr", + "c_vector": "CVector", + "std::set": "Set", + } + + # Custom Code + self.extra_code: List[str] = [] + self.prefix_code: List[str] = [] + self.prefix_text: str = "" + self.custom_generator: str = "" + + self.custom_generator_instance: cppwg_custom.Custom = None + + if info_config: + for key in [ + "arg_type_excludes", + "calldef_excludes", + "constructor_arg_type_excludes", + "constructor_signature_excludes", + "custom_generator", + "excluded", + "excluded_methods", + "excluded_variables", + "extra_code", + "name_replacements", + "pointer_call_policy", + "prefix_code", + "prefix_text", + "reference_call_policy", + "return_type_excludes", + "smart_ptr_type", + "source_includes", + "source_root", + "template_substitutions", + ]: + if key in info_config: + setattr(self, key, info_config[key]) + + self.load_custom_generator() + + @property + @abstractmethod + def parent(self) -> Optional["BaseInfo"]: + """ + Returns this object's parent node in the info tree hierarchy. + + This property is supplied by subclasses e.g. a ModuleInfo's parent + is a PackageInfo, a ClassInfo's parent is a ModuleInfo etc. + + Returns + ------- + Optional[BaseInfo] + The parent node in the info tree hierarchy. + """ + pass + + def load_custom_generator(self) -> None: + """ + Check if a custom generator is specified and load it. + """ + if not self.custom_generator: + return + + logger = logging.getLogger() + + # Replace the `CPPWG_SOURCEROOT` placeholder in the custom generator + # path if needed. For example, a custom generator might be specified + # as `custom_generator: CPPWG_SOURCEROOT/path/to/CustomGenerator.py` + filepath = self.custom_generator.replace( + CPPWG_SOURCEROOT_STRING, self.source_root + ) + filepath = os.path.abspath(filepath) + + # Verify that the custom generator file exists + if not os.path.isfile(filepath): + logger.error( + f"Could not find specified custom generator for {self.name}: {filepath}" + ) + raise FileNotFoundError() + + logger.info(f"Custom generator for {self.name}: {filepath}") + + # Load the custom generator as a module + module_name = os.path.splitext(filepath)[0] # /path/to/CustomGenerator + class_name = os.path.basename(module_name) # CustomGenerator + + spec = importlib.util.spec_from_file_location(module_name, filepath) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + # Get the custom generator class from the loaded module. + # Note: The custom generator class name must match the filename. + CustomGeneratorClass: cppwg_custom.Custom = getattr(module, class_name) + + # Instantiate the custom generator from the provided class + self.custom_generator_instance = CustomGeneratorClass() + + def hierarchy_attribute(self, attribute_name: str) -> Any: + """ + Get the attribute value from this object or one further up the info tree. + + Ascend the info tree hierarchy searching for the attribute and return + the first value found for it. + + Parameters + ---------- + attribute_name : str + The attribute name to search for. + + Returns + ------- + Any + The attribute value, or None if not found. + """ + value = getattr(self, attribute_name, None) + if value: + return value + + if self.parent is None: + # Reached the top of the hierarchy (i.e. PackageInfo) + return None + + return self.parent.hierarchy_attribute(attribute_name) + + def hierarchy_attribute_gather(self, attribute_name: str) -> List[Any]: + """ + Get a list of attribute values from this object and others in the info tree. + + Ascend the info tree hierarchy searching for the attribute and return + a list of all the values found for it. + + Parameters + ---------- + attribute_name : str + The attribute name to search for. + + Returns + ------- + List[Any] + The list of attribute values. + """ + value_list: List[Any] = [] + + value = getattr(self, attribute_name, None) + if value: + value_list.append(value) + + if self.parent is None: + # Reached the top of the hierarchy (i.e. PackageInfo) + return value_list + + value_list.extend(self.parent.hierarchy_attribute_gather(attribute_name)) + return value_list diff --git a/cppwg/input/class_info.py b/cppwg/info/class_info.py similarity index 77% rename from cppwg/input/class_info.py rename to cppwg/info/class_info.py index c2edd6e..1388ce3 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/info/class_info.py @@ -8,31 +8,31 @@ from pygccxml.declarations.matchers import access_type_matcher_t from pygccxml.declarations.runtime_errors import declaration_not_found_t -from cppwg.input.cpp_type_info import CppTypeInfo +from cppwg.info.cpp_entity_info import CppEntityInfo from cppwg.utils import utils -class CppClassInfo(CppTypeInfo): +class CppClassInfo(CppEntityInfo): """ An information structure for individual C++ classes to be wrapped. Attributes ---------- + base_decls : pygccxml.declarations.declaration_t + Declarations for the base classes, one per template instantiation cpp_names : List[str] The C++ names of the class e.g. ["Foo<2,2>", "Foo<3,3>"] py_names : List[str] The Python names of the class e.g. ["Foo_2_2", "Foo_3_3"] - decls : pygccxml.declarations.declaration_t - Declarations for this type's base class, one per template instantiation """ def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): super().__init__(name, class_config) - self.cpp_names: List[str] = None - self.py_names: List[str] = None - self.base_decls: Optional[List["declaration_t"]] = None # noqa: F821 + self.base_decls: List["declaration_t"] = [] # noqa: F821 + self.cpp_names: List[str] = [] + self.py_names: List[str] = [] def extract_templates_from_source(self) -> None: """ @@ -47,13 +47,19 @@ def extract_templates_from_source(self) -> None: return # Skip if there is no source file - source_path = self.source_file_full_path + source_path = self.source_file_path if not source_path: return # Get list of template substitutions applicable to this class # e.g. [ {"signature":"", "replacement":[[2,2], [3,3]]} ] - substitutions = self.hierarchy_attribute_gather("template_substitutions") + substitutions = [ + ts_dict + for ts_dict_list in self.hierarchy_attribute_gather( + "template_substitutions" + ) + for ts_dict in ts_dict_list + ] # Skip if there are no applicable template substitutions if not substitutions: @@ -84,7 +90,6 @@ def extract_templates_from_source(self) -> None: self.template_arg_lists = substitution["replacement"] # Extract parameters ["A", "B"] from "" - self.template_params = [] for part in signature.split(","): param = ( part.strip() @@ -97,9 +102,9 @@ def extract_templates_from_source(self) -> None: self.template_params.append(param) break - def is_child_of(self, other: "ClassInfo") -> bool: # noqa: F821 + def extends(self, other: "ClassInfo") -> bool: # noqa: F821 """ - Check if the class is a child of the specified class. + Check if the class extends the specified class. Parameters ---------- @@ -109,7 +114,7 @@ def is_child_of(self, other: "ClassInfo") -> bool: # noqa: F821 Returns ------- bool - True if the class is a child of the specified class, False otherwise + True if the class extends the specified class, False otherwise """ if not self.base_decls: return False @@ -168,8 +173,6 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 if self.excluded: return - self.decls = [] - for class_cpp_name in self.cpp_names: class_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> @@ -177,10 +180,7 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 class_decl = source_ns.class_(class_name) except declaration_not_found_t as e1: - if ( - self.template_signature is None - or "=" not in self.template_signature - ): + if "=" not in self.template_signature: logger.error(f"Could not find declaration for class {class_name}") raise e1 @@ -212,9 +212,9 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 self.decls.append(class_decl) # Update the class source file if not already set - if not self.source_file_full_path: - self.source_file_full_path = self.decls[0].location.file_name - self.source_file = os.path.basename(self.source_file_full_path) + if not self.source_file_path: + self.source_file_path = self.decls[0].location.file_name + self.source_file = os.path.basename(self.source_file_path) # Update the base class declarations self.base_decls = [ @@ -235,18 +235,18 @@ def update_from_source(self, source_file_paths: List[str]) -> None: return # Attempt to map class to a source file - if self.source_file_full_path: - self.source_file = os.path.basename(self.source_file_full_path) + if self.source_file_path: + self.source_file = os.path.basename(self.source_file_path) else: for file_path in source_file_paths: file_name = os.path.basename(file_path) # Match file name if set if self.source_file == file_name: - self.source_file_full_path = file_path + self.source_file_path = file_path # Match class name, assuming the file name is the class name elif self.name == os.path.splitext(file_name)[0]: self.source_file = file_name - self.source_file_full_path = file_path + self.source_file_path = file_path # Extract template args from the source file self.extract_templates_from_source() @@ -258,42 +258,39 @@ def update_py_names(self) -> None: """ Set the Python names for the class, accounting for template args. - Set the name of the class as it will appear on the Python side. This - collapses template arguments, separating them by underscores and removes - special characters. The return type is a list, as a class can have - multiple names if it is templated. For example, a class "Foo" with - template arguments [[2, 2], [3, 3]] will have a python name list - ["Foo_2_2", "Foo_3_3"]. + Set the name(s) of the class as it should appear in Python. This + collapses template arguments, separates them by underscores, and removes + special characters. There can be multiple names, one for each template + class instantiation. For example, class "Foo" with template arguments + [[2, 2], [3, 3]] will have a Python name list ["Foo_2_2", "Foo_3_3"]. """ # Handles untemplated classes - if self.template_arg_lists is None: + if not self.template_arg_lists: if self.name_override: - self.py_names = [self.name_override] + self.py_names.append(self.name_override) else: - self.py_names = [self.name] + self.py_names.append(self.name) return - self.py_names = [] - # Table of special characters for removal rm_chars = {"<": None, ">": None, ",": None, " ": None} rm_table = str.maketrans(rm_chars) - # Clean the type name - type_name = self.name - if self.name_override is not None: - type_name = self.name_override + # Clean the class name + class_name = self.name + if self.name_override: + class_name = self.name_override # Do standard name replacements e.g. "unsigned int" -> "Unsigned" for name, replacement in self.name_replacements.items(): - type_name = type_name.replace(name, replacement) + class_name = class_name.replace(name, replacement) # Remove special characters - type_name = type_name.translate(rm_table) + class_name = class_name.translate(rm_table) # Capitalize the first letter e.g. "foo" -> "Foo" - if len(type_name) > 1: - type_name = type_name[0].capitalize() + type_name[1:] + if len(class_name) > 1: + class_name = class_name[0].capitalize() + class_name[1:] # Create a string of template args separated by "_" e.g. 2_2 for template_arg_list in self.template_arg_lists: @@ -319,24 +316,22 @@ def update_py_names(self) -> None: if idx < len(template_arg_list) - 1: template_string += "_" - self.py_names.append(type_name + "_" + template_string) + self.py_names.append(class_name + "_" + template_string) def update_cpp_names(self) -> None: """ Set the C++ names for the class, accounting for template args. - Set the name of the class as it should appear in C++. - The return type is a list, as a class can have multiple names - if it is templated. For example, a class "Foo" with - template arguments [[2, 2], [3, 3]] will have a C++ name list - ["Foo<2, 2>", "Foo<3, 3>"]. + Set the name(s) of the class as it appears in C++. There can be + multiple names, one for each template class instantiation. + For example, a class "Foo" with template arguments [[2, 2], [3, 3]] + will have a C++ name list ["Foo<2, 2>", "Foo<3, 3>"]. """ # Handles untemplated classes - if self.template_arg_lists is None: - self.cpp_names = [self.name] + if not self.template_arg_lists: + self.cpp_names.append(self.name) return - self.cpp_names = [] for template_arg_list in self.template_arg_lists: # Create template string from arg list e.g. [2, 2] -> "<2, 2>" template_string = ", ".join([str(arg) for arg in template_arg_list]) @@ -351,10 +346,3 @@ def update_names(self) -> None: """ self.update_cpp_names() self.update_py_names() - - @property - def parent(self) -> "ModuleInfo": # noqa: F821 - """ - Returns the parent module info object. - """ - return self.module_info diff --git a/cppwg/info/cpp_entity_info.py b/cppwg/info/cpp_entity_info.py new file mode 100644 index 0000000..bbc60f0 --- /dev/null +++ b/cppwg/info/cpp_entity_info.py @@ -0,0 +1,66 @@ +"""C++ entity information structure.""" + +from typing import Any, Dict, List, Optional + +from cppwg.info.base_info import BaseInfo + + +class CppEntityInfo(BaseInfo): + """ + An information structure for C++ entities including classes, free functions etc. + + Attributes + ---------- + name_override : str + The name override specified in config e.g. "CustomFoo" -> "Foo" + source_file : str + The source file containing the entity + source_file_path : str + The full path to the source file containing the entity + + module_info : ModuleInfo + The module info object that this entity belongs to + + decls : pygccxml.declarations.declaration_t + The pygccxml declarations associated with this entity, one per template arg if templated + template_arg_lists : List[List[Any]] + List of template replacement arguments e.g. [[2, 2], [3, 3]] + template_params : List[str] + List of template parameters e.g. ["DIM_A", "DIM_B"] + template_signature : str + The template signature of the entity e.g. "" + """ + + def __init__(self, name: str, entity_config: Optional[Dict[str, Any]] = None): + + super().__init__(name, entity_config) + + self.name_override: str = "" + self.source_file: str = "" + self.source_file_path: str = "" + + self.module_info: Optional["ModuleInfo"] = None # noqa: F821 + + self.decls: List["declaration_t"] = [] # noqa: F821 + self.template_arg_lists: List[List[Any]] = [] + self.template_params: List[str] = [] + self.template_signature: str = "" + + if entity_config: + for key in ["name_override", "source_file", "source_file_path"]: + if key in entity_config: + setattr(self, key, entity_config[key]) + + @property + def parent(self) -> "ModuleInfo": # noqa: F821 + """ + Returns the module info object that holds this entity. + """ + return self.module_info + + @parent.setter + def parent(self, module_info: "ModuleInfo") -> None: # noqa: F821 + """ + Set the module info object that holds this entity. + """ + self.module_info = module_info diff --git a/cppwg/input/free_function_info.py b/cppwg/info/free_function_info.py similarity index 75% rename from cppwg/input/free_function_info.py rename to cppwg/info/free_function_info.py index 5f69134..3c927a9 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/info/free_function_info.py @@ -2,10 +2,10 @@ from typing import Any, Dict, Optional -from cppwg.input.cpp_type_info import CppTypeInfo +from cppwg.info.cpp_entity_info import CppEntityInfo -class CppFreeFunctionInfo(CppTypeInfo): +class CppFreeFunctionInfo(CppEntityInfo): """An information structure for individual free functions to be wrapped.""" def __init__( @@ -14,11 +14,6 @@ def __init__( super().__init__(name, free_function_config) - @property - def parent(self) -> "ModuleInfo": # noqa: F821 - """Returns the parent module info object.""" - return self.module_info - def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ Update with information from the source namespace. diff --git a/cppwg/info/method_info.py b/cppwg/info/method_info.py new file mode 100644 index 0000000..d004903 --- /dev/null +++ b/cppwg/info/method_info.py @@ -0,0 +1,36 @@ +"""Method information structure.""" + +from typing import Optional + +from cppwg.info.cpp_entity_info import CppEntityInfo + + +class CppMethodInfo(CppEntityInfo): + """ + An information structure for individual methods to be wrapped. + + Attributes + ---------- + class_info : CppClassInfo + The class info object that holds this method. + """ + + def __init__(self, name: str, _) -> None: + + super().__init__(name) + + self.class_info: Optional["CppClassInfo"] = None # noqa: F821 + + @property + def parent(self) -> "CppClassInfo": # noqa: F821 + """ + Returns the class info object that holds this method. + """ + return self.class_info + + @parent.setter + def parent(self, class_info: "CppClassInfo") -> None: # noqa: F821 + """ + Set the class info object that holds this method. + """ + self.class_info = class_info diff --git a/cppwg/input/module_info.py b/cppwg/info/module_info.py similarity index 62% rename from cppwg/input/module_info.py rename to cppwg/info/module_info.py index 7a48e50..3060886 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/info/module_info.py @@ -4,9 +4,9 @@ from pathlib import Path from typing import Any, Dict, List, Optional -from cppwg.input.base_info import BaseInfo -from cppwg.input.class_info import CppClassInfo -from cppwg.input.free_function_info import CppFreeFunctionInfo +from cppwg.info.base_info import BaseInfo +from cppwg.info.class_info import CppClassInfo +from cppwg.info.free_function_info import CppFreeFunctionInfo class ModuleInfo(BaseInfo): @@ -15,48 +15,97 @@ class ModuleInfo(BaseInfo): Attributes ---------- - package_info : PackageInfo - The package info parent object associated with this module source_locations : List[str] A list of source locations for this module - class_info_collection : List[CppClassInfo] - A list of class info objects associated with this module - free_function_info_collection : List[CppFreeFunctionInfo] - A list of free function info objects associated with this module - variable_info_collection : List[CppFreeFunctionInfo] - A list of variable info objects associated with this module use_all_classes : bool Use all classes in the module use_all_free_functions : bool Use all free functions in the module use_all_variables : bool Use all variables in the module + + package_info : PackageInfo + The package info object this module belongs to + + class_collection : List[CppClassInfo] + A list of class info objects that belong to this module + free_function_collection : List[CppFreeFunctionInfo] + A list of free function info objects that belong to this module + variable_collection : List[CppFreeFunctionInfo] + A list of variable info objects that belong to this module """ - def __init__(self, name: str, module_config: Optional[Dict[str, Any]] = None): + def __init__( + self, name: str, module_config: Optional[Dict[str, Any]] = None + ) -> None: + """ + Create a module info object from a module_config dict. - super().__init__(name) + Parameters + ---------- + name : str + The name of the module + module_config : Dict[str, Any] + A dictionary of module configuration settings + """ + super().__init__(name, module_config) - self.package_info: Optional["PackageInfo"] = None # noqa: F821 - self.source_locations: List[str] = None - self.class_info_collection: List["CppClassInfo"] = [] # noqa: F821 - self.free_function_info_collection: List["CppFreeFunctionInfo"] = [] # fmt: skip # noqa: F821 - self.variable_info_collection: List["CppFreeFunctionInfo"] = [] # noqa: F821 + self.source_locations: List[str] = [] self.use_all_classes: bool = False self.use_all_free_functions: bool = False self.use_all_variables: bool = False + self.package_info: Optional["PackageInfo"] = None # noqa: F821 + + self.class_collection: List[CppClassInfo] = [] + self.free_function_collection: List[CppFreeFunctionInfo] = [] + self.variable_collection: List["CppVariableInfo"] = [] # noqa: F821 + if module_config: - for key, value in module_config.items(): - setattr(self, key, value) + for key in [ + "source_locations", + "use_all_classes", + "use_all_free_functions", + "use_all_variables", + ]: + if key in module_config: + setattr(self, key, module_config[key]) @property def parent(self) -> "PackageInfo": # noqa: F821 """ - Returns the associated package info object. + Returns the package info object that holds this module info object. """ return self.package_info + @parent.setter + def parent(self, package_info: "PackageInfo") -> None: # noqa: F821 + """ + Set the package info object that holds this module. + """ + self.package_info = package_info + + def add_class(self, class_info: CppClassInfo) -> None: + """ + Add a class info object to the module. + """ + self.class_collection.append(class_info) + class_info.parent = self + + def add_free_function(self, free_function_info: CppFreeFunctionInfo) -> None: + """ + Add a free function info object to the module. + """ + self.free_function_collection.append(free_function_info) + free_function_info.parent = self + + def add_variable(self, variable_info: "CppVariableInfo") -> None: # noqa: F821 + """ + Add a variable info object to the module. + """ + self.variable_collection.append(variable_info) + variable_info.parent = self + def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 """ Check if the declaration is associated with a file in the specified source paths. @@ -71,7 +120,7 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 bool True if the declaration is associated with a file in a specified source path """ - if self.source_locations is None: + if not self.source_locations: return True for source_location in self.source_locations: @@ -109,12 +158,12 @@ def compare(a: CppClassInfo, b: CppClassInfo) -> int: a_req_b = a.requires(b) b_req_a = b.requires(a) - if a.is_child_of(b) or (a_req_b and not b_req_a): + if a.extends(b) or (a_req_b and not b_req_a): # a comes after b (ignore cyclic dependencies) cache[(a, b)] = 1 cache[(b, a)] = -1 return 1 - elif b.is_child_of(a) or (b_req_a and not a_req_b): + elif b.extends(a) or (b_req_a and not a_req_b): # a comes before b (ignore cyclic dependencies) cache[(a, b)] = -1 cache[(b, a)] = 1 @@ -125,19 +174,19 @@ def compare(a: CppClassInfo, b: CppClassInfo) -> int: cache[(b, a)] = 0 return 0 - self.class_info_collection.sort( - key=lambda x: (os.path.dirname(x.source_file_full_path), x.name) + self.class_collection.sort( + key=lambda x: (os.path.dirname(x.source_file_path), x.name) ) i = 0 - n = len(self.class_info_collection) + n = len(self.class_collection) while i < n - 1: - cls_i = self.class_info_collection[i] + cls_i = self.class_collection[i] ii = i # Tracks destination of cls_i j_pos = [] # Tracks positions of cls_i's dependents for j in range(i + 1, n): - cls_j = self.class_info_collection[j] + cls_j = self.class_collection[j] order = compare(cls_i, cls_j) if order == 1: # Position cls_i after all classes it depends on @@ -151,15 +200,15 @@ def compare(a: CppClassInfo, b: CppClassInfo) -> int: continue # No change in position # Move cls_i into new position ii - cls_i = self.class_info_collection.pop(i) - self.class_info_collection.insert(ii, cls_i) + cls_i = self.class_collection.pop(i) + self.class_collection.insert(ii, cls_i) # Move dependents into positions after ii for idx, j in enumerate(j_pos): if j > ii: break # Rest of dependents are already positioned after ii - cls_j = self.class_info_collection.pop(j - 1 - idx) - self.class_info_collection.insert(ii + idx, cls_j) + cls_j = self.class_collection.pop(j - 1 - idx) + self.class_collection.insert(ii + idx, cls_j) def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ @@ -180,10 +229,10 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 class_info = CppClassInfo(class_decl.name) class_info.update_names() class_info.module_info = self - self.class_info_collection.append(class_info) + self.class_collection.append(class_info) # Update classes with information from source namespace. - for class_info in self.class_info_collection: + for class_info in self.class_collection: class_info.update_from_ns(source_ns) # Sort classes by dependence @@ -198,10 +247,10 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 if self.is_decl_in_source_path(free_function): ff_info = CppFreeFunctionInfo(free_function.name) ff_info.module_info = self - self.free_function_info_collection.append(ff_info) + self.free_function_collection.append(ff_info) # Update free functions with information from source namespace. - for ff_info in self.free_function_info_collection: + for ff_info in self.free_function_collection: ff_info.update_from_ns(source_ns) def update_from_source(self, source_file_paths: List[str]) -> None: @@ -213,5 +262,8 @@ def update_from_source(self, source_file_paths: List[str]) -> None: source_files : List[str] A list of source file paths. """ - for class_info in self.class_info_collection: + for class_info in self.class_collection: class_info.update_from_source(source_file_paths) + + self.class_collection.sort(key=lambda x: x.name) + self.free_function_collection.sort(key=lambda x: x.name) diff --git a/cppwg/input/package_info.py b/cppwg/info/package_info.py similarity index 66% rename from cppwg/input/package_info.py rename to cppwg/info/package_info.py index b31ca76..0ca426d 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/info/package_info.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional -from cppwg.input.base_info import BaseInfo +from cppwg.info.base_info import BaseInfo from cppwg.utils.constants import CPPWG_EXT @@ -16,65 +16,85 @@ class PackageInfo(BaseInfo): Attributes ---------- + common_include_file : bool + Use a common include file for all source files + exclude_default_args : bool + Exclude default arguments from method wrappers. name : str The name of the package - source_locations : List[str] - A list of source locations for this package - module_info_collection : List[ModuleInfo] - A list of module info objects associated with this package - source_root : str - The root directory of the C++ source code source_hpp_patterns : List[str] A list of source file patterns to include + + module_collection : List[ModuleInfo] + A list of module info objects associated with this package source_hpp_files : List[str] A list of source file names to include - common_include_file : bool - Use a common include file for all source files - exclude_default_args : bool - Exclude default arguments from method wrappers. """ def __init__( - self, - name: str, - source_root: str, - package_config: Optional[Dict[str, Any]] = None, + self, name: str, package_config: Optional[Dict[str, Any]] = None ) -> None: """ - Create a package info object from a package_config. - - The package_config is a dictionary of package configuration settings - extracted from a yaml input file. + Create a package info object from a package_config dict. Parameters ---------- name : str The name of the package - source_root : str - The root directory of the C++ source code package_config : Dict[str, Any] A dictionary of package configuration settings """ - super().__init__(name) + super().__init__(name, package_config) - self.name: str = name - self.source_locations: List[str] = None - self.module_info_collection: List["ModuleInfo"] = [] # noqa: F821 - self.source_root: str = source_root - self.source_hpp_patterns: List[str] = ["*.hpp"] - self.source_hpp_files: List[str] = [] self.common_include_file: bool = False self.exclude_default_args: bool = False + self.source_hpp_patterns: List[str] = ["*.hpp"] + + self.module_collection: List["ModuleInfo"] = [] # noqa: F821 + self.source_hpp_files: List[str] = [] if package_config: - for key, value in package_config.items(): - setattr(self, key, value) + self.common_include_file = package_config.get( + "common_include_file", self.common_include_file + ) + self.exclude_default_args = package_config.get( + "exclude_default_args", self.exclude_default_args + ) + self.source_hpp_patterns = package_config.get( + "source_hpp_patterns", self.source_hpp_patterns + ) @property def parent(self) -> None: - """Returns None as this is the top level object in the hierarchy.""" + """ + Returns None, as this is the top level of the info tree hierarchy. + """ return None + def add_module(self, module_info: "ModuleInfo") -> None: # noqa: F821 + """ + Add a module info object to the package. + + Parameters + ---------- + module_info : ModuleInfo + The module info object to add + """ + self.module_collection.append(module_info) + module_info.parent = self + + def init(self, restricted_paths: List[str]) -> None: + """ + Initialise - collect header files and update info. + + Parameters + ---------- + restricted_paths : List[str] + A list of restricted paths to skip when collecting header files. + """ + self.collect_source_headers(restricted_paths) + self.update_from_source() + def collect_source_headers(self, restricted_paths: List[str]) -> None: """ Collect header files from the source root. @@ -116,9 +136,9 @@ def collect_source_headers(self, restricted_paths: List[str]) -> None: def update_from_source(self) -> None: """ - Update modules with information from the source headers. + Update with data from the source headers. """ - for module_info in self.module_info_collection: + for module_info in self.module_collection: module_info.update_from_source(self.source_hpp_files) def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 @@ -130,5 +150,5 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 source_ns : pygccxml.declarations.namespace_t The source namespace """ - for module_info in self.module_info_collection: + for module_info in self.module_collection: module_info.update_from_ns(source_ns) diff --git a/cppwg/input/variable_info.py b/cppwg/info/variable_info.py similarity index 76% rename from cppwg/input/variable_info.py rename to cppwg/info/variable_info.py index 541c246..8e58f93 100644 --- a/cppwg/input/variable_info.py +++ b/cppwg/info/variable_info.py @@ -2,10 +2,10 @@ from typing import Any, Dict, Optional -from cppwg.input.cpp_type_info import CppTypeInfo +from cppwg.info.cpp_entity_info import CppEntityInfo -class CppVariableInfo(CppTypeInfo): +class CppVariableInfo(CppEntityInfo): """An information structure for individual variables to be wrapped.""" def __init__(self, name: str, variable_config: Optional[Dict[str, Any]] = None): diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py deleted file mode 100644 index 81f39af..0000000 --- a/cppwg/input/base_info.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Generic information structure.""" - -from typing import Any, Dict, List, Optional - - -class BaseInfo: - """ - A generic information structure for features. - - Features include packages, modules, classes, free functions, etc. - Information structures are used to store information about the features. The - information structures for each feature type inherit from BaseInfo, which - sets a number of default attributes common to all features. - - Attributes - ---------- - name : str - The feature name, as it appears in its definition. - source_includes : List[str] - A list of source files to be included with the feature. - calldef_excludes : List[str] - Do not include calldefs matching these patterns. - smart_ptr_type : str, optional - Handle classes with this smart pointer type. - template_substitutions : Dict[str, List[Any]] - A list of template substitution sequences. - pointer_call_policy : str, optional - The default pointer call policy. - reference_call_policy : str, optional - The default reference call policy. - extra_code : List[str] - Any extra wrapper code for the feature. - prefix_code : List[str] - Any wrapper code that precedes the feature. - prefix_text : str, optional - Text to add at the top of all wrappers. - custom_generator : str, optional - A custom generator for the feature. - excluded: bool - Exclude this feature. - excluded_methods : List[str] - Do not include these methods. - excluded_variables : List[str] - Do not include these variables. - arg_type_excludes : List[str] - List of exclude patterns for arg types in methods. - constructor_arg_type_excludes : List[str] - List of exclude patterns for arg types in constructors. - constructor_signature_excludes : List[List[str]] - List of exclude patterns for constructor signatures. - return_type_excludes : List[str] - List of exclude patterns for return types. - name_replacements : Dict[str, str] - A dictionary of name replacements e.g. {"double":"Double", "unsigned - int":"Unsigned"} - """ - - def __init__(self, name): - self.name: str = name - self.source_includes: List[str] = [] - self.calldef_excludes: List[str] = [] - self.smart_ptr_type: Optional[str] = None - self.template_substitutions: Dict[str, List[Any]] = [] - self.pointer_call_policy: Optional[str] = None - self.reference_call_policy: Optional[str] = None - self.extra_code: List[str] = [] - self.prefix_code: List[str] = [] - self.custom_generator: Optional[str] = None - self.excluded = False - self.excluded_methods: List[str] = [] - self.excluded_variables: List[str] = [] - self.arg_type_excludes: List[str] = [] - self.constructor_arg_type_excludes: List[str] = [] - self.constructor_signature_excludes: List[List[str]] = [] - self.return_type_excludes: List[str] = [] - self.name_replacements: Dict[str, str] = { - "double": "Double", - "unsigned int": "Unsigned", - "Unsigned int": "Unsigned", - "unsigned": "Unsigned", - "double": "Double", - "std::vector": "Vector", - "std::pair": "Pair", - "std::map": "Map", - "std::string": "String", - "boost::shared_ptr": "SharedPtr", - "*": "Ptr", - "c_vector": "CVector", - "std::set": "Set", - } - - @property - def parent(self) -> Optional["BaseInfo"]: - """ - Get this object's parent. - - Return the parent object of the feature in the hierarchy. This is - overriden in subclasses e.g. ModuleInfo returns a PackageInfo, ClassInfo - returns a ModuleInfo, etc. - - Returns - ------- - Optional[BaseInfo] - The parent object. - """ - return None - - def hierarchy_attribute(self, attribute_name: str) -> Any: - """ - Get the attribute value from this object or one of its parents. - - For the supplied attribute, iterate through parent objects until a non-None - value is found. If the top level parent (i.e. package) attribute is - None, return None. - - Parameters - ---------- - attribute_name : str - The attribute name to search for. - - Returns - ------- - Any - The attribute value. - """ - if hasattr(self, attribute_name) and getattr(self, attribute_name) is not None: - return getattr(self, attribute_name) - - if hasattr(self, "parent") and self.parent is not None: - return self.parent.hierarchy_attribute(attribute_name) - - return None - - def hierarchy_attribute_gather(self, attribute_name: str) -> List[Any]: - """ - Get a list of attribute values from this object and its parents. - - For the supplied attribute, iterate through parent objects gathering list entries. - - Parameters - ---------- - attribute_name : str - The attribute name to search for. - - Returns - ------- - List[Any] - The list of attribute values. - """ - att_list: List[Any] = [] - - if hasattr(self, attribute_name) and getattr(self, attribute_name) is not None: - att_list.extend(getattr(self, attribute_name)) - - if hasattr(self, "parent") and self.parent is not None: - att_list.extend(self.parent.hierarchy_attribute_gather(attribute_name)) - - return att_list diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py deleted file mode 100644 index dd846eb..0000000 --- a/cppwg/input/cpp_type_info.py +++ /dev/null @@ -1,47 +0,0 @@ -"""C++ type information structure.""" - -from typing import Any, Dict, List, Optional - -from cppwg.input.base_info import BaseInfo - - -class CppTypeInfo(BaseInfo): - """ - An information structure for C++ types including classes, free functions etc. - - Attributes - ---------- - module_info : ModuleInfo - The module info parent object associated with this type - source_file : str - The source file containing the type - source_file_full_path : str - The full path to the source file containing the type - name_override : str - The name override specified in config e.g. "CustomFoo" -> "Foo" - template_signature : str - The template signature of the type e.g. "" - template_params : List[str] - List of template parameters e.g. ["DIM_A", "DIM_B"] - template_arg_lists : List[List[Any]] - List of template replacement arguments e.g. [[2, 2], [3, 3]] - decls : pygccxml.declarations.declaration_t - The pygccxml declarations associated with this type, one per template arg if templated - """ - - def __init__(self, name: str, type_config: Optional[Dict[str, Any]] = None): - - super().__init__(name) - - self.module_info: Optional["ModuleInfo"] = None # noqa: F821 - self.source_file_full_path: str = "" - self.source_file: str = "" - self.name_override: Optional[str] = None - self.template_signature: Optional[str] = None - self.template_params: Optional[List[str]] = None - self.template_arg_lists: Optional[List[List[Any]]] = None - self.decls: Optional[List["declaration_t"]] = None # noqa: F821 - - if type_config: - for key, value in type_config.items(): - setattr(self, key, value) diff --git a/cppwg/input/method_info.py b/cppwg/input/method_info.py deleted file mode 100644 index 2077d3b..0000000 --- a/cppwg/input/method_info.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Method information structure.""" - -from typing import Optional - -from cppwg.input.cpp_type_info import CppTypeInfo - - -class CppMethodInfo(CppTypeInfo): - """ - An information structure for individual methods to be wrapped. - - Attributes - ---------- - class_info : CppClassInfo - The class info parent object associated with this method - """ - - def __init__(self, name: str, _): - - super().__init__(name) - - self.class_info: Optional["CppClassInfo"] = None # noqa: F821 - - @property - def parent(self) -> "CppClassInfo": # noqa: F821 - """Returns the parent class info object.""" - return self.class_info diff --git a/cppwg/parsers/package_info_parser.py b/cppwg/parsers/package_info_parser.py index e2b62f7..38b6dfc 100644 --- a/cppwg/parsers/package_info_parser.py +++ b/cppwg/parsers/package_info_parser.py @@ -1,21 +1,16 @@ """Parser for input yaml.""" -import importlib.util import logging -import os -import sys -from typing import Any, Dict, Optional +from typing import Any, Dict import yaml -import cppwg.templates.custom -from cppwg.input.base_info import BaseInfo -from cppwg.input.class_info import CppClassInfo -from cppwg.input.free_function_info import CppFreeFunctionInfo -from cppwg.input.module_info import ModuleInfo -from cppwg.input.package_info import PackageInfo +from cppwg.info.class_info import CppClassInfo +from cppwg.info.free_function_info import CppFreeFunctionInfo +from cppwg.info.module_info import ModuleInfo +from cppwg.info.package_info import PackageInfo +from cppwg.info.variable_info import CppVariableInfo from cppwg.utils import utils -from cppwg.utils.constants import CPPWG_SOURCEROOT_STRING class PackageInfoParser: @@ -24,76 +19,15 @@ class PackageInfoParser: Attributes ---------- - input_filepath : str - The path to the package info yaml file + config_file : str + The path to the package info yaml config file source_root : str The root directory of the C++ source code - raw_package_info : Dict[str, Any] - Raw info from the yaml file - package_info : Optional[PackageInfo] - The parsed package info """ - def __init__(self, input_filepath: str, source_root: str): - self.input_filepath: str = input_filepath - self.source_root: str = source_root - - # For holding raw info from the yaml file - self.raw_package_info: Dict[str, Any] = {} - - # The parsed package info - self.package_info: Optional[PackageInfo] = None - - def check_for_custom_generators(self, info: BaseInfo) -> None: - """ - Check if a custom generator is specified and load it into a module. - - Parameters - ---------- - info : BaseInfo - The info object to check for a custom generator - might be info - about a package, module, class, or free function. - """ - logger = logging.getLogger() - - if not info.custom_generator: - return - - # Replace the `CPPWG_SOURCEROOT` placeholder in the custom generator - # string if needed. For example, a custom generator might be specified - # as `custom_generator: CPPWG_SOURCEROOT/path/to/CustomGenerator.py` - filepath: str = info.custom_generator.replace( - CPPWG_SOURCEROOT_STRING, self.source_root - ) - filepath = os.path.abspath(filepath) - - # Verify that the custom generator file exists - if not os.path.isfile(filepath): - logger.error( - f"Could not find specified custom generator for {info.name}: {filepath}" - ) - raise FileNotFoundError() - - logger.info(f"Custom generator for {info.name}: {filepath}") - - # Load the custom generator as a module - module_name: str = os.path.splitext(filepath)[0] # /path/to/CustomGenerator - class_name: str = os.path.basename(module_name) # CustomGenerator - - spec = importlib.util.spec_from_file_location(module_name, filepath) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - - # Get the custom generator class from the loaded module. - # Note: The custom generator class name must match the filename. - CustomGeneratorClass: cppwg.templates.custom.Custom = getattr( - module, class_name - ) - - # Replace the `info.custom_generator` string with a new object created - # from the provided custom generator class - info.custom_generator = CustomGeneratorClass() + def __init__(self, config_file: str, source_root: str): + self.config_file = config_file + self.source_root = source_root def parse(self) -> PackageInfo: """ @@ -110,26 +44,31 @@ def parse(self) -> PackageInfo: logger = logging.getLogger() logger.info("Parsing package info file.") - with open(self.input_filepath, "r") as input_filepath: - self.raw_package_info = yaml.safe_load(input_filepath) + # Load raw info from the yaml file + raw_package_info: Dict[str, Any] = {} - # Default config options that apply to the package, modules, classes, and free functions - global_config: Dict[str, Any] = { - "source_includes": [], - "smart_ptr_type": None, - "calldef_excludes": None, - "return_type_excludes": None, - "template_substitutions": [], - "pointer_call_policy": None, - "reference_call_policy": None, - "constructor_arg_type_excludes": None, - "constructor_signature_excludes": None, + with open(self.config_file, "r") as config_file: + raw_package_info = yaml.safe_load(config_file) + + # Base config options that apply to package, modules, classes, etc. + base_config: Dict[str, Any] = { + "calldef_excludes": "", + "constructor_arg_type_excludes": "", + "constructor_signature_excludes": "", + "custom_generator": "", "excluded": False, "excluded_methods": [], "excluded_variables": [], - "custom_generator": None, + "extra_code": [], + "pointer_call_policy": "", "prefix_code": [], "prefix_text": "", + "reference_call_policy": "", + "return_type_excludes": "", + "smart_ptr_type": "", + "source_includes": [], + "source_root": self.source_root, + "template_substitutions": [], } # Get package config from the raw package info @@ -139,35 +78,33 @@ def parse(self) -> PackageInfo: "exclude_default_args": False, "source_hpp_patterns": ["*.hpp"], } - package_config.update(global_config) + package_config.update(base_config) for key in package_config.keys(): - if key in self.raw_package_info: - package_config[key] = self.raw_package_info[key] + if key in raw_package_info: + package_config[key] = raw_package_info[key] # Replace boolean strings with booleans utils.substitute_bool_for_string(package_config, "common_include_file") utils.substitute_bool_for_string(package_config, "exclude_default_args") # Create the PackageInfo object from the package config dict - self.package_info = PackageInfo( - package_config["name"], self.source_root, package_config - ) - self.check_for_custom_generators(self.package_info) + package_info = PackageInfo(package_config["name"], package_config) # Parse the module data - for raw_module_info in self.raw_package_info["modules"]: + for raw_module_info in raw_package_info["modules"]: # Get module config from the raw module info module_config = { "name": "cppwg_module", - "source_locations": None, + "source_locations": "", + "use_all_classes": False, + "use_all_free_functions": False, + "use_all_variables": False, "classes": [], "free_functions": [], "variables": [], - "use_all_classes": False, - "use_all_free_functions": False, } - module_config.update(global_config) + module_config.update(base_config) for key in module_config.keys(): if key in raw_module_info: @@ -187,11 +124,9 @@ def parse(self) -> PackageInfo: # Create the ModuleInfo object from the module config dict module_info = ModuleInfo(module_config["name"], module_config) - self.check_for_custom_generators(module_info) - # Connect the module to the package - module_info.package_info = self.package_info - self.package_info.module_info_collection.append(module_info) + # Add the module to the package + package_info.add_module(module_info) # Parse the class data and create class info objects. # Note: if module_config["use_all_classes"] == True, class info @@ -200,8 +135,12 @@ def parse(self) -> PackageInfo: if module_config["classes"]: for raw_class_info in module_config["classes"]: # Get class config from the raw class info - class_config = {"name_override": None, "source_file": None} - class_config.update(global_config) + class_config = { + "name_override": "", + "source_file": "", + "source_file_path": "", + } + class_config.update(base_config) for key in class_config.keys(): if key in raw_class_info: @@ -209,13 +148,9 @@ def parse(self) -> PackageInfo: # Create the CppClassInfo object from the class config dict class_info = CppClassInfo(raw_class_info["name"], class_config) - self.check_for_custom_generators(class_info) - # Connect the class to the module - class_info.module_info = module_info - module_info.class_info_collection.append(class_info) - - module_info.class_info_collection.sort(key=lambda x: x.name) + # Add the class to the module + module_info.add_class(class_info) # Parse the free function data and create free function info objects. # Note: if module_config["use_all_free_functions"] == True, free function @@ -225,10 +160,11 @@ def parse(self) -> PackageInfo: for raw_free_function_info in module_config["free_functions"]: # Get free function config from the raw free function info free_function_config = { - "name_override": None, - "source_file": None, + "name_override": "", + "source_file": "", + "source_file_path": "", } - free_function_config.update(global_config) + free_function_config.update(base_config) for key in free_function_config.keys(): if key in raw_free_function_info: @@ -239,32 +175,31 @@ def parse(self) -> PackageInfo: free_function_config["name"], free_function_config ) - # Connect the free function to the module - free_function_info.module_info = module_info - module_info.free_function_info_collection.append( - free_function_info - ) - - module_info.free_function_info_collection.sort(key=lambda x: x.name) + # Add the free function to the module + module_info.add_free_function(free_function_info) # Parse the variable data if not module_config["use_all_variables"]: - for raw_variable_info in module_config["variables"]: - # Get variable config from the raw variable info - variable_config = {"name_override": None, "source_file": None} - variable_config.update(global_config) + if module_config["variables"]: + for raw_variable_info in module_config["variables"]: + # Get variable config from the raw variable info + variable_config = { + "name_override": "", + "source_file": "", + "source_file_path": "", + } + variable_config.update(base_config) - for key in variable_config.keys(): - if key in raw_variable_info: - variable_config[key] = raw_variable_info[key] + for key in variable_config.keys(): + if key in raw_variable_info: + variable_config[key] = raw_variable_info[key] - # Create the CppFreeFunctionInfo object from the variable config dict - variable_info = CppFreeFunctionInfo( - variable_config["name"], variable_config - ) + # Create the CppVariableInfo object from the variable config dict + variable_info = CppVariableInfo( + variable_config["name"], variable_config + ) - # Connect the variable to the module - variable_info.module_info = module_info - module_info.variable_info_collection.append(variable_info) + # Add the variable to the module + module_info.add_variable(variable_info) - return self.package_info + return package_info diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index ff4a73c..211b797 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -2,7 +2,7 @@ import logging from pathlib import Path -from typing import List, Optional +from typing import List from pygccxml import declarations, parser from pygccxml.declarations import declaration_t @@ -28,20 +28,20 @@ class CppSourceParser: Attributes ---------- - source_root : str - The root directory of the source code - wrapper_header_collection : str - The path to the header collection file - castxml_binary : str - The path to the CastXML binary - source_includes : List[str] - The list of source include paths castxml_cflags : str Optional cflags to be passed to CastXML e.g. "-std=c++17" + castxml_binary : str + The path to the CastXML binary global_ns : namespace_t The namespace containing all parsed C++ declarations + source_includes : List[str] + The list of source include paths source_ns : namespace_t The namespace containing C++ declarations from the source tree + source_root : str + The root directory of the source code + wrapper_header_collection : str + The path to the header collection file """ def __init__( @@ -58,9 +58,6 @@ def __init__( self.source_includes: List[str] = source_includes self.castxml_cflags: str = castxml_cflags - self.source_ns: Optional[namespace_t] = None - self.global_ns: Optional[namespace_t] = None - def parse(self) -> namespace_t: """ Parse the C++ source code from the header collection using CastXML and pygccxml. @@ -89,12 +86,12 @@ def parse(self) -> namespace_t: ) # Get access to the global namespace - self.global_ns: namespace_t = declarations.get_global_namespace(decls) + global_ns: namespace_t = declarations.get_global_namespace(decls) # Filter declarations for which files exist logger.info("Filtering source declarations.") query = declarations.custom_matcher_t(lambda decl: decl.location is not None) - filtered_decls: mdecl_wrapper_t = self.global_ns.decls(function=query) + filtered_decls: mdecl_wrapper_t = global_ns.decls(function=query) # Filter declarations in our source tree; include declarations from the # wrapper_header_collection file for explicit instantiations, typedefs etc. @@ -106,10 +103,10 @@ def parse(self) -> namespace_t: ] # Create a source namespace module for the filtered declarations - self.source_ns = namespace_t(name="source", declarations=source_decls) + source_ns = namespace_t(name="source", declarations=source_decls) # Initialise the source namespace's internal type hash tables for faster queries logger.info("Optimizing source declaration queries.") - self.source_ns.init_optimizer() + source_ns.init_optimizer() - return self.source_ns + return source_ns diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 8ca5310..50889e3 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -70,7 +70,9 @@ def add_hpp(self, class_py_name: str) -> None: The Python name of the class e.g. Foo_2_2 """ # Add the top prefix text - self.hpp_string += self.class_info.module_info.package_info.prefix_text + "\n" + prefix_text = self.class_info.hierarchy_attribute("prefix_text") + if prefix_text: + self.hpp_string += prefix_text + "\n" # Add the header guard, includes and declarations class_hpp_dict = {"class_py_name": class_py_name} @@ -91,7 +93,9 @@ def add_cpp_header(self, class_cpp_name: str, class_py_name: str) -> None: The Python name of the class e.g. Foo_2_2 """ # Add the top prefix text - self.cpp_string += self.class_info.module_info.package_info.prefix_text + "\n" + prefix_text = self.class_info.hierarchy_attribute("prefix_text") + if prefix_text: + self.cpp_string += prefix_text + "\n" # Add the includes for this class includes = "" @@ -100,9 +104,13 @@ def add_cpp_header(self, class_cpp_name: str, class_py_name: str) -> None: includes += f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n' else: - source_includes = self.class_info.hierarchy_attribute_gather( - "source_includes" - ) + source_includes = [ + inc + for inc_list in self.class_info.hierarchy_attribute_gather( + "source_includes" + ) + for inc in inc_list + ] for source_include in source_includes: if source_include[0] == "<": @@ -146,10 +154,9 @@ def add_cpp_header(self, class_cpp_name: str, class_py_name: str) -> None: self.cpp_string += code_line + "\n" # Run any custom generators to add additional prefix code - if self.class_info.custom_generator: - self.cpp_string += self.class_info.custom_generator.get_class_cpp_pre_code( - class_py_name - ) + generator = self.class_info.custom_generator_instance + if generator: + self.cpp_string += generator.get_class_cpp_pre_code(class_py_name) def add_virtual_overrides( self, template_idx: int @@ -158,7 +165,7 @@ def add_virtual_overrides( Add virtual "trampoline" overrides for the class. Identify any methods needing overrides (i.e. any that are virtual in the - current class or in a parent), and add the overrides to the cpp string. + current class or in a base class), and add the overrides to the cpp string. Parameters ---------- @@ -353,12 +360,9 @@ def write(self, work_dir: str) -> None: self.cpp_string += method_writer.generate_wrapper() # Run any custom generators to add additional class code - if self.class_info.custom_generator: - self.cpp_string += ( - self.class_info.custom_generator.get_class_cpp_def_code( - class_py_name - ) - ) + generator = self.class_info.custom_generator_instance + if generator: + self.cpp_string += generator.get_class_cpp_def_code(class_py_name) # Close the class definition self.cpp_string += " ;\n}\n" diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index b2aff6c..02073ba 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -107,29 +107,39 @@ def exclude(self) -> bool: # Exclude constructors with args matching patterns in calldef_excludes calldef_excludes = [ - x.replace(" ", "") - for x in self.class_info.hierarchy_attribute_gather("calldef_excludes") + ex + for ex_list in self.class_info.hierarchy_attribute_gather( + "calldef_excludes" + ) + for ex in ex_list ] + calldef_excludes = [ex.replace(" ", "") for ex in calldef_excludes] for arg_type in arg_types: if arg_type in calldef_excludes: return True # Exclude constructors with args matching patterns in constructor_arg_type_excludes ctor_arg_type_excludes = [ - x.replace(" ", "") - for x in self.class_info.hierarchy_attribute_gather( + ex + for ex_list in self.class_info.hierarchy_attribute_gather( "constructor_arg_type_excludes" ) + for ex in ex_list ] + ctor_arg_type_excludes = [ex.replace(" ", "") for ex in ctor_arg_type_excludes] for exclude_type in ctor_arg_type_excludes: for arg_type in arg_types: if exclude_type in arg_type: return True # Exclude constructors matching a signature in constructor_signature_excludes - ctor_signature_excludes = self.class_info.hierarchy_attribute_gather( - "constructor_signature_excludes" - ) + ctor_signature_excludes = [ + ex + for ex_list in self.class_info.hierarchy_attribute_gather( + "constructor_signature_excludes" + ) + for ex in ex_list + ] for exclude_types in ctor_signature_excludes: if len(exclude_types) != len(arg_types): diff --git a/cppwg/writers/free_function_writer.py b/cppwg/writers/free_function_writer.py index 2c87ef4..de02532 100644 --- a/cppwg/writers/free_function_writer.py +++ b/cppwg/writers/free_function_writer.py @@ -2,7 +2,7 @@ from typing import Dict, List -from cppwg.input.free_function_info import CppFreeFunctionInfo +from cppwg.info.free_function_info import CppFreeFunctionInfo from cppwg.writers.base_writer import CppBaseWrapperWriter diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index 6ddc5c3..838e0cd 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -3,9 +3,9 @@ import os from typing import Dict -from cppwg.input.class_info import CppClassInfo -from cppwg.input.free_function_info import CppFreeFunctionInfo -from cppwg.input.package_info import PackageInfo +from cppwg.info.class_info import CppClassInfo +from cppwg.info.free_function_info import CppFreeFunctionInfo +from cppwg.info.package_info import PackageInfo class CppHeaderCollectionWriter: @@ -49,11 +49,11 @@ def __init__( self.class_dict: Dict[str, CppClassInfo] = {} self.free_func_dict: Dict[str, CppFreeFunctionInfo] = {} - for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: + for module_info in self.package_info.module_collection: + for class_info in module_info.class_collection: self.class_dict[class_info.name] = class_info - for free_function_info in module_info.free_function_info_collection: + for free_function_info in module_info.free_function_collection: self.free_func_dict[free_function_info.name] = free_function_info def should_include_all(self) -> bool: @@ -65,7 +65,7 @@ def should_include_all(self) -> bool: bool """ # True if any module uses all classes or all free functions - for module_info in self.package_info.module_info_collection: + for module_info in self.package_info.module_collection: if module_info.use_all_classes or module_info.use_all_free_functions: return True return False @@ -73,7 +73,9 @@ def should_include_all(self) -> bool: def write(self) -> None: """Generate the header file output string and write it to file.""" # Add the top prefix text - self.hpp_collection += self.package_info.prefix_text + "\n" + prefix_text = self.package_info.hierarchy_attribute("prefix_text") + if prefix_text: + self.hpp_collection += prefix_text + "\n" # Add opening header guard self.hpp_collection += f"#ifndef {self.package_info.name}_HEADERS_HPP_\n" @@ -93,8 +95,8 @@ def write(self) -> None: else: # Include specific headers needed by classes - for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: + for module_info in self.package_info.module_collection: + for class_info in module_info.class_collection: # Skip excluded classes if class_info.excluded: continue @@ -105,11 +107,9 @@ def write(self) -> None: seen_files.add(filename) # Include specific headers needed by free functions - for free_function_info in module_info.free_function_info_collection: - if free_function_info.source_file_full_path: - filename = os.path.basename( - free_function_info.source_file_full_path - ) + for free_function_info in module_info.free_function_collection: + if free_function_info.source_file_path: + filename = os.path.basename(free_function_info.source_file_path) if filename not in seen_files: self.hpp_collection += f'#include "{filename}"\n' seen_files.add(filename) @@ -119,8 +119,8 @@ def write(self) -> None: template_instantiations = "" template_typedefs = "" - for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: + for module_info in self.package_info.module_collection: + for class_info in module_info.class_collection: # Skip excluded classes if class_info.excluded: continue diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index 2bba96a..0edb345 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -44,7 +44,7 @@ def __init__( # classes to be wrapped in the module self.class_decls: List["class_t"] = [] # noqa: F821 - for class_info in self.module_info.class_info_collection: + for class_info in self.module_info.class_collection: # Skip excluded classes if class_info.excluded: continue @@ -72,7 +72,9 @@ def write_module_wrapper(self) -> None: cpp_string = "" # Add the top prefix text - cpp_string += self.module_info.package_info.prefix_text + "\n" + prefix_text = self.module_info.hierarchy_attribute("prefix_text") + if prefix_text: + cpp_string += prefix_text + "\n" # Add top level includes cpp_string += "#include \n" @@ -81,11 +83,13 @@ def write_module_wrapper(self) -> None: cpp_string += f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n' # Add outputs from running custom generator code - if self.module_info.custom_generator: - cpp_string += self.module_info.custom_generator.get_module_pre_code() + if self.module_info.custom_generator_instance: + cpp_string += ( + self.module_info.custom_generator_instance.get_module_pre_code() + ) # Add includes for class wrappers in the module - for class_info in self.module_info.class_info_collection: + for class_info in self.module_info.class_collection: # Skip excluded classes if class_info.excluded: continue @@ -105,14 +109,14 @@ def write_module_wrapper(self) -> None: cpp_string += "{\n" # Add free functions - for free_function_info in self.module_info.free_function_info_collection: + for free_function_info in self.module_info.free_function_collection: function_writer = CppFreeFunctionWrapperWriter( free_function_info, self.wrapper_templates ) cpp_string += function_writer.generate_wrapper() # Add classes - for class_info in self.module_info.class_info_collection: + for class_info in self.module_info.class_collection: # Skip excluded classes if class_info.excluded: continue @@ -122,8 +126,8 @@ def write_module_wrapper(self) -> None: cpp_string += f" register_{py_name}_class(m);\n" # Add code from the module's custom generator - if self.module_info.custom_generator: - cpp_string += self.module_info.custom_generator.get_module_code() + if self.module_info.custom_generator_instance: + cpp_string += self.module_info.custom_generator_instance.get_module_code() cpp_string += "}\n" # End of the pybind11 module @@ -143,7 +147,7 @@ def write_class_wrappers(self) -> None: """Write wrappers for classes in the module.""" logger = logging.getLogger() - for class_info in self.module_info.class_info_collection: + for class_info in self.module_info.class_collection: # Skip excluded classes if class_info.excluded: logger.info(f"Skipping class {class_info.name}") diff --git a/cppwg/writers/package_writer.py b/cppwg/writers/package_writer.py index b311b43..229a9cc 100644 --- a/cppwg/writers/package_writer.py +++ b/cppwg/writers/package_writer.py @@ -33,7 +33,7 @@ def write(self) -> None: """ Write all the wrappers required for the package. """ - for module_info in self.package_info.module_info_collection: + for module_info in self.package_info.module_collection: module_writer = CppModuleWrapperWriter( module_info, self.wrapper_templates,