Add capability mixins to the power-meter family#105
Merged
Conversation
Problem: PowerMeterDevice baked wavelength-calibration hooks directly into the base class even though not every meter calibrates by wavelength, and there was no way to introspect what a given meter can do. The laser-source family already solves this with interface-segregated Capability mixins, but the power meters did not mirror that structure. Solution: Introduce hardwarelibrary/powermeters/capabilities.py with a Capability ABC and three orthogonal mixins matching the sources/ pattern: WavelengthCalibratable (wavelength hooks moved out of the base), AutoScalable (toggle automatic ranging), and ScaleAdjustable (read/set the manual full-scale range and enumerate available scales). PowerMeterDevice now keeps only the always-present doGetAbsolutePower hook and gains capabilities()/hasCapability() introspection copied from LaserSourceDevice. IntegraDevice and FieldMasterDevice declare WavelengthCalibratable; their existing hooks satisfy the contract unchanged. Adds TestFieldMasterCapabilities covering the introspection API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ialize Problem: PhysicalDevice.__init__ did not call super().__init__(), and in the MRO of a device that mixes in capabilities (e.g. IntegraDevice(PowerMeterDevice, WavelengthCalibratable)) PhysicalDevice sits ahead of the mixins. The init chain therefore terminated at PhysicalDevice and never reached a mixin's __init__. This forced mixins to fake instance defaults with class attributes, and any future mixin that actually initialized per-instance state would be silently skipped. DebugFieldMasterDevice made it worse by calling PhysicalDevice.__init__ directly, also skipping PowerMeterDevice (so absolutePower was never set). Solution: End PhysicalDevice.__init__ with super().__init__(). It still consumes the device-identity arguments and forwards nothing, so a mixin __init__ must take no required arguments and call super().__init__() itself; this contract is documented on the Capability base. WavelengthCalibratable and ScaleAdjustable now set their per-instance state (calibrationWavelength, scale) in a cooperative __init__ instead of shared class attributes. DebugFieldMasterDevice goes through super().__init__() so it runs the full chain. Verified across families (Matisse, Labjack, power meters); full suite passes (398 passed, 235 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PowerMeterDevicebaked wavelength-calibration hooks directly into the base class even though not every meter calibrates by wavelength, and there was no way to introspect what a given meter can do. The laser-source family (hardwarelibrary/sources/) already models optional features as interface-segregatedCapabilitymixins, but the power meters did not mirror that structure.Solution
Introduce
hardwarelibrary/powermeters/capabilities.py, a mirror ofsources/capabilities.py, with aCapabilityABC and three orthogonal mixins:do*hooksWavelengthCalibratablegetCalibrationWavelength/setCalibrationWavelengthdoGetCalibrationWavelength,doSetCalibrationWavelengthAutoScalableautoScaleIsOn/turnAutoScaleOn/turnAutoScaleOffdoGetAutoScale,doTurnAutoScaleOn,doTurnAutoScaleOffScaleAdjustablegetScale/setScale/availableScalesdoGetScale,doSetScale,doGetAvailableScalesPowerMeterDevicenow keeps only the always-presentdoGetAbsolutePowerhook and gainscapabilities()/hasCapability()introspection, copied fromLaserSourceDevice.WavelengthCalibratable.IntegraDeviceandFieldMasterDevicedeclareWavelengthCalibratable; their existing hooks satisfy the contract unchanged.AutoScalable/ScaleAdjustableare left for future auto-ranging meters — no driver falsely advertises them.AutoScalableandScaleAdjustableare kept fully orthogonal; a driver that needs "auto off before manual scale" handles it in its owndoSetScale.MRO / cooperative init (2nd commit)
Adding the mixins surfaced a latent init bug shared by the whole library.
PhysicalDevice.__init__did not callsuper().__init__(), and in the MRO of a device that mixes in a capability it sits ahead of the mixins:So the init chain terminated at
PhysicalDeviceand a mixin's__init__was never reached — mixins could only fake instance defaults with class attributes, and any future mixin holding real per-instance state would be silently skipped.Fix:
PhysicalDevice.__init__now ends withsuper().__init__(), making it a cooperative base. It still consumes the device-identity arguments and forwards nothing, so the contract (documented onCapability) is: a mixin__init__takes no required arguments and callssuper().__init__()itself.WavelengthCalibratable/ScaleAdjustablenow set their per-instance state (calibrationWavelength,scale) in a cooperative__init__instead of shared class attributes.DebugFieldMasterDeviceroutes throughsuper().__init__()instead of callingPhysicalDevice.__init__directly, so it no longer skipsPowerMeterDevice(absolutePoweris now initialized to0).Verified safe for every multiply-inheriting device (Matisse, Millennia, Labjack, Cobolt, power meters) — none of their mixins define an
__init__to collide with.Tests
Adds
TestFieldMasterCapabilitiescoveringcapabilities()/hasCapability(), including that the driver class does not leak into the reported list. Existing wavelength tests now exercise the mixin. Full suite: 398 passed, 235 skipped, 0 failures.🤖 Generated with Claude Code