From 6ba5734a941d5dc21bd9238bda95be8e5e8db5b8 Mon Sep 17 00:00:00 2001 From: PJ Reiniger Date: Fri, 15 May 2026 00:53:26 -0400 Subject: [PATCH] [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. --- wpiutil/robotpy_pybind_build_info.bzl | 8 ++++ wpiutil/src/main/python/pyproject.toml | 1 + .../src/main/python/semiwrap/Timestamp.yml | 6 +++ wpiutil/src/main/python/wpiutil/__init__.py | 8 ++++ wpiutil/src/main/python/wpiutil/src/main.cpp | 5 ++ .../src/main/python/wpiutil/src/timestamp.cpp | 46 +++++++++++++++++++ wpiutil/src/test/python/test_timestamp.py | 46 +++++++++++++++++++ 7 files changed, 120 insertions(+) create mode 100644 wpiutil/src/main/python/semiwrap/Timestamp.yml create mode 100644 wpiutil/src/main/python/wpiutil/src/timestamp.cpp create mode 100644 wpiutil/src/test/python/test_timestamp.py diff --git a/wpiutil/robotpy_pybind_build_info.bzl b/wpiutil/robotpy_pybind_build_info.bzl index c525f62315..85bf66752c 100644 --- a/wpiutil/robotpy_pybind_build_info.bzl +++ b/wpiutil/robotpy_pybind_build_info.bzl @@ -68,6 +68,14 @@ def wpiutil_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], inclu tmpl_class_names = [], 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( class_name = "Sendable", yml_file = "semiwrap/Sendable.yml", diff --git a/wpiutil/src/main/python/pyproject.toml b/wpiutil/src/main/python/pyproject.toml index 0453ed35f9..010b983a44 100644 --- a/wpiutil/src/main/python/pyproject.toml +++ b/wpiutil/src/main/python/pyproject.toml @@ -75,6 +75,7 @@ Synchronization = "wpi/util/Synchronization.hpp" PixelFormat = "wpi/util/PixelFormat.hpp" RawFrame_c = "wpi/util/RawFrame.h" RawFrame = "wpi/util/RawFrame.hpp" +Timestamp = "wpi/util/timestamp.hpp" # wpi/sendable Sendable = "wpi/util/sendable/Sendable.hpp" diff --git a/wpiutil/src/main/python/semiwrap/Timestamp.yml b/wpiutil/src/main/python/semiwrap/Timestamp.yml new file mode 100644 index 0000000000..6792d61bea --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/Timestamp.yml @@ -0,0 +1,6 @@ +functions: + NowDefault: + SetNowImpl: + ignore: true # This is more complicated, so it is handled by src/timestmap.cpp + Now: + GetSystemTime: diff --git a/wpiutil/src/main/python/wpiutil/__init__.py b/wpiutil/src/main/python/wpiutil/__init__.py index 9896d1db26..f4dd1a6a36 100644 --- a/wpiutil/src/main/python/wpiutil/__init__.py +++ b/wpiutil/src/main/python/wpiutil/__init__.py @@ -4,10 +4,14 @@ from . import _init__wpiutil from ._wpiutil import ( Color, Color8Bit, + getSystemTime, + now, + nowDefault, PixelFormat, Sendable, SendableBuilder, SendableRegistry, + SetNowImpl, TimestampSource, getStackTrace, getStackTraceDefault, @@ -16,10 +20,14 @@ from ._wpiutil import ( __all__ = [ "Color", "Color8Bit", + "getSystemTime", + "now", + "nowDefault", "PixelFormat", "Sendable", "SendableBuilder", "SendableRegistry", + "SetNowImpl", "TimestampSource", "getStackTrace", "getStackTraceDefault", diff --git a/wpiutil/src/main/python/wpiutil/src/main.cpp b/wpiutil/src/main/python/wpiutil/src/main.cpp index 785c8cdc5a..c540274fd2 100644 --- a/wpiutil/src/main/python/wpiutil/src/main.cpp +++ b/wpiutil/src/main/python/wpiutil/src/main.cpp @@ -7,6 +7,9 @@ void cleanup_stack_trace_hook(); void setup_safethread_gil(); void cleanup_safethread_gil(); +void set_now_impl(py::object fn); +void cleanup_now_impl(); + #ifndef __FIRST_SYSTEMCORE__ namespace wpi::util::impl { @@ -32,10 +35,12 @@ SEMIWRAP_PYBIND11_MODULE(m) { cleanup_sendable_registry(); cleanup_stack_trace_hook(); cleanup_safethread_gil(); + cleanup_now_impl(); }); setup_safethread_gil(); m.def("_setup_stack_trace_hook", &setup_stack_trace_hook); + m.def("SetNowImpl", &set_now_impl); m.add_object("_st_cleanup", cleanup); } diff --git a/wpiutil/src/main/python/wpiutil/src/timestamp.cpp b/wpiutil/src/main/python/wpiutil/src/timestamp.cpp new file mode 100644 index 0000000000..8ba04b0b6d --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/timestamp.cpp @@ -0,0 +1,46 @@ +#include +#include + +#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(); + } + } 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(); + } +} \ No newline at end of file diff --git a/wpiutil/src/test/python/test_timestamp.py b/wpiutil/src/test/python/test_timestamp.py new file mode 100644 index 0000000000..7bee1ae44e --- /dev/null +++ b/wpiutil/src/test/python/test_timestamp.py @@ -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) + +