From d700434fbd36ec574ffc5d82303c6df8dac70eff Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 29 Feb 2024 17:11:03 +0000 Subject: [PATCH 01/53] Make std=c++11 optional --- cppwg/parsers/source_parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 29066b5..7c2ce74 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -10,12 +10,13 @@ class CppSourceParser(): def __init__(self, source_root, wrapper_header_collection, - castxml_binary, source_includes): + castxml_binary, source_includes, cflags=""): self.source_root = source_root self.wrapper_header_collection = wrapper_header_collection self.castxml_binary = castxml_binary self.source_includes = source_includes + self.cflags = cflags self.global_ns = None self.source_ns = None @@ -23,7 +24,7 @@ def parse(self): xml_generator_config = parser.xml_generator_configuration_t(xml_generator_path=self.castxml_binary, xml_generator="castxml", - cflags="-std=c++11", + cflags=self.cflags, include_paths=self.source_includes) print ("INFO: Parsing Code") From 11cd0438c2bf284f70f0d8159b78e85db9857ce2 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 1 Mar 2024 17:43:58 +0000 Subject: [PATCH 02/53] #11 update source_parser --- cppwg/parsers/source_parser.py | 109 ++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 7c2ce74..0bd4411 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -1,50 +1,89 @@ -#!/usr/bin/env python - -""" -Parse the single header file using CastXML and pygccxml -""" +from typing import Optional, Type from pygccxml import parser, declarations +# declaration_t is the base type for all declarations in pygccxml including: +# - class_declaration_t (pygccxml.declarations.class_declaration.class_declaration_t) +# - class_t (pygccxml.declarations.class_declaration) +# - constructor_t (pygccxml.declarations.calldef_members.constructor_t) +# - free_function_t (pygccxml.declarations.free_calldef.free_function_t) +# - free_operator_t (pygccxml.declarations.free_calldef.free_operator_t) +# - destructor_t (pygccxml.declarations.calldef_members.destructor_t) +# - member_function_t (pygccxml.declarations.calldef_members.member_function_t) +# - member_operator_t (pygccxml.declarations.calldef_members.member_operator_t) +# - typedef_t (pygccxml.declarations.typedef.typedef_t) +# - variable_t (pygccxml.declarations.variable.variable_t) +from pygccxml.declarations import declaration_t -class CppSourceParser(): +from pygccxml.declarations.matchers import custom_matcher_t +from pygccxml.declarations.mdecl_wrapper import mdecl_wrapper_t +from pygccxml.declarations.namespace import namespace_t - def __init__(self, source_root, wrapper_header_collection, - castxml_binary, source_includes, cflags=""): +from pygccxml.parser.config import xml_generator_configuration_t - self.source_root = source_root - self.wrapper_header_collection = wrapper_header_collection - self.castxml_binary = castxml_binary - self.source_includes = source_includes - self.cflags = cflags - self.global_ns = None - self.source_ns = None - def parse(self): +class CppSourceParser: + """ + Parses the C++ source code using CastXML and pygccxml from the + information given in a single wrapper_header_collection file. + """ - xml_generator_config = parser.xml_generator_configuration_t(xml_generator_path=self.castxml_binary, - xml_generator="castxml", - cflags=self.cflags, - include_paths=self.source_includes) + def __init__( + self, + source_root: str, + wrapper_header_collection: str, + castxml_binary: str, + source_includes: list[str], + cflags: str = "", + ): + self.source_root: str = source_root + self.wrapper_header_collection: str = wrapper_header_collection + self.castxml_binary: str = castxml_binary + self.source_includes: list[str] = source_includes + self.cflags: str = cflags + self.global_ns: Optional[namespace_t] = None + self.source_ns: Optional[namespace_t] = None - print ("INFO: Parsing Code") - decls = parser.parse([self.wrapper_header_collection], xml_generator_config, - compilation_mode=parser.COMPILATION_MODE.ALL_AT_ONCE) + def parse(self): + xml_generator_config: Type[xml_generator_configuration_t] = ( + xml_generator_configuration_t( + xml_generator_path=self.castxml_binary, + xml_generator="castxml", + cflags=self.cflags, + include_paths=self.source_includes, + ) + ) + + print("INFO: Parsing Code") + decls: list[declaration_t] = parser.parse( + files=[self.wrapper_header_collection], + config=xml_generator_config, + compilation_mode=parser.COMPILATION_MODE.ALL_AT_ONCE, + ) # Get access to the global namespace self.global_ns = declarations.get_global_namespace(decls) - # Clean decls to only include those for which file locations exist - print ("INFO: Cleaning Decls") - query = declarations.custom_matcher_t(lambda decl: decl.location is not None) - decls_loc_not_none = self.global_ns.decls(function=query) + # Filter declarations for which files exist + print("INFO: Cleaning Declarations") + query: Type[custom_matcher_t] = custom_matcher_t( + lambda decl: decl.location is not None + ) + clean_decls: Type[mdecl_wrapper_t] = self.global_ns.decls(function=query) + + # Filter declarations in our source tree (+ wrapper_header_collection) + source_decls: list[declaration_t] = [ + decl + for decl in clean_decls + if self.source_root in decl.location.file_name + or "wrapper_header_collection" in decl.location.file_name + ] - # Identify decls in our source tree - def check_loc(loc): - return (self.source_root in loc) or ("wrapper_header_collection" in loc) - - source_decls = [decl for decl in decls_loc_not_none if check_loc(decl.location.file_name)] - self.source_ns = declarations.namespace_t("source", source_decls) + # Create a namespace module holding the list of C++ declarations + self.source_ns: namespace_t = namespace_t( + name="source", declarations=source_decls + ) - print ("INFO: Optimizing Decls") - self.source_ns.init_optimizer() \ No newline at end of file + # Initialise namespace module's internal type hash tables for faster queries + print("INFO: Optimizing Declaration Queries") + self.source_ns.init_optimizer() From 62c645de58c13ec4407427c7d1f234accddca5a6 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 2 Mar 2024 21:52:43 +0000 Subject: [PATCH 03/53] #11 further source parser updates --- cppwg/parsers/source_parser.py | 42 ++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 0bd4411..3766c01 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -1,4 +1,4 @@ -from typing import Optional, Type +from typing import Optional from pygccxml import parser, declarations @@ -23,10 +23,6 @@ class CppSourceParser: - """ - Parses the C++ source code using CastXML and pygccxml from the - information given in a single wrapper_header_collection file. - """ def __init__( self, @@ -41,11 +37,25 @@ def __init__( self.castxml_binary: str = castxml_binary self.source_includes: list[str] = source_includes self.cflags: str = cflags - self.global_ns: Optional[namespace_t] = None self.source_ns: Optional[namespace_t] = None def parse(self): - xml_generator_config: Type[xml_generator_configuration_t] = ( + """ + Parses the C++ source code using CastXML and pygccxml from the + information given in a single header collection file. + + Args: + 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 + cflags (str, optional): Optional cflags to be passed to CastXML e.g. "-std=c++17". + + Returns: + namespace_t: The filtered source namespace + """ + + xml_generator_config: xml_generator_configuration_t = ( xml_generator_configuration_t( xml_generator_path=self.castxml_binary, xml_generator="castxml", @@ -62,14 +72,14 @@ def parse(self): ) # Get access to the global namespace - self.global_ns = declarations.get_global_namespace(decls) + global_ns: namespace_t = declarations.get_global_namespace(decls) # Filter declarations for which files exist print("INFO: Cleaning Declarations") - query: Type[custom_matcher_t] = custom_matcher_t( + query: custom_matcher_t = custom_matcher_t( lambda decl: decl.location is not None ) - clean_decls: Type[mdecl_wrapper_t] = self.global_ns.decls(function=query) + clean_decls: mdecl_wrapper_t = global_ns.decls(function=query) # Filter declarations in our source tree (+ wrapper_header_collection) source_decls: list[declaration_t] = [ @@ -79,11 +89,13 @@ def parse(self): or "wrapper_header_collection" in decl.location.file_name ] - # Create a namespace module holding the list of C++ declarations - self.source_ns: namespace_t = namespace_t( - name="source", declarations=source_decls - ) + # Create a source namespace module for the filtered declarations + self.source_ns = namespace_t(name="source", declarations=source_decls) - # Initialise namespace module's internal type hash tables for faster queries + # Initialise the source namespace's internal type hash tables for faster queries print("INFO: Optimizing Declaration Queries") self.source_ns.init_optimizer() + + for decl in self.source_ns.decls(): + if str(decl).startswith("AbstractLinear"): + print(decl) From 47da70a9832c8c7de3848b340886793461b7f03a Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 2 Mar 2024 21:55:29 +0000 Subject: [PATCH 04/53] #11 more source parser updates --- cppwg/parsers/source_parser.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 3766c01..02803de 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -95,7 +95,3 @@ def parse(self): # Initialise the source namespace's internal type hash tables for faster queries print("INFO: Optimizing Declaration Queries") self.source_ns.init_optimizer() - - for decl in self.source_ns.decls(): - if str(decl).startswith("AbstractLinear"): - print(decl) From 71f2366942cafb1ce1e84e0f8fb5368e3fdd84be Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 3 Mar 2024 10:45:42 +0000 Subject: [PATCH 05/53] #11 Additional source parser updates --- cppwg/parsers/source_parser.py | 54 +++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 02803de..930c60d 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -1,19 +1,22 @@ +import logging + from typing import Optional from pygccxml import parser, declarations +from pygccxml.declarations import declaration_t + # declaration_t is the base type for all declarations in pygccxml including: # - class_declaration_t (pygccxml.declarations.class_declaration.class_declaration_t) -# - class_t (pygccxml.declarations.class_declaration) +# - class_t (pygccxml.declarations.class_declaration.class_t) # - constructor_t (pygccxml.declarations.calldef_members.constructor_t) +# - destructor_t (pygccxml.declarations.calldef_members.destructor_t) # - free_function_t (pygccxml.declarations.free_calldef.free_function_t) # - free_operator_t (pygccxml.declarations.free_calldef.free_operator_t) -# - destructor_t (pygccxml.declarations.calldef_members.destructor_t) # - member_function_t (pygccxml.declarations.calldef_members.member_function_t) # - member_operator_t (pygccxml.declarations.calldef_members.member_operator_t) # - typedef_t (pygccxml.declarations.typedef.typedef_t) # - variable_t (pygccxml.declarations.variable.variable_t) -from pygccxml.declarations import declaration_t from pygccxml.declarations.matchers import custom_matcher_t from pygccxml.declarations.mdecl_wrapper import mdecl_wrapper_t @@ -23,6 +26,24 @@ class CppSourceParser: + """ + Parser for C++ source code + + 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 + cflags : str + Optional cflags to be passed to CastXML e.g. "-std=c++17" + source_ns : namespace_t + The namespace containing declarations from the source code + """ def __init__( self, @@ -41,20 +62,12 @@ def __init__( def parse(self): """ - Parses the C++ source code using CastXML and pygccxml from the - information given in a single header collection file. - - Args: - 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 - cflags (str, optional): Optional cflags to be passed to CastXML e.g. "-std=c++17". - - Returns: - namespace_t: The filtered source namespace + Parses C++ source code from the header collection using CastXML and pygccxml. """ + logger = logging.getLogger() + logger.setLevel(logging.INFO) + # Configure the XML generator (CastXML) xml_generator_config: xml_generator_configuration_t = ( xml_generator_configuration_t( xml_generator_path=self.castxml_binary, @@ -64,7 +77,8 @@ def parse(self): ) ) - print("INFO: Parsing Code") + # Parse all the C++ source code to extract declarations + logger.info("Parsing code.") decls: list[declaration_t] = parser.parse( files=[self.wrapper_header_collection], config=xml_generator_config, @@ -75,16 +89,16 @@ def parse(self): global_ns: namespace_t = declarations.get_global_namespace(decls) # Filter declarations for which files exist - print("INFO: Cleaning Declarations") + logger.info("Filtering source declarations.") query: custom_matcher_t = custom_matcher_t( lambda decl: decl.location is not None ) - clean_decls: mdecl_wrapper_t = global_ns.decls(function=query) + filtered_decls: mdecl_wrapper_t = global_ns.decls(function=query) # Filter declarations in our source tree (+ wrapper_header_collection) source_decls: list[declaration_t] = [ decl - for decl in clean_decls + for decl in filtered_decls if self.source_root in decl.location.file_name or "wrapper_header_collection" in decl.location.file_name ] @@ -93,5 +107,5 @@ def parse(self): self.source_ns = namespace_t(name="source", declarations=source_decls) # Initialise the source namespace's internal type hash tables for faster queries - print("INFO: Optimizing Declaration Queries") + logger.info("Optimizing source declaration queries.") self.source_ns.init_optimizer() From 9501ad916a0bbce72ea2c6209e606447f02086f1 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 11:32:19 +0000 Subject: [PATCH 06/53] #11 extra source parser updates --- cppwg/parsers/source_parser.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 930c60d..982e4cb 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -41,8 +41,10 @@ class CppSourceParser: The list of source include paths cflags : str Optional cflags to be passed to CastXML e.g. "-std=c++17" + global_ns : namespace_t + The namespace containing all parsed C++ declarations source_ns : namespace_t - The namespace containing declarations from the source code + The namespace containing C++ declarations from the source tree """ def __init__( @@ -58,14 +60,20 @@ def __init__( self.castxml_binary: str = castxml_binary self.source_includes: list[str] = source_includes self.cflags: str = cflags + self.source_ns: Optional[namespace_t] = None + self.global_ns: Optional[namespace_t] = None - def parse(self): + def parse(self) -> namespace_t: """ Parses C++ source code from the header collection using CastXML and pygccxml. + + Returns + ------- + namespace_t + The namespace containing C++ declarations from the source tree """ logger = logging.getLogger() - logger.setLevel(logging.INFO) # Configure the XML generator (CastXML) xml_generator_config: xml_generator_configuration_t = ( @@ -86,14 +94,14 @@ def parse(self): ) # Get access to the global namespace - global_ns: namespace_t = declarations.get_global_namespace(decls) + self.global_ns: namespace_t = declarations.get_global_namespace(decls) # Filter declarations for which files exist logger.info("Filtering source declarations.") query: custom_matcher_t = custom_matcher_t( lambda decl: decl.location is not None ) - filtered_decls: mdecl_wrapper_t = global_ns.decls(function=query) + filtered_decls: mdecl_wrapper_t = self.global_ns.decls(function=query) # Filter declarations in our source tree (+ wrapper_header_collection) source_decls: list[declaration_t] = [ @@ -109,3 +117,5 @@ def parse(self): # Initialise the source namespace's internal type hash tables for faster queries logger.info("Optimizing source declaration queries.") self.source_ns.init_optimizer() + + return self.source_ns From a5375813d72b68faa9748e4785766f9aece56c42 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 16:36:29 +0000 Subject: [PATCH 07/53] #11 update generator and source parser --- cppwg/generators.py | 239 ++++++++++++++++++++++----------- cppwg/parsers/source_parser.py | 19 +-- 2 files changed, 166 insertions(+), 92 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 70d369e..7427984 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -1,56 +1,137 @@ import os -import logging +import re import fnmatch -import ntpath +import logging +import subprocess + +from typing import Optional + +from pygccxml import __version__ as pygccxml_version +from pygccxml.declarations.namespace import namespace_t +from cppwg.input.class_info import CppClassInfo +from cppwg.input.free_function_info import CppFreeFunctionInfo from cppwg.input.info_helper import CppInfoHelper from cppwg.input.package_info import PackageInfo -from cppwg.input.free_function_info import CppFreeFunctionInfo -from cppwg.input.class_info import CppClassInfo + from cppwg.parsers.package_info import PackageInfoParser from cppwg.parsers.source_parser import CppSourceParser + from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter from cppwg.writers.module_writer import CppModuleWrapperWriter import cppwg.templates.pybind11_default as wrapper_templates -class CppWrapperGenerator(object): +class CppWrapperGenerator: + """ + Main class for generating C++ wrappers - def __init__(self, source_root, - source_includes=None, - wrapper_root=None, - castxml_binary='castxml', - package_info_path='package_info.yaml'): + Attributes + ---------- + source_root : str + The root directory of the C++ source code + source_includes : list[str] + The list of source include paths + wrapper_root : str + The output directory for the wrapper code + castxml_binary : str + The path to the CastXML binary + package_info_file : str + The path to the package info yaml config file; defaults to "package_info.yaml" + source_hpp_files : list[str] + The list of C++ source header files + source_ns : namespace_t + The namespace containing C++ declarations parsed from the source tree + """ + def __init__( + self, + source_root: str, + source_includes: Optional[list[str]] = None, + wrapper_root: Optional[str] = None, + castxml_binary: Optional[str] = "castxml", + package_info_file: Optional[str] = None, + ): + logging.basicConfig( + format="%(levelname)s %(message)s", + handlers=[logging.FileHandler("filename.log"), logging.StreamHandler()], + ) logger = logging.getLogger() logger.setLevel(logging.INFO) - self.source_root = os.path.realpath(source_root) - self.source_includes = source_includes - self.wrapper_root = wrapper_root - self.castxml_binary = castxml_binary - self.package_info_path = package_info_path - self.source_hpp_files = [] - self.global_ns = None - self.source_ns = None + # Sanitize source_root + self.source_root: str = os.path.realpath(source_root) + if not os.path.exists(self.source_root): + message = f"Could not find source root directory: {source_root}" + logger.error(message) + raise FileNotFoundError() - if self.wrapper_root is None: + # Sanitize wrapper_root + self.wrapper_root: str # type hinting + if wrapper_root: + self.wrapper_root = os.path.realpath(wrapper_root) + + if not os.path.exists(self.wrapper_root): + message = f"Could not find wrapper root directory: {wrapper_root}" + logger.error(message) + raise FileNotFoundError() + else: self.wrapper_root = self.source_root + logger.info( + "Wrapper root not specified - using source_root: {self.source_root}" + ) - if self.source_includes is None: + self.source_includes: list[str] # type hinting + if source_includes: + self.source_includes = [ + os.path.realpath(include_path) for include_path in source_includes + ] + + for include_path in self.source_includes: + if not os.path.exists(include_path): + message = f"Could not find source include directory: {include_path}" + logger.error(message) + raise FileNotFoundError() + else: self.source_includes = [self.source_root] - # If we suspect that a valid info file has not been supplied - # fall back to the default behaviour - path_is_default = (self.package_info_path == 'package_info.yaml') - file_exists = os.path.exists(self.package_info_path) - if path_is_default and (not file_exists): - logger.info('YAML package info file not found. Using default info.') - self.package_info_path = None + self.package_info_file: Optional[str] = None + if package_info_file: + # If a package info config file is specified, check that it exists + self.package_info_file = package_info_file + if not os.path.exists(package_info_file): + message = f"Could not find package info file: {package_info_file}" + logger.error(message) + raise FileNotFoundError() + else: + # If no package info config file has been supplied, check the default + default_package_info_file = os.path.realpath("./package_info.yaml") + if os.path.exists(default_package_info_file): + self.package_info_file = default_package_info_file + logger.info( + f"Package info file not specified - using {default_package_info_file}" + ) + else: + logger.warning("No package info file found - using default settings.") - def collect_source_hpp_files(self): + self.castxml_binary: str = castxml_binary + castxml_version: str = ( + subprocess.check_output([self.castxml_binary, "--version"]) + .decode("ascii") + .strip() + ) + castxml_version = re.search( + r"castxml version \d+\.\d+\.\d+", castxml_version + ).group(0) + logger.info(castxml_version) + logger.info(f"pygccxml version {pygccxml_version}") + + self.source_hpp_files: list[str] = [] + self.source_ns: Optional[namespace_t] = None + + def collect_source_hpp_files(self): """ Walk through the source root and add any files matching the provided patterns. Keep the wrapper root out of the search path to avoid pollution. @@ -59,53 +140,53 @@ def collect_source_hpp_files(self): for pattern in self.package_info.source_hpp_patterns: for filename in fnmatch.filter(filenames, pattern): if "cppwg" not in filename: - self.package_info.source_hpp_files.append(os.path.join(root, filename)) - self.package_info.source_hpp_files = [path for path in self.package_info.source_hpp_files - if self.wrapper_root not in path] + self.package_info.source_hpp_files.append( + os.path.join(root, filename) + ) + self.package_info.source_hpp_files = [ + path + for path in self.package_info.source_hpp_files + if self.wrapper_root not in path + ] def generate_header_collection(self): - """ Write the header collection to file """ - - header_collection_writer = CppHeaderCollectionWriter(self.package_info, - self.wrapper_root) + + header_collection_writer = CppHeaderCollectionWriter( + self.package_info, self.wrapper_root + ) header_collection_writer.write() header_collection_path = self.wrapper_root + "/" header_collection_path += header_collection_writer.header_file_name return header_collection_path - def parse_header_collection(self, header_collection_path): - - """ - Parse the header collection with pygccxml and Castxml - to population the global and source namespaces + def parse_header_collection(self, header_collection_path: str): """ - - source_parser = CppSourceParser(self.source_root, - header_collection_path, - self.castxml_binary, - self.source_includes) - source_parser.parse() - self.global_ns = source_parser.global_ns - self.source_ns = source_parser.source_ns - - def get_wrapper_template(self): - - """ - Return the string templates for the wrappers + Parse the headers with pygccxml and CastXML to populate the source + namespace with C++ declarations collected from the source tree + + Parameters + ---------- + header_collection_path : str + The path to the header collection file """ - - return wrapper_templates.template_collection + + source_parser = CppSourceParser( + self.source_root, + header_collection_path, + self.castxml_binary, + self.source_includes, + ) + self.source_ns = source_parser.parse() def update_free_function_info(self): - """ Update the free function info pased on pygccxml output """ - + for eachModule in self.package_info.module_info: if eachModule.use_all_free_functions: free_functions = self.source_ns.free_functions(allow_empty=True) @@ -118,17 +199,17 @@ def update_free_function_info(self): else: for eachFunction in eachModule.free_function_info: - functions = self.source_ns.free_functions(eachFunction.name, - allow_empty=True) + functions = self.source_ns.free_functions( + eachFunction.name, allow_empty=True + ) if len(functions) == 1: eachFunction.decl = functions[0] def update_class_info(self): - """ Update the class info pased on pygccxml output """ - + for eachModule in self.package_info.module_info: if eachModule.use_all_classes: classes = self.source_ns.classes(allow_empty=True) @@ -140,36 +221,35 @@ def update_class_info(self): eachModule.class_info.append(class_info) else: for eachClass in eachModule.class_info: - classes = self.source_ns.classes(eachClass.name, - allow_empty=True) + classes = self.source_ns.classes(eachClass.name, allow_empty=True) if len(classes) == 1: eachClass.decl = classes[0] def generate_wrapper(self): - """ - Main method for wrapper generation + Main method for generating all the wrappers """ - # If there is an input file, parse it - if self.package_info_path is not None: - info_parser = PackageInfoParser(self.package_info_path, - self.source_root) + if self.package_info_file is not None: + # Parse the package info input file + info_parser = PackageInfoParser(self.package_info_file, self.source_root) info_parser.parse() self.package_info = info_parser.package_info + else: + # Create a default package info object self.package_info = PackageInfo("cppwg_package", self.source_root) # Generate a header collection self.collect_source_hpp_files() - # Attempt to assign source paths to each class, assuming the containing + # Attempt to assign source paths to each class, assuming the containing # file name is the class name for eachModule in self.package_info.module_info: for eachClass in eachModule.class_info: for eachPath in self.package_info.source_hpp_files: - base = ntpath.basename(eachPath) - if eachClass.name == base.split('.')[0]: + base = os.path.basename(eachPath) + if eachClass.name == base.split(".")[0]: eachClass.source_file_full_path = eachPath if eachClass.source_file is None: eachClass.source_file = base @@ -190,11 +270,12 @@ def generate_wrapper(self): self.update_class_info() self.update_free_function_info() - # Write the modules - for eachModule in self.package_info.module_info: - module_writer = CppModuleWrapperWriter(self.global_ns, - self.source_ns, - eachModule, - self.get_wrapper_template(), - self.wrapper_root) + # Write all the wrappers required for each module + for module_info in self.package_info.module_info: + module_writer = CppModuleWrapperWriter( + self.source_ns, + module_info, + wrapper_templates.template_collection, + self.wrapper_root, + ) module_writer.write() diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 982e4cb..ee2f530 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -18,12 +18,9 @@ # - typedef_t (pygccxml.declarations.typedef.typedef_t) # - variable_t (pygccxml.declarations.variable.variable_t) -from pygccxml.declarations.matchers import custom_matcher_t from pygccxml.declarations.mdecl_wrapper import mdecl_wrapper_t from pygccxml.declarations.namespace import namespace_t -from pygccxml.parser.config import xml_generator_configuration_t - class CppSourceParser: """ @@ -76,13 +73,11 @@ def parse(self) -> namespace_t: logger = logging.getLogger() # Configure the XML generator (CastXML) - xml_generator_config: xml_generator_configuration_t = ( - xml_generator_configuration_t( - xml_generator_path=self.castxml_binary, - xml_generator="castxml", - cflags=self.cflags, - include_paths=self.source_includes, - ) + xml_generator_config = parser.xml_generator_configuration_t( + xml_generator_path=self.castxml_binary, + xml_generator="castxml", + cflags=self.cflags, + include_paths=self.source_includes, ) # Parse all the C++ source code to extract declarations @@ -98,9 +93,7 @@ def parse(self) -> namespace_t: # Filter declarations for which files exist logger.info("Filtering source declarations.") - query: custom_matcher_t = custom_matcher_t( - lambda decl: decl.location is not None - ) + query = declarations.custom_matcher_t(lambda decl: decl.location is not None) filtered_decls: mdecl_wrapper_t = self.global_ns.decls(function=query) # Filter declarations in our source tree (+ wrapper_header_collection) From ea1decf1c7a3915d4e1494baab7ef5dd45fa89ed Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 16:52:51 +0000 Subject: [PATCH 08/53] #11 update generators --- cppwg/generators.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 7427984..04e042f 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -43,6 +43,8 @@ class CppWrapperGenerator: The list of C++ source header files source_ns : namespace_t The namespace containing C++ declarations parsed from the source tree + package_info : PackageInfo + A data structure containing the information parsed from package_info_file """ def __init__( @@ -82,6 +84,7 @@ def __init__( "Wrapper root not specified - using source_root: {self.source_root}" ) + # Sanitize source_includes self.source_includes: list[str] # type hinting if source_includes: self.source_includes = [ @@ -96,6 +99,7 @@ def __init__( else: self.source_includes = [self.source_root] + # Sanitize package_info_file self.package_info_file: Optional[str] = None if package_info_file: # If a package info config file is specified, check that it exists @@ -115,6 +119,7 @@ def __init__( else: logger.warning("No package info file found - using default settings.") + # Check castxml and pygccxml versions self.castxml_binary: str = castxml_binary castxml_version: str = ( subprocess.check_output([self.castxml_binary, "--version"]) @@ -127,10 +132,13 @@ def __init__( logger.info(castxml_version) logger.info(f"pygccxml version {pygccxml_version}") + # Initialize remaining attributes self.source_hpp_files: list[str] = [] self.source_ns: Optional[namespace_t] = None + self.package_info: Optional[PackageInfo] = None + def collect_source_hpp_files(self): """ Walk through the source root and add any files matching the provided patterns. @@ -182,6 +190,21 @@ def parse_header_collection(self, header_collection_path: str): ) self.source_ns = source_parser.parse() + def parse_package_info(self): + """ + Parse the package info file to create a PackageInfo object + """ + + if self.package_info_file: + # If a package info file exists, parse it to create a PackageInfo object + info_parser = PackageInfoParser(self.package_info_file, self.source_root) + info_parser.parse() + self.package_info = info_parser.package_info + + else: + # If no package info file exists, create a PackageInfo object with default settings + self.package_info = PackageInfo("cppwg_package", self.source_root) + def update_free_function_info(self): """ Update the free function info pased on pygccxml output @@ -230,15 +253,8 @@ def generate_wrapper(self): Main method for generating all the wrappers """ - if self.package_info_file is not None: - # Parse the package info input file - info_parser = PackageInfoParser(self.package_info_file, self.source_root) - info_parser.parse() - self.package_info = info_parser.package_info - - else: - # Create a default package info object - self.package_info = PackageInfo("cppwg_package", self.source_root) + # Parse the package info file + self.parse_package_info() # Generate a header collection self.collect_source_hpp_files() From a9ef41d93d7be1546c70fa9bcf73cf2d753e75a9 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 17:04:06 +0000 Subject: [PATCH 09/53] #11 more generator updates --- cppwg/generators.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 04e042f..f179fe1 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -157,21 +157,28 @@ def collect_source_hpp_files(self): if self.wrapper_root not in path ] - def generate_header_collection(self): + def generate_header_collection(self) -> str: """ Write the header collection to file + + Returns + ------- + str + The path to the header collection file """ header_collection_writer = CppHeaderCollectionWriter( self.package_info, self.wrapper_root ) header_collection_writer.write() - header_collection_path = self.wrapper_root + "/" - header_collection_path += header_collection_writer.header_file_name + + header_collection_path = os.path.join( + self.wrapper_root, header_collection_writer.header_file_name + ) return header_collection_path - def parse_header_collection(self, header_collection_path: str): + def parse_header_collection(self, header_collection_path: str) -> None: """ Parse the headers with pygccxml and CastXML to populate the source namespace with C++ declarations collected from the source tree From 90465d63662da8c040e0c951663a1cffc9d156ff Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 20:23:16 +0000 Subject: [PATCH 10/53] #11 update header collection writer --- cppwg/generators.py | 96 ++++---- cppwg/writers/header_collection_writer.py | 256 +++++++++++++--------- 2 files changed, 204 insertions(+), 148 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index f179fe1..0481143 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -72,12 +72,14 @@ def __init__( # Sanitize wrapper_root self.wrapper_root: str # type hinting if wrapper_root: + # Create the specified wrapper root directory if it doesn't exist self.wrapper_root = os.path.realpath(wrapper_root) if not os.path.exists(self.wrapper_root): - message = f"Could not find wrapper root directory: {wrapper_root}" - logger.error(message) - raise FileNotFoundError() + logger.info( + f"Could not find wrapper root directory - creating it at {self.wrapper_root}" + ) + os.makedirs(self.wrapper_root) else: self.wrapper_root = self.source_root logger.info( @@ -167,13 +169,17 @@ def generate_header_collection(self) -> str: The path to the header collection file """ + header_collection_filename = "wrapper_header_collection.hpp" + header_collection_writer = CppHeaderCollectionWriter( - self.package_info, self.wrapper_root + self.package_info, + self.wrapper_root, + header_collection_filename, ) header_collection_writer.write() header_collection_path = os.path.join( - self.wrapper_root, header_collection_writer.header_file_name + self.wrapper_root, header_collection_filename ) return header_collection_path @@ -217,43 +223,45 @@ def update_free_function_info(self): Update the free function info pased on pygccxml output """ - for eachModule in self.package_info.module_info: - if eachModule.use_all_free_functions: + for module_info in self.package_info.module_info_collection: + if module_info.use_all_free_functions: free_functions = self.source_ns.free_functions(allow_empty=True) - for eachFunction in free_functions: - if eachModule.is_decl_in_source_path(eachFunction): - function_info = CppFreeFunctionInfo(eachFunction.name) - function_info.module_info = eachModule - function_info.decl = eachFunction - eachModule.free_function_info.append(function_info) + for free_function in free_functions: + if module_info.is_decl_in_source_path(free_function): + function_info = CppFreeFunctionInfo(free_function.name) + function_info.module_info = module_info + function_info.decl = free_function + module_info.free_function_info.append(function_info) else: - for eachFunction in eachModule.free_function_info: - functions = self.source_ns.free_functions( - eachFunction.name, allow_empty=True + for free_function_info in module_info.free_function_info_collection: + free_functions = self.source_ns.free_functions( + free_function_info.name, allow_empty=True ) - if len(functions) == 1: - eachFunction.decl = functions[0] + if len(free_functions) == 1: + free_function_info.decl = free_functions[0] def update_class_info(self): """ Update the class info pased on pygccxml output """ - for eachModule in self.package_info.module_info: - if eachModule.use_all_classes: - classes = self.source_ns.classes(allow_empty=True) - for eachClass in classes: - if eachModule.is_decl_in_source_path(eachClass): - class_info = CppClassInfo(eachClass.name) - class_info.module_info = eachModule - class_info.decl = eachClass - eachModule.class_info.append(class_info) + for module_info in self.package_info.module_info_collection: + if module_info.use_all_classes: + class_decls = self.source_ns.classes(allow_empty=True) + for class_decl in class_decls: + if module_info.is_decl_in_source_path(class_decl): + class_info = CppClassInfo(class_decl.name) + class_info.module_info = module_info + class_info.decl = class_decl + module_info.class_info_collection.append(class_info) else: - for eachClass in eachModule.class_info: - classes = self.source_ns.classes(eachClass.name, allow_empty=True) - if len(classes) == 1: - eachClass.decl = classes[0] + for class_info in module_info.class_info_collection: + class_decls = self.source_ns.classes( + class_info.name, allow_empty=True + ) + if len(class_decls) == 1: + class_info.decl = class_decls[0] def generate_wrapper(self): """ @@ -268,20 +276,20 @@ def generate_wrapper(self): # Attempt to assign source paths to each class, assuming the containing # file name is the class name - for eachModule in self.package_info.module_info: - for eachClass in eachModule.class_info: - for eachPath in self.package_info.source_hpp_files: - base = os.path.basename(eachPath) - if eachClass.name == base.split(".")[0]: - eachClass.source_file_full_path = eachPath - if eachClass.source_file is None: - eachClass.source_file = base + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + for hpp_file_path in self.package_info.source_hpp_files: + hpp_file_name = os.path.basename(hpp_file_path) + if class_info.name == hpp_file_name.split(".")[0]: + class_info.source_file_full_path = hpp_file_path + if class_info.source_file is None: + class_info.source_file = hpp_file_name # Attempt to automatically generate template args for each class - for eachModule in self.package_info.module_info: - info_genenerator = CppInfoHelper(eachModule) - for eachClass in eachModule.class_info: - info_genenerator.expand_templates(eachClass, "class") + for module_info in self.package_info.module_info_collection: + info_genenerator = CppInfoHelper(module_info) + for class_info in module_info.class_info_collection: + info_genenerator.expand_templates(class_info, "class") # Generate the header collection header_collection_path = self.generate_header_collection() @@ -294,7 +302,7 @@ def generate_wrapper(self): self.update_free_function_info() # Write all the wrappers required for each module - for module_info in self.package_info.module_info: + for module_info in self.package_info.module_info_collection: module_writer = CppModuleWrapperWriter( self.source_ns, module_info, diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index 3ffbaac..82dac1a 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -1,131 +1,179 @@ -#!/usr/bin/env python - -""" -Generate the file classes_to_be_wrapped.hpp, which contains includes, -instantiation and naming typedefs for all classes that are to be -automatically wrapped. -""" - import os -import ntpath +from cppwg.input.class_info import CppClassInfo +from cppwg.input.free_function_info import CppFreeFunctionInfo +from cppwg.input.package_info import PackageInfo -class CppHeaderCollectionWriter(): +class CppHeaderCollectionWriter: """ - This class manages generation of the header collection file for - parsing by CastXML + This class manages the generation of the header collection file, which + includes all the headers to be parsed by CastXML. The header collection file + also contains explicit template instantiations and their corresponding + typedefs (e.g. typedef AbstractLinearPde2_2 AbstractLinearPde<2,2>) for all + classes that are to be automatically wrapped. + + Attributes + ---------- + package_info : PackageInfo + The package information + wrapper_root : str + The output directory for the generated wrapper code + hpp_collection_filename : str + The name of the header collection file + hpp_collection_string : str + The output string that gets written to the header collection file + class_dict : dict[str, CppClassInfo] + A dictionary of all class info objects + free_func_dict : dict[str, CppFreeFunctionInfo] + A dictionary of all free function info objects """ - def __init__(self, package_info, wrapper_root): + def __init__( + self, + package_info: PackageInfo, + wrapper_root: str, + hpp_collection_filename: str = "wrapper_header_collection.hpp", + ): - self.wrapper_root = wrapper_root - self.package_info = package_info - self.header_file_name = "wrapper_header_collection.hpp" - self.hpp_string = "" - self.class_dict = {} - self.free_func_dict = {} + # Populate the class and free function dictionaries from the package info + self.package_info: PackageInfo = package_info - for eachModule in self.package_info.module_info: - for eachClassInfo in eachModule.class_info: - self.class_dict[eachClassInfo.name] = eachClassInfo + self.class_dict: dict[str, CppClassInfo] = {} + self.free_func_dict: dict[str, CppFreeFunctionInfo] = {} - for eachFuncInfo in eachModule.free_function_info: - self.free_func_dict[eachFuncInfo.name] = eachFuncInfo - - def add_custom_header_code(self): - - """ - Any custom header code goes here - """ + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + self.class_dict[class_info.name] = class_info - pass + for free_function_info in module_info.free_function_info_collection: + self.free_func_dict[free_function_info.name] = free_function_info - def write_file(self): + # Set the remaining attributes + self.wrapper_root: str = wrapper_root + self.hpp_collection_filename: str = hpp_collection_filename + self.hpp_collection_string: str = "" + def should_include_all(self) -> bool: """ - The actual write - """ - - if not os.path.exists(self.wrapper_root + "/"): - os.makedirs(self.wrapper_root + "/") - file_path = self.wrapper_root + "/" + self.header_file_name - hpp_file = open(file_path, 'w') - hpp_file.write(self.hpp_string) - hpp_file.close() - - def should_include_all(self): - + Return whether all source files in the module source locations should be included + + Returns + ------- + bool """ - Return whether all source files in the module source locs should be included - """ - for eachModule in self.package_info.module_info: - if eachModule.use_all_classes or eachModule.use_all_free_functions: - return True + # True if any module uses all classes or all free functions + for module_info in self.package_info.module_info_collection: + if module_info.use_all_classes or module_info.use_all_free_functions: + return True return False def write(self): - """ - Main method for generating the header file output string + Generate the header file output string and write it to file """ - hpp_header_dict = {'package_name': self.package_info.name} - hpp_header_template = """\ -#ifndef {package_name}_HEADERS_HPP_ -#define {package_name}_HEADERS_HPP_ + # Add opening header guard + self.hpp_collection_string = f"#ifndef {self.package_info.name}_HEADERS_HPP_\n" + self.hpp_collection_string += f"#define {self.package_info.name}_HEADERS_HPP_\n" + + # Add the includes + self.hpp_collection_string += "\n// Includes\n" -// Includes -""" - self.hpp_string = hpp_header_template.format(**hpp_header_dict) + included_files = set() # Keep track of included files to avoid duplicates - # Now our own includes if self.should_include_all(): - for eachFile in self.package_info.source_hpp_files: - include_name = ntpath.basename(eachFile) - self.hpp_string += '#include "' + include_name + '"\n' - else: - for eachModule in self.package_info.module_info: - for eachClassInfo in eachModule.class_info: - if eachClassInfo.source_file is not None: - self.hpp_string += '#include "' + eachClassInfo.source_file + '"\n' - elif eachClassInfo.source_file_full_path is not None: - include_name = ntpath.basename(eachClassInfo.source_file_full_path) - self.hpp_string += '#include "' + include_name + '"\n' - for eachFuncInfo in eachModule.free_function_info: - if eachFuncInfo.source_file_full_path is not None: - include_name = ntpath.basename(eachFuncInfo.source_file_full_path) - self.hpp_string += '#include "' + include_name + '"\n' - - # Add the template instantiations - self.hpp_string += "\n// Instantiate Template Classes \n" - for eachModule in self.package_info.module_info: - for eachClassInfo in eachModule.class_info: - full_names = eachClassInfo.get_full_names() - if len(full_names) == 1: - continue - prefix = "template class " - for eachTemplateName in full_names: - self.hpp_string += prefix + eachTemplateName.replace(" ","") + ";\n" - - # Add typdefs for nice naming - self.hpp_string += "\n// Typedef for nicer naming\n" - self.hpp_string += "namespace cppwg{ \n" - for eachModule in self.package_info.module_info: - for eachClassInfo in eachModule.class_info: - full_names = eachClassInfo.get_full_names() - if len(full_names) == 1: - continue - - short_names = eachClassInfo.get_short_names() - for idx, eachTemplateName in enumerate(full_names): - short_name = short_names[idx] - typdef_prefix = "typedef " + eachTemplateName.replace(" ","") + " " - self.hpp_string += typdef_prefix + short_name + ";\n" - self.hpp_string += "}\n" - - self.add_custom_header_code() - self.hpp_string += "\n#endif // {}_HEADERS_HPP_\n".format(self.package_info.name) + for hpp_filepath in self.package_info.source_hpp_files: + hpp_filename = os.path.basename(hpp_filepath) + + if hpp_filename not in included_files: + self.hpp_collection_string += f'#include "{hpp_filename}"\n' + included_files.add(hpp_filename) + else: + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + hpp_filename = None + + if class_info.source_file: + hpp_filename = class_info.source_file + + elif class_info.source_file_full_path: + hpp_filename = os.path.basename( + class_info.source_file_full_path + ) + + if hpp_filename and hpp_filename not in included_files: + self.hpp_collection_string += f'#include "{hpp_filename}"\n' + included_files.add(hpp_filename) + + for free_function_info in module_info.free_function_info_collection: + if free_function_info.source_file_full_path: + hpp_filename = os.path.basename( + free_function_info.source_file_full_path + ) + + if hpp_filename not in included_files: + self.hpp_collection_string += f'#include "{hpp_filename}"\n' + included_files.add(hpp_filename) + + # Add the template instantiations e.g. `template class AbstractLinearPde<2,2>;` + self.hpp_collection_string += "\n// Instantiate Template Classes \n" + + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + # Class full names eg. ["AbstractLinearPde<2,2>", "AbstractLinearPde<3,3>"] + full_names = class_info.get_full_names() + + # TODO: What if the class is templated but has only one template instantiation? + if len(full_names) < 2: + continue # Skip if the class is untemplated + + for template_name in full_names: + clean_template_name = template_name.replace(" ", "") + self.hpp_collection_string += ( + f"template class {clean_template_name};\n" + ) + + # Add typdefs for nice naming e.g. `typedef AbstractLinearPde2_2 AbstractLinearPde<2,2>;` + self.hpp_collection_string += "\n// Typedef for nicer naming\n" + self.hpp_collection_string += "namespace cppwg{ \n" + + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + # Class full names eg. ["AbstractLinearPde<2,2>", "AbstractLinearPde<3,3>"] + full_names = class_info.get_full_names() + + # TODO: What if the class is templated but has only one template instantiation? + if len(full_names) < 2: + continue # Skip if the class is untemplated + + # Class short names eg. ["AbstractLinearPde2_2", "AbstractLinearPde3_3"] + short_names = class_info.get_short_names() + + for short_name, template_name in zip(short_names, full_names): + clean_template_name = template_name.replace(" ", "") + self.hpp_collection_string += ( + f"typedef {clean_template_name} {short_name};\n" + ) + + self.hpp_collection_string += "} // namespace cppwg\n" + + # Add closing header guard + self.hpp_collection_string += ( + f"\n#endif // {self.package_info.name}_HEADERS_HPP_\n" + ) + + # Write the header collection string to file self.write_file() + + def write_file(self) -> None: + """ + Write the header collection string to file + """ + + hpp_file_path = os.path.join(self.wrapper_root, self.hpp_collection_filename) + + with open(hpp_file_path, "w") as hpp_file: + hpp_file.write(self.hpp_collection_string) From 8b6d89b1306589987cf3970e866c5433c7fa2706 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 20:33:00 +0000 Subject: [PATCH 11/53] #11 update package info parser --- cppwg/parsers/package_info.py | 164 +++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index b75204b..d7ad961 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -1,81 +1,100 @@ import os import imp -import ntpath import yaml -from cppwg.input.package_info import PackageInfo -from cppwg.input.module_info import ModuleInfo +from typing import Optional + 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 + + +class PackageInfoParser: + def __init__(self, input_file: str, source_root: str): + """ + Args: -class PackageInfoParser(object): + input_file (str): The path to the package info yaml file. + source_root (str): The root directory of the C++ source code. + """ - def __init__(self, input_file, source_root): + self.input_file: str = input_file + self.source_root: str = source_root + + # Raw info from the yaml file + self.raw_info: dict = {} + + # The parsed package info + self.package_info: Optional[PackageInfo] = None + + def substitute_bool_string(self, option, input_dict, on_string="ON", off_string="OFF"): - self.input_file = input_file - self.raw_info = {} - self.package_info = None - self.source_root = source_root - - def subsititute_bool_string(self, option, input_dict, on_string="ON", off_string="OFF"): - is_string = isinstance(input_dict[option], str) if is_string and input_dict[option].strip().upper() == off_string: input_dict[option] = False elif is_string and input_dict[option].strip().upper() == on_string: input_dict[option] = True - + def is_option_ALL(self, option, input_dict, check_string = "CPPWG_ALL"): - + is_string = isinstance(input_dict[option], str) return is_string and input_dict[option].upper() == check_string - + def check_for_custom_generators(self, feature_info): - + # Replace source root if needed if feature_info.custom_generator is not None: path = feature_info.custom_generator.replace("CPPWG_SOURCEROOT", self.source_root) path = os.path.realpath(path) print (feature_info.name, path) if os.path.isfile(path): - module_name = ntpath.basename(path).split(".")[0] + module_name = os.path.basename(path).split(".")[0] custom_module = imp.load_source(os.path.splitext(path)[0], path) feature_info.custom_generator = getattr(custom_module, module_name)() - + def parse(self): + """ + Parse the package info yaml file to extract information about the + package, modules, classes, and free functions. + """ + + with open(self.input_file, "r") as input_file: + self.raw_info = yaml.safe_load(input_file) - with open(self.input_file, 'r') as inpfile: - data = inpfile.read() - - self.raw_info = yaml.safe_load(data) - - global_defaults = {'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, - 'excluded_methods': [], - 'excluded_variables': [], - 'custom_generator' : None, - 'prefix_code': []} + global_config = { + "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, + "excluded_methods": [], + "excluded_variables": [], + "custom_generator": None, + "prefix_code": [], + } # Parse package data - package_defaults = {'name': 'cppwg_package', - 'common_include_file': True, - 'source_hpp_patterns': ["*.hpp"]} - package_defaults.update(global_defaults) - for eachEntry in package_defaults.keys(): - if eachEntry in self.raw_info: - package_defaults[eachEntry] = self.raw_info[eachEntry] - self.subsititute_bool_string('common_include_file', package_defaults) - - self.package_info = PackageInfo(package_defaults['name'], self.source_root, package_defaults) - self.check_for_custom_generators(self.package_info) + package_config = { + "name": "cppwg_package", + "common_include_file": True, + "source_hpp_patterns": ["*.hpp"], + } + package_config.update(global_config) + + for key in package_config.keys(): + if key in self.raw_info: + package_config[key] = self.raw_info[key] + self.package_info = PackageInfo( + package_config["name"], self.source_root, package_config + ) + self.check_for_custom_generators(self.package_info) + # exit() # Parse module data for eachModule in self.raw_info['modules']: module_defaults = {'name':'cppwg_module', @@ -85,11 +104,11 @@ def parse(self): 'variables': [], 'use_all_classes': False, 'use_all_free_functions': False} - module_defaults.update(global_defaults) - - for eachEntry in module_defaults.keys(): - if eachEntry in eachModule: - module_defaults[eachEntry] = eachModule[eachEntry] + module_defaults.update(global_config) + + for key in module_defaults.keys(): + if key in eachModule: + module_defaults[key] = eachModule[key] # Do classes class_info_collection = [] @@ -99,11 +118,11 @@ def parse(self): for eachClass in module_defaults['classes']: class_defaults = { 'name_override': None, 'source_file': None} - class_defaults.update(global_defaults) - - for eachEntry in class_defaults.keys(): - if eachEntry in eachClass: - class_defaults[eachEntry] = eachClass[eachEntry] + class_defaults.update(global_config) + + for key in class_defaults.keys(): + if key in eachClass: + class_defaults[key] = eachClass[key] class_info = CppClassInfo(eachClass['name'], class_defaults) self.check_for_custom_generators(class_info) class_info_collection.append(class_info) @@ -117,30 +136,35 @@ def parse(self): for _ in module_defaults['free_functions']: ff_defaults = { 'name_override': None, 'source_file': None} - ff_defaults.update(global_defaults) + ff_defaults.update(global_config) function_info = CppFreeFunctionInfo(ff_defaults['name'], ff_defaults) function_info_collection.append(function_info) - - variable_collection = [] + + variable_info_collection = [] use_all_variables = self.is_option_ALL('variables', module_defaults) if not use_all_variables: for _ in module_defaults['variables']: variable_defaults = { 'name_override': None, 'source_file': None} - variable_defaults.update(global_defaults) + variable_defaults.update(global_config) variable_info = CppFreeFunctionInfo(variable_defaults['name'], variable_defaults) - variable_collection.append(variable_info) + variable_info_collection.append(variable_info) module_info = ModuleInfo(module_defaults['name'], module_defaults) - module_info.class_info = class_info_collection - module_info.free_function_info = function_info_collection - module_info.variable_info = variable_collection - for class_info in module_info.class_info: + + module_info.class_info_collection = class_info_collection + for class_info in module_info.class_info_collection: class_info.module_info = module_info - for free_function_info in module_info.free_function_info: - free_function_info.module_info = module_info - for variable_info in module_info.variable_info: - variable_info.module_info = module_info - self.package_info.module_info.append(module_info) + + module_info.free_function_info_collection = function_info_collection + for free_function_info in module_info.free_function_info_collection: + free_function_info.module_info = module_info + + module_info.variable_info_collection = variable_info_collection + for variable_info in module_info.variable_info_collection: + variable_info.module_info = module_info + + self.package_info.module_info_collection.append(module_info) module_info.package_info = self.package_info + self.check_for_custom_generators(module_info) From dcc06101794e21e11ef4f3d0ef3c9910a45bdcca Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 20:47:37 +0000 Subject: [PATCH 12/53] #11 Update info classes --- cppwg/input/base_info.py | 140 ++++++++++++++++++++---------------- cppwg/input/info_helper.py | 11 ++- cppwg/input/module_info.py | 11 +-- cppwg/input/package_info.py | 45 +++++++----- 4 files changed, 118 insertions(+), 89 deletions(-) diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index 09becfc..f337897 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -1,89 +1,107 @@ -""" -Generic information structure for packages, modules and cpp types -""" +from typing import Any, Optional -class BaseInfo(object): - +class BaseInfo: """ - :param: name - the feature name, as it appears in its definition - :param: source_includes - a list of source files to be included with the feature - :param: calldef_excludes - do not include calldefs matching these patterns - :param: smart_ptr_type - handle classes with this smart pointer type - :param: template_substitutions - a list of template substitution sequences - :param: pointer_call_policy - the default pointer call policy - :param: reference_call_policy - the default reference call policy - :param: extra_code - any extra wrapper code for the feature - :param: prefix_code - any wrapper code that precedes the feature - :param: excluded_methods - do not include these methods - :param: excluded_variables - do not include these variables - :param: constructor_arg_type_excludes - list of exlude patterns for ctors - :param: return_type_exludes - list of exlude patterns for return types - :param: arg_type_excludes - list of exlude patterns for arg types + Generic information structure for features (i.e packages, modules, cpp types, etc.) + + 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. + custom_generator : str, optional + A custom generator for the feature. + excluded_methods : list[str] + Do not include these methods. + excluded_variables : list[str] + Do not include these variables. + constructor_arg_type_excludes : list[str] + List of exlude patterns for ctors. + return_type_excludes : list[str] + List of exlude patterns for return types. + arg_type_excludes : list[str] + List of exlude patterns for arg types. + name_replacements : dict[str, str] + A list of name replacements. """ def __init__(self, name): - - self.name = name - self.source_includes = [] - self.calldef_excludes = [] - self.smart_ptr_type = None - self.template_substitutions = [] - self.pointer_call_policy = None - self.reference_call_policy = None - self.extra_code = [] - self.prefix_code = [] - self.custom_generator = None - self.excluded_methods = None - self.excluded_variables = None - self.constructor_arg_type_excludes = [] - self.return_type_excludes = [] - self.arg_type_excludes = [] - self.name_replacements = {"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"} - + 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_methods: list[str] = [] + self.excluded_variables: list[str] = [] + self.constructor_arg_type_excludes: list[str] = [] + self.return_type_excludes: list[str] = [] + self.arg_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): return None - + def hierarchy_attribute(self, attribute_name): - """ For the supplied attribute iterate through parents until a non None value is found. If the tope level parent attribute is None, return None. """ - + if hasattr(self, attribute_name) and getattr(self, attribute_name) is not None: return getattr(self, attribute_name) else: - if hasattr(self, 'parent') and self.parent is not None: - return self.parent.hierarchy_attribute(attribute_name) + if hasattr(self, "parent") and self.parent is not None: + return self.parent.hierarchy_attribute(attribute_name) else: return None def hierarchy_attribute_gather(self, attribute_name): - """ For the supplied attribute iterate through parents gathering list entries. """ - + att_list = [] 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)) + if hasattr(self, "parent") and self.parent is not None: + att_list.extend(self.parent.hierarchy_attribute_gather(attribute_name)) else: - if hasattr(self, 'parent') and self.parent is not None: - att_list.extend(self.parent.hierarchy_attribute_gather(attribute_name)) - return att_list \ No newline at end of file + 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/info_helper.py b/cppwg/input/info_helper.py index 333fd6a..c437e1b 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -6,7 +6,7 @@ import os -class CppInfoHelper(object): +class CppInfoHelper: """ This attempts to automatically fill in some class info based on @@ -23,8 +23,8 @@ def __init__(self, module_info): def setup_class_dict(self): # For convenience collect class info in a dict keyed by name - for eachClassInfo in self.module_info.class_info: - self.class_dict[eachClassInfo.name] = eachClassInfo + for class_info in self.module_info.class_info_collection: + self.class_dict[class_info.name] = class_info def expand_templates(self, feature_info, feature_type): @@ -57,6 +57,11 @@ def expand_templates(self, feature_info, feature_type): template_string = eachSub['signature'] cleaned_string = template_string.replace(" ", "") if cleaned_string in stripped_line: + + if feature_info.name.startswith("AbstractLinear"): + print(feature_info.name) + print(cleaned_string) + print("====================================") feature_string = feature_type + feature_info.name feature_decl_next = feature_string + ":" in stripped_next feature_decl_whole = feature_string == stripped_next diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 3cefa08..6ddf068 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,11 +1,6 @@ -""" -Information for individual modules -""" - from cppwg.input import base_info class ModuleInfo(base_info.BaseInfo): - """ Information for individual modules """ @@ -16,9 +11,9 @@ def __init__(self, name, type_info_dict = None): self.package_info = None self.source_locations = None - self.class_info = [] - self.free_function_info = [] - self.variable_info = [] + self.class_info_collection = [] + self.free_function_info_collection = [] + self.variable_info_collection = [] self.use_all_classes = False self.use_all_free_functions = False diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index c418504..e042507 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -1,38 +1,49 @@ -""" -Information for the package -""" +from typing import Any, Optional -from cppwg.input import base_info +from cppwg.input.base_info import BaseInfo -class PackageInfo(base_info.BaseInfo): - +class PackageInfo(BaseInfo): """ - Information for individual modules + This class holds the package information """ - def __init__(self, name, source_root, type_info_dict = None): - + def __init__( + self, + name: str, + source_root: str, + package_config: Optional[dict[str, Any]] = None, + ): + """ + 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(PackageInfo, self).__init__(name) self.name = name self.source_locations = None - self.module_info = [] + self.module_info_collection = [] self.source_root = source_root self.source_hpp_patterns = ["*.hpp"] self.source_hpp_files = [] self.common_include_file = False - - if type_info_dict is not None: - for key in type_info_dict: - setattr(self, key, type_info_dict[key]) - + + if package_config: + for key, value in package_config.items(): + setattr(self, key, value) + @property def parent(self): return None - - def is_decl_in_source_path(self, decl): + def is_decl_in_source_path(self, decl): """ Return is the declaration associated with a file in the current source path """ From 5214376825264c88b20c885eb1e7386dd064add8 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 4 Mar 2024 20:52:27 +0000 Subject: [PATCH 13/53] #11 update info helper --- cppwg/input/info_helper.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index c437e1b..f332918 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -1,25 +1,19 @@ -""" -Helper class for generating extra feature information based -on simple analysis of the source tree. -""" - import os class CppInfoHelper: - """ - This attempts to automatically fill in some class info based on - simple analysis of the source tree. + Helper class that attempts to automatically fill in extra feature + information based on simple analysis of the source tree. """ def __init__(self, module_info): self.module_info = module_info - + self.class_dict = {} self.setup_class_dict() - + def setup_class_dict(self): # For convenience collect class info in a dict keyed by name @@ -28,8 +22,10 @@ def setup_class_dict(self): def expand_templates(self, feature_info, feature_type): - template_substitutions = feature_info.hierarchy_attribute_gather('template_substitutions') - + template_substitutions = feature_info.hierarchy_attribute_gather( + "template_substitutions" + ) + if len(template_substitutions) == 0: return @@ -47,21 +43,16 @@ def expand_templates(self, feature_info, feature_type): lines = list(line for line in lines if line) for idx, eachLine in enumerate(lines): stripped_line = eachLine.replace(" ", "") - if idx+1 < len(lines): - stripped_next = lines[idx+1].replace(" ", "") + if idx + 1 < len(lines): + stripped_next = lines[idx + 1].replace(" ", "") else: continue for idx, eachSub in enumerate(template_substitutions): - template_args = eachSub['replacement'] - template_string = eachSub['signature'] + template_args = eachSub["replacement"] + template_string = eachSub["signature"] cleaned_string = template_string.replace(" ", "") if cleaned_string in stripped_line: - - if feature_info.name.startswith("AbstractLinear"): - print(feature_info.name) - print(cleaned_string) - print("====================================") feature_string = feature_type + feature_info.name feature_decl_next = feature_string + ":" in stripped_next feature_decl_whole = feature_string == stripped_next @@ -72,4 +63,4 @@ def expand_templates(self, feature_info, feature_type): def do_custom_template_substitution(self, feature_info): - pass \ No newline at end of file + pass From b497b32a7ac7cc6c1b2c8dd70c2a974b1fd6c9dd Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 17:24:40 +0000 Subject: [PATCH 14/53] #11 update package info parser --- cppwg/parsers/package_info.py | 213 ++++++++++++++++++++++------------ cppwg/utils/__init__.py | 0 cppwg/utils/utils.py | 49 ++++++++ 3 files changed, 188 insertions(+), 74 deletions(-) create mode 100644 cppwg/utils/__init__.py create mode 100644 cppwg/utils/utils.py diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index d7ad961..3cbd57d 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -1,69 +1,124 @@ import os -import imp +import importlib +import logging import yaml -from typing import Optional +from typing import Any, Optional +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.utils import utils -class PackageInfoParser: - def __init__(self, input_file: str, source_root: str): +class PackageInfoParser: + """ + Parse for the package info yaml file + + Attributes + ---------- + input_filepath : str + The path to the package info yaml 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): """ - Args: - - input_file (str): The path to the package info yaml file. - source_root (str): The root directory of the C++ source code. + Parameters + ---------- + input_filepath : str + The path to the package info yaml file. + source_root : str + The root directory of the C++ source code. """ - self.input_file: str = input_file + self.input_filepath: str = input_filepath self.source_root: str = source_root - # Raw info from the yaml file - self.raw_info: dict = {} + # 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 substitute_bool_string(self, option, input_dict, on_string="ON", off_string="OFF"): + 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() - is_string = isinstance(input_dict[option], str) - if is_string and input_dict[option].strip().upper() == off_string: - input_dict[option] = False - elif is_string and input_dict[option].strip().upper() == on_string: - input_dict[option] = True + if not info.custom_generator: + return - def is_option_ALL(self, option, input_dict, check_string = "CPPWG_ALL"): + # 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", self.source_root + ) + filepath = os.path.realpath(filepath) - is_string = isinstance(input_dict[option], str) - return is_string and input_dict[option].upper() == check_string + # 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() - def check_for_custom_generators(self, feature_info): + logger.info(f"Custom generator for {info.name}: {filepath}") - # Replace source root if needed - if feature_info.custom_generator is not None: - path = feature_info.custom_generator.replace("CPPWG_SOURCEROOT", self.source_root) - path = os.path.realpath(path) - print (feature_info.name, path) - if os.path.isfile(path): - module_name = os.path.basename(path).split(".")[0] - custom_module = imp.load_source(os.path.splitext(path)[0], path) - feature_info.custom_generator = getattr(custom_module, module_name)() + # 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 - def parse(self): + custom_module = importlib.machinery.SourceFileLoader( + module_name, filepath + ).load_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( + custom_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 parse(self) -> PackageInfo: """ - Parse the package info yaml file to extract information about the + Parse the package info yaml file to extract information about the package, modules, classes, and free functions. + + Returns + ------- + PackageInfo + The object holding data from the parsed package info yaml file. """ + logger = logging.getLogger() + logger.info("Parsing package info file.") - with open(self.input_file, "r") as input_file: - self.raw_info = yaml.safe_load(input_file) + with open(self.input_filepath, "r") as input_filepath: + self.raw_package_info = yaml.safe_load(input_filepath) - global_config = { + # 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, @@ -78,8 +133,8 @@ def parse(self): "prefix_code": [], } - # Parse package data - package_config = { + # Get package config from the raw package info + package_config: dict[str, Any] = { "name": "cppwg_package", "common_include_file": True, "source_hpp_patterns": ["*.hpp"], @@ -87,23 +142,27 @@ def parse(self): package_config.update(global_config) for key in package_config.keys(): - if key in self.raw_info: - package_config[key] = self.raw_info[key] + if key in self.raw_package_info: + package_config[key] = self.raw_package_info[key] + utils.substitute_bool_for_string(package_config, "common_include_file") + # 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) - # exit() + # Parse module data - for eachModule in self.raw_info['modules']: - module_defaults = {'name':'cppwg_module', - 'source_locations': None, - 'classes': [], - 'free_functions': [], - 'variables': [], - 'use_all_classes': False, - 'use_all_free_functions': False} + for eachModule in self.raw_package_info["modules"]: + module_defaults = { + "name": "cppwg_module", + "source_locations": None, + "classes": [], + "free_functions": [], + "variables": [], + "use_all_classes": False, + "use_all_free_functions": False, + } module_defaults.update(global_config) for key in module_defaults.keys(): @@ -112,45 +171,49 @@ def parse(self): # Do classes class_info_collection = [] - module_defaults['use_all_classes'] = self.is_option_ALL('classes', module_defaults) - if not module_defaults['use_all_classes']: - if module_defaults['classes'] is not None: - for eachClass in module_defaults['classes']: - class_defaults = { 'name_override': None, - 'source_file': None} + module_defaults["use_all_classes"] = utils.is_option_ALL( + "classes", module_defaults + ) + if not module_defaults["use_all_classes"]: + if module_defaults["classes"] is not None: + for eachClass in module_defaults["classes"]: + class_defaults = {"name_override": None, "source_file": None} class_defaults.update(global_config) for key in class_defaults.keys(): if key in eachClass: class_defaults[key] = eachClass[key] - class_info = CppClassInfo(eachClass['name'], class_defaults) + class_info = CppClassInfo(eachClass["name"], class_defaults) self.check_for_custom_generators(class_info) class_info_collection.append(class_info) # Do functions function_info_collection = [] - module_defaults['use_all_free_functions'] = self.is_option_ALL('free_functions', - module_defaults) - if not module_defaults['use_all_free_functions']: - if module_defaults['free_functions'] is not None: - for _ in module_defaults['free_functions']: - ff_defaults = { 'name_override': None, - 'source_file': None} - ff_defaults.update(global_config) - function_info = CppFreeFunctionInfo(ff_defaults['name'], ff_defaults) + module_defaults["use_all_free_functions"] = utils.is_option_ALL( + "free_functions", module_defaults + ) + if not module_defaults["use_all_free_functions"]: + if module_defaults["free_functions"] is not None: + for _ in module_defaults["free_functions"]: + ff_defaults = {"name_override": None, "source_file": None} + ff_defaults.update(global_config) + function_info = CppFreeFunctionInfo( + ff_defaults["name"], ff_defaults + ) function_info_collection.append(function_info) variable_info_collection = [] - use_all_variables = self.is_option_ALL('variables', module_defaults) + use_all_variables = utils.is_option_ALL("variables", module_defaults) if not use_all_variables: - for _ in module_defaults['variables']: - variable_defaults = { 'name_override': None, - 'source_file': None} - variable_defaults.update(global_config) - variable_info = CppFreeFunctionInfo(variable_defaults['name'], variable_defaults) - variable_info_collection.append(variable_info) + for _ in module_defaults["variables"]: + variable_defaults = {"name_override": None, "source_file": None} + variable_defaults.update(global_config) + variable_info = CppFreeFunctionInfo( + variable_defaults["name"], variable_defaults + ) + variable_info_collection.append(variable_info) - module_info = ModuleInfo(module_defaults['name'], module_defaults) + module_info = ModuleInfo(module_defaults["name"], module_defaults) module_info.class_info_collection = class_info_collection for class_info in module_info.class_info_collection: @@ -158,13 +221,15 @@ def parse(self): module_info.free_function_info_collection = function_info_collection for free_function_info in module_info.free_function_info_collection: - free_function_info.module_info = module_info + free_function_info.module_info = module_info module_info.variable_info_collection = variable_info_collection for variable_info in module_info.variable_info_collection: - variable_info.module_info = module_info + variable_info.module_info = module_info self.package_info.module_info_collection.append(module_info) module_info.package_info = self.package_info self.check_for_custom_generators(module_info) + + return self.package_info diff --git a/cppwg/utils/__init__.py b/cppwg/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py new file mode 100644 index 0000000..2ee1a36 --- /dev/null +++ b/cppwg/utils/utils.py @@ -0,0 +1,49 @@ +""" +Utility functions for the cppwg package +""" + +from typing import Any + + +def is_option_ALL(input_obj: Any, option_ALL_string: str = "CPPWG_ALL") -> bool: + """ + Check if the input is a string that matches the "ALL" indicator e.g. "CPPWG_ALL" + + Parameters + ---------- + input_obj : Any + The object to check + option_ALL_string : str + The string to check against + + Returns + ------- + bool + True if the input is a string that matches the "ALL" indicator + """ + return isinstance(input_obj, str) and input_obj.upper() == option_ALL_string + + +def substitute_bool_for_string(input_dict: dict[Any, Any], key: Any) -> None: + """ + Substitute a string in the input dictionary with a boolean value if the + string is a boolean indicator e.g. "ON", "OFF", "YES", "NO", "TRUE", "FALSE" + + Parameters + ---------- + input_dict : dict[Any, Any] + The input dictionary + key : Any + The key to check + """ + + if not isinstance(input_dict[key], str): + return + + caps_string = input_dict[key].strip().upper() + + if caps_string in ["OFF", "NO", "N", "FALSE", "F"]: + input_dict[key] = False + + elif caps_string in ["ON", "YES", "Y", "TRUE", "T"]: + input_dict[key] = True From 951e7ad2318e3db33669a504552295f66dbd7eae Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 18:56:05 +0000 Subject: [PATCH 15/53] #11 update package info parser --- cppwg/parsers/package_info.py | 156 ++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index 3cbd57d..fa4d331 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -1,6 +1,7 @@ import os -import importlib +import importlib.util import logging +import sys import yaml from typing import Any, Optional @@ -87,14 +88,15 @@ def check_for_custom_generators(self, info: BaseInfo) -> None: module_name: str = os.path.splitext(filepath)[0] # /path/to/CustomGenerator class_name: str = os.path.basename(module_name) # CustomGenerator - custom_module = importlib.machinery.SourceFileLoader( - module_name, filepath - ).load_module() + 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( - custom_module, class_name + module, class_name ) # Replace the `info.custom_generator` string with a new object created @@ -152,9 +154,10 @@ def parse(self) -> PackageInfo: ) self.check_for_custom_generators(self.package_info) - # Parse module data - for eachModule in self.raw_package_info["modules"]: - module_defaults = { + # Parse the module data + for raw_module_info in self.raw_package_info["modules"]: + # Get module config from the raw module info + module_config = { "name": "cppwg_module", "source_locations": None, "classes": [], @@ -163,73 +166,96 @@ def parse(self) -> PackageInfo: "use_all_classes": False, "use_all_free_functions": False, } - module_defaults.update(global_config) + module_config.update(global_config) - for key in module_defaults.keys(): - if key in eachModule: - module_defaults[key] = eachModule[key] + for key in module_config.keys(): + if key in raw_module_info: + module_config[key] = raw_module_info[key] - # Do classes - class_info_collection = [] - module_defaults["use_all_classes"] = utils.is_option_ALL( - "classes", module_defaults + module_config["use_all_classes"] = utils.is_option_ALL( + module_config["classes"] ) - if not module_defaults["use_all_classes"]: - if module_defaults["classes"] is not None: - for eachClass in module_defaults["classes"]: - class_defaults = {"name_override": None, "source_file": None} - class_defaults.update(global_config) - - for key in class_defaults.keys(): - if key in eachClass: - class_defaults[key] = eachClass[key] - class_info = CppClassInfo(eachClass["name"], class_defaults) - self.check_for_custom_generators(class_info) - class_info_collection.append(class_info) - # Do functions - function_info_collection = [] - module_defaults["use_all_free_functions"] = utils.is_option_ALL( - "free_functions", module_defaults + module_config["use_all_free_functions"] = utils.is_option_ALL( + module_config["free_functions"] ) - if not module_defaults["use_all_free_functions"]: - if module_defaults["free_functions"] is not None: - for _ in module_defaults["free_functions"]: - ff_defaults = {"name_override": None, "source_file": None} - ff_defaults.update(global_config) - function_info = CppFreeFunctionInfo( - ff_defaults["name"], ff_defaults - ) - function_info_collection.append(function_info) - - variable_info_collection = [] - use_all_variables = utils.is_option_ALL("variables", module_defaults) - if not use_all_variables: - for _ in module_defaults["variables"]: - variable_defaults = {"name_override": None, "source_file": None} - variable_defaults.update(global_config) - variable_info = CppFreeFunctionInfo( - variable_defaults["name"], variable_defaults - ) - variable_info_collection.append(variable_info) - module_info = ModuleInfo(module_defaults["name"], module_defaults) + module_config["use_all_variables"] = utils.is_option_ALL( + module_config["variables"] + ) - module_info.class_info_collection = class_info_collection - for class_info in module_info.class_info_collection: - class_info.module_info = module_info + # Create the ModuleInfo object from the module config dict + module_info = ModuleInfo(module_config["name"], module_config) + self.check_for_custom_generators(module_info) - module_info.free_function_info_collection = function_info_collection - for free_function_info in module_info.free_function_info_collection: - free_function_info.module_info = module_info + # Connect the module to the package + module_info.package_info = self.package_info + self.package_info.module_info_collection.append(module_info) - module_info.variable_info_collection = variable_info_collection - for variable_info in module_info.variable_info_collection: - variable_info.module_info = module_info + # Parse the class data + if not module_config["use_all_classes"]: + 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) - self.package_info.module_info_collection.append(module_info) - module_info.package_info = self.package_info + for key in class_config.keys(): + if key in raw_class_info: + class_config[key] = raw_class_info[key] - self.check_for_custom_generators(module_info) + # 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) + + # Parse the free function data + if not module_config["use_all_free_functions"]: + if module_config["free_functions"]: + 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, + } + free_function_config.update(global_config) + + for key in free_function_config.keys(): + if key in raw_free_function_info: + free_function_config[key] = raw_free_function_info[key] + + # Create the CppFreeFunctionInfo object from the free function config dict + free_function_info = CppFreeFunctionInfo( + 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 + ) + + # 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) + + 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 + ) + + # Connect the variable to the module + variable_info.module_info = module_info + module_info.variable_info_collection.append(variable_info) return self.package_info From 04176f96ea382730748e018a4fab99b5a93509db Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 19:36:58 +0000 Subject: [PATCH 16/53] #11 update module_info and package_info --- cppwg/generators.py | 56 ++++++++++++----------- cppwg/input/module_info.py | 88 +++++++++++++++++++++++++++---------- cppwg/input/package_info.py | 46 ++++++++++--------- 3 files changed, 117 insertions(+), 73 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 0481143..19fcdc0 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -29,22 +29,22 @@ class CppWrapperGenerator: Attributes ---------- - source_root : str - The root directory of the C++ source code - source_includes : list[str] - The list of source include paths - wrapper_root : str - The output directory for the wrapper code - castxml_binary : str - The path to the CastXML binary - package_info_file : str - The path to the package info yaml config file; defaults to "package_info.yaml" - source_hpp_files : list[str] - The list of C++ source header files - source_ns : namespace_t - The namespace containing C++ declarations parsed from the source tree - package_info : PackageInfo - A data structure containing the information parsed from package_info_file + source_root : str + The root directory of the C++ source code + source_includes : list[str] + The list of source include paths + wrapper_root : str + The output directory for the wrapper code + castxml_binary : str + The path to the CastXML binary + package_info_file : str + The path to the package info yaml config file; defaults to "package_info.yaml" + source_hpp_files : list[str] + The list of C++ source header files + source_ns : namespace_t + The namespace containing C++ declarations parsed from the source tree + package_info : PackageInfo + A data structure containing the information parsed from package_info_file """ def __init__( @@ -64,9 +64,8 @@ def __init__( # Sanitize source_root self.source_root: str = os.path.realpath(source_root) - if not os.path.exists(self.source_root): - message = f"Could not find source root directory: {source_root}" - logger.error(message) + if not os.path.isdir(self.source_root): + logger.error(f"Could not find source root directory: {source_root}") raise FileNotFoundError() # Sanitize wrapper_root @@ -75,7 +74,7 @@ def __init__( # Create the specified wrapper root directory if it doesn't exist self.wrapper_root = os.path.realpath(wrapper_root) - if not os.path.exists(self.wrapper_root): + if not os.path.isdir(self.wrapper_root): logger.info( f"Could not find wrapper root directory - creating it at {self.wrapper_root}" ) @@ -94,9 +93,10 @@ def __init__( ] for include_path in self.source_includes: - if not os.path.exists(include_path): - message = f"Could not find source include directory: {include_path}" - logger.error(message) + if not os.path.isdir(include_path): + logger.error( + f"Could not find source include directory: {include_path}" + ) raise FileNotFoundError() else: self.source_includes = [self.source_root] @@ -106,14 +106,13 @@ def __init__( if package_info_file: # If a package info config file is specified, check that it exists self.package_info_file = package_info_file - if not os.path.exists(package_info_file): - message = f"Could not find package info file: {package_info_file}" - logger.error(message) + if not os.path.isfile(package_info_file): + logger.error(f"Could not find package info file: {package_info_file}") raise FileNotFoundError() else: # If no package info config file has been supplied, check the default default_package_info_file = os.path.realpath("./package_info.yaml") - if os.path.exists(default_package_info_file): + if os.path.isfile(default_package_info_file): self.package_info_file = default_package_info_file logger.info( f"Package info file not specified - using {default_package_info_file}" @@ -211,8 +210,7 @@ def parse_package_info(self): if self.package_info_file: # If a package info file exists, parse it to create a PackageInfo object info_parser = PackageInfoParser(self.package_info_file, self.source_root) - info_parser.parse() - self.package_info = info_parser.package_info + self.package_info = info_parser.parse() else: # If no package info file exists, create a PackageInfo object with default settings diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 6ddf068..ac86044 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,41 +1,81 @@ -from cppwg.input import base_info +import os -class ModuleInfo(base_info.BaseInfo): +from typing import Any, 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.input.module_info import ModuleInfo +from cppwg.input.package_info import PackageInfo + +from pygccxml.declarations import declaration_t + + +class ModuleInfo(BaseInfo): """ - Information for individual modules + This class holds information for individual modules + + 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 """ - def __init__(self, name, type_info_dict = None): - + def __init__(self, name: str, module_config: Optional[dict[str, Any]] = None): + super(ModuleInfo, self).__init__(name) - self.package_info = None - self.source_locations = None - self.class_info_collection = [] - self.free_function_info_collection = [] - self.variable_info_collection = [] - self.use_all_classes = False - self.use_all_free_functions = False - - if type_info_dict is not None: - for key in type_info_dict: - setattr(self, key, type_info_dict[key]) - + self.package_info: PackageInfo = None + self.source_locations: list[str] = None + self.class_info_collection: list[CppClassInfo] = [] + self.free_function_info_collection: list[CppFreeFunctionInfo] = [] + self.variable_info_collection: list[CppFreeFunctionInfo] = [] + self.use_all_classes: bool = False + self.use_all_free_functions: bool = False + self.use_all_variables: bool = False + + if module_config: + for key, value in module_config.items(): + setattr(self, key, value) + @property - def parent(self): + def parent(self) -> PackageInfo: return self.package_info - def is_decl_in_source_path(self, decl): - + def is_decl_in_source_path(self, decl: declaration_t) -> bool: """ - Return is the declaration associated with a file in the current source path + Check if the declaration is associated with a file in the current source path + + Parameters + ---------- + decl : pygccxml.declarations.declaration_t + The declaration to check + + Returns + ------- + bool + True if the declaration is associated with a file in the current source path """ if self.source_locations is None: return True - for eachSourceLocation in self.source_locations: - location = self.package_info.source_root + "/" + eachSourceLocation + "/" - if location in decl.location.file_name: + for source_location in self.source_locations: + full_path = os.path.join(self.package_info.source_root, source_location) + if full_path in decl.location.file_name: return True return False diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index e042507..e921206 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -6,6 +6,23 @@ class PackageInfo(BaseInfo): """ This class holds the package information + + Attributes + ---------- + 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 + 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 """ def __init__( @@ -17,12 +34,12 @@ def __init__( """ 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 + 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(PackageInfo, self).__init__(name) @@ -40,19 +57,8 @@ def __init__( setattr(self, key, value) @property - def parent(self): - return None - - def is_decl_in_source_path(self, decl): + def parent(self) -> None: """ - Return is the declaration associated with a file in the current source path + Returns None as this is the top level object in the hierarchy """ - - if self.source_locations is None: - return True - - for eachSourceLocation in self.source_locations: - location = self.package_info.source_root + "/" + eachSourceLocation + "/" - if location in decl.location.file_name: - return True - return False + return None From eacfc8c5919955be2e4dbd297ac20946fbdfdd73 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 19:37:41 +0000 Subject: [PATCH 17/53] #11 update base_info --- cppwg/input/base_info.py | 76 +++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index f337897..502da35 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -3,41 +3,42 @@ class BaseInfo: """ - Generic information structure for features (i.e packages, modules, cpp types, etc.) + Generic information structure for features (i.e packages, modules, classes, + free functions, etc.) 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. - custom_generator : str, optional - A custom generator for the feature. - excluded_methods : list[str] - Do not include these methods. - excluded_variables : list[str] - Do not include these variables. - constructor_arg_type_excludes : list[str] - List of exlude patterns for ctors. - return_type_excludes : list[str] - List of exlude patterns for return types. - arg_type_excludes : list[str] - List of exlude patterns for arg types. - name_replacements : dict[str, str] + 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. + custom_generator : str, optional + A custom generator for the feature. + excluded_methods : list[str] + Do not include these methods. + excluded_variables : list[str] + Do not include these variables. + constructor_arg_type_excludes : list[str] + List of exlude patterns for ctors. + return_type_excludes : list[str] + List of exlude patterns for return types. + arg_type_excludes : list[str] + List of exlude patterns for arg types. + name_replacements : dict[str, str] A list of name replacements. """ @@ -74,7 +75,16 @@ def __init__(self, name): } @property - def parent(self): + def parent(self) -> Optional["BaseInfo"]: + """ + Return the parent object of the feature. This is overriden in derived + classes - a module returns a package, a classe returns a module, etc. + + Returns + ------- + Optional[BaseInfo] + The parent object. + """ return None def hierarchy_attribute(self, attribute_name): From 925edc71f7213e3746f8ade3fe1d83a84d7c4e16 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 20:04:17 +0000 Subject: [PATCH 18/53] #11 update cpp type info --- cppwg/input/cpp_type_info.py | 79 +++++++++++++++++++---------------- cppwg/parsers/package_info.py | 2 +- cppwg/utils/utils.py | 18 +++++--- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index cea3eb6..5df56a8 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -1,36 +1,43 @@ -""" -Information structure common to C++ variables, functions, methods and classes -""" +from typing import Any, Optional -from cppwg.input import base_info +from cppwg.input.base_info import BaseInfo +from cppwg.input.module_info import ModuleInfo -class CppTypeInfo(base_info.BaseInfo): - +class CppTypeInfo(BaseInfo): """ - :param: module_info - info of the owning module - :param: decl - the pygccxml version of the declaration - :param: source_file - over-ridden feature source file - :param: name_override - feature name override + This class holds informatioin for C++ variables and functions + + Attributes + ---------- + module_info : ModuleInfo + The module info parent object associated with this variable or function + source_file : str + The source file containing the variable or function + source_file_full_path : str + The full path to the source file containing the variable or function + name_override : str + The name override specified in config e.g. "SharedPottsMeshGenerator" -> "PottsMeshGenerator" + decl : declaration_t + The pygccxml declaration associated with this variable or function """ - def __init__(self, name, type_info_dict = None): - + def __init__(self, name: str, type_config: Optional[list[str, Any]] = None): + super(CppTypeInfo, self).__init__(name) - + self.module_info = None self.source_file_full_path = None self.source_file = None self.name_override = None self.template_args = None self.decl = None - if type_info_dict is not None: - for key in type_info_dict: - setattr(self, key, type_info_dict[key]) - - - def get_short_names(self): + if type_config is not None: + for key, value in type_config.items(): + setattr(self, key, value) + + def get_short_names(self): """ Return the name of the class as it will appear on the Python side. This collapses template arguements, separating them by underscores and removes @@ -53,17 +60,18 @@ def get_short_names(self): current_name = str(eachTemplateEntry) for eachReplacementString in self.name_replacements.keys(): replacement = self.name_replacements[eachReplacementString] - current_name = current_name.replace(eachReplacementString, - replacement) + current_name = current_name.replace( + eachReplacementString, replacement + ) - table = current_name.maketrans(dict.fromkeys('<>:,')) + table = current_name.maketrans(dict.fromkeys("<>:,")) cleaned_entry = current_name.translate(table) cleaned_entry = cleaned_entry.replace(" ", "") if len(cleaned_entry) > 1: first_letter = cleaned_entry[0].capitalize() cleaned_entry = first_letter + cleaned_entry[1:] template_string += str(cleaned_entry) - if(idx != len(eachTemplateArg)-1): + if idx != len(eachTemplateArg) - 1: template_string += "_" current_name = self.name @@ -73,20 +81,18 @@ def get_short_names(self): # Do standard translations for eachReplacementString in self.name_replacements.keys(): replacement = self.name_replacements[eachReplacementString] - current_name = current_name.replace(eachReplacementString, - replacement) + current_name = current_name.replace(eachReplacementString, replacement) # Strip templates and scopes - table = current_name.maketrans(dict.fromkeys('<>:,')) + table = current_name.maketrans(dict.fromkeys("<>:,")) cleaned_name = current_name.translate(table) cleaned_name = cleaned_name.replace(" ", "") if len(cleaned_name) > 1: - cleaned_name = cleaned_name[0].capitalize()+cleaned_name[1:] - names.append(cleaned_name+template_string) + cleaned_name = cleaned_name[0].capitalize() + cleaned_name[1:] + names.append(cleaned_name + template_string) return names def get_full_names(self): - """ Return the name (declaration) of the class as it appears on the C++ side. The return type is a list, as a class can have multiple @@ -101,7 +107,7 @@ def get_full_names(self): template_string = "<" for idx, eachTemplateEntry in enumerate(eachTemplateArg): template_string += str(eachTemplateEntry) - if(idx == len(eachTemplateArg)-1): + if idx == len(eachTemplateArg) - 1: template_string += " >" else: template_string += "," @@ -109,17 +115,17 @@ def get_full_names(self): return names def needs_header_file_instantiation(self): - """ Does this class need to be instantiated in the header file """ - return ((self.template_args is not None) and - (not self.include_file_only) and - (self.needs_instantiation)) + return ( + (self.template_args is not None) + and (not self.include_file_only) + and (self.needs_instantiation) + ) def needs_header_file_typdef(self): - """ Does this type need to be typdef'd with a nicer name in the header file. All template classes need this. @@ -128,9 +134,8 @@ def needs_header_file_typdef(self): return (self.template_args is not None) and (not self.include_file_only) def needs_auto_wrapper_generation(self): - """ Does this class need a wrapper to be autogenerated. """ - return not self.include_file_only \ No newline at end of file + return not self.include_file_only diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index fa4d331..88aaa8d 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -71,7 +71,7 @@ def check_for_custom_generators(self, info: BaseInfo) -> None: # 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", self.source_root + utils.SOURCE_ROOT_STRING, self.source_root ) filepath = os.path.realpath(filepath) diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py index 2ee1a36..1349da7 100644 --- a/cppwg/utils/utils.py +++ b/cppwg/utils/utils.py @@ -1,11 +1,17 @@ """ -Utility functions for the cppwg package +Utility functions and constants for the cppwg package """ from typing import Any +SOURCE_ROOT_STRING = "CPPWG_SOURCEROOT" +ALL_STRING = "CPPWG_ALL" -def is_option_ALL(input_obj: Any, option_ALL_string: str = "CPPWG_ALL") -> bool: +TRUE_STRINGS = ["ON", "YES", "Y", "TRUE", "T"] +FALSE_STRINGS = ["OFF", "NO", "N", "FALSE", "F"] + + +def is_option_ALL(input_obj: Any, option_ALL_string: str = ALL_STRING) -> bool: """ Check if the input is a string that matches the "ALL" indicator e.g. "CPPWG_ALL" @@ -42,8 +48,8 @@ def substitute_bool_for_string(input_dict: dict[Any, Any], key: Any) -> None: caps_string = input_dict[key].strip().upper() - if caps_string in ["OFF", "NO", "N", "FALSE", "F"]: - input_dict[key] = False - - elif caps_string in ["ON", "YES", "Y", "TRUE", "T"]: + if caps_string in TRUE_STRINGS: input_dict[key] = True + + elif caps_string in FALSE_STRINGS: + input_dict[key] = False From f789684ca17349aeb45ee67ef5a7b00a8e233b55 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 20:25:22 +0000 Subject: [PATCH 19/53] #11 fix circular type hint imports --- cppwg/input/class_info.py | 16 +++++++--------- cppwg/input/cpp_type_info.py | 13 +++++++------ cppwg/input/free_function_info.py | 7 +++---- cppwg/input/module_info.py | 14 +++++--------- cppwg/input/package_info.py | 16 +++++++--------- 5 files changed, 29 insertions(+), 37 deletions(-) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 96c399c..81cfd77 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,17 +1,15 @@ -""" -Information structure common to C++ classes -""" +from cppwg.input.cpp_type_info import CppTypeInfo -from cppwg.input import cpp_type_info +class CppClassInfo(CppTypeInfo): + """ + Information structure common to C++ classes + """ -class CppClassInfo(cpp_type_info.CppTypeInfo): + def __init__(self, name, type_info_dict=None): - - def __init__(self, name, type_info_dict = None): - super(CppClassInfo, self).__init__(name, type_info_dict) - + @property def parent(self): return self.module_info diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index 5df56a8..3334cdf 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -1,7 +1,8 @@ from typing import Any, Optional from cppwg.input.base_info import BaseInfo -from cppwg.input.module_info import ModuleInfo + +from pygccxml.declarations import declaration_t class CppTypeInfo(BaseInfo): @@ -26,12 +27,12 @@ def __init__(self, name: str, type_config: Optional[list[str, Any]] = None): super(CppTypeInfo, self).__init__(name) - self.module_info = None - self.source_file_full_path = None - self.source_file = None - self.name_override = None + self.module_info = None # : ModuleInfo + self.source_file_full_path: str = None + self.source_file: str = None + self.name_override: str = None self.template_args = None - self.decl = None + self.decl: declaration_t = None if type_config is not None: for key, value in type_config.items(): diff --git a/cppwg/input/free_function_info.py b/cppwg/input/free_function_info.py index 1eaacb3..18ec213 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/input/free_function_info.py @@ -6,15 +6,14 @@ class CppFreeFunctionInfo(cpp_type_info.CppTypeInfo): - """ A container for free function types to be wrapped """ - def __init__(self, name, type_info_dict = None): - + def __init__(self, name, type_info_dict=None): + super(CppFreeFunctionInfo, self).__init__(name, type_info_dict) @property def parent(self): - return self.module_info \ No newline at end of file + return self.module_info diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index ac86044..92bb250 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -3,10 +3,6 @@ from typing import Any, 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.input.module_info import ModuleInfo -from cppwg.input.package_info import PackageInfo from pygccxml.declarations import declaration_t @@ -39,11 +35,11 @@ def __init__(self, name: str, module_config: Optional[dict[str, Any]] = None): super(ModuleInfo, self).__init__(name) - self.package_info: PackageInfo = None + self.package_info = None # : PackageInfo self.source_locations: list[str] = None - self.class_info_collection: list[CppClassInfo] = [] - self.free_function_info_collection: list[CppFreeFunctionInfo] = [] - self.variable_info_collection: list[CppFreeFunctionInfo] = [] + self.class_info_collection: list = [] # : list[CppClassInfo] + self.free_function_info_collection: list = [] # : list[CppFreeFunctionInfo] + self.variable_info_collection: list = [] # : list[CppFreeFunctionInfo] self.use_all_classes: bool = False self.use_all_free_functions: bool = False self.use_all_variables: bool = False @@ -53,7 +49,7 @@ def __init__(self, name: str, module_config: Optional[dict[str, Any]] = None): setattr(self, key, value) @property - def parent(self) -> PackageInfo: + def parent(self): # -> PackageInfo return self.package_info def is_decl_in_source_path(self, decl: declaration_t) -> bool: diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index e921206..34d5c26 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -35,22 +35,20 @@ def __init__( 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(PackageInfo, self).__init__(name) - self.name = name - self.source_locations = None - self.module_info_collection = [] - self.source_root = source_root - self.source_hpp_patterns = ["*.hpp"] - self.source_hpp_files = [] - self.common_include_file = False + self.name: str = name + self.source_locations: list[str] = None + self.module_info_collection: list = [] # list[ModuleInfo] + 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 if package_config: for key, value in package_config.items(): From 681243333f952dfa07834f20a19f149989c89024 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 20:50:08 +0000 Subject: [PATCH 20/53] #11 more updates to info --- cppwg/input/cpp_type_info.py | 4 ++-- cppwg/input/free_function_info.py | 21 ++++++++++++--------- cppwg/input/module_info.py | 13 ++++++++----- cppwg/input/package_info.py | 2 +- cppwg/input/variable_info.py | 16 ++++++---------- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index 3334cdf..e78b99a 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -23,11 +23,11 @@ class CppTypeInfo(BaseInfo): The pygccxml declaration associated with this variable or function """ - def __init__(self, name: str, type_config: Optional[list[str, Any]] = None): + def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): super(CppTypeInfo, self).__init__(name) - self.module_info = None # : ModuleInfo + self.module_info: Optional["ModuleInfo"] = None self.source_file_full_path: str = None self.source_file: str = None self.name_override: str = None diff --git a/cppwg/input/free_function_info.py b/cppwg/input/free_function_info.py index 18ec213..a5d1e16 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/input/free_function_info.py @@ -1,19 +1,22 @@ -""" -Information for free functions -""" +from typing import Any, Optional -from cppwg.input import cpp_type_info +from cppwg.input.cpp_type_info import CppTypeInfo -class CppFreeFunctionInfo(cpp_type_info.CppTypeInfo): +class CppFreeFunctionInfo(CppTypeInfo): """ - A container for free function types to be wrapped + This class holds information for individual free functions to be wrapped """ - def __init__(self, name, type_info_dict=None): + def __init__( + self, name: str, free_function_config: Optional[dict[str, Any]] = None + ): - super(CppFreeFunctionInfo, self).__init__(name, type_info_dict) + super(CppFreeFunctionInfo, self).__init__(name, free_function_config) @property - def parent(self): + def parent(self) -> "ModuleInfo": + """ + Returns the parent module info object + """ return self.module_info diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 92bb250..9bfecd8 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -35,11 +35,11 @@ def __init__(self, name: str, module_config: Optional[dict[str, Any]] = None): super(ModuleInfo, self).__init__(name) - self.package_info = None # : PackageInfo + self.package_info: Optional["PackageInfo"] = None self.source_locations: list[str] = None - self.class_info_collection: list = [] # : list[CppClassInfo] - self.free_function_info_collection: list = [] # : list[CppFreeFunctionInfo] - self.variable_info_collection: list = [] # : list[CppFreeFunctionInfo] + self.class_info_collection: list["CppClassInfo"] = [] + self.free_function_info_collection: list["CppFreeFunctionInfo"] = [] + self.variable_info_collection: list["CppFreeFunctionInfo"] = [] self.use_all_classes: bool = False self.use_all_free_functions: bool = False self.use_all_variables: bool = False @@ -49,7 +49,10 @@ def __init__(self, name: str, module_config: Optional[dict[str, Any]] = None): setattr(self, key, value) @property - def parent(self): # -> PackageInfo + def parent(self) -> "PackageInfo": + """ + Returns the parent package info object + """ return self.package_info def is_decl_in_source_path(self, decl: declaration_t) -> bool: diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index 34d5c26..7bb3aae 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -44,7 +44,7 @@ def __init__( self.name: str = name self.source_locations: list[str] = None - self.module_info_collection: list = [] # list[ModuleInfo] + self.module_info_collection: list["ModuleInfo"] = [] self.source_root: str = source_root self.source_hpp_patterns: list[str] = ["*.hpp"] self.source_hpp_files: list[str] = [] diff --git a/cppwg/input/variable_info.py b/cppwg/input/variable_info.py index 77f2c5a..9ce2c5a 100644 --- a/cppwg/input/variable_info.py +++ b/cppwg/input/variable_info.py @@ -1,17 +1,13 @@ -""" -Information for variables -""" +from typing import Any, Optional -from cppwg.input import cpp_type_info +from cppwg.input.cpp_type_info import CppTypeInfo -class CppVariableInfo(cpp_type_info.CppTypeInfo): - +class CppVariableInfo(CppTypeInfo): """ - A container for variable types to be wrapped + This class holds information for individual variable types to be wrapped """ - def __init__(self, name, type_info_dict = None): - - super(CppVariableInfo, self).__init__(name, type_info_dict) + def __init__(self, name, variable_config: Optional[dict[str, Any]] = None): + super(CppVariableInfo, self).__init__(name, variable_config) From 0a9666bc364448ceb6124a4370b62e16e7869a92 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 21:07:54 +0000 Subject: [PATCH 21/53] #11 extra info updates --- cppwg/input/class_info.py | 13 +++++++++---- cppwg/input/cpp_type_info.py | 10 +++++----- cppwg/input/method_info.py | 32 +++++++++++++++++--------------- cppwg/input/variable_info.py | 4 ++-- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 81cfd77..749b10e 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,15 +1,20 @@ +from typing import Any, Optional + from cppwg.input.cpp_type_info import CppTypeInfo class CppClassInfo(CppTypeInfo): """ - Information structure common to C++ classes + This class holds information for individual C++ classes to be wrapped """ - def __init__(self, name, type_info_dict=None): + def __init__(self, name: str, class_config: Optional[dict[str, Any]] = None): - super(CppClassInfo, self).__init__(name, type_info_dict) + super(CppClassInfo, self).__init__(name, class_config) @property - def parent(self): + def parent(self) -> "ModuleInfo": + """ + Returns the parent module info object + """ return self.module_info diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index e78b99a..a07e7c3 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -7,20 +7,20 @@ class CppTypeInfo(BaseInfo): """ - This class holds informatioin for C++ variables and functions + This class holds information for C++ types including classes, free functions etc. Attributes ---------- module_info : ModuleInfo - The module info parent object associated with this variable or function + The module info parent object associated with this type source_file : str - The source file containing the variable or function + The source file containing the type source_file_full_path : str - The full path to the source file containing the variable or function + The full path to the source file containing the type name_override : str The name override specified in config e.g. "SharedPottsMeshGenerator" -> "PottsMeshGenerator" decl : declaration_t - The pygccxml declaration associated with this variable or function + The pygccxml declaration associated with this type """ def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): diff --git a/cppwg/input/method_info.py b/cppwg/input/method_info.py index 1bd3def..ffb4f8d 100644 --- a/cppwg/input/method_info.py +++ b/cppwg/input/method_info.py @@ -1,25 +1,27 @@ -""" -Information for methods -""" +from typing import Optional -from cppwg.input import cpp_type_info +from cppwg.input.cpp_type_info import CppTypeInfo -class CppMethodInfo(cpp_type_info.CppTypeInfo): - +class CppMethodInfo(CppTypeInfo): """ - A container for method types to be wrapped + This class holds information for individual methods to be wrapped + + Attributes + ---------- + class_info : CppClassInfo + The class info parent object associated with this method """ - def __init__(self, name, _): - + def __init__(self, name: str, _): + super(CppMethodInfo, self).__init__(name) - - self.class_info = None - + + self.class_info: Optional["CppClassInfo"] = None + @property def parent(self): + """ + Returns the parent class info object + """ return self.class_info - - - diff --git a/cppwg/input/variable_info.py b/cppwg/input/variable_info.py index 9ce2c5a..894df08 100644 --- a/cppwg/input/variable_info.py +++ b/cppwg/input/variable_info.py @@ -5,9 +5,9 @@ class CppVariableInfo(CppTypeInfo): """ - This class holds information for individual variable types to be wrapped + This class holds information for individual variables to be wrapped """ - def __init__(self, name, variable_config: Optional[dict[str, Any]] = None): + def __init__(self, name: str, variable_config: Optional[dict[str, Any]] = None): super(CppVariableInfo, self).__init__(name, variable_config) From 87aba132ca37cfc1e61577cf68e30284164d7c20 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 5 Mar 2024 22:21:33 +0000 Subject: [PATCH 22/53] #11 set castxml cflags in generator --- cppwg/generators.py | 6 ++++++ cppwg/parsers/source_parser.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 19fcdc0..aefdcd7 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -37,6 +37,8 @@ class CppWrapperGenerator: The output directory for the wrapper code castxml_binary : str The path to the CastXML binary + castxml_cflags : str + Optional cflags to be passed to CastXML e.g. "-std=c++17" package_info_file : str The path to the package info yaml config file; defaults to "package_info.yaml" source_hpp_files : list[str] @@ -54,6 +56,7 @@ def __init__( wrapper_root: Optional[str] = None, castxml_binary: Optional[str] = "castxml", package_info_file: Optional[str] = None, + castxml_cflags: Optional[str] = "", ): logging.basicConfig( format="%(levelname)s %(message)s", @@ -133,6 +136,8 @@ def __init__( logger.info(castxml_version) logger.info(f"pygccxml version {pygccxml_version}") + self.castxml_cflags: str = castxml_cflags + # Initialize remaining attributes self.source_hpp_files: list[str] = [] @@ -199,6 +204,7 @@ def parse_header_collection(self, header_collection_path: str) -> None: header_collection_path, self.castxml_binary, self.source_includes, + self.castxml_cflags, ) self.source_ns = source_parser.parse() diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index ee2f530..ea8d92f 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -36,7 +36,7 @@ class CppSourceParser: The path to the CastXML binary source_includes : list[str] The list of source include paths - cflags : str + castxml_cflags : str Optional cflags to be passed to CastXML e.g. "-std=c++17" global_ns : namespace_t The namespace containing all parsed C++ declarations @@ -50,13 +50,13 @@ def __init__( wrapper_header_collection: str, castxml_binary: str, source_includes: list[str], - cflags: str = "", + castxml_cflags: str = "", ): self.source_root: str = source_root self.wrapper_header_collection: str = wrapper_header_collection self.castxml_binary: str = castxml_binary self.source_includes: list[str] = source_includes - self.cflags: str = cflags + self.castxml_cflags: str = castxml_cflags self.source_ns: Optional[namespace_t] = None self.global_ns: Optional[namespace_t] = None @@ -76,7 +76,7 @@ def parse(self) -> namespace_t: xml_generator_config = parser.xml_generator_configuration_t( xml_generator_path=self.castxml_binary, xml_generator="castxml", - cflags=self.cflags, + cflags=self.castxml_cflags, include_paths=self.source_includes, ) From 49988965e616d25030f30297364bc915f2c1dd98 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Wed, 6 Mar 2024 13:53:03 +0000 Subject: [PATCH 23/53] #11 update cpp type info --- cppwg/input/base_info.py | 58 ++++++++++----- cppwg/input/cpp_type_info.py | 130 +++++++++++++++++++-------------- cppwg/input/info_helper.py | 6 +- cppwg/parsers/source_parser.py | 2 +- cppwg/templates/custom.py | 2 +- cppwg/utils/utils.py | 2 +- 6 files changed, 120 insertions(+), 80 deletions(-) diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index 502da35..82fb949 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -39,7 +39,7 @@ class BaseInfo: arg_type_excludes : list[str] List of exlude patterns for arg types. name_replacements : dict[str, str] - A list of name replacements. + A dictionary of name replacements e.g. {"double":"Double", "unsigned int":"Unsigned"} """ def __init__(self, name): @@ -77,8 +77,9 @@ def __init__(self, name): @property def parent(self) -> Optional["BaseInfo"]: """ - Return the parent object of the feature. This is overriden in derived - classes - a module returns a package, a classe returns a module, etc. + 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 ------- @@ -87,31 +88,52 @@ def parent(self) -> Optional["BaseInfo"]: """ return None - def hierarchy_attribute(self, attribute_name): + def hierarchy_attribute(self, attribute_name: str): """ - For the supplied attribute iterate through parents until a non None - value is found. If the tope level parent attribute is None, return None. + For the supplied attribute iterate through parents 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) - else: - if hasattr(self, "parent") and self.parent is not None: - return self.parent.hierarchy_attribute(attribute_name) - else: - return None - def hierarchy_attribute_gather(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): """ For the supplied attribute iterate through parents gathering list entries. + + Parameters + ---------- + attribute_name : str + The attribute name to search for. + + Returns + ------- + list[Any] + The list of attribute values. """ - att_list = [] + 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)) - else: - if hasattr(self, "parent") and self.parent is not None: - att_list.extend(self.parent.hierarchy_attribute_gather(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 index a07e7c3..fb81f1f 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -19,6 +19,8 @@ class CppTypeInfo(BaseInfo): The full path to the source file containing the type name_override : str The name override specified in config e.g. "SharedPottsMeshGenerator" -> "PottsMeshGenerator" + template_arg_lists : list[list[Any]] + List of template replacement arguments for the type e.g. [[2, 2], [3, 3]] decl : declaration_t The pygccxml declaration associated with this type """ @@ -28,70 +30,86 @@ def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): super(CppTypeInfo, self).__init__(name) self.module_info: Optional["ModuleInfo"] = None - self.source_file_full_path: str = None - self.source_file: str = None - self.name_override: str = None - self.template_args = None - self.decl: declaration_t = None + self.source_file_full_path: Optional[str] = None + self.source_file: Optional[str] = None + self.name_override: Optional[str] = None + self.template_arg_lists: Optional[list[list[Any]]] = None + self.decl: Optional[declaration_t] = None - if type_config is not None: + if type_config: for key, value in type_config.items(): setattr(self, key, value) - def get_short_names(self): + def get_short_names(self) -> list[str]: """ Return the name of the class as it will appear on the Python side. This - collapses template arguements, 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. + 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 short name list + ["Foo2_2", "Foo3_3"]. + + Returns + ------- + list[str] + The list of short names """ - if self.template_args is None: + # Handles untemplated classes + if self.template_arg_lists is None: if self.name_override is None: return [self.name] - else: - return [self.name_override] + return [self.name_override] + + short_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 + + # Do standard name replacements e.g. "unsigned int" -> "Unsigned" + for name, replacement in self.name_replacements.items(): + type_name = type_name.replace(name, replacement) + + # Remove special characters + type_name = type_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:] + + # Create a string of template args separated by "_" e.g. 2_2 + for template_arg_list in self.template_arg_lists: + # Example template_arg_list : [2, 2] - names = [] - for eachTemplateArg in self.template_args: template_string = "" - for idx, eachTemplateEntry in enumerate(eachTemplateArg): - - # Do standard translations - current_name = str(eachTemplateEntry) - for eachReplacementString in self.name_replacements.keys(): - replacement = self.name_replacements[eachReplacementString] - current_name = current_name.replace( - eachReplacementString, replacement - ) - - table = current_name.maketrans(dict.fromkeys("<>:,")) - cleaned_entry = current_name.translate(table) - cleaned_entry = cleaned_entry.replace(" ", "") - if len(cleaned_entry) > 1: - first_letter = cleaned_entry[0].capitalize() - cleaned_entry = first_letter + cleaned_entry[1:] - template_string += str(cleaned_entry) - if idx != len(eachTemplateArg) - 1: + for idx, arg in enumerate(template_arg_list): + + # Do standard name replacements + arg_str = str(arg) + for name, replacement in self.name_replacements.items(): + arg_str = arg_str.replace(name, replacement) + + # Remove special characters + arg_str = arg_str.translate(rm_table) + + # Capitalize the first letter + if len(arg_str) > 1: + arg_str = arg_str[0].capitalize() + arg_str[1:] + + # Add "_" between template arguments + template_string += arg_str + if idx < len(template_arg_list) - 1: template_string += "_" - current_name = self.name - if self.name_override is not None: - current_name = self.name_override - - # Do standard translations - for eachReplacementString in self.name_replacements.keys(): - replacement = self.name_replacements[eachReplacementString] - current_name = current_name.replace(eachReplacementString, replacement) - - # Strip templates and scopes - table = current_name.maketrans(dict.fromkeys("<>:,")) - cleaned_name = current_name.translate(table) - cleaned_name = cleaned_name.replace(" ", "") - if len(cleaned_name) > 1: - cleaned_name = cleaned_name[0].capitalize() + cleaned_name[1:] - names.append(cleaned_name + template_string) - return names + short_names.append(type_name + template_string) + + return short_names def get_full_names(self): """ @@ -100,15 +118,15 @@ def get_full_names(self): names (declarations) if it is templated. """ - if self.template_args is None: + if self.template_arg_lists is None: return [self.name] names = [] - for eachTemplateArg in self.template_args: + for template_arg in self.template_arg_lists: template_string = "<" - for idx, eachTemplateEntry in enumerate(eachTemplateArg): + for idx, eachTemplateEntry in enumerate(template_arg): template_string += str(eachTemplateEntry) - if idx == len(eachTemplateArg) - 1: + if idx == len(template_arg) - 1: template_string += " >" else: template_string += "," @@ -121,7 +139,7 @@ def needs_header_file_instantiation(self): """ return ( - (self.template_args is not None) + (self.template_arg_lists is not None) and (not self.include_file_only) and (self.needs_instantiation) ) @@ -132,7 +150,7 @@ def needs_header_file_typdef(self): file. All template classes need this. """ - return (self.template_args is not None) and (not self.include_file_only) + return (self.template_arg_lists is not None) and (not self.include_file_only) def needs_auto_wrapper_generation(self): """ diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index f332918..febcc98 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -30,7 +30,7 @@ def expand_templates(self, feature_info, feature_type): return # Skip any features with pre-defined template args - no_template = feature_info.template_args is None + no_template = feature_info.template_arg_lists is None source_path = feature_info.source_file_full_path if not (no_template and source_path is not None): return @@ -49,7 +49,7 @@ def expand_templates(self, feature_info, feature_type): continue for idx, eachSub in enumerate(template_substitutions): - template_args = eachSub["replacement"] + template_arg_lists = eachSub["replacement"] template_string = eachSub["signature"] cleaned_string = template_string.replace(" ", "") if cleaned_string in stripped_line: @@ -57,7 +57,7 @@ def expand_templates(self, feature_info, feature_type): feature_decl_next = feature_string + ":" in stripped_next feature_decl_whole = feature_string == stripped_next if feature_decl_next or feature_decl_whole: - feature_info.template_args = template_args + feature_info.template_arg_lists = template_arg_lists break f.close() diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index ea8d92f..5e08e4a 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -101,7 +101,7 @@ def parse(self) -> namespace_t: decl for decl in filtered_decls if self.source_root in decl.location.file_name - or "wrapper_header_collection" in decl.location.file_name + or self.wrapper_header_collection in decl.location.file_name ] # Create a source namespace module for the filtered declarations diff --git a/cppwg/templates/custom.py b/cppwg/templates/custom.py index 0e5a372..e350ad0 100644 --- a/cppwg/templates/custom.py +++ b/cppwg/templates/custom.py @@ -4,7 +4,7 @@ custom code generators. """ -class Custom(object): +class Custom: def __init__(self): diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py index 1349da7..95f18c1 100644 --- a/cppwg/utils/utils.py +++ b/cppwg/utils/utils.py @@ -50,6 +50,6 @@ def substitute_bool_for_string(input_dict: dict[Any, Any], key: Any) -> None: if caps_string in TRUE_STRINGS: input_dict[key] = True - + elif caps_string in FALSE_STRINGS: input_dict[key] = False From 5a85db012131e504d7bb4442b7c810e8fe37a2e4 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 7 Mar 2024 14:32:11 +0000 Subject: [PATCH 24/53] #11 additional info updates --- cppwg/input/cpp_type_info.py | 40 ++++++++++++++++++++++-------------- cppwg/input/method_info.py | 2 +- cppwg/input/module_info.py | 2 +- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index fb81f1f..d67dd7b 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -111,28 +111,36 @@ def get_short_names(self) -> list[str]: return short_names - def get_full_names(self): + def get_full_names(self) -> list[str]: """ - Return the name (declaration) of the class as it appears on the C++ side. - The return type is a list, as a class can have multiple - names (declarations) if it is templated. + Return the name (declaration) of the class as it appears on the C++ + side. The return type is a list, as a class can have multiple names + (declarations) if it is templated. For example, a class "Foo" with + template arguments [[2, 2], [3, 3]] will have a full name list + ["Foo<2,2 >", "Foo<3,3 >"]. + + Returns + ------- + list[str] + The list of full names """ + # Handles untemplated classes if self.template_arg_lists is None: return [self.name] - names = [] - for template_arg in self.template_arg_lists: - template_string = "<" - for idx, eachTemplateEntry in enumerate(template_arg): - template_string += str(eachTemplateEntry) - if idx == len(template_arg) - 1: - template_string += " >" - else: - template_string += "," - names.append(self.name + template_string) - return names + full_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]) + template_string = "<" + template_string + " >" + + # Join full name e.g. "Foo<2,2 >" + full_names.append(self.name + template_string) + + return full_names + # TODO: This method is not used, remove it? def needs_header_file_instantiation(self): """ Does this class need to be instantiated in the header file @@ -144,6 +152,7 @@ def needs_header_file_instantiation(self): and (self.needs_instantiation) ) + # TODO: This method is not used, remove it? def needs_header_file_typdef(self): """ Does this type need to be typdef'd with a nicer name in the header @@ -152,6 +161,7 @@ def needs_header_file_typdef(self): return (self.template_arg_lists is not None) and (not self.include_file_only) + # TODO: This method is not used, remove it? def needs_auto_wrapper_generation(self): """ Does this class need a wrapper to be autogenerated. diff --git a/cppwg/input/method_info.py b/cppwg/input/method_info.py index ffb4f8d..18b7529 100644 --- a/cppwg/input/method_info.py +++ b/cppwg/input/method_info.py @@ -20,7 +20,7 @@ def __init__(self, name: str, _): self.class_info: Optional["CppClassInfo"] = None @property - def parent(self): + def parent(self) -> "CppClassInfo": """ Returns the parent class info object """ diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 9bfecd8..7d53b2a 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -61,7 +61,7 @@ def is_decl_in_source_path(self, decl: declaration_t) -> bool: Parameters ---------- - decl : pygccxml.declarations.declaration_t + decl : declaration_t The declaration to check Returns From 89c491ddfd8c6e434314d00626941ef61fe822d9 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 7 Mar 2024 18:21:44 +0000 Subject: [PATCH 25/53] #11 update info helper --- cppwg/generators.py | 8 ++- cppwg/input/base_info.py | 8 +-- cppwg/input/info_helper.py | 136 ++++++++++++++++++++++++------------- 3 files changed, 99 insertions(+), 53 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index aefdcd7..4bcc694 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -147,9 +147,11 @@ def __init__( def collect_source_hpp_files(self): """ - Walk through the source root and add any files matching the provided patterns. - Keep the wrapper root out of the search path to avoid pollution. + Walk through the source root and add any files matching the provided + patterns. Keep the wrapper root out of the search path to avoid + pollution. """ + # TODO: Check if file exists for root, _, filenames in os.walk(self.source_root, followlinks=True): for pattern in self.package_info.source_hpp_patterns: for filename in fnmatch.filter(filenames, pattern): @@ -293,7 +295,7 @@ def generate_wrapper(self): for module_info in self.package_info.module_info_collection: info_genenerator = CppInfoHelper(module_info) for class_info in module_info.class_info_collection: - info_genenerator.expand_templates(class_info, "class") + info_genenerator.expand_templates(class_info) # Generate the header collection header_collection_path = self.generate_header_collection() diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index 82fb949..9b38496 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -88,9 +88,9 @@ def parent(self) -> Optional["BaseInfo"]: """ return None - def hierarchy_attribute(self, attribute_name: str): + def hierarchy_attribute(self, attribute_name: str) -> Any: """ - For the supplied attribute iterate through parents until a non-None + 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. @@ -113,9 +113,9 @@ def hierarchy_attribute(self, attribute_name: str): return None - def hierarchy_attribute_gather(self, attribute_name: str): + def hierarchy_attribute_gather(self, attribute_name: str) -> list[Any]: """ - For the supplied attribute iterate through parents gathering list entries. + For the supplied attribute, iterate through parent objects gathering list entries. Parameters ---------- diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index febcc98..2ce5042 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -1,66 +1,110 @@ import os +import logging + +from cppwg.input.base_info import BaseInfo +from cppwg.input.class_info import CppClassInfo +from cppwg.input.module_info import ModuleInfo class CppInfoHelper: """ Helper class that attempts to automatically fill in extra feature information based on simple analysis of the source tree. + + Attributes + __________ + module_info : ModuleInfo + The module info object that this helper is working with. + class_dict : dict + A dictionary of class info objects keyed by class name. """ - def __init__(self, module_info): + def __init__(self, module_info: ModuleInfo): + + self.module_info: ModuleInfo = module_info + + # For convenience, collect class info in a dict keyed by name + self.class_dict: dict[str, CppClassInfo] = { + class_info.name: class_info + for class_info in module_info.class_info_collection + } + + def expand_templates(self, feature_info: BaseInfo) -> None: + """ + Extract template arguments for a feature from the associated source + file. + + Parameters + __________ + feature_info : BaseInfo + The feature info object to expand. + """ + logger = logging.getLogger() + + if isinstance(feature_info, CppClassInfo): + feature_type = "class" + else: + logger.error(f"Unsupported feature type: {type(feature_info)}") + raise TypeError() + + # Get list of template substitutions from this feature and its parents + # e.g. {"signature":"","replacement":[[2,2], [3,3]]} + template_substitutions: list[dict[str, str]] = ( + feature_info.hierarchy_attribute_gather("template_substitutions") + ) - self.module_info = module_info + # Skip if there are no template substitutions + if len(template_substitutions) == 0: + return - self.class_dict = {} - self.setup_class_dict() + # Skip if there are pre-defined template args + if feature_info.template_arg_lists: + return - def setup_class_dict(self): + # Skip if there is no source file + source_path = feature_info.source_file_full_path + if not source_path: + return - # For convenience collect class info in a dict keyed by name - for class_info in self.module_info.class_info_collection: - self.class_dict[class_info.name] = class_info + if not os.path.isfile(source_path): + logger.error(f"Could not find source file: {source_path}") + raise FileNotFoundError() - def expand_templates(self, feature_info, feature_type): + # Search for template signatures in the source file + with open(source_path, "r") as f: + # Remove spaces and blank lines + lines = [line.strip().replace(" ", "") for line in f] + lines = list(line for line in lines if line) - template_substitutions = feature_info.hierarchy_attribute_gather( - "template_substitutions" - ) + for idx in range(len(lines) - 1): + curr_line = lines[idx] + next_line = lines[idx + 1] - if len(template_substitutions) == 0: - return + for template_substitution in template_substitutions: + # e.g. + signature = template_substitution["signature"].replace(" ", "") - # Skip any features with pre-defined template args - no_template = feature_info.template_arg_lists is None - source_path = feature_info.source_file_full_path - if not (no_template and source_path is not None): - return - if not os.path.exists(source_path): - return + # e.g. [[2,2], [3,3]] + replacement = template_substitution["replacement"] - f = open(source_path) - lines = (line.rstrip() for line in f) # Remove blank lines - - lines = list(line for line in lines if line) - for idx, eachLine in enumerate(lines): - stripped_line = eachLine.replace(" ", "") - if idx + 1 < len(lines): - stripped_next = lines[idx + 1].replace(" ", "") - else: - continue - - for idx, eachSub in enumerate(template_substitutions): - template_arg_lists = eachSub["replacement"] - template_string = eachSub["signature"] - cleaned_string = template_string.replace(" ", "") - if cleaned_string in stripped_line: - feature_string = feature_type + feature_info.name - feature_decl_next = feature_string + ":" in stripped_next - feature_decl_whole = feature_string == stripped_next - if feature_decl_next or feature_decl_whole: - feature_info.template_arg_lists = template_arg_lists - break - f.close() + if signature in curr_line: + feature_string = feature_type + feature_info.name # e.g. "classFoo" + + # Simple declaration example: + # template + # classFoo + simple_declaration = feature_string == next_line - def do_custom_template_substitution(self, feature_info): + # Derived declaration example: + # template + # classFoo:publicBar + derived_declaration = feature_string + ":" in next_line - pass + # TODO: Add support for more cases + # e.g. + # template classFoo + # template classFoo:publicBar + + if simple_declaration or derived_declaration: + feature_info.template_arg_lists = replacement + break From 565256d0796aa76a50c8342132be4175852155f4 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 7 Mar 2024 18:45:16 +0000 Subject: [PATCH 26/53] #11 extra info helper updates --- cppwg/input/info_helper.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index 2ce5042..7973eec 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -1,4 +1,5 @@ import os +import re import logging from cppwg.input.base_info import BaseInfo @@ -70,12 +71,13 @@ def expand_templates(self, feature_info: BaseInfo) -> None: logger.error(f"Could not find source file: {source_path}") raise FileNotFoundError() - # Search for template signatures in the source file - with open(source_path, "r") as f: - # Remove spaces and blank lines - lines = [line.strip().replace(" ", "") for line in f] - lines = list(line for line in lines if line) + # Remove whitespace and blank lines from the source file + whitespace_regex = re.compile(r"\s+") + with open(source_path, "r") as in_file: + lines = [re.sub(whitespace_regex, "", line) for line in in_file] + lines = [line for line in lines if line] + # Search for template signatures in the source file lines for idx in range(len(lines) - 1): curr_line = lines[idx] next_line = lines[idx + 1] From cdcae0db9f5eadc873b4a642e6d9219537d1285b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 8 Mar 2024 23:11:06 +0000 Subject: [PATCH 27/53] #11 update generators --- cppwg/generators.py | 153 ++++++++++++++++++++-------------- cppwg/input/info_helper.py | 2 +- cppwg/parsers/package_info.py | 3 +- cppwg/utils/constants.py | 12 +++ cppwg/utils/utils.py | 15 ++-- 5 files changed, 110 insertions(+), 75 deletions(-) create mode 100644 cppwg/utils/constants.py diff --git a/cppwg/generators.py b/cppwg/generators.py index 4bcc694..5bf7a00 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -4,6 +4,7 @@ import logging import subprocess +from pathlib import Path from typing import Optional from pygccxml import __version__ as pygccxml_version @@ -20,7 +21,9 @@ from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter from cppwg.writers.module_writer import CppModuleWrapperWriter -import cppwg.templates.pybind11_default as wrapper_templates +from cppwg.templates import pybind11_default as wrapper_templates + +from cppwg.utils.constants import CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILE class CppWrapperGenerator: @@ -145,50 +148,52 @@ def __init__( self.package_info: Optional[PackageInfo] = None - def collect_source_hpp_files(self): + def collect_source_hpp_files(self) -> None: """ Walk through the source root and add any files matching the provided - patterns. Keep the wrapper root out of the search path to avoid - pollution. + patterns e.g. "*.hpp". Skip the wrapper root and wrappers to + avoid pollution. """ - # TODO: Check if file exists + for root, _, filenames in os.walk(self.source_root, followlinks=True): for pattern in self.package_info.source_hpp_patterns: for filename in fnmatch.filter(filenames, pattern): - if "cppwg" not in filename: - self.package_info.source_hpp_files.append( - os.path.join(root, filename) - ) - self.package_info.source_hpp_files = [ - path - for path in self.package_info.source_hpp_files - if self.wrapper_root not in path - ] - - def generate_header_collection(self) -> str: - """ - Write the header collection to file + filepath = os.path.realpath(os.path.join(root, filename)) - Returns - ------- - str - The path to the header collection file - """ + # Skip files in wrapper root dir + if Path(self.wrapper_root) in Path(filepath).parents: + continue - header_collection_filename = "wrapper_header_collection.hpp" + # Skip files with the extensions like .cppwg.hpp + suffix = os.path.splitext(os.path.splitext(filename)[0])[1] + if suffix == CPPWG_EXT: + continue - header_collection_writer = CppHeaderCollectionWriter( - self.package_info, - self.wrapper_root, - header_collection_filename, - ) - header_collection_writer.write() + self.package_info.source_hpp_files.append(filepath) - header_collection_path = os.path.join( - self.wrapper_root, header_collection_filename - ) + def extract_templates_from_source(self) -> None: + """ + Extract template arguments for each class from the associated source file + """ - return header_collection_path + for module_info in self.package_info.module_info_collection: + info_genenerator = CppInfoHelper(module_info) + for class_info in module_info.class_info_collection: + info_genenerator.extract_templates_from_source(class_info) + + def map_classes_to_hpp_files(self) -> None: + """ + Attempt to map source file paths to each class, assuming the containing + file name is the class name + """ + for module_info in self.package_info.module_info_collection: + for class_info in module_info.class_info_collection: + for hpp_file_path in self.package_info.source_hpp_files: + hpp_file_name = os.path.basename(hpp_file_path) + if class_info.name == os.path.splitext(hpp_file_name)[0]: + class_info.source_file_full_path = hpp_file_path + if class_info.source_file is None: + class_info.source_file = hpp_file_name def parse_header_collection(self, header_collection_path: str) -> None: """ @@ -269,7 +274,43 @@ def update_class_info(self): if len(class_decls) == 1: class_info.decl = class_decls[0] - def generate_wrapper(self): + def write_header_collection(self) -> str: + """ + Write the header collection to file + + Returns + ------- + str + The path to the header collection file + """ + + header_collection_writer = CppHeaderCollectionWriter( + self.package_info, + self.wrapper_root, + CPPWG_HEADER_COLLECTION_FILE, + ) + header_collection_writer.write() + + header_collection_path = os.path.join( + self.wrapper_root, CPPWG_HEADER_COLLECTION_FILE + ) + + return header_collection_path + + def write_wrappers(self) -> None: + """ + Write all the wrappers required for the package + """ + for module_info in self.package_info.module_info_collection: + module_writer = CppModuleWrapperWriter( + self.source_ns, + module_info, + wrapper_templates.template_collection, + self.wrapper_root, + ) + module_writer.write() + + def generate_wrapper(self) -> None: """ Main method for generating all the wrappers """ @@ -277,42 +318,26 @@ def generate_wrapper(self): # Parse the package info file self.parse_package_info() - # Generate a header collection + # Search for header files in the source root self.collect_source_hpp_files() - # Attempt to assign source paths to each class, assuming the containing - # file name is the class name - for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: - for hpp_file_path in self.package_info.source_hpp_files: - hpp_file_name = os.path.basename(hpp_file_path) - if class_info.name == hpp_file_name.split(".")[0]: - class_info.source_file_full_path = hpp_file_path - if class_info.source_file is None: - class_info.source_file = hpp_file_name + # Map each class to a header file + self.map_classes_to_hpp_files() - # Attempt to automatically generate template args for each class - for module_info in self.package_info.module_info_collection: - info_genenerator = CppInfoHelper(module_info) - for class_info in module_info.class_info_collection: - info_genenerator.expand_templates(class_info) + # Attempt to extract template args for each class from the source file + self.extract_templates_from_source() - # Generate the header collection - header_collection_path = self.generate_header_collection() + # Write the header collection to file + header_collection_path = self.write_header_collection() - # Parse the header collection + # Parse the headers with pygccxml and CastXML self.parse_header_collection(header_collection_path) - # Update the Class and Free Function Info from the parsed code + # Update the Class Info from the parsed code self.update_class_info() + + # Update the Free Function Info from the parsed code self.update_free_function_info() - # Write all the wrappers required for each module - for module_info in self.package_info.module_info_collection: - module_writer = CppModuleWrapperWriter( - self.source_ns, - module_info, - wrapper_templates.template_collection, - self.wrapper_root, - ) - module_writer.write() + # Write all the wrappers required + self.write_wrappers() diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index 7973eec..c16a2fa 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -30,7 +30,7 @@ def __init__(self, module_info: ModuleInfo): for class_info in module_info.class_info_collection } - def expand_templates(self, feature_info: BaseInfo) -> None: + def extract_templates_from_source(self, feature_info: BaseInfo) -> None: """ Extract template arguments for a feature from the associated source file. diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index 88aaa8d..46c380a 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -15,6 +15,7 @@ from cppwg.input.package_info import PackageInfo from cppwg.utils import utils +from cppwg.utils.constants import CPPWG_SOURCEROOT class PackageInfoParser: @@ -71,7 +72,7 @@ def check_for_custom_generators(self, info: BaseInfo) -> None: # 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( - utils.SOURCE_ROOT_STRING, self.source_root + CPPWG_SOURCEROOT, self.source_root ) filepath = os.path.realpath(filepath) diff --git a/cppwg/utils/constants.py b/cppwg/utils/constants.py new file mode 100644 index 0000000..7dc82bb --- /dev/null +++ b/cppwg/utils/constants.py @@ -0,0 +1,12 @@ +""" +Constants for the cppwg package +""" + +CPPWG_SOURCEROOT = "CPPWG_SOURCEROOT" +CPPWG_ALL_STRING = "CPPWG_ALL" + +CPPWG_EXT = "cppwg" +CPPWG_HEADER_COLLECTION_FILE = "wrapper_header_collection.hpp" + +CPPWG_TRUE_STRINGS = ["ON", "YES", "Y", "TRUE", "T"] +CPPWG_FALSE_STRINGS = ["OFF", "NO", "N", "FALSE", "F"] diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py index 95f18c1..b799a9c 100644 --- a/cppwg/utils/utils.py +++ b/cppwg/utils/utils.py @@ -1,17 +1,14 @@ """ -Utility functions and constants for the cppwg package +Utility functions for the cppwg package """ from typing import Any -SOURCE_ROOT_STRING = "CPPWG_SOURCEROOT" -ALL_STRING = "CPPWG_ALL" +from cppwg.utils.constants import CPPWG_ALL_STRING +from cppwg.utils.constants import CPPWG_TRUE_STRINGS, CPPWG_FALSE_STRINGS -TRUE_STRINGS = ["ON", "YES", "Y", "TRUE", "T"] -FALSE_STRINGS = ["OFF", "NO", "N", "FALSE", "F"] - -def is_option_ALL(input_obj: Any, option_ALL_string: str = ALL_STRING) -> bool: +def is_option_ALL(input_obj: Any, option_ALL_string: str = CPPWG_ALL_STRING) -> bool: """ Check if the input is a string that matches the "ALL" indicator e.g. "CPPWG_ALL" @@ -48,8 +45,8 @@ def substitute_bool_for_string(input_dict: dict[Any, Any], key: Any) -> None: caps_string = input_dict[key].strip().upper() - if caps_string in TRUE_STRINGS: + if caps_string in CPPWG_TRUE_STRINGS: input_dict[key] = True - elif caps_string in FALSE_STRINGS: + elif caps_string in CPPWG_FALSE_STRINGS: input_dict[key] = False From c196845baea3ba452aeafc31bfe5383054fb5bc7 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 8 Mar 2024 23:30:49 +0000 Subject: [PATCH 28/53] #11 update info helper --- cppwg/input/info_helper.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index c16a2fa..961f913 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -48,16 +48,6 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: logger.error(f"Unsupported feature type: {type(feature_info)}") raise TypeError() - # Get list of template substitutions from this feature and its parents - # e.g. {"signature":"","replacement":[[2,2], [3,3]]} - template_substitutions: list[dict[str, str]] = ( - feature_info.hierarchy_attribute_gather("template_substitutions") - ) - - # Skip if there are no template substitutions - if len(template_substitutions) == 0: - return - # Skip if there are pre-defined template args if feature_info.template_arg_lists: return @@ -71,6 +61,21 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: logger.error(f"Could not find source file: {source_path}") raise FileNotFoundError() + # Get list of template substitutions from this feature and its parents + # e.g. {"signature":"","replacement":[[2,2], [3,3]]} + template_substitutions: list[dict[str, str]] = ( + feature_info.hierarchy_attribute_gather("template_substitutions") + ) + + # Skip if there are no template substitutions + if len(template_substitutions) == 0: + return + + # Remove spaces from template substitution signatures + # e.g. -> + for tpl_sub in template_substitutions: + tpl_sub["signature"] = tpl_sub["signature"].replace(" ", "") + # Remove whitespace and blank lines from the source file whitespace_regex = re.compile(r"\s+") with open(source_path, "r") as in_file: @@ -84,7 +89,7 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: for template_substitution in template_substitutions: # e.g. - signature = template_substitution["signature"].replace(" ", "") + signature = template_substitution["signature"] # e.g. [[2,2], [3,3]] replacement = template_substitution["replacement"] From 9a6ce01869fd08a22a87896fe0a53eb9ed7724c8 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 8 Mar 2024 23:40:57 +0000 Subject: [PATCH 29/53] #11 extra info helper updates --- cppwg/input/info_helper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index 961f913..f3b4923 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -2,6 +2,8 @@ import re import logging +from typing import Any + from cppwg.input.base_info import BaseInfo from cppwg.input.class_info import CppClassInfo from cppwg.input.module_info import ModuleInfo @@ -63,7 +65,7 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: # Get list of template substitutions from this feature and its parents # e.g. {"signature":"","replacement":[[2,2], [3,3]]} - template_substitutions: list[dict[str, str]] = ( + template_substitutions: list[dict[str, Any]] = ( feature_info.hierarchy_attribute_gather("template_substitutions") ) @@ -89,10 +91,10 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: for template_substitution in template_substitutions: # e.g. - signature = template_substitution["signature"] + signature: str = template_substitution["signature"] # e.g. [[2,2], [3,3]] - replacement = template_substitution["replacement"] + replacement: list[list[Any]] = template_substitution["replacement"] if signature in curr_line: feature_string = feature_type + feature_info.name # e.g. "classFoo" From 5c56287b76076fc6ba713386d862d20fa988554a Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 9 Mar 2024 12:25:59 +0000 Subject: [PATCH 30/53] #11 update parsers --- cppwg/generators.py | 20 ++++++-------- cppwg/input/info_helper.py | 50 ++++++++++++++++++++++------------ cppwg/input/module_info.py | 1 + cppwg/parsers/package_info.py | 6 ++-- cppwg/parsers/source_parser.py | 10 ++++--- cppwg/utils/constants.py | 2 +- 6 files changed, 52 insertions(+), 37 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 5bf7a00..2b3ed33 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -69,7 +69,7 @@ def __init__( logger.setLevel(logging.INFO) # Sanitize source_root - self.source_root: str = os.path.realpath(source_root) + self.source_root: str = os.path.abspath(source_root) if not os.path.isdir(self.source_root): logger.error(f"Could not find source root directory: {source_root}") raise FileNotFoundError() @@ -78,7 +78,7 @@ def __init__( self.wrapper_root: str # type hinting if wrapper_root: # Create the specified wrapper root directory if it doesn't exist - self.wrapper_root = os.path.realpath(wrapper_root) + self.wrapper_root = os.path.abspath(wrapper_root) if not os.path.isdir(self.wrapper_root): logger.info( @@ -95,7 +95,7 @@ def __init__( self.source_includes: list[str] # type hinting if source_includes: self.source_includes = [ - os.path.realpath(include_path) for include_path in source_includes + os.path.abspath(include_path) for include_path in source_includes ] for include_path in self.source_includes: @@ -117,7 +117,7 @@ def __init__( raise FileNotFoundError() else: # If no package info config file has been supplied, check the default - default_package_info_file = os.path.realpath("./package_info.yaml") + default_package_info_file = os.path.abspath("./package_info.yaml") if os.path.isfile(default_package_info_file): self.package_info_file = default_package_info_file logger.info( @@ -158,7 +158,7 @@ def collect_source_hpp_files(self) -> None: for root, _, filenames in os.walk(self.source_root, followlinks=True): for pattern in self.package_info.source_hpp_patterns: for filename in fnmatch.filter(filenames, pattern): - filepath = os.path.realpath(os.path.join(root, filename)) + filepath = os.path.abspath(os.path.join(root, filename)) # Skip files in wrapper root dir if Path(self.wrapper_root) in Path(filepath).parents: @@ -289,11 +289,7 @@ def write_header_collection(self) -> str: self.wrapper_root, CPPWG_HEADER_COLLECTION_FILE, ) - header_collection_writer.write() - - header_collection_path = os.path.join( - self.wrapper_root, CPPWG_HEADER_COLLECTION_FILE - ) + header_collection_path = header_collection_writer.write() return header_collection_path @@ -315,7 +311,7 @@ def generate_wrapper(self) -> None: Main method for generating all the wrappers """ - # Parse the package info file + # Parse the input yaml for package, module, and class information self.parse_package_info() # Search for header files in the source root @@ -324,7 +320,7 @@ def generate_wrapper(self) -> None: # Map each class to a header file self.map_classes_to_hpp_files() - # Attempt to extract template args for each class from the source file + # Attempt to extract template args for each class from the source files self.extract_templates_from_source() # Write the header collection to file diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index f3b4923..5b6ce1e 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -78,11 +78,11 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: for tpl_sub in template_substitutions: tpl_sub["signature"] = tpl_sub["signature"].replace(" ", "") - # Remove whitespace and blank lines from the source file + # Remove whitespaces, blank lines, and directives from the source file whitespace_regex = re.compile(r"\s+") with open(source_path, "r") as in_file: lines = [re.sub(whitespace_regex, "", line) for line in in_file] - lines = [line for line in lines if line] + lines = [line for line in lines if line and not line.startswith("#")] # Search for template signatures in the source file lines for idx in range(len(lines) - 1): @@ -90,8 +90,8 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: next_line = lines[idx + 1] for template_substitution in template_substitutions: - # e.g. - signature: str = template_substitution["signature"] + # e.g. template + signature: str = "template" + template_substitution["signature"] # e.g. [[2,2], [3,3]] replacement: list[list[Any]] = template_substitution["replacement"] @@ -99,21 +99,37 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: if signature in curr_line: feature_string = feature_type + feature_info.name # e.g. "classFoo" - # Simple declaration example: - # template - # classFoo - simple_declaration = feature_string == next_line + declaration_found = False - # Derived declaration example: - # template - # classFoo:publicBar - derived_declaration = feature_string + ":" in next_line + if feature_string == next_line: + # template + # classFoo + declaration_found = True - # TODO: Add support for more cases - # e.g. - # template classFoo - # template classFoo:publicBar + elif next_line.startswith(feature_string + "{"): + # template + # classFoo{ + declaration_found = True - if simple_declaration or derived_declaration: + elif next_line.startswith(feature_string + ":"): + # template + # classFoo:publicBar + declaration_found = True + + elif curr_line == signature + feature_string: + # templateclassFoo + declaration_found = True + + elif curr_line.startswith(signature + feature_string + "{"): + # templateclassFoo{ + declaration_found = True + + elif curr_line.startswith(signature + feature_string + ":"): + # templateclassFoo:publicBar + declaration_found = True + + # TODO: Add support for more cases, or find a better way e.g. regex or castxml? + + if declaration_found: feature_info.template_arg_lists = replacement break diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 7d53b2a..233eeb5 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -70,6 +70,7 @@ def is_decl_in_source_path(self, decl: declaration_t) -> bool: True if the declaration is associated with a file in the current source path """ + # TODO: Logic for source_locations is not implemented e.g. package info parser does not set it if self.source_locations is None: return True diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index 46c380a..9ca625f 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -15,7 +15,7 @@ from cppwg.input.package_info import PackageInfo from cppwg.utils import utils -from cppwg.utils.constants import CPPWG_SOURCEROOT +from cppwg.utils.constants import CPPWG_SOURCEROOT_STRING class PackageInfoParser: @@ -72,9 +72,9 @@ def check_for_custom_generators(self, info: BaseInfo) -> None: # 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, self.source_root + CPPWG_SOURCEROOT_STRING, self.source_root ) - filepath = os.path.realpath(filepath) + filepath = os.path.abspath(filepath) # Verify that the custom generator file exists if not os.path.isfile(filepath): diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index 5e08e4a..edd73e5 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -1,5 +1,6 @@ import logging +from pathlib import Path from typing import Optional from pygccxml import parser, declarations @@ -81,7 +82,7 @@ def parse(self) -> namespace_t: ) # Parse all the C++ source code to extract declarations - logger.info("Parsing code.") + logger.info("Parsing source code for declarations.") decls: list[declaration_t] = parser.parse( files=[self.wrapper_header_collection], config=xml_generator_config, @@ -96,12 +97,13 @@ def parse(self) -> namespace_t: query = declarations.custom_matcher_t(lambda decl: decl.location is not None) filtered_decls: mdecl_wrapper_t = self.global_ns.decls(function=query) - # Filter declarations in our source tree (+ wrapper_header_collection) + # Filter declarations in our source tree; include declarations from the + # wrapper_header_collection file for explicit instantiations, typedefs etc. source_decls: list[declaration_t] = [ decl for decl in filtered_decls - if self.source_root in decl.location.file_name - or self.wrapper_header_collection in decl.location.file_name + if Path(self.source_root) in Path(decl.location.file_name).parents + or decl.location.file_name == self.wrapper_header_collection ] # Create a source namespace module for the filtered declarations diff --git a/cppwg/utils/constants.py b/cppwg/utils/constants.py index 7dc82bb..f82bdfe 100644 --- a/cppwg/utils/constants.py +++ b/cppwg/utils/constants.py @@ -2,7 +2,7 @@ Constants for the cppwg package """ -CPPWG_SOURCEROOT = "CPPWG_SOURCEROOT" +CPPWG_SOURCEROOT_STRING = "CPPWG_SOURCEROOT" CPPWG_ALL_STRING = "CPPWG_ALL" CPPWG_EXT = "cppwg" From b54d9564fd0c347f228831c4f0650906551238a9 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 11 Mar 2024 17:42:39 +0000 Subject: [PATCH 31/53] #11 update generator --- cppwg/generators.py | 73 ++++++++++++++--------- cppwg/parsers/package_info.py | 9 ++- cppwg/writers/header_collection_writer.py | 44 +++++++------- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 2b3ed33..3512c7d 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -59,7 +59,7 @@ def __init__( wrapper_root: Optional[str] = None, castxml_binary: Optional[str] = "castxml", package_info_file: Optional[str] = None, - castxml_cflags: Optional[str] = "", + castxml_cflags: Optional[str] = "-std=c++17", ): logging.basicConfig( format="%(levelname)s %(message)s", @@ -177,9 +177,9 @@ def extract_templates_from_source(self) -> None: """ for module_info in self.package_info.module_info_collection: - info_genenerator = CppInfoHelper(module_info) + info_helper = CppInfoHelper(module_info) for class_info in module_info.class_info_collection: - info_genenerator.extract_templates_from_source(class_info) + info_helper.extract_templates_from_source(class_info) def map_classes_to_hpp_files(self) -> None: """ @@ -229,13 +229,49 @@ def parse_package_info(self): # If no package info file exists, create a PackageInfo object with default settings self.package_info = PackageInfo("cppwg_package", self.source_root) - def update_free_function_info(self): + def update_class_info(self) -> None: """ - Update the free function info pased on pygccxml output + Update the class info with class declarations parsed by pygccxml from + the C++ source code. + """ + + for module_info in self.package_info.module_info_collection: + if module_info.use_all_classes: + # Create class info objects for all class declarations found + # from parsing the source code with pygccxml. + # Note: as module_info.use_all_classes == True, no class info + # objects were created while parsing the package info yaml file. + class_decls = self.source_ns.classes(allow_empty=True) + for class_decl in class_decls: + if module_info.is_decl_in_source_path(class_decl): + class_info = CppClassInfo(class_decl.name) + class_info.module_info = module_info + class_info.decl = class_decl + module_info.class_info_collection.append(class_info) + + else: + # As module_info.use_all_classes == False, class info objects + # have already been created while parsing the package info file. + # We only need to add the decl from pygccxml's output. + for class_info in module_info.class_info_collection: + class_decls = self.source_ns.classes( + class_info.name, allow_empty=True + ) + if len(class_decls) == 1: + class_info.decl = class_decls[0] + + def update_free_function_info(self) -> None: + """ + Update the free function info with declarations parsed by pygccxml from + the C++ source code. """ for module_info in self.package_info.module_info_collection: if module_info.use_all_free_functions: + # Create free function info objects for all free function + # declarations found from parsing the source code with pygccxml. + # Note: as module_info.use_all_free_functions == True, no class info + # objects were created while parsing the package info yaml file. free_functions = self.source_ns.free_functions(allow_empty=True) for free_function in free_functions: if module_info.is_decl_in_source_path(free_function): @@ -245,6 +281,9 @@ def update_free_function_info(self): module_info.free_function_info.append(function_info) else: + # As module_info.use_all_free_functions == False, free function + # info objects have already been created while parsing the + # package info file. We only need to add the decl from pygccxml's output. for free_function_info in module_info.free_function_info_collection: free_functions = self.source_ns.free_functions( free_function_info.name, allow_empty=True @@ -252,28 +291,6 @@ def update_free_function_info(self): if len(free_functions) == 1: free_function_info.decl = free_functions[0] - def update_class_info(self): - """ - Update the class info pased on pygccxml output - """ - - for module_info in self.package_info.module_info_collection: - if module_info.use_all_classes: - class_decls = self.source_ns.classes(allow_empty=True) - for class_decl in class_decls: - if module_info.is_decl_in_source_path(class_decl): - class_info = CppClassInfo(class_decl.name) - class_info.module_info = module_info - class_info.decl = class_decl - module_info.class_info_collection.append(class_info) - else: - for class_info in module_info.class_info_collection: - class_decls = self.source_ns.classes( - class_info.name, allow_empty=True - ) - if len(class_decls) == 1: - class_info.decl = class_decls[0] - def write_header_collection(self) -> str: """ Write the header collection to file @@ -320,7 +337,7 @@ def generate_wrapper(self) -> None: # Map each class to a header file self.map_classes_to_hpp_files() - # Attempt to extract template args for each class from the source files + # Attempt to extract templates for each class from the source files self.extract_templates_from_source() # Write the header collection to file diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index 9ca625f..0346ec7 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -193,7 +193,9 @@ def parse(self) -> PackageInfo: module_info.package_info = self.package_info self.package_info.module_info_collection.append(module_info) - # Parse the class data + # Parse the class data and create class info objects. + # Note: if module_config["use_all_classes"] == True, class info + # objects will be added later after parsing the C++ source code. if not module_config["use_all_classes"]: if module_config["classes"]: for raw_class_info in module_config["classes"]: @@ -213,7 +215,10 @@ def parse(self) -> PackageInfo: class_info.module_info = module_info module_info.class_info_collection.append(class_info) - # Parse the free function data + + # Parse the free function data and create free function info objects. + # Note: if module_config["use_all_free_functions"] == True, free function + # info objects will be added later after parsing the C++ source code. if not module_config["use_all_free_functions"]: if module_config["free_functions"]: for raw_free_function_info in module_config["free_functions"]: diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index 82dac1a..5cd1c11 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -10,7 +10,7 @@ class CppHeaderCollectionWriter: This class manages the generation of the header collection file, which includes all the headers to be parsed by CastXML. The header collection file also contains explicit template instantiations and their corresponding - typedefs (e.g. typedef AbstractLinearPde2_2 AbstractLinearPde<2,2>) for all + typedefs (e.g. typedef Foo<2,2> Foo2_2) for all classes that are to be automatically wrapped. Attributes @@ -36,9 +36,12 @@ def __init__( hpp_collection_filename: str = "wrapper_header_collection.hpp", ): - # Populate the class and free function dictionaries from the package info self.package_info: PackageInfo = package_info + self.wrapper_root: str = wrapper_root + self.hpp_collection_filename: str = hpp_collection_filename + self.hpp_collection_string: str = "" + # For convenience, collect all class and free function info into dicts keyed by name self.class_dict: dict[str, CppClassInfo] = {} self.free_func_dict: dict[str, CppFreeFunctionInfo] = {} @@ -49,11 +52,6 @@ def __init__( for free_function_info in module_info.free_function_info_collection: self.free_func_dict[free_function_info.name] = free_function_info - # Set the remaining attributes - self.wrapper_root: str = wrapper_root - self.hpp_collection_filename: str = hpp_collection_filename - self.hpp_collection_string: str = "" - def should_include_all(self) -> bool: """ Return whether all source files in the module source locations should be included @@ -69,21 +67,26 @@ def should_include_all(self) -> bool: return True return False - def write(self): + def write(self) -> str: """ Generate the header file output string and write it to file + + Returns + ------- + str + The path to the header collection file """ # Add opening header guard self.hpp_collection_string = f"#ifndef {self.package_info.name}_HEADERS_HPP_\n" self.hpp_collection_string += f"#define {self.package_info.name}_HEADERS_HPP_\n" - # Add the includes self.hpp_collection_string += "\n// Includes\n" included_files = set() # Keep track of included files to avoid duplicates if self.should_include_all(): + # Include all the headers for hpp_filepath in self.package_info.source_hpp_files: hpp_filename = os.path.basename(hpp_filepath) @@ -92,6 +95,7 @@ def write(self): included_files.add(hpp_filename) 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: hpp_filename = None @@ -108,6 +112,7 @@ def write(self): self.hpp_collection_string += f'#include "{hpp_filename}"\n' included_files.add(hpp_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: hpp_filename = os.path.basename( @@ -118,15 +123,16 @@ def write(self): self.hpp_collection_string += f'#include "{hpp_filename}"\n' included_files.add(hpp_filename) - # Add the template instantiations e.g. `template class AbstractLinearPde<2,2>;` + # Add the template instantiations e.g. `template class Foo<2,2>;` self.hpp_collection_string += "\n// Instantiate Template Classes \n" for module_info in self.package_info.module_info_collection: for class_info in module_info.class_info_collection: - # Class full names eg. ["AbstractLinearPde<2,2>", "AbstractLinearPde<3,3>"] + # Class full names eg. ["Foo<2,2>", "Foo<3,3>"] full_names = class_info.get_full_names() # TODO: What if the class is templated but has only one template instantiation? + # See https://github.com/Chaste/cppwg/issues/2 if len(full_names) < 2: continue # Skip if the class is untemplated @@ -136,20 +142,21 @@ def write(self): f"template class {clean_template_name};\n" ) - # Add typdefs for nice naming e.g. `typedef AbstractLinearPde2_2 AbstractLinearPde<2,2>;` + # Add typdefs for nice naming e.g. `typedef Foo<2,2> Foo2_2` self.hpp_collection_string += "\n// Typedef for nicer naming\n" self.hpp_collection_string += "namespace cppwg{ \n" for module_info in self.package_info.module_info_collection: for class_info in module_info.class_info_collection: - # Class full names eg. ["AbstractLinearPde<2,2>", "AbstractLinearPde<3,3>"] + # Class full names eg. ["Foo<2,2>", "Foo<3,3>"] full_names = class_info.get_full_names() # TODO: What if the class is templated but has only one template instantiation? + # See https://github.com/Chaste/cppwg/issues/2 if len(full_names) < 2: continue # Skip if the class is untemplated - # Class short names eg. ["AbstractLinearPde2_2", "AbstractLinearPde3_3"] + # Class short names eg. ["Foo2_2", "Foo3_3"] short_names = class_info.get_short_names() for short_name, template_name in zip(short_names, full_names): @@ -166,14 +173,9 @@ def write(self): ) # Write the header collection string to file - self.write_file() - - def write_file(self) -> None: - """ - Write the header collection string to file - """ - hpp_file_path = os.path.join(self.wrapper_root, self.hpp_collection_filename) with open(hpp_file_path, "w") as hpp_file: hpp_file.write(self.hpp_collection_string) + + return hpp_file_path From 71517ce96948a94f702a219f3c448b9966aab765 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 11 Mar 2024 18:48:33 +0000 Subject: [PATCH 32/53] #11 update class writer --- cppwg/generators.py | 32 +++--- cppwg/input/module_info.py | 3 +- cppwg/utils/constants.py | 2 +- cppwg/writers/module_writer.py | 183 +++++++++++++++++++-------------- 4 files changed, 118 insertions(+), 102 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 3512c7d..5e985b2 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -23,7 +23,7 @@ from cppwg.templates import pybind11_default as wrapper_templates -from cppwg.utils.constants import CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILE +from cppwg.utils.constants import CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILENAME class CppWrapperGenerator: @@ -148,6 +148,10 @@ def __init__( self.package_info: Optional[PackageInfo] = None + self.header_collection_filepath: str = os.path.join( + self.wrapper_root, CPPWG_HEADER_COLLECTION_FILENAME + ) + def collect_source_hpp_files(self) -> None: """ Walk through the source root and add any files matching the provided @@ -195,20 +199,15 @@ def map_classes_to_hpp_files(self) -> None: if class_info.source_file is None: class_info.source_file = hpp_file_name - def parse_header_collection(self, header_collection_path: str) -> None: + def parse_header_collection(self) -> None: """ Parse the headers with pygccxml and CastXML to populate the source namespace with C++ declarations collected from the source tree - - Parameters - ---------- - header_collection_path : str - The path to the header collection file """ source_parser = CppSourceParser( self.source_root, - header_collection_path, + self.header_collection_filepath, self.castxml_binary, self.source_includes, self.castxml_cflags, @@ -291,24 +290,17 @@ def update_free_function_info(self) -> None: if len(free_functions) == 1: free_function_info.decl = free_functions[0] - def write_header_collection(self) -> str: + def write_header_collection(self) -> None: """ Write the header collection to file - - Returns - ------- - str - The path to the header collection file """ header_collection_writer = CppHeaderCollectionWriter( self.package_info, self.wrapper_root, - CPPWG_HEADER_COLLECTION_FILE, + self.header_collection_filepath, ) - header_collection_path = header_collection_writer.write() - - return header_collection_path + header_collection_writer.write() def write_wrappers(self) -> None: """ @@ -341,10 +333,10 @@ def generate_wrapper(self) -> None: self.extract_templates_from_source() # Write the header collection to file - header_collection_path = self.write_header_collection() + self.write_header_collection() # Parse the headers with pygccxml and CastXML - self.parse_header_collection(header_collection_path) + self.parse_header_collection() # Update the Class Info from the parsed code self.update_class_info() diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 233eeb5..90a154e 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -70,7 +70,8 @@ def is_decl_in_source_path(self, decl: declaration_t) -> bool: True if the declaration is associated with a file in the current source path """ - # TODO: Logic for source_locations is not implemented e.g. package info parser does not set it + # TODO: Logic for source_locations is not implemented e.g. package info + # parser does not set it so this will always return True if self.source_locations is None: return True diff --git a/cppwg/utils/constants.py b/cppwg/utils/constants.py index f82bdfe..a81f002 100644 --- a/cppwg/utils/constants.py +++ b/cppwg/utils/constants.py @@ -6,7 +6,7 @@ CPPWG_ALL_STRING = "CPPWG_ALL" CPPWG_EXT = "cppwg" -CPPWG_HEADER_COLLECTION_FILE = "wrapper_header_collection.hpp" +CPPWG_HEADER_COLLECTION_FILENAME = "wrapper_header_collection.hpp" CPPWG_TRUE_STRINGS = ["ON", "YES", "Y", "TRUE", "T"] CPPWG_FALSE_STRINGS = ["OFF", "NO", "N", "FALSE", "F"] diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index d7cdd2c..851ff0e 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -1,73 +1,99 @@ -#!/usr/bin/env python - -""" -This scipt automatically generates Python bindings using a rule based approach -""" - import os - -from cppwg.writers import free_function_writer -from cppwg.writers import class_writer - - -class CppModuleWrapperWriter(object): - - def __init__(self, global_ns, - source_ns, - module_info, - wrapper_templates, - wrapper_root, - package_license=None): - - self.global_ns = global_ns - self.source_ns = source_ns - self.module_info = module_info - self.wrapper_root = wrapper_root - self.exposed_class_full_names = [] - self.wrapper_templates = wrapper_templates - self.license = package_license - - self.exposed_class_full_names = [] - - def generate_main_cpp(self): - +import logging + +from pygccxml.declarations.class_declaration import class_t +from pygccxml.declarations.namespace import namespace_t + +from cppwg.input.module_info import ModuleInfo + +from cppwg.writers.free_function_writer import CppFreeFunctionWrapperWriter +from cppwg.writers.class_writer import CppClassWrapperWriter + +from cppwg.utils.constants import CPPWG_HEADER_COLLECTION_FILENAME + + +class CppModuleWrapperWriter: + """ + This class automatically generates Python bindings using a rule based approach + + Attributes + ---------- + source_ns : namespace_t + The pygccxml namespace containing declarations from the source code + module_info : ModuleInfo + The module information to generate Python bindings for + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + wrapper_root : str + The output directory for the generated wrapper code + package_license : str + The license to include in the generated wrapper code + """ + + def __init__( + self, + source_ns: namespace_t, + module_info: ModuleInfo, + wrapper_templates: dict[str, str], + wrapper_root: str, + package_license: str = "", + ): + self.source_ns: namespace_t = source_ns + self.module_info: ModuleInfo = module_info + self.wrapper_templates: dict[str, str] = wrapper_templates + self.wrapper_root: str = wrapper_root + self.package_license: str = ( + package_license # TODO: use this in the generated wrappers + ) + + # For convenience, create a list of all full class names in the module + # e.g. ['Foo', 'Bar<2>', 'Bar<3>'] + self.exposed_class_full_names: list[str] = [] + + for class_info in self.module_info.class_info_collection: + self.exposed_class_full_names.extend(class_info.get_full_names()) + + def write_module_wrapper(self) -> None: """ - Generate the main cpp for the module + Write the main cpp file for the module """ - # Generate the main cpp file + # Format module name as _packagename_modulename module_name = self.module_info.name full_module_name = "_" + self.module_info.package_info.name + "_" + module_name - cpp_string = "" - cpp_string += '#include \n' + # Add top level includes + cpp_string = "#include \n" - if self.module_info.package_info.common_include_file: - cpp_string += '#include "wrapper_header_collection.hpp"\n' - - # Custom code - if self.module_info.custom_generator is not 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() - # Add includes - for eachClass in self.module_info.class_info: - for short_name in eachClass.get_short_names(): - cpp_string += '#include "' + short_name + '.cppwg.hpp"\n' - cpp_string += '\nnamespace py = pybind11;\n\n' - cpp_string += 'PYBIND11_MODULE(' + full_module_name + ', m)\n{\n' + # Add includes for class wrappers in the module + for class_info in self.module_info.class_info_collection: + for short_name in class_info.get_short_names(): + # Example: #include "Foo2_2.cppwg.hpp" + cpp_string += f'#include "{short_name}.cppwg.hpp"\n' + + # Create the module + cpp_string += "\nnamespace py = pybind11;\n\n" + cpp_string += "PYBIND11_MODULE({full_module_name}, m)\n{\n" # Add free functions - for eachFunction in self.module_info.free_function_info: - writer = free_function_writer.CppFreeFunctionWrapperWriter(eachFunction, - self.wrapper_templates) - cpp_string = writer.add_self(cpp_string) + for free_function_info in self.module_info.free_function_info_collection: + function_writer = CppFreeFunctionWrapperWriter( + free_function_info, self.wrapper_templates + ) + cpp_string = function_writer.add_self(cpp_string) # Add viable classes - for eachClass in self.module_info.class_info: - for short_name in eachClass.get_short_names(): - cpp_string += ' register_' + short_name + '_class(m);\n' - + for class_info in self.module_info.class_info_collection: + for short_name in class_info.get_short_names(): + cpp_string += " register_" + short_name + "_class(m);\n" + # Add any custom code if self.module_info.custom_generator is not None: cpp_string += self.module_info.custom_generator.get_module_code() @@ -76,39 +102,36 @@ def generate_main_cpp(self): if not os.path.exists(output_dir): os.makedirs(output_dir) main_cpp_file = open(output_dir + self.module_info.name + ".main.cpp", "w") - main_cpp_file.write(cpp_string + '}\n') + main_cpp_file.write(cpp_string + "}\n") main_cpp_file.close() - def get_class_writer(self, class_info): - + def write_class_wrappers(self) -> None: """ - Return the class writer, override for custom writers + Write wrappers for classes in the module """ + logger = logging.getLogger() - this_class_writer = class_writer.CppClassWrapperWriter(class_info, self.wrapper_templates) - return this_class_writer - - def write(self): - - """ - Main method for writing the module - """ + for class_info in self.module_info.class_info_collection: + logger.info(f"Generating wrapper for class {class_info.name}") - print ('Generating Wrapper Code for: ' + self.module_info.name + ' Module.') + class_writer = CppClassWrapperWriter( + class_info, self.wrapper_templates, self.exposed_class_full_names + ) - self.generate_main_cpp() + for full_name in class_info.get_full_names(): + name: str = full_name.replace(" ", "") - # Generate class files - for eachClassInfo in self.module_info.class_info: - self.exposed_class_full_names.extend(eachClassInfo.get_full_names()) + class_decl: class_t = self.source_ns.class_(name) + class_writer.class_decls.append(class_decl) + class_writer.write(os.path.join(self.wrapper_root, self.module_info.name)) - for eachClassInfo in self.module_info.class_info: + def write(self) -> None: + """ + Main method for writing the module + """ + logger = logging.getLogger() - print ('Generating Wrapper Code for: ' + eachClassInfo.name + ' Class.') + logger.info(f"Generating wrappers for module {self.module_info.name}") - class_writer = self.get_class_writer(eachClassInfo) - class_writer.exposed_class_full_names = self.exposed_class_full_names - for fullName in eachClassInfo.get_full_names(): - class_decl = self.source_ns.class_(fullName.replace(" ","")) - class_writer.class_decls.append(class_decl) - class_writer.write(self.wrapper_root + "/" + self.module_info.name + "/") + self.write_module_wrapper() + self.write_class_wrappers() From 07facf0306352a5cb3e8518e32f86a399d2a8d6d Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 12 Mar 2024 09:54:15 +0000 Subject: [PATCH 33/53] #11 update writers --- cppwg/writers/header_collection_writer.py | 22 ++++++---------------- cppwg/writers/module_writer.py | 14 ++++++++------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index 5cd1c11..ab36338 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -4,7 +4,6 @@ from cppwg.input.free_function_info import CppFreeFunctionInfo from cppwg.input.package_info import PackageInfo - class CppHeaderCollectionWriter: """ This class manages the generation of the header collection file, which @@ -19,8 +18,8 @@ class CppHeaderCollectionWriter: The package information wrapper_root : str The output directory for the generated wrapper code - hpp_collection_filename : str - The name of the header collection file + hpp_collection_filepath : str + The path to save the header collection file to hpp_collection_string : str The output string that gets written to the header collection file class_dict : dict[str, CppClassInfo] @@ -33,12 +32,12 @@ def __init__( self, package_info: PackageInfo, wrapper_root: str, - hpp_collection_filename: str = "wrapper_header_collection.hpp", + hpp_collection_filepath: str, ): self.package_info: PackageInfo = package_info self.wrapper_root: str = wrapper_root - self.hpp_collection_filename: str = hpp_collection_filename + self.hpp_collection_filepath: str = hpp_collection_filepath self.hpp_collection_string: str = "" # For convenience, collect all class and free function info into dicts keyed by name @@ -67,14 +66,9 @@ def should_include_all(self) -> bool: return True return False - def write(self) -> str: + def write(self) -> None: """ Generate the header file output string and write it to file - - Returns - ------- - str - The path to the header collection file """ # Add opening header guard @@ -173,9 +167,5 @@ def write(self) -> str: ) # Write the header collection string to file - hpp_file_path = os.path.join(self.wrapper_root, self.hpp_collection_filename) - - with open(hpp_file_path, "w") as hpp_file: + with open(self.hpp_collection_filepath, "w") as hpp_file: hpp_file.write(self.hpp_collection_string) - - return hpp_file_path diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index 851ff0e..23abedb 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -58,10 +58,6 @@ def write_module_wrapper(self) -> None: Write the main cpp file for the module """ - # Format module name as _packagename_modulename - module_name = self.module_info.name - full_module_name = "_" + self.module_info.package_info.name + "_" + module_name - # Add top level includes cpp_string = "#include \n" @@ -78,9 +74,15 @@ def write_module_wrapper(self) -> None: # Example: #include "Foo2_2.cppwg.hpp" cpp_string += f'#include "{short_name}.cppwg.hpp"\n' + # Format module name as _packagename_modulename + full_module_name = ( + "_" + self.module_info.package_info.name + "_" + self.module_info.name + ) + # Create the module - cpp_string += "\nnamespace py = pybind11;\n\n" - cpp_string += "PYBIND11_MODULE({full_module_name}, m)\n{\n" + cpp_string += "\nnamespace py = pybind11;\n" + cpp_string += f"\nPYBIND11_MODULE({full_module_name}, m)\n" + cpp_string += "{\n" # Add free functions for free_function_info in self.module_info.free_function_info_collection: From 7e1448685a7e62e7061c4195a2d74b719ec6ee56 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Wed, 13 Mar 2024 10:17:56 +0000 Subject: [PATCH 34/53] #11 update module writer --- cppwg/input/cpp_type_info.py | 2 +- cppwg/writers/module_writer.py | 46 +++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index d67dd7b..4fd97a0 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -18,7 +18,7 @@ class CppTypeInfo(BaseInfo): 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. "SharedPottsMeshGenerator" -> "PottsMeshGenerator" + The name override specified in config e.g. "CustomFoo" -> "Foo" template_arg_lists : list[list[Any]] List of template replacement arguments for the type e.g. [[2, 2], [3, 3]] decl : declaration_t diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index 23abedb..1f6b54f 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -55,7 +55,22 @@ def __init__( def write_module_wrapper(self) -> None: """ - Write the main cpp file for the module + Generate the contents of the main cpp file for the module and write it + to modulename.main.cpp. This file contains the pybind11 module + definition. Within the module definition, the module's free functions + and classes are registered. + + For example, the generated file might look like this: + + ``` + #include + #include "Foo.cppwg.hpp" + + PYBIND11_MODULE(_packagename_modulename, m) + { + register_Foo_class(m); + } + ``` """ # Add top level includes @@ -79,7 +94,7 @@ def write_module_wrapper(self) -> None: "_" + self.module_info.package_info.name + "_" + self.module_info.name ) - # Create the module + # Create the pybind11 module cpp_string += "\nnamespace py = pybind11;\n" cpp_string += f"\nPYBIND11_MODULE({full_module_name}, m)\n" cpp_string += "{\n" @@ -91,21 +106,27 @@ def write_module_wrapper(self) -> None: ) cpp_string = function_writer.add_self(cpp_string) - # Add viable classes + # Add classes for class_info in self.module_info.class_info_collection: for short_name in class_info.get_short_names(): - cpp_string += " register_" + short_name + "_class(m);\n" + # Example: register_Foo2_2_class(m);" + cpp_string += f" register_{short_name}_class(m);\n" - # Add any custom code - if self.module_info.custom_generator is not None: + # Add code from the module's custom generator + if self.module_info.custom_generator: cpp_string += self.module_info.custom_generator.get_module_code() - output_dir = self.wrapper_root + "/" + self.module_info.name + "/" - if not os.path.exists(output_dir): + cpp_string += "}\n" # End of the pybind11 module + + # Write to /path/to/wrapper_root/modulename/modulename.main.cpp + output_dir = os.path.join(self.wrapper_root, self.module_info.name) + if not os.path.isdir(output_dir): os.makedirs(output_dir) - main_cpp_file = open(output_dir + self.module_info.name + ".main.cpp", "w") - main_cpp_file.write(cpp_string + "}\n") - main_cpp_file.close() + + output_file = os.path.join(output_dir, self.module_info.name + ".main.cpp") + + with open(output_file, "w") as main_cpp_file: + main_cpp_file.write(cpp_string) def write_class_wrappers(self) -> None: """ @@ -121,10 +142,11 @@ def write_class_wrappers(self) -> None: ) for full_name in class_info.get_full_names(): - name: str = full_name.replace(" ", "") + name = full_name.replace(" ", "") # e.g. Foo<2,2> class_decl: class_t = self.source_ns.class_(name) class_writer.class_decls.append(class_decl) + class_writer.write(os.path.join(self.wrapper_root, self.module_info.name)) def write(self) -> None: From 7ec940958830dabf26f43851e8c4b1091191bfe9 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Wed, 13 Mar 2024 17:40:55 +0000 Subject: [PATCH 35/53] #11 update module writer --- cppwg/writers/module_writer.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index 1f6b54f..4745c1d 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -28,6 +28,8 @@ class CppModuleWrapperWriter: The output directory for the generated wrapper code package_license : str The license to include in the generated wrapper code + exposed_class_full_names : list[str] + A list of all class full names in the module """ def __init__( @@ -46,7 +48,7 @@ def __init__( package_license # TODO: use this in the generated wrappers ) - # For convenience, create a list of all full class names in the module + # For convenience, create a list of all class full names in the module # e.g. ['Foo', 'Bar<2>', 'Bar<3>'] self.exposed_class_full_names: list[str] = [] @@ -119,14 +121,14 @@ def write_module_wrapper(self) -> None: cpp_string += "}\n" # End of the pybind11 module # Write to /path/to/wrapper_root/modulename/modulename.main.cpp - output_dir = os.path.join(self.wrapper_root, self.module_info.name) - if not os.path.isdir(output_dir): - os.makedirs(output_dir) + module_dir = os.path.join(self.wrapper_root, self.module_info.name) + if not os.path.isdir(module_dir): + os.makedirs(module_dir) - output_file = os.path.join(output_dir, self.module_info.name + ".main.cpp") + module_cpp_file = os.path.join(module_dir, self.module_info.name + ".main.cpp") - with open(output_file, "w") as main_cpp_file: - main_cpp_file.write(cpp_string) + with open(module_cpp_file, "w") as out_file: + out_file.write(cpp_string) def write_class_wrappers(self) -> None: """ @@ -141,13 +143,16 @@ def write_class_wrappers(self) -> None: class_info, self.wrapper_templates, self.exposed_class_full_names ) + # Get the declaration for each class and add it to the class writer for full_name in class_info.get_full_names(): name = full_name.replace(" ", "") # e.g. Foo<2,2> class_decl: class_t = self.source_ns.class_(name) class_writer.class_decls.append(class_decl) - class_writer.write(os.path.join(self.wrapper_root, self.module_info.name)) + # Write the class wrappers into /path/to/wrapper_root/modulename/ + module_dir = os.path.join(self.wrapper_root, self.module_info.name) + class_writer.write(module_dir) def write(self) -> None: """ From 7346b00ab56308b10a2becfd048d28d6e20f508e Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Wed, 13 Mar 2024 20:09:10 +0000 Subject: [PATCH 36/53] #11 update base writer --- cppwg/writers/base_writer.py | 84 ++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/cppwg/writers/base_writer.py b/cppwg/writers/base_writer.py index da8bb11..25e838a 100644 --- a/cppwg/writers/base_writer.py +++ b/cppwg/writers/base_writer.py @@ -1,36 +1,82 @@ -import collections +from collections import OrderedDict +from pygccxml.declarations import free_function_t -class CppBaseWrapperWriter(object): +class CppBaseWrapperWriter: """ Base class for wrapper writers + + Attributes + ---------- + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + tidy_replacements : OrderedDict[str, str] + A dictionary of replacements to use when tidying up C++ declarations """ - def __init__(self, wrapper_templates): + def __init__(self, wrapper_templates: dict[str, str]): self.wrapper_templates = wrapper_templates - self.tidy_replacements = collections.OrderedDict([(", ", "_"), ("<", "_lt_"), - (">", "_gt_"), ("::", "_"), - ("*", "Ptr"), ("&", "Ref"), - ("-", "neg")]) + self.tidy_replacements = OrderedDict( + [ + (" ", ""), + (",", "_"), + ("<", "_lt_"), + (">", "_gt_"), + ("::", "_"), + ("*", "Ptr"), + ("&", "Ref"), + ("-", "neg"), + ] + ) - def tidy_name(self, name): - + def tidy_name(self, name) -> str: """ - This method replaces full c++ declarations with a simple version for use + This method replaces full C++ declarations with a simple version for use in typedefs + + Example: + "::foo::bar" -> "_foo_bar_lt_double_2_gt_" + + Parameters + ---------- + name : str + The C++ declaration to tidy up + + Returns + ------- + str + The tidied up C++ declaration """ for key, value in self.tidy_replacements.items(): name = name.replace(key, value) - return name.replace(" ", "") - def exclusion_critera(self, decl, exclusion_args): - + return name + + # TODO: Consider moving this implementation of exclusion_criteria to the + # free function writer it is only used there. exclusion_criteria is + # currently overriden in method writer and constructor writer. + def exclusion_critera( + self, decl: free_function_t, exclusion_args: list[str] + ) -> bool: + """ + Checks if any of the types in the function declaration appear in the + exclusion args. + + Parameters + ---------- + decl : free_function_t + The declaration of the function or class + exclusion_args : list[str] + A list of arguments to exclude from the wrapper code + + Returns + ------- + bool + True if the function should be excluded from the wrapper code """ - Fails if any of the types in the declaration appear in the exclusion args - """ # Are any return types not wrappable return_type = decl.return_type.decl_string.replace(" ", "") @@ -38,12 +84,14 @@ def exclusion_critera(self, decl, exclusion_args): return True # Are any arguments not wrappable - for eachArg in decl.argument_types: - arg_type = eachArg.decl_string.split()[0].replace(" ", "") + for decl_arg_type in decl.argument_types: + arg_type = decl_arg_type.decl_string.split()[0].replace(" ", "") if arg_type in exclusion_args: return True + return False - def default_arg_exclusion_criteria(self): + # TODO: This method is currently a placeholder. Consider implementing or removing. + def default_arg_exclusion_criteria(self) -> bool: return False From aa1d3474cf1cc49eb28590f944f0b1422edebbce Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Wed, 13 Mar 2024 20:28:21 +0000 Subject: [PATCH 37/53] #11 update PYBIND11_OVERRIDE --- cppwg/templates/pybind11_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cppwg/templates/pybind11_default.py b/cppwg/templates/pybind11_default.py index d59ba1d..c1a0790 100644 --- a/cppwg/templates/pybind11_default.py +++ b/cppwg/templates/pybind11_default.py @@ -46,7 +46,7 @@ class {class_short_name}_Overloads : public {class_short_name}{{ method_virtual_override = """\ {return_type} {method_name}({arg_string}){const_adorn} override {{ - PYBIND11_OVERLOAD{overload_adorn}( + PYBIND11_OVERRIDE{overload_adorn}( {tidy_method_name}, {short_class_name}, {method_name}, From 566c53026935680615ff183cc4d44d9116433ef5 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 11:43:32 +0000 Subject: [PATCH 38/53] #11 update free function writer --- cppwg/writers/base_writer.py | 2 +- cppwg/writers/free_function_writer.py | 76 ++++++++++++++++------- cppwg/writers/header_collection_writer.py | 2 +- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/cppwg/writers/base_writer.py b/cppwg/writers/base_writer.py index 25e838a..23e1fb2 100644 --- a/cppwg/writers/base_writer.py +++ b/cppwg/writers/base_writer.py @@ -31,7 +31,7 @@ def __init__(self, wrapper_templates: dict[str, str]): ] ) - def tidy_name(self, name) -> str: + def tidy_name(self, name: str) -> str: """ This method replaces full C++ declarations with a simple version for use in typedefs diff --git a/cppwg/writers/free_function_writer.py b/cppwg/writers/free_function_writer.py index fe37447..e8d231c 100644 --- a/cppwg/writers/free_function_writer.py +++ b/cppwg/writers/free_function_writer.py @@ -1,49 +1,79 @@ -from cppwg.writers import base_writer +from cppwg.input.free_function_info import CppFreeFunctionInfo +from cppwg.writers.base_writer import CppBaseWrapperWriter -class CppFreeFunctionWrapperWriter(base_writer.CppBaseWrapperWriter): +class CppFreeFunctionWrapperWriter(CppBaseWrapperWriter): """ Manage addition of free function wrapper code + + Attributes + ---------- + free_function_info : CppFreeFunctionInfo + The free function information to generate Python bindings for + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + exclusion_args : list[str] + A list of argument types to exclude from the wrapper code """ def __init__(self, free_function_info, wrapper_templates): - + super(CppFreeFunctionWrapperWriter, self).__init__(wrapper_templates) - self.free_function_info = free_function_info - self.wrapper_templates = wrapper_templates - self.exclusion_args = [] + self.free_function_info: CppFreeFunctionInfo = free_function_info + self.wrapper_templates: dict[str, str] = wrapper_templates + self.exclusion_args: list[str] = [] + + def add_self(self, wrapper_string) -> str: + """ + Add the free function wrapper code to the wrapper code string - def add_self(self, output_string): + Parameters + ---------- + wrapper_string : str + String containing the current C++ wrapper code - # Check for exclusions + Returns + ------- + str + The updated C++ wrapper code string + """ + + # Skip this free function if it uses any excluded arg types or return types if self.exclusion_critera(self.free_function_info.decl, self.exclusion_args): - return output_string + return wrapper_string - # Which definition type + # Pybind11 def type e.g. "_static" for def_static() def_adorn = "" + # TODO: arg_signature isn't used. Remove? # Get the arg signature arg_signature = "" arg_types = self.free_function_info.decl.argument_types num_arg_types = len(arg_types) for idx, eachArg in enumerate(arg_types): arg_signature += eachArg.decl_string - if idx < num_arg_types-1: + if idx < num_arg_types - 1: arg_signature += ", " - # Default args + # Pybind11 arg string with or without default values. + # e.g. without default values: ', py::arg("foo"), py::arg("bar")' + # e.g. with default values: ', py::arg("foo") = 1, py::arg("bar") = 2' default_args = "" if not self.default_arg_exclusion_criteria(): - for eachArg in self.free_function_info.decl.arguments: - default_args += ', py::arg("{}")'.format(eachArg.name) - if eachArg.default_value is not None: - default_args += ' = ' + eachArg.default_value - - method_dict = {'def_adorn': def_adorn, - 'function_name': self.free_function_info.decl.name, - 'function_docs': '" "', - 'default_args': default_args} - output_string += self.wrapper_templates["free_function"].format(**method_dict) - return output_string + for argument in self.free_function_info.decl.arguments: + default_args += f', py::arg("{argument.name}")' + if argument.default_value is not None: + default_args += f" = {argument.default_value}" + + # Add the free function wrapper code to the wrapper string + func_dict = { + "def_adorn": def_adorn, + "function_name": self.free_function_info.decl.name, + "function_docs": '" "', + "default_args": default_args, + } + wrapper_string += self.wrapper_templates["free_function"].format(**func_dict) + + return wrapper_string diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index ab36338..acfdf36 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -137,7 +137,7 @@ def write(self) -> None: ) # Add typdefs for nice naming e.g. `typedef Foo<2,2> Foo2_2` - self.hpp_collection_string += "\n// Typedef for nicer naming\n" + self.hpp_collection_string += "\n// Typedefs for nicer naming\n" self.hpp_collection_string += "namespace cppwg{ \n" for module_info in self.package_info.module_info_collection: From 560295bb91b9e8d176e56fb4247fcfaf26e7ebbd Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 13:14:11 +0000 Subject: [PATCH 39/53] #11 update class writer --- cppwg/input/cpp_type_info.py | 1 + cppwg/writers/class_writer.py | 409 ++++++++++++++++++++++----------- cppwg/writers/module_writer.py | 5 +- 3 files changed, 278 insertions(+), 137 deletions(-) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index 4fd97a0..fc98749 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -40,6 +40,7 @@ def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): for key, value in type_config.items(): setattr(self, key, value) + # TODO: Consider setting short and full names on init as read-only properties def get_short_names(self) -> list[str]: """ Return the name of the class as it will appear on the Python side. This diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 1077171..a99c7f9 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -1,157 +1,233 @@ -import ntpath +import os +import logging from pygccxml import declarations +from pygccxml.declarations.calldef_members import member_function_t +from pygccxml.declarations.class_declaration import class_t -from cppwg.writers import base_writer -from cppwg.writers import method_writer -from cppwg.writers import constructor_writer +from cppwg.input.class_info import CppClassInfo +from cppwg.writers.base_writer import CppBaseWrapperWriter +from cppwg.writers.method_writer import CppMethodWrapperWriter +from cppwg.writers.constructor_writer import CppConstructorWrapperWriter -class CppClassWrapperWriter(base_writer.CppBaseWrapperWriter): +from cppwg.utils.constants import CPPWG_EXT + +class CppClassWrapperWriter(CppBaseWrapperWriter): """ - This class generates wrapper code for Cpp classes + This class generates wrapper code for C++ classes + + Attributes + ---------- + class_info : CppClassInfo + The class information + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + exposed_class_full_names : list[str] + A list of full names for all classes in the module + class_full_names : list[str] + A list of full names for this class e.g. ["Foo<2,2>", "Foo<3,3>"] + class_short_names : list[str] + A list of short names for this class e.g. ["Foo2_2", "Foo3_3"] + class_decls : list[class_t] + A list of class declarations associated with the class + has_shared_ptr : bool + Whether the class uses shared pointers + is_abstract : bool + Whether the class is abstract + hpp_string : str + The hpp wrapper code + cpp_string : str + The cpp wrapper code """ - def __init__(self, class_info, wrapper_templates): - + def __init__( + self, + class_info: CppClassInfo, + wrapper_templates: dict[str, str], + exposed_class_full_names: list[str], + ): + logger = logging.getLogger() + super(CppClassWrapperWriter, self).__init__(wrapper_templates) - self.hpp_string = "" - self.cpp_string = "" - self.class_info = class_info - self.class_decls = [] - self.exposed_class_full_names = [] - self.class_full_names = self.class_info.get_full_names() - self.class_short_names = self.class_info.get_short_names() - self.has_shared_ptr = True - self.is_abstract = False - - if(len(self.class_full_names) != len(self.class_short_names)): - message = 'Full and short name lists should be the same length' - raise ValueError(message) - - def write_files(self, work_dir, class_short_name): + self.class_info: CppClassInfo = class_info - """ - Write the hpp and cpp wrapper codes to file - """ + # Class full names eg. ["Foo<2,2>", "Foo<3,3>"] + self.class_full_names: list[str] = self.class_info.get_full_names() - path = work_dir + "/" + class_short_name - hpp_file = open(path + ".cppwg.hpp", "w") - hpp_file.write(self.hpp_string) - hpp_file.close() + # Class short names eg. ["Foo2_2", "Foo3_3"] + self.class_short_names: list[str] = self.class_info.get_short_names() - cpp_file = open(path + ".cppwg.cpp", "w") - cpp_file.write(self.cpp_string) - cpp_file.close() + if len(self.class_full_names) != len(self.class_short_names): + logger.error("Full and short name lists should be the same length") + raise AssertionError() - def add_hpp(self, class_short_name): - + self.exposed_class_full_names: list[str] = exposed_class_full_names + + self.class_decls: list[class_t] = [] + self.has_shared_ptr: bool = True + self.is_abstract: bool = False + + self.hpp_string: str = "" + self.cpp_string: str = "" + + def add_hpp(self, class_short_name: str) -> None: """ - Add the class wrapper hpp file + Fill the class hpp string from the wrapper template + + Parameters + ---------- + class_short_name: str + The short name of the class e.g. Foo2_2 """ - wrapper_dict = {'class_short_name': class_short_name} - self.hpp_string += self.wrapper_templates['class_hpp_header'].format(**wrapper_dict) + class_hpp_dict = {"class_short_name": class_short_name} - def add_cpp_header(self, class_full_name, class_short_name): - + self.hpp_string += self.wrapper_templates["class_hpp_header"].format( + **class_hpp_dict + ) + + def add_cpp_header(self, class_full_name: str, class_short_name: str) -> None: """ Add the 'top' of the class wrapper cpp file + + Parameters + ---------- + class_full_name : str + The full name of the class e.g. Foo<2,2> + class_short_name : str + The short name of the class e.g. Foo2_2 """ header = "wrapper_header_collection" - + # Check for custom smart pointers smart_ptr_handle = "" - smart_pointer_handle = self.class_info.hierarchy_attribute('smart_ptr_type') + smart_pointer_handle = self.class_info.hierarchy_attribute("smart_ptr_type") if smart_pointer_handle != None: smart_ptr_template = self.wrapper_templates["smart_pointer_holder"] - smart_ptr_handle = "\n" + smart_ptr_template.format(smart_pointer_handle) + ";" - - header_dict = {'wrapper_header_collection': header, - 'class_short_name': class_short_name, - 'class_full_name': class_full_name, - 'smart_ptr_handle': smart_ptr_handle, - 'includes': '#include "' + header +'.hpp"\n'} + smart_ptr_handle = ( + "\n" + smart_ptr_template.format(smart_pointer_handle) + ";" + ) + + header_dict = { + "wrapper_header_collection": header, + "class_short_name": class_short_name, + "class_full_name": class_full_name, + "smart_ptr_handle": smart_ptr_handle, + "includes": '#include "' + header + '.hpp"\n', + } extra_include_string = "" - common_include_file = self.class_info.hierarchy_attribute('common_include_file') - - source_includes = self.class_info.hierarchy_attribute_gather('source_includes') + common_include_file = self.class_info.hierarchy_attribute("common_include_file") + + source_includes = self.class_info.hierarchy_attribute_gather("source_includes") if not common_include_file: for eachInclude in source_includes: if eachInclude[0] != "<": - extra_include_string += '#include "' + eachInclude +'"\n' + extra_include_string += '#include "' + eachInclude + '"\n' else: - extra_include_string += '#include ' + eachInclude +'\n' + extra_include_string += "#include " + eachInclude + "\n" if self.class_info.source_file is not None: - extra_include_string += '#include "' + self.class_info.source_file +'"\n' + extra_include_string += ( + '#include "' + self.class_info.source_file + '"\n' + ) else: - include_name = ntpath.basename(self.class_info.decl.location.file_name) - extra_include_string += '#include "' + include_name +'"\n' - header_dict['includes'] = extra_include_string + include_name = os.path.basename(self.class_info.decl.location.file_name) + extra_include_string += '#include "' + include_name + '"\n' + header_dict["includes"] = extra_include_string header_string = self.wrapper_templates["class_cpp_header"].format(**header_dict) self.cpp_string += header_string - + for eachLine in self.class_info.prefix_code: self.cpp_string += eachLine + "\n" - + # Any custom generators if self.class_info.custom_generator is not None: - self.cpp_string += self.class_info.custom_generator.get_class_cpp_pre_code(class_short_name) + self.cpp_string += self.class_info.custom_generator.get_class_cpp_pre_code( + class_short_name + ) - def add_virtual_overides(self, class_decl, short_class_name): - + def add_virtual_overrides( + self, class_decl: class_t, short_class_name: str + ) -> list[member_function_t]: """ - Virtual over-rides if neeeded + Virtual overrides if neeeded + + Parameters + ---------- + class_decl : class_t + The class declaration + short_class_name : str + The short name of the class e.g. Foo2_2 + + Returns + ------- + list[member_function_t]: A list of member functions needing override """ # Identify any methods needing over-rides, i.e. any that are virtual # here or in a parent. methods_needing_override = [] return_types = [] - for eachMemberFunction in class_decl.member_functions(allow_empty=True): - is_pure_virtual = eachMemberFunction.virtuality == "pure virtual" - is_virtual = eachMemberFunction.virtuality == "virtual" + for member_function in class_decl.member_functions(allow_empty=True): + is_pure_virtual = member_function.virtuality == "pure virtual" + is_virtual = member_function.virtuality == "virtual" if is_pure_virtual or is_virtual: - methods_needing_override.append(eachMemberFunction) - return_types.append(eachMemberFunction.return_type.decl_string) + methods_needing_override.append(member_function) + return_types.append(member_function.return_type.decl_string) if is_pure_virtual: self.is_abstract = True for eachReturnString in return_types: if eachReturnString != self.tidy_name(eachReturnString): typdef_string = "typedef {full_name} {tidy_name};\n" - typdef_dict = {'full_name': eachReturnString, - 'tidy_name': self.tidy_name(eachReturnString)} + typdef_dict = { + "full_name": eachReturnString, + "tidy_name": self.tidy_name(eachReturnString), + } self.cpp_string += typdef_string.format(**typdef_dict) self.cpp_string += "\n" needs_override = len(methods_needing_override) > 0 if needs_override: - over_ride_dict = {'class_short_name': short_class_name, - 'class_base_name': self.class_info.name} - override_template = self.wrapper_templates['class_virtual_override_header'] + over_ride_dict = { + "class_short_name": short_class_name, + "class_base_name": self.class_info.name, + } + override_template = self.wrapper_templates["class_virtual_override_header"] self.cpp_string += override_template.format(**over_ride_dict) for eachMethod in methods_needing_override: - writer = method_writer.CppMethodWrapperWriter(self.class_info, - eachMethod, - class_decl, - self.wrapper_templates, - short_class_name) + writer = CppMethodWrapperWriter( + self.class_info, + eachMethod, + class_decl, + self.wrapper_templates, + short_class_name, + ) self.cpp_string = writer.add_override(self.cpp_string) self.cpp_string += "\n};\n" + return methods_needing_override - def write(self, work_dir): + def write(self, work_dir: str) -> None: + """ + Write the hpp and cpp wrapper codes to file - if(len(self.class_decls) != len(self.class_full_names)): - message = 'Not enough class decls added to do write.' - raise ValueError(message) + Parameters + ---------- + work_dir : str + The directory to write the files to + """ + logger = logging.getLogger() + + if len(self.class_decls) != len(self.class_full_names): + logger.error("Not enough class decls added to do write.") + raise AssertionError() for idx, full_name in enumerate(self.class_full_names): short_name = self.class_short_names[idx] @@ -161,103 +237,164 @@ def write(self, work_dir): # Add the cpp file header self.add_cpp_header(full_name, short_name) - + # Check for struct-enum pattern if declarations.is_struct(class_decl): enums = class_decl.enumerations(allow_empty=True) - if len(enums)==1: - replacements = {'class': class_decl.name, 'enum': enums[0].name} - self.cpp_string += 'void register_{class}_class(py::module &m){{\n'.format(**replacements) - self.cpp_string += ' py::class_<{class}> myclass(m, "{class}");\n'.format(**replacements) - self.cpp_string += ' py::enum_<{class}::{enum}>(myclass, "{enum}")\n'.format(**replacements) + if len(enums) == 1: + replacements = {"class": class_decl.name, "enum": enums[0].name} + self.cpp_string += ( + "void register_{class}_class(py::module &m){{\n".format( + **replacements + ) + ) + self.cpp_string += ( + ' py::class_<{class}> myclass(m, "{class}");\n'.format( + **replacements + ) + ) + self.cpp_string += ( + ' py::enum_<{class}::{enum}>(myclass, "{enum}")\n'.format( + **replacements + ) + ) for eachval in enums[0].values: - replacements = {'class': class_decl.name, - 'enum': enums[0].name, - 'val': eachval[0]} - self.cpp_string += ' .value("{val}", {class}::{enum}::{val})\n'.format(**replacements) + replacements = { + "class": class_decl.name, + "enum": enums[0].name, + "val": eachval[0], + } + self.cpp_string += ( + ' .value("{val}", {class}::{enum}::{val})\n'.format( + **replacements + ) + ) self.cpp_string += " .export_values();\n}\n" - + # Set up the hpp self.add_hpp(short_name) - + # Do the write self.write_files(work_dir, short_name) continue # Define any virtual function overloads - methods_needing_override = self.add_virtual_overides(class_decl, short_name) + methods_needing_override = self.add_virtual_overrides( + class_decl, short_name + ) # Add overrides if needed overrides_string = "" - if len(methods_needing_override)>0: - overrides_string = ', ' + short_name + '_Overloads' + if len(methods_needing_override) > 0: + overrides_string = ", " + short_name + "_Overloads" # Add smart ptr support if needed - smart_pointer_handle = self.class_info.hierarchy_attribute('smart_ptr_type') + smart_pointer_handle = self.class_info.hierarchy_attribute("smart_ptr_type") ptr_support = "" if self.has_shared_ptr and smart_pointer_handle is not None: - ptr_support = ', ' + smart_pointer_handle + '<' + short_name + ' > ' + ptr_support = ", " + smart_pointer_handle + "<" + short_name + " > " # Add base classes if needed bases = "" - for eachBase in class_decl.bases: - cleaned_base = eachBase.related_class.name.replace(" ","") - exposed = any(cleaned_base in t.replace(" ","") for t in self.exposed_class_full_names) - public = not eachBase.access_type == "private" + for base in class_decl.bases: + cleaned_base = base.related_class.name.replace(" ", "") + exposed = any( + cleaned_base in t.replace(" ", "") + for t in self.exposed_class_full_names + ) + public = not base.access_type == "private" if exposed and public: - bases += ', ' + eachBase.related_class.name + " " - - # Add the class refistration - class_definition_dict = {'short_name': short_name, - 'overrides_string': overrides_string, - 'ptr_support': ptr_support, - 'bases': bases} + bases += ", " + base.related_class.name + " " + + # Add the class registration + class_definition_dict = { + "short_name": short_name, + "overrides_string": overrides_string, + "ptr_support": ptr_support, + "bases": bases, + } class_definition_template = self.wrapper_templates["class_definition"] self.cpp_string += class_definition_template.format(**class_definition_dict) # Add constructors - #if not self.is_abstract and not class_decl.is_abstract: + # if not self.is_abstract and not class_decl.is_abstract: # No constructors for classes with private pure virtual methods! + ppv_class = False - for eachMemberFunction in class_decl.member_functions(allow_empty=True): - if eachMemberFunction.virtuality == "pure virtual" and eachMemberFunction.access_type == "private": + for member_function in class_decl.member_functions(allow_empty=True): + if ( + member_function.virtuality == "pure virtual" + and member_function.access_type == "private" + ): ppv_class = True break - + if not ppv_class: - query = declarations.access_type_matcher_t('public') - for eachConstructor in class_decl.constructors(function=query, - allow_empty=True): - writer = constructor_writer.CppConsturctorWrapperWriter(self.class_info, - eachConstructor, - class_decl, - self.wrapper_templates, - short_name) + query = declarations.access_type_matcher_t("public") + for constructor in class_decl.constructors( + function=query, allow_empty=True + ): + writer = CppConstructorWrapperWriter( + self.class_info, + constructor, + class_decl, + self.wrapper_templates, + short_name, + ) + # TODO: Consider returning the constructor string instead self.cpp_string = writer.add_self(self.cpp_string) # Add public member functions - query = declarations.access_type_matcher_t('public') - for eachMemberFunction in class_decl.member_functions(function=query, allow_empty=True): - exlcuded = False + query = declarations.access_type_matcher_t("public") + for member_function in class_decl.member_functions( + function=query, allow_empty=True + ): + excluded = False if self.class_info.excluded_methods is not None: - exlcuded = (eachMemberFunction.name in self.class_info.excluded_methods) - if not exlcuded: - writer = method_writer.CppMethodWrapperWriter(self.class_info, - eachMemberFunction, - class_decl, - self.wrapper_templates, - short_name) + excluded = member_function.name in self.class_info.excluded_methods + if not excluded: + writer = CppMethodWrapperWriter( + self.class_info, + member_function, + class_decl, + self.wrapper_templates, + short_name, + ) + # TODO: Consider returning the member string instead self.cpp_string = writer.add_self(self.cpp_string) - + # Any custom generators if self.class_info.custom_generator is not None: - self.cpp_string += self.class_info.custom_generator.get_class_cpp_def_code(short_name) + self.cpp_string += ( + self.class_info.custom_generator.get_class_cpp_def_code(short_name) + ) # Close the class definition - self.cpp_string += ' ;\n}\n' + self.cpp_string += " ;\n}\n" # Set up the hpp self.add_hpp(short_name) # Do the write self.write_files(work_dir, short_name) + + def write_files(self, work_dir: str, class_short_name: str) -> None: + """ + Write the hpp and cpp wrapper code to file + + Parameters + ---------- + work_dir : str + The directory to write the files to + class_short_name : str + The short name of the class + """ + + hpp_filepath = os.path.join(work_dir, f"{class_short_name}.{CPPWG_EXT}.hpp") + cpp_filepath = os.path.join(work_dir, f"{class_short_name}.{CPPWG_EXT}.cpp") + + with open(hpp_filepath, "w") as hpp_file: + hpp_file.write(self.hpp_string) + + with open(cpp_filepath, "w") as cpp_file: + cpp_file.write(self.cpp_string) diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index 4745c1d..c37dfc0 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -9,6 +9,7 @@ from cppwg.writers.free_function_writer import CppFreeFunctionWrapperWriter from cppwg.writers.class_writer import CppClassWrapperWriter +from cppwg.utils.constants import CPPWG_EXT from cppwg.utils.constants import CPPWG_HEADER_COLLECTION_FILENAME @@ -89,7 +90,7 @@ def write_module_wrapper(self) -> None: for class_info in self.module_info.class_info_collection: for short_name in class_info.get_short_names(): # Example: #include "Foo2_2.cppwg.hpp" - cpp_string += f'#include "{short_name}.cppwg.hpp"\n' + cpp_string += f'#include "{short_name}.{CPPWG_EXT}.hpp"\n' # Format module name as _packagename_modulename full_module_name = ( @@ -106,6 +107,7 @@ def write_module_wrapper(self) -> None: function_writer = CppFreeFunctionWrapperWriter( free_function_info, self.wrapper_templates ) + # TODO: Consider returning the function string instead cpp_string = function_writer.add_self(cpp_string) # Add classes @@ -144,6 +146,7 @@ def write_class_wrappers(self) -> None: ) # Get the declaration for each class and add it to the class writer + # TODO: Consider using class_info.decl instead for full_name in class_info.get_full_names(): name = full_name.replace(" ", "") # e.g. Foo<2,2> From 4085e3c326c72b3d71c7c45f99d47bcc1b71c69d Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 17:30:27 +0000 Subject: [PATCH 40/53] #11 more class writer updates --- cppwg/input/module_info.py | 7 ++- cppwg/templates/pybind11_default.py | 6 ++- cppwg/writers/class_writer.py | 81 ++++++++++++++++------------- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 90a154e..8afc067 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -57,7 +57,7 @@ def parent(self) -> "PackageInfo": def is_decl_in_source_path(self, decl: declaration_t) -> bool: """ - Check if the declaration is associated with a file in the current source path + Check if the declaration is associated with a file in a specified source path Parameters ---------- @@ -67,11 +67,9 @@ def is_decl_in_source_path(self, decl: declaration_t) -> bool: Returns ------- bool - True if the declaration is associated with a file in the current source path + True if the declaration is associated with a file in a specified source path """ - # TODO: Logic for source_locations is not implemented e.g. package info - # parser does not set it so this will always return True if self.source_locations is None: return True @@ -79,4 +77,5 @@ def is_decl_in_source_path(self, decl: declaration_t) -> bool: full_path = os.path.join(self.package_info.source_root, source_location) if full_path in decl.location.file_name: return True + return False diff --git a/cppwg/templates/pybind11_default.py b/cppwg/templates/pybind11_default.py index c1a0790..56f6ffa 100644 --- a/cppwg/templates/pybind11_default.py +++ b/cppwg/templates/pybind11_default.py @@ -6,7 +6,8 @@ #include "{class_short_name}.cppwg.hpp" namespace py = pybind11; -typedef {class_full_name} {class_short_name};{smart_ptr_handle} +typedef {class_full_name} {class_short_name}; +{smart_ptr_handle}; """ class_cpp_header_chaste = """\ @@ -19,7 +20,8 @@ namespace py = pybind11; //PYBIND11_CVECTOR_TYPECASTER2(); //PYBIND11_CVECTOR_TYPECASTER3(); -typedef {class_full_name} {class_short_name};{smart_ptr_handle} +typedef {class_full_name} {class_short_name}; +{smart_ptr_handle}; """ class_hpp_header = """\ diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index a99c7f9..e687786 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -11,7 +11,7 @@ from cppwg.writers.method_writer import CppMethodWrapperWriter from cppwg.writers.constructor_writer import CppConstructorWrapperWriter -from cppwg.utils.constants import CPPWG_EXT +from cppwg.utils.constants import CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILENAME class CppClassWrapperWriter(CppBaseWrapperWriter): @@ -75,7 +75,7 @@ def __init__( def add_hpp(self, class_short_name: str) -> None: """ - Fill the class hpp string from the wrapper template + Fill the class hpp string for a single class using the wrapper template Parameters ---------- @@ -91,7 +91,7 @@ def add_hpp(self, class_short_name: str) -> None: def add_cpp_header(self, class_full_name: str, class_short_name: str) -> None: """ - Add the 'top' of the class wrapper cpp file + Add the 'top' of the class wrapper cpp file for a single class Parameters ---------- @@ -101,52 +101,59 @@ def add_cpp_header(self, class_full_name: str, class_short_name: str) -> None: The short name of the class e.g. Foo2_2 """ - header = "wrapper_header_collection" + # Add the includes for this class + includes = "" + common_include_file = self.class_info.hierarchy_attribute("common_include_file") + + if common_include_file: + includes += f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n' + + else: + source_includes = self.class_info.hierarchy_attribute_gather( + "source_includes" + ) + + for source_include in source_includes: + if source_include[0] == "<": + # e.g. #include + includes += f"#include {source_include}\n" + else: + # e.g. #include "Foo.hpp" + includes += f'#include "{source_include}"\n' + + source_file = self.class_info.source_file + if not source_file: + source_file = os.path.basename(self.class_info.decl.location.file_name) + includes += f'#include "{source_file}"\n' + + # Check for custom smart pointers e.g. "boost::shared_ptr" + smart_ptr_type: str = self.class_info.hierarchy_attribute("smart_ptr_type") - # Check for custom smart pointers smart_ptr_handle = "" - smart_pointer_handle = self.class_info.hierarchy_attribute("smart_ptr_type") - if smart_pointer_handle != None: - smart_ptr_template = self.wrapper_templates["smart_pointer_holder"] - smart_ptr_handle = ( - "\n" + smart_ptr_template.format(smart_pointer_handle) + ";" + if smart_ptr_type: + # Adds e.g. "PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr)" + smart_ptr_handle = self.wrapper_templates["smart_pointer_holder"].format( + smart_ptr_type ) + # Fill in the cpp header template header_dict = { - "wrapper_header_collection": header, + "includes": includes, "class_short_name": class_short_name, "class_full_name": class_full_name, "smart_ptr_handle": smart_ptr_handle, - "includes": '#include "' + header + '.hpp"\n', } - extra_include_string = "" - common_include_file = self.class_info.hierarchy_attribute("common_include_file") - source_includes = self.class_info.hierarchy_attribute_gather("source_includes") - - if not common_include_file: - for eachInclude in source_includes: - if eachInclude[0] != "<": - extra_include_string += '#include "' + eachInclude + '"\n' - else: - extra_include_string += "#include " + eachInclude + "\n" - if self.class_info.source_file is not None: - extra_include_string += ( - '#include "' + self.class_info.source_file + '"\n' - ) - else: - include_name = os.path.basename(self.class_info.decl.location.file_name) - extra_include_string += '#include "' + include_name + '"\n' - header_dict["includes"] = extra_include_string - - header_string = self.wrapper_templates["class_cpp_header"].format(**header_dict) - self.cpp_string += header_string + self.cpp_string += self.wrapper_templates["class_cpp_header"].format( + **header_dict + ) - for eachLine in self.class_info.prefix_code: - self.cpp_string += eachLine + "\n" + # Add any specified custom prefix code + for code_line in self.class_info.prefix_code: + self.cpp_string += code_line + "\n" - # Any custom generators - if self.class_info.custom_generator is not None: + # 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_short_name ) From c461abbe1915b13973cc4721c7c4710a142e8709 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 18:10:04 +0000 Subject: [PATCH 41/53] #11 update class writer --- cppwg/writers/class_writer.py | 49 ++++++++++++++--------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index e687786..a01e171 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -103,9 +103,8 @@ def add_cpp_header(self, class_full_name: str, class_short_name: str) -> None: # Add the includes for this class includes = "" - common_include_file = self.class_info.hierarchy_attribute("common_include_file") - if common_include_file: + if self.class_info.hierarchy_attribute("common_include_file"): includes += f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n' else: @@ -245,37 +244,27 @@ def write(self, work_dir: str) -> None: # Add the cpp file header self.add_cpp_header(full_name, short_name) - # Check for struct-enum pattern + # Check for struct-enum pattern. For example: + # struct Foo{ + # enum Value{A, B, C}; + # }; + # TODO: Consider moving some parts into templates if declarations.is_struct(class_decl): enums = class_decl.enumerations(allow_empty=True) + if len(enums) == 1: + enum_tpl = "void register_{class}_class(py::module &m){{\n" + enum_tpl += ' py::class_<{class}> myclass(m, "{class}");\n' + enum_tpl += ' py::enum_<{class}::{enum}>(myclass, "{enum}")\n' + replacements = {"class": class_decl.name, "enum": enums[0].name} - self.cpp_string += ( - "void register_{class}_class(py::module &m){{\n".format( - **replacements - ) - ) - self.cpp_string += ( - ' py::class_<{class}> myclass(m, "{class}");\n'.format( - **replacements - ) - ) - self.cpp_string += ( - ' py::enum_<{class}::{enum}>(myclass, "{enum}")\n'.format( - **replacements - ) - ) - for eachval in enums[0].values: - replacements = { - "class": class_decl.name, - "enum": enums[0].name, - "val": eachval[0], - } - self.cpp_string += ( - ' .value("{val}", {class}::{enum}::{val})\n'.format( - **replacements - ) - ) + self.cpp_string += enum_tpl.format(**replacements) + + value_tpl = ' .value("{val}", {class}::{enum}::{val})\n' + for value in enums[0].values: + replacements["val"] = value[0] + self.cpp_string += value_tpl.format(**replacements) + self.cpp_string += " .export_values();\n}\n" # Set up the hpp @@ -394,7 +383,7 @@ def write_files(self, work_dir: str, class_short_name: str) -> None: work_dir : str The directory to write the files to class_short_name : str - The short name of the class + The short name of the class e.g. Foo2_2 """ hpp_filepath = os.path.join(work_dir, f"{class_short_name}.{CPPWG_EXT}.hpp") From e66432b260094b571f790bd83a45282b5926f6ea Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 19:23:46 +0000 Subject: [PATCH 42/53] #11 additional class writer updates --- cppwg/writers/class_writer.py | 79 +++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index a01e171..6118ab5 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -161,7 +161,8 @@ def add_virtual_overrides( self, class_decl: class_t, short_class_name: str ) -> list[member_function_t]: """ - Virtual overrides if neeeded + 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. Parameters ---------- @@ -175,10 +176,10 @@ def add_virtual_overrides( list[member_function_t]: A list of member functions needing override """ - # Identify any methods needing over-rides, i.e. any that are virtual - # here or in a parent. - methods_needing_override = [] - return_types = [] + methods_needing_override: list[member_function_t] = [] + return_types: list[str] = [] # e.g. ["void", "unsigned int", "::Bar<2> *"] + + # Collect all virtual methods and their return types for member_function in class_decl.member_functions(allow_empty=True): is_pure_virtual = member_function.virtuality == "pure virtual" is_virtual = member_function.virtuality == "virtual" @@ -188,34 +189,52 @@ def add_virtual_overrides( if is_pure_virtual: self.is_abstract = True - for eachReturnString in return_types: - if eachReturnString != self.tidy_name(eachReturnString): - typdef_string = "typedef {full_name} {tidy_name};\n" - typdef_dict = { - "full_name": eachReturnString, - "tidy_name": self.tidy_name(eachReturnString), + # Add typedefs for return types with special characters + # e.g. typedef ::Bar<2> * _Bar_lt_2_gt_Ptr; + for return_type in return_types: + if return_type != self.tidy_name(return_type): + typedef_template = "typedef {full_name} {tidy_name};\n" + typedef_dict = { + "full_name": return_type, + "tidy_name": self.tidy_name(return_type), } - self.cpp_string += typdef_string.format(**typdef_dict) + self.cpp_string += typedef_template.format(**typedef_dict) self.cpp_string += "\n" - needs_override = len(methods_needing_override) > 0 - if needs_override: - over_ride_dict = { + # Override virtual methods + if methods_needing_override: + # Add override header, e.g.: + # class Foo_Overloads : public Foo{{ + # public: + # using Foo::Foo; + override_header_dict = { "class_short_name": short_class_name, "class_base_name": self.class_info.name, } - override_template = self.wrapper_templates["class_virtual_override_header"] - self.cpp_string += override_template.format(**over_ride_dict) - for eachMethod in methods_needing_override: - writer = CppMethodWrapperWriter( + self.cpp_string += self.wrapper_templates[ + "class_virtual_override_header" + ].format(**override_header_dict) + + # Override each method, e.g.: + # void bar(int a, bool b) override {{ + # PYBIND11_OVERRIDE( + # void, + # Foo, + # bar, + # a, + # b); + for method in methods_needing_override: + method_writer = CppMethodWrapperWriter( self.class_info, - eachMethod, + method, class_decl, self.wrapper_templates, short_class_name, ) - self.cpp_string = writer.add_override(self.cpp_string) + # TODO: Consider returning the override string instead + self.cpp_string = method_writer.add_override(self.cpp_string) + self.cpp_string += "\n};\n" return methods_needing_override @@ -270,18 +289,20 @@ def write(self, work_dir: str) -> None: # Set up the hpp self.add_hpp(short_name) - # Do the write + # Write the struct cpp and hpp files self.write_files(work_dir, short_name) continue - # Define any virtual function overloads - methods_needing_override = self.add_virtual_overrides( - class_decl, short_name + # Find and define virtual function overrides e.g. using PYBIND11_OVERRIDE + methods_needing_override: list[member_function_t] = ( + self.add_virtual_overrides(class_decl, short_name) ) - # Add overrides if needed + # Add "Foo_Overloads" to the wrapper class definition if needed + # e.g. py::class_ >(m, "Foo") + # TODO: Assign the "_Overloads" literal to a constant overrides_string = "" - if len(methods_needing_override) > 0: + if methods_needing_override: overrides_string = ", " + short_name + "_Overloads" # Add smart ptr support if needed @@ -312,6 +333,10 @@ def write(self, work_dir: str) -> None: class_definition_template = self.wrapper_templates["class_definition"] self.cpp_string += class_definition_template.format(**class_definition_dict) + if overrides_string: + print(class_definition_template.format(**class_definition_dict)) + exit() + # Add constructors # if not self.is_abstract and not class_decl.is_abstract: # No constructors for classes with private pure virtual methods! From 684f58f35d5bf14c6e093a893e2ea933c85c34bd Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Thu, 14 Mar 2024 19:25:21 +0000 Subject: [PATCH 43/53] #11 minor writer edits --- cppwg/writers/class_writer.py | 4 ---- cppwg/writers/method_writer.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 6118ab5..6f311cc 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -333,10 +333,6 @@ def write(self, work_dir: str) -> None: class_definition_template = self.wrapper_templates["class_definition"] self.cpp_string += class_definition_template.format(**class_definition_dict) - if overrides_string: - print(class_definition_template.format(**class_definition_dict)) - exit() - # Add constructors # if not self.is_abstract and not class_decl.is_abstract: # No constructors for classes with private pure virtual methods! diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 53aad63..576423e 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -59,7 +59,7 @@ def add_self(self, output_string): if self.exclusion_critera(): return output_string - # Which definition type + # Pybind11 def type e.g. "_static" for def_static() def_adorn = "" if self.method_decl.has_static: def_adorn += "_static" From b0a374dd28fa74aecf43984ed640725c1725f539 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 15 Mar 2024 12:42:23 +0000 Subject: [PATCH 44/53] #11 update class writer registration --- cppwg/writers/class_writer.py | 51 +++++++++++++++++----------------- cppwg/writers/module_writer.py | 7 +++-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 6f311cc..1cbee3b 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -203,7 +203,7 @@ def add_virtual_overrides( # Override virtual methods if methods_needing_override: - # Add override header, e.g.: + # Add virtual override class, e.g.: # class Foo_Overloads : public Foo{{ # public: # using Foo::Foo; @@ -218,12 +218,7 @@ def add_virtual_overrides( # Override each method, e.g.: # void bar(int a, bool b) override {{ - # PYBIND11_OVERRIDE( - # void, - # Foo, - # bar, - # a, - # b); + # PYBIND11_OVERRIDE(void, Foo, bar, a, b); for method in methods_needing_override: method_writer = CppMethodWrapperWriter( self.class_info, @@ -293,35 +288,39 @@ def write(self, work_dir: str) -> None: self.write_files(work_dir, short_name) continue - # Find and define virtual function overrides e.g. using PYBIND11_OVERRIDE + # Find and define virtual function "trampoline" overrides methods_needing_override: list[member_function_t] = ( self.add_virtual_overrides(class_decl, short_name) ) - # Add "Foo_Overloads" to the wrapper class definition if needed - # e.g. py::class_ >(m, "Foo") - # TODO: Assign the "_Overloads" literal to a constant + # Add the virtual "trampoline" overrides from "Foo_Overloads" to + # the "Foo" wrapper class definition if needed + # e.g. py::class_(m, "Foo") overrides_string = "" if methods_needing_override: - overrides_string = ", " + short_name + "_Overloads" + # TODO: Assign the "_Overloads" literal to a constant + overrides_string = f", {short_name}_Overloads" - # Add smart ptr support if needed - smart_pointer_handle = self.class_info.hierarchy_attribute("smart_ptr_type") + # Add smart pointer support to the wrapper class definition if needed + # e.g. py::class_ >(m, "Foo") + smart_ptr_type: str = self.class_info.hierarchy_attribute("smart_ptr_type") ptr_support = "" - if self.has_shared_ptr and smart_pointer_handle is not None: - ptr_support = ", " + smart_pointer_handle + "<" + short_name + " > " + if self.has_shared_ptr and smart_ptr_type: + ptr_support = f", {smart_ptr_type}<{short_name} > " - # Add base classes if needed + # Add base classes to the wrapper class definition if needed + # e.g. py::class_(m, "Foo") bases = "" - for base in class_decl.bases: - cleaned_base = base.related_class.name.replace(" ", "") - exposed = any( - cleaned_base in t.replace(" ", "") - for t in self.exposed_class_full_names - ) - public = not base.access_type == "private" - if exposed and public: - bases += ", " + base.related_class.name + " " + + for base in class_decl.bases: # type(base) -> hierarchy_info_t + # Check that the base class is not private + if base.access_type == "private": + continue + + # Check if the base class is exposed (i.e. to be wrapped in the module) + base_class_name: str = base.related_class.name.replace(" ", "") + if base_class_name in self.exposed_class_full_names: + bases += f", {base.related_class.name} " # Add the class registration class_definition_dict = { diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index c37dfc0..aabe202 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -30,7 +30,7 @@ class CppModuleWrapperWriter: package_license : str The license to include in the generated wrapper code exposed_class_full_names : list[str] - A list of all class full names in the module + A list of full names of all classes to be wrapped in the module """ def __init__( @@ -49,12 +49,13 @@ def __init__( package_license # TODO: use this in the generated wrappers ) - # For convenience, create a list of all class full names in the module + # For convenience, create a list of all classes to be wrapped in the module # e.g. ['Foo', 'Bar<2>', 'Bar<3>'] self.exposed_class_full_names: list[str] = [] for class_info in self.module_info.class_info_collection: - self.exposed_class_full_names.extend(class_info.get_full_names()) + for full_name in class_info.get_full_names(): + self.exposed_class_full_names.append(full_name.replace(" ", "")) def write_module_wrapper(self) -> None: """ From 9662c9eb9dcc3891d8763498589e3f213bfe4537 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Fri, 15 Mar 2024 18:31:00 +0000 Subject: [PATCH 45/53] #11 update constructor writer --- cppwg/input/base_info.py | 6 +- cppwg/writers/base_writer.py | 8 ++ cppwg/writers/class_writer.py | 45 +++----- cppwg/writers/constructor_writer.py | 166 ++++++++++++++++++++-------- 4 files changed, 146 insertions(+), 79 deletions(-) diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index 9b38496..cc5876a 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -33,11 +33,11 @@ class BaseInfo: excluded_variables : list[str] Do not include these variables. constructor_arg_type_excludes : list[str] - List of exlude patterns for ctors. + List of exclude patterns for ctors. return_type_excludes : list[str] - List of exlude patterns for return types. + List of exclude patterns for return types. arg_type_excludes : list[str] - List of exlude patterns for arg types. + List of exclude patterns for arg types. name_replacements : dict[str, str] A dictionary of name replacements e.g. {"double":"Double", "unsigned int":"Unsigned"} """ diff --git a/cppwg/writers/base_writer.py b/cppwg/writers/base_writer.py index 23e1fb2..5ce5a06 100644 --- a/cppwg/writers/base_writer.py +++ b/cppwg/writers/base_writer.py @@ -93,5 +93,13 @@ def exclusion_critera( # TODO: This method is currently a placeholder. Consider implementing or removing. def default_arg_exclusion_criteria(self) -> bool: + """ + Check if default arguments should be excluded from the wrapper code + + Returns + ------- + bool + True if the default arguments should be excluded + """ return False diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 1cbee3b..e822beb 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -68,7 +68,7 @@ def __init__( self.class_decls: list[class_t] = [] self.has_shared_ptr: bool = True - self.is_abstract: bool = False + self.is_abstract: bool = False # TODO: Consider removing unused attribute self.hpp_string: str = "" self.cpp_string: str = "" @@ -333,32 +333,19 @@ def write(self, work_dir: str) -> None: self.cpp_string += class_definition_template.format(**class_definition_dict) # Add constructors - # if not self.is_abstract and not class_decl.is_abstract: - # No constructors for classes with private pure virtual methods! - - ppv_class = False - for member_function in class_decl.member_functions(allow_empty=True): - if ( - member_function.virtuality == "pure virtual" - and member_function.access_type == "private" - ): - ppv_class = True - break - - if not ppv_class: - query = declarations.access_type_matcher_t("public") - for constructor in class_decl.constructors( - function=query, allow_empty=True - ): - writer = CppConstructorWrapperWriter( - self.class_info, - constructor, - class_decl, - self.wrapper_templates, - short_name, - ) - # TODO: Consider returning the constructor string instead - self.cpp_string = writer.add_self(self.cpp_string) + query = declarations.access_type_matcher_t("public") + for constructor in class_decl.constructors( + function=query, allow_empty=True + ): + constructor_writer = CppConstructorWrapperWriter( + self.class_info, + constructor, + class_decl, + self.wrapper_templates, + short_name, + ) + # TODO: Consider returning the constructor string instead + self.cpp_string = constructor_writer.add_self(self.cpp_string) # Add public member functions query = declarations.access_type_matcher_t("public") @@ -369,7 +356,7 @@ def write(self, work_dir: str) -> None: if self.class_info.excluded_methods is not None: excluded = member_function.name in self.class_info.excluded_methods if not excluded: - writer = CppMethodWrapperWriter( + method_writer = CppMethodWrapperWriter( self.class_info, member_function, class_decl, @@ -377,7 +364,7 @@ def write(self, work_dir: str) -> None: short_name, ) # TODO: Consider returning the member string instead - self.cpp_string = writer.add_self(self.cpp_string) + self.cpp_string = method_writer.add_self(self.cpp_string) # Any custom generators if self.class_info.custom_generator is not None: diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index 1b5db81..433fe62 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -1,77 +1,149 @@ +from typing import Optional + from pygccxml import declarations +from pygccxml.declarations.class_declaration import class_t +from pygccxml.declarations.calldef_members import constructor_t -from cppwg.writers import base_writer +from cppwg.input.class_info import CppClassInfo +from cppwg.writers.base_writer import CppBaseWrapperWriter -class CppConsturctorWrapperWriter(base_writer.CppBaseWrapperWriter): +class CppConstructorWrapperWriter(CppBaseWrapperWriter): """ Manage addition of constructor wrapper code + + Attributes + ---------- + class_info : ClassInfo + The class information for the class containing the constructor + ctor_decl : constructor_t + The pygccxml declaration object for the constructor + class_decl : class_t + The class declaration for the class containing the constructor + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + class_short_name : Optional[str] + The short name of the class e.g. 'Foo2_2' """ - def __init__(self, class_info, - ctor_decl, - class_decl, - wrapper_templates, - class_short_name=None): - - super(CppConsturctorWrapperWriter, self).__init__(wrapper_templates) + def __init__( + self, + class_info: CppClassInfo, + ctor_decl: constructor_t, + class_decl: class_t, + wrapper_templates: dict[str, str], + class_short_name: Optional[str] = None, + ): + + super(CppConstructorWrapperWriter, self).__init__(wrapper_templates) - self.class_info = class_info - self.ctor_decl = ctor_decl - self.class_decl = class_decl + self.class_info: CppClassInfo = class_info + self.ctor_decl: constructor_t = ctor_decl + self.class_decl: class_t = class_decl self.class_short_name = class_short_name if self.class_short_name is None: self.class_short_name = self.class_decl.name - def exclusion_critera(self): - - # Check for exclusions - exclusion_args = self.class_info.hierarchy_attribute_gather('calldef_excludes') - ctor_arg_exludes = self.class_info.hierarchy_attribute_gather('constructor_arg_type_excludes') - - for eachArg in self.ctor_decl.argument_types: - if eachArg.decl_string.replace(" ", "") in exclusion_args: - return True - - for eachExclude in ctor_arg_exludes: - if eachExclude in eachArg.decl_string: - return True + def exclusion_critera(self) -> bool: + """ + Check if the constructor should be excluded from the wrapper code + + Returns + ------- + bool + True if the constructor should be excluded, False otherwise + """ + + # Exclude constructors for classes with private pure virtual methods + if any( + mf.virtuality == "pure virtual" and mf.access_type == "private" + for mf in self.class_decl.member_functions(allow_empty=True) + ): + return True - for eachArg in self.ctor_decl.argument_types: - if "iterator" in eachArg.decl_string.lower(): + # Exclude constructors for abstract classes inheriting from abstract bases + if self.class_decl.is_abstract and len(self.class_decl.recursive_bases) > 0: + if any( + base.related_class.is_abstract + for base in self.class_decl.recursive_bases + ): return True + # Exclude sub class (e.g. iterator) constructors such as: + # class Foo { + # public: + # class FooIterator { if self.ctor_decl.parent != self.class_decl: return True - if self.ctor_decl.is_artificial and declarations.is_copy_constructor(self.ctor_decl): + # Exclude default copy constructors e.g. Foo::Foo(Foo const & foo) + if ( + declarations.is_copy_constructor(self.ctor_decl) + and self.ctor_decl.is_artificial + ): return True - - if self.class_decl.is_abstract and len(self.class_decl.recursive_bases)>0: - if any(t.related_class.is_abstract for t in self.class_decl.recursive_bases): - return True + + # Check for excluded arg patterns + calldef_excludes = self.class_info.hierarchy_attribute_gather( + "calldef_excludes" + ) + + ctor_arg_type_excludes = self.class_info.hierarchy_attribute_gather( + "constructor_arg_type_excludes" + ) + + for arg_type in self.ctor_decl.argument_types: + # Exclude constructors with "iterator" args + if "iterator" in arg_type.decl_string.lower(): + return True + + # Exclude args matching calldef_excludes + if arg_type.decl_string.replace(" ", "") in calldef_excludes: + return True + + # Exclude args matching constructor_arg_type_excludes + for excluded_arg_type in ctor_arg_type_excludes: + if excluded_arg_type in arg_type.decl_string: + return True return False - def add_self(self, output_string): + def add_self(self, cpp_string: str) -> str: + """ + Add the constructor wrapper code + + Parameters + ---------- + cpp_string : str + The current wrapper code + Returns + ------- + str + The wrapper code with the constructor added + """ + + # Skip excluded constructors if self.exclusion_critera(): - return output_string + return cpp_string + + # Add the constructor definition e.g. + # .def(py::init(), py::arg("i") = 1, py::arg("b") = false) + cpp_string += " .def(py::init<" + + arg_types = [t.decl_string for t in self.ctor_decl.argument_types] + cpp_string += ", ".join(arg_types) - output_string += " "*8 + '.def(py::init<' - num_arg_types = len(self.ctor_decl.argument_types) - for idx, eachArg in enumerate(self.ctor_decl.argument_types): - output_string += eachArg.decl_string - if idx < num_arg_types-1: - output_string += ", " - output_string += ' >()' + cpp_string += " >()" default_args = "" if not self.default_arg_exclusion_criteria(): - for eachArg in self.ctor_decl.arguments: - default_args += ', py::arg("{}")'.format(eachArg.name) - if eachArg.default_value is not None: - default_args += ' = ' + eachArg.default_value - output_string += default_args + ')\n' - return output_string + for arg in self.ctor_decl.arguments: + default_args += f', py::arg("{arg.name}")' + if arg.default_value is not None: + default_args += " = " + arg.default_value + + cpp_string += default_args + ")\n" + + return cpp_string From 56922e0a2ff076412454e4c7b687def6bc3b1905 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 16 Mar 2024 00:11:58 +0000 Subject: [PATCH 46/53] #11 update method writer --- cppwg/writers/base_writer.py | 2 +- cppwg/writers/class_writer.py | 27 ++-- cppwg/writers/constructor_writer.py | 46 +++--- cppwg/writers/free_function_writer.py | 2 +- cppwg/writers/method_writer.py | 204 +++++++++++++++++--------- 5 files changed, 176 insertions(+), 105 deletions(-) diff --git a/cppwg/writers/base_writer.py b/cppwg/writers/base_writer.py index 5ce5a06..4c5ac9a 100644 --- a/cppwg/writers/base_writer.py +++ b/cppwg/writers/base_writer.py @@ -58,7 +58,7 @@ def tidy_name(self, name: str) -> str: # TODO: Consider moving this implementation of exclusion_criteria to the # free function writer it is only used there. exclusion_criteria is # currently overriden in method writer and constructor writer. - def exclusion_critera( + def exclusion_criteria( self, decl: free_function_t, exclusion_args: list[str] ) -> bool: """ diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index e822beb..5b4b124 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -332,7 +332,7 @@ def write(self, work_dir: str) -> None: class_definition_template = self.wrapper_templates["class_definition"] self.cpp_string += class_definition_template.format(**class_definition_dict) - # Add constructors + # Add public constructors query = declarations.access_type_matcher_t("public") for constructor in class_decl.constructors( function=query, allow_empty=True @@ -352,19 +352,20 @@ def write(self, work_dir: str) -> None: for member_function in class_decl.member_functions( function=query, allow_empty=True ): - excluded = False if self.class_info.excluded_methods is not None: - excluded = member_function.name in self.class_info.excluded_methods - if not excluded: - method_writer = CppMethodWrapperWriter( - self.class_info, - member_function, - class_decl, - self.wrapper_templates, - short_name, - ) - # TODO: Consider returning the member string instead - self.cpp_string = method_writer.add_self(self.cpp_string) + # Skip excluded methods + if member_function.name in self.class_info.excluded_methods: + continue + + method_writer = CppMethodWrapperWriter( + self.class_info, + member_function, + class_decl, + self.wrapper_templates, + short_name, + ) + # TODO: Consider returning the member string instead + self.cpp_string = method_writer.add_self(self.cpp_string) # Any custom generators if self.class_info.custom_generator is not None: diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index 433fe62..17b8fa3 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -45,7 +45,7 @@ def __init__( if self.class_short_name is None: self.class_short_name = self.class_decl.name - def exclusion_critera(self) -> bool: + def exclusion_criteria(self) -> bool: """ Check if the constructor should be excluded from the wrapper code @@ -84,48 +84,56 @@ def exclusion_critera(self) -> bool: ): return True - # Check for excluded arg patterns - calldef_excludes = self.class_info.hierarchy_attribute_gather( - "calldef_excludes" - ) + # Check for excluded argument patterns + calldef_excludes = [ + x.replace(" ", "") + for x in self.class_info.hierarchy_attribute_gather("calldef_excludes") + ] - ctor_arg_type_excludes = self.class_info.hierarchy_attribute_gather( - "constructor_arg_type_excludes" - ) + ctor_arg_type_excludes = [ + x.replace(" ", "") + for x in self.class_info.hierarchy_attribute_gather( + "constructor_arg_type_excludes" + ) + ] + # TODO: test pattern matching for arg_type in self.ctor_decl.argument_types: - # Exclude constructors with "iterator" args - if "iterator" in arg_type.decl_string.lower(): + # e.g. ::std::vector const & -> ::std::vectorconst& + arg_type_str = arg_type.decl_string.replace(" ", "") + + # Exclude constructors with "iterator" in args + if "iterator" in arg_type_str.lower(): return True - # Exclude args matching calldef_excludes - if arg_type.decl_string.replace(" ", "") in calldef_excludes: + # Exclude constructors with args matching calldef_excludes + if arg_type_str in calldef_excludes: return True - # Exclude args matching constructor_arg_type_excludes - for excluded_arg_type in ctor_arg_type_excludes: - if excluded_arg_type in arg_type.decl_string: + # Exclude constructurs with args matching constructor_arg_type_excludes + for excluded_type in ctor_arg_type_excludes: + if excluded_type in arg_type_str: return True return False def add_self(self, cpp_string: str) -> str: """ - Add the constructor wrapper code + Add the constructor wrapper code to the input string Parameters ---------- cpp_string : str - The current wrapper code + The input string containing current wrapper code Returns ------- str - The wrapper code with the constructor added + The input string with the constructor wrapper code added """ # Skip excluded constructors - if self.exclusion_critera(): + if self.exclusion_criteria(): return cpp_string # Add the constructor definition e.g. diff --git a/cppwg/writers/free_function_writer.py b/cppwg/writers/free_function_writer.py index e8d231c..048c355 100644 --- a/cppwg/writers/free_function_writer.py +++ b/cppwg/writers/free_function_writer.py @@ -41,7 +41,7 @@ def add_self(self, wrapper_string) -> str: """ # Skip this free function if it uses any excluded arg types or return types - if self.exclusion_critera(self.free_function_info.decl, self.exclusion_args): + if self.exclusion_criteria(self.free_function_info.decl, self.exclusion_args): return wrapper_string # Pybind11 def type e.g. "_static" for def_static() diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 576423e..50dfcab 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -1,63 +1,118 @@ +from typing import Optional + from pygccxml import declarations +from pygccxml.declarations.class_declaration import class_t +from pygccxml.declarations.calldef_members import member_function_t -from cppwg.writers import base_writer +from cppwg.input.class_info import CppClassInfo +from cppwg.writers.base_writer import CppBaseWrapperWriter -class CppMethodWrapperWriter(base_writer.CppBaseWrapperWriter): +class CppMethodWrapperWriter(CppBaseWrapperWriter): """ Manage addition of method wrapper code + + Attributes + ---------- + class_info : ClassInfo + The class information for the class containing the method + method_decl : member_function_t + The pygccxml declaration object for the method + class_decl : class_t + The class declaration for the class containing the method + wrapper_templates : dict[str, str] + String templates with placeholders for generating wrapper code + class_short_name : Optional[str] + The short name of the class e.g. 'Foo2_2' """ - def __init__(self, class_info, - method_decl, - class_decl, - wrapper_templates, - class_short_name=None): + def __init__( + self, + class_info: CppClassInfo, + method_decl: member_function_t, + class_decl: class_t, + wrapper_templates: dict[str, str], + class_short_name: Optional[str] = None, + ): super(CppMethodWrapperWriter, self).__init__(wrapper_templates) - self.class_info = class_info - self.method_decl = method_decl - self.class_decl = class_decl - self.class_short_name = class_short_name + self.class_info: CppClassInfo = class_info + self.method_decl: member_function_t = method_decl + self.class_decl: class_t = class_decl + + self.class_short_name: str = class_short_name if self.class_short_name is None: - self.class_short_name = self.class_decl + self.class_short_name = self.class_decl.name - def exclusion_critera(self): + def exclusion_criteria(self) -> bool: + """ + Check if the method should be excluded from the wrapper code - # Are any return types not wrappable - exclusion_args = self.class_info.hierarchy_attribute_gather('calldef_excludes') - return_excludes = self.class_info.hierarchy_attribute_gather('return_type_excludes') - - return_type = self.method_decl.return_type.decl_string.replace(" ", "") - if return_type in exclusion_args or return_type in return_excludes: + Returns + ------- + bool + True if the method should be excluded, False otherwise + """ + + # Exclude private methods without over-rides + if self.method_decl.access_type == "private": return True - # Don't include sub class (e.g. iterator) methods + # Exclude sub class (e.g. iterator) methods such as: + # class Foo { + # public: + # class FooIterator { if self.method_decl.parent != self.class_decl: return True - # No private methods without over-rides - if self.method_decl.access_type == "private": + # Check for excluded return types + calldef_excludes = [ + x.replace(" ", "") + for x in self.class_info.hierarchy_attribute_gather("calldef_excludes") + ] + + return_type_excludes = [ + x.replace(" ", "") + for x in self.class_info.hierarchy_attribute_gather("return_type_excludes") + ] + + return_type = self.method_decl.return_type.decl_string.replace(" ", "") + if return_type in calldef_excludes or return_type in return_type_excludes: return True - # Are any arguments not wrappable - tidied_excl = [x.replace(" ", "") for x in exclusion_args] - for eachArg in self.method_decl.argument_types: - arg_type = eachArg.decl_string.split()[0].replace(" ", "") - if arg_type in tidied_excl: + # Check for excluded argument patterns + for argument_type in self.method_decl.argument_types: + # e.g. ::std::vector const & -> ::std::vector const & -> ::std::vectorconst& + arg_type_full = argument_type.decl_string.replace(" ", "") + if arg_type_full in calldef_excludes: + return True + return False - def add_self(self, output_string): + def add_self(self, cpp_string): + """ + Add the method wrapper code to the input string - # Check for exclusions - if self.exclusion_critera(): - return output_string + Parameters + ---------- + cpp_string : str + The input string containing current wrapper code + + Returns + ------- + str + The input string with the constructor wrapper code added + """ + + # Skip excluded methods + if self.exclusion_criteria(): + return cpp_string # Pybind11 def type e.g. "_static" for def_static() def_adorn = "" @@ -75,13 +130,13 @@ def add_self(self, output_string): num_arg_types = len(self.method_decl.argument_types) for idx, eachArg in enumerate(self.method_decl.argument_types): arg_signature += eachArg.decl_string - if idx < num_arg_types-1: + if idx < num_arg_types - 1: arg_signature += ", " # Const-ness const_adorn = "" if self.method_decl.has_const: - const_adorn = ' const ' + const_adorn = " const " # Default args default_args = "" @@ -90,20 +145,22 @@ def add_self(self, output_string): for idx, eachArg in enumerate(self.method_decl.arguments): default_args += ', py::arg("{}")'.format(eachArg.name) if eachArg.default_value is not None: - + # Hack for missing template in default args repl_value = str(eachArg.default_value) if "" in repl_value: if "<2>" in str(arg_types[idx]).replace(" ", ""): - repl_value = repl_value.replace("","<2>") + repl_value = repl_value.replace("", "<2>") elif "<3>" in str(arg_types[idx]).replace(" ", ""): - repl_value= repl_value.replace("","<3>") - default_args += ' = ' + repl_value + repl_value = repl_value.replace("", "<3>") + default_args += " = " + repl_value # Call policy - pointer_call_policy = self.class_info.hierarchy_attribute('pointer_call_policy') - reference_call_policy = self.class_info.hierarchy_attribute('reference_call_policy') - + pointer_call_policy = self.class_info.hierarchy_attribute("pointer_call_policy") + reference_call_policy = self.class_info.hierarchy_attribute( + "reference_call_policy" + ) + call_policy = "" is_ptr = declarations.is_pointer(self.method_decl.return_type) if pointer_call_policy is not None and is_ptr: @@ -111,20 +168,22 @@ def add_self(self, output_string): is_ref = declarations.is_reference(self.method_decl.return_type) if reference_call_policy is not None and is_ref: call_policy = ", py::return_value_policy::" + reference_call_policy - - method_dict = {'def_adorn': def_adorn, - 'method_name': self.method_decl.name, - 'return_type': self.method_decl.return_type.decl_string, - 'self_ptr': self_ptr, - 'arg_signature': arg_signature, - 'const_adorn': const_adorn, - 'class_short_name': self.class_short_name, - 'method_docs': '" "', - 'default_args': default_args, - 'call_policy': call_policy} + + method_dict = { + "def_adorn": def_adorn, + "method_name": self.method_decl.name, + "return_type": self.method_decl.return_type.decl_string, + "self_ptr": self_ptr, + "arg_signature": arg_signature, + "const_adorn": const_adorn, + "class_short_name": self.class_short_name, + "method_docs": '" "', + "default_args": default_args, + "call_policy": call_policy, + } template = self.wrapper_templates["class_method"] - output_string += template.format(**method_dict) - return output_string + cpp_string += template.format(**method_dict) + return cpp_string def add_override(self, output_string): @@ -136,7 +195,7 @@ def add_override(self, output_string): args = self.method_decl.arguments for idx, eachArg in enumerate(self.method_decl.argument_types): arg_string += eachArg.decl_string + " " + args[idx].name - if idx < num_arg_types-1: + if idx < num_arg_types - 1: arg_string += ", " const_adorn = "" @@ -145,23 +204,26 @@ def add_override(self, output_string): overload_adorn = "" if self.method_decl.virtuality == "pure virtual": - overload_adorn = "_PURE" + overload_adorn = "_PURE" all_args_string = "" for idx, eachArg in enumerate(self.method_decl.argument_types): - all_args_string += ""*8 + args[idx].name - if idx < num_arg_types-1: + all_args_string += "" * 8 + args[idx].name + if idx < num_arg_types - 1: all_args_string += ", \n" return_string = self.method_decl.return_type.decl_string - override_dict = {'return_type': return_string, - 'method_name': self.method_decl.name, - 'arg_string': arg_string, - 'const_adorn': const_adorn, - 'overload_adorn': overload_adorn, - 'tidy_method_name': self.tidy_name(return_string), - 'short_class_name': self.class_short_name, - 'args_string': all_args_string, - } - output_string += self.wrapper_templates["method_virtual_override"].format(**override_dict) + override_dict = { + "return_type": return_string, + "method_name": self.method_decl.name, + "arg_string": arg_string, + "const_adorn": const_adorn, + "overload_adorn": overload_adorn, + "tidy_method_name": self.tidy_name(return_string), + "short_class_name": self.class_short_name, + "args_string": all_args_string, + } + output_string += self.wrapper_templates["method_virtual_override"].format( + **override_dict + ) return output_string From deb4e8b68871ece86557bb693de2c5561174f151 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 16 Mar 2024 23:30:45 +0000 Subject: [PATCH 47/53] #11 update method writer add self --- cppwg/writers/class_writer.py | 8 +-- cppwg/writers/constructor_writer.py | 5 +- cppwg/writers/method_writer.py | 76 +++++++++++++++-------------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index 5b4b124..feeabc3 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -352,7 +352,7 @@ def write(self, work_dir: str) -> None: for member_function in class_decl.member_functions( function=query, allow_empty=True ): - if self.class_info.excluded_methods is not None: + if self.class_info.excluded_methods: # Skip excluded methods if member_function.name in self.class_info.excluded_methods: continue @@ -367,8 +367,8 @@ def write(self, work_dir: str) -> None: # TODO: Consider returning the member string instead self.cpp_string = method_writer.add_self(self.cpp_string) - # Any custom generators - if self.class_info.custom_generator is not None: + # 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(short_name) ) @@ -379,7 +379,7 @@ def write(self, work_dir: str) -> None: # Set up the hpp self.add_hpp(short_name) - # Do the write + # Write the class cpp and hpp files self.write_files(work_dir, short_name) def write_files(self, work_dir: str, class_short_name: str) -> None: diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index 17b8fa3..49a0c1c 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -97,7 +97,6 @@ def exclusion_criteria(self) -> bool: ) ] - # TODO: test pattern matching for arg_type in self.ctor_decl.argument_types: # e.g. ::std::vector const & -> ::std::vectorconst& arg_type_str = arg_type.decl_string.replace(" ", "") @@ -149,8 +148,10 @@ def add_self(self, cpp_string: str) -> str: if not self.default_arg_exclusion_criteria(): for arg in self.ctor_decl.arguments: default_args += f', py::arg("{arg.name}")' + if arg.default_value is not None: - default_args += " = " + arg.default_value + # TODO: Fix in default args (see method_writer) + default_args += f" = {arg.default_value}" cpp_string += default_args + ")\n" diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 50dfcab..1ec0412 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -120,54 +120,55 @@ def add_self(self, cpp_string): def_adorn += "_static" # How to point to class - if not self.method_decl.has_static: - self_ptr = self.class_short_name + "::*" - else: + if self.method_decl.has_static: self_ptr = "*" - - # Get the arg signature - arg_signature = "" - num_arg_types = len(self.method_decl.argument_types) - for idx, eachArg in enumerate(self.method_decl.argument_types): - arg_signature += eachArg.decl_string - if idx < num_arg_types - 1: - arg_signature += ", " + else: + # e.g. Foo2_2::* + self_ptr = self.class_short_name + "::*" # Const-ness const_adorn = "" if self.method_decl.has_const: const_adorn = " const " + # Get the arg signature e.g. "int, bool" + arg_types = [t.decl_string for t in self.method_decl.argument_types] + arg_signature = ", ".join(arg_types) + # Default args default_args = "" if not self.default_arg_exclusion_criteria(): - arg_types = self.method_decl.argument_types - for idx, eachArg in enumerate(self.method_decl.arguments): - default_args += ', py::arg("{}")'.format(eachArg.name) - if eachArg.default_value is not None: + for arg, arg_type in zip( + self.method_decl.arguments, self.method_decl.argument_types + ): + default_args += f', py::arg("{arg.name}")' - # Hack for missing template in default args - repl_value = str(eachArg.default_value) - if "" in repl_value: - if "<2>" in str(arg_types[idx]).replace(" ", ""): - repl_value = repl_value.replace("", "<2>") - elif "<3>" in str(arg_types[idx]).replace(" ", ""): - repl_value = repl_value.replace("", "<3>") - default_args += " = " + repl_value - - # Call policy - pointer_call_policy = self.class_info.hierarchy_attribute("pointer_call_policy") - reference_call_policy = self.class_info.hierarchy_attribute( - "reference_call_policy" - ) + if arg.default_value is not None: + default_value = str(arg.default_value) + # Hack for missing template in default args + # e.g. Foo<2>::bar(Bar<2> const & b = Bar()) + # TODO: Make more robust + arg_type_str = str(arg_type).replace(" ", "") + if "" in default_value: + if "<2>" in arg_type_str: + default_value = default_value.replace("", "<2>") + elif "<3>" in arg_type_str: + default_value = default_value.replace("", "<3>") + + default_args += f" = {default_value}" + + # Call policy, e.g. "py::return_value_policy::reference" call_policy = "" - is_ptr = declarations.is_pointer(self.method_decl.return_type) - if pointer_call_policy is not None and is_ptr: - call_policy = ", py::return_value_policy::" + pointer_call_policy - is_ref = declarations.is_reference(self.method_decl.return_type) - if reference_call_policy is not None and is_ref: - call_policy = ", py::return_value_policy::" + reference_call_policy + if declarations.is_pointer(self.method_decl.return_type): + ptr_policy = self.class_info.hierarchy_attribute("pointer_call_policy") + if ptr_policy: + call_policy = f", py::return_value_policy::{ptr_policy}" + + elif declarations.is_reference(self.method_decl.return_type): + ref_policy = self.class_info.hierarchy_attribute("reference_call_policy") + if ref_policy: + call_policy = f", py::return_value_policy::{ref_policy}" method_dict = { "def_adorn": def_adorn, @@ -181,8 +182,9 @@ def add_self(self, cpp_string): "default_args": default_args, "call_policy": call_policy, } - template = self.wrapper_templates["class_method"] - cpp_string += template.format(**method_dict) + class_method_template = self.wrapper_templates["class_method"] + cpp_string += class_method_template.format(**method_dict) + return cpp_string def add_override(self, output_string): From 7ff85adf6a07b548ccb970f492479f3325bdc816 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 17 Mar 2024 00:01:39 +0000 Subject: [PATCH 48/53] #11 update method writer --- cppwg/writers/constructor_writer.py | 9 +++++---- cppwg/writers/method_writer.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index 49a0c1c..fa640ae 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -118,7 +118,8 @@ def exclusion_criteria(self) -> bool: def add_self(self, cpp_string: str) -> str: """ - Add the constructor wrapper code to the input string + Add the constructor wrapper code to the input string for example: + .def(py::init(), py::arg("i") = 1, py::arg("b") = false) Parameters ---------- @@ -135,15 +136,15 @@ def add_self(self, cpp_string: str) -> str: if self.exclusion_criteria(): return cpp_string - # Add the constructor definition e.g. - # .def(py::init(), py::arg("i") = 1, py::arg("b") = false) + # Get the arg signature e.g. "int, bool" cpp_string += " .def(py::init<" arg_types = [t.decl_string for t in self.ctor_decl.argument_types] cpp_string += ", ".join(arg_types) cpp_string += " >()" - + + # Default args e.g. py::arg("i") = 1 default_args = "" if not self.default_arg_exclusion_criteria(): for arg in self.ctor_decl.arguments: diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 1ec0412..d46fe2b 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -97,7 +97,8 @@ def exclusion_criteria(self) -> bool: def add_self(self, cpp_string): """ - Add the method wrapper code to the input string + Add the method wrapper code to the input string. For example: + .def("bar", (void(Foo::*)(double)) &Foo::bar, " ", py::arg("d") = 1.0) Parameters ---------- @@ -107,7 +108,7 @@ def add_self(self, cpp_string): Returns ------- str - The input string with the constructor wrapper code added + The input string with the method wrapper code added """ # Skip excluded methods @@ -135,7 +136,7 @@ def add_self(self, cpp_string): arg_types = [t.decl_string for t in self.method_decl.argument_types] arg_signature = ", ".join(arg_types) - # Default args + # Default args e.g. py::arg("d") = 1.0 default_args = "" if not self.default_arg_exclusion_criteria(): for arg, arg_type in zip( From d6faa946c068e9ba12da5609e762495e9aecb39c Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 17 Mar 2024 22:27:30 +0000 Subject: [PATCH 49/53] #11 update method writer add override --- cppwg/writers/method_writer.py | 65 ++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index d46fe2b..1f09b8a 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -95,7 +95,7 @@ def exclusion_criteria(self) -> bool: return False - def add_self(self, cpp_string): + def add_self(self, cpp_string) -> str: """ Add the method wrapper code to the input string. For example: .def("bar", (void(Foo::*)(double)) &Foo::bar, " ", py::arg("d") = 1.0) @@ -188,34 +188,60 @@ def add_self(self, cpp_string): return cpp_string - def add_override(self, output_string): + def add_override(self, cpp_string) -> str: + """ + Add overrides for virtual methods to the input string. + + Parameters + ---------- + cpp_string : str + The input string containing current wrapper code + + Returns + ------- + str + The input string with the virtual override wrapper code added + """ + # Skip private methods if self.method_decl.access_type == "private": - return output_string + return cpp_string + + # arg_string = "" + # num_arg_types = len(self.method_decl.argument_types) + # args = self.method_decl.arguments + # for idx, eachArg in enumerate(self.method_decl.argument_types): + # arg_string += eachArg.decl_string + " " + args[idx].name + # if idx < num_arg_types-1: + # arg_string += ", " + + # Get list of arguments and types + arg_list = [] + arg_name_list = [] + + for arg, arg_type in zip( + self.method_decl.arguments, self.method_decl.argument_types + ): + arg_list.append(f"{arg_type.decl_string} {arg.name}") + arg_name_list.append(f" {arg.name}") - arg_string = "" - num_arg_types = len(self.method_decl.argument_types) - args = self.method_decl.arguments - for idx, eachArg in enumerate(self.method_decl.argument_types): - arg_string += eachArg.decl_string + " " + args[idx].name - if idx < num_arg_types - 1: - arg_string += ", " + arg_string = ", ".join(arg_list) # e.g. "int a, bool b, double c" + arg_name_string = ",\n".join(arg_name_list) # e.g. "a,\n b,\n c" + # Const-ness const_adorn = "" if self.method_decl.has_const: const_adorn = " const " + # For pure virtual methods, use PYBIND11_OVERRIDE_PURE overload_adorn = "" if self.method_decl.virtuality == "pure virtual": overload_adorn = "_PURE" - all_args_string = "" - for idx, eachArg in enumerate(self.method_decl.argument_types): - all_args_string += "" * 8 + args[idx].name - if idx < num_arg_types - 1: - all_args_string += ", \n" - + # Get the return type e.g. "void" return_string = self.method_decl.return_type.decl_string + + # Add the override code from the template override_dict = { "return_type": return_string, "method_name": self.method_decl.name, @@ -224,9 +250,10 @@ def add_override(self, output_string): "overload_adorn": overload_adorn, "tidy_method_name": self.tidy_name(return_string), "short_class_name": self.class_short_name, - "args_string": all_args_string, + "args_string": arg_name_string, } - output_string += self.wrapper_templates["method_virtual_override"].format( + cpp_string += self.wrapper_templates["method_virtual_override"].format( **override_dict ) - return output_string + + return cpp_string From 000e9257eed48c9eeeed913cc3c185d191fc07ef Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 17 Mar 2024 22:31:21 +0000 Subject: [PATCH 50/53] #11 update method writer --- cppwg/writers/method_writer.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 1f09b8a..77abbe9 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -207,14 +207,6 @@ def add_override(self, cpp_string) -> str: if self.method_decl.access_type == "private": return cpp_string - # arg_string = "" - # num_arg_types = len(self.method_decl.argument_types) - # args = self.method_decl.arguments - # for idx, eachArg in enumerate(self.method_decl.argument_types): - # arg_string += eachArg.decl_string + " " + args[idx].name - # if idx < num_arg_types-1: - # arg_string += ", " - # Get list of arguments and types arg_list = [] arg_name_list = [] From d0ad79b513a2a6cf864c3797db9d51489583755b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 18 Mar 2024 11:31:50 +0000 Subject: [PATCH 51/53] #11 update generator --- cppwg/generators.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 5e985b2..ed667df 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -42,14 +42,14 @@ class CppWrapperGenerator: The path to the CastXML binary castxml_cflags : str Optional cflags to be passed to CastXML e.g. "-std=c++17" - package_info_file : str + package_info_path : str The path to the package info yaml config file; defaults to "package_info.yaml" source_hpp_files : list[str] The list of C++ source header files source_ns : namespace_t The namespace containing C++ declarations parsed from the source tree package_info : PackageInfo - A data structure containing the information parsed from package_info_file + A data structure containing the information parsed from package_info_path """ def __init__( @@ -58,7 +58,7 @@ def __init__( source_includes: Optional[list[str]] = None, wrapper_root: Optional[str] = None, castxml_binary: Optional[str] = "castxml", - package_info_file: Optional[str] = None, + package_info_path: Optional[str] = None, castxml_cflags: Optional[str] = "-std=c++17", ): logging.basicConfig( @@ -107,19 +107,19 @@ def __init__( else: self.source_includes = [self.source_root] - # Sanitize package_info_file - self.package_info_file: Optional[str] = None - if package_info_file: + # Sanitize package_info_path + self.package_info_path: Optional[str] = None + if package_info_path: # If a package info config file is specified, check that it exists - self.package_info_file = package_info_file - if not os.path.isfile(package_info_file): - logger.error(f"Could not find package info file: {package_info_file}") + self.package_info_path = package_info_path + if not os.path.isfile(package_info_path): + logger.error(f"Could not find package info file: {package_info_path}") raise FileNotFoundError() else: # If no package info config file has been supplied, check the default default_package_info_file = os.path.abspath("./package_info.yaml") if os.path.isfile(default_package_info_file): - self.package_info_file = default_package_info_file + self.package_info_path = default_package_info_file logger.info( f"Package info file not specified - using {default_package_info_file}" ) @@ -219,9 +219,9 @@ def parse_package_info(self): Parse the package info file to create a PackageInfo object """ - if self.package_info_file: + if self.package_info_path: # If a package info file exists, parse it to create a PackageInfo object - info_parser = PackageInfoParser(self.package_info_file, self.source_root) + info_parser = PackageInfoParser(self.package_info_path, self.source_root) self.package_info = info_parser.parse() else: From 9b2a32a28eddc07a4e343f5440915d340361f304 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 18 Mar 2024 15:51:25 +0000 Subject: [PATCH 52/53] #11 use Python3.8 compatible type hints --- cppwg/generators.py | 12 +++--- cppwg/input/base_info.py | 52 +++++++++++------------ cppwg/input/class_info.py | 4 +- cppwg/input/cpp_type_info.py | 16 +++---- cppwg/input/free_function_info.py | 4 +- cppwg/input/info_helper.py | 6 +-- cppwg/input/module_info.py | 20 ++++----- cppwg/input/package_info.py | 22 +++++----- cppwg/input/variable_info.py | 2 +- cppwg/parsers/package_info.py | 8 ++-- cppwg/parsers/source_parser.py | 12 +++--- cppwg/utils/utils.py | 6 +-- cppwg/writers/base_writer.py | 9 ++-- cppwg/writers/class_writer.py | 32 +++++++------- cppwg/writers/constructor_writer.py | 6 +-- cppwg/writers/free_function_writer.py | 8 ++-- cppwg/writers/header_collection_writer.py | 8 ++-- cppwg/writers/method_writer.py | 6 +-- cppwg/writers/module_writer.py | 12 +++--- tests/test_shapes.py | 8 ++-- 20 files changed, 130 insertions(+), 123 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index ed667df..f29ed5a 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -5,7 +5,7 @@ import subprocess from pathlib import Path -from typing import Optional +from typing import Dict, List, Optional from pygccxml import __version__ as pygccxml_version from pygccxml.declarations.namespace import namespace_t @@ -34,7 +34,7 @@ class CppWrapperGenerator: ---------- source_root : str The root directory of the C++ source code - source_includes : list[str] + source_includes : List[str] The list of source include paths wrapper_root : str The output directory for the wrapper code @@ -44,7 +44,7 @@ class CppWrapperGenerator: Optional cflags to be passed to CastXML e.g. "-std=c++17" package_info_path : str The path to the package info yaml config file; defaults to "package_info.yaml" - source_hpp_files : list[str] + source_hpp_files : List[str] The list of C++ source header files source_ns : namespace_t The namespace containing C++ declarations parsed from the source tree @@ -55,7 +55,7 @@ class CppWrapperGenerator: def __init__( self, source_root: str, - source_includes: Optional[list[str]] = None, + source_includes: Optional[List[str]] = None, wrapper_root: Optional[str] = None, castxml_binary: Optional[str] = "castxml", package_info_path: Optional[str] = None, @@ -92,7 +92,7 @@ def __init__( ) # Sanitize source_includes - self.source_includes: list[str] # type hinting + self.source_includes: List[str] # type hinting if source_includes: self.source_includes = [ os.path.abspath(include_path) for include_path in source_includes @@ -142,7 +142,7 @@ def __init__( self.castxml_cflags: str = castxml_cflags # Initialize remaining attributes - self.source_hpp_files: list[str] = [] + self.source_hpp_files: List[str] = [] self.source_ns: Optional[namespace_t] = None diff --git a/cppwg/input/base_info.py b/cppwg/input/base_info.py index cc5876a..24cfa2a 100644 --- a/cppwg/input/base_info.py +++ b/cppwg/input/base_info.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Dict, List, Optional class BaseInfo: @@ -10,55 +10,55 @@ class BaseInfo: ---------- name : str The feature name, as it appears in its definition. - source_includes : list[str] + source_includes : List[str] A list of source files to be included with the feature. - calldef_excludes : list[str] + 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]] + 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] + extra_code : List[str] Any extra wrapper code for the feature. - prefix_code : list[str] + prefix_code : List[str] Any wrapper code that precedes the feature. custom_generator : str, optional A custom generator for the feature. - excluded_methods : list[str] + excluded_methods : List[str] Do not include these methods. - excluded_variables : list[str] + excluded_variables : List[str] Do not include these variables. - constructor_arg_type_excludes : list[str] + constructor_arg_type_excludes : List[str] List of exclude patterns for ctors. - return_type_excludes : list[str] + return_type_excludes : List[str] List of exclude patterns for return types. - arg_type_excludes : list[str] + arg_type_excludes : List[str] List of exclude patterns for arg types. - name_replacements : dict[str, str] + 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.source_includes: List[str] = [] + self.calldef_excludes: List[str] = [] self.smart_ptr_type: Optional[str] = None - self.template_substitutions: dict[str, list[Any]] = [] + 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.extra_code: List[str] = [] + self.prefix_code: List[str] = [] self.custom_generator: Optional[str] = None - self.excluded_methods: list[str] = [] - self.excluded_variables: list[str] = [] - self.constructor_arg_type_excludes: list[str] = [] - self.return_type_excludes: list[str] = [] - self.arg_type_excludes: list[str] = [] - self.name_replacements: dict[str, str] = { + self.excluded_methods: List[str] = [] + self.excluded_variables: List[str] = [] + self.constructor_arg_type_excludes: List[str] = [] + self.return_type_excludes: List[str] = [] + self.arg_type_excludes: List[str] = [] + self.name_replacements: Dict[str, str] = { "double": "Double", "unsigned int": "Unsigned", "Unsigned int": "Unsigned", @@ -113,7 +113,7 @@ def hierarchy_attribute(self, attribute_name: str) -> Any: return None - def hierarchy_attribute_gather(self, attribute_name: str) -> list[Any]: + def hierarchy_attribute_gather(self, attribute_name: str) -> List[Any]: """ For the supplied attribute, iterate through parent objects gathering list entries. @@ -124,11 +124,11 @@ def hierarchy_attribute_gather(self, attribute_name: str) -> list[Any]: Returns ------- - list[Any] + List[Any] The list of attribute values. """ - att_list: list[Any] = [] + att_list: List[Any] = [] if hasattr(self, attribute_name) and getattr(self, attribute_name) is not None: att_list.extend(getattr(self, attribute_name)) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 749b10e..865d36e 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Dict, Optional from cppwg.input.cpp_type_info import CppTypeInfo @@ -8,7 +8,7 @@ class CppClassInfo(CppTypeInfo): This class holds information for individual C++ classes to be wrapped """ - def __init__(self, name: str, class_config: Optional[dict[str, Any]] = None): + def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): super(CppClassInfo, self).__init__(name, class_config) diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index fc98749..2832b8a 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -19,13 +19,13 @@ class CppTypeInfo(BaseInfo): The full path to the source file containing the type name_override : str The name override specified in config e.g. "CustomFoo" -> "Foo" - template_arg_lists : list[list[Any]] + template_arg_lists : List[List[Any]] List of template replacement arguments for the type e.g. [[2, 2], [3, 3]] decl : declaration_t The pygccxml declaration associated with this type """ - def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): + def __init__(self, name: str, type_config: Optional[Dict[str, Any]] = None): super(CppTypeInfo, self).__init__(name) @@ -33,7 +33,7 @@ def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): self.source_file_full_path: Optional[str] = None self.source_file: Optional[str] = None self.name_override: Optional[str] = None - self.template_arg_lists: Optional[list[list[Any]]] = None + self.template_arg_lists: Optional[list[List[Any]]] = None self.decl: Optional[declaration_t] = None if type_config: @@ -41,7 +41,7 @@ def __init__(self, name: str, type_config: Optional[dict[str, Any]] = None): setattr(self, key, value) # TODO: Consider setting short and full names on init as read-only properties - def get_short_names(self) -> list[str]: + def get_short_names(self) -> List[str]: """ Return the name of the class as it will appear on the Python side. This collapses template arguments, separating them by underscores and removes @@ -52,7 +52,7 @@ def get_short_names(self) -> list[str]: Returns ------- - list[str] + List[str] The list of short names """ @@ -112,7 +112,7 @@ def get_short_names(self) -> list[str]: return short_names - def get_full_names(self) -> list[str]: + def get_full_names(self) -> List[str]: """ Return the name (declaration) of the class as it appears on the C++ side. The return type is a list, as a class can have multiple names @@ -122,7 +122,7 @@ def get_full_names(self) -> list[str]: Returns ------- - list[str] + List[str] The list of full names """ diff --git a/cppwg/input/free_function_info.py b/cppwg/input/free_function_info.py index a5d1e16..fe008bd 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/input/free_function_info.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Dict, Optional from cppwg.input.cpp_type_info import CppTypeInfo @@ -9,7 +9,7 @@ class CppFreeFunctionInfo(CppTypeInfo): """ def __init__( - self, name: str, free_function_config: Optional[dict[str, Any]] = None + self, name: str, free_function_config: Optional[Dict[str, Any]] = None ): super(CppFreeFunctionInfo, self).__init__(name, free_function_config) diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py index 5b6ce1e..0acb7d3 100644 --- a/cppwg/input/info_helper.py +++ b/cppwg/input/info_helper.py @@ -27,7 +27,7 @@ def __init__(self, module_info: ModuleInfo): self.module_info: ModuleInfo = module_info # For convenience, collect class info in a dict keyed by name - self.class_dict: dict[str, CppClassInfo] = { + self.class_dict: Dict[str, CppClassInfo] = { class_info.name: class_info for class_info in module_info.class_info_collection } @@ -65,7 +65,7 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: # Get list of template substitutions from this feature and its parents # e.g. {"signature":"","replacement":[[2,2], [3,3]]} - template_substitutions: list[dict[str, Any]] = ( + template_substitutions: List[Dict[str, Any]] = ( feature_info.hierarchy_attribute_gather("template_substitutions") ) @@ -94,7 +94,7 @@ def extract_templates_from_source(self, feature_info: BaseInfo) -> None: signature: str = "template" + template_substitution["signature"] # e.g. [[2,2], [3,3]] - replacement: list[list[Any]] = template_substitution["replacement"] + replacement: List[List[Any]] = template_substitution["replacement"] if signature in curr_line: feature_string = feature_type + feature_info.name # e.g. "classFoo" diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 8afc067..75a2b97 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,6 +1,6 @@ import os -from typing import Any, Optional +from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -15,13 +15,13 @@ class ModuleInfo(BaseInfo): ---------- package_info : PackageInfo The package info parent object associated with this module - source_locations : list[str] + source_locations : List[str] A list of source locations for this module - class_info_collection : list[CppClassInfo] + class_info_collection : List[CppClassInfo] A list of class info objects associated with this module - free_function_info_collection : list[CppFreeFunctionInfo] + free_function_info_collection : List[CppFreeFunctionInfo] A list of free function info objects associated with this module - variable_info_collection : list[CppFreeFunctionInfo] + 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 @@ -31,15 +31,15 @@ class ModuleInfo(BaseInfo): Use all variables in the 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): super(ModuleInfo, self).__init__(name) self.package_info: Optional["PackageInfo"] = None - self.source_locations: list[str] = None - self.class_info_collection: list["CppClassInfo"] = [] - self.free_function_info_collection: list["CppFreeFunctionInfo"] = [] - self.variable_info_collection: list["CppFreeFunctionInfo"] = [] + self.source_locations: List[str] = None + self.class_info_collection: List["CppClassInfo"] = [] + self.free_function_info_collection: List["CppFreeFunctionInfo"] = [] + self.variable_info_collection: List["CppFreeFunctionInfo"] = [] self.use_all_classes: bool = False self.use_all_free_functions: bool = False self.use_all_variables: bool = False diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index 7bb3aae..2c030cc 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -11,15 +11,15 @@ class PackageInfo(BaseInfo): ---------- name : str The name of the package - source_locations : list[str] + source_locations : List[str] A list of source locations for this package - module_info_collection : list[ModuleInfo] + 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] + source_hpp_patterns : List[str] A list of source file patterns to include - source_hpp_files : list[str] + 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 @@ -29,25 +29,25 @@ def __init__( self, name: str, source_root: str, - package_config: Optional[dict[str, Any]] = None, + package_config: Optional[Dict[str, Any]] = None, ): """ Parameters: ----------- name : str source_root : str - package_config : dict[str, Any] + package_config : Dict[str, Any] A dictionary of package configuration settings """ super(PackageInfo, self).__init__(name) self.name: str = name - self.source_locations: list[str] = None - self.module_info_collection: list["ModuleInfo"] = [] + self.source_locations: List[str] = None + self.module_info_collection: List["ModuleInfo"] = [] self.source_root: str = source_root - self.source_hpp_patterns: list[str] = ["*.hpp"] - self.source_hpp_files: list[str] = [] + self.source_hpp_patterns: List[str] = ["*.hpp"] + self.source_hpp_files: List[str] = [] self.common_include_file: bool = False if package_config: diff --git a/cppwg/input/variable_info.py b/cppwg/input/variable_info.py index 894df08..97b35a7 100644 --- a/cppwg/input/variable_info.py +++ b/cppwg/input/variable_info.py @@ -8,6 +8,6 @@ class CppVariableInfo(CppTypeInfo): This class holds information for individual variables to be wrapped """ - def __init__(self, name: str, variable_config: Optional[dict[str, Any]] = None): + def __init__(self, name: str, variable_config: Optional[Dict[str, Any]] = None): super(CppVariableInfo, self).__init__(name, variable_config) diff --git a/cppwg/parsers/package_info.py b/cppwg/parsers/package_info.py index 0346ec7..252e2a9 100644 --- a/cppwg/parsers/package_info.py +++ b/cppwg/parsers/package_info.py @@ -28,7 +28,7 @@ class PackageInfoParser: The path to the package info yaml file source_root : str The root directory of the C++ source code - raw_package_info : dict[str, Any] + raw_package_info : Dict[str, Any] Raw info from the yaml file package_info : Optional[PackageInfo] The parsed package info @@ -48,7 +48,7 @@ def __init__(self, input_filepath: str, source_root: str): self.source_root: str = source_root # For holding raw info from the yaml file - self.raw_package_info: dict[str, Any] = {} + self.raw_package_info: Dict[str, Any] = {} # The parsed package info self.package_info: Optional[PackageInfo] = None @@ -121,7 +121,7 @@ def parse(self) -> PackageInfo: self.raw_package_info = yaml.safe_load(input_filepath) # Default config options that apply to the package, modules, classes, and free functions - global_config: dict[str, Any] = { + global_config: Dict[str, Any] = { "source_includes": [], "smart_ptr_type": None, "calldef_excludes": None, @@ -137,7 +137,7 @@ def parse(self) -> PackageInfo: } # Get package config from the raw package info - package_config: dict[str, Any] = { + package_config: Dict[str, Any] = { "name": "cppwg_package", "common_include_file": True, "source_hpp_patterns": ["*.hpp"], diff --git a/cppwg/parsers/source_parser.py b/cppwg/parsers/source_parser.py index edd73e5..4f263b9 100644 --- a/cppwg/parsers/source_parser.py +++ b/cppwg/parsers/source_parser.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -from typing import Optional +from typing import List, Optional from pygccxml import parser, declarations @@ -35,7 +35,7 @@ class CppSourceParser: The path to the header collection file castxml_binary : str The path to the CastXML binary - source_includes : list[str] + 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" @@ -50,13 +50,13 @@ def __init__( source_root: str, wrapper_header_collection: str, castxml_binary: str, - source_includes: list[str], + source_includes: List[str], castxml_cflags: str = "", ): self.source_root: str = source_root self.wrapper_header_collection: str = wrapper_header_collection self.castxml_binary: str = castxml_binary - self.source_includes: list[str] = source_includes + self.source_includes: List[str] = source_includes self.castxml_cflags: str = castxml_cflags self.source_ns: Optional[namespace_t] = None @@ -83,7 +83,7 @@ def parse(self) -> namespace_t: # Parse all the C++ source code to extract declarations logger.info("Parsing source code for declarations.") - decls: list[declaration_t] = parser.parse( + decls: List[declaration_t] = parser.parse( files=[self.wrapper_header_collection], config=xml_generator_config, compilation_mode=parser.COMPILATION_MODE.ALL_AT_ONCE, @@ -99,7 +99,7 @@ def parse(self) -> namespace_t: # Filter declarations in our source tree; include declarations from the # wrapper_header_collection file for explicit instantiations, typedefs etc. - source_decls: list[declaration_t] = [ + source_decls: List[declaration_t] = [ decl for decl in filtered_decls if Path(self.source_root) in Path(decl.location.file_name).parents diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py index b799a9c..b568112 100644 --- a/cppwg/utils/utils.py +++ b/cppwg/utils/utils.py @@ -2,7 +2,7 @@ Utility functions for the cppwg package """ -from typing import Any +from typing import Any, Dict from cppwg.utils.constants import CPPWG_ALL_STRING from cppwg.utils.constants import CPPWG_TRUE_STRINGS, CPPWG_FALSE_STRINGS @@ -27,14 +27,14 @@ def is_option_ALL(input_obj: Any, option_ALL_string: str = CPPWG_ALL_STRING) -> return isinstance(input_obj, str) and input_obj.upper() == option_ALL_string -def substitute_bool_for_string(input_dict: dict[Any, Any], key: Any) -> None: +def substitute_bool_for_string(input_dict: Dict[Any, Any], key: Any) -> None: """ Substitute a string in the input dictionary with a boolean value if the string is a boolean indicator e.g. "ON", "OFF", "YES", "NO", "TRUE", "FALSE" Parameters ---------- - input_dict : dict[Any, Any] + input_dict : Dict[Any, Any] The input dictionary key : Any The key to check diff --git a/cppwg/writers/base_writer.py b/cppwg/writers/base_writer.py index 4c5ac9a..aba0190 100644 --- a/cppwg/writers/base_writer.py +++ b/cppwg/writers/base_writer.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from typing import Dict, List from pygccxml.declarations import free_function_t @@ -9,13 +10,13 @@ class CppBaseWrapperWriter: Attributes ---------- - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code tidy_replacements : OrderedDict[str, str] A dictionary of replacements to use when tidying up C++ declarations """ - def __init__(self, wrapper_templates: dict[str, str]): + def __init__(self, wrapper_templates: Dict[str, str]): self.wrapper_templates = wrapper_templates self.tidy_replacements = OrderedDict( @@ -59,7 +60,7 @@ def tidy_name(self, name: str) -> str: # free function writer it is only used there. exclusion_criteria is # currently overriden in method writer and constructor writer. def exclusion_criteria( - self, decl: free_function_t, exclusion_args: list[str] + self, decl: free_function_t, exclusion_args: List[str] ) -> bool: """ Checks if any of the types in the function declaration appear in the @@ -69,7 +70,7 @@ def exclusion_criteria( ---------- decl : free_function_t The declaration of the function or class - exclusion_args : list[str] + exclusion_args : List[str] A list of arguments to exclude from the wrapper code Returns diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index feeabc3..e42570f 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -1,6 +1,8 @@ import os import logging +from typing import Dict, List + from pygccxml import declarations from pygccxml.declarations.calldef_members import member_function_t from pygccxml.declarations.class_declaration import class_t @@ -22,15 +24,15 @@ class CppClassWrapperWriter(CppBaseWrapperWriter): ---------- class_info : CppClassInfo The class information - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code - exposed_class_full_names : list[str] + exposed_class_full_names : List[str] A list of full names for all classes in the module - class_full_names : list[str] + class_full_names : List[str] A list of full names for this class e.g. ["Foo<2,2>", "Foo<3,3>"] - class_short_names : list[str] + class_short_names : List[str] A list of short names for this class e.g. ["Foo2_2", "Foo3_3"] - class_decls : list[class_t] + class_decls : List[class_t] A list of class declarations associated with the class has_shared_ptr : bool Whether the class uses shared pointers @@ -45,8 +47,8 @@ class CppClassWrapperWriter(CppBaseWrapperWriter): def __init__( self, class_info: CppClassInfo, - wrapper_templates: dict[str, str], - exposed_class_full_names: list[str], + wrapper_templates: Dict[str, str], + exposed_class_full_names: List[str], ): logger = logging.getLogger() @@ -55,18 +57,18 @@ def __init__( self.class_info: CppClassInfo = class_info # Class full names eg. ["Foo<2,2>", "Foo<3,3>"] - self.class_full_names: list[str] = self.class_info.get_full_names() + self.class_full_names: List[str] = self.class_info.get_full_names() # Class short names eg. ["Foo2_2", "Foo3_3"] - self.class_short_names: list[str] = self.class_info.get_short_names() + self.class_short_names: List[str] = self.class_info.get_short_names() if len(self.class_full_names) != len(self.class_short_names): logger.error("Full and short name lists should be the same length") raise AssertionError() - self.exposed_class_full_names: list[str] = exposed_class_full_names + self.exposed_class_full_names: List[str] = exposed_class_full_names - self.class_decls: list[class_t] = [] + self.class_decls: List[class_t] = [] self.has_shared_ptr: bool = True self.is_abstract: bool = False # TODO: Consider removing unused attribute @@ -159,7 +161,7 @@ def add_cpp_header(self, class_full_name: str, class_short_name: str) -> None: def add_virtual_overrides( self, class_decl: class_t, short_class_name: str - ) -> list[member_function_t]: + ) -> List[member_function_t]: """ 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. @@ -176,8 +178,8 @@ def add_virtual_overrides( list[member_function_t]: A list of member functions needing override """ - methods_needing_override: list[member_function_t] = [] - return_types: list[str] = [] # e.g. ["void", "unsigned int", "::Bar<2> *"] + methods_needing_override: List[member_function_t] = [] + return_types: List[str] = [] # e.g. ["void", "unsigned int", "::Bar<2> *"] # Collect all virtual methods and their return types for member_function in class_decl.member_functions(allow_empty=True): @@ -289,7 +291,7 @@ def write(self, work_dir: str) -> None: continue # Find and define virtual function "trampoline" overrides - methods_needing_override: list[member_function_t] = ( + methods_needing_override: List[member_function_t] = ( self.add_virtual_overrides(class_decl, short_name) ) diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index fa640ae..f981565 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, Optional from pygccxml import declarations from pygccxml.declarations.class_declaration import class_t @@ -20,7 +20,7 @@ class CppConstructorWrapperWriter(CppBaseWrapperWriter): The pygccxml declaration object for the constructor class_decl : class_t The class declaration for the class containing the constructor - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code class_short_name : Optional[str] The short name of the class e.g. 'Foo2_2' @@ -31,7 +31,7 @@ def __init__( class_info: CppClassInfo, ctor_decl: constructor_t, class_decl: class_t, - wrapper_templates: dict[str, str], + wrapper_templates: Dict[str, str], class_short_name: Optional[str] = None, ): diff --git a/cppwg/writers/free_function_writer.py b/cppwg/writers/free_function_writer.py index 048c355..2a417b2 100644 --- a/cppwg/writers/free_function_writer.py +++ b/cppwg/writers/free_function_writer.py @@ -11,9 +11,9 @@ class CppFreeFunctionWrapperWriter(CppBaseWrapperWriter): ---------- free_function_info : CppFreeFunctionInfo The free function information to generate Python bindings for - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code - exclusion_args : list[str] + exclusion_args : List[str] A list of argument types to exclude from the wrapper code """ @@ -22,8 +22,8 @@ def __init__(self, free_function_info, wrapper_templates): super(CppFreeFunctionWrapperWriter, self).__init__(wrapper_templates) self.free_function_info: CppFreeFunctionInfo = free_function_info - self.wrapper_templates: dict[str, str] = wrapper_templates - self.exclusion_args: list[str] = [] + self.wrapper_templates: Dict[str, str] = wrapper_templates + self.exclusion_args: List[str] = [] def add_self(self, wrapper_string) -> str: """ diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index acfdf36..ed1234f 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -22,9 +22,9 @@ class CppHeaderCollectionWriter: The path to save the header collection file to hpp_collection_string : str The output string that gets written to the header collection file - class_dict : dict[str, CppClassInfo] + class_dict : Dict[str, CppClassInfo] A dictionary of all class info objects - free_func_dict : dict[str, CppFreeFunctionInfo] + free_func_dict : Dict[str, CppFreeFunctionInfo] A dictionary of all free function info objects """ @@ -41,8 +41,8 @@ def __init__( self.hpp_collection_string: str = "" # For convenience, collect all class and free function info into dicts keyed by name - self.class_dict: dict[str, CppClassInfo] = {} - self.free_func_dict: dict[str, CppFreeFunctionInfo] = {} + 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: diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 77abbe9..9cb3671 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, Optional from pygccxml import declarations from pygccxml.declarations.class_declaration import class_t @@ -20,7 +20,7 @@ class CppMethodWrapperWriter(CppBaseWrapperWriter): The pygccxml declaration object for the method class_decl : class_t The class declaration for the class containing the method - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code class_short_name : Optional[str] The short name of the class e.g. 'Foo2_2' @@ -31,7 +31,7 @@ def __init__( class_info: CppClassInfo, method_decl: member_function_t, class_decl: class_t, - wrapper_templates: dict[str, str], + wrapper_templates: Dict[str, str], class_short_name: Optional[str] = None, ): diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index aabe202..aa4ef34 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -1,6 +1,8 @@ import os import logging +from typing import Dict + from pygccxml.declarations.class_declaration import class_t from pygccxml.declarations.namespace import namespace_t @@ -23,13 +25,13 @@ class CppModuleWrapperWriter: The pygccxml namespace containing declarations from the source code module_info : ModuleInfo The module information to generate Python bindings for - wrapper_templates : dict[str, str] + wrapper_templates : Dict[str, str] String templates with placeholders for generating wrapper code wrapper_root : str The output directory for the generated wrapper code package_license : str The license to include in the generated wrapper code - exposed_class_full_names : list[str] + exposed_class_full_names : List[str] A list of full names of all classes to be wrapped in the module """ @@ -37,13 +39,13 @@ def __init__( self, source_ns: namespace_t, module_info: ModuleInfo, - wrapper_templates: dict[str, str], + wrapper_templates: Dict[str, str], wrapper_root: str, package_license: str = "", ): self.source_ns: namespace_t = source_ns self.module_info: ModuleInfo = module_info - self.wrapper_templates: dict[str, str] = wrapper_templates + self.wrapper_templates: Dict[str, str] = wrapper_templates self.wrapper_root: str = wrapper_root self.package_license: str = ( package_license # TODO: use this in the generated wrappers @@ -51,7 +53,7 @@ def __init__( # For convenience, create a list of all classes to be wrapped in the module # e.g. ['Foo', 'Bar<2>', 'Bar<3>'] - self.exposed_class_full_names: list[str] = [] + self.exposed_class_full_names: List[str] = [] for class_info in self.module_info.class_info_collection: for full_name in class_info.get_full_names(): diff --git a/tests/test_shapes.py b/tests/test_shapes.py index e070953..e142389 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -2,8 +2,10 @@ import subprocess import unittest +from typing import List -def get_file_lines(file_path: str) -> "list[str]": + +def get_file_lines(file_path: str) -> List[str]: """ Load a file into a list of lines @@ -14,7 +16,7 @@ def get_file_lines(file_path: str) -> "list[str]": Returns ------- - list[str] + List[str] A list of lines read from the file, with excess whitespace and empty lines removed """ @@ -52,7 +54,7 @@ def compare_files(file_path_a: str, file_path_b: str) -> bool: class TestShapes(unittest.TestCase): - def test_wrapper_generation(self): + def test_wrapper_generation(self) -> None: """ Generate wrappers and compare with the reference wrappers. """ From 394312d14247b448bbcafc5f039818e66316995b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 18 Mar 2024 15:53:50 +0000 Subject: [PATCH 53/53] #11 fix free_function_info_collection --- cppwg/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index f29ed5a..488a562 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -277,7 +277,7 @@ def update_free_function_info(self) -> None: function_info = CppFreeFunctionInfo(free_function.name) function_info.module_info = module_info function_info.decl = free_function - module_info.free_function_info.append(function_info) + module_info.free_function_info_collection.append(function_info) else: # As module_info.use_all_free_functions == False, free function