Below is an example of what the adjusted API would look like.
# Current API, renamed to _Tunable
class _Tunable:
"""
A Python-backed tunable value.
Tunables can be published with publish() or added and published in one step
with one of the add() functions.
"""
def __init__(self, value: typing.Any, *, getter: collections.abc.Callable[[], typing.Any] | None = None, setter: collections.abc.Callable[[typing.Any], None] | None = None, on_tune: collections.abc.Callable[[typing.Any], None] | None = None, robust: bool = False, mutable: bool = True, value_type: type[typing.Any] | None = None, element_type: type[typing.Any] | None = None, properties: dict[str, typing.Any] | None = None, type_string: str = '', always_get: bool = False) -> None:
"""
Creates an unpublished tunable. The tunable type is inferred from value
unless an explicit type selector is provided. Bytes, bytearray, and
memoryview values infer a raw tunable. Wrap other buffer exporters in
memoryview to explicitly use their raw bytes in C order.
:param value: initial value
:param getter: optional function that supplies the current local value
:param setter: optional function that receives values set locally or remotely
:param on_tune: callback that receives the value after a remote update
:param robust: whether to separately echo a remotely set value
:param mutable: whether remote updates may change the tunable
:param value_type: explicit value type, or None to infer it from value
:param element_type: explicit sequence element type, or None to infer it
:param properties: additional tunable properties
:param type_string: custom tunable type string
:param always_get: whether to call getter on every backend update
"""
def get(self) -> typing.Any:
"""
Gets the current local value. If a getter was provided, this calls it;
otherwise, it returns the stored value.
:returns: current value
"""
def mutate(self) -> typing.Any:
"""
Gets the current individual WPIStruct object for in-place mutation and
marks the tunable as changed. The object is returned without copying it.
If a getter was provided, it supplies the current object.
Raises TypeError for all other types, including struct arrays. Use get(),
edit the value, and call set() for those types instead.
Call mutate() again before further edits after a backend update.
:returns: struct object to mutate
"""
def set(self, value: typing.Any) -> None:
"""
Sets the local value and marks the tunable as changed. If a setter was
provided, it is called with value. If a getter was also provided, its
result becomes the stored value.
:param value: new value
"""
# New generic API
T = TypeVar("T")
class Tunable(Generic[T]):
"""
A Python-backed tunable value.
Tunables can be published with publish() or added and published in one step
with one of the add() functions.
"""
def __init__(self, value: T, *, getter: collections.abc.Callable[[], T] | None = None, setter: collections.abc.Callable[[T], None] | None = None, on_tune: collections.abc.Callable[[T], None] | None = None, robust: bool = False, mutable: bool = True, value_type: type[T] | None = None, element_type: type[typing.Any] | None = None, properties: dict[str, typing.Any] | None = None, type_string: str = '', always_get: bool = False) -> None:
"""
Creates an unpublished tunable. The tunable type is inferred from value
unless an explicit type selector is provided. Bytes, bytearray, and
memoryview values infer a raw tunable. Wrap other buffer exporters in
memoryview to explicitly use their raw bytes in C order.
:param value: initial value
:param getter: optional function that supplies the current local value
:param setter: optional function that receives values set locally or remotely
:param on_tune: callback that receives the value after a remote update
:param robust: whether to separately echo a remotely set value
:param mutable: whether remote updates may change the tunable
:param value_type: explicit value type, or None to infer it from value
:param element_type: explicit sequence element type, or None to infer it
:param properties: additional tunable properties
:param type_string: custom tunable type string
:param always_get: whether to call getter on every backend update
"""
self._tunable = _Tunable(
value,
getter=getter,
setter=setter,
on_tune=on_tune,
robust=robust,
mutable=mutable,
value_type=value_type,
element_type=element_type,
properties=properties,
type_string=type_string,
always_get=always_get,
)
def get(self) -> T:
"""
Gets the current local value. If a getter was provided, this calls it;
otherwise, it returns the stored value.
:returns: current value
"""
return self._tunable.get()
def mutate(self) -> T:
"""
Gets the current individual WPIStruct object for in-place mutation and
marks the tunable as changed. The object is returned without copying it.
If a getter was provided, it supplies the current object.
Raises TypeError for all other types, including struct arrays. Use get(),
edit the value, and call set() for those types instead.
Call mutate() again before further edits after a backend update.
:returns: struct object to mutate
"""
return self._tunable.mutate()
def set(self, value: T) -> None:
"""
Sets the local value and marks the tunable as changed. If a setter was
provided, it is called with value. If a getter was also provided, its
result becomes the stored value.
:param value: new value
"""
return self._tunable.set(value)
# Example usages
setpoint = Tunable[float](0.0) # setpoint is a float
s: float = setpoint.get() # Good, types match
setpoint.set("string") # Type checking error: Argument 1 to "set" of "Tunable"
# has incompatible type "str"; expected "float"
Currently, all the methods on
Tunableoperate ontyping.Any, which sacrifices type checking on the three APIs (get,set, andmutate). IfTunablewas instead aGeneric, type checking would properly warn of invalid operations.Below is an example of what the adjusted API would look like.