Skip to content
Draft
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
27 changes: 26 additions & 1 deletion src/semiwrap/autowrap/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class EnumeratorContext:
#: Documentation
doc: Documentation

#: Marked deprecated in C++
deprecated: bool


@dataclass
class EnumContext:
Expand Down Expand Up @@ -82,6 +85,9 @@ class EnumContext:
#: Documentation
doc: Documentation

#: Marked deprecated in C++
deprecated: bool

#
# Copied from user's EnumData
#
Expand Down Expand Up @@ -153,6 +159,12 @@ def cpp_type_no_const(self) -> str:
return ct


@dataclass
class GeneratedTypeAlias:
declaration: str
deprecated: bool = False


@dataclass
class GeneratedLambda:
"""
Expand Down Expand Up @@ -190,6 +202,9 @@ class FunctionContext:
#: Documentation
doc: Documentation

#: Marked deprecated in C++
deprecated: bool

#: parent variable to attach to
scope_var: str

Expand Down Expand Up @@ -262,6 +277,9 @@ class FunctionContext:

genlambda: typing.Optional[GeneratedLambda] = None

#: True when cpp_code contains semiwrap-generated py::self operator code
generated_operator_cpp_code: bool = False

#: Is this a constructor?
is_constructor: bool = False

Expand Down Expand Up @@ -300,6 +318,7 @@ class PropContext:
cpp_type: str
readonly: bool
doc: Documentation
deprecated: bool

array_size: typing.Optional[int]
array: bool # cannot sensibly autowrap an array of incomplete size
Expand Down Expand Up @@ -437,6 +456,9 @@ class ClassContext:
#: Documentation
doc: Documentation

#: Marked deprecated in C++
deprecated: bool

bases: typing.List[BaseClassData]

template: typing.Optional[ClassTemplateData]
Expand Down Expand Up @@ -502,7 +524,7 @@ class ClassContext:
unnamed_enums: typing.List[EnumContext] = field(default_factory=list)

#: Extra autodetected 'using' directives
auto_typealias: typing.List[str] = field(default_factory=list)
auto_typealias: typing.List[GeneratedTypeAlias] = field(default_factory=list)

#: vcheck are various static asserts that check things about the
#: inline functions
Expand Down Expand Up @@ -534,6 +556,9 @@ class TemplateInstanceContext:
doc_set: Documentation
doc_add: Documentation

#: Marked deprecated in C++
deprecated: bool = False

#: If true, instantiated in class order
matched: bool = False

Expand Down
117 changes: 110 additions & 7 deletions src/semiwrap/autowrap/cxxparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# suitable for use with the autowrap templates
#

import ast
import dataclasses
import pathlib
import re
Expand Down Expand Up @@ -30,6 +31,7 @@
from cxxheaderparser.types import (
AnonymousName,
Array,
Attribute,
ClassDecl,
Concept,
DecoratedType,
Expand Down Expand Up @@ -86,6 +88,7 @@
FnTemplateImpl,
FunctionContext,
GeneratedLambda,
GeneratedTypeAlias,
HeaderContext,
ParamCategory,
ParamContext,
Expand Down Expand Up @@ -289,6 +292,61 @@ def _fmt_array_size(t: Array) -> typing.Optional[int]:
return None


@dataclasses.dataclass(frozen=True)
class Deprecation:
message: typing.Optional[str]


def _is_deprecated_attribute_name(name: str) -> bool:
return name.rsplit("::", 1)[-1].strip("_") == "deprecated"


def _decode_deprecation_message(value: typing.Optional[Value]) -> typing.Optional[str]:
if value is None:
return None

message_tokens = []
depth = 0
for token in value.tokens:
if token.value in ("(", "[", "{"):
depth += 1
elif token.value in (")", "]", "}"):
depth -= 1
elif token.value == "," and depth == 0:
break
message_tokens.append(token)

message_parts = []
for token in message_tokens:
if not token.type.endswith("STRING_LITERAL"):
return None
match = re.fullmatch(r'(?:u8|u|U|L)?("(?:\\.|[^"\\])*")', token.value)
if match is None:
return None
try:
part = ast.literal_eval(match.group(1))
except (SyntaxError, ValueError):
return None
if not isinstance(part, str):
return None
message_parts.append(part)

return "".join(message_parts) if message_parts else None


def _get_deprecation(
attributes: typing.Sequence[Attribute],
) -> typing.Optional[Deprecation]:
deprecated = [a for a in attributes if _is_deprecated_attribute_name(a.name)]
if not deprecated:
return None
for attribute in deprecated:
message = _decode_deprecation_message(attribute.value)
if message:
return Deprecation(message)
return Deprecation(None)


@dataclasses.dataclass
class _ReturnParamContext:
#: was x_type
Expand Down Expand Up @@ -492,7 +550,10 @@ def on_using_alias(self, state: AWState, using: UsingAlias) -> None:
):
ctx = state.user_data.ctx
ctx.auto_typealias.append(
f"using {using.alias} [[maybe_unused]] = typename {ctx.full_cpp_name}::{using.alias}"
GeneratedTypeAlias(
f"using {using.alias} [[maybe_unused]] = typename "
f"{ctx.full_cpp_name}::{using.alias}"
)
)

def on_using_declaration(self, state: AWState, using: UsingDecl) -> None:
Expand Down Expand Up @@ -569,6 +630,7 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
enum_namespace = cls_ctx.namespace
scope_var = cls_ctx.var_name

deprecation = _get_deprecation(enum.attributes)
strip_prefixes: typing.List[str] = []
values: typing.List[EnumeratorContext] = []

Expand All @@ -594,6 +656,7 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
if v_data.ignore:
continue

value_deprecation = _get_deprecation(v.attributes)
values.append(
EnumeratorContext(
full_cpp_name=f"{full_cpp_name}::{name}",
Expand All @@ -603,7 +666,13 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
strip_prefixes,
name_transform=self.enum_value_name_transform,
),
doc=self._process_doc(v.doxygen, v_data, append_prefix=" "),
doc=self._process_doc(
v.doxygen,
v_data,
append_prefix=" ",
deprecation=value_deprecation,
),
deprecated=value_deprecation is not None,
)
)

Expand All @@ -616,7 +685,8 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
full_cpp_name=full_cpp_name,
py_name=py_name,
values=values,
doc=self._process_doc(enum.doxygen, enum_data),
doc=self._process_doc(enum.doxygen, enum_data, deprecation=deprecation),
deprecated=deprecation is not None,
arithmetic=enum_data.arithmetic,
inline_code=enum_data.inline_code,
)
Expand Down Expand Up @@ -766,7 +836,8 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]:
# bad assumption? probably
is_polymorphic = len(class_decl.bases) > 0

doc = self._process_doc(class_decl.doxygen, class_data)
deprecation = _get_deprecation(class_decl.attributes)
doc = self._process_doc(class_decl.doxygen, class_data, deprecation=deprecation)
py_name = self._make_py_name(cls_name, class_data)

constants: typing.List[typing.Tuple[str, str]] = []
Expand Down Expand Up @@ -794,6 +865,7 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]:
nodelete=class_data.nodelete,
final=class_decl.final,
doc=doc,
deprecated=deprecation is not None,
bases=bases,
template=template_data,
user_typealias=user_typealias,
Expand Down Expand Up @@ -1031,7 +1103,8 @@ def _on_class_field(
else:
prop_readonly = propdata.access == PropAccess.readonly

doc = self._process_doc(f.doxygen, propdata)
deprecation = _get_deprecation(f.attributes)
doc = self._process_doc(f.doxygen, propdata, deprecation=deprecation)

array_size = None
is_array = False
Expand All @@ -1050,6 +1123,7 @@ def _on_class_field(
cpp_type=cpp_type,
readonly=prop_readonly,
doc=doc,
deprecated=deprecation is not None,
array_size=array_size,
array=is_array,
reference=isinstance(f.type, Reference),
Expand All @@ -1062,7 +1136,11 @@ def _on_class_field(
if f.access == "public" and f.constexpr:
cctx = state.user_data.ctx
cctx.auto_typealias.append(
f"static constexpr auto {prop_name} [[maybe_unused]] = {cctx.full_cpp_name}::{prop_name}"
GeneratedTypeAlias(
f"static constexpr auto {prop_name} [[maybe_unused]] = "
f"{cctx.full_cpp_name}::{prop_name}",
deprecated=deprecation is not None,
)
)

def on_class_method(self, state: AWClassBlockState, method: Method) -> None:
Expand Down Expand Up @@ -1163,6 +1241,7 @@ def _on_class_method(

# Use cpp_code to setup the operator
if fctx.cpp_code is None:
fctx.generated_operator_cpp_code = True
if len(method.parameters) == 0:
fctx.cpp_code = f"{operator} py::self"
else:
Expand Down Expand Up @@ -1475,7 +1554,13 @@ def _on_fn_or_method(
if data.internal or internal:
py_name = f"_{py_name}"

doc = self._process_doc(fn.doxygen, data, param_remap=param_remap)
deprecation = _get_deprecation(fn.attributes)
doc = self._process_doc(
fn.doxygen,
data,
param_remap=param_remap,
deprecation=deprecation,
)

# Allow the user to override our auto-detected keepalives
if data.keepalive is not None:
Expand Down Expand Up @@ -1562,6 +1647,7 @@ def _on_fn_or_method(
fctx = FunctionContext(
cpp_name=fn_name,
doc=doc,
deprecated=deprecation is not None,
scope_var=scope_var,
# transforms
py_name=py_name,
Expand Down Expand Up @@ -1994,6 +2080,7 @@ def _process_doc(
data: HasDoc,
append_prefix: str = "",
param_remap: typing.Dict[str, str] = {},
deprecation: typing.Optional[Deprecation] = None,
) -> Documentation:
doc = ""

Expand All @@ -2016,6 +2103,21 @@ def _process_doc(
"\n", f"\n{append_prefix}"
)

if deprecation is not None:
message = deprecation.message
already_documented = (
message in doc
if message
else re.search(r"\bdeprecated\b", doc, re.IGNORECASE) is not None
)
if not already_documented:
warning = ".. warning::\n Deprecated."
if message:
warning = ".. warning::\n Deprecated: " + message.replace(
"\n", "\n "
)
doc = f"{doc}\n\n{warning}" if doc else warning

return self._quote_doc(doc)

def _quote_doc(self, doc: typing.Optional[str]) -> Documentation:
Expand Down Expand Up @@ -2260,6 +2362,7 @@ def parse_header(
header_name=generated_header,
doc_set=visitor._quote_doc(tmpl_data.doc),
doc_add=visitor._quote_doc(doc_add),
deprecated=matched_cctx.deprecated if matched_cctx is not None else False,
)
hctx.template_instances.append(tctx)

Expand Down
Loading
Loading