From e85605992cfbea5d989f7acc69a99a0ac6406251 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 12 Sep 2026 18:00:20 +0000 Subject: [PATCH] Add repr bindings for data structs and signals - Fixes #92 --- semiwrap/A301.yml | 22 ++++++ semiwrap/ColorSensorV3.yml | 5 ++ semiwrap/DetachedEncoder.yml | 7 ++ semiwrap/DetachedEncoderLowLevel.yml | 38 +++++++++ semiwrap/ServoChannelConfig.yml | 6 ++ semiwrap/ServoHub.yml | 18 +++++ semiwrap/ServoHubLowLevel.yml | 72 +++++++++++++++++ semiwrap/Signal.yml | 6 ++ semiwrap/SparkBase.yml | 16 ++++ semiwrap/SparkLowLevel.yml | 82 ++++++++++++++++++++ tests/test_repr.py | 112 +++++++++++++++++++++++++++ 11 files changed, 384 insertions(+) create mode 100644 tests/test_repr.py diff --git a/semiwrap/A301.yml b/semiwrap/A301.yml index 35c7b1c..77015fd 100644 --- a/semiwrap/A301.yml +++ b/semiwrap/A301.yml @@ -72,12 +72,25 @@ classes: AbsoluteEncoderPositionPeriodMs: GetAbsoluteEncoderPositionPeriodMs: first::a301::A301::FirmwareVersion: + inline_code: | + .def("__repr__", [](const first::a301::A301::FirmwareVersion &self) { + return py::str("A301.FirmwareVersion(major={!r}, minor={!r}, patch={!r}, prerelease={!r})") + .format(self.major, self.minor, self.patch, self.prerelease); + }); attributes: major: minor: patch: prerelease: first::a301::A301::Faults: + inline_code: | + .def("__repr__", [](const first::a301::A301::Faults &self) { + return py::str("A301.Faults(other={!r}, sensor={!r}, can={!r}, temperature={!r}, " + "gate_driver={!r}, esc_eeprom={!r}, firmware={!r}, " + "motor_startup={!r}, raw_bits={!r})") + .format(self.other, self.sensor, self.can, self.temperature, self.gateDriver, + self.escEeprom, self.firmware, self.motorStartup, self.rawBits); + }); attributes: other: sensor: @@ -94,6 +107,15 @@ classes: "": uint16_t: first::a301::A301::Warnings: + inline_code: | + .def("__repr__", [](const first::a301::A301::Warnings &self) { + return py::str("A301.Warnings(overvoltage={!r}, motor_loop_speed={!r}, " + "brownout={!r}, overcurrent={!r}, esc_eeprom={!r}, ext_eeprom={!r}, " + "sensor={!r}, stall={!r}, has_reset={!r}, other={!r}, raw_bits={!r})") + .format(self.overvoltage, self.motorLoopSpeed, self.brownout, self.overcurrent, + self.escEeprom, self.extEeprom, self.sensor, self.stall, self.hasReset, + self.other, self.rawBits); + }); attributes: overvoltage: motorLoopSpeed: diff --git a/semiwrap/ColorSensorV3.yml b/semiwrap/ColorSensorV3.yml index 0d6bed1..6b953a4 100644 --- a/semiwrap/ColorSensorV3.yml +++ b/semiwrap/ColorSensorV3.yml @@ -22,6 +22,11 @@ classes: HasReset: IsConnected: rev::ColorSensorV3::RawColor: + inline_code: | + .def("__repr__", [](const rev::ColorSensorV3::RawColor &self) { + return py::str("ColorSensorV3.RawColor(red={!r}, green={!r}, blue={!r}, ir={!r})") + .format(self.red, self.green, self.blue, self.ir); + }); attributes: red: green: diff --git a/semiwrap/DetachedEncoder.yml b/semiwrap/DetachedEncoder.yml index 0669f47..4f44e42 100644 --- a/semiwrap/DetachedEncoder.yml +++ b/semiwrap/DetachedEncoder.yml @@ -22,6 +22,13 @@ classes: ClearFaults: DetachedEncoder: rev::detached::DetachedEncoder::Faults: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoder::Faults &self) { + return py::str("DetachedEncoder.Faults(unexpected={!r}, has_reset={!r}, " + "can_tx={!r}, can_rx={!r}, eeprom={!r}, raw_bits={!r})") + .format(self.unexpected, self.hasReset, self.canTx, self.canRx, + self.eeprom, self.rawBits); + }); attributes: unexpected: hasReset: diff --git a/semiwrap/DetachedEncoderLowLevel.yml b/semiwrap/DetachedEncoderLowLevel.yml index 52ad1c5..2ad9b28 100644 --- a/semiwrap/DetachedEncoderLowLevel.yml +++ b/semiwrap/DetachedEncoderLowLevel.yml @@ -18,6 +18,13 @@ classes: DetachedEncoderLowLevel: GetCanPort: rev::detached::DetachedEncoderLowLevel::FirmwareVersion: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::FirmwareVersion &self) { + return py::str("DetachedEncoderLowLevel.FirmwareVersion(year={!r}, minor={!r}, " + "fix={!r}, prerelease={!r}, hardware_major={!r}, hardware_minor={!r})") + .format(self.year, self.minor, self.fix, self.prerelease, + self.hardwareMajor, self.hardwareMinor); + }); attributes: year: minor: @@ -26,9 +33,25 @@ classes: hardwareMajor: hardwareMinor: rev::detached::DetachedEncoderLowLevel::PeriodicStatus0: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::PeriodicStatus0 &self) { + return py::str("DetachedEncoderLowLevel.PeriodicStatus0(model={!r})").format(self.model); + }); attributes: model: rev::detached::DetachedEncoderLowLevel::PeriodicStatus1: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::PeriodicStatus1 &self) { + return py::str("DetachedEncoderLowLevel.PeriodicStatus1(unexpected_fault={!r}, " + "has_reset_fault={!r}, can_tx_fault={!r}, can_rx_fault={!r}, eeprom_fault={!r}, " + "sticky_unexpected_fault={!r}, sticky_has_reset_fault={!r}, " + "sticky_can_tx_fault={!r}, sticky_can_rx_fault={!r}, " + "sticky_eeprom_fault={!r}, timestamp={!r})") + .format(self.unexpectedFault, self.hasResetFault, self.canTxFault, self.canRxFault, + self.eepromFault, self.stickyUnexpectedFault, self.stickyHasResetFault, + self.stickyCanTxFault, self.stickyCanRxFault, self.stickyEepromFault, + self.timestamp); + }); attributes: unexpectedFault: hasResetFault: @@ -42,15 +65,30 @@ classes: stickyEepromFault: timestamp: rev::detached::DetachedEncoderLowLevel::PeriodicStatus2: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::PeriodicStatus2 &self) { + return py::str("DetachedEncoderLowLevel.PeriodicStatus2(raw_angle={!r}, angle={!r}, timestamp={!r})") + .format(self.rawAngle, self.angle, self.timestamp); + }); attributes: rawAngle: angle: timestamp: rev::detached::DetachedEncoderLowLevel::PeriodicStatus3: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::PeriodicStatus3 &self) { + return py::str("DetachedEncoderLowLevel.PeriodicStatus3(position={!r}, timestamp={!r})") + .format(self.position, self.timestamp); + }); attributes: position: timestamp: rev::detached::DetachedEncoderLowLevel::PeriodicStatus4: + inline_code: | + .def("__repr__", [](const rev::detached::DetachedEncoderLowLevel::PeriodicStatus4 &self) { + return py::str("DetachedEncoderLowLevel.PeriodicStatus4(velocity={!r}, timestamp={!r})") + .format(self.velocity, self.timestamp); + }); attributes: velocity: timestamp: diff --git a/semiwrap/ServoChannelConfig.yml b/semiwrap/ServoChannelConfig.yml index 56528e9..5f12d3c 100644 --- a/semiwrap/ServoChannelConfig.yml +++ b/semiwrap/ServoChannelConfig.yml @@ -13,6 +13,12 @@ classes: const PulseRange_t&: DisableBehavior: rev::servohub::ServoChannelConfig::PulseRange_t: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoChannelConfig::PulseRange_t &self) { + return py::str("ServoChannelConfig.PulseRange_t(min_pulse_us={!r}, " + "center_pulse_us={!r}, max_pulse_us={!r})") + .format(self.minPulse_us, self.centerPulse_us, self.maxPulse_us); + }); attributes: minPulse_us: centerPulse_us: diff --git a/semiwrap/ServoHub.yml b/semiwrap/ServoHub.yml index a2cf54f..c24c437 100644 --- a/semiwrap/ServoHub.yml +++ b/semiwrap/ServoHub.yml @@ -33,6 +33,13 @@ classes: GetServoChannel: SetBankPulsePeriod: rev::servohub::ServoHub::Faults: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHub::Faults &self) { + return py::str("ServoHub.Faults(regulator_power_good={!r}, hardware={!r}, " + "firmware={!r}, low_battery={!r}, raw_bits={!r})") + .format(self.regulatorPowerGood, self.hardware, self.firmware, + self.lowBattery, self.rawBits); + }); attributes: regulatorPowerGood: hardware: @@ -45,6 +52,17 @@ classes: "": uint16_t: rev::servohub::ServoHub::Warnings: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHub::Warnings &self) { + return py::str("ServoHub.Warnings(brownout={!r}, can_warning={!r}, can_bus_off={!r}, " + "has_reset={!r}, channel0_overcurrent={!r}, channel1_overcurrent={!r}, " + "channel2_overcurrent={!r}, channel3_overcurrent={!r}, " + "channel4_overcurrent={!r}, channel5_overcurrent={!r}, raw_bits={!r})") + .format(self.brownout, self.canWarning, self.canBusOff, self.hasReset, + self.channel0Overcurrent, self.channel1Overcurrent, self.channel2Overcurrent, + self.channel3Overcurrent, self.channel4Overcurrent, self.channel5Overcurrent, + self.rawBits); + }); attributes: brownout: canWarning: diff --git a/semiwrap/ServoHubLowLevel.yml b/semiwrap/ServoHubLowLevel.yml index 8534bb2..f5e545e 100644 --- a/semiwrap/ServoHubLowLevel.yml +++ b/semiwrap/ServoHubLowLevel.yml @@ -22,6 +22,13 @@ classes: ServoHubLowLevel: GetCanPort: rev::servohub::ServoHubLowLevel::FirmwareVersion: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::FirmwareVersion &self) { + return py::str("ServoHubLowLevel.FirmwareVersion(firmware_fix={!r}, firmware_minor={!r}, " + "firmware_year={!r}, hardware_minor={!r}, hardware_major={!r})") + .format(self.firmwareFix, self.firmwareMinor, self.firmwareYear, + self.hardwareMinor, self.hardwareMajor); + }); attributes: firmwareFix: firmwareMinor: @@ -29,6 +36,16 @@ classes: hardwareMinor: hardwareMajor: rev::servohub::ServoHubLowLevel::PeriodicStatus0: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::PeriodicStatus0 &self) { + return py::str("ServoHubLowLevel.PeriodicStatus0(voltage={!r}, servo_voltage={!r}, " + "device_current={!r}, primary_heartbeat_lock={!r}, system_enabled={!r}, " + "communication_mode={!r}, programming_enabled={!r}, " + "actively_programming={!r}, timestamp={!r})") + .format(self.voltage, self.servoVoltage, self.deviceCurrent, self.primaryHeartbeatLock, + self.systemEnabled, self.communicationMode, self.programmingEnabled, + self.activelyProgramming, self.timestamp); + }); attributes: voltage: servoVoltage: @@ -40,6 +57,31 @@ classes: activelyProgramming: timestamp: rev::servohub::ServoHubLowLevel::PeriodicStatus1: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::PeriodicStatus1 &self) { + return py::str("ServoHubLowLevel.PeriodicStatus1(regulator_power_good_fault={!r}, " + "brownout={!r}, can_warning={!r}, can_bus_off={!r}, hardware_fault={!r}, " + "firmware_fault={!r}, has_reset={!r}, low_battery_fault={!r}, " + "channel0_overcurrent={!r}, channel1_overcurrent={!r}, " + "channel2_overcurrent={!r}, channel3_overcurrent={!r}, " + "channel4_overcurrent={!r}, channel5_overcurrent={!r}, " + "sticky_regulator_power_good_fault={!r}, sticky_brownout={!r}, " + "sticky_can_warning={!r}, sticky_can_bus_off={!r}, sticky_hardware_fault={!r}, " + "sticky_firmware_fault={!r}, sticky_has_reset={!r}, sticky_low_battery_fault={!r}, " + "sticky_channel0_overcurrent={!r}, sticky_channel1_overcurrent={!r}, " + "sticky_channel2_overcurrent={!r}, sticky_channel3_overcurrent={!r}, " + "sticky_channel4_overcurrent={!r}, sticky_channel5_overcurrent={!r}, timestamp={!r})") + .format(self.regulatorPowerGoodFault, self.brownout, self.canWarning, self.canBusOff, + self.hardwareFault, self.firmwareFault, self.hasReset, self.lowBatteryFault, + self.channel0Overcurrent, self.channel1Overcurrent, self.channel2Overcurrent, + self.channel3Overcurrent, self.channel4Overcurrent, self.channel5Overcurrent, + self.stickyRegulatorPowerGoodFault, self.stickyBrownout, self.stickyCanWarning, + self.stickyCanBusOff, self.stickyHardwareFault, self.stickyFirmwareFault, + self.stickyHasReset, self.stickyLowBatteryFault, self.stickyChannel0Overcurrent, + self.stickyChannel1Overcurrent, self.stickyChannel2Overcurrent, + self.stickyChannel3Overcurrent, self.stickyChannel4Overcurrent, + self.stickyChannel5Overcurrent, self.timestamp); + }); attributes: regulatorPowerGoodFault: brownout: @@ -71,6 +113,17 @@ classes: lowBatteryFault: stickyLowBatteryFault: rev::servohub::ServoHubLowLevel::PeriodicStatus2: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::PeriodicStatus2 &self) { + return py::str("ServoHubLowLevel.PeriodicStatus2(channel0_pulse_width={!r}, " + "channel1_pulse_width={!r}, channel2_pulse_width={!r}, channel0_enabled={!r}, " + "channel1_enabled={!r}, channel2_enabled={!r}, channel0_out_of_range={!r}, " + "channel1_out_of_range={!r}, channel2_out_of_range={!r}, timestamp={!r})") + .format(self.channel0PulseWidth, self.channel1PulseWidth, self.channel2PulseWidth, + self.channel0Enabled, self.channel1Enabled, self.channel2Enabled, + self.channel0OutOfRange, self.channel1OutOfRange, self.channel2OutOfRange, + self.timestamp); + }); attributes: channel0PulseWidth: channel1PulseWidth: @@ -83,6 +136,17 @@ classes: channel2OutOfRange: timestamp: rev::servohub::ServoHubLowLevel::PeriodicStatus3: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::PeriodicStatus3 &self) { + return py::str("ServoHubLowLevel.PeriodicStatus3(channel3_pulse_width={!r}, " + "channel4_pulse_width={!r}, channel5_pulse_width={!r}, channel3_enabled={!r}, " + "channel4_enabled={!r}, channel5_enabled={!r}, channel3_out_of_range={!r}, " + "channel4_out_of_range={!r}, channel5_out_of_range={!r}, timestamp={!r})") + .format(self.channel3PulseWidth, self.channel4PulseWidth, self.channel5PulseWidth, + self.channel3Enabled, self.channel4Enabled, self.channel5Enabled, + self.channel3OutOfRange, self.channel4OutOfRange, self.channel5OutOfRange, + self.timestamp); + }); attributes: channel3PulseWidth: channel4PulseWidth: @@ -95,6 +159,14 @@ classes: channel5OutOfRange: timestamp: rev::servohub::ServoHubLowLevel::PeriodicStatus4: + inline_code: | + .def("__repr__", [](const rev::servohub::ServoHubLowLevel::PeriodicStatus4 &self) { + return py::str("ServoHubLowLevel.PeriodicStatus4(channel0_current={!r}, channel1_current={!r}, " + "channel2_current={!r}, channel3_current={!r}, channel4_current={!r}, " + "channel5_current={!r}, timestamp={!r})") + .format(self.channel0Current, self.channel1Current, self.channel2Current, + self.channel3Current, self.channel4Current, self.channel5Current, self.timestamp); + }); attributes: channel0Current: channel1Current: diff --git a/semiwrap/Signal.yml b/semiwrap/Signal.yml index 1231475..f02966c 100644 --- a/semiwrap/Signal.yml +++ b/semiwrap/Signal.yml @@ -6,6 +6,12 @@ extra_includes_first: classes: rev::util::Signal: + template_inline_code: | + std::string clsNameCopy = clsName; + cls_Signal.def("__repr__", [clsNameCopy](const rev::util::Signal &self) { + return py::str("{}(value={!r}, error={!r}, timestamp={!r})") + .format(clsNameCopy, self.Get(), self.GetError(), self.GetTimestamp()); + }); template_params: - T typealias: diff --git a/semiwrap/SparkBase.yml b/semiwrap/SparkBase.yml index b621500..58badc1 100644 --- a/semiwrap/SparkBase.yml +++ b/semiwrap/SparkBase.yml @@ -112,6 +112,14 @@ classes: SetThrottle: GetThrottle: rev::spark::SparkBase::Faults: + inline_code: | + .def("__repr__", [](const rev::spark::SparkBase::Faults &self) { + return py::str("SparkBase.Faults(other={!r}, motor_type={!r}, sensor={!r}, " + "can={!r}, temperature={!r}, gate_driver={!r}, esc_eeprom={!r}, " + "firmware={!r}, raw_bits={!r})") + .format(self.other, self.motorType, self.sensor, self.can, self.temperature, + self.gateDriver, self.escEeprom, self.firmware, self.rawBits); + }); attributes: other: motorType: @@ -128,6 +136,14 @@ classes: "": uint16_t: rev::spark::SparkBase::Warnings: + inline_code: | + .def("__repr__", [](const rev::spark::SparkBase::Warnings &self) { + return py::str("SparkBase.Warnings(brownout={!r}, overcurrent={!r}, " + "esc_eeprom={!r}, ext_eeprom={!r}, sensor={!r}, stall={!r}, " + "has_reset={!r}, other={!r}, raw_bits={!r})") + .format(self.brownout, self.overcurrent, self.escEeprom, self.extEeprom, + self.sensor, self.stall, self.hasReset, self.other, self.rawBits); + }); attributes: brownout: overcurrent: diff --git a/semiwrap/SparkLowLevel.yml b/semiwrap/SparkLowLevel.yml index 0b19aa2..74ee0e4 100644 --- a/semiwrap/SparkLowLevel.yml +++ b/semiwrap/SparkLowLevel.yml @@ -57,6 +57,17 @@ classes: GetPeriodicStatus8: GetPeriodicStatus9: rev::spark::SparkLowLevel::PeriodicStatus0: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus0 &self) { + return py::str("SparkLowLevel.PeriodicStatus0(applied_output={!r}, voltage={!r}, current={!r}, " + "motor_temperature={!r}, hard_forward_limit_reached={!r}, " + "hard_reverse_limit_reached={!r}, soft_forward_limit_reached={!r}, " + "soft_reverse_limit_reached={!r}, inverted={!r}, primary_heartbeat_lock={!r}, timestamp={!r})") + .format(self.appliedOutput, self.voltage, self.current, self.motorTemperature, + self.hardForwardLimitReached, self.hardReverseLimitReached, + self.softForwardLimitReached, self.softReverseLimitReached, self.inverted, + self.primaryHeartbeatLock, self.timestamp); + }); attributes: appliedOutput: voltage: @@ -70,6 +81,30 @@ classes: primaryHeartbeatLock: timestamp: rev::spark::SparkLowLevel::PeriodicStatus1: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus1 &self) { + return py::str("SparkLowLevel.PeriodicStatus1(other_fault={!r}, motor_type_fault={!r}, " + "sensor_fault={!r}, can_fault={!r}, temperature_fault={!r}, drv_fault={!r}, " + "esc_eeprom_fault={!r}, firmware_fault={!r}, brownout_warning={!r}, " + "overcurrent_warning={!r}, esc_eeprom_warning={!r}, ext_eeprom_warning={!r}, " + "sensor_warning={!r}, stall_warning={!r}, has_reset_warning={!r}, other_warning={!r}, " + "other_sticky_fault={!r}, motor_type_sticky_fault={!r}, sensor_sticky_fault={!r}, " + "can_sticky_fault={!r}, temperature_sticky_fault={!r}, drv_sticky_fault={!r}, " + "esc_eeprom_sticky_fault={!r}, firmware_sticky_fault={!r}, brownout_sticky_warning={!r}, " + "overcurrent_sticky_warning={!r}, esc_eeprom_sticky_warning={!r}, ext_eeprom_sticky_warning={!r}, " + "sensor_sticky_warning={!r}, stall_sticky_warning={!r}, has_reset_sticky_warning={!r}, " + "other_sticky_warning={!r}, is_follower={!r}, timestamp={!r})") + .format(self.otherFault, self.motorTypeFault, self.sensorFault, self.canFault, + self.temperatureFault, self.drvFault, self.escEepromFault, self.firmwareFault, + self.brownoutWarning, self.overcurrentWarning, self.escEepromWarning, + self.extEepromWarning, self.sensorWarning, self.stallWarning, self.hasResetWarning, + self.otherWarning, self.otherStickyFault, self.motorTypeStickyFault, + self.sensorStickyFault, self.canStickyFault, self.temperatureStickyFault, + self.drvStickyFault, self.escEepromStickyFault, self.firmwareStickyFault, + self.brownoutStickyWarning, self.overcurrentStickyWarning, self.escEepromStickyWarning, + self.extEepromStickyWarning, self.sensorStickyWarning, self.stallStickyWarning, + self.hasResetStickyWarning, self.otherStickyWarning, self.isFollower, self.timestamp); + }); attributes: otherFault: motorTypeFault: @@ -106,43 +141,90 @@ classes: isFollower: timestamp: rev::spark::SparkLowLevel::PeriodicStatus2: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus2 &self) { + return py::str("SparkLowLevel.PeriodicStatus2(primary_encoder_velocity={!r}, " + "primary_encoder_position={!r}, timestamp={!r})") + .format(self.primaryEncoderVelocity, self.primaryEncoderPosition, self.timestamp); + }); attributes: primaryEncoderVelocity: primaryEncoderPosition: timestamp: rev::spark::SparkLowLevel::PeriodicStatus3: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus3 &self) { + return py::str("SparkLowLevel.PeriodicStatus3(analog_voltage={!r}, analog_velocity={!r}, " + "analog_position={!r}, timestamp={!r})") + .format(self.analogVoltage, self.analogVelocity, self.analogPosition, self.timestamp); + }); attributes: analogVoltage: analogVelocity: analogPosition: timestamp: rev::spark::SparkLowLevel::PeriodicStatus4: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus4 &self) { + return py::str("SparkLowLevel.PeriodicStatus4(external_or_alt_encoder_velocity={!r}, " + "external_or_alt_encoder_position={!r}, timestamp={!r})") + .format(self.externalOrAltEncoderVelocity, self.externalOrAltEncoderPosition, self.timestamp); + }); attributes: externalOrAltEncoderVelocity: externalOrAltEncoderPosition: timestamp: rev::spark::SparkLowLevel::PeriodicStatus5: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus5 &self) { + return py::str("SparkLowLevel.PeriodicStatus5(duty_cycle_encoder_velocity={!r}, " + "duty_cycle_encoder_position={!r}, timestamp={!r})") + .format(self.dutyCycleEncoderVelocity, self.dutyCycleEncoderPosition, self.timestamp); + }); attributes: dutyCycleEncoderVelocity: dutyCycleEncoderPosition: timestamp: rev::spark::SparkLowLevel::PeriodicStatus6: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus6 &self) { + return py::str("SparkLowLevel.PeriodicStatus6(unadjusted_duty_cycle={!r}, duty_cycle_period={!r}, " + "duty_cycle_no_signal={!r}, timestamp={!r})") + .format(self.unadjustedDutyCycle, self.dutyCyclePeriod, self.dutyCycleNoSignal, self.timestamp); + }); attributes: unadjustedDutyCycle: dutyCyclePeriod: dutyCycleNoSignal: timestamp: rev::spark::SparkLowLevel::PeriodicStatus7: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus7 &self) { + return py::str("SparkLowLevel.PeriodicStatus7(i_accumulation={!r}, timestamp={!r})") + .format(self.iAccumulation, self.timestamp); + }); attributes: iAccumulation: timestamp: rev::spark::SparkLowLevel::PeriodicStatus8: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus8 &self) { + return py::str("SparkLowLevel.PeriodicStatus8(setpoint={!r}, is_at_setpoint={!r}, " + "selected_pid_slot={!r}, timestamp={!r})") + .format(self.setpoint, self.isAtSetpoint, self.selectedPidSlot, self.timestamp); + }); attributes: setpoint: isAtSetpoint: selectedPidSlot: timestamp: rev::spark::SparkLowLevel::PeriodicStatus9: + inline_code: | + .def("__repr__", [](const rev::spark::SparkLowLevel::PeriodicStatus9 &self) { + return py::str("SparkLowLevel.PeriodicStatus9(max_motion_setpoint_position={!r}, " + "max_motion_setpoint_velocity={!r}, timestamp={!r})") + .format(self.maxMotionSetpointPosition, self.maxMotionSetpointVelocity, self.timestamp); + }); attributes: maxMotionSetpointPosition: maxMotionSetpointVelocity: diff --git a/tests/test_repr.py b/tests/test_repr.py new file mode 100644 index 0000000..290d92c --- /dev/null +++ b/tests/test_repr.py @@ -0,0 +1,112 @@ +import pytest + +import rev + +STRUCT_TYPES = [ + rev.SparkBase.Faults, + rev.SparkBase.Warnings, + rev.DetachedEncoder.Faults, + rev.ServoHub.Faults, + rev.ServoHub.Warnings, + rev.A301.FirmwareVersion, + rev.A301.Faults, + rev.A301.Warnings, + rev.ServoChannelConfig.PulseRange_t, + *[getattr(rev.SparkLowLevel, f"PeriodicStatus{i}") for i in range(10)], + rev.DetachedEncoderLowLevel.FirmwareVersion, + *[getattr(rev.DetachedEncoderLowLevel, f"PeriodicStatus{i}") for i in range(5)], + rev.ServoHubLowLevel.FirmwareVersion, + *[getattr(rev.ServoHubLowLevel, f"PeriodicStatus{i}") for i in range(5)], +] + + +@pytest.mark.parametrize("cls", STRUCT_TYPES, ids=lambda cls: cls.__qualname__) +def test_struct_repr_includes_all_fields_and_current_values(cls): + fields = [name for name, attr in vars(cls).items() if isinstance(attr, property)] + # Change one field at a time to catch swapped boolean arguments as well. + for changed_field in (None, *fields): + value = cls() + expected = {} + for index, name in enumerate(fields, 1): + current = getattr(value, name) + if name == changed_field: + if isinstance(current, bool): + current = not current + elif isinstance(current, float): + current = index + 0.25 + elif isinstance(current, int): + current = index + else: + current = list(type(current).__members__.values())[-1] + setattr(value, name, current) + expected[name] = repr(current) + + result = repr(value) + assert result.startswith(f"{cls.__qualname__}(") + assert result.endswith(")") + # Compare fields independently of declaration order. + actual = dict( + field.split("=", 1) for field in result.split("(", 1)[1][:-1].split(", ") + ) + assert actual == expected + + +def test_raw_color_repr(): + value = rev.ColorSensorV3.RawColor(1, 20, 300, 4000) + assert repr(value) == "ColorSensorV3.RawColor(red=1, green=20, blue=300, ir=4000)" + value.green = 42 + assert repr(value) == "ColorSensorV3.RawColor(red=1, green=42, blue=300, ir=4000)" + + +def test_periodic_status_repr_with_enum_and_boolean(): + value = rev.SparkLowLevel.PeriodicStatus8() + value.setpoint = -1.25 + value.is_at_setpoint = True + value.selected_pid_slot = rev.ClosedLoopSlot.SLOT2 + value.timestamp = 1234567890123 + assert repr(value) == ( + "SparkLowLevel.PeriodicStatus8(setpoint=-1.25, is_at_setpoint=True, " + "selected_pid_slot=, timestamp=1234567890123)" + ) + + +def signal_value(name): + scalar_values = { + "double": -1.25, + "bool": True, + "int": -42, + "ClosedLoopSlot": rev.ClosedLoopSlot.SLOT2, + "A301GearboxRPM": list(rev.A301.GearboxRPM.__members__.values())[-1], + } + if name in scalar_values: + return scalar_values[name] + for prefix, device, low_level in ( + ("Spark", rev.SparkBase, rev.SparkLowLevel), + ("Detached", rev.DetachedEncoder, rev.DetachedEncoderLowLevel), + ("ServoHub", rev.ServoHub, rev.ServoHubLowLevel), + ("A301", rev.A301, rev.A301), + ): + if name.startswith(prefix): + suffix = name[len(prefix) :] + parent = low_level if suffix.startswith("PeriodicStatus") else device + return getattr(parent, suffix)() + raise AssertionError(f"Unhandled Signal specialization: {name}") + + +@pytest.mark.parametrize( + "name", [name for name in dir(rev) if name.startswith("Signal_")] +) +@pytest.mark.parametrize("error", [rev.REVLibError.OK, rev.REVLibError.TIMEOUT]) +def test_signal_repr_includes_value_error_and_timestamp(name, error): + value = signal_value(name.removeprefix("Signal_")) + signal = getattr(rev, name)(value, error, 1234567890123) + assert repr(signal) == ( + f"{name}(value={value!r}, error={error!r}, timestamp=1234567890123)" + ) + + +def test_invalid_signal_repr_preserves_stored_value(): + signal = rev.Signal_double(-1.25, rev.REVLibError.TIMEOUT, 0) + assert repr(signal) == ( + "Signal_double(value=-1.25, error=, timestamp=0)" + )