Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 73 additions & 81 deletions cppwg/info/base_info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Generic information structure."""

import copy
import importlib.util
import logging
import os
Expand All @@ -12,6 +13,63 @@
from cppwg.templates.custom import Custom


# The configuration options shared by every info level (package, module, class,
# free function, ...), each mapped to its default value. This is the single
# source of truth for the shared options: BaseInfo seeds these as attribute
# defaults and copies any the config overrides, and the parser
# (cppwg.parsers.package_info_parser) builds its config dicts from the same
# schema. An option added here is therefore understood everywhere - defined in
# one place instead of being restated in BaseInfo and the parser (which is how
# options such as name_replacements previously became unreachable from the YAML).
# Mutable defaults are deep-copied per use so no two objects share a list/dict.
# See the class Attributes docstring for what each option means; tri-state
# options default to None, meaning "inherit from further up the info tree".
BASE_INFO_OPTIONS: dict[str, Any] = {
"arg_type_excludes": [],
"auto_includes": None,
"calldef_excludes": [],
"constructor_arg_type_excludes": [],
"constructor_signature_excludes": [],
"custom_generator": "",
"discover_arg_excludes": {},
"discover_template_instantiations": None,
"excluded": False,
"excluded_methods": [],
"excluded_variables": [],
"export_values": None,
"name_replacements": {
"double": "Double",
"unsigned int": "Unsigned",
"Unsigned int": "Unsigned",
"unsigned": "Unsigned",
"std::vector": "Vector",
"std::pair": "Pair",
"std::map": "Map",
"std::string": "String",
"boost::shared_ptr": "SharedPtr",
"*": "Ptr",
"c_vector": "CVector",
"std::set": "Set",
},
"pointer_call_policy": "",
"prefix_code": [],
"prefix_text": "",
"reference_call_policy": "",
"return_type_excludes": [],
"smart_ptr_type": "",
"source_includes": [],
"source_root": "",
"suffix_code": [],
"template_substitutions": [],
}

# Options copied from the config but deliberately not in BASE_INFO_OPTIONS:
# exclude_inherited_overrides is a tri-state the parser seeds per level (package
# False, module/class None) to drive the package->module->class cascade, so it
# must not be given a single shared default here.
_EXTRA_CONFIG_KEYS: tuple[str, ...] = ("exclude_inherited_overrides",)


class BaseInfo(ABC):
"""
A generic information structure for features.
Expand Down Expand Up @@ -115,92 +173,26 @@ def __init__(self, name: str, info_config: dict[str, Any] | None = None) -> None
"""
self.name: str = name

# Paths
self.source_includes: list[str] = []
self.source_root: str = ""

# Exclusions
self.arg_type_excludes: list[str] = []
self.calldef_excludes: list[str] = []
self.constructor_arg_type_excludes: list[str] = []
self.constructor_signature_excludes: list[list[str]] = []
# Tri-state (None inherits): automatically add includes for the project
# types used in a class's wrapped signatures. Off unless set.
self.auto_includes: bool | None = None
# Tri-state: None means inherit from further up the info tree, so that a
# package/module-level setting propagates to classes (hierarchy_attribute
# stops at the first non-None value it finds ascending the tree).
self.discover_arg_excludes: dict[str, list] = {}
self.discover_template_instantiations: bool | None = None
self.excluded: bool = False
self.excluded_methods: list[str] = []
self.excluded_variables: list[str] = []
self.return_type_excludes: list[str] = []
# Tri-state (None inherits): whether a wrapped enum exports its
# enumerators into the module scope (pybind11's .export_values()). Only
# meaningful for enums, but inheritable so a package/module setting
# applies to all enums below it. See CppEnumInfo.should_export_values.
self.export_values: bool | None = None

# Pointers
self.pointer_call_policy: str = ""
self.reference_call_policy: str = ""
self.smart_ptr_type: str = ""

# Substitutions
self.template_substitutions: list[dict[str, Any]] = []
self.name_replacements: dict[str, str] = {
"double": "Double",
"unsigned int": "Unsigned",
"Unsigned int": "Unsigned",
"unsigned": "Unsigned",
"std::vector": "Vector",
"std::pair": "Pair",
"std::map": "Map",
"std::string": "String",
"boost::shared_ptr": "SharedPtr",
"*": "Ptr",
"c_vector": "CVector",
"std::set": "Set",
}

# Custom Code
self.prefix_code: list[str] = []
self.suffix_code: list[str] = []
self.prefix_text: str = ""
self.custom_generator: str = ""
# Seed the shared options from the single schema, deep-copying each
# default so no two info objects share a mutable list/dict. See the
# Attributes docstring for what each option means.
for key, default in BASE_INFO_OPTIONS.items():
setattr(self, key, copy.deepcopy(default))

self.custom_generator_instance: "Custom | None" = None

if info_config:
for key in [
"arg_type_excludes",
"auto_includes",
"calldef_excludes",
"constructor_arg_type_excludes",
"constructor_signature_excludes",
"custom_generator",
"discover_arg_excludes",
"discover_template_instantiations",
"exclude_inherited_overrides",
"excluded",
"excluded_methods",
"excluded_variables",
"export_values",
"name_replacements",
"pointer_call_policy",
"prefix_code",
"prefix_text",
"reference_call_policy",
"return_type_excludes",
"smart_ptr_type",
"source_includes",
"source_root",
"suffix_code",
"template_substitutions",
]:
# Copy any option the config provides, over the schema defaults.
# Deep-copy each value: the parser shallow-copies one base_config into
# every package/module/class config, so the same list/dict object
# reaches many info objects; without this copy they would alias it (a
# later mutation of one object's option would leak to its siblings and
# parent). exclude_inherited_overrides (in _EXTRA_CONFIG_KEYS) is
# copied when present but has no shared default - the parser seeds it
# per level.
for key in (*BASE_INFO_OPTIONS, *_EXTRA_CONFIG_KEYS):
if key in info_config:
setattr(self, key, info_config[key])
setattr(self, key, copy.deepcopy(info_config[key]))

self.load_custom_generator()

Expand Down
78 changes: 53 additions & 25 deletions cppwg/info/class_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from cppwg.info.cpp_entity_info import CppEntityInfo
from cppwg.utils import utils
from cppwg.utils.constants import CPPWG_EXT

if TYPE_CHECKING:
from pygccxml.declarations import declaration_t
Expand All @@ -25,7 +26,7 @@ def _unqualified_base_name(name: str) -> str:
identity, which pygccxml does not preserve across template/typedef
resolution.
"""
return name.split("<", 1)[0].rsplit("::", 1)[-1].strip()
return utils.unqualified_name(name.split("<", 1)[0]).strip()


