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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion docs/config/autowrap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,118 @@ generated by semiwrap. In your ``meson.build`` after you include the generated
)


Name transforms
---------------

Generated Python names for functions, methods, attributes/properties, enum
values, and function/method parameters can be configured with
``name_transform``. Explicit ``rename`` entries are final and are not
transformed. Explicit ``param_override`` ``name`` entries are also final and are
not transformed.

Precedence is:

#. YAML ``name_transform``
#. ``[tool.semiwrap.extension_modules."pkg.name"].name_transform``
#. ``[tool.semiwrap].name_transform``
#. built-in ``default`` behavior

``name_transform`` may be a string or a mapping. String form applies to all
transformable kinds, including enum values:

.. code-block:: yaml

name_transform: snake_case

Mapping form can configure each kind separately:

.. code-block:: yaml

name_transform:
default: snake_case
method: camelCase
attribute: none
enum_value: PascalCase
parameter: snake_case

Mapping keys are ``default``, ``function``, ``method``, ``attribute``,
``enum_value``, ``parameter``, and ``known_words``. The ``known_words`` key is a list
of strings; the other keys are transform specs. Missing kind-specific keys use
``default``.
Mapping values merge across precedence levels, so a YAML file can override only
one kind while preserving project-level settings. A string value applies to
every kind at that precedence level.

Built-in transform names are:

* ``none``: passes the C++ name directly through without changing case.
* ``default``: function and method names lowercase the first character unless
the first two characters are uppercase, while attribute, enum value, and
parameter names are unchanged.
* ``camelCase``: converts names to lower camel case, such as ``GetFoo`` or
``get_foo`` becoming ``getFoo``.
Comment thread
virtuald marked this conversation as resolved.
* ``snake_case``: converts names to lowercase words separated by underscores,
such as ``GetFoo`` or ``getFoo`` becoming ``get_foo``.
* ``PascalCase``: converts names to upper camel case, such as ``getFoo`` or
Comment thread
virtuald marked this conversation as resolved.
``get_foo`` becoming ``GetFoo``.
* ``kCamelCase``: converts names to ``k`` followed by upper camel case, such
as ``getFoo`` or ``get_foo`` becoming ``kGetFoo``. When transforming from
names like ``kGetFoo``, the leading ``k`` prefix is ignored by built-in case
splitting.
* ``CAPS_CASE``: converts names to uppercase words separated by underscores,
such as ``GetFoo`` or ``getFoo`` becoming ``GET_FOO``.

You may also specify a python function to perform custom transform logic.
Custom transforms use ``custom: package.name:function`` and receive
``(name: str, kind: str)`` where kind is ``function``, ``method``,
``attribute``, ``enum_value``, or ``parameter``.

Enum value transforms run after semiwrap's existing enum value prefix stripping.
``rename`` on an enum value remains exact and is not transformed.

Parameter transforms apply to generated Python argument names used in
``py::arg``. They do not change C++ local variable names or the C++ parameter
names used as ``param_override`` keys. ``param_override`` ``name`` values remain
exact and are not transformed.

Known word splitting
~~~~~~~~~~~~~~~~~~~~

Built-in case transforms can split names using a case-sensitive list of known
words. Configure project defaults in ``pyproject.toml`` with the
``known_words`` key under ``[tool.semiwrap.name_transform]``:

.. code-block:: toml

[tool.semiwrap.name_transform]
default = "snake_case"
known_words = ["KiB", "mDNS"]

A YAML file can replace the project known word list for one header with a
top-level ``known_words`` key:

.. code-block:: yaml

name_transform: snake_case
known_words: [KiB, mDNS]

When a YAML file omits ``known_words``, it uses the project known word list.
When a YAML file specifies ``known_words: []``, it uses no known words for
that header.

Known word matching is case-sensitive and prefers the longest configured known
word at each position. For example, with ``KiB`` configured,
``GetKiBValue`` becomes ``get_kib_value`` under ``snake_case`` instead of
``get_ki_b_value``.

Reference
---------

The following strctures describe the dictionaries that are read from the YAML
The following structures describe the dictionaries that are read from the YAML
files. The toplevel structure is :class:`.AutowrapConfigYaml`.

.. autoclass:: semiwrap.name_transform.NameTransformConfig
:members:

.. automodule:: semiwrap.config.autowrap_yml
:members:
85 changes: 69 additions & 16 deletions src/semiwrap/autowrap/cxxparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#

import dataclasses
import functools
import pathlib
import re
import sys
Expand All @@ -15,7 +16,6 @@
else:
Protocol = object # pragma: no cover


import sphinxify
from cxxheaderparser.errors import CxxParseError
from cxxheaderparser.options import ParserOptions
Expand Down Expand Up @@ -92,6 +92,7 @@
TrampolineData,
)

from ..name_transform import NameTransforms, resolve_name_transforms
from ..util import relpath_walk_up


