diff --git a/src/semiwrap/autowrap/context.py b/src/semiwrap/autowrap/context.py index ee12f599..d2125c5a 100644 --- a/src/semiwrap/autowrap/context.py +++ b/src/semiwrap/autowrap/context.py @@ -53,6 +53,9 @@ class EnumeratorContext: #: Documentation doc: Documentation + #: Marked deprecated in C++ + deprecated: bool + @dataclass class EnumContext: @@ -82,6 +85,9 @@ class EnumContext: #: Documentation doc: Documentation + #: Marked deprecated in C++ + deprecated: bool + # # Copied from user's EnumData # @@ -153,6 +159,12 @@ def cpp_type_no_const(self) -> str: return ct +@dataclass +class GeneratedTypeAlias: + declaration: str + deprecated: bool = False + + @dataclass class GeneratedLambda: """ @@ -190,6 +202,9 @@ class FunctionContext: #: Documentation doc: Documentation + #: Marked deprecated in C++ + deprecated: bool + #: parent variable to attach to scope_var: str @@ -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 @@ -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 @@ -437,6 +456,9 @@ class ClassContext: #: Documentation doc: Documentation + #: Marked deprecated in C++ + deprecated: bool + bases: typing.List[BaseClassData] template: typing.Optional[ClassTemplateData] @@ -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 @@ -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 diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index daa5ad2a..d3e71ab1 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -3,6 +3,7 @@ # suitable for use with the autowrap templates # +import ast import dataclasses import pathlib import re @@ -30,6 +31,7 @@ from cxxheaderparser.types import ( AnonymousName, Array, + Attribute, ClassDecl, Concept, DecoratedType, @@ -86,6 +88,7 @@ FnTemplateImpl, FunctionContext, GeneratedLambda, + GeneratedTypeAlias, HeaderContext, ParamCategory, ParamContext, @@ -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 @@ -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: @@ -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] = [] @@ -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}", @@ -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, ) ) @@ -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, ) @@ -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]] = [] @@ -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, @@ -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 @@ -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), @@ -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: @@ -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: @@ -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: @@ -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, @@ -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 = "" @@ -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: @@ -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) diff --git a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py index e0503d8a..e7d89fd5 100644 --- a/src/semiwrap/autowrap/render_cls_trampoline_hpp.py +++ b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py @@ -11,6 +11,7 @@ ) from .mangle import trampoline_signature from .namespace_utils import generated_qualname, namespace_scope +from .render_deprecated import suppress_deprecated from . import render_pybind11 as rpybind11 @@ -150,35 +151,36 @@ def _render_cls_trampoline_scoped( f"\ntemplate <{postcomma(template_parameter_list)}typename CfgBase = swgen::EmptyTrampolineCfg>" ) - if cls.bases: - r.writeln(f"struct PyTrampolineCfg_{cls.cpp_name} :") + with suppress_deprecated(r, cls.deprecated): + if cls.bases: + r.writeln(f"struct PyTrampolineCfg_{cls.cpp_name} :") - with r.indent(): - for base in cls.bases: - base_cfg = "::" + generated_qualname( - base.namespace_, f"PyTrampolineCfg_{base.cls_name}" - ) - r.writeln(f"{base_cfg}<{postcomma(base.template_params)}") + with r.indent(): + for base in cls.bases: + base_cfg = "::" + generated_qualname( + base.namespace_, f"PyTrampolineCfg_{base.cls_name}" + ) + r.writeln(f"{base_cfg}<{postcomma(base.template_params)}") - r.writeln("CfgBase") + r.writeln("CfgBase") - for base in cls.bases: - r.writeln(">") - else: - r.writeln(f"struct PyTrampolineCfg_{cls.cpp_name} : CfgBase") + for base in cls.bases: + r.writeln(">") + else: + r.writeln(f"struct PyTrampolineCfg_{cls.cpp_name} : CfgBase") - r.writeln("{") + r.writeln("{") - with r.indent(): - r.writeln(f"using Base = {cls.full_cpp_name};\n") + with r.indent(): + r.writeln(f"using Base = {cls.full_cpp_name};\n") - # specify base class to use for each virtual function - for fn in trampoline.virtual_methods: - r.writeln( - f"using override_base_{ trampoline_signature(fn) } = { cls.full_cpp_name };" - ) + # specify base class to use for each virtual function + for fn in trampoline.virtual_methods: + r.writeln( + f"using override_base_{ trampoline_signature(fn) } = { cls.full_cpp_name };" + ) - r.writeln("};") + r.writeln("};") if cls.bases: # To avoid multiple inheritance here, we define a single base with bases that @@ -208,44 +210,49 @@ def _render_cls_trampoline_scoped( r.writeln(", PyTrampolineCfg>") r.rel_indent(-2) - r.write_trim( - f""" - ; + r.writeln(";") - template - struct PyTrampoline_{ cls.cpp_name } : PyTrampolineBase_{ cls.cpp_name } {{ - using PyTrampolineBase_{ cls.cpp_name }::PyTrampolineBase_{ cls.cpp_name }; - """ - ) + with suppress_deprecated(r, cls.deprecated): + r.write_trim( + f""" + template + struct PyTrampoline_{ cls.cpp_name } : PyTrampolineBase_{ cls.cpp_name } {{ + using PyTrampolineBase_{ cls.cpp_name }::PyTrampolineBase_{ cls.cpp_name }; + """ + ) else: r.writeln() - r.write_trim( - f""" - template - struct PyTrampoline_{ cls.cpp_name } : PyTrampolineBase {{ - using PyTrampolineBase::PyTrampolineBase; - """ - ) + with suppress_deprecated(r, cls.deprecated): + r.write_trim( + f""" + template + struct PyTrampoline_{ cls.cpp_name } : PyTrampolineBase {{ + using PyTrampolineBase::PyTrampolineBase; + """ + ) with r.indent(): for ccls in cls.child_classes: if not ccls.template: - r.writeln( - f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" - ) + with suppress_deprecated(r, cls.deprecated or ccls.deprecated): + r.writeln( + f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" + ) for enum in cls.enums: if enum.cpp_name: - r.writeln( - f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" - ) + with suppress_deprecated(r, cls.deprecated or enum.deprecated): + r.writeln( + f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" + ) for typealias in cls.user_typealias: r.writeln(f"{typealias};") - for typealias in cls.auto_typealias: - r.writeln(f"{typealias};") + for generated_alias in cls.auto_typealias: + with suppress_deprecated(r, cls.deprecated or generated_alias.deprecated): + r.writeln(f"{generated_alias.declaration};") if cls.constants: r.writeln() @@ -257,23 +264,24 @@ def _render_cls_trampoline_scoped( # for fn in trampoline.protected_constructors: - r.writeln( - f"\n#ifdef SWGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" - ) - with r.indent(): - all_decls = ", ".join(p.decl for p in fn.all_params) - all_names = ", ".join(p.arg_name for p in fn.all_params) - r.writeln(f"PyTrampoline_{cls.cpp_name}({all_decls}) :") - - if cls.bases: - r.writeln( - f" PyTrampolineBase_{cls.cpp_name}({all_names})" - ) - else: - r.writeln(f" PyTrampolineBase({all_names})") - - r.writeln("{}") - r.writeln("#endif") + with suppress_deprecated(r, cls.deprecated or fn.deprecated): + r.writeln( + f"\n#ifdef SWGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" + ) + with r.indent(): + all_decls = ", ".join(p.decl for p in fn.all_params) + all_names = ", ".join(p.arg_name for p in fn.all_params) + r.writeln(f"PyTrampoline_{cls.cpp_name}({all_decls}) :") + + if cls.bases: + r.writeln( + f" PyTrampolineBase_{cls.cpp_name}({all_names})" + ) + else: + r.writeln(f" PyTrampolineBase({all_names})") + + r.writeln("{}") + r.writeln("#endif") # # virtual methods @@ -287,27 +295,29 @@ def _render_cls_trampoline_scoped( # for fn in trampoline.non_virtual_protected_methods: - r.writeln(f"\n#ifndef SWGEN_DISABLE_{ trampoline_signature(fn) }") + with suppress_deprecated(r, cls.deprecated or fn.deprecated): + r.writeln(f"\n#ifndef SWGEN_DISABLE_{ trampoline_signature(fn) }") - # hack to ensure we don't do 'using' twice' in the same class, while - # also ensuring that the overrides can be selectively disabled by - # child trampoline functions - with r.indent(): - r.writeln(f"#ifndef SWGEN_UDISABLE_{ using_signature(cls, fn) }") + # hack to ensure we don't do 'using' twice' in the same class, while + # also ensuring that the overrides can be selectively disabled by + # child trampoline functions with r.indent(): - r.write_trim( - f""" - using { cls.full_cpp_name }::{ fn.cpp_name }; - #define SWGEN_UDISABLE_{ using_signature(cls, fn) } - """ - ) + r.writeln(f"#ifndef SWGEN_UDISABLE_{ using_signature(cls, fn) }") + with r.indent(): + r.write_trim( + f""" + using { cls.full_cpp_name }::{ fn.cpp_name }; + #define SWGEN_UDISABLE_{ using_signature(cls, fn) } + """ + ) + r.writeln("#endif") r.writeln("#endif") - r.writeln("#endif") if cls.protected_properties: r.writeln() for prop in cls.protected_properties: - r.writeln(f"using {cls.full_cpp_name}::{prop.cpp_name};") + with suppress_deprecated(r, cls.deprecated or prop.deprecated): + r.writeln(f"using {cls.full_cpp_name}::{prop.cpp_name};") if trampoline.inline_code: r.writeln() @@ -318,6 +328,13 @@ def _render_cls_trampoline_scoped( def _render_cls_trampoline_virtual_method( r: RenderBuffer, cls: ClassContext, fn: FunctionContext +): + with suppress_deprecated(r, cls.deprecated or fn.deprecated): + _render_cls_trampoline_virtual_method_impl(r, cls, fn) + + +def _render_cls_trampoline_virtual_method_impl( + r: RenderBuffer, cls: ClassContext, fn: FunctionContext ): r.writeln(f"\n#ifndef SWGEN_DISABLE_{ trampoline_signature(fn) }") with r.indent(): diff --git a/src/semiwrap/autowrap/render_deprecated.py b/src/semiwrap/autowrap/render_deprecated.py new file mode 100644 index 00000000..f615b9d1 --- /dev/null +++ b/src/semiwrap/autowrap/render_deprecated.py @@ -0,0 +1,15 @@ +from contextlib import contextmanager +import typing + +from .buffer import RenderBuffer + + +@contextmanager +def suppress_deprecated(r: RenderBuffer, enabled: bool) -> typing.Iterator[None]: + if enabled: + r.writeln("SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN") + try: + yield + finally: + if enabled: + r.writeln("SEMIWRAP_SUPPRESS_DEPRECATED_END") diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index a1ab089c..6d2f7be3 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -10,6 +10,33 @@ GeneratedLambda, PropContext, ) +from .render_deprecated import suppress_deprecated + + +_OPERATOR_PY_NAMES = { + "-": "__sub__", + "+": "__add__", + "*": "__mul__", + "/": "__truediv__", + "%": "__mod__", + "&": "__and__", + "^": "__xor__", + "==": "__eq__", + "!=": "__ne__", + "|": "__or__", + ">": "__gt__", + ">=": "__ge__", + "<": "__lt__", + "<=": "__le__", + "+=": "__iadd__", + "-=": "__isub__", + "*=": "__imul__", + "/=": "__itruediv__", + "%=": "__imod__", + "&=": "__iand__", + "^=": "__ixor__", + "|=": "__ior__", +} def mkdoc(pre: str, doc: Documentation, post: str) -> str: @@ -44,13 +71,14 @@ def _gensig(cls_qualname: T.Optional[str], fn: FunctionContext) -> str: ) -def _genmethod( +def _genmethod_impl( r: RenderBuffer, varname: str, cls_qualname: T.Optional[str], fn: FunctionContext, trampoline_qualname: T.Optional[str], tmpl: str, + avoid_deprecated_pybind_instantiation: bool, ): qualname = cls_qualname arg_params = fn.filtered_params @@ -64,7 +92,30 @@ def _genmethod( r.rel_indent(2) if fn.operator: - r.writeln(f"{varname}.def({fn.cpp_code}") + if avoid_deprecated_pybind_instantiation and fn.generated_operator_cpp_code: + assert cls_qualname is not None + lam_params = [ + f"{'const ' if fn.const else ''}{cls_qualname} &self", + *(param.decl for param in arg_params), + ] + if arg_params: + expression = f"self {fn.operator} {arg_params[0].call_name}" + else: + expression = f"{fn.operator}self" + py_name = ( + {"-": "__neg__", "+": "__pos__"}.get(fn.operator) + if not arg_params + else _OPERATOR_PY_NAMES[fn.operator] + ) + r.writeln( + f'{varname}.def("{py_name}", ' + f"[]({', '.join(lam_params)}) -> decltype(auto) {{" + ) + with r.indent(): + r.writeln(f"return {expression};") + r.writeln("}, py::is_operator()") + else: + r.writeln(f"{varname}.def({fn.cpp_code}") arg_params = [] elif fn.is_constructor: if fn.cpp_code: @@ -85,6 +136,15 @@ def _genmethod( f'{varname}.def_static("{fn.py_name}"', lam_params, ) + elif avoid_deprecated_pybind_instantiation: + assert cls_qualname is not None + constructor_qualname = trampoline_qualname or cls_qualname + constructor_params = ", ".join(param.decl for param in arg_params) + call_params = ", ".join(param.call_name for param in arg_params) + r.writeln(f"{varname}.def(py::init([]({constructor_params}) {{") + with r.indent(): + r.writeln(f"return new {constructor_qualname}({call_params});") + r.writeln("})") elif trampoline_qualname: r.writeln( f"{varname}.def(py::init_alias<{', '.join(param.full_cpp_type for param in arg_params)}>()" @@ -186,6 +246,28 @@ def _genmethod( r.writeln(f"#endif // {fn.ifndef}\n") +def _genmethod( + r: RenderBuffer, + varname: str, + cls_qualname: T.Optional[str], + fn: FunctionContext, + trampoline_qualname: T.Optional[str], + tmpl: str, + force_suppress_deprecated: bool = False, +): + deprecated = fn.deprecated or force_suppress_deprecated + with suppress_deprecated(r, deprecated): + _genmethod_impl( + r, + varname, + cls_qualname, + fn, + trampoline_qualname, + tmpl, + deprecated, + ) + + def _gen_method_lambda( r: RenderBuffer, fn: FunctionContext, @@ -217,9 +299,18 @@ def genmethod( cls_qualname: T.Optional[str], fn: FunctionContext, trampoline_qualname: T.Optional[str], + force_suppress_deprecated: bool = False, ): if not fn.template_impls: - _genmethod(r, varname, cls_qualname, fn, trampoline_qualname, "") + _genmethod( + r, + varname, + cls_qualname, + fn, + trampoline_qualname, + "", + force_suppress_deprecated, + ) else: for tmpl in fn.template_impls: r.writeln("{") @@ -234,11 +325,12 @@ def genmethod( fn, trampoline_qualname, f"<{', '.join(tmpl.params)}>", + force_suppress_deprecated, ) r.writeln("}") -def _genprop(r: RenderBuffer, varname: str, qualname: str, prop: PropContext): +def _genprop_impl(r: RenderBuffer, varname: str, qualname: str, prop: PropContext): doc = "" if prop.doc: doc = mkdoc(", py::doc(", prop.doc, ")") @@ -285,8 +377,29 @@ def _genprop(r: RenderBuffer, varname: str, qualname: str, prop: PropContext): ) -def enum_decl(r: RenderBuffer, enum: EnumContext, varname: str): - r.writeln(f"py::enum_<{ enum.full_cpp_name }> {varname};") +def _genprop( + r: RenderBuffer, + varname: str, + qualname: str, + prop: PropContext, + force_suppress_deprecated: bool = False, +): + if prop.array and prop.array_size is None: + _genprop_impl(r, varname, qualname, prop) + return + + with suppress_deprecated(r, prop.deprecated or force_suppress_deprecated): + _genprop_impl(r, varname, qualname, prop) + + +def enum_decl( + r: RenderBuffer, + enum: EnumContext, + varname: str, + force_suppress_deprecated: bool = False, +): + with suppress_deprecated(r, enum.deprecated or force_suppress_deprecated): + r.writeln(f"py::enum_<{ enum.full_cpp_name }> {varname};") def enum_init_args(scope: str, enum: EnumContext): @@ -301,14 +414,32 @@ def enum_init_args(scope: str, enum: EnumContext): return ", ".join(params) -def enum_def(r: RenderBuffer, varname: str, enum: EnumContext): - for val in enum.values: - doc = mkdoc(",", val.doc, "") - r.writeln(f'.value("{val.py_name}", {val.full_cpp_name}{doc})') +def enum_def( + r: RenderBuffer, + varname: str, + enum: EnumContext, + force_suppress_deprecated: bool = False, +): + if enum.deprecated or force_suppress_deprecated: + with suppress_deprecated(r, bool(enum.values)): + for val in enum.values: + doc = mkdoc(",", val.doc, "") + r.writeln( + f'{varname}.value("{val.py_name}", {val.full_cpp_name}{doc});' + ) + else: + for val in enum.values: + doc = mkdoc(",", val.doc, "") + with suppress_deprecated(r, val.deprecated): + r.writeln( + f'{varname}.value("{val.py_name}", {val.full_cpp_name}{doc});' + ) if enum.inline_code: - r.write_trim(enum.inline_code) - r.writeln(";") + r.writeln(varname) + with r.indent(): + r.write_trim(enum.inline_code) + r.writeln(";") def cls_user_using(r: RenderBuffer, cls: ClassContext): @@ -319,16 +450,22 @@ def cls_user_using(r: RenderBuffer, cls: ClassContext): def cls_auto_using(r: RenderBuffer, cls: ClassContext): for ccls in cls.child_classes: if not ccls.template: - r.writeln( - f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" - ) + with suppress_deprecated(r, cls.deprecated or ccls.deprecated): + r.writeln( + f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" + ) for enum in cls.enums: if enum.cpp_name: - r.writeln( - f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" - ) + with suppress_deprecated(r, cls.deprecated or enum.deprecated): + r.writeln( + f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" + ) for typealias in cls.auto_typealias: - r.writeln(f"{typealias};") + with suppress_deprecated(r, cls.deprecated or typealias.deprecated): + r.writeln(f"{typealias.declaration};") + for ccls in cls.child_classes: + if not ccls.template: + cls_auto_using(r, ccls) def cls_consts(r: RenderBuffer, cls: ClassContext): @@ -339,43 +476,44 @@ def cls_consts(r: RenderBuffer, cls: ClassContext): def cls_decl(r: RenderBuffer, cls: ClassContext): - if cls.trampoline: - tctx = cls.trampoline - # py::trampoline_self_life_support - r.write_trim( - f""" - struct {tctx.var} : {tctx.full_cpp_name}, py::trampoline_self_life_support {{ - using RpyBase = {tctx.full_cpp_name}; - using RpyBase::RpyBase; - }}; - - """ - ) - r.writeln( - f'static_assert(std::is_abstract<{tctx.var}>::value == false, "{cls.full_cpp_name} " SEMIWRAP_BAD_TRAMPOLINE);' - ) + with suppress_deprecated(r, cls.deprecated): + if cls.trampoline: + tctx = cls.trampoline + # py::trampoline_self_life_support + r.write_trim( + f""" + struct {tctx.var} : {tctx.full_cpp_name}, py::trampoline_self_life_support {{ + using RpyBase = {tctx.full_cpp_name}; + using RpyBase::RpyBase; + }}; + + """ + ) + r.writeln( + f'static_assert(std::is_abstract<{tctx.var}>::value == false, "{cls.full_cpp_name} " SEMIWRAP_BAD_TRAMPOLINE);' + ) - class_params = [f"typename {cls.full_cpp_name}"] - if cls.nodelete: - class_params.append( - f"std::unique_ptr" - ) - else: - class_params.append("py::smart_holder") + class_params = [f"typename {cls.full_cpp_name}"] + if cls.nodelete: + class_params.append( + f"std::unique_ptr" + ) + else: + class_params.append("py::smart_holder") - if cls.trampoline: - class_params.append(cls.trampoline.var) + if cls.trampoline: + class_params.append(cls.trampoline.var) - if cls.bases: - bases = ", ".join(base.full_cpp_name_w_templates for base in cls.bases) - class_params.append(bases) + if cls.bases: + bases = ", ".join(base.full_cpp_name_w_templates for base in cls.bases) + class_params.append(bases) - r.writeln(f"py::class_<{', '.join(class_params)}> {cls.var_name};") + r.writeln(f"py::class_<{', '.join(class_params)}> {cls.var_name};") if cls.enums: r.writeln() for index, enum in enumerate(cls.enums, start=1): - enum_decl(r, enum, f"{cls.var_name}_enum{index}") + enum_decl(r, enum, f"{cls.var_name}_enum{index}", cls.deprecated) for ccls in cls.child_classes: if not ccls.template: @@ -408,9 +546,7 @@ def cls_init( def cls_def_enum(r: RenderBuffer, cctx: ClassContext, varname: str): for idx, enum in enumerate(cctx.enums, start=1): - r.writeln(f"{cctx.var_name}_enum{idx}") - with r.indent(): - enum_def(r, cctx.var_name, enum) + enum_def(r, f"{cctx.var_name}_enum{idx}", enum, cctx.deprecated) def cls_def(r: RenderBuffer, cls: ClassContext, varname: str): @@ -418,41 +554,69 @@ def cls_def(r: RenderBuffer, cls: ClassContext, varname: str): for fn in cls.vcheck_fns: assert fn.cpp_code is not None - r.writeln("{") - with r.indent(): - r.writeln(f"auto vcheck = {fn.cpp_code.strip()};") + with suppress_deprecated(r, cls.deprecated or fn.deprecated): + r.writeln("{") + with r.indent(): + r.writeln(f"auto vcheck = {fn.cpp_code.strip()};") - sig_params = [f"{cls.full_cpp_name}*"] - sig_params.extend(p.full_cpp_type for p in fn.all_params) + sig_params = [f"{cls.full_cpp_name}*"] + sig_params.extend(p.full_cpp_type for p in fn.all_params) - signature = f"{fn.cpp_return_type}({', '.join(sig_params)})" - r.writeln( - f"static_assert(std::is_convertible>::value," - ) - r.writeln( - f' "{cls.full_cpp_name}::{fn.cpp_name} must have virtual_xform if cpp_code signature doesn\'t match original function");' - ) - r.writeln("}") + signature = f"{fn.cpp_return_type}({', '.join(sig_params)})" + r.writeln( + f"static_assert(std::is_convertible>::value," + ) + r.writeln( + f' "{cls.full_cpp_name}::{fn.cpp_name} must have virtual_xform if cpp_code signature doesn\'t match original function");' + ) + r.writeln("}") if cls.doc: r.writeln(f'{varname}.doc() = {mkdoc("", cls.doc, "")};') if cls.add_default_constructor: - r.writeln(f"{varname}.def(py::init<>(), release_gil());") + with suppress_deprecated(r, cls.deprecated): + r.writeln(f"{varname}.def(py::init<>(), release_gil());") for fn in cls.wrapped_public_methods: - genmethod(r, varname, cls.full_cpp_name, fn, None) + genmethod( + r, + varname, + cls.full_cpp_name, + fn, + None, + force_suppress_deprecated=cls.deprecated, + ) if cls.trampoline is not None: for fn in cls.wrapped_protected_methods: - genmethod(r, varname, cls.full_cpp_name, fn, cls.trampoline.var) + genmethod( + r, + varname, + cls.full_cpp_name, + fn, + cls.trampoline.var, + force_suppress_deprecated=cls.deprecated, + ) for prop in cls.public_properties: - _genprop(r, varname, cls.full_cpp_name, prop) + _genprop( + r, + varname, + cls.full_cpp_name, + prop, + force_suppress_deprecated=cls.deprecated, + ) if cls.trampoline is not None: for prop in cls.protected_properties: - _genprop(r, varname, cls.trampoline.full_cpp_name, prop) + _genprop( + r, + varname, + cls.trampoline.full_cpp_name, + prop, + force_suppress_deprecated=cls.deprecated, + ) if cls.inline_code: r.writeln(varname) @@ -463,10 +627,20 @@ def cls_def(r: RenderBuffer, cls: ClassContext, varname: str): if cls.unnamed_enums: r.writeln() for enum in cls.unnamed_enums: - for val in enum.values: - r.writeln( - f'{varname}.attr("{val.py_name}") = (int){val.full_cpp_name};' - ) + if cls.deprecated or enum.deprecated: + with suppress_deprecated(r, bool(enum.values)): + for val in enum.values: + r.writeln( + f'{varname}.attr("{val.py_name}") = ' + f"(int){val.full_cpp_name};" + ) + else: + for val in enum.values: + with suppress_deprecated(r, val.deprecated): + r.writeln( + f'{varname}.attr("{val.py_name}") = ' + f"(int){val.full_cpp_name};" + ) for ccls in cls.child_classes: if not ccls.template: diff --git a/src/semiwrap/autowrap/render_tmpl_inst.py b/src/semiwrap/autowrap/render_tmpl_inst.py index d1f645b6..56d7ce18 100644 --- a/src/semiwrap/autowrap/render_tmpl_inst.py +++ b/src/semiwrap/autowrap/render_tmpl_inst.py @@ -1,6 +1,7 @@ from .buffer import RenderBuffer from .context import HeaderContext, TemplateInstanceContext from .namespace_utils import generated_qualname, namespace_scope +from .render_deprecated import suppress_deprecated from .render_cls_prologue import render_class_prologue @@ -25,29 +26,36 @@ def render_template_inst_cpp( r.writeln() with namespace_scope(r, tmpl_data.namespace, anonymous=True): - r.write_trim( - f""" - using BindType = {binder_type}<{tmpl_params}>; - static std::unique_ptr inst; - """ - ) + with suppress_deprecated(r, tmpl_data.deprecated): + r.write_trim( + f""" + using BindType = {binder_type}<{tmpl_params}>; + static std::unique_ptr inst; + """ + ) r.writeln() with namespace_scope(r, tmpl_data.namespace, generated=True): - r.write_trim( - f""" - {tmpl_data.binder_typename}::{tmpl_data.binder_typename}(py::module &m, const char * clsName) - {{ - inst = std::make_unique(m, clsName); - }} + r.writeln( + f"{tmpl_data.binder_typename}::{tmpl_data.binder_typename}" + "(py::module &m, const char * clsName)" + ) + r.writeln("{") + with r.indent(): + with suppress_deprecated(r, tmpl_data.deprecated): + r.writeln("inst = std::make_unique(m, clsName);") + r.writeln("}\n") - void {tmpl_data.binder_typename}::finish(const char *set_doc, const char *add_doc) - {{ - inst->finish(set_doc, add_doc); - inst.reset(); - }} - """ + r.writeln( + f"void {tmpl_data.binder_typename}::finish" + "(const char *set_doc, const char *add_doc)" ) + r.writeln("{") + with r.indent(): + with suppress_deprecated(r, tmpl_data.deprecated): + r.writeln("inst->finish(set_doc, add_doc);") + r.writeln("inst.reset();") + r.writeln("}") r.writeln() return r.getvalue() @@ -66,13 +74,14 @@ def render_template_inst_hpp(hctx: HeaderContext) -> str: for tmpl_data in hctx.template_instances: r.writeln() with namespace_scope(r, tmpl_data.namespace, generated=True): - r.write_trim( - f""" - struct {tmpl_data.binder_typename} {{ - {tmpl_data.binder_typename}(py::module &m, const char * clsName); - void finish(const char *set_doc, const char *add_doc); - }}; - """ - ) + with suppress_deprecated(r, tmpl_data.deprecated): + r.write_trim( + f""" + struct {tmpl_data.binder_typename} {{ + {tmpl_data.binder_typename}(py::module &m, const char * clsName); + void finish(const char *set_doc, const char *add_doc); + }}; + """ + ) return r.getvalue() diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index eab0faad..d36be33c 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -12,6 +12,7 @@ from . import render_pybind11 as rpybind11 from .render_cls_prologue import render_class_prologue +from .render_deprecated import suppress_deprecated from .render_wrapped_class import ( class_helper_qualname, class_scope_exports_qualname, @@ -57,10 +58,7 @@ def _render_enum_helper( r.writeln("{}") r.writeln("void finish() {") with r.indent(): - # enum_def emits fluent suffixes, so provide their receiver here. - r.writeln("value") - with r.indent(): - rpybind11.enum_def(r, "value", enum) + rpybind11.enum_def(r, "value", enum) r.writeln("}") r.writeln(f"}}; // struct {helper_name}") @@ -249,10 +247,11 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: # template decls for tmpl_data in hctx.template_instances: if not tmpl_data.matched: - r.writeln( - f"{_absolute_qualname(tmpl_data.binder_full_cpp_name)} " - f"{tmpl_data.var_name};" - ) + with suppress_deprecated(r, tmpl_data.deprecated): + r.writeln( + f"{_absolute_qualname(tmpl_data.binder_full_cpp_name)} " + f"{tmpl_data.var_name};" + ) # class decls for cls in hctx.classes: @@ -264,12 +263,18 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: ) _render_class_access_decls(r, cls, cls) for tmpl_data in _template_instances(cls): - r.writeln( - f"{_absolute_qualname(tmpl_data.binder_full_cpp_name)} " - f"{tmpl_data.var_name};" - ) + with suppress_deprecated(r, tmpl_data.deprecated): + r.writeln( + f"{_absolute_qualname(tmpl_data.binder_full_cpp_name)} " + f"{tmpl_data.var_name};" + ) r.writeln("\npy::module &m;\n") + suppress_template_initializers = any( + tmpl_data.deprecated for tmpl_data in hctx.template_instances + ) + if suppress_template_initializers: + r.writeln("SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN") r.writeln(f"{coordinator_name}(py::module &m) :") with r.indent(): @@ -311,24 +316,27 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: r.writeln("}") else: r.writeln("{}") + if suppress_template_initializers: + r.writeln("SEMIWRAP_SUPPRESS_DEPRECATED_END") r.writeln("\nvoid finish() {\n") with r.indent(): # Templates for tdata in hctx.template_instances: - r.writeln(f"\n{tdata.var_name}.finish(") - with r.indent(): - if tdata.doc_set: - r.writeln(f'{rpybind11.mkdoc("", tdata.doc_set, "")},') - else: - r.writeln("nullptr,") - - if tdata.doc_add: - r.writeln(rpybind11.mkdoc("", tdata.doc_add, "")) - else: - r.writeln("nullptr") - r.writeln(");") + with suppress_deprecated(r, tdata.deprecated): + r.writeln(f"\n{tdata.var_name}.finish(") + with r.indent(): + if tdata.doc_set: + r.writeln(f'{rpybind11.mkdoc("", tdata.doc_set, "")},') + else: + r.writeln("nullptr,") + + if tdata.doc_add: + r.writeln(rpybind11.mkdoc("", tdata.doc_add, "")) + else: + r.writeln("nullptr") + r.writeln(");") # Class methods for cls in hctx.classes: diff --git a/src/semiwrap/include/semiwrap.h b/src/semiwrap/include/semiwrap.h index 188d1972..929a8eda 100644 --- a/src/semiwrap/include/semiwrap.h +++ b/src/semiwrap/include/semiwrap.h @@ -6,6 +6,25 @@ namespace py = pybind11; +#if defined(__clang__) +#define SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#define SEMIWRAP_SUPPRESS_DEPRECATED_END _Pragma("clang diagnostic pop") +#elif defined(__GNUC__) +#define SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#define SEMIWRAP_SUPPRESS_DEPRECATED_END _Pragma("GCC diagnostic pop") +#elif defined(_MSC_VER) +#define SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN \ + __pragma(warning(push)) __pragma(warning(disable : 4996)) +#define SEMIWRAP_SUPPRESS_DEPRECATED_END __pragma(warning(pop)) +#else +#define SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN +#define SEMIWRAP_SUPPRESS_DEPRECATED_END +#endif + // Use this to release the gil typedef py::call_guard release_gil; diff --git a/tests/cpp/sw-test/meson.build b/tests/cpp/sw-test/meson.build index 6e4f06f0..41ff20e5 100644 --- a/tests/cpp/sw-test/meson.build +++ b/tests/cpp/sw-test/meson.build @@ -2,6 +2,13 @@ project('sw-test', ['cpp'], default_options: ['warning_level=1', 'cpp_std=c++20', 'b_colorout=auto', 'optimization=2', 'b_pie=true']) +cpp = meson.get_compiler('cpp') +if cpp.get_argument_syntax() == 'msvc' + add_project_arguments('/we4996', language: 'cpp') +else + add_project_arguments('-Werror=deprecated-declarations', language: 'cpp') +endif + subdir('semiwrap') swtest_ft__ft_sources += files( diff --git a/tests/cpp/sw-test/pyproject.toml b/tests/cpp/sw-test/pyproject.toml index 0c862a91..afb039e7 100644 --- a/tests/cpp/sw-test/pyproject.toml +++ b/tests/cpp/sw-test/pyproject.toml @@ -36,6 +36,7 @@ base_qualname_hidden = "base_qualname_hidden.h" buffers = "buffers.h" custom_type_caster = "custom_type_caster.h" define = "define.h" +deprecated = "deprecated.h" default_args_as_kw_only = "default_args_as_kw_only.h" defaults = "defaults.h" docstrings = "docstrings.h" diff --git a/tests/cpp/sw-test/semiwrap/ft/deprecated.yml b/tests/cpp/sw-test/semiwrap/ft/deprecated.yml new file mode 100644 index 00000000..987b5162 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/ft/deprecated.yml @@ -0,0 +1,44 @@ +functions: + deprecated_without_message: + deprecated_free_function: + deprecated_documented_function: +classes: + DeprecatedMembers: + methods: + DeprecatedMembers: + deprecated_method: + deprecated_static_method: + operator==: + attributes: + deprecated_field: + deprecated_static_field: + DeprecatedClass: + methods: + DeprecatedClass: + value: + DeprecatedVirtual: + force_no_default_constructor: true + methods: + deprecated_virtual: + deprecated_protected_method: + attributes: + deprecated_protected_property: + DeprecatedTemplate: + template_params: + - T + methods: + DeprecatedTemplate: + get: +enums: + DeprecatedEnum: + values: + Value: + MixedDeprecatedEnum: + values: + DeprecatedValue: + CurrentValue: +templates: + DeprecatedTemplateInt: + qualname: DeprecatedTemplate + params: + - int diff --git a/tests/cpp/sw-test/src/swtest/ft/include/deprecated.h b/tests/cpp/sw-test/src/swtest/ft/include/deprecated.h new file mode 100644 index 00000000..ce108381 --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/include/deprecated.h @@ -0,0 +1,89 @@ +#pragma once + +[[deprecated]] inline int deprecated_without_message() { + return 1; +} + +[[deprecated("Use " "current_free_function().")]] inline int deprecated_free_function() { + return 2; +} + +/** + * Legacy documented function. + * + * .. warning::
Deprecated: Use current_documented_function(). + */ +[[deprecated("Use current_documented_function().")]] inline int deprecated_documented_function() { + return 3; +} + +class DeprecatedMembers { +public: + [[deprecated("Use DeprecatedMembers::create().")]] DeprecatedMembers() = default; + + [[deprecated("Use current_method().")]] int deprecated_method() const { + return 4; + } + + [[deprecated("Use current_static_method().")]] static int deprecated_static_method() { + return 5; + } + + [[deprecated("Use operator!=().")]] bool operator==( + const DeprecatedMembers &other) const { + return this == &other; + } + + /** Legacy member field. */ + [[deprecated("Use current_field.")]] int deprecated_field; + + /** Legacy static field. */ + [[deprecated("Use current_static_field.")]] static constexpr int deprecated_static_field = 7; +}; + +/** Legacy class. */ +class [[deprecated("Use CurrentClass.")]] DeprecatedClass { +public: + DeprecatedClass() = default; + + int value() const { + return 8; + } +}; + +enum class [[deprecated("Use CurrentEnum.")]] DeprecatedEnum { + Value = 9, +}; + +enum class MixedDeprecatedEnum { + DeprecatedValue [[deprecated("Use MixedDeprecatedEnum::CurrentValue.")]] = 10, + CurrentValue = 11, +}; + +class DeprecatedVirtual { +public: + virtual ~DeprecatedVirtual() = default; + + [[deprecated("Use current_virtual().")]] virtual int deprecated_virtual() { + return 12; + } + +protected: + [[deprecated("Use current_protected_method().")]] int deprecated_protected_method() { + return 13; + } + + /** Legacy protected property. */ + [[deprecated("Use current_protected_property.")]] int deprecated_protected_property; +}; + +/** Legacy class template. */ +template +class [[deprecated("Use CurrentTemplate.")]] DeprecatedTemplate { +public: + DeprecatedTemplate() = default; + + T get() const { + return T{15}; + } +}; diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py new file mode 100644 index 00000000..510861db --- /dev/null +++ b/tests/test_deprecated.py @@ -0,0 +1,755 @@ +import ast +from pathlib import Path + +from cxxheaderparser.options import ParserOptions + +from semiwrap.autowrap.cxxparser import parse_header +from semiwrap.autowrap.generator_data import GeneratorData +from semiwrap.autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp +from semiwrap.autowrap.render_tmpl_inst import ( + render_template_inst_cpp, + render_template_inst_hpp, +) +from semiwrap.autowrap.render_wrapped import render_wrapped_cpp +from semiwrap.config.autowrap_yml import ( + AutowrapConfigYaml, + ClassData, + EnumData, + FunctionData, + PropData, + TemplateData, +) + + +def parse_deprecated_header(tmp_path: Path, source: str, config: AutowrapConfigYaml): + header = tmp_path / "deprecated.h" + header.write_text(source) + return parse_header( + "deprecated", + header, + tmp_path, + GeneratorData(config, tmp_path / "deprecated.yml"), + ParserOptions(), + {}, + False, + ) + + +def unquote_doc(doc): + return "".join(ast.literal_eval(line) for line in doc or []) + + +def assert_rendered_suppression(rendered: str, needle: str, suppressed: bool): + depth = 0 + needle_depths = [] + for line in rendered.splitlines(): + if line.strip() == "SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN": + depth += 1 + elif line.strip() == "SEMIWRAP_SUPPRESS_DEPRECATED_END": + assert depth > 0 + depth -= 1 + elif needle in line: + needle_depths.append(depth) + + assert depth == 0 + assert len(needle_depths) == 1 + assert (needle_depths[0] > 0) is suppressed + + +def test_render_suppresses_only_deprecated_callable_property_and_enum_bindings( + tmp_path, +): + source = r""" +[[deprecated("Use current_free().")]] void old_free(); +void current_free(); + +enum class [[deprecated("Use CurrentGlobalEnum.")]] OldGlobalEnum { + OldGlobalValue, + CurrentGlobalValue, +}; +enum class MixedGlobalEnum { + OldMixedGlobal [[deprecated("Use CurrentMixedGlobal.")]], + CurrentMixedGlobal, +}; + +struct Widget { + [[deprecated("Use current_method().")]] void old_method(); + void current_method(); + + [[deprecated("Use current_field.")]] int old_field; + int current_field; + + enum class [[deprecated("Use CurrentClassEnum.")]] OldClassEnum { + OldClassValue, + CurrentClassValue, + }; + enum class MixedClassEnum { + OldMixedClass [[deprecated("Use CurrentMixedClass.")]], + CurrentMixedClass, + }; + enum { + OldUnnamed [[deprecated("Use CurrentUnnamed.")]], + CurrentUnnamed, + }; +}; +""" + config = AutowrapConfigYaml( + functions={ + "old_free": FunctionData(ifdef="HAS_OLD_FREE"), + "current_free": FunctionData(), + }, + enums={ + "OldGlobalEnum": EnumData(), + "MixedGlobalEnum": EnumData(), + }, + classes={ + "Widget": ClassData( + methods={ + "old_method": FunctionData(), + "current_method": FunctionData(), + }, + attributes={ + "old_field": PropData(), + "current_field": PropData(), + }, + enums={ + "OldClassEnum": EnumData(), + "MixedClassEnum": EnumData(), + }, + ) + }, + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + + assert_rendered_suppression(rendered, 'scope.def("old_free"', True) + assert_rendered_suppression(rendered, "#ifdef HAS_OLD_FREE", True) + assert_rendered_suppression(rendered, "#endif // HAS_OLD_FREE", True) + assert_rendered_suppression(rendered, 'scope.def("current_free"', False) + + assert_rendered_suppression(rendered, 'cls_Widget.def("old_method"', True) + assert_rendered_suppression(rendered, 'cls_Widget.def("current_method"', False) + assert_rendered_suppression(rendered, 'cls_Widget.def_readwrite("old_field"', True) + assert_rendered_suppression( + rendered, 'cls_Widget.def_readwrite("current_field"', False + ) + + assert_rendered_suppression(rendered, "py::enum_<::OldGlobalEnum> value;", True) + assert_rendered_suppression( + rendered, + 'value.value("OldGlobalValue", ::OldGlobalEnum::OldGlobalValue);', + True, + ) + assert_rendered_suppression(rendered, '.value("OldGlobalValue"', True) + assert_rendered_suppression(rendered, '.value("CurrentGlobalValue"', True) + assert_rendered_suppression(rendered, "py::enum_<::MixedGlobalEnum> value;", False) + assert_rendered_suppression(rendered, '.value("OldMixedGlobal"', True) + assert_rendered_suppression(rendered, '.value("CurrentMixedGlobal"', False) + + assert_rendered_suppression( + rendered, "py::enum_<::Widget::OldClassEnum> cls_Widget_enum1;", True + ) + assert_rendered_suppression(rendered, '.value("OldClassValue"', True) + assert_rendered_suppression(rendered, '.value("CurrentClassValue"', True) + assert_rendered_suppression( + rendered, "py::enum_<::Widget::MixedClassEnum> cls_Widget_enum2;", False + ) + assert_rendered_suppression(rendered, '.value("OldMixedClass"', True) + assert_rendered_suppression(rendered, '.value("CurrentMixedClass"', False) + assert_rendered_suppression(rendered, 'cls_Widget.attr("OldUnnamed")', True) + assert_rendered_suppression(rendered, 'cls_Widget.attr("CurrentUnnamed")', False) + + assert rendered.count("SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN") == rendered.count( + "SEMIWRAP_SUPPRESS_DEPRECATED_END" + ) + + +def test_enum_suppression_preserves_registration_and_inline_statement_structure( + tmp_path, +): + source = r""" +enum class [[deprecated("Use CurrentEnum.")]] OldEnum { + Value, + OtherValue, +}; +""" + marker = 'm.attr("enum_marker") = py::none()' + config = AutowrapConfigYaml( + enums={ + "OldEnum": EnumData( + inline_code=(f'.value("Alias", ::OldEnum::Value);\n{marker}') + ) + } + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + finish = rendered.split(" void finish() {\n", 1)[1].split("\n }", 1)[0] + finish_lines = [line.strip() for line in finish.splitlines() if line.strip()] + registrations = [ + 'value.value("Value", ::OldEnum::Value);', + 'value.value("OtherValue", ::OldEnum::OtherValue);', + ] + + receiver_index = finish_lines.index("value") + assert all(finish_lines.index(line) < receiver_index for line in registrations) + assert finish_lines.count("value") == 1 + assert finish_lines[receiver_index:] == [ + "value", + '.value("Alias", ::OldEnum::Value);', + marker, + ";", + ] + + suppression_begin = finish_lines.index("SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN") + suppression_end = finish_lines.index("SEMIWRAP_SUPPRESS_DEPRECATED_END") + assert finish_lines.count("SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN") == 1 + assert finish_lines.count("SEMIWRAP_SUPPRESS_DEPRECATED_END") == 1 + assert all( + suppression_begin < finish_lines.index(line) < suppression_end + for line in registrations + ) + assert suppression_end < receiver_index + assert_rendered_suppression(rendered, marker, False) + + +def test_render_suppresses_deprecated_class_and_generated_constexpr_alias(tmp_path): + source = r""" +class [[deprecated("Use CurrentWidget.")]] OldWidget { +public: + OldWidget(); + void old_widget_method(); + int old_widget_field; + static constexpr int OldWidgetConstant = 1; + enum { + OldWidgetUnnamed, + }; +}; +""" + config = AutowrapConfigYaml( + classes={ + "OldWidget": ClassData( + methods={ + "OldWidget": FunctionData(), + "old_widget_method": FunctionData(), + }, + attributes={ + "old_widget_field": PropData(), + "OldWidgetConstant": PropData(), + }, + inline_code="/* OLD_WIDGET_INLINE_MARKER */", + ) + } + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + + assert_rendered_suppression(rendered, "py::class_()" not in rendered + assert_rendered_suppression(rendered, 'cls_OldWidget.def("old_widget_method"', True) + assert_rendered_suppression( + rendered, 'cls_OldWidget.def_readwrite("old_widget_field"', True + ) + assert_rendered_suppression( + rendered, + "static constexpr auto OldWidgetConstant [[maybe_unused]] = " + "::OldWidget::OldWidgetConstant", + True, + ) + assert_rendered_suppression( + rendered, 'cls_OldWidget.attr("OldWidgetUnnamed")', True + ) + assert_rendered_suppression(rendered, "OLD_WIDGET_INLINE_MARKER", False) + + +def test_render_suppresses_only_deprecated_nested_classes_and_enums(tmp_path): + source = r""" +struct Parent { + struct [[deprecated("Use CurrentChild.")]] OldChild { + void old_child_method(); + static constexpr int OldChildConstant = 1; + }; + struct CurrentChild { + void current_child_method(); + static constexpr int CurrentChildConstant = 2; + }; + + enum class [[deprecated("Use CurrentNestedEnum.")]] OldNestedEnum { + OldNestedValue, + }; + enum class CurrentNestedEnum { + CurrentNestedValue, + }; +}; +""" + config = AutowrapConfigYaml( + classes={ + "Parent": ClassData( + enums={ + "OldNestedEnum": EnumData(), + "CurrentNestedEnum": EnumData(), + } + ), + "Parent::OldChild": ClassData( + methods={"old_child_method": FunctionData()}, + attributes={"OldChildConstant": PropData()}, + ), + "Parent::CurrentChild": ClassData( + methods={"current_child_method": FunctionData()}, + attributes={"CurrentChildConstant": PropData()}, + ), + } + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + + assert_rendered_suppression(rendered, "py::class_ cls_Parent_enum1", + True, + ) + assert_rendered_suppression( + rendered, + "py::enum_<::Parent::CurrentNestedEnum> cls_Parent_enum2", + False, + ) + assert_rendered_suppression( + rendered, + "using OldNestedEnum [[maybe_unused]] = typename ::Parent::OldNestedEnum", + True, + ) + assert_rendered_suppression( + rendered, + "using CurrentNestedEnum [[maybe_unused]] = typename ::Parent::CurrentNestedEnum", + False, + ) + + +def test_trampoline_suppresses_deprecated_class_and_method_references(tmp_path): + source = r""" +class [[deprecated("Use CurrentVirtual.")]] OldVirtual { +public: + virtual int public_old_virtual(); +protected: + OldVirtual(int value); + virtual int protected_old_virtual(); + void old_protected_method(); + int old_protected_property; +}; + +class CurrentVirtual { +public: + [[deprecated("Use current_virtual().")]] virtual int old_virtual(); + virtual int current_virtual(); +}; +""" + config = AutowrapConfigYaml( + classes={ + "OldVirtual": ClassData( + methods={ + "OldVirtual": FunctionData(), + "public_old_virtual": FunctionData(), + "protected_old_virtual": FunctionData(), + "old_protected_method": FunctionData(), + }, + attributes={"old_protected_property": PropData()}, + trampoline_inline_code="/* OLD_TRAMPOLINE_INLINE_MARKER */", + ), + "CurrentVirtual": ClassData( + methods={ + "old_virtual": FunctionData( + cpp_code="[](CurrentVirtual *) -> int { return 1; }" + ), + "current_virtual": FunctionData(), + } + ), + } + ) + hctx = parse_deprecated_header(tmp_path, source, config) + classes = {cls.cpp_name: cls for cls in hctx.classes} + + old_rendered = render_cls_trampoline_hpp(hctx, classes["OldVirtual"]) + assert_rendered_suppression(old_rendered, "using Base = ::OldVirtual;", True) + assert_rendered_suppression( + old_rendered, + "struct PyTrampoline_OldVirtual : PyTrampolineBase", + True, + ) + assert_rendered_suppression( + old_rendered, "PyTrampoline_OldVirtual(int value) :", True + ) + assert_rendered_suppression( + old_rendered, "return CxxCallBase::protected_old_virtual()", True + ) + assert_rendered_suppression( + old_rendered, "using ::OldVirtual::old_protected_method;", True + ) + assert_rendered_suppression( + old_rendered, "using ::OldVirtual::old_protected_property;", True + ) + assert_rendered_suppression(old_rendered, "OLD_TRAMPOLINE_INLINE_MARKER", False) + + current_rendered = render_cls_trampoline_hpp(hctx, classes["CurrentVirtual"]) + assert_rendered_suppression( + current_rendered, "return CxxCallBase::old_virtual()", True + ) + assert_rendered_suppression( + current_rendered, "return CxxCallBase::current_virtual()", False + ) + + wrapped = render_wrapped_cpp(hctx) + assert_rendered_suppression( + wrapped, + "auto vcheck = [](CurrentVirtual *) -> int { return 1; };", + True, + ) + + +def test_render_uses_local_callables_for_deprecated_constructor_and_operator(tmp_path): + source = r""" +struct Widget { + [[deprecated("Use create().")]] Widget(); + [[deprecated("Use is_same().")]] bool operator==(const Widget &other) const; +}; +""" + config = AutowrapConfigYaml( + classes={ + "Widget": ClassData( + methods={ + "Widget": FunctionData(), + "operator==": FunctionData(), + } + ) + } + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + + assert_rendered_suppression(rendered, "return new ::Widget()", True) + assert_rendered_suppression( + rendered, + 'cls_Widget.def("__eq__", [](const ::Widget &self', + True, + ) + assert_rendered_suppression(rendered, "return self ==", True) + assert "cls_Widget.def(py::init<>()" not in rendered + assert "cls_Widget.def(py::self == py::self" not in rendered + + +def test_render_preserves_custom_cpp_code_for_deprecated_operator(tmp_path): + source = r""" +struct [[deprecated("Use CurrentWidget.")]] Widget { + bool operator==(const Widget &other) const; +}; +""" + config = AutowrapConfigYaml( + classes={ + "Widget": ClassData( + methods={ + "operator==": FunctionData(cpp_code="py::self != py::self"), + } + ) + } + ) + + rendered = render_wrapped_cpp(parse_deprecated_header(tmp_path, source, config)) + + assert_rendered_suppression(rendered, "cls_Widget.def(py::self != py::self", True) + assert 'cls_Widget.def("__eq__", [](' not in rendered + + +def test_deprecated_class_template_suppresses_generated_binder_references(tmp_path): + source = r""" +template +struct [[deprecated("Use CurrentTemplate.")]] OldTemplate { + T get(); +}; +""" + config = AutowrapConfigYaml( + classes={ + "OldTemplate": ClassData( + template_params=["T"], + methods={"get": FunctionData()}, + inline_code="/* OLD_TEMPLATE_CLASS_INLINE_MARKER */", + template_inline_code="/* OLD_TEMPLATE_INLINE_MARKER */", + ) + }, + templates={ + "OldTemplateInt": TemplateData(qualname="OldTemplate", params=["int"]) + }, + ) + hctx = parse_deprecated_header(tmp_path, source, config) + tmpl_data = hctx.template_instances[0] + + binder = render_cls_trampoline_hpp(hctx, hctx.classes[0]) + assert_rendered_suppression(binder, "py::class_", True) + assert_rendered_suppression(binder, 'cls_OldTemplate.def("get"', True) + assert_rendered_suppression(binder, "OLD_TEMPLATE_CLASS_INLINE_MARKER", False) + assert_rendered_suppression(binder, "OLD_TEMPLATE_INLINE_MARKER", False) + + inst_hpp = render_template_inst_hpp(hctx) + assert_rendered_suppression(inst_hpp, f"struct {tmpl_data.binder_typename}", True) + + inst_cpp = render_template_inst_cpp(hctx, tmpl_data) + assert_rendered_suppression(inst_cpp, "using BindType =", True) + assert_rendered_suppression(inst_cpp, "inst = std::make_unique", True) + assert_rendered_suppression(inst_cpp, "inst->finish(set_doc, add_doc);", True) + assert_rendered_suppression(inst_cpp, "inst.reset();", True) + + wrapped = render_wrapped_cpp(hctx) + assert ( + "SEMIWRAP_SUPPRESS_DEPRECATED_BEGIN\n" + " semiwrap_deprecated_initializer(py::module &m) :" in wrapped + ) + assert_rendered_suppression( + wrapped, f"::{tmpl_data.binder_full_cpp_name} {tmpl_data.var_name};", True + ) + assert_rendered_suppression( + wrapped, f'{tmpl_data.var_name}(m, "OldTemplateInt"),', True + ) + assert_rendered_suppression(wrapped, f"{tmpl_data.var_name}.finish(", True) + + +def test_parses_supported_deprecated_function_attributes(tmp_path): + source = r""" +[[deprecated]] int old_plain(); +[[deprecated("Use " "new_free().")]] int old_free(); +__attribute__((__deprecated__("Use new_gnu()."))) int old_gnu(); +__declspec(deprecated("Use new_msvc().")) int old_msvc(); +__attribute__((deprecated("Use new_clang().", "new_clang"))) int old_clang(); +""" + config = AutowrapConfigYaml( + functions={ + "old_plain": FunctionData(), + "old_free": FunctionData(), + "old_gnu": FunctionData(), + "old_msvc": FunctionData(), + "old_clang": FunctionData(), + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + functions = {fn.cpp_name: fn for fn in hctx.functions} + + assert all(fn.deprecated is True for fn in functions.values()) + assert functions["old_plain"].doc == [ + '".. warning::\\n"', + '" Deprecated."', + ] + assert "Deprecated: Use new_free()." in unquote_doc(functions["old_free"].doc) + assert "Deprecated: Use new_gnu()." in unquote_doc(functions["old_gnu"].doc) + assert "Deprecated: Use new_msvc()." in unquote_doc(functions["old_msvc"].doc) + assert unquote_doc(functions["old_clang"].doc) == ( + ".. warning::\n Deprecated: Use new_clang()." + ) + + +def test_decodes_escaped_newline_in_deprecation_message(tmp_path): + source = r'[[deprecated("line\nnext")]] int old_lines();' + config = AutowrapConfigYaml(functions={"old_lines": FunctionData()}) + + hctx = parse_deprecated_header(tmp_path, source, config) + + assert unquote_doc(hctx.functions[0].doc) == ( + ".. warning::\n Deprecated: line\n next" + ) + + +def test_concatenates_multiline_adjacent_deprecation_literals(tmp_path): + source = r""" +[[deprecated( + "Use " + "new_multiline()." +)]] int old_multiline(); +""" + config = AutowrapConfigYaml(functions={"old_multiline": FunctionData()}) + + hctx = parse_deprecated_header(tmp_path, source, config) + + assert unquote_doc(hctx.functions[0].doc) == ( + ".. warning::\n Deprecated: Use new_multiline()." + ) + + +def test_marks_each_supported_declaration_kind(tmp_path): + source = r""" +class [[deprecated("Use NewThing.")]] OldThing { +public: + [[deprecated("Use another constructor.")]] OldThing(); + [[deprecated("Use new_method().")]] void old_method(); + [[deprecated("Use new_field.")]] int old_field; + + enum class [[deprecated("Use NewEnum.")]] OldEnum { + OldValue [[deprecated("Use NewValue.")]], + NewValue, + }; + + /** Current documentation. */ + void current_method(); +}; +""" + config = AutowrapConfigYaml( + classes={ + "OldThing": ClassData( + methods={ + "OldThing": FunctionData(), + "old_method": FunctionData(), + "current_method": FunctionData(), + }, + attributes={"old_field": PropData()}, + enums={"OldEnum": EnumData()}, + ) + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + cls = hctx.classes[0] + methods = {method.cpp_name: method for method in cls.wrapped_public_methods} + enum = cls.enums[0] + values = {value.py_name: value for value in enum.values} + + assert cls.deprecated is True + assert methods["OldThing"].deprecated is True + assert methods["old_method"].deprecated is True + assert cls.public_properties[0].deprecated is True + assert enum.deprecated is True + assert values["OldValue"].deprecated is True + + assert methods["current_method"].deprecated is False + assert unquote_doc(methods["current_method"].doc) == "Current documentation." + assert values["NewValue"].deprecated is False + assert values["NewValue"].doc is None + + +def test_doxygen_message_prevents_duplicate_deprecation_warning(tmp_path): + source = r""" +/** Use new_doxygen(). */ +[[deprecated("Use new_doxygen().")]] int old_doxygen(); +""" + config = AutowrapConfigYaml(functions={"old_doxygen": FunctionData()}) + + hctx = parse_deprecated_header(tmp_path, source, config) + doc = unquote_doc(hctx.functions[0].doc) + + assert doc.count("Use new_doxygen().") == 1 + assert ".. warning::" not in doc + + +def test_yaml_message_prevents_duplicate_deprecation_warning(tmp_path): + source = '[[deprecated("Use new_yaml().")]] int old_yaml();' + config = AutowrapConfigYaml( + functions={ + "old_yaml": FunctionData(doc="Use new_yaml()."), + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + doc = unquote_doc(hctx.functions[0].doc) + + assert doc.count("Use new_yaml().") == 1 + assert ".. warning::" not in doc + + +def test_existing_deprecated_text_prevents_generic_warning(tmp_path): + source = "[[deprecated]] int old_documented();" + config = AutowrapConfigYaml( + functions={ + "old_documented": FunctionData(doc="This API is DEPRECATED already."), + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + doc = unquote_doc(hctx.functions[0].doc) + + assert hctx.functions[0].deprecated is True + assert doc == "This API is DEPRECATED already." + assert ".. warning::" not in doc + + +def test_doxygen_and_similar_attribute_names_do_not_mark_declarations(tmp_path): + source = r""" +/** @deprecated Use replacement(). */ +int docs_only(); +[[vendor::not_deprecated("Still current.")]] int similar_attribute(); +""" + config = AutowrapConfigYaml( + functions={ + "docs_only": FunctionData(), + "similar_attribute": FunctionData(), + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + functions = {fn.cpp_name: fn for fn in hctx.functions} + + assert functions["docs_only"].deprecated is False + assert functions["similar_attribute"].deprecated is False + assert functions["similar_attribute"].doc is None + + +def test_decodes_supported_cpp_string_prefixes(tmp_path): + source = r""" +[[deprecated(u8"UTF-8 message")]] int old_u8(); +[[deprecated(u"UTF-16 message")]] int old_u(); +[[deprecated(U"UTF-32 message")]] int old_U(); +[[deprecated(L"wide message")]] int old_L(); +""" + config = AutowrapConfigYaml( + functions={ + "old_u8": FunctionData(), + "old_u": FunctionData(), + "old_U": FunctionData(), + "old_L": FunctionData(), + } + ) + + hctx = parse_deprecated_header(tmp_path, source, config) + docs = {fn.cpp_name: unquote_doc(fn.doc) for fn in hctx.functions} + + assert "Deprecated: UTF-8 message" in docs["old_u8"] + assert "Deprecated: UTF-16 message" in docs["old_u"] + assert "Deprecated: UTF-32 message" in docs["old_U"] + assert "Deprecated: wide message" in docs["old_L"] + + +def test_non_string_attribute_argument_falls_back_to_generic_warning(tmp_path): + source = "[[deprecated(DEPRECATION_MESSAGE)]] int old_macro_message();" + config = AutowrapConfigYaml(functions={"old_macro_message": FunctionData()}) + + hctx = parse_deprecated_header(tmp_path, source, config) + + assert hctx.functions[0].deprecated is True + assert unquote_doc(hctx.functions[0].doc) == ".. warning::\n Deprecated." diff --git a/tests/test_ft_deprecated.py b/tests/test_ft_deprecated.py new file mode 100644 index 00000000..7e3b7269 --- /dev/null +++ b/tests/test_ft_deprecated.py @@ -0,0 +1,95 @@ +import inspect + +import swtest.ft as ft + + +def warning(message: str) -> str: + return f".. warning::\n Deprecated: {message}" + + +def test_deprecated_free_function_docs_and_calls(): + module = ft._ft + + assert module.deprecated_without_message() == 1 + assert ".. warning::\n Deprecated." in inspect.getdoc( + module.deprecated_without_message + ) + + assert module.deprecated_free_function() == 2 + assert warning("Use current_free_function().") in inspect.getdoc( + module.deprecated_free_function + ) + + assert module.deprecated_documented_function() == 3 + documented_doc = inspect.getdoc(module.deprecated_documented_function) + documented_warning = warning("Use current_documented_function().") + assert documented_warning in documented_doc + assert documented_doc.count("Use current_documented_function().") == 1 + + +def test_deprecated_member_docs_and_calls(): + cls = ft._ft.DeprecatedMembers + value = cls() + other = cls() + + assert warning("Use DeprecatedMembers::create().") in inspect.getdoc(cls.__init__) + assert value.deprecated_method() == 4 + assert warning("Use current_method().") in inspect.getdoc(cls.deprecated_method) + assert cls.deprecated_static_method() == 5 + assert warning("Use current_static_method().") in inspect.getdoc( + cls.deprecated_static_method + ) + + assert value == value + assert value != other + assert warning("Use operator!=().") in inspect.getdoc(cls.__eq__) + + value.deprecated_field = 6 + assert value.deprecated_field == 6 + assert warning("Use current_field.") in inspect.getdoc( + inspect.getattr_static(cls, "deprecated_field") + ) + + assert cls.deprecated_static_field == 7 + assert warning("Use current_static_field.") in inspect.getdoc( + inspect.getattr_static(cls, "deprecated_static_field") + ) + + +def test_deprecated_class_and_template_docs_and_calls(): + deprecated_class = ft._ft.DeprecatedClass + assert callable(deprecated_class) + assert deprecated_class().value() == 8 + assert warning("Use CurrentClass.") in inspect.getdoc(deprecated_class) + + deprecated_template = ft._ft.DeprecatedTemplateInt + assert callable(deprecated_template) + assert deprecated_template().get() == 15 + assert warning("Use CurrentTemplate.") in inspect.getdoc(deprecated_template) + + +def test_deprecated_enum_and_value_docs_are_readable(): + deprecated_enum = ft._ft.DeprecatedEnum + assert deprecated_enum.Value.value == 9 + assert warning("Use CurrentEnum.") in inspect.getdoc(deprecated_enum) + assert warning("Use CurrentEnum.") in inspect.getdoc(deprecated_enum.Value) + + mixed_enum = ft._ft.MixedDeprecatedEnum + assert mixed_enum.DeprecatedValue.value == 10 + assert mixed_enum.CurrentValue.value == 11 + deprecated_value_doc = inspect.getdoc(mixed_enum.DeprecatedValue) + assert ".. warning::" in deprecated_value_doc + assert "Deprecated: Use MixedDeprecatedEnum::CurrentValue." in deprecated_value_doc + + +def test_deprecated_virtual_trampoline_member_docs_are_readable(): + cls = ft._ft.DeprecatedVirtual + assert callable(cls.deprecated_virtual) + assert warning("Use current_virtual().") in inspect.getdoc(cls.deprecated_virtual) + assert callable(cls._deprecated_protected_method) + assert warning("Use current_protected_method().") in inspect.getdoc( + cls._deprecated_protected_method + ) + assert warning("Use current_protected_property.") in inspect.getdoc( + inspect.getattr_static(cls, "_deprecated_protected_property") + )