Skip to content

Commit 9e78a51

Browse files
committed
Replace tunable type selector caster with typing Optional
1 parent 4b18820 commit 9e78a51

8 files changed

Lines changed: 124 additions & 98 deletions

File tree

subprojects/robotpy-tunables/semiwrap/TunableTable.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ classes:
3232
"retained Python values and callbacks."))
3333
.def("add", &wpi::tunables::python::table::Add,
3434
py::arg("name"), py::arg("value"), py::kw_only(),
35-
py::arg("value_type") = std::nullopt,
36-
py::arg("element_type") = std::nullopt,
35+
py::arg("value_type") = py::none(),
36+
py::arg("element_type") = py::none(),
3737
py::arg("robust") = false, py::arg("mutable") = true,
3838
py::arg("on_tune") = std::nullopt,
3939
py::arg("properties") = std::nullopt,
@@ -83,8 +83,8 @@ classes:
8383
"on_tune receives the stored float value."))
8484
.def("publish_value", &wpi::tunables::python::table::PublishValue,
8585
py::arg("name"), py::arg("getter"), py::arg("setter"),
86-
py::kw_only(), py::arg("value_type") = std::nullopt,
87-
py::arg("element_type") = std::nullopt,
86+
py::kw_only(), py::arg("value_type") = py::none(),
87+
py::arg("element_type") = py::none(),
8888
py::arg("robust") = false, py::arg("mutable") = true,
8989
py::arg("properties") = std::nullopt,
9090
py::arg("type_string") = "",

subprojects/robotpy-tunables/tests/test_tunable.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,21 +149,66 @@ def test_tunable_get_set():
149149
assert value.get() == 2
150150

151151

152-
def test_tunable_type_selectors_use_python_types():
153-
integer = tunables.Tunable(1, value_type=int)
154-
strings = tunables.Tunable([], element_type=str)
152+
@pytest.fixture(params=["constructor", "add", "table_add", "publish_value"])
153+
def create_tunable(backend, request):
154+
count = 0
155+
156+
def create(value, **kwargs):
157+
nonlocal count
158+
count += 1
159+
name = f"value{count}"
160+
if request.param == "constructor":
161+
return tunables.Tunable(value, **kwargs)
162+
if request.param == "add":
163+
return tunables.add(name, value, **kwargs)
164+
table = tunables.get_table("table")
165+
if request.param == "table_add":
166+
return table.add(name, value, **kwargs)
167+
return table.publish_value(name, lambda: value, lambda value: None, **kwargs)
168+
169+
return create
170+
171+
172+
def test_tunable_type_selectors_use_python_types(create_tunable):
173+
integer = create_tunable(1, value_type=int)
174+
strings = create_tunable([], element_type=str)
155175

156176
assert integer.get() == 1
157177
assert strings.get() == []
158178

159179
with pytest.raises(TypeError, match="value_type must be a Python type"):
160-
tunables.Tunable(1, value_type="integer")
180+
create_tunable(1, value_type="integer")
161181

162182
with pytest.raises(TypeError, match="element_type must be a Python type"):
163-
tunables.Tunable([], element_type="string")
183+
create_tunable([], element_type="string")
164184

165185
with pytest.raises(TypeError, match="use element_type for sequences"):
166-
tunables.Tunable([], value_type=str)
186+
create_tunable([], value_type=str)
187+
188+
189+
@pytest.mark.parametrize("initial", [1, [1, 2], TunablePoint(1, 2)])
190+
def test_none_type_selectors_infer_tunable_type(create_tunable, initial):
191+
value = create_tunable(initial, value_type=None, element_type=None)
192+
193+
assert value.get() == initial
194+
195+
196+
def test_tunable_type_selectors_are_mutually_exclusive(create_tunable):
197+
with pytest.raises(
198+
TypeError, match="value_type and element_type are mutually exclusive"
199+
):
200+
create_tunable([1], value_type=int, element_type=int)
201+
202+
203+
def test_tunable_type_selector_annotations():
204+
for method in (
205+
tunables.Tunable.__init__,
206+
tunables.add,
207+
tunables.TunableTable.add,
208+
tunables.TunableTable.publish_value,
209+
):
210+
assert "value_type: type[object] | None = None" in method.__doc__
211+
assert "element_type: type[object] | None = None" in method.__doc__
167212

168213

169214
def test_backend_updates_tunables(backend):

subprojects/robotpy-tunables/tunables/src/rpy/PyTunable.cpp

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,26 @@ ValueKind InferSequenceKind(const py::sequence& value) {
161161
}
162162

163163
ValueKind InferValueKind(
164-
py::handle value, const std::optional<PyTunable::PythonType>& valueType,
165-
const std::optional<PyTunable::PythonType>& elementType) {
166-
if (valueType && elementType) {
164+
py::handle value,
165+
const py::typing::Optional<PyTunable::PythonType>& valueType,
166+
const py::typing::Optional<PyTunable::PythonType>& elementType) {
167+
if (!valueType.is_none() && !elementType.is_none()) {
167168
throw py::type_error("value_type and element_type are mutually exclusive");
168169
}
169-
if (elementType) {
170+
if (!elementType.is_none()) {
170171
if (!IsSequenceValue(value)) {
171172
throw py::type_error(
172173
"element_type is only supported for tunable sequences");
173174
}
174-
return KindFromElementType(*elementType);
175+
return KindFromElementType(elementType);
175176
}
176-
if (valueType) {
177+
if (!valueType.is_none()) {
177178
if (IsSequenceValue(value)) {
178179
throw py::type_error(
179180
"value_type is only supported for scalar tunables; use "
180181
"element_type for sequences");
181182
}
182-
return KindFromScalarType(*valueType);
183+
return KindFromScalarType(valueType);
183184
}
184185
if (py::isinstance<py::bool_>(value)) {
185186
return ValueKind::BOOLEAN;
@@ -210,8 +211,8 @@ ValueKind InferValueKind(
210211
PyTunable::PyTunable(py::object value, std::optional<Getter> getter,
211212
std::optional<Setter> setter,
212213
std::optional<TuneCallback> onTune, bool robust,
213-
bool isMutable, std::optional<PythonType> valueType,
214-
std::optional<PythonType> elementType,
214+
bool isMutable, py::typing::Optional<PythonType> valueType,
215+
py::typing::Optional<PythonType> elementType,
215216
std::optional<Properties> properties,
216217
std::string typeString, bool alwaysGet, bool narrowScalar)
217218
: m_getter{std::move(getter)},
@@ -436,8 +437,8 @@ wpi::tunables::TunableConfig PyTunable::MakeConfig(
436437

437438
PyTunable::TunableVariant PyTunable::MakeValue(
438439
py::handle value, bool robust, bool isMutable,
439-
const std::optional<PythonType>& valueType,
440-
const std::optional<PythonType>& elementType,
440+
const py::typing::Optional<PythonType>& valueType,
441+
const py::typing::Optional<PythonType>& elementType,
441442
const std::optional<Properties>& properties, std::string typeString,
442443
bool alwaysGet, bool narrowScalar) {
443444
auto kind = InferValueKind(value, valueType, elementType);
@@ -478,8 +479,8 @@ PyTunable::TunableVariant PyTunable::MakeValue(
478479
return wpi::tunables::TunableStringVector{
479480
value.cast<std::vector<std::string>>(), config};
480481
case ValueKind::STRUCT: {
481-
py::type type = valueType && IsWpiStructType(*valueType)
482-
? py::reinterpret_borrow<py::type>(*valueType)
482+
py::type type = !valueType.is_none() && IsWpiStructType(valueType)
483+
? py::reinterpret_borrow<py::type>(valueType)
483484
: py::type::of(value);
484485
int isInstance = PyObject_IsInstance(value.ptr(), type.ptr());
485486
if (isInstance < 0) {
@@ -496,8 +497,8 @@ PyTunable::TunableVariant PyTunable::MakeValue(
496497
}
497498
case ValueKind::STRUCT_ARRAY: {
498499
auto sequence = py::reinterpret_borrow<py::sequence>(value);
499-
py::type type = elementType && IsWpiStructType(*elementType)
500-
? py::reinterpret_borrow<py::type>(*elementType)
500+
py::type type = !elementType.is_none() && IsWpiStructType(elementType)
501+
? py::reinterpret_borrow<py::type>(elementType)
501502
: GetStructSequenceType(sequence);
502503
ValidateStructSequenceType(sequence, type);
503504
WPyStructInfo info{type};

subprojects/robotpy-tunables/tunables/src/rpy/PyTunable.h

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,6 @@
1616
#include "wpi/tunables/Tunable.hpp"
1717
#include "wpystruct.h"
1818

19-
namespace pybind11::detail {
20-
21-
template <>
22-
struct type_caster<
23-
std::optional<pybind11::typing::Type<pybind11::object>>>
24-
: optional_caster<
25-
std::optional<pybind11::typing::Type<pybind11::object>>> {
26-
bool load(handle src, bool) {
27-
if (!src) {
28-
return false;
29-
}
30-
if (src.is_none()) {
31-
return true;
32-
}
33-
value.emplace(pybind11::reinterpret_borrow<
34-
pybind11::typing::Type<pybind11::object>>(src));
35-
return true;
36-
}
37-
};
38-
39-
} // namespace pybind11::detail
40-
4119
namespace wpi::tunables::python {
4220

4321
/**
@@ -80,8 +58,8 @@ class PyTunable : public std::enable_shared_from_this<PyTunable> {
8058
std::optional<Setter> setter = std::nullopt,
8159
std::optional<TuneCallback> onTune = std::nullopt,
8260
bool robust = false, bool isMutable = true,
83-
std::optional<PythonType> valueType = std::nullopt,
84-
std::optional<PythonType> elementType = std::nullopt,
61+
pybind11::typing::Optional<PythonType> valueType = pybind11::none(),
62+
pybind11::typing::Optional<PythonType> elementType = pybind11::none(),
8563
std::optional<Properties> properties = std::nullopt,
8664
std::string typeString = "", bool alwaysGet = false,
8765
bool narrowScalar = false);
@@ -173,8 +151,8 @@ class PyTunable : public std::enable_shared_from_this<PyTunable> {
173151
bool alwaysGet);
174152
TunableVariant MakeValue(
175153
pybind11::handle value, bool robust, bool isMutable,
176-
const std::optional<PythonType>& valueType,
177-
const std::optional<PythonType>& elementType,
154+
const pybind11::typing::Optional<PythonType>& valueType,
155+
const pybind11::typing::Optional<PythonType>& elementType,
178156
const std::optional<Properties>& properties, std::string typeString,
179157
bool alwaysGet, bool narrowScalar);
180158

0 commit comments

Comments
 (0)