Skip to content

Commit 6ced688

Browse files
SimonHeybrockclaude
andcommitted
Fix TemporalBufferManager test and API issues
- Change type hints from list to Sequence for extractor parameters - Add public get_required_timespan() method to BufferProtocol and implementations - Refactor tests to use public API instead of accessing private fields - Fix test data to use proper DataArray instead of scalar Variables - Use sc.identical() for DataArray comparisons instead of == Original task: Fix issues in TemporalBufferManager tests - private field access and type hint complaints about passing extractors (list vs Sequence). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7b65409 commit 6ced688

File tree

3 files changed

+64
-35
lines changed

3 files changed

+64
-35
lines changed

src/ess/livedata/dashboard/temporal_buffer_manager.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
import logging
8-
from collections.abc import Hashable, Iterator, Mapping
8+
from collections.abc import Hashable, Iterator, Mapping, Sequence
99
from dataclasses import dataclass, field
1010
from typing import Generic, TypeVar
1111

@@ -24,7 +24,9 @@ class _BufferState:
2424
"""Internal state for a managed buffer."""
2525

2626
buffer: BufferProtocol[sc.DataArray]
27-
extractors: list[UpdateExtractor] = field(default_factory=list)
27+
extractors: list[UpdateExtractor] = field(
28+
default_factory=list
29+
) # Stored as list internally
2830

2931

3032
class TemporalBufferManager(Mapping[K, BufferProtocol[sc.DataArray]], Generic[K]):
@@ -77,7 +79,7 @@ def get_buffered_data(self, key: K) -> sc.DataArray | None:
7779
return None
7880
return self._states[key].buffer.get()
7981

80-
def create_buffer(self, key: K, extractors: list[UpdateExtractor]) -> None:
82+
def create_buffer(self, key: K, extractors: Sequence[UpdateExtractor]) -> None:
8183
"""
8284
Create a buffer with appropriate type based on extractors.
8385
@@ -86,7 +88,7 @@ def create_buffer(self, key: K, extractors: list[UpdateExtractor]) -> None:
8688
key:
8789
Key to identify this buffer.
8890
extractors:
89-
List of extractors that will use this buffer.
91+
Sequence of extractors that will use this buffer.
9092
"""
9193
if key in self._states:
9294
raise ValueError(f"Buffer with key {key} already exists")
@@ -133,7 +135,7 @@ def add_extractor(self, key: K, extractor: UpdateExtractor) -> None:
133135
state.extractors.append(extractor)
134136
self._reconfigure_buffer_if_needed(key, state)
135137

136-
def set_extractors(self, key: K, extractors: list[UpdateExtractor]) -> None:
138+
def set_extractors(self, key: K, extractors: Sequence[UpdateExtractor]) -> None:
137139
"""
138140
Replace all extractors for an existing buffer.
139141
@@ -151,7 +153,7 @@ def set_extractors(self, key: K, extractors: list[UpdateExtractor]) -> None:
151153
key:
152154
Key identifying the buffer to update.
153155
extractors:
154-
New list of extractors that will use this buffer.
156+
New sequence of extractors that will use this buffer.
155157
"""
156158
state = self._states[key]
157159
state.extractors = list(extractors)
@@ -227,7 +229,7 @@ def delete_buffer(self, key: K) -> None:
227229
del self._states[key]
228230

