[tunables] Make `Tunable` a generic instead of using `typing.Any`
- Dominant language
- Python
- Stars
- 27
- Forks
- 29
- Avg merge
- 4d 6h
- Merged PRs (30d)
- 12
Description
Currently, all the methods on `Tunable` operate on `typing.Any`, which sacrifices type checking on the three APIs (`get`, `set`, and `mutate`). If `Tunable` was instead a `Generic`, type checking would properly warn of invalid operations.
Below is an example of what the adjusted API would look like.
```py
# 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"
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by locating the Python Tunable implementation and existing tests for get(), set(), and mutate(). Compare the current Any-based API with the generic API shown in the issue, then run the relevant test and type-checking suites; done means these APIs preserve behavior while exposing the value type to static checking.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- developer-experience
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100