Skip to content

Commit 9eebcd9

Browse files
github-actions[bot]Copilotdbrattliclaude
authored
[Repo Assist] fix(operators): forward scheduler in pairwise, to_marbles, delay_with_mapper (#757)
* fix(operators): forward scheduler in pairwise, to_marbles, delay_with_mapper Three operators were subscribing to their source without forwarding the scheduler received at subscription time, breaking scheduler propagation for downstream operators. * _pairwise.py: add scheduler=scheduler to source.subscribe() * _tomarbles.py: add scheduler=scheduler to source.subscribe() * _delaywithmapper.py: add scheduler=scheduler to sub_delay.subscribe() in the subscription-delay path (the main source path was already fixed) The other operators originally listed in issue #480 (delay_with_mapper main path, throttle_first, timeout_with_mapper) were already forwarding the scheduler correctly. Closes #480 (partial) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(operators): add regression tests for scheduler propagation Cover pairwise, to_marbles, and delay_with_mapper (subscription-delay path): each test installs a source whose subscribe captures the scheduler it received, pipes through the operator, subscribes with a known scheduler, and asserts the captured scheduler matches. Verified to fail on the pre-fix code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Dag Brattli <dag@brattli.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7d3f66c commit 9eebcd9

7 files changed

Lines changed: 97 additions & 4 deletions

File tree

changes.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
## Unreleased
44

5+
<<<<<<< repo-assist/fix-issue-480-scheduler-forwarding-ea9c5c512fb11be9
6+
- Operators: Fixed scheduler forwarding in `pairwise`, `to_marbles`, and
7+
`delay_with_mapper` (subscription-delay path). These operators now pass the
8+
`scheduler` argument through to `source.subscribe(...)` and, in the case of
9+
`delay_with_mapper`, to the subscription-delay observable, consistent with
10+
all other pipeable operators. Closes #480 (partial — the operators listed
11+
in the issue that were not yet fixed).
12+
13+
=======
514
- CI: Skip `tests/test_scheduler/test_mainloop/test_tkinterscheduler.py` on
615
PyPy (all platforms). The module creates a Tk root at import time; PyPy's
716
`_tkinter` finalizer calls `threading.notify_all` during interpreter
817
shutdown and aborts the xdist worker, failing whichever unrelated test
918
was running. Previously guarded only on macOS+PyPy.
19+
>>>>>>> master
1020
- Fix: `reactivex.timer(duetime, period)` now correctly resets the initial
1121
delay on each resubscription (e.g. via `repeat()`). Previously `nonlocal
1222
duetime` in the subscribe closure mutated the shared outer variable so

reactivex/operators/_delaywithmapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def on_completed() -> None:
9595
start()
9696
else:
9797
subscription.disposable = sub_delay.subscribe(
98-
lambda _: start(), observer.on_error, start
98+
lambda _: start(), observer.on_error, start, scheduler=scheduler
9999
)
100100

101101
return CompositeDisposable(subscription, delays)

reactivex/operators/_pairwise.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ def on_next(x: _T) -> None:
4949
if pair:
5050
observer.on_next(pair)
5151

52-
return source.subscribe(on_next, observer.on_error, observer.on_completed)
52+
return source.subscribe(
53+
on_next, observer.on_error, observer.on_completed, scheduler=scheduler
54+
)
5355

5456
return Observable(subscribe)
5557

reactivex/operators/_tomarbles.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ def on_completed():
5959
observer.on_next("".join(n for n in result))
6060
observer.on_completed()
6161

62-
return source.subscribe(on_next, on_error, on_completed)
62+
return source.subscribe(
63+
on_next, on_error, on_completed, scheduler=scheduler
64+
)
6365

6466
return Observable(subscribe)
6567

tests/test_observable/test_delaywithmapper.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import unittest
22

3+
from reactivex import Observable, abc
34
from reactivex import operators as ops
5+
from reactivex.disposable import Disposable
6+
from reactivex.scheduler import ImmediateScheduler
47
from reactivex.testing import ReactiveTest, TestScheduler
58

