Skip to content

Commit cc6bb47

Browse files
authored
Merge pull request #334 from robotpy/priority
Add configurable overload binding order
2 parents b3cccdb + a1d5133 commit cc6bb47

10 files changed

Lines changed: 293 additions & 9 deletions

File tree

‎docs/config/autowrap.rst‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,38 @@ Semiwrap emits ``py::kw_only()`` before the first exposed argument that retains
9696
a default. An argument with ``param_override.no_default: true`` does not trigger
9797
the keyword-only marker.
9898

99+
Overload binding order
100+
----------------------
101+
102+
When multiple overloads can accept the same Python arguments, pybind11 tries
103+
matching overloads in registration order within each overload-resolution pass.
104+
Use ``binding_order`` to register a preferred overload earlier:
105+
106+
.. code-block:: yaml
107+
108+
classes:
109+
MyClass:
110+
methods:
111+
my_method:
112+
overloads:
113+
SomeType:
114+
binding_order: -1
115+
OtherType:
116+
117+
Lower integers bind first. The default is ``0``, so negative values move an
118+
overload earlier and positive values move it later. Equal values preserve the
119+
existing binding order; the order of entries in YAML does not affect it.
120+
121+
The setting also works for constructors, static and exposed protected methods,
122+
and free functions under ``functions``. It can be set on a function or method
123+
as a default for its overloads; an overload's explicit value (including ``0``)
124+
overrides that default.
125+
126+
Ordering applies within a class (including public and exposed protected methods)
127+
or among free functions generated from one header. It does not reorder classes,
128+
headers, or handwritten ``inline_code`` bindings, and does not change pybind11's
129+
preference for matches that require no argument conversion.
130+
99131
Name transforms
100132
---------------
101133

‎src/semiwrap/autowrap/context.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ class FunctionContext:
227227
# User settings from autowrap_yml.FunctionData
228228
#
229229

230+
#: Lower integers are registered first; ties preserve existing order
231+
binding_order: int
232+
230233
#: If True, don't wrap this, but provide a pure virtual implementation
231234
ignore_pure: bool
232235