Expand Down Expand Up @@ -342,12 +343,29 @@ def __init__(
gendata: GeneratorData,
casters: CastersData,
report_only: bool,
name_transforms: typing.Optional[NameTransforms] = None,
) -> None:
self.hctx = hctx
self.gendata = gendata
self.user_cfg = gendata.data
self.report_only = report_only
self.casters = casters
self.name_transforms = name_transforms or resolve_name_transforms("default")
self.function_name_transform = functools.partial(
self.name_transforms.function, kind="function"
)
self.method_name_transform = functools.partial(
self.name_transforms.method, kind="method"
)
self.attribute_name_transform = functools.partial(
self.name_transforms.attribute, kind="attribute"
)
self.enum_value_name_transform = functools.partial(
self.name_transforms.enum_value, kind="enum_value"
)
self.parameter_name_transform = functools.partial(
self.name_transforms.parameter, kind="parameter"
)
self.types = set()
self.user_types = set()

Expand Down Expand Up @@ -542,11 +560,13 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
full_cpp_name = f"{enum_scope}::{ename}"
py_name = self._make_py_name(ename, enum_data)

value_prefix = enum_data.value_prefix
if not value_prefix:
value_prefix = ename
value_prefixes = [ename]
if enum_data.value_prefix and enum_data.value_prefix != ename:
value_prefixes.append(enum_data.value_prefix)

strip_prefixes = [f"{value_prefix}_", value_prefix]
strip_prefixes = []
for value_prefix in value_prefixes:
strip_prefixes.extend([f"{value_prefix}_", value_prefix])
else:
full_cpp_name = enum_scope

Expand All @@ -559,7 +579,12 @@ def on_enum(self, state: AWState, enum: EnumDecl) -> None:
values.append(
EnumeratorContext(
full_cpp_name=f"{full_cpp_name}::{name}",
py_name=self._make_py_name(name, v_data, strip_prefixes),
py_name=self._make_py_name(
name,
v_data,
strip_prefixes,
name_transform=self.enum_value_name_transform,
),
doc=self._process_doc(v.doxygen, v_data, append_prefix=" "),
)
)
Expand Down Expand Up @@ -954,14 +979,24 @@ def _on_class_field(
return

self._add_type_caster(f.type)
is_static_constant = f.static and (
f.constexpr or getattr(f.type, "const", False)
)
if propdata.rename:
py_name = propdata.rename
elif f.access != "public":
py_name = f"_{prop_name}"
elif iskeyword(prop_name):
py_name = f"{prop_name}_"
else:
py_name = prop_name
name_transform = (
self.enum_value_name_transform
if is_static_constant
else self.attribute_name_transform
)
py_name = self._make_py_name(
prop_name,
propdata,
name_transform=name_transform,
)
if f.access != "public":
py_name = f"_{py_name}"

if propdata.access == PropAccess.auto:
# const variables can't be written
Expand Down Expand Up @@ -1399,12 +1434,17 @@ def _on_fn_or_method(
elif isinstance(fn, Method) and fn.constructor:
py_name = "__init__"
else:
# Python exposed function name converted to camelcase
if isinstance(fn, Method):
name_transform = self.method_name_transform
else:
name_transform = self.function_name_transform

py_name = self._make_py_name(
fn_name, data, is_operator=fn.operator is not None
fn_name,
data,
is_operator=fn.operator is not None,
name_transform=name_transform,
)
if not py_name[:2].isupper():
py_name = f"{py_name[0].lower()}{py_name[1:]}"

if data.internal or internal:
py_name = f"_{py_name}"
Expand Down Expand Up @@ -1568,10 +1608,12 @@ def _on_fn_param(
param_is_out = False
default = None
disable_none = fn_disable_none
explicit_param_name = False
if param_override is not _default_param_data:
param_is_out = param_override.force_out
if param_override.name:
p_name = param_override.name
explicit_param_name = True
if param_override.x_type:
cpp_type = param_override.x_type
self._add_user_type_caster(cpp_type)
Expand All @@ -1583,8 +1625,14 @@ def _on_fn_param(
disable_none = param_override.disable_none

py_pname = p_name
if not explicit_param_name:
py_pname = self.parameter_name_transform(py_pname)

if iskeyword(py_pname):
py_pname = f"{py_pname}_"
if not py_pname.isidentifier():
if not self.report_only:
raise ValueError(f"name {py_pname!r} is not a valid identifier")

if orig_pname != py_pname:
param_remap[orig_pname] = py_pname
Expand Down Expand Up @@ -1868,6 +1916,7 @@ def _make_py_name(
data: HasNameData,
strip_prefixes: typing.Optional[typing.List[str]] = None,
is_operator: bool = False,
name_transform: typing.Optional[typing.Callable[[str], str]] = None,
):
if data.rename:
return data.rename
Expand All @@ -1883,6 +1932,9 @@ def _make_py_name(
name = n
break

if name_transform is not None:
name = name_transform(name)

if iskeyword(name):
return f"{name}_"
if not name.isidentifier() and not is_operator:
Expand Down Expand Up @@ -2077,6 +2129,7 @@ def parse_header(
parser_options: ParserOptions,
casters: CastersData,
report_only: bool,
name_transforms: typing.Optional[NameTransforms] = None,
) -> HeaderContext:
user_cfg = gendata.data

Expand All @@ -2091,7 +2144,7 @@ def parse_header(
)

# Parse the header using a custom visitor
visitor = AutowrapVisitor(hctx, gendata, casters, report_only)
visitor = AutowrapVisitor(hctx, gendata, casters, report_only, name_transforms)
parser = CxxParser(
str(header_path), None, visitor, parser_options, encoding=user_cfg.encoding
)
Expand Down
Loading
Loading