From cb999e9e80c32c32dc073293406970e9af7b9bd5 Mon Sep 17 00:00:00 2001 From: Don Wong Date: Wed, 13 Oct 2021 10:22:27 +0800 Subject: [PATCH 01/12] robinhood issues 514 --- faust/tables/base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 91ba34939..6db7bb56b 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -366,16 +366,17 @@ async def _del_old_keys(self) -> None: assert window for partition, timestamps in self._partition_timestamps.items(): while timestamps and window.stale( - timestamps[0], self._partition_latest_timestamp[partition] - ): + timestamps[0], + self._partition_latest_timestamp[partition]): timestamp = heappop(timestamps) - keys_to_remove = self._partition_timestamp_keys.pop( - (partition, timestamp), None - ) + keysList = [self._partition_timestamp_keys.get((partition, window_range[1])) for window_range in self._window_ranges(timestamp)] + keys_to_remove = self._partition_timestamp_keys.pop((partition, timestamp), None) if keys_to_remove: + windowData = [item for keys in keysList for key in keys for item in self.data.get(key, None)] for key in keys_to_remove: value = self.data.pop(key, None) - await self.on_window_close(key, value) + if key[1][0] > self.last_closed_window: + await self._on_window_close(key, windowData) self.last_closed_window = max( self.last_closed_window, max(key[1][0] for key in keys_to_remove), From 086814387535f7c4dc9f465d3d64d4f77f809f25 Mon Sep 17 00:00:00 2001 From: Don Wong Date: Wed, 13 Oct 2021 10:25:44 +0800 Subject: [PATCH 02/12] add hopping example --- examples/windowing/hopping_2.py | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 examples/windowing/hopping_2.py diff --git a/examples/windowing/hopping_2.py b/examples/windowing/hopping_2.py new file mode 100644 index 000000000..39a0871fc --- /dev/null +++ b/examples/windowing/hopping_2.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +# In this exapmple we have a function `publish_every_2secs` publishing a +# message every 2 seconds to topic `hopping_topic`. +# We have created an agent `print_windowed_events` consuming events from +# `hopping_topic` that mutates the windowed table `hopping_table`. + +# `hopping_table` is a table with hopping (overlaping) windows. Each of +# its windows is 10 seconds of duration, and we create a new window every 5 +# seconds. +# |----------| +# |-----------| +# |-----------| +# |-----------| +# Since we produce an event every 2 seconds and our windows are 10 +# seconds of duration we expect different the following results per method +# called in `WindowWrapper`: +# - now(): Gets the closest value to current local time. It will always be +# between 1 and 3. +# - current(): Gets the value relative to the event's timestamp. It will +# always be between 1 and 3. +# - value(): Gets the value relative to default relative option. It will +# always be between 1 and 3. +# - delta(30): Gets the value of window 30 secs before the current event. For +# the first 20 seconds it will be 0 and after second 30 it will always be 5. + +import sys +from datetime import datetime, timedelta +from time import time +import faust + + +class RawModel(faust.Record): + date: datetime + value: float + + +class WinModel(faust.Record): + win: list + + +TOPIC = 'raw-event' +TABLE = 'hopping_table' +KAFKA = 'kafka://localhost:9092' +CLEANUP_INTERVAL = 10 +WINDOW = 6 +STEP = 3 +WINDOW_EXPIRES = 60 +PARTITIONS = 1 + +app = faust.App('windowed-hopping', broker=KAFKA, topic_partitions=PARTITIONS) + +app.conf.table_cleanup_interval = CLEANUP_INTERVAL +source = app.topic(TOPIC, value_type=RawModel) + + +def window_processor(key, events): + timestamp = key[1][0] + count = len(events) + + print( + f'processing window:' + f'{count} events,' + f'timestamp {timestamp}', + ) + + +hopping_table = ( + app.Table( + TABLE, + default=list, + partitions=PARTITIONS, + on_window_close=window_processor, + ) + .hopping(WINDOW, STEP, expires=timedelta(seconds=WINDOW_EXPIRES)) + .relative_to_field(RawModel.date) +) + + +@app.agent(source) +async def print_windowed_events(stream): + async for event in stream: + value_list = hopping_table['events'].value() + + if len(value_list) > 0: + event.value = value_list[-1].value + 1 + print("Receive message : " + str(event)) + + value_list.append(event) + hopping_table['events'] = value_list + + +@app.timer(0.1) +async def produce(): + value = 1 + await source.send(value=RawModel(value=value, date=int(time()))) + # print(f'Produce Message :: send messge {value}') + + +if __name__ == '__main__': + if len(sys.argv) < 2: + sys.argv.extend(['worker', '-l', 'info']) + app.main() From 4259492027827e7cc51f891e019170d816bd76f6 Mon Sep 17 00:00:00 2001 From: Don Wong Date: Wed, 13 Oct 2021 21:02:12 +0800 Subject: [PATCH 03/12] fix: 'Nonetype' object is not iterable problem --- faust/tables/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 6db7bb56b..0ce186a42 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -372,11 +372,11 @@ async def _del_old_keys(self) -> None: keysList = [self._partition_timestamp_keys.get((partition, window_range[1])) for window_range in self._window_ranges(timestamp)] keys_to_remove = self._partition_timestamp_keys.pop((partition, timestamp), None) if keys_to_remove: - windowData = [item for keys in keysList for key in keys for item in self.data.get(key, None)] + windowData = [item for keys in keysList if keys for key in keys for item in self.data.get(key, None)] for key in keys_to_remove: value = self.data.pop(key, None) if key[1][0] > self.last_closed_window: - await self._on_window_close(key, windowData) + await self.on_window_close(key, windowData) self.last_closed_window = max( self.last_closed_window, max(key[1][0] for key in keys_to_remove), From e6c75afcb22f49239d367c8249cbd34a745bc3de Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 18 Nov 2022 10:02:18 -0500 Subject: [PATCH 04/12] lint --- faust/tables/base.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 08cb2dce6..edcd250c6 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -375,14 +375,23 @@ async def _del_old_keys(self) -> None: window = cast(WindowT, self.window) assert window for partition, timestamps in self._partition_timestamps.items(): - while timestamps and window.stale( - timestamps[0], - time.time()): + while timestamps and window.stale(timestamps[0], time.time()): timestamp = heappop(timestamps) - keysList = [self._partition_timestamp_keys.get((partition, window_range[1])) for window_range in self._window_ranges(timestamp)] - keys_to_remove = self._partition_timestamp_keys.pop((partition, timestamp), None) + keysList = [ + self._partition_timestamp_keys.get((partition, window_range[1])) + for window_range in self._window_ranges(timestamp) + ] + keys_to_remove = self._partition_timestamp_keys.pop( + (partition, timestamp), None + ) if keys_to_remove: - windowData = [item for keys in keysList if keys for key in keys for item in self.data.get(key, None)] + windowData = [ + item + for keys in keysList + if keys + for key in keys + for item in self.data.get(key, None) + ] for key in keys_to_remove: value = self.data.pop(key, None) if key[1][0] > self.last_closed_window: From 98a81e77b8bc013eba0e4bb9c75030c9821dcebb Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 18 Nov 2022 10:07:03 -0500 Subject: [PATCH 05/12] remove unused var --- faust/tables/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index edcd250c6..e35d358c1 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -393,7 +393,7 @@ async def _del_old_keys(self) -> None: for item in self.data.get(key, None) ] for key in keys_to_remove: - value = self.data.pop(key, None) + self.data.pop(key, None) if key[1][0] > self.last_closed_window: await self.on_window_close(key, windowData) self.last_closed_window = max( From 84882c68c8d88c783376dc473628a0156662dfa5 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 18 Nov 2022 10:58:29 -0500 Subject: [PATCH 06/12] add mock ranges to table tests --- tests/unit/tables/test_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/tables/test_base.py b/tests/unit/tables/test_base.py index f4feba385..137a7a5f6 100644 --- a/tests/unit/tables/test_base.py +++ b/tests/unit/tables/test_base.py @@ -191,6 +191,7 @@ async def test_last_closed_window(self, *, table): assert table.last_closed_window == 0.0 table.window = Mock(name="window") + self.mock_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", @@ -233,6 +234,7 @@ async def test_del_old_keys(self, *, table): on_window_close = table._on_window_close = AsyncMock(name="on_window_close") table.window = Mock(name="window") + self.mock_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", @@ -289,6 +291,7 @@ async def test_del_old_keys_non_async_cb(self, *, table): on_window_close = table._on_window_close = Mock(name="on_window_close") table.window = Mock(name="window") + self.mock_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", From 1f62dc5c28a3932d384f3c94c020cef4242f6970 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Tue, 22 Nov 2022 15:31:01 -0500 Subject: [PATCH 07/12] Pull in changes by @thomas-chauvet --- faust/tables/base.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index e35d358c1..dd6c8f8a5 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -377,25 +377,36 @@ async def _del_old_keys(self) -> None: for partition, timestamps in self._partition_timestamps.items(): while timestamps and window.stale(timestamps[0], time.time()): timestamp = heappop(timestamps) - keysList = [ - self._partition_timestamp_keys.get((partition, window_range[1])) + triggered_windows = [ + self._partition_timestamp_keys.get((partition, window_range)) for window_range in self._window_ranges(timestamp) ] keys_to_remove = self._partition_timestamp_keys.pop( (partition, timestamp), None ) + window_data = {} if keys_to_remove: - windowData = [ - item - for keys in keysList - if keys - for key in keys - for item in self.data.get(key, None) - ] - for key in keys_to_remove: - self.data.pop(key, None) - if key[1][0] > self.last_closed_window: - await self.on_window_close(key, windowData) + for windows in triggered_windows: + if windows: + for processed_window in windows: + # we use set to avoid duplicate element in window's data + # window[0] is the window's key + # it is not related to window's timestamp + # windows are in format: + # (key, (window_start, window_end)) + window_data.setdefault(processed_window[0], []).extend( + self.data.get(processed_window, []) + ) + + for key_to_remove in keys_to_remove: + self.data.pop(key_to_remove, None) + if key_to_remove[1][0] > self.last_closed_window: + await self.on_window_close( + key_to_remove, + window_data[key_to_remove[0]] + if key_to_remove[0] in window_data + else {}, + ) self.last_closed_window = max( self.last_closed_window, max(key[1][0] for key in keys_to_remove), From 0608d16113fad560fb8b2b665ea935790e95d1df Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Tue, 22 Nov 2022 15:38:46 -0500 Subject: [PATCH 08/12] save the popped value as a backup for now --- faust/tables/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index dd6c8f8a5..5a80c62bd 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -399,13 +399,13 @@ async def _del_old_keys(self) -> None: ) for key_to_remove in keys_to_remove: - self.data.pop(key_to_remove, None) + value = self.data.pop(key_to_remove, None) if key_to_remove[1][0] > self.last_closed_window: await self.on_window_close( key_to_remove, window_data[key_to_remove[0]] if key_to_remove[0] in window_data - else {}, + else value, ) self.last_closed_window = max( self.last_closed_window, From 8fc89aef52766ba37e6a42c3837dc4b11a071d7b Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 29 Mar 2024 19:29:48 -0400 Subject: [PATCH 09/12] cleanup and add more tests --- faust/tables/base.py | 8 +- tests/unit/tables/test_base.py | 147 +++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 5e03eab98..0c50a785e 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -408,9 +408,11 @@ async def _del_old_keys(self) -> None: if key_to_remove[1][0] > self.last_closed_window: await self.on_window_close( key_to_remove, - window_data[key_to_remove[0]] - if key_to_remove[0] in window_data - else value, + ( + window_data[key_to_remove[0]] + if key_to_remove[0] in window_data + else value + ), ) self.last_closed_window = max( self.last_closed_window, diff --git a/tests/unit/tables/test_base.py b/tests/unit/tables/test_base.py index bfe82961a..724c05fc0 100644 --- a/tests/unit/tables/test_base.py +++ b/tests/unit/tables/test_base.py @@ -190,6 +190,43 @@ def test_on_changelog_sent__transactions(self, *, table): async def test_last_closed_window(self, *, table): assert table.last_closed_window == 0.0 + table.window = Mock(name="window") + table._data = { + ("boo", (1.1, 1.4)): "BOO", + ("moo", (1.4, 1.6)): "MOO", + ("faa", (1.9, 2.0)): "FAA", + ("bar", (4.1, 4.2)): "BAR", + } + table._partition_timestamps = { + TP1: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + } + table._partition_timestamp_keys = { + (TP1, 2.0): [ + ("boo", (1.1, 1.4)), + ("moo", (1.4, 1.6)), + ("faa", (1.9, 2.0)), + ], + (TP1, 5.0): [ + ("bar", (4.1, 4.2)), + ], + } + + def get_stale(limit): + def is_stale(timestamp, latest_timestamp): + return timestamp < limit + + return is_stale + + table.window.stale.side_effect = get_stale(4.0) + + await table._del_old_keys() + + assert table.last_closed_window == 1.9 + + @pytest.mark.asyncio + async def test_last_closed_window__mock_ranges(self, *, table): + assert table.last_closed_window == 0.0 + table.window = Mock(name="window") self.mock_ranges(table) table._data = { @@ -233,6 +270,62 @@ async def test_del_old_keys__empty(self, *, table): async def test_del_old_keys(self, *, table): on_window_close = table._on_window_close = AsyncMock(name="on_window_close") + table.window = Mock(name="window") + table._data = { + ("boo", (1.1, 1.4)): "BOO", + ("moo", (1.4, 1.6)): "MOO", + ("faa", (1.9, 2.0)): "FAA", + ("bar", (4.1, 4.2)): "BAR", + } + table._partition_timestamps = { + TP1: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + } + table._partition_timestamp_keys = { + (TP1, 2.0): [ + ("boo", (1.1, 1.4)), + ("moo", (1.4, 1.6)), + ("faa", (1.9, 2.0)), + ], + (TP1, 5.0): [ + ("bar", (4.1, 4.2)), + ], + } + + def get_stale(limit): + def is_stale(timestamp, latest_timestamp): + return timestamp < limit + + return is_stale + + table.window.stale.side_effect = get_stale(4.0) + + await table._del_old_keys() + + assert table._partition_timestamps[TP1] == [4.0, 5.0, 6.0, 7.0] + assert table.data == {("bar", (4.1, 4.2)): "BAR"} + + on_window_close.assert_has_calls( + [ + call.__bool__(), + call(("boo", (1.1, 1.4)), "BOO"), + call.__bool__(), + call(("moo", (1.4, 1.6)), "MOO"), + call.__bool__(), + call(("faa", (1.9, 2.0)), "FAA"), + ] + ) + + table.last_closed_window = 8.0 + table.window.stale.side_effect = get_stale(6.0) + + await table._del_old_keys() + + assert not table.data + + @pytest.mark.asyncio + async def test_del_old_keys__mock_ranges(self, *, table): + on_window_close = table._on_window_close = AsyncMock(name="on_window_close") + table.window = Mock(name="window") self.mock_ranges(table) table._data = { @@ -340,6 +433,60 @@ def is_stale(timestamp, latest_timestamp): assert not table.data + @pytest.mark.asyncio + async def test_del_old_keys_non_async_cb__mock_ranges(self, *, table): + on_window_close = table._on_window_close = Mock(name="on_window_close") + + table.window = Mock(name="window") + self.mock_ranges(table) + table._data = { + ("boo", (1.1, 1.4)): "BOO", + ("moo", (1.4, 1.6)): "MOO", + ("faa", (1.9, 2.0)): "FAA", + ("bar", (4.1, 4.2)): "BAR", + } + table._partition_timestamps = { + TP1: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + } + table._partition_timestamp_keys = { + (TP1, 2.0): [ + ("boo", (1.1, 1.4)), + ("moo", (1.4, 1.6)), + ("faa", (1.9, 2.0)), + ], + (TP1, 5.0): [ + ("bar", (4.1, 4.2)), + ], + } + + def get_stale(limit): + def is_stale(timestamp, latest_timestamp): + return timestamp < limit + + return is_stale + + table.window.stale.side_effect = get_stale(4.0) + + await table._del_old_keys() + + assert table._partition_timestamps[TP1] == [4.0, 5.0, 6.0, 7.0] + assert table.data == {("bar", (4.1, 4.2)): "BAR"} + + on_window_close.assert_has_calls( + [ + call(("boo", (1.1, 1.4)), "BOO"), + call(("moo", (1.4, 1.6)), "MOO"), + call(("faa", (1.9, 2.0)), "FAA"), + ] + ) + + table.last_closed_window = 8.0 + table.window.stale.side_effect = get_stale(6.0) + + await table._del_old_keys() + + assert not table.data + @pytest.mark.asyncio async def test_on_window_close__default(self, *, table): assert table._on_window_close is None From 09a600b4f4577e18504a780af1a02b143313e477 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 29 Mar 2024 19:52:18 -0400 Subject: [PATCH 10/12] test for ranges when full and empty --- faust/tables/base.py | 2 +- tests/unit/tables/test_base.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 0c50a785e..260798e26 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -383,7 +383,7 @@ async def _del_old_keys(self) -> None: while timestamps and window.stale(timestamps[0], time.time()): timestamp = heappop(timestamps) triggered_windows = [ - self._partition_timestamp_keys.get((partition, window_range)) + self._partition_timestamp_keys.get((partition, window_range)) # noqa for window_range in self._window_ranges(timestamp) ] keys_to_remove = self._partition_timestamp_keys.pop( diff --git a/tests/unit/tables/test_base.py b/tests/unit/tables/test_base.py index 724c05fc0..a6d664803 100644 --- a/tests/unit/tables/test_base.py +++ b/tests/unit/tables/test_base.py @@ -1,7 +1,7 @@ import asyncio import operator from copy import copy -from unittest.mock import Mock, call, patch +from unittest.mock import Mock, call, patch, MagicMock import pytest from mode import label, shortlabel @@ -191,6 +191,7 @@ async def test_last_closed_window(self, *, table): assert table.last_closed_window == 0.0 table.window = Mock(name="window") + self.mock_no_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", @@ -271,6 +272,7 @@ async def test_del_old_keys(self, *, table): on_window_close = table._on_window_close = AsyncMock(name="on_window_close") table.window = Mock(name="window") + self.mock_no_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", @@ -384,7 +386,7 @@ async def test_del_old_keys_non_async_cb(self, *, table): on_window_close = table._on_window_close = Mock(name="on_window_close") table.window = Mock(name="window") - self.mock_ranges(table) + self.mock_no_ranges(table) table._data = { ("boo", (1.1, 1.4)): "BOO", ("moo", (1.4, 1.6)): "MOO", @@ -673,7 +675,12 @@ def test_window_ranges(self, *, table): assert list(table._window_ranges(300.3)) == [1, 2, 3] def mock_ranges(self, table, ranges=[1.1, 1.2, 1.3]): # noqa - table._window_ranges = Mock(name="_window_ranges") + table._window_ranges = MagicMock(name="_window_ranges") + table._window_ranges.return_value = ranges + return ranges + + def mock_no_ranges(self, table, ranges=[]): # noqa + table._window_ranges = MagicMock(name="_window_ranges") table._window_ranges.return_value = ranges return ranges From e17219b72e9203c45150071f4c607157158d04e1 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 29 Mar 2024 19:52:33 -0400 Subject: [PATCH 11/12] add linting --- faust/tables/base.py | 4 +++- tests/unit/tables/test_base.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/faust/tables/base.py b/faust/tables/base.py index 260798e26..ca26c0f11 100644 --- a/faust/tables/base.py +++ b/faust/tables/base.py @@ -383,7 +383,9 @@ async def _del_old_keys(self) -> None: while timestamps and window.stale(timestamps[0], time.time()): timestamp = heappop(timestamps) triggered_windows = [ - self._partition_timestamp_keys.get((partition, window_range)) # noqa + self._partition_timestamp_keys.get( + (partition, window_range) + ) # noqa for window_range in self._window_ranges(timestamp) ] keys_to_remove = self._partition_timestamp_keys.pop( diff --git a/tests/unit/tables/test_base.py b/tests/unit/tables/test_base.py index a6d664803..5408581de 100644 --- a/tests/unit/tables/test_base.py +++ b/tests/unit/tables/test_base.py @@ -1,7 +1,7 @@ import asyncio import operator from copy import copy -from unittest.mock import Mock, call, patch, MagicMock +from unittest.mock import MagicMock, Mock, call, patch import pytest from mode import label, shortlabel From c103bceddf1b466aae4c2cf8c6b4688c94fe3617 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Fri, 29 Mar 2024 19:54:23 -0400 Subject: [PATCH 12/12] remove MagicMock import --- tests/unit/tables/test_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/tables/test_base.py b/tests/unit/tables/test_base.py index 5408581de..bcd6e5fd1 100644 --- a/tests/unit/tables/test_base.py +++ b/tests/unit/tables/test_base.py @@ -1,7 +1,7 @@ import asyncio import operator from copy import copy -from unittest.mock import MagicMock, Mock, call, patch +from unittest.mock import Mock, call, patch import pytest from mode import label, shortlabel @@ -675,12 +675,12 @@ def test_window_ranges(self, *, table): assert list(table._window_ranges(300.3)) == [1, 2, 3] def mock_ranges(self, table, ranges=[1.1, 1.2, 1.3]): # noqa - table._window_ranges = MagicMock(name="_window_ranges") + table._window_ranges = Mock(name="_window_ranges") table._window_ranges.return_value = ranges return ranges def mock_no_ranges(self, table, ranges=[]): # noqa - table._window_ranges = MagicMock(name="_window_ranges") + table._window_ranges = Mock(name="_window_ranges") table._window_ranges.return_value = ranges return ranges