‎src/semiwrap/autowrap/cxxparser.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,7 @@ def _on_fn_or_method(
15751575
# info
15761576
# vararg=fn.vararg,
15771577
# user settings
1578+
binding_order=data.binding_order if data.binding_order is not None else 0,
15781579
ignore_pure=data.ignore_pure,
15791580
ignore_py=data.ignore_py,
15801581
cpp_code=data.cpp_code,

‎src/semiwrap/autowrap/render_pybind11.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,14 @@ def cls_def(r: RenderBuffer, cls: ClassContext, varname: str):
440440
if cls.add_default_constructor:
441441
r.writeln(f"{varname}.def(py::init<>(), release_gil());")
442442

443-
for fn in cls.wrapped_public_methods:
444-
genmethod(r, varname, cls.full_cpp_name, fn, None)
445-
443+
methods: T.List[T.Tuple[FunctionContext, T.Optional[str]]] = [
444+
(fn, None) for fn in cls.wrapped_public_methods
445+
]
446446
if cls.trampoline is not None:
447-
for fn in cls.wrapped_protected_methods:
448-
genmethod(r, varname, cls.full_cpp_name, fn, cls.trampoline.var)
447+
methods.extend((fn, cls.trampoline.var) for fn in cls.wrapped_protected_methods)
448+
449+
for fn, trampoline in sorted(methods, key=lambda method: method[0].binding_order):
450+
genmethod(r, varname, cls.full_cpp_name, fn, trampoline)
449451

450452
for prop in cls.public_properties:
451453
_genprop(r, varname, cls.full_cpp_name, prop)

‎src/semiwrap/autowrap/render_wrapped.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,10 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str:
338338
# Global methods
339339
if hctx.functions:
340340
r.writeln()
341-
for index, fn in enumerate(hctx.functions, start=1):
341+
for index, fn in sorted(
342+
enumerate(hctx.functions, start=1),
343+
key=lambda item: item[1].binding_order,
344+
):
342345
if not fn.ignore_py:
343346
r.writeln(
344347
f"{_absolute_qualname(_function_helper_qualname(hctx, fn, index))}"

‎src/semiwrap/config/autowrap_yml.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ class OverloadData:
152152
#: ``defaults.default_args_as_kw_only``.
153153
default_args_as_kw_only: Optional[bool] = None
154154

155+
#: Binding priority: lower integers are registered with pybind11 first.
156+
#: Ties preserve the existing order. Inherits the function/method setting
157+
#: when omitted, otherwise defaults to 0. Applies within a class or within
158+
#: the free functions of a single header, not across headers or classes.
159+
binding_order: Optional[int] = None
160+
155161
#: If True, prepends an underscore to the python name
156162
internal: bool = False
157163

‎tests/cpp/sw-test/semiwrap/ft/overloads.yml‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,109 @@
11
functions:
2+
bindingOrder:
3+
overloads:
4+
int:
5+
long:
6+
binding_order: -1
7+
bindingOrderUnchanged:
8+
overloads:
9+
long:
10+
int:
11+
bindingOrderLater:
12+
overloads:
13+
int:
14+
binding_order: 3
15+
long:
16+
bindingOrderTied:
17+
overloads:
18+
int, int:
19+
long long, int:
20+
binding_order: -2
21+
long:
22+
binding_order: -2
23+
bindingOrderInherited:
24+
binding_order: 2
25+
overloads:
26+
int, int, int:
27+
long, int, int:
28+
long long, int:
29+
binding_order: 0
30+
short:
31+
binding_order: -1
232
fnOverload:
333
overloads:
434
int, int:
535
int:
636
classes:
37+
BindingOrder:
38+
attributes:
39+
selected:
40+
methods:
41+
BindingOrder:
42+
overloads:
43+
int:
44+
binding_order: 1
45+
long:
46+
choose:
47+
overloads:
48+
int:
49+
long:
50+
binding_order: -1
51+
unchanged:
52+
overloads:
53+
int:
54+
long:
55+
chooseStatic:
56+
overloads:
57+
int:
58+
long:
59+
binding_order: -1
60+
later:
61+
overloads:
62+
int:
63+
binding_order: 3
64+
long:
65+
tied:
66+
overloads:
67+
int, int:
68+
long long, int:
69+
binding_order: -2
70+
long:
71+
binding_order: -2
72+
inherited:
73+
binding_order: 2
74+
overloads:
75+
int, int, int:
76+
long, int, int:
77+
long long, int:
78+
binding_order: 0
79+
short:
80+
binding_order: -1
81+
mixedUnchanged:
82+
rename: mixedUnchanged
83+
overloads:
84+
long:
85+
int:
86+
mixedPrioritized:
87+
rename: mixedPrioritized
88+
overloads:
89+
long:
90+
binding_order: -1
91+
int:
92+
chooseProtected:
93+
overloads:
94+
int:
95+
long:
96+
binding_order: -1
97+
BindingOrderTemplate:
98+
template_params:
99+
- T
100+
methods:
101+
choose:
102+
binding_order: 2
103+
overloads:
104+
T:
105+
long:
106+
binding_order: 0
7107
OverloadedObject:
8108
methods:
9109
overloaded:
@@ -31,3 +131,8 @@ classes:
31131
overloaded_private:
32132
overloads:
33133
int:
134+
templates:
135+
BindingOrderTemplateInt:
136+
qualname: BindingOrderTemplate
137+
params:
138+
- int

‎tests/cpp/sw-test/src/swtest/ft/include/overloads.h‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,75 @@
11
#pragma once
22

3-
int fnOverload(int i, int j)
3+
// Template bindings include this header in multiple translation units.
4+
inline int bindingOrder(int value) { return 1; }
5+
inline int bindingOrder(long value) { return 2; }
6+
7+
inline int bindingOrderUnchanged(int value) { return 1; }
8+
inline int bindingOrderUnchanged(long value) { return 2; }
9+
10+
inline int bindingOrderLater(int value) { return 1; }
11+
inline int bindingOrderLater(long value) { return 2; }
12+
13+
inline int bindingOrderTied(int value, int skip = 0) { return 1; }
14+
inline int bindingOrderTied(long value) { return 2; }
15+
inline int bindingOrderTied(long long value, int skip = 0) { return 3; }
16+
17+
inline int bindingOrderInherited(int value, int subset = 0, int inherited = 0) { return 1; }
18+
inline int bindingOrderInherited(long value, int subset = 0, int inherited = 0) { return 2; }
19+
inline int bindingOrderInherited(long long value, int subset = 0) { return 3; }
20+
inline int bindingOrderInherited(short value) { return 4; }
21+
22+
struct BindingOrder {
23+
BindingOrder(int value) : selected(1) {}
24+
BindingOrder(long value) : selected(2) {}
25+
virtual ~BindingOrder() = default;
26+
27+
int choose(int value) { return 1; }
28+
int choose(long value) { return 2; }
29+
int unchanged(int value) { return 1; }
30+
int unchanged(long value) { return 2; }
31+
static int chooseStatic(int value) { return 1; }
32+
static int chooseStatic(long value) { return 2; }
33+
34+
int later(int value) { return 1; }
35+
int later(long value) { return 2; }
36+
37+
int tied(int value, int skip = 0) { return 1; }
38+
int tied(long value) { return 2; }
39+
int tied(long long value, int skip = 0) { return 3; }
40+
41+
int inherited(int value, int subset = 0, int inherited = 0) { return 1; }
42+
int inherited(long value, int subset = 0, int inherited = 0) { return 2; }
43+
int inherited(long long value, int subset = 0) { return 3; }
44+
int inherited(short value) { return 4; }
45+
46+
int selected;
47+
48+
protected:
49+
int chooseProtected(int value) { return 1; }
50+
int chooseProtected(long value) { return 2; }
51+
52+
// Declared before public overloads to test the existing public-first order.
53+
int mixedUnchanged(long value) { return 2; }
54+
int mixedPrioritized(long value) { return 2; }
55+
56+
public:
57+
int mixedUnchanged(int value) { return 1; }
58+
int mixedPrioritized(int value) { return 1; }
59+
};
60+
61+
template <typename T>
62+
struct BindingOrderTemplate {
63+
int choose(T value) { return 1; }
64+
int choose(long value) { return 2; }
65+
};
66+
67+
inline int fnOverload(int i, int j)
468
{
569
return j;
670
}
771

8-
int fnOverload(int i)
72+
inline int fnOverload(int i)
973
{
1074
return i;
1175
}

‎tests/test_binding_order.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import pytest
2+
3+
from swtest.ft import _ft as ft
4+
5+
6+
@pytest.fixture(params=["function", "method"])
7+
def overloads(request):
8+
if request.param == "function":
9+
return (
10+
ft.bindingOrderUnchanged,
11+
ft.bindingOrderLater,
12+
ft.bindingOrderTied,
13+
ft.bindingOrderInherited,
14+
)
15+
obj = ft.BindingOrder(42)
16+
return obj.unchanged, obj.later, obj.tied, obj.inherited
17+
18+
19+
def test_binding_order_defaults_preserve_declaration_order(overloads):
20+
unchanged, _, _, _ = overloads
21+
assert unchanged(42) == 1
22+
23+
24+
def test_positive_binding_order_moves_overload_later(overloads):
25+
_, later, _, _ = overloads
26+
assert later(42) == 2
27+
28+
29+
def test_equal_priorities_preserve_declaration_order(overloads):
30+
_, _, tied, _ = overloads
31+
# The second and third overloads have the same negative priority.
32+
assert tied(42) == 2
33+
# Exclude the second overload: the third still precedes the first.
34+
assert tied(42, skip=0) == 3
35+
36+
37+
def test_binding_order_inheritance_and_explicit_zero(overloads):
38+
_, _, _, inherited = overloads
39+
assert inherited(42) == 4 # Explicit -1 precedes explicit 0 and inherited 2.
40+
# Exclude the fourth overload: explicit 0 overrides the inherited 2.
41+
assert inherited(42, subset=0) == 3
42+
# Exclude both overrides: equal inherited priorities retain original order.
43+
assert inherited(42, inherited=0) == 1
44+
45+
46+
def test_template_method_binding_order():
47+
assert ft.BindingOrderTemplateInt().choose(42) == 2
48+
49+
50+
def test_equal_priorities_preserve_public_before_protected_order():
51+
# The protected overload is declared first, but public bindings came first
52+
# before binding_order was introduced. Both are renamed to the same Python
53+
# name so this tests actual overload resolution across the access boundary.
54+
assert ft.BindingOrder(42).mixedUnchanged(42) == 1
55+
56+
57+
def test_protected_overload_can_precede_public_overload():
58+
assert ft.BindingOrder(42).mixedPrioritized(42) == 2

‎tests/test_ft_overloads.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
from swtest import ft
1+
from swtest.ft import _ft as ft
2+
3+
4+
def test_binding_order_resolves_ambiguous_overloads():
5+
assert ft.bindingOrder(42) == 2
6+
obj = ft.BindingOrder(42)
7+
assert obj.selected == 2
8+
assert obj.choose(42) == 2
9+
assert obj.chooseStatic(42) == 2
10+
assert obj._chooseProtected(42) == 2
11+
assert obj.unchanged(42) == 1
212

313

414
def test_fn_overloads():

0 commit comments

Comments
 (0)