class CppClassInfo(CppEntityInfo):
Expand Down Expand Up @@ -533,14 +534,55 @@ class it is cleaned the same way as the instantiation names.
if not self.template_arg_lists:
return base

# A class base name drops `<`/`,` (separator="") rather than turning them
# into `_`, since it is a single token, not a list of template args.
return self._mangle_py_token(base, separator="")

def _mangle_py_token(self, text: str, separator: str = "") -> str:
"""
Mangle a C++ token into a Python-name-safe fragment.

Applies the configured name_replacements, then reduces the C++
punctuation: ``<`` and ``,`` become ``separator`` (``""`` to drop them in
a class base name, ``"_"`` to split nested template arguments), while
``>`` and spaces are always removed. Finally the first character is
capitalised. Shared by py_name_base and update_py_names so the two
mangle names the same way apart from that deliberate separator choice.

Parameters
----------
text : str
The C++ token to mangle, e.g. a class base name or a template arg.
separator : str
What ``<`` and ``,`` become ("" to remove, "_" to split).

Returns
-------
str
The mangled, Python-name-safe fragment.
"""
for name, replacement in self.name_replacements.items():
base = base.replace(name, replacement)
base = base.translate(
str.maketrans({"<": None, ">": None, ",": None, " ": None})
)
if len(base) > 1:
base = base[0].capitalize() + base[1:]
return base
text = text.replace(name, replacement)
text = text.replace("<", separator).replace(",", separator)
text = text.replace(">", "").replace(" ", "")
if len(text) > 1:
text = text[0].capitalize() + text[1:]
return text

def wrapper_header_filename(self) -> str:
"""
Return the class's wrapper header filename, e.g. ``Foo.cppwg.hpp``.

All of the class's instantiations share this single header, named after
py_name_base(). Single source for the wrapper filename so the file that
is written, the module's ``#include`` of it and the cpp's own
``#include`` cannot drift apart into a missing-header compile error.
"""
return f"{self.py_name_base()}.{CPPWG_EXT}.hpp"

def wrapper_source_filename(self) -> str:
"""Return the class's wrapper source filename, e.g. ``Foo.cppwg.cpp``."""
return f"{self.py_name_base()}.{CPPWG_EXT}.cpp"

def update_py_names(self) -> None:
"""
Expand All @@ -559,29 +601,15 @@ class instantiation. For example, class "Foo" with template arguments
self.py_names.append(class_name)
return

# Table of special characters for removal
rm_chars = {"<": None, ">": None, ",": None, " ": None}
rm_table = str.maketrans(rm_chars)

# 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]

template_string = ""
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.replace("<", "_").replace(",", "_").translate(rm_table)
)

# Capitalize the first letter
if len(arg_str) > 1:
arg_str = arg_str[0].capitalize() + arg_str[1:]
# A nested template arg keeps its structure via "_" separators,
# e.g. PottsMesh<2> -> PottsMesh_2 (separator="_").
arg_str = self._mangle_py_token(str(arg), separator="_")

# Add "_" between template arguments
template_string += arg_str
Expand Down
11 changes: 6 additions & 5 deletions cppwg/info/enum_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ def should_export_values(self) -> bool:
the info tree (package/module); otherwise mirror the C++ enum kind -
export for an unscoped enum, not for a scoped one.
"""
override = self.hierarchy_attribute("export_values")
if override is not None:
return override
return not self.scoped
return utils.should_export_enum_values(
self.hierarchy_attribute("export_values"), self.scoped
)

def update_from_ns(self, source_ns: "namespace_t") -> None:
"""
Expand Down Expand Up @@ -74,5 +73,7 @@ def update_from_ns(self, source_ns: "namespace_t") -> None:
# pygccxml does not expose enum scopedness, so read it from the source
# file the enum was declared in (via the resolved decl's location).
self.scoped = utils.is_scoped_enum_in_source_file(
self.decls[0].location.file_name, self.decls[0].name
self.decls[0].location.file_name,
self.decls[0].name,
self.decls[0].location.line,
)
Loading
Loading