From 3048323f235a6d9879c256d1338b1d92bf25cdd2 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 21 Sep 2024 13:57:52 +0000 Subject: [PATCH 01/16] #12 Sort classes in inheritance order --- .flake8 | 1 + cppwg/generators.py | 3 +++ cppwg/input/module_info.py | 27 +++++++++++++++++++ examples/shapes/wrapper/package_info.yaml | 4 +-- .../wrapper/wrapper_header_collection.hpp | 8 +++--- pyproject.toml | 4 +-- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index a298ec3..21f7699 100644 --- a/.flake8 +++ b/.flake8 @@ -8,4 +8,5 @@ exclude= examples, cppwg/templates, tests, + venv, docstring-convention=numpy diff --git a/cppwg/generators.py b/cppwg/generators.py index 72c3044..f1b4191 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -366,6 +366,9 @@ def add_class_decls(self) -> None: class_info.decls.append(class_decl) + # Sort the class info collection in inheritance order + module_info.sort_classes() + def add_discovered_free_functions(self) -> None: """ Add discovered free function. diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index ac2c792..f21831f 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,6 +1,7 @@ """Module information structure.""" import os +from functools import cmp_to_key from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -75,3 +76,29 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 return True return False + + def sort_classes(self) -> None: + """Sort the class info collection in inheritance order.""" + + def compare(class_info_0, class_info_1): + if class_info_0.decls == class_info_1.decls: + return 0 + if class_info_0.decls is None: + return 1 + if class_info_1.decls is None: + return -1 + + bases_0 = [ + base.related_class for decl in class_info_0.decls for base in decl.bases + ] + bases_1 = [ + base.related_class for decl in class_info_1.decls for base in decl.bases + ] + + child_0 = int(any(base in class_info_1.decls for base in bases_0)) + child_1 = int(any(base in class_info_0.decls for base in bases_1)) + + return child_0 - child_1 + + self.class_info_collection.sort(key=lambda x: x.name) + self.class_info_collection.sort(key=cmp_to_key(compare)) diff --git a/examples/shapes/wrapper/package_info.yaml b/examples/shapes/wrapper/package_info.yaml index 0a10610..e41e0c2 100644 --- a/examples/shapes/wrapper/package_info.yaml +++ b/examples/shapes/wrapper/package_info.yaml @@ -67,17 +67,17 @@ modules: - name: primitives source_locations: classes: - - name: Shape - name: Cuboid - name: Rectangle + - name: Shape - name: Triangle excluded: True # Exclude this class from wrapping. - name: mesh source_locations: classes: - - name: AbstractMesh - name: ConcreteMesh + - name: AbstractMesh # Text to add at the top of all wrappers prefix_text: | diff --git a/examples/shapes/wrapper/wrapper_header_collection.hpp b/examples/shapes/wrapper/wrapper_header_collection.hpp index ca26ea3..6bfa1af 100644 --- a/examples/shapes/wrapper/wrapper_header_collection.hpp +++ b/examples/shapes/wrapper/wrapper_header_collection.hpp @@ -21,10 +21,10 @@ template class Point<2>; template class Point<3>; template class Shape<2>; template class Shape<3>; -template class AbstractMesh<2,2>; -template class AbstractMesh<3,3>; template class ConcreteMesh<2>; template class ConcreteMesh<3>; +template class AbstractMesh<2,2>; +template class AbstractMesh<3,3>; // Typedefs for nicer naming namespace cppwg @@ -33,10 +33,10 @@ typedef Point<2> Point2; typedef Point<3> Point3; typedef Shape<2> Shape2; typedef Shape<3> Shape3; -typedef AbstractMesh<2,2> AbstractMesh2_2; -typedef AbstractMesh<3,3> AbstractMesh3_3; typedef ConcreteMesh<2> ConcreteMesh2; typedef ConcreteMesh<3> ConcreteMesh3; +typedef AbstractMesh<2,2> AbstractMesh2_2; +typedef AbstractMesh<3,3> AbstractMesh3_3; } // namespace cppwg #endif // pyshapes_HEADERS_HPP_ diff --git a/pyproject.toml b/pyproject.toml index 6856659..f8e2429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,10 @@ authors = [ license = { file = "LICENSE" } keywords = ["C++", "Python", "Pybind11"] readme = "README.md" -version = "0.0.1-alpha" +version = "0.2.1" classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", From 847912ca98c33c941517f3b8e864c87264628b7b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 21 Sep 2024 19:57:00 +0000 Subject: [PATCH 02/16] #12 Document class sorting --- cppwg/input/module_info.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index f21831f..03bc3eb 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -80,7 +80,8 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 def sort_classes(self) -> None: """Sort the class info collection in inheritance order.""" - def compare(class_info_0, class_info_1): + def compare(class_info_0: "ClassInfo", class_info_1: "ClassInfo"): + # Sort classes with no declarations to the bottom if class_info_0.decls == class_info_1.decls: return 0 if class_info_0.decls is None: @@ -88,6 +89,7 @@ def compare(class_info_0, class_info_1): if class_info_1.decls is None: return -1 + # Get the base classes for each class bases_0 = [ base.related_class for decl in class_info_0.decls for base in decl.bases ] @@ -95,7 +97,10 @@ def compare(class_info_0, class_info_1): base.related_class for decl in class_info_1.decls for base in decl.bases ] + # 1 if class_0 is a child of class_1 child_0 = int(any(base in class_info_1.decls for base in bases_0)) + + # 1 if class_1 is a child of class 0 child_1 = int(any(base in class_info_0.decls for base in bases_1)) return child_0 - child_1 From 1ee9a5b480eff2c2c439a909013911b297fdae8e Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 21 Sep 2024 20:05:03 +0000 Subject: [PATCH 03/16] #12 fix linting --- cppwg/input/module_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 03bc3eb..d658bb0 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -80,7 +80,7 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 def sort_classes(self) -> None: """Sort the class info collection in inheritance order.""" - def compare(class_info_0: "ClassInfo", class_info_1: "ClassInfo"): + def compare(class_info_0: "ClassInfo", class_info_1: "ClassInfo"): # noqa: F821 # Sort classes with no declarations to the bottom if class_info_0.decls == class_info_1.decls: return 0 From dfa7a166daaa0e9066e027f5dc00323c92855d47 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sat, 21 Sep 2024 23:04:53 +0000 Subject: [PATCH 04/16] #12 Refactor add_class_decls --- cppwg/generators.py | 66 +++++------------------------------ cppwg/input/class_info.py | 71 ++++++++++++++++++++++++++++++++++++++ cppwg/input/module_info.py | 31 +++++++++++------ 3 files changed, 100 insertions(+), 68 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index f1b4191..1b3305b 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -10,7 +10,6 @@ from typing import List, Optional import pygccxml -from pygccxml.declarations.runtime_errors import declaration_not_found_t from cppwg.input.class_info import CppClassInfo from cppwg.input.free_function_info import CppFreeFunctionInfo @@ -176,6 +175,8 @@ def collect_source_hpp_files(self) -> None: patterns e.g. "*.hpp". Skip the wrapper root and wrappers to avoid pollution. """ + logger = logging.getLogger() + 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): @@ -194,7 +195,7 @@ def collect_source_hpp_files(self) -> None: # Check if any source files were found if not self.package_info.source_hpp_files: - logging.error(f"No header files found in source root: {self.source_root}") + logger.error(f"No header files found in source root: {self.source_root}") raise FileNotFoundError() def extract_templates_from_source(self) -> None: @@ -210,6 +211,8 @@ def extract_templates_from_source(self) -> None: def log_unknown_classes(self) -> None: """Get unwrapped classes.""" + logger = logging.getLogger() + all_class_decls = self.source_ns.classes(allow_empty=True) seen_class_names = set() @@ -226,7 +229,7 @@ def log_unknown_classes(self) -> None: ): seen_class_names.add(decl.name) seen_class_names.add(decl.name.split("<")[0].strip()) - logging.info( + logger.info( f"Unknown class {decl.name} from {decl.location.file_name}:{decl.location.line}" ) @@ -238,7 +241,7 @@ def log_unknown_classes(self) -> None: for _, class_name, _ in class_list: if class_name not in seen_class_names: seen_class_names.add(class_name) - logging.info(f"Unknown class {class_name} from {hpp_file_path}") + logger.info(f"Unknown class {class_name} from {hpp_file_path}") def map_classes_to_hpp_files(self) -> None: """ @@ -314,60 +317,7 @@ def add_class_decls(self) -> None: declarations found by pygccxml in the C++ source code. """ for module_info in self.package_info.module_info_collection: - for class_info in module_info.class_info_collection: - # Skip excluded classes - if class_info.excluded: - continue - - class_info.decls: List["class_t"] = [] # noqa: F821 - - for class_cpp_name in class_info.cpp_names: - decl_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> - - try: - class_decl = self.source_ns.class_(decl_name) - - except declaration_not_found_t as e1: - if ( - class_info.template_signature is None - or "=" not in class_info.template_signature - ): - logging.error( - f"Could not find declaration for class {decl_name}" - ) - raise e1 - - # If class has default args, try to compress the template signature - logging.warning( - f"Could not find declaration for class {decl_name}: trying for a partial match." - ) - - # Try to find the class without default template args - # e.g. for template class Foo {}; - # Look for Foo<2> instead of Foo<2,2> - pos = 0 - for i, s in enumerate(class_info.template_signature.split(",")): - if "=" in s: - pos = i - break - - decl_name = ",".join(decl_name.split(",")[0:pos]) + " >" - - try: - class_decl = self.source_ns.class_(decl_name) - - except declaration_not_found_t as e2: - logging.error( - f"Could not find declaration for class {decl_name}" - ) - raise e2 - - logging.info(f"Found {decl_name}") - - class_info.decls.append(class_decl) - - # Sort the class info collection in inheritance order - module_info.sort_classes() + module_info.update_from_ns(self.source_ns) def add_discovered_free_functions(self) -> None: """ diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index af4c566..ffd686f 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,7 +1,10 @@ """Class information structure.""" +import logging from typing import Any, Dict, List, Optional +from pygccxml.declarations.runtime_errors import declaration_not_found_t + from cppwg.input.cpp_type_info import CppTypeInfo @@ -15,6 +18,8 @@ class CppClassInfo(CppTypeInfo): The C++ names of the class e.g. ["Foo<2,2>", "Foo<3,3>"] py_names : List[str] The Python names of the class e.g. ["Foo2_2", "Foo3_3"] + decls : pygccxml.declarations.declaration_t + Declarations for this type's base class, one per template instantiation """ def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): @@ -23,6 +28,72 @@ def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): self.cpp_names: List[str] = None self.py_names: List[str] = None + self.base_decls: Optional[List["declaration_t"]] = None # noqa: F821 + + def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 + """ + Update the class information from the source namespace. + + Adds the class declarations and base class declarations. + + Parameters + ---------- + ns : pygccxml.declarations.namespace_t + The source namespace + """ + logger = logging.getLogger() + + # Skip excluded classes + if self.excluded: + return + + self.decls = [] + + for class_cpp_name in self.cpp_names: + decl_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> + + try: + class_decl = ns.class_(decl_name) + + except declaration_not_found_t as e1: + if ( + self.template_signature is None + or "=" not in self.template_signature + ): + logger.error(f"Could not find declaration for class {decl_name}") + raise e1 + + # If class has default args, try to compress the template signature + logger.warning( + f"Could not find declaration for class {decl_name}: trying for a partial match." + ) + + # Try to find the class without default template args + # e.g. for template class Foo {}; + # Look for Foo<2> instead of Foo<2,2> + pos = 0 + for i, s in enumerate(self.template_signature.split(",")): + if "=" in s: + pos = i + break + + decl_name = ",".join(decl_name.split(",")[0:pos]) + " >" + + try: + class_decl = ns.class_(decl_name) + + except declaration_not_found_t as e2: + logger.error(f"Could not find declaration for class {decl_name}") + raise e2 + + logger.info(f"Found {decl_name}") + + self.decls.append(class_decl) + + # Update the base class declarations + self.base_decls = [ + base.related_class for decl in self.decls for base in decl.bases + ] def update_py_names(self) -> None: """ diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index d658bb0..0fe9edd 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -89,21 +89,32 @@ def compare(class_info_0: "ClassInfo", class_info_1: "ClassInfo"): # noqa: F821 if class_info_1.decls is None: return -1 - # Get the base classes for each class - bases_0 = [ - base.related_class for decl in class_info_0.decls for base in decl.bases - ] - bases_1 = [ - base.related_class for decl in class_info_1.decls for base in decl.bases - ] - # 1 if class_0 is a child of class_1 - child_0 = int(any(base in class_info_1.decls for base in bases_0)) + child_0 = int( + any(base in class_info_1.decls for base in class_info_0.base_decls) + ) # 1 if class_1 is a child of class 0 - child_1 = int(any(base in class_info_0.decls for base in bases_1)) + child_1 = int( + any(base in class_info_0.decls for base in class_info_1.base_decls) + ) return child_0 - child_1 self.class_info_collection.sort(key=lambda x: x.name) self.class_info_collection.sort(key=cmp_to_key(compare)) + + def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 + """ + Update class info objects with information from the source namespace. + + Parameters + ---------- + ns : pygccxml.declarations.namespace_t + The source namespace + """ + for class_info in self.class_info_collection: + class_info.update_from_ns(ns) + + # Sort the class info collection in inheritance order + self.sort_classes() From 7cfce4b4d079a1f6b6f3f182ce59efd833d71b76 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 00:38:29 +0000 Subject: [PATCH 05/16] #12 Fix class sorting --- cppwg/input/class_info.py | 22 +++++++- cppwg/input/module_info.py | 51 +++++++++++-------- .../wrapper/primitives/primitives.main.cpp | 4 +- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index ffd686f..01651f9 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -30,9 +30,29 @@ def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): self.py_names: List[str] = None self.base_decls: Optional[List["declaration_t"]] = None # noqa: F821 + def is_child_of(self, other: "ClassInfo") -> bool: # noqa: F821 + """ + Check if the class is a child of the specified class. + + Parameters + ---------- + other : ClassInfo + The other class to check + + Returns + ------- + bool + True if the class is a child of the specified class, False otherwise + """ + if not self.base_decls: + return False + if not other.decls: + return False + return any(base in other.decls for base in self.base_decls) + def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 """ - Update the class information from the source namespace. + Update class with information from the source namespace. Adds the class declarations and base class declarations. diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 0fe9edd..5ab5a85 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,7 +1,6 @@ """Module information structure.""" import os -from functools import cmp_to_key from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -79,34 +78,44 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 def sort_classes(self) -> None: """Sort the class info collection in inheritance order.""" + self.class_info_collection.sort(key=lambda x: x.name) - def compare(class_info_0: "ClassInfo", class_info_1: "ClassInfo"): # noqa: F821 - # Sort classes with no declarations to the bottom - if class_info_0.decls == class_info_1.decls: - return 0 - if class_info_0.decls is None: - return 1 - if class_info_1.decls is None: - return -1 + order_changed = True + while order_changed: + order_changed = False - # 1 if class_0 is a child of class_1 - child_0 = int( - any(base in class_info_1.decls for base in class_info_0.base_decls) - ) + i = 0 + n = len(self.class_info_collection) + while i < n - 1: + cls_i = self.class_info_collection[i] + ii = i # destination of cls_i + child_pos = [] # positions of cls_i's children - # 1 if class_1 is a child of class 0 - child_1 = int( - any(base in class_info_0.decls for base in class_info_1.base_decls) - ) + for j in range(i + 1, n): + cls_j = self.class_info_collection[j] + if cls_i.is_child_of(cls_j): + ii = j + elif cls_j.is_child_of(cls_i): + child_pos.append(j) - return child_0 - child_1 + if ii <= i: + i += 1 + continue # no change in cls_i's position - self.class_info_collection.sort(key=lambda x: x.name) - self.class_info_collection.sort(key=cmp_to_key(compare)) + cls_i = self.class_info_collection.pop(i) + self.class_info_collection.insert(ii, cls_i) + + for j, idx in enumerate(child_pos): + if j > ii: + break # children already positioned after cls_i + cls_j = self.class_info_collection.pop(j - 1 - idx) + self.class_info_collection.insert(ii + idx, cls_j) + + order_changed = True def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 """ - Update class info objects with information from the source namespace. + Update module with information from the source namespace. Parameters ---------- diff --git a/examples/shapes/wrapper/primitives/primitives.main.cpp b/examples/shapes/wrapper/primitives/primitives.main.cpp index f90aac5..b691509 100644 --- a/examples/shapes/wrapper/primitives/primitives.main.cpp +++ b/examples/shapes/wrapper/primitives/primitives.main.cpp @@ -5,8 +5,8 @@ #include "wrapper_header_collection.hpp" #include "Shape2.cppwg.hpp" #include "Shape3.cppwg.hpp" -#include "Cuboid.cppwg.hpp" #include "Rectangle.cppwg.hpp" +#include "Cuboid.cppwg.hpp" namespace py = pybind11; @@ -14,6 +14,6 @@ PYBIND11_MODULE(_pyshapes_primitives, m) { register_Shape2_class(m); register_Shape3_class(m); - register_Cuboid_class(m); register_Rectangle_class(m); + register_Cuboid_class(m); } From bbb7184d4f089a8d59d47e85de8eb41d503e3aa6 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 10:17:34 +0000 Subject: [PATCH 06/16] #12 Sort classes by dependence --- cppwg/input/class_info.py | 54 ++++++++++++++++++++++++----- cppwg/input/module_info.py | 6 +++- cppwg/writers/class_writer.py | 9 ++--- cppwg/writers/constructor_writer.py | 13 +++---- cppwg/writers/method_writer.py | 8 ++--- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 01651f9..deed989 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,8 +1,10 @@ """Class information structure.""" import logging +import re from typing import Any, Dict, List, Optional +from pygccxml.declarations.matchers import access_type_matcher_t from pygccxml.declarations.runtime_errors import declaration_not_found_t from cppwg.input.cpp_type_info import CppTypeInfo @@ -48,7 +50,41 @@ def is_child_of(self, other: "ClassInfo") -> bool: # noqa: F821 return False if not other.decls: return False - return any(base in other.decls for base in self.base_decls) + return any(decl in other.decls for decl in self.base_decls) + + def requires(self, other: "ClassInfo") -> bool: # noqa: F821 + """ + Check if the specified class is used in method signatures of this class. + + Parameters + ---------- + other : ClassInfo + The specified class to check. + + Returns + ------- + bool + True if the specified class is used in method signatures of this class. + """ + if not self.decls: + return False + + query = access_type_matcher_t("public") + name_regex = re.compile(r"\b" + re.escape(other.name) + r"\b") + + for class_decl in self.decls: + method_decls = class_decl.member_functions(function=query, allow_empty=True) + for method_decl in method_decls: + for arg_type in method_decl.argument_types: + if name_regex.search(arg_type.decl_string): + return True + + ctor_decls = class_decl.constructors(function=query, allow_empty=True) + for ctor_decl in ctor_decls: + for arg_type in ctor_decl.argument_types: + if name_regex.search(arg_type.decl_string): + return True + return False def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 """ @@ -70,22 +106,22 @@ def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 self.decls = [] for class_cpp_name in self.cpp_names: - decl_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> + class_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> try: - class_decl = ns.class_(decl_name) + class_decl = ns.class_(class_name) except declaration_not_found_t as e1: if ( self.template_signature is None or "=" not in self.template_signature ): - logger.error(f"Could not find declaration for class {decl_name}") + logger.error(f"Could not find declaration for class {class_name}") raise e1 # If class has default args, try to compress the template signature logger.warning( - f"Could not find declaration for class {decl_name}: trying for a partial match." + f"Could not find declaration for class {class_name}: trying for a partial match." ) # Try to find the class without default template args @@ -97,16 +133,16 @@ def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 pos = i break - decl_name = ",".join(decl_name.split(",")[0:pos]) + " >" + class_name = ",".join(class_name.split(",")[0:pos]) + " >" try: - class_decl = ns.class_(decl_name) + class_decl = ns.class_(class_name) except declaration_not_found_t as e2: - logger.error(f"Could not find declaration for class {decl_name}") + logger.error(f"Could not find declaration for class {class_name}") raise e2 - logger.info(f"Found {decl_name}") + logger.info(f"Found {class_name}") self.decls.append(class_decl) diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 5ab5a85..b5509cd 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -77,7 +77,7 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 return False def sort_classes(self) -> None: - """Sort the class info collection in inheritance order.""" + """Sort the class info collection in order of dependence.""" self.class_info_collection.sort(key=lambda x: x.name) order_changed = True @@ -94,6 +94,10 @@ def sort_classes(self) -> None: for j in range(i + 1, n): cls_j = self.class_info_collection[j] if cls_i.is_child_of(cls_j): + # sort by inheritance + ii = j + elif cls_i.requires(cls_j) and not cls_j.requires(cls_i): + # sort by dependence, ignoring forward declaration cycles ii = j elif cls_j.is_child_of(cls_i): child_pos.append(j) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index cf9d22b..d40d561 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -4,7 +4,8 @@ import os from typing import Dict, List -from pygccxml import declarations +from pygccxml.declarations import type_traits_classes +from pygccxml.declarations.matchers import access_type_matcher_t from cppwg.utils.constants import ( CPPWG_CLASS_OVERRIDE_SUFFIX, @@ -258,7 +259,7 @@ def write(self, work_dir: str) -> None: # struct Foo{ # enum Value{A, B, C}; # }; - if declarations.is_struct(class_decl): + if type_traits_classes.is_struct(class_decl): enums = class_decl.enumerations(allow_empty=True) if len(enums) == 1: @@ -326,7 +327,7 @@ def write(self, work_dir: str) -> None: self.cpp_string += class_definition_template.format(**class_definition_dict) # Add public constructors - query = declarations.access_type_matcher_t("public") + query = access_type_matcher_t("public") for constructor in class_decl.constructors( function=query, allow_empty=True ): @@ -339,7 +340,7 @@ def write(self, work_dir: str) -> None: self.cpp_string += constructor_writer.generate_wrapper() # Add public member functions - query = declarations.access_type_matcher_t("public") + query = access_type_matcher_t("public") for member_function in class_decl.member_functions( function=query, allow_empty=True ): diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index d8b4281..f57601a 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -3,7 +3,7 @@ import re from typing import Dict -from pygccxml import declarations +from pygccxml.declarations import type_traits, type_traits_classes from cppwg.writers.base_writer import CppBaseWrapperWriter @@ -87,9 +87,9 @@ def exclude(self) -> bool: if self.ctor_decl.parent != self.class_decl: return True - # Exclude default copy constructors e.g. Foo::Foo(Foo const & foo) + # Exclude compiler-added copy constructors e.g. Foo::Foo(Foo const & foo) if ( - declarations.is_copy_constructor(self.ctor_decl) + type_traits_classes.is_copy_constructor(self.ctor_decl) and self.ctor_decl.is_artificial ): return True @@ -200,11 +200,8 @@ def generate_wrapper(self) -> str: # `Foo(std::vector laminas = std::vector{})` # which generates `py::arg("laminas") = std::vector{}` if default_value.replace(" ", "") == "{}": - default_value = arg.decl_type.decl_string + " {}" - - # Remove const keyword - default_value = re.sub(r"\bconst\b", "", default_value) - default_value = default_value.replace(" ", " ") + decl_type = type_traits.remove_const(arg.decl_type) + default_value = decl_type.decl_string + " {}" keyword_args += f" = {default_value}" diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 6309c2b..2f7e87f 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -3,7 +3,7 @@ import re from typing import Dict -from pygccxml import declarations +from pygccxml.declarations import type_traits from cppwg.writers.base_writer import CppBaseWrapperWriter @@ -129,7 +129,7 @@ def generate_wrapper(self) -> str: # Pybind11 def type e.g. "_static" for def_static() def_adorn = "" if self.method_decl.has_static: - def_adorn += "_static" + def_adorn = "_static" # How to point to class if self.method_decl.has_static: @@ -178,12 +178,12 @@ def generate_wrapper(self) -> str: # Call policy, e.g. "py::return_value_policy::reference" call_policy = "" - if declarations.is_pointer(self.method_decl.return_type): + if type_traits.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): + elif type_traits.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}" From 24d3b50b95c5e0a4417ceaf1e036519457300977 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 12:17:03 +0000 Subject: [PATCH 07/16] #12 Simplify update_from_ns --- cppwg/generators.py | 75 ++------------------------- cppwg/input/class_info.py | 10 ++-- cppwg/input/cpp_type_info.py | 2 +- cppwg/input/free_function_info.py | 15 +++++- cppwg/input/method_info.py | 2 +- cppwg/input/module_info.py | 40 ++++++++++++-- cppwg/input/package_info.py | 2 +- cppwg/input/variable_info.py | 2 +- cppwg/writers/class_writer.py | 2 +- cppwg/writers/constructor_writer.py | 2 +- cppwg/writers/free_function_writer.py | 2 +- cppwg/writers/method_writer.py | 2 +- 12 files changed, 66 insertions(+), 90 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 1b3305b..7ce416a 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -11,8 +11,6 @@ import pygccxml -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.parsers.package_info_parser import PackageInfoParser @@ -289,67 +287,11 @@ def parse_package_info(self) -> None: # If no package info file exists, create a PackageInfo object with default settings self.package_info = PackageInfo("cppwg_package", self.source_root) - def add_discovered_classes(self) -> None: - """ - Add discovered classes. - - Add class info objects for classes discovered by pygccxml from - parsing the C++ source code. This is run for modules which set - `use_all_classes` to True. No class info objects were created for - those modules while parsing the package info yaml file. - """ - 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.update_names() - class_info.module_info = module_info - module_info.class_info_collection.append(class_info) - - def add_class_decls(self) -> None: - """ - Add declarations to class info objects. - - Update all class info objects with their corresponding - declarations found by pygccxml in the C++ source code. - """ + def update_from_ns(self) -> None: + """Update modules with information from the source namespace.""" for module_info in self.package_info.module_info_collection: module_info.update_from_ns(self.source_ns) - def add_discovered_free_functions(self) -> None: - """ - Add discovered free function. - - Add free function info objects discovered by pygccxml from - parsing the C++ source code. This is run for modules which set - `use_all_free_functions` to True. No free function info objects were - created for those modules while parsing the package info yaml file. - """ - 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 free_function in free_functions: - if module_info.is_decl_in_source_path(free_function): - ff_info = CppFreeFunctionInfo(free_function.name) - ff_info.module_info = module_info - module_info.free_function_info_collection.append(ff_info) - - def add_free_function_decls(self) -> None: - """ - Add declarations to free function info objects. - - Update all free function info objects with their corresponding - declarations found by pygccxml in the C++ source code. - """ - for module_info in self.package_info.module_info_collection: - for ff_info in module_info.free_function_info_collection: - decls = self.source_ns.free_functions(ff_info.name, allow_empty=True) - ff_info.decls = [decls[0]] - def write_header_collection(self) -> None: """Write the header collection to file.""" header_collection_writer = CppHeaderCollectionWriter( @@ -389,17 +331,8 @@ def generate_wrapper(self) -> None: # Parse the headers with pygccxml and castxml self.parse_header_collection() - # Add discovered classes from the parsed code - self.add_discovered_classes() - - # Add declarations to class info objects - self.add_class_decls() - - # Add discovered free functions from the parsed code - self.add_discovered_free_functions() - - # Add declarations to free function info objects - self.add_free_function_decls() + # Update modules with information from the source namespace + self.update_from_ns() # Log list of unknown classes in the source root self.log_unknown_classes() diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index deed989..358a1f8 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -26,7 +26,7 @@ class CppClassInfo(CppTypeInfo): def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): - super(CppClassInfo, self).__init__(name, class_config) + super().__init__(name, class_config) self.cpp_names: List[str] = None self.py_names: List[str] = None @@ -86,7 +86,7 @@ def requires(self, other: "ClassInfo") -> bool: # noqa: F821 return True return False - def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 + def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ Update class with information from the source namespace. @@ -94,7 +94,7 @@ def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 Parameters ---------- - ns : pygccxml.declarations.namespace_t + source_ns : pygccxml.declarations.namespace_t The source namespace """ logger = logging.getLogger() @@ -109,7 +109,7 @@ def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 class_name = class_cpp_name.replace(" ", "") # e.g. Foo<2,2> try: - class_decl = ns.class_(class_name) + class_decl = source_ns.class_(class_name) except declaration_not_found_t as e1: if ( @@ -136,7 +136,7 @@ def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 class_name = ",".join(class_name.split(",")[0:pos]) + " >" try: - class_decl = ns.class_(class_name) + class_decl = source_ns.class_(class_name) except declaration_not_found_t as e2: logger.error(f"Could not find declaration for class {class_name}") diff --git a/cppwg/input/cpp_type_info.py b/cppwg/input/cpp_type_info.py index 3260d85..1c89538 100644 --- a/cppwg/input/cpp_type_info.py +++ b/cppwg/input/cpp_type_info.py @@ -31,7 +31,7 @@ class CppTypeInfo(BaseInfo): def __init__(self, name: str, type_config: Optional[Dict[str, Any]] = None): - super(CppTypeInfo, self).__init__(name) + super().__init__(name) self.module_info: Optional["ModuleInfo"] = None # noqa: F821 self.source_file_full_path: Optional[str] = None diff --git a/cppwg/input/free_function_info.py b/cppwg/input/free_function_info.py index 9879011..89e3e72 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/input/free_function_info.py @@ -12,9 +12,22 @@ def __init__( self, name: str, free_function_config: Optional[Dict[str, Any]] = None ): - super(CppFreeFunctionInfo, self).__init__(name, free_function_config) + super().__init__(name, free_function_config) @property def parent(self) -> "ModuleInfo": # noqa: F821 """Returns the parent module info object.""" return self.module_info + + def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 + """ + Update with information from the source namespace. + + Adds the free function declaration. + + Parameters + ---------- + source_ns : pygccxml.declarations.namespace_t + The source namespace + """ + self.decls = source_ns.free_functions(self.name, allow_empty=True)[0] diff --git a/cppwg/input/method_info.py b/cppwg/input/method_info.py index 49e6109..2077d3b 100644 --- a/cppwg/input/method_info.py +++ b/cppwg/input/method_info.py @@ -17,7 +17,7 @@ class CppMethodInfo(CppTypeInfo): def __init__(self, name: str, _): - super(CppMethodInfo, self).__init__(name) + super().__init__(name) self.class_info: Optional["CppClassInfo"] = None # noqa: F821 diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index b5509cd..f1f0195 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -4,6 +4,8 @@ from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo +from cppwg.input.class_info import CppClassInfo +from cppwg.input.free_function_info import CppFreeFunctionInfo class ModuleInfo(BaseInfo): @@ -32,7 +34,7 @@ class ModuleInfo(BaseInfo): def __init__(self, name: str, module_config: Optional[Dict[str, Any]] = None): - super(ModuleInfo, self).__init__(name) + super().__init__(name) self.package_info: Optional["PackageInfo"] = None # noqa: F821 self.source_locations: List[str] = None @@ -117,17 +119,45 @@ def sort_classes(self) -> None: order_changed = True - def update_from_ns(self, ns: "namespace_t") -> None: # noqa: F821 + def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ Update module with information from the source namespace. Parameters ---------- - ns : pygccxml.declarations.namespace_t + source_ns : pygccxml.declarations.namespace_t The source namespace """ + # Add discovered classes: if `use_all_classes` is True, this module + # has no class info objects. Use class declarations from the + # source namespace to create class info objects. + if self.use_all_classes: + class_decls = source_ns.classes(allow_empty=True) + for class_decl in class_decls: + if self.is_decl_in_source_path(class_decl): + class_info = CppClassInfo(class_decl.name) + class_info.update_names() + class_info.module_info = self + self.class_info_collection.append(class_info) + + # Update classes with information from source namespace. for class_info in self.class_info_collection: - class_info.update_from_ns(ns) + class_info.update_from_ns(source_ns) - # Sort the class info collection in inheritance order + # Sort classes by dependence self.sort_classes() + + # Add discovered free functions: if `use_all_free_functions` is True, + # this module has no free function info objects. Use free function + # decls from the source namespace to create free function info objects. + if self.use_all_free_functions: + free_functions = source_ns.free_functions(allow_empty=True) + for free_function in free_functions: + if self.is_decl_in_source_path(free_function): + ff_info = CppFreeFunctionInfo(free_function.name) + ff_info.module_info = self + self.free_function_info_collection.append(ff_info) + + # Update free functions with information from source namespace. + for ff_info in self.free_function_info_collection: + ff_info.update_from_ns(source_ns) diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index fdbe78c..82ad62d 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -50,7 +50,7 @@ def __init__( package_config : Dict[str, Any] A dictionary of package configuration settings """ - super(PackageInfo, self).__init__(name) + super().__init__(name) self.name: str = name self.source_locations: List[str] = None diff --git a/cppwg/input/variable_info.py b/cppwg/input/variable_info.py index 3cfd792..541c246 100644 --- a/cppwg/input/variable_info.py +++ b/cppwg/input/variable_info.py @@ -10,4 +10,4 @@ class CppVariableInfo(CppTypeInfo): def __init__(self, name: str, variable_config: Optional[Dict[str, Any]] = None): - super(CppVariableInfo, self).__init__(name, variable_config) + super().__init__(name, variable_config) diff --git a/cppwg/writers/class_writer.py b/cppwg/writers/class_writer.py index d40d561..db69ea1 100644 --- a/cppwg/writers/class_writer.py +++ b/cppwg/writers/class_writer.py @@ -45,7 +45,7 @@ def __init__( ) -> None: logger = logging.getLogger() - super(CppClassWrapperWriter, self).__init__(wrapper_templates) + super().__init__(wrapper_templates) self.class_info: "CppClassInfo" = class_info # noqa: F821 diff --git a/cppwg/writers/constructor_writer.py b/cppwg/writers/constructor_writer.py index f57601a..5994420 100644 --- a/cppwg/writers/constructor_writer.py +++ b/cppwg/writers/constructor_writer.py @@ -40,7 +40,7 @@ def __init__( wrapper_templates: Dict[str, str], ) -> None: - super(CppConstructorWrapperWriter, self).__init__(wrapper_templates) + super().__init__(wrapper_templates) self.class_info: "CppClassInfo" = class_info # noqa: F821 self.ctor_decl: "constructor_t" = ctor_decl # noqa: F821 diff --git a/cppwg/writers/free_function_writer.py b/cppwg/writers/free_function_writer.py index c3f29ac..2c87ef4 100644 --- a/cppwg/writers/free_function_writer.py +++ b/cppwg/writers/free_function_writer.py @@ -22,7 +22,7 @@ class CppFreeFunctionWrapperWriter(CppBaseWrapperWriter): def __init__(self, free_function_info, wrapper_templates) -> None: - super(CppFreeFunctionWrapperWriter, self).__init__(wrapper_templates) + super().__init__(wrapper_templates) self.free_function_info: CppFreeFunctionInfo = free_function_info self.wrapper_templates: Dict[str, str] = wrapper_templates diff --git a/cppwg/writers/method_writer.py b/cppwg/writers/method_writer.py index 2f7e87f..29ce8a7 100644 --- a/cppwg/writers/method_writer.py +++ b/cppwg/writers/method_writer.py @@ -40,7 +40,7 @@ def __init__( wrapper_templates: Dict[str, str], ) -> None: - super(CppMethodWrapperWriter, self).__init__(wrapper_templates) + super().__init__(wrapper_templates) self.class_info: "CppClassInfo" = class_info # noqa: F821 self.method_decl: "member_function_t" = method_decl # noqa: F821 From a363ad2d95c3bd3b065533b7a0c0017b02f400bc Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 13:26:35 +0000 Subject: [PATCH 08/16] #12 Remove info_helper --- .flake8 | 8 ++- cppwg/generators.py | 49 +++++++------ cppwg/input/class_info.py | 83 ++++++++++++++++++++- cppwg/input/free_function_info.py | 3 +- cppwg/input/info_helper.py | 115 ------------------------------ cppwg/input/module_info.py | 8 +++ 6 files changed, 126 insertions(+), 140 deletions(-) delete mode 100644 cppwg/input/info_helper.py diff --git a/.flake8 b/.flake8 index 21f7699..95b9d39 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,8 @@ [flake8] max_line_length = 120 -exclude= + +exclude = + __pycache__, .git, .github, build, @@ -9,4 +11,8 @@ exclude= cppwg/templates, tests, venv, + docstring-convention=numpy + +# D200 One-line docstring should fit on one line with quotes +extend-ignore = D200 diff --git a/cppwg/generators.py b/cppwg/generators.py index 7ce416a..0191df3 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -11,7 +11,6 @@ import pygccxml -from cppwg.input.info_helper import CppInfoHelper from cppwg.input.package_info import PackageInfo from cppwg.parsers.package_info_parser import PackageInfoParser from cppwg.parsers.source_parser import CppSourceParser @@ -196,19 +195,10 @@ def collect_source_hpp_files(self) -> None: logger.error(f"No header files found in source root: {self.source_root}") raise FileNotFoundError() - def extract_templates_from_source(self) -> None: - """Extract template arguments for each class from the associated source file.""" - for module_info in self.package_info.module_info_collection: - info_helper = CppInfoHelper(module_info) - for class_info in module_info.class_info_collection: - # Skip excluded classes - if class_info.excluded: - continue - info_helper.extract_templates_from_source(class_info) - class_info.update_names() - def log_unknown_classes(self) -> None: - """Get unwrapped classes.""" + """ + Log unwrapped classes. + """ logger = logging.getLogger() all_class_decls = self.source_ns.classes(allow_empty=True) @@ -277,7 +267,9 @@ def parse_header_collection(self) -> None: self.source_ns = source_parser.parse() def parse_package_info(self) -> None: - """Parse the package info file to create a PackageInfo object.""" + """ + Parse the package info file to create a PackageInfo object. + """ if self.package_info_path: # If a package info file exists, parse it to create a PackageInfo object info_parser = PackageInfoParser(self.package_info_path, self.source_root) @@ -288,12 +280,23 @@ def parse_package_info(self) -> None: self.package_info = PackageInfo("cppwg_package", self.source_root) def update_from_ns(self) -> None: - """Update modules with information from the source namespace.""" + """ + Update modules with information from the parsed source namespace. + """ for module_info in self.package_info.module_info_collection: module_info.update_from_ns(self.source_ns) + def update_from_source(self) -> None: + """ + Update modules with information from the source headers. + """ + for module_info in self.package_info.module_info_collection: + module_info.update_from_source() + def write_header_collection(self) -> None: - """Write the header collection to file.""" + """ + Write the header collection to file. + """ header_collection_writer = CppHeaderCollectionWriter( self.package_info, self.wrapper_root, @@ -302,7 +305,9 @@ def write_header_collection(self) -> None: header_collection_writer.write() def write_wrappers(self) -> None: - """Write all the wrappers required for the package.""" + """ + Write all the wrappers required for the package. + """ for module_info in self.package_info.module_info_collection: module_writer = CppModuleWrapperWriter( module_info, @@ -312,7 +317,9 @@ def write_wrappers(self) -> None: module_writer.write() def generate_wrapper(self) -> None: - """Parse input yaml and C++ source to generate Python wrappers.""" + """ + Parse input yaml and C++ source to generate Python wrappers. + """ # Parse the input yaml for package, module, and class information self.parse_package_info() @@ -322,8 +329,8 @@ def generate_wrapper(self) -> None: # Map each class to a header file self.map_classes_to_hpp_files() - # Attempt to extract templates for each class from the source files - self.extract_templates_from_source() + # Update modules with information from the source headers + self.update_from_source() # Write the header collection to file self.write_header_collection() @@ -331,7 +338,7 @@ def generate_wrapper(self) -> None: # Parse the headers with pygccxml and castxml self.parse_header_collection() - # Update modules with information from the source namespace + # Update modules with information from the parsed source namespace self.update_from_ns() # Log list of unknown classes in the source root diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 358a1f8..07b30c2 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -8,6 +8,7 @@ from pygccxml.declarations.runtime_errors import declaration_not_found_t from cppwg.input.cpp_type_info import CppTypeInfo +from cppwg.utils import utils class CppClassInfo(CppTypeInfo): @@ -32,6 +33,69 @@ def __init__(self, name: str, class_config: Optional[Dict[str, Any]] = None): self.py_names: List[str] = None self.base_decls: Optional[List["declaration_t"]] = None # noqa: F821 + def extract_templates_from_source(self) -> None: + """ + Extract template args from the associated source file. + + Search the source file for a class signature matching one of the + template signatures defined in `template_substitutions`. If a match + is found, set the corresponding template arg replacements for the class. + """ + # Skip if there are template args attached directly to the class + if self.template_arg_lists: + return + + # Skip if there is no source file + source_path = self.source_file_full_path + if not source_path: + return + + # Get list of template substitutions applicable to this class + # e.g. [ {"signature":"", "replacement":[[2,2], [3,3]]} ] + substitutions = self.hierarchy_attribute_gather("template_substitutions") + + # Skip if there are no applicable template substitutions + if not substitutions: + return + + source = utils.read_source_file( + source_path, + strip_comments=True, + strip_preprocessor=True, + strip_whitespace=True, + ) + + # Search for template signatures in the source file + for substitution in substitutions: + # Signature e.g. + signature = substitution["signature"].strip() + + class_list = utils.find_classes_in_source( + source, + class_name=self.name, + template_signature=signature, + ) + + if class_list: + self.template_signature = signature + + # Replacement e.g. [[2,2], [3,3]] + self.template_arg_lists = substitution["replacement"] + + # Extract parameters ["A", "B"] from "" + self.template_params = [] + for part in signature.split(","): + param = ( + part.strip() + .replace("<", "") + .replace(">", "") + .split(" ")[1] + .split("=")[0] + .strip() + ) + self.template_params.append(param) + break + def is_child_of(self, other: "ClassInfo") -> bool: # noqa: F821 """ Check if the class is a child of the specified class. @@ -151,6 +215,17 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 base.related_class for decl in self.decls for base in decl.bases ] + def update_from_source(self) -> None: + """ + Update class with information from the source headers. + """ + # Skip excluded classes + if self.excluded: + return + + self.extract_templates_from_source() + self.update_names() + def update_py_names(self) -> None: """ Set the Python names for the class, accounting for template args. @@ -243,11 +318,15 @@ def update_cpp_names(self) -> None: self.cpp_names.append(self.name + template_string) def update_names(self) -> None: - """Update the C++ and Python names for the class.""" + """ + Update the C++ and Python names for the class. + """ self.update_cpp_names() self.update_py_names() @property def parent(self) -> "ModuleInfo": # noqa: F821 - """Returns the parent module info object.""" + """ + Returns the parent module info object. + """ return self.module_info diff --git a/cppwg/input/free_function_info.py b/cppwg/input/free_function_info.py index 89e3e72..5f69134 100644 --- a/cppwg/input/free_function_info.py +++ b/cppwg/input/free_function_info.py @@ -30,4 +30,5 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 source_ns : pygccxml.declarations.namespace_t The source namespace """ - self.decls = source_ns.free_functions(self.name, allow_empty=True)[0] + ff_decls = source_ns.free_functions(self.name, allow_empty=True) + self.decls = [ff_decls[0]] diff --git a/cppwg/input/info_helper.py b/cppwg/input/info_helper.py deleted file mode 100644 index 71b00bc..0000000 --- a/cppwg/input/info_helper.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Helper utilities for info structures.""" - -import logging -import os -from typing import Any, Dict, List - -from cppwg.input.base_info import BaseInfo -from cppwg.input.class_info import CppClassInfo -from cppwg.input.module_info import ModuleInfo -from cppwg.utils import utils - - -class CppInfoHelper: - """ - Adds information extracted from C++ source code to info objects. - - 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: 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 extract_templates_from_source(self, feature_info: BaseInfo) -> None: - """ - Get template args from the source file associated with an info object. - - __________ - Parameters - __________ - feature_info : BaseInfo - The feature info object to expand. - """ - logger = logging.getLogger() - - if not isinstance(feature_info, CppClassInfo): - logger.error(f"Unsupported feature type: {type(feature_info)}") - raise TypeError() - - # Skip if there are pre-defined template args - if feature_info.template_arg_lists: - return - - # Skip if there is no source file - source_path = feature_info.source_file_full_path - if not source_path: - return - - if not os.path.isfile(source_path): - 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, Any]] = ( - feature_info.hierarchy_attribute_gather("template_substitutions") - ) - - # Skip if there are no template substitutions - if len(template_substitutions) == 0: - return - - source = utils.read_source_file( - source_path, - strip_comments=True, - strip_preprocessor=True, - strip_whitespace=True, - ) - - # Search for template signatures in the source file - for template_substitution in template_substitutions: - # Signature e.g. - signature = template_substitution["signature"].strip() - - class_list = utils.find_classes_in_source( - source, - class_name=feature_info.name, - template_signature=signature, - ) - - if class_list: - # Replacement e.g. [[2,2], [3,3]] - replacement = template_substitution["replacement"] - - feature_info.template_arg_lists = replacement - feature_info.template_signature = signature - - # Extract ["DIM_A", "DIM_B"] from "" - template_params = [] - for tp in signature.split(","): - template_params.append( - tp.strip() - .replace("<", "") - .replace(">", "") - .split(" ")[1] - .split("=")[0] - .strip() - ) - feature_info.template_params = template_params - break diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index f1f0195..4867b4c 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -161,3 +161,11 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 # Update free functions with information from source namespace. for ff_info in self.free_function_info_collection: ff_info.update_from_ns(source_ns) + + def update_from_source(self) -> None: + """ + Update module with information from the source headers. + + """ + for class_info in self.class_info_collection: + class_info.update_from_source() From 8f288df4b0d2bff26a4fba6313cd2f9b46a06a32 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 13:45:11 +0000 Subject: [PATCH 09/16] #12 Simplify class mapping to source --- cppwg/generators.py | 40 +++++++++----------------------------- cppwg/input/class_info.py | 14 ++++++++++++- cppwg/input/module_info.py | 5 ++--- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index 0191df3..e1a956a 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -231,26 +231,7 @@ def log_unknown_classes(self) -> None: seen_class_names.add(class_name) logger.info(f"Unknown class {class_name} from {hpp_file_path}") - def map_classes_to_hpp_files(self) -> None: - """ - Map each class to a header file. - - 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: - # Skip excluded classes - if class_info.excluded: - continue - 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) -> None: + def parse_headers(self) -> None: """ Parse the hpp files to collect C++ declarations. @@ -279,19 +260,19 @@ def parse_package_info(self) -> None: # If no package info file exists, create a PackageInfo object with default settings self.package_info = PackageInfo("cppwg_package", self.source_root) - def update_from_ns(self) -> None: + def update_modules_from_ns(self) -> None: """ Update modules with information from the parsed source namespace. """ for module_info in self.package_info.module_info_collection: module_info.update_from_ns(self.source_ns) - def update_from_source(self) -> None: + def update_modules_from_source(self) -> None: """ Update modules with information from the source headers. """ for module_info in self.package_info.module_info_collection: - module_info.update_from_source() + module_info.update_from_source(self.package_info.source_hpp_files) def write_header_collection(self) -> None: """ @@ -326,20 +307,17 @@ def generate_wrapper(self) -> None: # Search for header files in the source root self.collect_source_hpp_files() - # Map each class to a header file - self.map_classes_to_hpp_files() - # Update modules with information from the source headers - self.update_from_source() + self.update_modules_from_source() - # Write the header collection to file + # Write the header collection file self.write_header_collection() - # Parse the headers with pygccxml and castxml - self.parse_header_collection() + # Parse the headers with pygccxml (+ castxml) + self.parse_headers() # Update modules with information from the parsed source namespace - self.update_from_ns() + self.update_modules_from_ns() # Log list of unknown classes in the source root self.log_unknown_classes() diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 07b30c2..61587ae 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -1,6 +1,7 @@ """Class information structure.""" import logging +import os import re from typing import Any, Dict, List, Optional @@ -215,7 +216,7 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 base.related_class for decl in self.decls for base in decl.bases ] - def update_from_source(self) -> None: + def update_from_source(self, source_files: List[str]) -> None: """ Update class with information from the source headers. """ @@ -223,7 +224,18 @@ def update_from_source(self) -> None: if self.excluded: return + # Map class to a source file, assuming the file name is the class name + for file_path in source_files: + file_name = os.path.basename(file_path) + if self.name == os.path.splitext(file_name)[0]: + self.source_file_full_path = file_path + if self.source_file is None: + self.source_file = file_name + + # Extract template args from the source file self.extract_templates_from_source() + + # Update the C++ and Python class names self.update_names() def update_py_names(self) -> None: diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 4867b4c..f0b0b28 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -162,10 +162,9 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 for ff_info in self.free_function_info_collection: ff_info.update_from_ns(source_ns) - def update_from_source(self) -> None: + def update_from_source(self, source_files: List[str]) -> None: """ Update module with information from the source headers. - """ for class_info in self.class_info_collection: - class_info.update_from_source() + class_info.update_from_source(source_files) From ecb1c2bcd0379a3c69a1afca2d63da202755dc53 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 14:02:48 +0000 Subject: [PATCH 10/16] #12 PackageInfo responsible for collect_source_headers --- cppwg/__main__.py | 2 +- cppwg/generators.py | 41 ++++--------------------------------- cppwg/input/package_info.py | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/cppwg/__main__.py b/cppwg/__main__.py index a6bbc99..34fc930 100644 --- a/cppwg/__main__.py +++ b/cppwg/__main__.py @@ -113,7 +113,7 @@ def generate(args: argparse.Namespace) -> None: castxml_cflags=castxml_cflags, ) - generator.generate_wrapper() + generator.generate_wrappers() def main() -> None: diff --git a/cppwg/generators.py b/cppwg/generators.py index e1a956a..b1e304b 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -1,6 +1,5 @@ """Contains the main interface for generating Python wrappers.""" -import fnmatch import logging import os import re @@ -18,7 +17,6 @@ from cppwg.utils import utils from cppwg.utils.constants import ( CPPWG_DEFAULT_WRAPPER_DIR, - CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILENAME, ) from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter @@ -164,37 +162,6 @@ def __init__( self.wrapper_root, CPPWG_HEADER_COLLECTION_FILENAME ) - def collect_source_hpp_files(self) -> None: - """ - Collect *.hpp files from the source root. - - Walk through the source root and add any files matching the provided - patterns e.g. "*.hpp". Skip the wrapper root and wrappers to - avoid pollution. - """ - logger = logging.getLogger() - - 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.abspath(os.path.join(root, filename)) - - # Skip files in wrapper root dir - if Path(self.wrapper_root) in Path(filepath).parents: - continue - - # Skip files with the extensions like .cppwg.hpp - suffix = os.path.splitext(os.path.splitext(filename)[0])[1] - if suffix == CPPWG_EXT: - continue - - self.package_info.source_hpp_files.append(filepath) - - # Check if any source files were found - if not self.package_info.source_hpp_files: - logger.error(f"No header files found in source root: {self.source_root}") - raise FileNotFoundError() - def log_unknown_classes(self) -> None: """ Log unwrapped classes. @@ -297,15 +264,15 @@ def write_wrappers(self) -> None: ) module_writer.write() - def generate_wrapper(self) -> None: + def generate_wrappers(self) -> None: """ - Parse input yaml and C++ source to generate Python wrappers. + Parse yaml configuration and C++ source to generate Python wrappers. """ # Parse the input yaml for package, module, and class information self.parse_package_info() - # Search for header files in the source root - self.collect_source_hpp_files() + # Collect header files, skipping wrappers to avoid pollution + self.package_info.collect_source_headers(restricted_paths=[self.wrapper_root]) # Update modules with information from the source headers self.update_modules_from_source() diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index 82ad62d..0d26547 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -1,8 +1,13 @@ """Package information structure.""" +import fnmatch +import logging +import os +from pathlib import Path from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo +from cppwg.utils.constants import CPPWG_EXT class PackageInfo(BaseInfo): @@ -69,3 +74,39 @@ def __init__( def parent(self) -> None: """Returns None as this is the top level object in the hierarchy.""" return None + + def collect_source_headers(self, restricted_paths: List[str]) -> None: + """ + Collect header files from the source root. + + Walk through the source root and add any files matching the provided + source file patterns e.g. "*.hpp". + + Parameters + ---------- + restricted_paths : List[str] + A list of restricted paths to skip when collecting header files. + """ + logger = logging.getLogger() + + for root, _, filenames in os.walk(self.source_root, followlinks=True): + for pattern in self.source_hpp_patterns: + for filename in fnmatch.filter(filenames, pattern): + filepath = os.path.abspath(os.path.join(root, filename)) + + # Skip files in restricted paths + for restricted_path in restricted_paths: + if Path(restricted_path) in Path(filepath).parents: + continue + + # Skip files with the extensions like .cppwg.hpp + suffix = os.path.splitext(os.path.splitext(filename)[0])[1] + if suffix == CPPWG_EXT: + continue + + self.source_hpp_files.append(filepath) + + # Check if any source files were found + if not self.source_hpp_files: + logger.error(f"No header files found in source root: {self.source_root}") + raise FileNotFoundError() From 5fa3f0b7ca518d0cfb4fd9341e386796cd00389a Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 14:13:04 +0000 Subject: [PATCH 11/16] #12 Simplify info updates from source and ns --- cppwg/generators.py | 22 ++++------------------ cppwg/input/class_info.py | 9 +++++++-- cppwg/input/module_info.py | 9 +++++++-- cppwg/input/package_info.py | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/cppwg/generators.py b/cppwg/generators.py index b1e304b..b32c999 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -227,20 +227,6 @@ def parse_package_info(self) -> None: # If no package info file exists, create a PackageInfo object with default settings self.package_info = PackageInfo("cppwg_package", self.source_root) - def update_modules_from_ns(self) -> None: - """ - Update modules with information from the parsed source namespace. - """ - for module_info in self.package_info.module_info_collection: - module_info.update_from_ns(self.source_ns) - - def update_modules_from_source(self) -> None: - """ - Update modules with information from the source headers. - """ - for module_info in self.package_info.module_info_collection: - module_info.update_from_source(self.package_info.source_hpp_files) - def write_header_collection(self) -> None: """ Write the header collection to file. @@ -274,8 +260,8 @@ def generate_wrappers(self) -> None: # Collect header files, skipping wrappers to avoid pollution self.package_info.collect_source_headers(restricted_paths=[self.wrapper_root]) - # Update modules with information from the source headers - self.update_modules_from_source() + # Update info objects with data from the source headers + self.package_info.update_from_source() # Write the header collection file self.write_header_collection() @@ -283,8 +269,8 @@ def generate_wrappers(self) -> None: # Parse the headers with pygccxml (+ castxml) self.parse_headers() - # Update modules with information from the parsed source namespace - self.update_modules_from_ns() + # Update info objects with data from the parsed source namespace + self.package_info.update_from_ns(self.source_ns) # Log list of unknown classes in the source root self.log_unknown_classes() diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 61587ae..531d6fb 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -216,16 +216,21 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 base.related_class for decl in self.decls for base in decl.bases ] - def update_from_source(self, source_files: List[str]) -> None: + def update_from_source(self, source_file_paths: List[str]) -> None: """ Update class with information from the source headers. + + Parameters + ---------- + source_file_paths : List[str] + A list of source file paths """ # Skip excluded classes if self.excluded: return # Map class to a source file, assuming the file name is the class name - for file_path in source_files: + for file_path in source_file_paths: file_name = os.path.basename(file_path) if self.name == os.path.splitext(file_name)[0]: self.source_file_full_path = file_path diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index f0b0b28..2746cf9 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -162,9 +162,14 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 for ff_info in self.free_function_info_collection: ff_info.update_from_ns(source_ns) - def update_from_source(self, source_files: List[str]) -> None: + def update_from_source(self, source_file_paths: List[str]) -> None: """ Update module with information from the source headers. + + Parameters + ---------- + source_files : List[str] + A list of source file paths. """ for class_info in self.class_info_collection: - class_info.update_from_source(source_files) + class_info.update_from_source(source_file_paths) diff --git a/cppwg/input/package_info.py b/cppwg/input/package_info.py index 0d26547..f90f24a 100644 --- a/cppwg/input/package_info.py +++ b/cppwg/input/package_info.py @@ -110,3 +110,22 @@ def collect_source_headers(self, restricted_paths: List[str]) -> None: if not self.source_hpp_files: logger.error(f"No header files found in source root: {self.source_root}") raise FileNotFoundError() + + def update_from_source(self) -> None: + """ + Update modules with information from the source headers. + """ + for module_info in self.module_info_collection: + module_info.update_from_source(self.source_hpp_files) + + def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 + """ + Update modules with information from the parsed source namespace. + + Parameters + ---------- + source_ns : pygccxml.declarations.namespace_t + The source namespace + """ + for module_info in self.module_info_collection: + module_info.update_from_ns(source_ns) From 1567eac98ad005199aa8fac6e62175653a3d45bd Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 14:40:40 +0000 Subject: [PATCH 12/16] #12 Add PackageWriter --- cppwg/generators.py | 19 +++++++-------- cppwg/writers/package_writer.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 cppwg/writers/package_writer.py diff --git a/cppwg/generators.py b/cppwg/generators.py index b32c999..46ceb29 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -1,4 +1,4 @@ -"""Contains the main interface for generating Python wrappers.""" +"""The main interface for generating Python wrappers.""" import logging import os @@ -20,7 +20,7 @@ CPPWG_HEADER_COLLECTION_FILENAME, ) from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter -from cppwg.writers.module_writer import CppModuleWrapperWriter +from cppwg.writers.package_writer import CppPackageWrapperWriter class CppWrapperGenerator: @@ -240,15 +240,12 @@ def write_header_collection(self) -> None: def write_wrappers(self) -> None: """ - Write all the wrappers required for the package. + Write the wrapper code for the package. """ - for module_info in self.package_info.module_info_collection: - module_writer = CppModuleWrapperWriter( - module_info, - wrapper_templates.template_collection, - self.wrapper_root, - ) - module_writer.write() + package_writer = CppPackageWrapperWriter( + self.package_info, wrapper_templates.template_collection, self.wrapper_root + ) + package_writer.write() def generate_wrappers(self) -> None: """ @@ -275,5 +272,5 @@ def generate_wrappers(self) -> None: # Log list of unknown classes in the source root self.log_unknown_classes() - # Write all the wrappers required + # Write the wrapper code for the package self.write_wrappers() diff --git a/cppwg/writers/package_writer.py b/cppwg/writers/package_writer.py new file mode 100644 index 0000000..b311b43 --- /dev/null +++ b/cppwg/writers/package_writer.py @@ -0,0 +1,42 @@ +"""Wrapper code writer for the package.""" + +from typing import Dict + +from cppwg.writers.module_writer import CppModuleWrapperWriter + + +class CppPackageWrapperWriter: + """ + Class to generates Python bindings for all modules in the package. + + Attributes + ---------- + package_info : PackageInfo + The package 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 + """ + + def __init__( + self, + package_info: "PackageInfo", # noqa: F821 + wrapper_templates: Dict[str, str], + wrapper_root: str, + ): + self.package_info = package_info + self.wrapper_templates = wrapper_templates + self.wrapper_root = wrapper_root + + def write(self) -> None: + """ + Write all the wrappers required for the package. + """ + for module_info in self.package_info.module_info_collection: + module_writer = CppModuleWrapperWriter( + module_info, + self.wrapper_templates, + self.wrapper_root, + ) + module_writer.write() From 3bde9a1b339295f199b8231cba3536386788aa9c Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 14:54:19 +0000 Subject: [PATCH 13/16] #12 Use generator.generate() --- cppwg/__main__.py | 2 +- cppwg/generators.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cppwg/__main__.py b/cppwg/__main__.py index 34fc930..332c93c 100644 --- a/cppwg/__main__.py +++ b/cppwg/__main__.py @@ -113,7 +113,7 @@ def generate(args: argparse.Namespace) -> None: castxml_cflags=castxml_cflags, ) - generator.generate_wrappers() + generator.generate() def main() -> None: diff --git a/cppwg/generators.py b/cppwg/generators.py index 46ceb29..d734780 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -178,15 +178,17 @@ def log_unknown_classes(self) -> None: seen_class_names.update(decl.name for decl in class_info.decls) for decl in all_class_decls: - if ( - Path(self.source_root) in Path(decl.location.file_name).parents - and decl.name not in seen_class_names - ): - seen_class_names.add(decl.name) - seen_class_names.add(decl.name.split("<")[0].strip()) - logger.info( - f"Unknown class {decl.name} from {decl.location.file_name}:{decl.location.line}" - ) + if decl.name in seen_class_names: + continue + + if Path(self.source_root) not in Path(decl.location.file_name).parents: + continue + + seen_class_names.add(decl.name) # e.g. Foo<2,2> + seen_class_names.add(decl.name.split("<")[0].strip()) # e.g. Foo + logger.info( + f"Unknown class {decl.name} from {decl.location.file_name}:{decl.location.line}" + ) # Check for uninstantiated class templates not parsed by pygccxml for hpp_file_path in self.package_info.source_hpp_files: @@ -247,7 +249,7 @@ def write_wrappers(self) -> None: ) package_writer.write() - def generate_wrappers(self) -> None: + def generate(self) -> None: """ Parse yaml configuration and C++ source to generate Python wrappers. """ From fdc44a5cb95272b5330157cf804770b5de82e1cf Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 15:44:12 +0000 Subject: [PATCH 14/16] #12 Update class sorting --- cppwg/input/module_info.py | 80 +++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index 2746cf9..c3b0a6d 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -1,6 +1,7 @@ """Module information structure.""" import os +from pathlib import Path from typing import Any, Dict, List, Optional from cppwg.input.base_info import BaseInfo @@ -51,7 +52,9 @@ def __init__(self, name: str, module_config: Optional[Dict[str, Any]] = None): @property def parent(self) -> "PackageInfo": # noqa: F821 - """Returns the parent package info object.""" + """ + Returns the associated package info object. + """ return self.package_info def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 @@ -73,51 +76,50 @@ def is_decl_in_source_path(self, decl: "declaration_t") -> bool: # noqa: F821 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: + if Path(full_path) in Path(decl.location.file_name).parents: return True return False def sort_classes(self) -> None: - """Sort the class info collection in order of dependence.""" + """ + Sort the class info collection in order of dependence. + """ self.class_info_collection.sort(key=lambda x: x.name) - order_changed = True - while order_changed: - order_changed = False - - i = 0 - n = len(self.class_info_collection) - while i < n - 1: - cls_i = self.class_info_collection[i] - ii = i # destination of cls_i - child_pos = [] # positions of cls_i's children - - for j in range(i + 1, n): - cls_j = self.class_info_collection[j] - if cls_i.is_child_of(cls_j): - # sort by inheritance - ii = j - elif cls_i.requires(cls_j) and not cls_j.requires(cls_i): - # sort by dependence, ignoring forward declaration cycles - ii = j - elif cls_j.is_child_of(cls_i): - child_pos.append(j) - - if ii <= i: - i += 1 - continue # no change in cls_i's position - - cls_i = self.class_info_collection.pop(i) - self.class_info_collection.insert(ii, cls_i) - - for j, idx in enumerate(child_pos): - if j > ii: - break # children already positioned after cls_i - cls_j = self.class_info_collection.pop(j - 1 - idx) - self.class_info_collection.insert(ii + idx, cls_j) - - order_changed = True + i = 0 + n = len(self.class_info_collection) + while i < n - 1: + cls_i = self.class_info_collection[i] + ii = i # Tracks destination of cls_i + j_pos = [] # Tracks positions of cls_i's dependents + + for j in range(i + 1, n): + cls_j = self.class_info_collection[j] + i_requires_j = cls_i.requires(cls_j) + j_requires_i = cls_j.requires(cls_i) + if cls_i.is_child_of(cls_j) or (i_requires_j and not j_requires_i): + # Position cls_i after all classes it depends on, + # ignoring forward declaration cycles + ii = j + elif cls_j.is_child_of(cls_i) or (j_requires_i and not i_requires_j): + # Collect positions of cls_i's dependents + j_pos.append(j) + + if ii <= i: + i += 1 + continue # No change in position + + # Move cls_i into new position ii + cls_i = self.class_info_collection.pop(i) + self.class_info_collection.insert(ii, cls_i) + + # Move dependents into positions after ii + for j, idx in enumerate(j_pos): + if j > ii: + break # Rest of dependents already positioned after ii + cls_j = self.class_info_collection.pop(j - 1 - idx) + self.class_info_collection.insert(ii + idx, cls_j) def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ From 9d5d0738bae2695634619fa7adbebce8326a10f4 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 17:20:25 +0000 Subject: [PATCH 15/16] #12 Fix class sorting --- cppwg/input/class_info.py | 2 +- cppwg/input/module_info.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cppwg/input/class_info.py b/cppwg/input/class_info.py index 531d6fb..60d71f3 100644 --- a/cppwg/input/class_info.py +++ b/cppwg/input/class_info.py @@ -186,7 +186,7 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 # If class has default args, try to compress the template signature logger.warning( - f"Could not find declaration for class {class_name}: trying for a partial match." + f"Could not find declaration for class {class_name}: trying a partial match." ) # Try to find the class without default template args diff --git a/cppwg/input/module_info.py b/cppwg/input/module_info.py index c3b0a6d..88bd064 100644 --- a/cppwg/input/module_info.py +++ b/cppwg/input/module_info.py @@ -96,13 +96,13 @@ def sort_classes(self) -> None: for j in range(i + 1, n): cls_j = self.class_info_collection[j] - i_requires_j = cls_i.requires(cls_j) - j_requires_i = cls_j.requires(cls_i) - if cls_i.is_child_of(cls_j) or (i_requires_j and not j_requires_i): + i_req_j = cls_i.requires(cls_j) + j_req_i = cls_j.requires(cls_i) + if cls_i.is_child_of(cls_j) or (i_req_j and not j_req_i): # Position cls_i after all classes it depends on, # ignoring forward declaration cycles ii = j - elif cls_j.is_child_of(cls_i) or (j_requires_i and not i_requires_j): + elif cls_j.is_child_of(cls_i) or (j_req_i and not i_req_j): # Collect positions of cls_i's dependents j_pos.append(j) @@ -115,9 +115,9 @@ def sort_classes(self) -> None: self.class_info_collection.insert(ii, cls_i) # Move dependents into positions after ii - for j, idx in enumerate(j_pos): + for idx, j in enumerate(j_pos): if j > ii: - break # Rest of dependents already positioned after ii + break # Rest of dependents are already positioned after ii cls_j = self.class_info_collection.pop(j - 1 - idx) self.class_info_collection.insert(ii + idx, cls_j) From 07e0fc06658f50147d1314bec6ca88aa818dd157 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Sun, 22 Sep 2024 17:20:58 +0000 Subject: [PATCH 16/16] #12 Print version during run --- cppwg/__init__.py | 11 ++--------- cppwg/__main__.py | 3 ++- cppwg/generators.py | 3 +++ cppwg/version.py | 5 +++++ 4 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 cppwg/version.py diff --git a/cppwg/__init__.py b/cppwg/__init__.py index 986567b..802c215 100644 --- a/cppwg/__init__.py +++ b/cppwg/__init__.py @@ -13,21 +13,14 @@ Contains string templates for Python wrappers. utils Contains utility functions and constants. +version + Contains version information. writers Contains writers for creating Python wrappers and writing to file. - -Utilities ---------- -__version__ - cppwg version string """ -from importlib import metadata - from cppwg.generators import CppWrapperGenerator __all__ = [ "CppWrapperGenerator", ] - -__version__ = metadata.version("cppwg") diff --git a/cppwg/__main__.py b/cppwg/__main__.py index 332c93c..0d6e209 100644 --- a/cppwg/__main__.py +++ b/cppwg/__main__.py @@ -3,7 +3,8 @@ import argparse import logging -from cppwg import CppWrapperGenerator, __version__ +from cppwg import CppWrapperGenerator +from cppwg.version import __version__ def parse_args() -> argparse.Namespace: diff --git a/cppwg/generators.py b/cppwg/generators.py index d734780..3a1b421 100644 --- a/cppwg/generators.py +++ b/cppwg/generators.py @@ -19,6 +19,7 @@ CPPWG_DEFAULT_WRAPPER_DIR, CPPWG_HEADER_COLLECTION_FILENAME, ) +from cppwg.version import __version__ as cppwg_version from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter from cppwg.writers.package_writer import CppPackageWrapperWriter @@ -58,6 +59,8 @@ def __init__( ): logger = logging.getLogger() + logger.info(f"cppwg version {cppwg_version}") + # Check that castxml_binary exists and is executable self.castxml_binary: str = "" diff --git a/cppwg/version.py b/cppwg/version.py new file mode 100644 index 0000000..ef4a8c9 --- /dev/null +++ b/cppwg/version.py @@ -0,0 +1,5 @@ +"""Version information.""" + +from importlib import metadata + +__version__ = metadata.version("cppwg")