69
on_next = ReactiveTest.on_next
@@ -199,3 +202,37 @@ def create():
199202
assert results.messages == [on_next(210 + 50, 2)]
200203
assert xs.subscriptions == [subscribe(200, 300)]
201204
assert ys.subscriptions == [subscribe(210, 260)]
205+
206+
def test_delay_with_mapper_forwards_scheduler_to_subscription_delay(
207+
self,
208+
) -> None:
209+
captured: dict[str, abc.SchedulerBase | None] = {}
210+
expected: abc.SchedulerBase = ImmediateScheduler()
211+
212+
def sub_delay_subscribe(
213+
observer: abc.ObserverBase[int],
214+
scheduler: abc.SchedulerBase | None = None,
215+
) -> abc.DisposableBase:
216+
captured["scheduler"] = scheduler
217+
observer.on_completed()
218+
return Disposable()
219+
220+
sub_delay: Observable[int] = Observable(sub_delay_subscribe)
221+
222+
def source_subscribe(
223+
observer: abc.ObserverBase[int],
224+
scheduler: abc.SchedulerBase | None = None,
225+
) -> abc.DisposableBase:
226+
observer.on_completed()
227+
return Disposable()
228+
229+
source: Observable[int] = Observable(source_subscribe)
230+
231+
def mapper(_: int) -> Observable[int]:
232+
return source
233+
234+
source.pipe(ops.delay_with_mapper(sub_delay, mapper)).subscribe(
235+
scheduler=expected
236+
)
237+
238+
assert captured["scheduler"] is expected

tests/test_observable/test_marbles.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import unittest
33

44
import reactivex
5-
from reactivex import notification
5+
from reactivex import Observable, abc, notification
6+
from reactivex import operators as ops
7+
from reactivex.disposable import Disposable
68
from reactivex.observable.marbles import parse
9+
from reactivex.scheduler import ImmediateScheduler
710
from reactivex.testing import TestScheduler
811
from reactivex.testing.reactivetest import ReactiveTest
912

@@ -578,3 +581,22 @@ def test_hot_marble_with_timedelta(self):
578581
ReactiveTest.on_completed(300.7),
579582
]
580583
assert results == expected
584+
585+
586+
class TestToMarbles(unittest.TestCase):
587+
def test_to_marbles_forwards_scheduler_to_source(self) -> None:
588+
captured: dict[str, abc.SchedulerBase | None] = {}
589+
expected: abc.SchedulerBase = ImmediateScheduler()
590+
591+
def source_subscribe(
592+
observer: abc.ObserverBase[int],
593+
scheduler: abc.SchedulerBase | None = None,
594+
) -> abc.DisposableBase:
595+
captured["scheduler"] = scheduler
596+
observer.on_completed()
597+
return Disposable()
598+
599+
source: Observable[int] = Observable(source_subscribe)
600+
source.pipe(ops.to_marbles()).subscribe(scheduler=expected)
601+
602+
assert captured["scheduler"] is expected

tests/test_observable/test_pairwise.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import unittest
22

3+
from reactivex import Observable, abc
34
from reactivex import operators as ops
5+
from reactivex.disposable import Disposable
6+
from reactivex.scheduler import ImmediateScheduler
47
from reactivex.testing import ReactiveTest, TestScheduler
58

69
on_next = ReactiveTest.on_next
@@ -134,3 +137,20 @@ def create():
134137
assert results.messages == [on_next(240, (4, 3))]
135138

136139
assert xs.subscriptions == [subscribe(200, 280)]
140+
141+
def test_pairwise_forwards_scheduler_to_source(self) -> None:
142+
captured: dict[str, abc.SchedulerBase | None] = {}
143+
expected: abc.SchedulerBase = ImmediateScheduler()
144+
145+
def source_subscribe(
146+
observer: abc.ObserverBase[int],
147+
scheduler: abc.SchedulerBase | None = None,
148+
) -> abc.DisposableBase:
149+
captured["scheduler"] = scheduler
150+
observer.on_completed()
151+
return Disposable()
152+
153+
source: Observable[int] = Observable(source_subscribe)
154+
source.pipe(ops.pairwise()).subscribe(scheduler=expected)
155+
156+
assert captured["scheduler"] is expected

0 commit comments

Comments
 (0)