From 6255bc1bab62874f8509e6fb7ebe12252cf05a4c Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Fri, 1 Sep 2023 19:24:14 -0400 Subject: [PATCH] Migrate to cxxheaderparser --- robotpy_build/autowrap/cxxparser.py | 756 +++++++++++++++++++++++++++ robotpy_build/autowrap/hooks.py | 535 +++++++++---------- robotpy_build/autowrap/j2_context.py | 72 +-- robotpy_build/generator_data.py | 3 + 4 files changed, 1037 insertions(+), 329 deletions(-) create mode 100644 robotpy_build/autowrap/cxxparser.py diff --git a/robotpy_build/autowrap/cxxparser.py b/robotpy_build/autowrap/cxxparser.py new file mode 100644 index 00000000..4dd57fea --- /dev/null +++ b/robotpy_build/autowrap/cxxparser.py @@ -0,0 +1,756 @@ +# parses a header file and outputs a HeaderContext suitable for use with +# the autowrap templates + +from keyword import iskeyword +import pathlib +import re +import sys +import typing + +if sys.version_info >= (3, 8): + from typing import Protocol +else: + Protocol = object # pragma: no cover + +from dataclasses import dataclass, field +import sphinxify + +from ..config.autowrap_yml import ( + AutowrapConfigYaml, + BufferData, + BufferType, + ClassData, + EnumData, + EnumValue, + FunctionData, + ParamData, + PropAccess, + ReturnValuePolicy, +) +from ..generator_data import GeneratorData + +from .j2_context import ( + BaseClassData, + ClassContext, + ClassTemplateData, + Documentation, + EnumContext, + EnumeratorContext, + FunctionContext, + GeneratedLambda, + HeaderContext, + ParamContext, + PropContext, + TemplateInstanceContext, + TrampolineData, +) +from .mangle import trampoline_signature + +from cxxheaderparser.types import ( + ClassDecl, + EnumDecl, + Field, + ForwardDecl, + FriendDecl, + Function, + Method, + NamespaceAlias, + NameSpecifier, + TemplateInst, + Typedef, + UsingAlias, + UsingDecl, + Variable, + Value, +) + +from cxxheaderparser.parserstate import ( + State, + EmptyBlockState, + ClassBlockState, + ExternBlockState, + NamespaceBlockState, +) +from cxxheaderparser.parser import CxxParser +from cxxheaderparser.options import ParserOptions + + +class HasSubpackage(Protocol): + subpackage: str + + +class HasDoc(Protocol): + doc: str + doc_append: str + + +class HasNameData(Protocol): + rename: str + + +# TODO: this isn't the best solution +def _gen_int_types(): + for i in ("int", "uint"): + for j in ("", "_fast", "_least"): + for k in ("8", "16", "32", "64"): + yield f"{i}{j}{k}_t" + yield "intmax_t" + yield "uintmax_t" + + +_int32_types = frozenset(_gen_int_types()) + + +_rvp_map = { + ReturnValuePolicy.TAKE_OWNERSHIP: ", py::return_value_policy::take_ownership", + ReturnValuePolicy.COPY: ", py::return_value_policy::copy", + ReturnValuePolicy.MOVE: ", py::return_value_policy::move", + ReturnValuePolicy.REFERENCE: ", py::return_value_policy::reference", + ReturnValuePolicy.REFERENCE_INTERNAL: ", py::return_value_policy::reference_internal", + ReturnValuePolicy.AUTOMATIC: "", + ReturnValuePolicy.AUTOMATIC_REFERENCE: ", py::return_value_policy::automatic_reference", +} + +# fmt: off +_operators = { + # binary + "-", "+", "*", "/", "%", "&", "^", "==", "!=", "|", ">", ">=", "<", "<=", + # inplace + "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", +} +# fmt: on + +_type_caster_seps = re.compile(r"[<>\(\)]") + +_qualname_bad = ":<>=" +_qualname_trans = str.maketrans(_qualname_bad, "_" * len(_qualname_bad)) + +_lambda_predent = " " + +_default_param_data = ParamData() +_default_enum_value = EnumValue() + + +@dataclass +class _ReturnParamContext: + #: was x_type + cpp_type: str + + #: If this is an out parameter, the name of the parameter + cpp_retname: str + + +def _using_signature(cls: ClassContext, fn: FunctionContext) -> str: + return f"{cls.full_cpp_name_identifier}_{fn.cpp_name}" + + +# +# Visitor implementation +# + + +class ClassStateData(typing.NamedTuple): + ctx: ClassContext + cls_key: str + data: ClassData + + +Context = typing.Union[str, ClassStateData] + +# define what user_data we store in each state type +# - classes are ClassStateData +# - everything else is the current namespace name +AWState = State[Context, Context] +AWEmptyBlockState = EmptyBlockState[Context, Context] +AWExternBlockState = ExternBlockState[Context, Context] +AWNamespaceBlockState = NamespaceBlockState[str, str] +AWClassBlockState = ClassBlockState[ClassStateData, Context] + + +class AutowrapVisitor: + """ + Collects the results of parsing a header file + """ + + types: typing.Set[str] + + def __init__( + self, hctx: HeaderContext, gendata: GeneratorData, report_only: bool + ) -> None: + self.gendata = gendata + self.hctx = hctx + self.report_only = report_only + self.types = set() + + # + # Visitor interface + # + + def on_parse_start(self, state: AWNamespaceBlockState) -> None: + state.user_data = "" + + def on_pragma(self, state: AWState, content: Value) -> None: + pass + + def on_include(self, state: AWState, filename: str) -> None: + pass + + def on_empty_block_start(self, state: AWEmptyBlockState) -> None: + state.user_data = state.parent.user_data + + def on_empty_block_end(self, state: AWEmptyBlockState) -> None: + pass + + def on_extern_block_start(self, state: AWExternBlockState) -> None: + state.user_data = state.parent.user_data + + def on_extern_block_end(self, state: AWExternBlockState) -> None: + pass + + def on_namespace_start(self, state: AWNamespaceBlockState) -> None: + parent_ns = state.user_data + names = state.namespace.names + if not names: + # anonymous namespace items are referenced using the parent + ns = parent_ns + else: + ns = f"{parent_ns}::{'::'.join(names)}" + + state.user_data = ns + + def on_namespace_end(self, state: AWNamespaceBlockState) -> None: + pass + + def on_namespace_alias(self, state: AWState, alias: NamespaceAlias) -> None: + # TODO: add to some sort of resolver? + pass + + def on_forward_decl(self, state: AWState, fdecl: ForwardDecl) -> None: + # TODO: add to some sort of resolver? + pass + + def on_template_inst(self, state: AWState, inst: TemplateInst) -> None: + pass + + def on_variable(self, state: AWState, v: Variable) -> None: + # TODO: robotpy-build doesn't wrap global variables at this time + pass + + def on_function(self, state: AWState, fn: Function) -> None: + pass # TODO + + def on_method_impl(self, state: AWState, method: Method) -> None: + pass + + def on_typedef(self, state: AWState, typedef: Typedef) -> None: + pass + + def on_using_namespace(self, state: AWState, namespace: typing.List[str]) -> None: + pass + + def on_using_alias(self, state: AWState, using: UsingAlias) -> None: + pass + # if using.access == "public" + # self._add_type_caster(u["raw_type"]) + + def on_using_declaration(self, state: AWState, using: UsingDecl) -> None: + if using.access is None: + self.hctx.using_declarations.append(using.typename) + else: + # TODO in a class + pass + + # for _, u in cls["using"].items(): + # self._add_type_caster(u["raw_type"]) + + # + # Enums + # + + def on_enum(self, state: AWState, enum: EnumDecl) -> None: + + # If the name has no components, its unnamed + # If it has more than one component, they are part of its full name + ename = "::".join(map(str, enum.typename.segments)) + + user_data = state.user_data + + if isinstance(user_data, str): + # global enum + + enum_data = self.gendata.get_enum_data(ename) + if enum_data.ignore: + return + + scope_var = self._get_module_var(enum_data) + + ctxlist = self.hctx.enums + var_name = f"enum{len(ctxlist)}" + value_scope = namespace + + # self.hctx.enums.append( + # self._enum_hook(en["namespace"], scope_var, var_name, en, enum_data) + # ) + else: + # per-class + if enum.access != "public": + return + + # cls_key needed for prop data + # ... I think this is just the full cpp name without the namespace + enum_data = self.gendata.get_cls_enum_data( + ename, user_data.cls_key, user_data.data + ) + if enum_data.ignore: + return + + cls_ctx = user_data.ctx + + scope = f"{cls_ctx.full_cpp_name}::" + + if ename: + ctxlist = cls_ctx.enums + var_name = f"{cls_ctx.var_name}_enum{len(ctxlist)}" + else: + ctxlist = cls_ctx.unnamed_enums + var_name = f"{cls_ctx.var_name}_enum_u{len(ctxlist)}" + + value_prefix = None + strip_prefixes = [] + values: typing.List[EnumeratorContext] = [] + + # TODO: THIS ISN'T RIGHT YET, NEED VALUE_SCOPE INIT PROPERLY + py_name = "" + full_cpp_name = "" + value_scope = scope + + if ename: + full_cpp_name = f"{scope}{ename}" + value_scope = f"{full_cpp_name}::" + py_name = self._make_py_name(ename, enum_data) + + value_prefix = enum_data.value_prefix + if not value_prefix: + value_prefix = ename + + strip_prefixes = [f"{value_prefix}_", value_prefix] + + for v in enum.values: + name = v.name + v_data = enum_data.values.get(name, _default_enum_value) + if v_data.ignore: + continue + + values.append( + EnumeratorContext( + full_cpp_name=f"{value_scope}{name}", + py_name=self._make_py_name(name, v_data, strip_prefixes), + doc=self._process_doc(v.doxygen, v_data, append_prefix=" "), + ) + ) + + ctxlist.append( + EnumContext( + scope_var=scope_var, + var_name=var_name, + full_cpp_name=full_cpp_name, + py_name=py_name, + values=values, + doc=self._process_doc(enum.doxygen, enum_data), + arithmetic=enum_data.arithmetic, + inline_code=enum_data.inline_code, + ) + ) + + # + # Class/union/struct + # + + def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]: + # parse everything there is to parse about the declaration + # of this class, and append it + + # ignore non-public nested classes or classes that have ignored parents + if state.typedef or ( + isinstance(state.parent, ClassBlockState) + and state.parent.access != "public" + ): + return False + + cls_key, cls_name, cls_namespace, parent_ctx = self._get_cls_key_name_and_ns( + state + ) + class_data = self.gendata.get_class_data(cls_key) + + # Ignore explicitly ignored classes + if class_data.ignore: + return False + + for typename in class_data.force_type_casters: + self._add_type_caster(typename) + + var_name = f"cls_{cls_name}" + + # No template stuff + simple_cls_qualname = f"{cls_namespace}::{cls_name}" + + # Template stuff + if parent_ctx: + cls_qualname = f"{parent_ctx.full_cpp_name}::{cls_name}" + scope_var = parent_ctx.var_name + else: + cls_qualname = simple_cls_qualname + scope_var = self._get_module_var(class_data) + + cls_cpp_identifier = cls_qualname.translate(_qualname_trans) + + # + # Process inheritance + # + + pybase_params = set() + bases: typing.List[BaseClassData] = [] + ignored_bases = {ib: True for ib in class_data.ignored_bases} + + # ignored_bases includes specializations + + for base in state.class_decl.bases: + if ignored_bases.pop(base["class"], None) or base["access"] == "private": + continue + + bqual = class_data.base_qualnames.get(base["decl_name"]) + if bqual: + full_cpp_name_w_templates = bqual + # TODO: sometimes need to add this to pybase_params, but + # that would require parsing this more. Seems sufficiently + # obscure, going to omit it for now. + tp = bqual.find("<") + if tp == -1: + base_full_cpp_name = bqual + template_params = "" + else: + base_full_cpp_name = bqual[:tp] + template_params = bqual[tp + 1 : -1] + else: + if "::" not in base["decl_name"]: + base_full_cpp_name = f'{cls_namespace}::{base["decl_name"]}' + else: + base_full_cpp_name = base["decl_name"] + + base_decl_params = base.get("decl_params") + if base_decl_params: + template_params = self._make_base_params( + base_decl_params, pybase_params + ) + full_cpp_name_w_templates = ( + f"{base_full_cpp_name}<{template_params}>" + ) + else: + template_params = "" + full_cpp_name_w_templates = base_full_cpp_name + + base_identifier = base_full_cpp_name.translate(_qualname_trans) + + bases.append( + BaseClassData( + full_cpp_name=base_full_cpp_name, + full_cpp_name_w_templates=full_cpp_name_w_templates, + full_cpp_name_identifier=base_identifier, + template_params=template_params, + ) + ) + + if not self.report_only and ignored_bases: + bases = ", ".join(str(base.typename) for base in state.class_decl.bases) + invalid_bases = ", ".join(ignored_bases.keys()) + raise ValueError( + f"{cls_name}: ignored_bases contains non-existant bases " + + f"{invalid_bases}; valid bases are {bases}" + ) + + self.hctx.class_hierarchy[simple_cls_qualname] = [ + base.full_cpp_name for base in bases + ] + class_data.force_depends + + # + # Process template parameters + # + + # + template_argument_list = "" + # + template_parameter_list = "" + + template_data: typing.Optional[ClassTemplateData] = None + + if class_data.template_params: + if class_data.subpackage: + raise ValueError( + f"{cls_name}: classes with subpackages must define subpackage on template instantiation" + ) + + template_args = [] + template_params = [] + + base_template_args = [] + base_template_params = [] + + for param in class_data.template_params: + if " " in param: + arg = param.split(" ", 1)[1] + else: + arg = param + param = f"typename {param}" + + template_args.append(arg) + template_params.append(param) + + if arg in pybase_params: + base_template_args.append(arg) + base_template_params.append(param) + + template_argument_list = ", ".join(template_args) + template_parameter_list = ", ".join(template_params) + + template_data = ClassTemplateData( + parameter_list=template_parameter_list, + inline_code=class_data.template_inline_code, + ) + + cls_qualname = f"{cls_qualname}<{template_argument_list}>" + else: + base_template_params = None + base_template_args = None + + if not self.report_only: + if "template" in cls: + if template_parameter_list == "": + raise ValueError( + f"{cls_name}: must specify template_params for templated class, or ignore it" + ) + else: + if template_parameter_list != "": + raise ValueError( + f"{cls_name}: cannot specify template_params for non-template class" + ) + + doc = self._process_doc(class_decl.doxygen, class_data) + py_name = self._make_py_name(cls_name, class_data) + + constants: typing.List[typing.Tuple[str, str]] = [] + for constant in class_data.constants: + name = constant.split("::")[-1] + constants.append((name, constant)) + + ctx = ClassContext( + parent=parent, + namespace=cls_namespace, + cpp_name=cpp_name, + full_cpp_name=full_cpp_name, + py_name=py_name, + scope_var=scope_var, + var_name=var_name, + nodelete=class_data.nodelete, + final=state.class_decl.final, + doc=doc, + bases=bases, + template=template_data, + user_typealias=user_typealias, + constants=constants, + inline_code=class_data.inline_code, + ) + state.user_data = ClassStateData(ctx=ctx, cls_key=cls_key, data=class_data) + + def _get_cls_key_name_and_ns( + self, state: AWClassBlockState + ) -> typing.Tuple[str, str, str, typing.Optional[ClassContext]]: + class_decl = state.class_decl + segments = class_decl.typename.segments + assert len(segments) > 0 + + name_segment = segments[-1] + if not isinstance(name_segment, NameSpecifier): + raise ValueError(f"not sure how to handle '{class_decl.typename}'") + + cls_name = name_segment.name + + # for now, don't support these? + other_segments = segments[:-1] + if other_segments: + raise ValueError( + f"not sure what to do with compound name '{class_decl.typename}'" + ) + + parent_ctx: typing.Optional[ClassContext] = None + + if not isinstance(state.parent, ClassBlockState): + # easy case -- namespace is the next user_data up + cls_key = cls_name + cls_namespace = typing.cast(str, state.parent.user_data) + else: + # Use things the parent already computed + parent: AWClassBlockState = state.parent + parent_ctx = parent.user_data.ctx + # the parent context already computed namespace, so use that + cls_key = f"{parent.user_data.cls_key}::{cls_name}" + cls_namespace = parent_ctx.namespace + + return cls_key, cls_name, cls_namespace, parent_ctx + + def _on_class_templates(self): + pass + + def on_class_field(self, state: AWClassBlockState, f: Field) -> None: + # cannot bind without a trampoline - but we can't know if we need one + # until it's done, so defer it? + pass + + def on_class_method(self, state: AWClassBlockState, method: Method) -> None: + pass + + def on_class_friend(self, state: AWClassBlockState, friend: FriendDecl) -> None: + pass + + def on_class_end(self, state: AWClassBlockState) -> None: + # post-process the class data + pass + + # + # Utility methods + # + + def _add_type_caster(self, typename: str): + # defer until the end since there's lots of duplication + self.types.add(typename) + + def _get_module_var(self, data: HasSubpackage) -> str: + if data.subpackage: + var = f"pkg_{data.subpackage.replace('.', '_')}" + self.hctx.subpackages[data.subpackage] = var + return var + + return "m" + + def _make_py_name( + self, + name: str, + data: HasNameData, + strip_prefixes: typing.Optional[typing.List[str]] = None, + is_operator: bool = False, + ): + if data.rename: + return data.rename + + if strip_prefixes is None: + strip_prefixes = self.rawdata.strip_prefixes + + if strip_prefixes: + for pfx in strip_prefixes: + if name.startswith(pfx): + n = name[len(pfx) :] + if n.isidentifier(): + name = n + break + + if iskeyword(name): + return f"{name}_" + if not name.isidentifier() and not is_operator: + if not self.report_only: + raise ValueError(f"name {name!r} is not a valid identifier") + + return name + + def _process_doc( + self, + doxygen: typing.Optional[str], + data: HasDoc, + append_prefix: str = "", + param_remap: typing.Dict[str, str] = {}, + ) -> Documentation: + doc = "" + + if data.doc is not None: + doc = data.doc + elif doxygen: + doc = doxygen + if param_remap: + d = sphinxify.Doc.from_comment(doc) + for param in d.params: + new_name = param_remap.get(param.name) + if new_name: + param.name = new_name + doc = str(d) + else: + doc = sphinxify.process_raw(doc) + + if data.doc_append is not None: + doc += f"\n{append_prefix}" + data.doc_append.replace( + "\n", f"\n{append_prefix}" + ) + + return self._quote_doc(doc) + + def _quote_doc(self, doc: typing.Optional[str]) -> Documentation: + doc_quoted: Documentation = None + if doc: + # TODO + doc = doc.replace("\\", "\\\\").replace('"', '\\"') + doc_quoted = doc.splitlines(keepends=True) + doc_quoted = ['"%s"' % (dq.replace("\n", "\\n"),) for dq in doc_quoted] + + return doc_quoted + + +def parse_header( + header_path: pathlib.Path, + user_cfg: AutowrapConfigYaml, + parser_options: ParserOptions, +) -> HeaderContext: + + # defines, include_paths need to be set the parent + # and its probably the same for every header, so we accept + # parser options here.. + + # TODO: should add encoding to user configuration? + + with open(header_path, encoding="utf-8-sig") as fp: + content = fp.read() + + # Initialize the header context with user configuration + hctx = HeaderContext( + hname=header_path.name, + extra_includes_first=user_cfg.extra_includes_first, + extra_includes=user_cfg.extra_includes, + inline_code=user_cfg.inline_code, + trampoline_signature=trampoline_signature, + using_signature=_using_signature, + ) + + # Why not just use the simple parser? + # . seems more performant to not use it + # . is there a way I could use it but not build the data structure + + visitor = AutowrapVisitor(hctx) + parser = CxxParser(str(header_path), content, visitor, parser_options) + parser.parse() + + # post-process per-header things + # - user typealias + # - type caster includes + + # missing reporter + + return hctx + + +if __name__ == "__main__": + # eventually wanted meson to manage the autogeneration, but + # that isn't possible because we have no idea what the + # individual dependencies are and meson needs to know before + # we do it + # + # .. so hopefully this is just fast enough and it won't matter + + # report only + # casters? hm. + pass diff --git a/robotpy_build/autowrap/hooks.py b/robotpy_build/autowrap/hooks.py index 010a357c..7e36c13f 100644 --- a/robotpy_build/autowrap/hooks.py +++ b/robotpy_build/autowrap/hooks.py @@ -41,44 +41,34 @@ ) -# TODO: this isn't the best solution -def _gen_int_types(): - for i in ("int", "uint"): - for j in ("", "_fast", "_least"): - for k in ("8", "16", "32", "64"): - yield f"{i}{j}{k}_t" - yield "intmax_t" - yield "uintmax_t" +# class HasSubpackage(Protocol): +# subpackage: str +# _rvp_map = { +# ReturnValuePolicy.TAKE_OWNERSHIP: ", py::return_value_policy::take_ownership", +# ReturnValuePolicy.COPY: ", py::return_value_policy::copy", +# ReturnValuePolicy.MOVE: ", py::return_value_policy::move", +# ReturnValuePolicy.REFERENCE: ", py::return_value_policy::reference", +# ReturnValuePolicy.REFERENCE_INTERNAL: ", py::return_value_policy::reference_internal", +# ReturnValuePolicy.AUTOMATIC: "", +# ReturnValuePolicy.AUTOMATIC_REFERENCE: ", py::return_value_policy::automatic_reference", +# } -_int32_types = frozenset(_gen_int_types()) +# # fmt: off +# _operators = { +# # binary +# "-", "+", "*", "/", "%", "&", "^", "==", "!=", "|", ">", ">=", "<", "<=", +# # inplace +# "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", +# } +# # fmt: on +# _type_caster_seps = re.compile(r"[<>\(\)]") -_rvp_map = { - ReturnValuePolicy.TAKE_OWNERSHIP: ", py::return_value_policy::take_ownership", - ReturnValuePolicy.COPY: ", py::return_value_policy::copy", - ReturnValuePolicy.MOVE: ", py::return_value_policy::move", - ReturnValuePolicy.REFERENCE: ", py::return_value_policy::reference", - ReturnValuePolicy.REFERENCE_INTERNAL: ", py::return_value_policy::reference_internal", - ReturnValuePolicy.AUTOMATIC: "", - ReturnValuePolicy.AUTOMATIC_REFERENCE: ", py::return_value_policy::automatic_reference", -} +# _lambda_predent = " " -# fmt: off -_operators = { - # binary - "-", "+", "*", "/", "%", "&", "^", "==", "!=", "|", ">", ">=", "<", "<=", - # inplace - "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", -} -# fmt: on - -_type_caster_seps = re.compile(r"[<>\(\)]") - -_lambda_predent = " " - -_default_param_data = ParamData() -_default_enum_value = EnumValue() +# _default_param_data = ParamData() +# _default_enum_value = EnumValue() @dataclass @@ -90,17 +80,17 @@ class _ReturnParamContext: cpp_retname: str -class HasSubpackage(Protocol): - subpackage: str +# class HasSubpackage(Protocol): +# subpackage: str -class HasDoc(Protocol): - doc: str - doc_append: str +# class HasDoc(Protocol): +# doc: str +# doc_append: str -class HasNameData(Protocol): - rename: str +# class HasNameData(Protocol): +# rename: str class HookError(Exception): @@ -149,13 +139,13 @@ def _add_type_caster(self, typename: str): # defer until the end since there's lots of duplication self.types.add(typename) - def _get_module_var(self, data: HasSubpackage) -> str: - if data.subpackage: - var = f"pkg_{data.subpackage.replace('.', '_')}" - self.hctx.subpackages[data.subpackage] = var - return var + # def _get_module_var(self, data: HasSubpackage) -> str: + # if data.subpackage: + # var = f"pkg_{data.subpackage.replace('.', '_')}" + # self.hctx.subpackages[data.subpackage] = var + # return var - return "m" + # return "m" def _get_type_caster_cfgs(self, typename: str): tmpl_idx = typename.find("<") @@ -207,45 +197,45 @@ def _make_py_name( return name - def _process_doc( - self, - thing, - data: HasDoc, - append_prefix="", - param_remap: typing.Dict[str, str] = {}, - ) -> Documentation: - doc = "" - - if data.doc is not None: - doc = data.doc - elif "doxygen" in thing: - doc = thing["doxygen"] - if param_remap: - d = sphinxify.Doc.from_comment(doc) - for param in d.params: - new_name = param_remap.get(param.name) - if new_name: - param.name = new_name - doc = str(d) - else: - doc = sphinxify.process_raw(doc) - - if data.doc_append is not None: - doc += f"\n{append_prefix}" + data.doc_append.replace( - "\n", f"\n{append_prefix}" - ) - - return self._quote_doc(doc) - - def _quote_doc(self, doc: typing.Optional[str]) -> Documentation: - doc_quoted: Documentation = None - if doc: - # TODO - doc = doc.replace("\\", "\\\\").replace('"', '\\"') - doc_quoted = doc.splitlines(keepends=True) - doc_quoted = ['"%s"' % (dq.replace("\n", "\\n"),) for dq in doc_quoted] - - return doc_quoted + # def _process_doc( + # self, + # thing, + # data: HasDoc, + # append_prefix="", + # param_remap: typing.Dict[str, str] = {}, + # ) -> Documentation: + # doc = "" + + # if data.doc is not None: + # doc = data.doc + # elif "doxygen" in thing: + # doc = thing["doxygen"] + # if param_remap: + # d = sphinxify.Doc.from_comment(doc) + # for param in d.params: + # new_name = param_remap.get(param.name) + # if new_name: + # param.name = new_name + # doc = str(d) + # else: + # doc = sphinxify.process_raw(doc) + + # if data.doc_append is not None: + # doc += f"\n{append_prefix}" + data.doc_append.replace( + # "\n", f"\n{append_prefix}" + # ) + + # return self._quote_doc(doc) + + # def _quote_doc(self, doc: typing.Optional[str]) -> Documentation: + # doc_quoted: Documentation = None + # if doc: + # # TODO + # doc = doc.replace("\\", "\\\\").replace('"', '\\"') + # doc_quoted = doc.splitlines(keepends=True) + # doc_quoted = ['"%s"' % (dq.replace("\n", "\\n"),) for dq in doc_quoted] + + # return doc_quoted def _resolve_default(self, fn, p, name, cpp_type) -> str: if isinstance(name, (int, float)): @@ -333,55 +323,6 @@ def _extract_typealias( out_ta.append(f"using {ta_name} = {typealias}") ta_names.add(ta_name) - def _enum_hook( - self, cpp_scope: str, scope_var: str, var_name: str, en, enum_data: EnumData - ) -> EnumContext: - value_prefix = None - strip_prefixes = [] - values: typing.List[EnumeratorContext] = [] - - py_name = "" - full_cpp_name = "" - value_scope = cpp_scope - - ename = en.get("name", "") - - if ename: - full_cpp_name = f"{cpp_scope}{ename}" - value_scope = f"{full_cpp_name}::" - py_name = self._make_py_name(ename, enum_data) - - value_prefix = enum_data.value_prefix - if not value_prefix: - value_prefix = ename - - strip_prefixes = [f"{value_prefix}_", value_prefix] - - for v in en["values"]: - name = v["name"] - v_data = enum_data.values.get(name, _default_enum_value) - if v_data.ignore: - continue - - values.append( - EnumeratorContext( - full_cpp_name=f"{value_scope}{name}", - py_name=self._make_py_name(name, v_data, strip_prefixes), - doc=self._process_doc(v, v_data, append_prefix=" "), - ) - ) - - return EnumContext( - scope_var=scope_var, - var_name=var_name, - full_cpp_name=full_cpp_name, - py_name=py_name, - values=values, - doc=self._process_doc(en, enum_data), - arithmetic=enum_data.arithmetic, - inline_code=enum_data.inline_code, - ) - def header_hook(self, header, data): """Called for each header""" @@ -874,183 +815,183 @@ def function_hook(self, fn, h2w_data): self.hctx.functions.append(fctx) def class_hook(self, cls, h2w_data): - # ignore private classes - if cls["parent"] is not None and cls["access_in_parent"] == "private": - return + # # ignore private classes + # if cls["parent"] is not None and cls["access_in_parent"] == "private": + # return - cls_name = cls["name"] - cls_key = cls_name - c = cls - while c["parent"]: - c = c["parent"] - cls_key = c["name"] + "::" + cls_key + # cls_name = cls["name"] + # cls_key = cls_name + # c = cls + # while c["parent"]: + # c = c["parent"] + # cls_key = c["name"] + "::" + cls_key - class_data = self.gendata.get_class_data(cls_key) + # class_data = self.gendata.get_class_data(cls_key) - if class_data.ignore: - return + # if class_data.ignore: + # return - for _, u in cls["using"].items(): - self._add_type_caster(u["raw_type"]) + # for _, u in cls["using"].items(): + # self._add_type_caster(u["raw_type"]) - for typename in class_data.force_type_casters: - self._add_type_caster(typename) + # for typename in class_data.force_type_casters: + # self._add_type_caster(typename) - scope_var = self._get_module_var(class_data) - var_name = f"cls_{cls_name}" + # scope_var = self._get_module_var(class_data) + # var_name = f"cls_{cls_name}" - # No template stuff - simple_cls_qualname = f'{cls["namespace"]}::{cls_name}' + # # No template stuff + # simple_cls_qualname = f'{cls["namespace"]}::{cls_name}' - # Template stuff - parent_ctx: typing.Optional[ClassContext] = None - if cls["parent"]: - parent_ctx: ClassContext = cls["parent"]["class_ctx"] - cls_qualname = f"{parent_ctx.full_cpp_name}::{cls_name}" - scope_var = parent_ctx.var_name - else: - cls_qualname = simple_cls_qualname + # # Template stuff + # parent_ctx: typing.Optional[ClassContext] = None + # if cls["parent"]: + # parent_ctx: ClassContext = cls["parent"]["class_ctx"] + # cls_qualname = f"{parent_ctx.full_cpp_name}::{cls_name}" + # scope_var = parent_ctx.var_name + # else: + # cls_qualname = simple_cls_qualname - cls_cpp_identifier = cls_qualname.translate(self._qualname_trans) + # cls_cpp_identifier = cls_qualname.translate(self._qualname_trans) enums: typing.List[EnumContext] = [] unnamed_enums: typing.List[EnumContext] = [] # fix enum paths - for i, e in enumerate(cls["enums"]["public"]): - enum_data = self.gendata.get_cls_enum_data( - e.get("name"), cls_key, class_data - ) - if not enum_data.ignore: - scope = f"{cls_qualname}::" - enum_var_name = f"{var_name}_enum{i}" - ectx = self._enum_hook(scope, var_name, enum_var_name, e, enum_data) - if ectx.full_cpp_name: - enums.append(ectx) - else: - unnamed_enums.append(ectx) - - # update inheritance - - pybase_params = set() - bases: typing.List[BaseClassData] = [] - ignored_bases = {ib: True for ib in class_data.ignored_bases} - - for base in cls["inherits"]: - if ignored_bases.pop(base["class"], None) or base["access"] == "private": - continue - - bqual = class_data.base_qualnames.get(base["decl_name"]) - if bqual: - full_cpp_name_w_templates = bqual - # TODO: sometimes need to add this to pybase_params, but - # that would require parsing this more. Seems sufficiently - # obscure, going to omit it for now. - tp = bqual.find("<") - if tp == -1: - base_full_cpp_name = bqual - template_params = "" - else: - base_full_cpp_name = bqual[:tp] - template_params = bqual[tp + 1 : -1] - else: - if "::" not in base["decl_name"]: - base_full_cpp_name = f'{cls["namespace"]}::{base["decl_name"]}' - else: - base_full_cpp_name = base["decl_name"] - - base_decl_params = base.get("decl_params") - if base_decl_params: - template_params = self._make_base_params( - base_decl_params, pybase_params - ) - full_cpp_name_w_templates = ( - f"{base_full_cpp_name}<{template_params}>" - ) - else: - template_params = "" - full_cpp_name_w_templates = base_full_cpp_name - - base_identifier = base_full_cpp_name.translate(self._qualname_trans) - - bases.append( - BaseClassData( - full_cpp_name=base_full_cpp_name, - full_cpp_name_w_templates=full_cpp_name_w_templates, - full_cpp_name_identifier=base_identifier, - template_params=template_params, - ) - ) - - if not self.report_only and ignored_bases: - bases = ", ".join(base["class"] for base in cls["inherits"]) - invalid_bases = ", ".join(ignored_bases.keys()) - raise ValueError( - f"{cls_name}: ignored_bases contains non-existant bases " - + f"{invalid_bases}; valid bases are {bases}" - ) - - self.hctx.class_hierarchy[simple_cls_qualname] = [ - base.full_cpp_name for base in bases - ] + class_data.force_depends - - # - template_argument_list = "" - # - template_parameter_list = "" - - template_data: typing.Optional[ClassTemplateData] = None - - if class_data.template_params: - if class_data.subpackage: - raise ValueError( - f"{cls_name}: classes with subpackages must define subpackage on template instantiation" - ) - - template_args = [] - template_params = [] - - base_template_args = [] - base_template_params = [] - - for param in class_data.template_params: - if " " in param: - arg = param.split(" ", 1)[1] - else: - arg = param - param = f"typename {param}" - - template_args.append(arg) - template_params.append(param) - - if arg in pybase_params: - base_template_args.append(arg) - base_template_params.append(param) - - template_argument_list = ", ".join(template_args) - template_parameter_list = ", ".join(template_params) - - template_data = ClassTemplateData( - parameter_list=template_parameter_list, - inline_code=class_data.template_inline_code, - ) - - cls_qualname = f"{cls_qualname}<{template_argument_list}>" - else: - base_template_params = None - base_template_args = None - - if not self.report_only: - if "template" in cls: - if template_parameter_list == "": - raise ValueError( - f"{cls_name}: must specify template_params for templated class, or ignore it" - ) - else: - if template_parameter_list != "": - raise ValueError( - f"{cls_name}: cannot specify template_params for non-template class" - ) + # for i, e in enumerate(cls["enums"]["public"]): + # enum_data = self.gendata.get_cls_enum_data( + # e.get("name"), cls_key, class_data + # ) + # if not enum_data.ignore: + # scope = f"{cls_qualname}::" + # enum_var_name = f"{var_name}_enum{i}" + # ectx = self._enum_hook(scope, var_name, enum_var_name, e, enum_data) + # if ectx.full_cpp_name: + # enums.append(ectx) + # else: + # unnamed_enums.append(ectx) + + # # update inheritance + + # pybase_params = set() + # bases: typing.List[BaseClassData] = [] + # ignored_bases = {ib: True for ib in class_data.ignored_bases} + + # for base in cls["inherits"]: + # if ignored_bases.pop(base["class"], None) or base["access"] == "private": + # continue + + # bqual = class_data.base_qualnames.get(base["decl_name"]) + # if bqual: + # full_cpp_name_w_templates = bqual + # # TODO: sometimes need to add this to pybase_params, but + # # that would require parsing this more. Seems sufficiently + # # obscure, going to omit it for now. + # tp = bqual.find("<") + # if tp == -1: + # base_full_cpp_name = bqual + # template_params = "" + # else: + # base_full_cpp_name = bqual[:tp] + # template_params = bqual[tp + 1 : -1] + # else: + # if "::" not in base["decl_name"]: + # base_full_cpp_name = f'{cls["namespace"]}::{base["decl_name"]}' + # else: + # base_full_cpp_name = base["decl_name"] + + # base_decl_params = base.get("decl_params") + # if base_decl_params: + # template_params = self._make_base_params( + # base_decl_params, pybase_params + # ) + # full_cpp_name_w_templates = ( + # f"{base_full_cpp_name}<{template_params}>" + # ) + # else: + # template_params = "" + # full_cpp_name_w_templates = base_full_cpp_name + + # base_identifier = base_full_cpp_name.translate(self._qualname_trans) + + # bases.append( + # BaseClassData( + # full_cpp_name=base_full_cpp_name, + # full_cpp_name_w_templates=full_cpp_name_w_templates, + # full_cpp_name_identifier=base_identifier, + # template_params=template_params, + # ) + # ) + + # if not self.report_only and ignored_bases: + # bases = ", ".join(base["class"] for base in cls["inherits"]) + # invalid_bases = ", ".join(ignored_bases.keys()) + # raise ValueError( + # f"{cls_name}: ignored_bases contains non-existant bases " + # + f"{invalid_bases}; valid bases are {bases}" + # ) + + # self.hctx.class_hierarchy[simple_cls_qualname] = [ + # base.full_cpp_name for base in bases + # ] + class_data.force_depends + + # # + # template_argument_list = "" + # # + # template_parameter_list = "" + + # template_data: typing.Optional[ClassTemplateData] = None + + # if class_data.template_params: + # if class_data.subpackage: + # raise ValueError( + # f"{cls_name}: classes with subpackages must define subpackage on template instantiation" + # ) + + # template_args = [] + # template_params = [] + + # base_template_args = [] + # base_template_params = [] + + # for param in class_data.template_params: + # if " " in param: + # arg = param.split(" ", 1)[1] + # else: + # arg = param + # param = f"typename {param}" + + # template_args.append(arg) + # template_params.append(param) + + # if arg in pybase_params: + # base_template_args.append(arg) + # base_template_params.append(param) + + # template_argument_list = ", ".join(template_args) + # template_parameter_list = ", ".join(template_params) + + # template_data = ClassTemplateData( + # parameter_list=template_parameter_list, + # inline_code=class_data.template_inline_code, + # ) + + # cls_qualname = f"{cls_qualname}<{template_argument_list}>" + # else: + # base_template_params = None + # base_template_args = None + + # if not self.report_only: + # if "template" in cls: + # if template_parameter_list == "": + # raise ValueError( + # f"{cls_name}: must specify template_params for templated class, or ignore it" + # ) + # else: + # if template_parameter_list != "": + # raise ValueError( + # f"{cls_name}: cannot specify template_params for non-template class" + # ) has_constructor = False is_polymorphic = class_data.is_polymorphic diff --git a/robotpy_build/autowrap/j2_context.py b/robotpy_build/autowrap/j2_context.py index ba61bd49..a4ce59e8 100644 --- a/robotpy_build/autowrap/j2_context.py +++ b/robotpy_build/autowrap/j2_context.py @@ -12,6 +12,8 @@ from dataclasses import dataclass, field import typing +from cxxheaderparser.types import PQName + from ..config.autowrap_yml import ReturnValuePolicy @@ -349,7 +351,7 @@ class ClassContext: parent: typing.Optional["ClassContext"] #: Namespace that this class lives in - namespace: typing.Optional[str] + namespace: str #: C++ name (only the class) cpp_name: str @@ -382,15 +384,39 @@ class ClassContext: bases: typing.List[BaseClassData] + template: typing.Optional[ClassTemplateData] + + # + # User specified settings copied from ClassData + # + + #: Extra 'using' directives to insert into the trampoline and the + #: wrapping scope + user_typealias: typing.List[str] + + #: Extra constexpr to insert into the trampoline and wrapping scopes + #: (name, value) + constants: typing.List[typing.Tuple[str, str]] + + #: Extra code to insert into the class scope + inline_code: str + + #: User specified settings + force_multiple_inheritance: bool + + # + # Everything else + # + #: was x_has_trampoline - trampoline: typing.Optional[TrampolineData] + trampoline: typing.Optional[TrampolineData] = None # # Properties (member variables) # - public_properties: typing.List[PropContext] - protected_properties: typing.List[PropContext] + public_properties: typing.List[PropContext] = field(default_factory=list) + protected_properties: typing.List[PropContext] = field(default_factory=list) # # Methods: the idea here is have a bunch of descriptive lists here so that @@ -401,45 +427,27 @@ class ClassContext: # Method lists for wrapping # - add_default_constructor: bool + add_default_constructor: bool = False # public + not (ignore_pure + ignore_py) - wrapped_public_methods: typing.List[FunctionContext] + wrapped_public_methods: typing.List[FunctionContext] = field(default_factory=list) # only if trampoline: # - protected + not (ignore_pure + ignore_py) - wrapped_protected_methods: typing.List[FunctionContext] + wrapped_protected_methods: typing.List[FunctionContext] = field( + default_factory=list + ) #: Public enums + unnamed enums - enums: typing.List[EnumContext] - unnamed_enums: typing.List[EnumContext] - - template: typing.Optional[ClassTemplateData] + enums: typing.List[EnumContext] = field(default_factory=list) + unnamed_enums: typing.List[EnumContext] = field(default_factory=list) #: Extra autodetected 'using' directives - auto_typealias: typing.List[str] + auto_typealias: typing.List[str] = field(default_factory=list) #: vcheck are various static asserts that check things about the #: inline functions - vcheck_fns: typing.List[FunctionContext] - - # - # User specified settings copied from ClassData - # - - #: Extra 'using' directives to insert into the trampoline and the - #: wrapping scope - user_typealias: typing.List[str] - - #: Extra constexpr to insert into the trampoline and wrapping scopes - #: (name, value) - constants: typing.List[typing.Tuple[str, str]] - - #: Extra code to insert into the class scope - inline_code: str - - #: User specified settings - force_multiple_inheritance: bool + vcheck_fns: typing.List[FunctionContext] = field(default_factory=list) child_classes: typing.List["ClassContext"] = field(default_factory=list) @@ -486,7 +494,7 @@ class HeaderContext: #: True if is needed need_operators_h: bool = False - using_declarations: typing.List[str] = field(default_factory=list) + using_declarations: typing.List[PQName] = field(default_factory=list) # TODO: anon enums? enums: typing.List[EnumContext] = field(default_factory=list) diff --git a/robotpy_build/generator_data.py b/robotpy_build/generator_data.py index 3f3dc1de..c0422bee 100644 --- a/robotpy_build/generator_data.py +++ b/robotpy_build/generator_data.py @@ -51,6 +51,9 @@ def __init__(self, data: AutowrapConfigYaml): self.attributes: AttrMissingData = {} def get_class_data(self, name: str) -> ClassData: + """ + The 'name' is [parent_class::]class_name + """ data = self.data.classes.get(name) missing = data is None if missing: