Summary
CoordinateAxis.__eq__ resolves through the MRO to the dataclass-generated AxisBase.__eq__, which compares only unit. ArrayWithNamedDims.__eq__ — written to compare dims and data — is never reached, so two coordinate axes with different data, different dtypes, or different lengths compare equal.
Reproducer
import numpy as np
from ezmsg.util.messages.axisarray import AxisArray
C = AxisArray.CoordinateAxis
print(C.__eq__.__qualname__)
# AxisBase.__eq__
print([c.__name__ for c in C.__mro__])
# ['CoordinateAxis', 'AxisBase', 'ABC', 'ArrayWithNamedDims', 'object']
a = C(data=np.zeros(3), dims=['ch'], unit='')
b = C(data=np.ones(5), dims=['ch'], unit='')
print(a == b) # True <-- different values AND different lengths
# The case that bites in practice: a structured channel axis being relabelled.
D = np.dtype([('label', 'U8')])
x = np.zeros(2, dtype=D); x['label'] = ['a', 'b']
y = np.zeros(2, dtype=D); y['label'] = ['c', 'd']
print(C(data=x, dims=['ch'], unit='') == C(data=y, dims=['ch'], unit='')) # True
Observed on ezmsg 3.9.0 (v3.9.0-10-gc9572db).
Cause
AxisBase is a plain @dataclass, so it gets a generated __eq__ comparing its own fields (unit). CoordinateAxis(AxisBase, ArrayWithNamedDims) is declared @dataclass(eq=False), which correctly stops it generating its own __eq__ — but AxisBase's generated one still precedes ArrayWithNamedDims in the MRO, so the data-aware implementation is dead code for this class.
ArrayWithNamedDims.__eq__ looks intentional and correct:
def __eq__(self, other):
if self is other:
return True
if other.__class__ is self.__class__:
xp = get_namespace(self.data)
if self.dims == other.dims and xp.array_equal(self.data, other.data):
return True
return NotImplemented
so this reads as an MRO accident rather than a deliberate choice.
AxisArray is unaffected — it defines its own __eq__. LinearAxis is fine, since unit/gain/offset are all it has.
Impact
Any change detection built on == over coordinate axes silently never fires. We hit this building a metadata side channel over the shared-memory bridge in ezmsg-tools: comparing the incoming ch axis against the last published one to decide whether to republish per-channel labels. With ==, a relabelling — or an entirely different channel set of a different length — was reported as "unchanged", so the labels downstream were never updated. We worked around it by comparing dims/dtype/shape/data explicitly, but anything else in the ecosystem using == here is silently wrong in the same way.
Suggested fix
Either give CoordinateAxis an explicit __eq__ (delegating to the ArrayWithNamedDims implementation), or declare AxisBase as @dataclass(eq=False) so it stops shadowing subclass implementations. The second is tidier but worth checking against anything relying on LinearAxis equality, which the generated __eq__ currently provides.
Worth a regression test asserting inequality for coordinate axes differing in data, in dtype, and in length — the current behaviour returns True for all three.
Summary
CoordinateAxis.__eq__resolves through the MRO to the dataclass-generatedAxisBase.__eq__, which compares onlyunit.ArrayWithNamedDims.__eq__— written to comparedimsanddata— is never reached, so two coordinate axes with different data, different dtypes, or different lengths compare equal.Reproducer
Observed on ezmsg 3.9.0 (
v3.9.0-10-gc9572db).Cause
AxisBaseis a plain@dataclass, so it gets a generated__eq__comparing its own fields (unit).CoordinateAxis(AxisBase, ArrayWithNamedDims)is declared@dataclass(eq=False), which correctly stops it generating its own__eq__— butAxisBase's generated one still precedesArrayWithNamedDimsin the MRO, so the data-aware implementation is dead code for this class.ArrayWithNamedDims.__eq__looks intentional and correct:so this reads as an MRO accident rather than a deliberate choice.
AxisArrayis unaffected — it defines its own__eq__.LinearAxisis fine, sinceunit/gain/offsetare all it has.Impact
Any change detection built on
==over coordinate axes silently never fires. We hit this building a metadata side channel over the shared-memory bridge inezmsg-tools: comparing the incomingchaxis against the last published one to decide whether to republish per-channel labels. With==, a relabelling — or an entirely different channel set of a different length — was reported as "unchanged", so the labels downstream were never updated. We worked around it by comparingdims/dtype/shape/dataexplicitly, but anything else in the ecosystem using==here is silently wrong in the same way.Suggested fix
Either give
CoordinateAxisan explicit__eq__(delegating to theArrayWithNamedDimsimplementation), or declareAxisBaseas@dataclass(eq=False)so it stops shadowing subclass implementations. The second is tidier but worth checking against anything relying onLinearAxisequality, which the generated__eq__currently provides.Worth a regression test asserting inequality for coordinate axes differing in
data, indtype, and in length — the current behaviour returnsTruefor all three.