229231
def _create_buffer_for_extractors(
230-
self, extractors: list[UpdateExtractor]
232+
self, extractors: Sequence[UpdateExtractor]
231233
) -> BufferProtocol[sc.DataArray]:
232234
"""
233235
Create appropriate buffer type based on extractors.
@@ -238,7 +240,7 @@ def _create_buffer_for_extractors(
238240
Parameters
239241
----------
240242
extractors:
241-
List of extractors that will use the buffer.
243+
Sequence of extractors that will use the buffer.
242244
243245
Returns
244246
-------
@@ -258,7 +260,9 @@ def _create_buffer_for_extractors(
258260
return TemporalBuffer()
259261

260262
def _update_buffer_requirements(
261-
self, buffer: BufferProtocol[sc.DataArray], extractors: list[UpdateExtractor]
263+
self,
264+
buffer: BufferProtocol[sc.DataArray],
265+
extractors: Sequence[UpdateExtractor],
262266
) -> None:
263267
"""
264268
Update buffer requirements based on extractors.
@@ -271,7 +275,7 @@ def _update_buffer_requirements(
271275
buffer:
272276
The buffer to update.
273277
extractors:
274-
List of extractors to gather requirements from.
278+
Sequence of extractors to gather requirements from.
275279
"""
276280
# Compute maximum required timespan
277281
if extractors:

src/ess/livedata/dashboard/temporal_buffers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def set_required_timespan(self, seconds: float) -> None:
5252
Required timespan in seconds.
5353
"""
5454

55+
@abstractmethod
56+
def get_required_timespan(self) -> float:
57+
"""
58+
Get the required timespan for the buffer.
59+
60+
Returns
61+
-------
62+
:
63+
Required timespan in seconds.
64+
"""
65+
5566
@abstractmethod
5667
def set_max_memory(self, max_bytes: int) -> None:
5768
"""
@@ -93,6 +104,10 @@ def set_required_timespan(self, seconds: float) -> None:
93104
"""Set required timespan (unused for SingleValueBuffer)."""
94105
self._required_timespan = seconds
95106

107+
def get_required_timespan(self) -> float:
108+
"""Get the required timespan."""
109+
return self._required_timespan
110+
96111
def set_max_memory(self, max_bytes: int) -> None:
97112
"""Set max memory (unused for SingleValueBuffer)."""
98113
self._max_memory = max_bytes
@@ -343,6 +358,10 @@ def set_required_timespan(self, seconds: float) -> None:
343358
"""Set the required timespan for the buffer."""
344359
self._required_timespan = seconds
345360

361+
def get_required_timespan(self) -> float:
362+
"""Get the required timespan for the buffer."""
363+
return self._required_timespan
364+
346365
def set_max_memory(self, max_bytes: int) -> None:
347366
"""Set the maximum memory usage for the buffer."""
348367
self._max_memory = max_bytes

tests/dashboard/temporal_buffer_manager_test.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_create_buffer_with_only_latest_extractors_uses_single_value_buffer(self
2626

2727
manager.create_buffer('test', extractors)
2828

29-
assert isinstance(manager._states['test'].buffer, SingleValueBuffer)
29+
assert isinstance(manager['test'], SingleValueBuffer)
3030

3131
def test_create_buffer_with_mixed_extractors_uses_temporal_buffer(self):
3232
"""
@@ -37,7 +37,7 @@ def test_create_buffer_with_mixed_extractors_uses_temporal_buffer(self):
3737

3838
manager.create_buffer('test', extractors)
3939

40-
assert isinstance(manager._states['test'].buffer, TemporalBuffer)
40+
assert isinstance(manager['test'], TemporalBuffer)
4141

4242
def test_create_buffer_with_window_extractor_uses_temporal_buffer(self):
4343
"""Test that TemporalBuffer is used with WindowAggregatingExtractor."""
@@ -46,7 +46,7 @@ def test_create_buffer_with_window_extractor_uses_temporal_buffer(self):
4646

4747
manager.create_buffer('test', extractors)
4848

49-
assert isinstance(manager._states['test'].buffer, TemporalBuffer)
49+
assert isinstance(manager['test'], TemporalBuffer)
5050

5151
def test_create_buffer_with_no_extractors_uses_single_value_buffer(self):
5252
"""
@@ -56,7 +56,7 @@ def test_create_buffer_with_no_extractors_uses_single_value_buffer(self):
5656

5757
manager.create_buffer('test', [])
5858

59-
assert isinstance(manager._states['test'].buffer, SingleValueBuffer)
59+
assert isinstance(manager['test'], SingleValueBuffer)
6060

6161
def test_create_buffer_raises_error_for_duplicate_key(self):
6262
"""Test that creating a buffer with existing key raises ValueError."""
@@ -72,18 +72,24 @@ def test_update_buffer_adds_data(self):
7272
"""Test that update_buffer adds data to the buffer."""
7373
manager = TemporalBufferManager()
7474
extractors = [LatestValueExtractor()]
75-
data = sc.scalar(42, unit='counts')
75+
data = sc.DataArray(
76+
sc.scalar(42, unit='counts'),
77+
coords={'time': sc.scalar(1.0, unit='s')},
78+
)
7679

7780
manager.create_buffer('test', extractors)
7881
manager.update_buffer('test', data)
7982

8083
result = manager.get_buffered_data('test')
81-
assert result == data
84+
assert sc.identical(result, data)
8285

8386
def test_update_buffer_raises_error_for_missing_key(self):
8487
"""Test that updating non-existent buffer raises KeyError."""
8588
manager = TemporalBufferManager()
86-
data = sc.scalar(42, unit='counts')
89+
data = sc.DataArray(
90+
sc.scalar(42, unit='counts'),
91+
coords={'time': sc.scalar(1.0, unit='s')},
92+
)
8793

8894
with pytest.raises(KeyError, match="No buffer found"):
8995
manager.update_buffer('test', data)
@@ -94,12 +100,12 @@ def test_add_extractor_keeps_same_buffer_type(self):
94100
extractors = [LatestValueExtractor()]
95101

96102
manager.create_buffer('test', extractors)
97-
original_buffer = manager._states['test'].buffer
103+
original_buffer = manager['test']
98104

99105
manager.add_extractor('test', LatestValueExtractor())
100106

101-
assert manager._states['test'].buffer is original_buffer
102-
assert isinstance(manager._states['test'].buffer, SingleValueBuffer)
107+
assert manager['test'] is original_buffer
108+
assert isinstance(manager['test'], SingleValueBuffer)
103109

104110
def test_add_extractor_switches_to_temporal_buffer(self):
105111
"""Test that switching buffer types preserves existing data."""
@@ -213,9 +219,9 @@ def test_window_extractor_sets_timespan_on_buffer(self):
213219

214220
manager.create_buffer('test', extractors)
215221

216-
buffer = manager._states['test'].buffer
222+
buffer = manager['test']
217223
assert isinstance(buffer, TemporalBuffer)
218-
assert buffer._required_timespan == window_duration
224+
assert buffer.get_required_timespan() == window_duration
219225

220226
def test_multiple_window_extractors_use_max_timespan(self):
221227
"""Test that maximum timespan from multiple extractors is used."""
@@ -228,8 +234,8 @@ def test_multiple_window_extractors_use_max_timespan(self):
228234

229235
manager.create_buffer('test', extractors)
230236

231-
buffer = manager._states['test'].buffer
232-
assert buffer._required_timespan == 5.0
237+
buffer = manager['test']
238+
assert buffer.get_required_timespan() == 5.0
233239

234240
def test_latest_extractor_does_not_set_timespan(self):
235241
"""Test that LatestValueExtractor doesn't set a timespan."""
@@ -238,9 +244,9 @@ def test_latest_extractor_does_not_set_timespan(self):
238244

239245
manager.create_buffer('test', extractors)
240246

241-
buffer = manager._states['test'].buffer
247+
buffer = manager['test']
242248
assert isinstance(buffer, SingleValueBuffer)
243-
assert buffer._required_timespan == 0.0
249+
assert buffer.get_required_timespan() == 0.0
244250

245251
def test_mixed_extractors_use_window_timespan(self):
246252
"""Test that timespan is set when mixing Latest and Window extractors."""
@@ -252,25 +258,25 @@ def test_mixed_extractors_use_window_timespan(self):
252258

253259
manager.create_buffer('test', extractors)
254260

255-
buffer = manager._states['test'].buffer
261+
buffer = manager['test']
256262
assert isinstance(buffer, TemporalBuffer)
257-
assert buffer._required_timespan == 4.0
263+
assert buffer.get_required_timespan() == 4.0
258264

259265
def test_adding_extractor_updates_timespan(self):
260266
"""Test that adding an extractor updates the buffer's timespan."""
261267
manager = TemporalBufferManager()
262268
extractors = [WindowAggregatingExtractor(window_duration_seconds=2.0)]
263269

264270
manager.create_buffer('test', extractors)
265-
buffer = manager._states['test'].buffer
266-
assert buffer._required_timespan == 2.0
271+
buffer = manager['test']
272+
assert buffer.get_required_timespan() == 2.0
267273

268274
# Add extractor with larger timespan
269275
manager.add_extractor(
270276
'test', WindowAggregatingExtractor(window_duration_seconds=10.0)
271277
)
272278

273-
assert buffer._required_timespan == 10.0
279+
assert buffer.get_required_timespan() == 10.0
274280

275281
def test_full_history_extractor_infinite_timespan(self):
276282
"""Test that FullHistoryExtractor sets infinite timespan."""
@@ -279,9 +285,9 @@ def test_full_history_extractor_infinite_timespan(self):
279285

280286
manager.create_buffer('test', extractors)
281287

282-
buffer = manager._states['test'].buffer
288+
buffer = manager['test']
283289
assert isinstance(buffer, TemporalBuffer)
284-
assert buffer._required_timespan == float('inf')
290+
assert buffer.get_required_timespan() == float('inf')
285291

286292
def test_full_history_with_window_uses_infinite(self):
287293
"""Test that mixing FullHistory with Window uses infinite timespan."""
@@ -293,10 +299,10 @@ def test_full_history_with_window_uses_infinite(self):
293299

294300
manager.create_buffer('test', extractors)
295301

296-
buffer = manager._states['test'].buffer
302+
buffer = manager['test']
297303
assert isinstance(buffer, TemporalBuffer)
298304
# max(5.0, inf) = inf
299-
assert buffer._required_timespan == float('inf')
305+
assert buffer.get_required_timespan() == float('inf')
300306

301307

302308
class TestTemporalBufferManagerWithRealData:

0 commit comments

Comments
 (0)