[robotpy] Add wrapper for timestamp functions, like SetNowImpl (#8889)

`SetNowImpl` is used somewhat often in unit tests. It is a little bit
goofier to wrap because it takes a C function, so a little bit more work
has to be done to get that wrapped in pybind.

Claude helped.
This commit is contained in:
PJ Reiniger
2026-05-15 00:53:26 -04:00
committed by GitHub
parent 68d24bb29e
commit 6ba5734a94
7 changed files with 120 additions and 0 deletions

View File

@@ -68,6 +68,14 @@ def wpiutil_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], inclu
tmpl_class_names = [], tmpl_class_names = [],
trampolines = [], trampolines = [],
), ),
struct(
class_name = "Timestamp",
yml_file = "semiwrap/Timestamp.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/util/timestamp.hpp",
tmpl_class_names = [],
trampolines = [],
),
struct( struct(
class_name = "Sendable", class_name = "Sendable",
yml_file = "semiwrap/Sendable.yml", yml_file = "semiwrap/Sendable.yml",

View File

@@ -75,6 +75,7 @@ Synchronization = "wpi/util/Synchronization.hpp"
PixelFormat = "wpi/util/PixelFormat.hpp" PixelFormat = "wpi/util/PixelFormat.hpp"
RawFrame_c = "wpi/util/RawFrame.h" RawFrame_c = "wpi/util/RawFrame.h"
RawFrame = "wpi/util/RawFrame.hpp" RawFrame = "wpi/util/RawFrame.hpp"
Timestamp = "wpi/util/timestamp.hpp"
# wpi/sendable # wpi/sendable
Sendable = "wpi/util/sendable/Sendable.hpp" Sendable = "wpi/util/sendable/Sendable.hpp"

View File

@@ -0,0 +1,6 @@
functions:
NowDefault:
SetNowImpl:
ignore: true # This is more complicated, so it is handled by src/timestmap.cpp
Now:
GetSystemTime:

View File

@@ -4,10 +4,14 @@ from . import _init__wpiutil
from ._wpiutil import ( from ._wpiutil import (
Color, Color,
Color8Bit, Color8Bit,
getSystemTime,
now,
nowDefault,
PixelFormat, PixelFormat,
Sendable, Sendable,
SendableBuilder, SendableBuilder,
SendableRegistry, SendableRegistry,
SetNowImpl,
TimestampSource, TimestampSource,
getStackTrace, getStackTrace,
getStackTraceDefault, getStackTraceDefault,
@@ -16,10 +20,14 @@ from ._wpiutil import (
__all__ = [ __all__ = [
"Color", "Color",
"Color8Bit", "Color8Bit",
"getSystemTime",
"now",
"nowDefault",
"PixelFormat", "PixelFormat",
"Sendable", "Sendable",
"SendableBuilder", "SendableBuilder",
"SendableRegistry", "SendableRegistry",
"SetNowImpl",
"TimestampSource", "TimestampSource",
"getStackTrace", "getStackTrace",
"getStackTraceDefault", "getStackTraceDefault",

View File

@@ -7,6 +7,9 @@ void cleanup_stack_trace_hook();
void setup_safethread_gil(); void setup_safethread_gil();
void cleanup_safethread_gil(); void cleanup_safethread_gil();
void set_now_impl(py::object fn);
void cleanup_now_impl();
#ifndef __FIRST_SYSTEMCORE__ #ifndef __FIRST_SYSTEMCORE__
namespace wpi::util::impl { namespace wpi::util::impl {
@@ -32,10 +35,12 @@ SEMIWRAP_PYBIND11_MODULE(m) {
cleanup_sendable_registry(); cleanup_sendable_registry();
cleanup_stack_trace_hook(); cleanup_stack_trace_hook();
cleanup_safethread_gil(); cleanup_safethread_gil();
cleanup_now_impl();
}); });
setup_safethread_gil(); setup_safethread_gil();
m.def("_setup_stack_trace_hook", &setup_stack_trace_hook); m.def("_setup_stack_trace_hook", &setup_stack_trace_hook);
m.def("SetNowImpl", &set_now_impl);
m.add_object("_st_cleanup", cleanup); m.add_object("_st_cleanup", cleanup);
} }

View File

@@ -0,0 +1,46 @@
#include <pybind11/pybind11.h>
#include <pybind11/functional.h>
#include "wpi/util/timestamp.hpp"
namespace py = pybind11;
py::object &get_now_impl_ref() {
static py::object get_now_impl_ref;
return get_now_impl_ref;
}
// Helper function to massage the python callback into a C api
uint64_t now_impl_trampoline() {
py::gil_scoped_acquire acquire;
try {
auto &hook = get_now_impl_ref();
if (hook) {
return hook().cast<uint64_t>();
}
} catch (py::error_already_set &e) {
e.discard_as_unraisable("wpiutil.now_impl_trampoline");
}
return wpi::util::NowDefault();
}
void set_now_impl(py::object func) {
get_now_impl_ref() = func;
if (func.is_none()) {
wpi::util::SetNowImpl(nullptr);
} else {
wpi::util::SetNowImpl(&now_impl_trampoline);
}
}
void cleanup_now_impl() {
wpi::util::SetNowImpl(nullptr);
// release the function during interpreter shutdown
auto &hook = get_now_impl_ref();
if (hook) {
hook.dec_ref();
hook.release();
}
}

View File

@@ -0,0 +1,46 @@
import wpiutil
import time
import pytest
def test_default():
wpi_now = wpiutil.now() * 1e-6
py_now = int(time.time())
# Allow a one second delta. We don't care about it being all that accurate in the
# test, just that we are in the same galaxy
assert py_now == pytest.approx(wpi_now, abs=1)
NOW_TIMESTAMP_S = 0
def custom_now_getter():
global NOW_TIMESTAMP_S
return int(NOW_TIMESTAMP_S * 1e6)
@pytest.fixture
def custom_fixture():
wpiutil.SetNowImpl(custom_now_getter)
yield
wpiutil.SetNowImpl(None)
def test_custom_timestamp(custom_fixture):
global NOW_TIMESTAMP_S
assert 0 == wpiutil.now()
NOW_TIMESTAMP_S = 1.5
assert 1_500_000 == wpiutil.now()
NOW_TIMESTAMP_S = 100
assert 100_000_000 == wpiutil.now()
# Set it back to the standard implementation and expect its roughly milliseconds since 1970
wpiutil.SetNowImpl(None)
wpi_now = wpiutil.now() * 1e-6
py_now = int(time.time())
assert py_now == pytest.approx(wpi_now, abs=1)