From 367e75c9c47ab85f9b36903304856a1460183741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Sun, 27 Jan 2019 18:39:03 +0100 Subject: [PATCH 01/17] complete marbles syntax + add tests + add new testing functions --- examples/marbles/frommarbles_error.py | 16 + examples/marbles/frommarbles_flatmap.py | 22 ++ examples/marbles/frommarbles_lookup.py | 18 + examples/marbles/frommarbles_merge.py | 15 + examples/marbles/testing_debounce.py | 30 ++ examples/marbles/testing_flatmap.py | 35 ++ rx/testing/marbles.py | 361 +++++++++++++++---- tests/test_testing/test_marbles.py | 457 +++++++++++++++++++++++- 8 files changed, 893 insertions(+), 61 deletions(-) create mode 100644 examples/marbles/frommarbles_error.py create mode 100644 examples/marbles/frommarbles_flatmap.py create mode 100644 examples/marbles/frommarbles_lookup.py create mode 100644 examples/marbles/frommarbles_merge.py create mode 100644 examples/marbles/testing_debounce.py create mode 100644 examples/marbles/testing_flatmap.py diff --git a/examples/marbles/frommarbles_error.py b/examples/marbles/frommarbles_error.py new file mode 100644 index 000000000..24e1e110a --- /dev/null +++ b/examples/marbles/frommarbles_error.py @@ -0,0 +1,16 @@ +import rx +from rx import concurrency as ccy +from rx.testing import marbles + +err = ValueError("I don't like 5!") + +src0 = marbles.from_marbles('12-----4-----67--|', timespan=0.2) +src1 = marbles.from_marbles('----3----5-# ', timespan=0.2, error=err) + +source = rx.merge(src0, src1) +source.subscribe( + on_next=print, + on_error=lambda e: print('boom!! {}'.format(e)), + on_completed=lambda: print('good job!'), + scheduler=ccy.timeout_scheduler, + ) diff --git a/examples/marbles/frommarbles_flatmap.py b/examples/marbles/frommarbles_flatmap.py new file mode 100644 index 000000000..7dddf9b65 --- /dev/null +++ b/examples/marbles/frommarbles_flatmap.py @@ -0,0 +1,22 @@ +from rx import operators as ops +from rx import concurrency as ccy +from rx.testing.marbles import from_marbles + +a = from_marbles(' ---a---a----------------a-|') +b = from_marbles(' ---b---b---| ') +c = from_marbles(' ---c---c---| ') +d = from_marbles(' --d---d---| ') +e1 = from_marbles('a--b--------c--d-------| ') + +observableLookup = {"a": a, "b": b, "c": c, "d": d} + +source = e1.pipe( + ops.flat_map(lambda value: observableLookup[value]), + ) + +source.subscribe_( + on_next=print, + on_error=lambda e: print('boom!! {}'.format(e)), + on_completed=lambda: print('good job!'), + scheduler=ccy.timeout_scheduler, + ) diff --git a/examples/marbles/frommarbles_lookup.py b/examples/marbles/frommarbles_lookup.py new file mode 100644 index 000000000..12f3dac87 --- /dev/null +++ b/examples/marbles/frommarbles_lookup.py @@ -0,0 +1,18 @@ +import rx +from rx import concurrency as ccy +from rx.testing import marbles + +lookup0 = {'a': 1, 'b': 3, 'c': 5} +lookup1 = {'x': 2, 'y': 4, 'z': 6} +source0 = marbles.from_marbles('a---b----c----|', timespan=0.2, lookup=lookup0) +source1 = marbles.from_marbles('---x---y---z--|', timespan=0.2, lookup=lookup1) + +observable = rx.merge(source0, source1) + +observable.subscribe_( + on_next=print, + on_error=lambda e: print('boom!! {}'.format(e)), + on_completed=lambda: print('good job!'), + scheduler=ccy.timeout_scheduler, + ) + diff --git a/examples/marbles/frommarbles_merge.py b/examples/marbles/frommarbles_merge.py new file mode 100644 index 000000000..7840260df --- /dev/null +++ b/examples/marbles/frommarbles_merge.py @@ -0,0 +1,15 @@ +import rx +from rx import concurrency as ccy +from rx.testing import marbles + +source0 = marbles.from_marbles('a-----d---1--------4-|', timespan=0.2) +source1 = marbles.from_marbles('--b-c-------2---3-| ', timespan=0.2) + +observable = rx.merge(source0, source1) + +observable.subscribe( + on_next=print, + on_error=lambda e: print('boom!! {}'.format(e)), + on_completed=lambda: print('good job!'), + scheduler=ccy.timeout_scheduler, + ) diff --git a/examples/marbles/testing_debounce.py b/examples/marbles/testing_debounce.py new file mode 100644 index 000000000..8f0ae920c --- /dev/null +++ b/examples/marbles/testing_debounce.py @@ -0,0 +1,30 @@ +from rx import operators as ops +from rx.testing import marbles + +""" +Tests debounceTime from rxjs +https://github.com/ReactiveX/rxjs/blob/master/spec/operators/debounceTime-spec.ts + +it should delay all element by the specified time +""" + +start, cold, hot, exp = marbles.test_context(timespan=1) + +e1 = hot('-a--------b------c----|') +ex = exp('------a--------b------(c|)') +expected = ex + + +def create(): + return e1.pipe( + ops.debounce(5), + ) + + +results = start(create) +assert results == expected + +print('\ndebounce: results vs expected') +for r, e in zip(results, expected): + print(r, e) + diff --git a/examples/marbles/testing_flatmap.py b/examples/marbles/testing_flatmap.py new file mode 100644 index 000000000..4fc22a356 --- /dev/null +++ b/examples/marbles/testing_flatmap.py @@ -0,0 +1,35 @@ +from rx import operators as ops +from rx.testing import marbles + +""" +Tests MergeMap from rxjs +https://github.com/ReactiveX/rxjs/blob/master/spec/operators/mergeMap-spec.ts + +it should flat_map many regular interval inners +""" +start, cold, hot, exp = marbles.test_context(timespan=1) + +a = cold(' ----a---a---a---(a|) ') +b = cold(' ----1---b---(b|) ') +c = cold(' ----c---c---c---c---(c|)') +d = cold(' ----(d|) ') +e1 = hot('-a---b-----------c-------d-------| ') +ex = exp('-----a---(a1)(ab)(ab)c---c---(cd)c---(c|)') +expected = ex + +observableLookup = {"a": a, "b": b, "c": c, "d": d} + + +def create(): + return e1.pipe( + ops.flat_map(lambda value: observableLookup[value]) + ) + + +results = start(create) +assert results == expected + + +print('\nflat_map: results vs expected') +for r, e in zip(results, expected): + print(r, e) diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index 04d42be1f..fa8c1e90c 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -1,35 +1,61 @@ -from typing import List -import re +from typing import List, Union, Dict +from collections import namedtuple from rx.core import Observable from rx.disposable import CompositeDisposable from rx.concurrency import NewThreadScheduler -from rx.core.notification import OnNext, OnError, OnCompleted +from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Callable, Scheduler +from rx.testing import ReactiveTest, TestScheduler, Recorded new_thread_scheduler = NewThreadScheduler() -_pattern = r"\(?([a-zA-Z0-9]+)\)?|(-|[xX]|\|)" -_tokens = re.compile(_pattern) -def from_marbles(string: str, timespan=0.1) -> Observable: - """Convert a marble diagram string to an observable sequence, using +def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, + error: Exception = None, scheduler: Scheduler = None) -> Observable: + """Convert a marble diagram string to a cold observable sequence, using an optional scheduler to enumerate the events. - Special characters: - - = Timespan of timespan seconds - x = on_error() - | = on_completed() + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted. Digit 0-9 will be cast + to int. - All other characters are treated as an on_next() event at the given - moment they are found on the string. + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time.| + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. Examples: - >>> res = rx.from_marbles("1-2-3-|") - >>> res = rx.from_marbles("1-(42)-3-|") - >>> res = rx.from_marbles("1-2-3-x", timeout_scheduler) + >>> from_marbles("--1--(42)-3--|") + >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) + >>> from_marbles("a--b---#", error=ValueError("foo")) Args: string: String with marble diagram + + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + scheduler: [Optional] Scheduler to run the the input sequence on. @@ -39,49 +65,11 @@ def from_marbles(string: str, timespan=0.1) -> Observable: """ disp = CompositeDisposable() - completed = [False] - messages = [] - timedelta = [0] - - def handle_timespan(value): - timedelta[0] += timespan - - def handle_on_next(value): - try: - value = int(value) - except Exception: - pass + records = parse(string, timespan=timespan, lookup=lookup, error=error) - if value in ('T', 'F'): - value = value == 'T' - messages.append((OnNext(value), timedelta[0])) - - def handle_on_completed(value): - messages.append((OnCompleted(), timedelta[0])) - completed[0] = True - - def handle_on_error(value): - messages.append((OnError(value), timedelta[0])) - completed[0] = True - - specials = { - '-': handle_timespan, - 'x': handle_on_error, - 'X': handle_on_error, - '|': handle_on_completed - } - - for groups in _tokens.findall(string): - for token in groups: - if token: - func = specials.get(token, handle_on_next) - func(token) - - if not completed[0]: - messages.append((OnCompleted(), timedelta[0])) - - def schedule_msg(message, observer, scheduler): - notification, timespan = message + def schedule_msg(record, observer, scheduler): + timespan = record.time + notification = record.value def action(scheduler, state=None): notification.accept(observer) @@ -91,9 +79,9 @@ def action(scheduler, state=None): def subscribe(observer, scheduler): scheduler = scheduler or new_thread_scheduler - for message in messages: + for record in records: # Don't make closures within a loop - schedule_msg(message, observer, scheduler) + schedule_msg(record, observer, scheduler) return disp return Observable(subscribe) @@ -154,3 +142,256 @@ def stringify(value): string = "(%s)" % string return string + + +def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, + lookup: Dict = None, error: Exception = None) -> List[Recorded]: + """Convert a marble diagram string to a list of records of type + :class:`rx.testing.recorded.Recorded`. + + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted according to their horizontal + position in the diagram. Digit 0-9 will be cast to :class:`int`. + + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | `^` | subscription (hot observable only) | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time.| + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + + If a subscription symbol `^` is specified (hot observable), each marble + will be emitted at a time relative to their position from the '^'symbol. + E.g. if subscription time is set to 0, Every marbles + that appears before the `^` will have negative timestamp. + + Examples: + >>> res = parse("1-2-3-|") + >>> res = parse("--1--^-(42)-3--|") + >>> res = parse("a--B---c-", lookup={'a': 1, 'B': 2, 'c': 3}) + + Args: + string: String with marble diagram + + timespan: [Optional] duration of each character. + Default set to 1. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + time_shift: [Optional] absolute time of subscription. + If not specified, defaults to 0. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + + Returns: + A list of records of type :class:`rx.testing.recorded.Recorded` + containing time and :class:`Notification` as value. + """ + + error = error or Exception('error') + lookup = lookup or {} + + string = string.replace(' ', '') + + isub = string.find('^') + if isub > 0: + time_shift -= isub * timespan + + records = [] + group_frame = 0 + in_group = False + has_subscribe = False + + def check_group_opening(): + if in_group: + raise ValueError( + "A group of items must be closed before opening a new one. " + 'Got "{}..."'.format(string[:char_frame+1])) + + def check_group_closing(): + if not in_group: + raise ValueError( + "A Group of items have already been closed before. " + "Got {} ...".format(string[:char_frame+1])) + + def check_subscription(): + if has_subscribe: + raise ValueError( + "Only one subscription is allowed for a hot observable. " + "Got {} ...".format(string[:char_frame+1])) + + def shift(frame): + return frame + time_shift + + for i, char in enumerate(string): + char_frame = i * timespan + + if char == '(': + check_group_opening() + in_group = True + group_frame = char_frame + + elif char == ')': + check_group_closing() + in_group = False + + elif char == '-': + pass + + elif char == '|': + frame = group_frame if in_group else char_frame + record = ReactiveTest.on_completed(shift(frame)) + records.append(record) + + elif char == '#': + frame = group_frame if in_group else char_frame + record = ReactiveTest.on_error(shift(frame), error) + records.append(record) + + elif char == '^': + check_subscription() + + else: + frame = group_frame if in_group else char_frame + try: + char = int(char) + except ValueError: + pass + value = lookup.get(char, char) + record = ReactiveTest.on_next(shift(frame), value) + records.append(record) + + if in_group: + raise ValueError( + "The last group of items has been opened but never closed. " + "Missing a ')." + ) + + return records + + +TestContext = namedtuple('TestContext', 'start, cold, hot, exp') + + +def test_context(timespan=1): + """ + Initialize a :class:`TestScheduler` and return a namedtuple containing the + following functions that wrap its methods. + + :func:`cold()`: + Parse a marbles string and return a cold observable + + :func:`hot()`: + Parse a marbles string and return a hot observable + + :func:`start()`: + Start the test scheduler and invoke the create function, + subscribe to the resulting sequence, dispose the subscription and + return the resulting records + + :func:`exp()`: + Parse a marbles string and return a list of records + + Examples: + >>> start, cold, hot, exp = test_context() + + >>> context = test_context() + >>> context.cold("--a--b--#", error=Exception("foo")) + + >>> e0 = hot("a---^-b---c-|") + >>> ex = exp(" --b---c-|") + >>> results = start(e1) + >>> assert results.messages == ex + + The underlying test scheduler is initialized with the following + parameters: + - created time = 100 + - subscribed = 200 + - disposed = 1000 + + **IMPORTANT**: regarding :func:`hot()`, a marble declared as the + fisrt character will be skipped by the test scheduler. + E.g. `hot("a--b--")` will only emit `b`. + """ + + scheduler = TestScheduler() + created = 100 + disposed = 1000 + subscribed = 200 + + def start(create: Union[Observable, Callable[[], Observable]]) -> List[Recorded]: + + def default_create(): + return create + + if isinstance(create, Observable): + create_function = default_create + else: + create_function = create + + mock_observer = scheduler.start( + create=create_function, + created=created, + subscribed=subscribed, + disposed=disposed, + ) + return mock_observer.messages + + def expected(string: str, lookup: Dict = None, error: Exception = None) -> List[Recorded]: + if string.find('^') >= 0: + raise ValueError( + 'Expected function does not support subscription symbol "^".' + 'Got "{}"'.format(string)) + + return parse( + string, + timespan=timespan, + time_shift=subscribed, + lookup=lookup, + error=error, + ) + + def cold(string: str, lookup: Dict = None, error: Exception = None) -> Observable: + if string.find('^') >= 0: + raise ValueError( + 'Cold observable does not support subscription symbol "^".' + 'Got "{}"'.format(string)) + + records = parse( + string, + timespan=timespan, + time_shift=0, + lookup=lookup, + error=error, + ) + return scheduler.create_cold_observable(records) + + def hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: + records = parse( + string, + timespan=timespan, + time_shift=subscribed, + lookup=lookup, + error=error, + ) + return scheduler.create_hot_observable(records) + + return TestContext(start, cold, hot, expected) + diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 37c71c125..9f93111c7 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -2,10 +2,13 @@ from rx import Observable from rx.testing import marbles, TestScheduler +from rx.testing.reactivetest import ReactiveTest + + #from rx.concurrency import timeout_scheduler, new_thread_scheduler # marble sequences to test: -tested_marbles = '0-1-(10)|', '0|', '(10)-(20)|', '(abc)-|' +#tested_marbles = '0-1-(10)|', '0|', '(10)-(20)|', '(abc)-|' # class TestFromToMarbles(unittest.TestCase): @@ -41,4 +44,456 @@ # expected = [t.replace('-', '') for t in tested_marbles] # self._run_test(expected, new_thread_scheduler, TestScheduler()) +class TestParse(unittest.TestCase): + + def test_parse_on_error(self): + string = "#" + results = marbles.parse(string) + expected = [ReactiveTest.on_error(0, Exception('error'))] + assert results == expected + + def test_parse_on_error_specified(self): + string = "#" + ex = Exception('Foo') + results = marbles.parse(string, error=ex) + expected = [ReactiveTest.on_error(0, ex)] + assert results == expected + + def test_parse_on_complete(self): + string = "|" + results = marbles.parse(string) + expected = [ReactiveTest.on_completed(0)] + assert results == expected + + def test_parse_on_next(self): + string = "a" + results = marbles.parse(string) + expected = [ReactiveTest.on_next(0, 'a')] + assert results == expected + + def test_parse_timespan(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.1 + results = marbles.parse(string, timespan=ts) + expected = [ + ReactiveTest.on_next(0 * ts, 'a'), + ReactiveTest.on_next(3 * ts, 'b'), + ReactiveTest.on_next(7 * ts, 'c'), + ] + assert results == expected + + def test_parse_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + results = marbles.parse(string) + expected = [ + ReactiveTest.on_next(1, 'a'), + ReactiveTest.on_next(2, 'b'), + ReactiveTest.on_next(4, 'c'), + ReactiveTest.on_completed(7), + ] + assert results == expected + + def test_parse_marble_with_error(self): + string = "-ab-c--#--" + " 012345678901234567890" + ex = Exception('ex') + results = marbles.parse(string, error=ex) + expected = [ + ReactiveTest.on_next(1, 'a'), + ReactiveTest.on_next(2, 'b'), + ReactiveTest.on_next(4, 'c'), + ReactiveTest.on_error(7, ex), + ] + assert results == expected + + def test_parse_marble_with_space(self): + string = " -a b- c- - |" + " 012345678901234567890" + results = marbles.parse(string) + expected = [ + ReactiveTest.on_next(1, 'a'), + ReactiveTest.on_next(2, 'b'), + ReactiveTest.on_next(4, 'c'), + ReactiveTest.on_completed(7), + ] + assert results == expected + + + def test_parse_marble_with_group(self): + string = "-(ab)-c--|" + " 012345678901234567890" + results = marbles.parse(string) + expected = [ + ReactiveTest.on_next(1, 'a'), + ReactiveTest.on_next(1, 'b'), + ReactiveTest.on_next(6, 'c'), + ReactiveTest.on_completed(9), + ] + assert results == expected + + + + def test_parse_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'a': 'aa', + 'b': 'bb', + 'c': 'cc', + 1: '11', + 2: '22', + 3: 33, + } + + results = marbles.parse(string, lookup=lookup) + expected = [ + ReactiveTest.on_next(1, 'aa'), + ReactiveTest.on_next(2, 'bb'), + ReactiveTest.on_next(4, 'cc'), + ReactiveTest.on_next(6, '11'), + ReactiveTest.on_next(7, '22'), + ReactiveTest.on_next(9, 33), + ReactiveTest.on_completed(11), + ] + assert results == expected + + + def test_parse_marble_subscribe(self): + string = "-ab--^-c-d-|" + " 8654321012345678901234567890" + results = marbles.parse(string) + expected = [ + ReactiveTest.on_next(-4, 'a'), + ReactiveTest.on_next(-3, 'b'), + ReactiveTest.on_next(2, 'c'), + ReactiveTest.on_next(4, 'd'), + ReactiveTest.on_completed(6), + ] + assert results == expected + + def test_parse_marble_time_shift(self): + string = "-ab----c-d-|" + " 012345678901234567890" + offset = 10 + results = marbles.parse(string, time_shift=offset) + expected = [ + ReactiveTest.on_next(1 + offset, 'a'), + ReactiveTest.on_next(2 + offset, 'b'), + ReactiveTest.on_next(7 + offset, 'c'), + ReactiveTest.on_next(9 + offset, 'd'), + ReactiveTest.on_completed(11 + offset), + ] + assert results == expected + + + def test_parse_marble_time_shift_and_subscribe(self): + string = "-ab--^--c-d-|" + " 654321012345678901234567890" + offset = 10 + results = marbles.parse(string, time_shift=offset) + expected = [ + ReactiveTest.on_next(-4 + offset, 'a'), + ReactiveTest.on_next(-3 + offset, 'b'), + ReactiveTest.on_next(3 + offset, 'c'), + ReactiveTest.on_next(5 + offset, 'd'), + ReactiveTest.on_completed(7 + offset), + ] + assert results == expected + + +class TestFromMarble(unittest.TestCase): + def create_factory(self, observable): + def create(): + return observable + return create + + def test_from_marbles_on_error(self): + string = "#" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, Exception('error'))] + assert results == expected + + def test_from_marbles_on_error_specified(self): + string = "#" + ex = Exception('Foo') + obs = marbles.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, ex)] + assert results == expected + + def test_from_marbles_on_complete(self): + string = "|" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_completed(200)] + assert results == expected + + def test_from_marbles_on_next(self): + string = "a" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_next(200, 'a')] + assert results == expected + + def test_from_marbles_timespan(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.5 + obs = marbles.from_marbles(string, timespan=ts) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(0 * ts + 200, 'a'), + ReactiveTest.on_next(3 * ts + 200, 'b'), + ReactiveTest.on_next(7 * ts + 200, 'c'), + ] + assert results == expected + + def test_from_marbles_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'a'), + ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_from_marbles_marble_with_error(self): + string = "-ab-c--#--" + " 012345678901234567890" + ex = Exception('ex') + obs = marbles.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'a'), + ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_from_marbles_marble_with_space(self): + string = " -a b- c- - |" + " 012 34 56 7 8901234567890" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'a'), + ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_from_marbles_marble_with_group(self): + string = "-(ab)-c--|" + " 012345678901234567890" + obs = marbles.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'a'), + ReactiveTest.on_next(200.1, 'b'), + ReactiveTest.on_next(200.6, 'c'), + ReactiveTest.on_completed(200.9), + ] + assert results == expected + + def test_from_marbles_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'a': 'aa', + 'b': 'bb', + 'c': 'cc', + 1: '11', + 2: '22', + 3: 33, + } + obs = marbles.from_marbles(string, lookup=lookup) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'aa'), + ReactiveTest.on_next(200.2, 'bb'), + ReactiveTest.on_next(200.4, 'cc'), + ReactiveTest.on_next(200.6, '11'), + ReactiveTest.on_next(200.7, '22'), + ReactiveTest.on_next(200.9, 33), + ReactiveTest.on_completed(201.1), + ] + assert results == expected + + +class TestTestContext(unittest.TestCase): + + def test_start_with_cold_never(self): + start, cold, hot, exp = marbles.test_context() + obs = cold("----") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [] + assert results == expected + + def test_start_with_cold_empty(self): + start, cold, hot, exp = marbles.test_context() + obs = cold("------|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [ReactiveTest.on_completed(206)] + assert results == expected + + def test_start_with_cold_normal(self): + start, cold, hot, exp = marbles.test_context() + obs = cold("12--3-|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [ + ReactiveTest.on_next(200, 1), + ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(204, 3), + ReactiveTest.on_completed(206), + ] + assert results == expected + + def test_start_with_cold_no_create_function(self): + start, cold, hot, exp = marbles.test_context() + obs = cold("12--3-|") + " 012345678901234567890" + + results = start(obs) + expected = [ + ReactiveTest.on_next(200, 1), + ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(204, 3), + ReactiveTest.on_completed(206), + ] + assert results == expected + + + def test_start_with_hot_never(self): + start, cold, hot, exp = marbles.test_context() + obs = hot("------") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [] + assert results == expected + + def test_start_with_hot_empty(self): + start, cold, hot, exp = marbles.test_context() + obs = hot("---|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [ReactiveTest.on_completed(203),] + assert results == expected + + def test_start_with_hot_normal(self): + start, cold, hot, exp = marbles.test_context() + obs = hot("-12--3-|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [ + ReactiveTest.on_next(201, 1), + ReactiveTest.on_next(202, 2), + ReactiveTest.on_next(205, 3), + ReactiveTest.on_completed(207), + ] + assert results == expected + + def test_start_with_hot_subscribe(self): + start, cold, hot, exp = marbles.test_context() + obs = hot("12-^-3--4--5-|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + expected = [ + ReactiveTest.on_next(202, 3), + ReactiveTest.on_next(205, 4), + ReactiveTest.on_next(208, 5), + ReactiveTest.on_completed(210), + ] + assert results == expected + + def test_exp(self): + start, cold, hot, exp = marbles.test_context() + results = exp("12--3--4--5-|") + " 012345678901234567890" + + expected = [ + ReactiveTest.on_next(200, 1), + ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(204, 3), + ReactiveTest.on_next(207, 4), + ReactiveTest.on_next(210, 5), + ReactiveTest.on_completed(212), + ] + assert results == expected + + def test_start_with_hot_and_exp(self): + + start, cold, hot, exp = marbles.test_context() + obs = hot(" 12-^-3--4--5-|") + expected = exp(" --3--4--5-|") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + assert results == expected + + def test_start_with_cold_and_exp(self): + + start, cold, hot, exp = marbles.test_context() + obs = cold(" 12--3--4--5-|") + expected = exp(" 12--3--4--5-|") + " 012345678901234567890" + + def create(): + return obs + results = start(create) + assert results == expected From ae22327d8e21511ad56aa77b1918d59ae9f49bc9 Mon Sep 17 00:00:00 2001 From: jcafhe Date: Tue, 29 Jan 2019 10:43:25 +0100 Subject: [PATCH 02/17] sleep the main thread in examples --- examples/marbles/frommarbles_error.py | 4 ++++ examples/marbles/frommarbles_flatmap.py | 4 ++++ examples/marbles/frommarbles_lookup.py | 3 +++ examples/marbles/frommarbles_merge.py | 8 ++++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/marbles/frommarbles_error.py b/examples/marbles/frommarbles_error.py index 24e1e110a..4c7e9956a 100644 --- a/examples/marbles/frommarbles_error.py +++ b/examples/marbles/frommarbles_error.py @@ -1,3 +1,5 @@ +import time + import rx from rx import concurrency as ccy from rx.testing import marbles @@ -14,3 +16,5 @@ on_completed=lambda: print('good job!'), scheduler=ccy.timeout_scheduler, ) + +time.sleep(3) diff --git a/examples/marbles/frommarbles_flatmap.py b/examples/marbles/frommarbles_flatmap.py index 7dddf9b65..051ecdd95 100644 --- a/examples/marbles/frommarbles_flatmap.py +++ b/examples/marbles/frommarbles_flatmap.py @@ -1,3 +1,5 @@ +import time + from rx import operators as ops from rx import concurrency as ccy from rx.testing.marbles import from_marbles @@ -20,3 +22,5 @@ on_completed=lambda: print('good job!'), scheduler=ccy.timeout_scheduler, ) + +time.sleep(3) diff --git a/examples/marbles/frommarbles_lookup.py b/examples/marbles/frommarbles_lookup.py index 12f3dac87..9a642bf99 100644 --- a/examples/marbles/frommarbles_lookup.py +++ b/examples/marbles/frommarbles_lookup.py @@ -1,3 +1,5 @@ +import time + import rx from rx import concurrency as ccy from rx.testing import marbles @@ -16,3 +18,4 @@ scheduler=ccy.timeout_scheduler, ) +time.sleep(3) diff --git a/examples/marbles/frommarbles_merge.py b/examples/marbles/frommarbles_merge.py index 7840260df..c6d3edf11 100644 --- a/examples/marbles/frommarbles_merge.py +++ b/examples/marbles/frommarbles_merge.py @@ -1,9 +1,11 @@ +import time + import rx from rx import concurrency as ccy from rx.testing import marbles -source0 = marbles.from_marbles('a-----d---1--------4-|', timespan=0.2) -source1 = marbles.from_marbles('--b-c-------2---3-| ', timespan=0.2) +source0 = marbles.from_marbles('a-----d---1--------4-|', timespan=0.1) +source1 = marbles.from_marbles('--b-c-------2---3-| ', timespan=0.1) observable = rx.merge(source0, source1) @@ -13,3 +15,5 @@ on_completed=lambda: print('good job!'), scheduler=ccy.timeout_scheduler, ) + +time.sleep(3) From dbe6856b7aed772bc9c0d9c8c6f793d2852efcef Mon Sep 17 00:00:00 2001 From: jcafhe Date: Tue, 29 Jan 2019 14:37:05 +0100 Subject: [PATCH 03/17] fix the scheduler selection mechanism --- rx/testing/marbles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index fa8c1e90c..846df900a 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -76,12 +76,12 @@ def action(scheduler, state=None): disp.add(scheduler.schedule_relative(timespan, action)) - def subscribe(observer, scheduler): - scheduler = scheduler or new_thread_scheduler + def subscribe(observer, scheduler_): + _scheduler = scheduler or scheduler_ or new_thread_scheduler for record in records: # Don't make closures within a loop - schedule_msg(record, observer, scheduler) + schedule_msg(record, observer, _scheduler) return disp return Observable(subscribe) From 07785335365f3b4836d5327680746e06f3d7ada8 Mon Sep 17 00:00:00 2001 From: jcafhe Date: Tue, 29 Jan 2019 14:52:12 +0100 Subject: [PATCH 04/17] remove dependency to testscheduler.create_cold_observable() for cold() function + typo --- rx/testing/marbles.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index 846df900a..847a12608 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -327,7 +327,7 @@ def test_context(timespan=1): - disposed = 1000 **IMPORTANT**: regarding :func:`hot()`, a marble declared as the - fisrt character will be skipped by the test scheduler. + first character will be skipped by the test scheduler. E.g. `hot("a--b--")` will only emit `b`. """ @@ -374,14 +374,12 @@ def cold(string: str, lookup: Dict = None, error: Exception = None) -> Observabl 'Cold observable does not support subscription symbol "^".' 'Got "{}"'.format(string)) - records = parse( + return from_marbles( string, timespan=timespan, - time_shift=0, lookup=lookup, error=error, ) - return scheduler.create_cold_observable(records) def hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: records = parse( From 7f918eeaab1eb37a95feb98d32c078d5e6d488a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Tue, 29 Jan 2019 20:42:58 +0100 Subject: [PATCH 05/17] implement hot not dependent to test scheduler --- examples/marbles/hot.py | 16 ++++++++++++ rx/testing/marbles.py | 57 +++++++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 examples/marbles/hot.py diff --git a/examples/marbles/hot.py b/examples/marbles/hot.py new file mode 100644 index 000000000..74792f1cd --- /dev/null +++ b/examples/marbles/hot.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from rx.testing import marbles +import rx.concurrency as ccy +import datetime + +#start_time = 5 +now = datetime.datetime.utcnow() +start_time = now + datetime.timedelta(seconds=1.0) +hot = marbles.hot('--a--b--c--|', +# timespan=0.3, + start_time=start_time, +# scheduler=ccy.timeout_scheduler, + ) + +hot.subscribe(print, print, lambda: print('completed')) diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index 847a12608..9ce271670 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -9,6 +9,38 @@ new_thread_scheduler = NewThreadScheduler() +from rx import operators as ops +from datetime import datetime +import rx + +def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, + lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: + + scheduler_ = scheduler or new_thread_scheduler + + cold_observable = from_marbles( + string, + timespan=timespan, + lookup=lookup, + error=error, + scheduler=scheduler_, + ) + values = rx.timer(start_time, scheduler=scheduler_).pipe( + ops.flat_map(lambda _: cold_observable), + ops.publish(), + ) + +# duetime = start_time +# if isinstance(start_time, datetime): +# duetime = scheduler_.to_timedelta(start_time - scheduler_.now) +# values = cold_observable.pipe( +# ops.delay(duetime, scheduler=scheduler_), +# ops.publish(), +# ) + + values.connect() + return values + def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: @@ -336,7 +368,7 @@ def test_context(timespan=1): disposed = 1000 subscribed = 200 - def start(create: Union[Observable, Callable[[], Observable]]) -> List[Recorded]: + def test_start(create: Union[Observable, Callable[[], Observable]]) -> List[Recorded]: def default_create(): return create @@ -354,7 +386,7 @@ def default_create(): ) return mock_observer.messages - def expected(string: str, lookup: Dict = None, error: Exception = None) -> List[Recorded]: + def test_expected(string: str, lookup: Dict = None, error: Exception = None) -> List[Recorded]: if string.find('^') >= 0: raise ValueError( 'Expected function does not support subscription symbol "^".' @@ -368,7 +400,7 @@ def expected(string: str, lookup: Dict = None, error: Exception = None) -> List[ error=error, ) - def cold(string: str, lookup: Dict = None, error: Exception = None) -> Observable: + def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Observable: if string.find('^') >= 0: raise ValueError( 'Cold observable does not support subscription symbol "^".' @@ -381,15 +413,24 @@ def cold(string: str, lookup: Dict = None, error: Exception = None) -> Observabl error=error, ) - def hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: - records = parse( + def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: +# records = parse( +# string, +# timespan=timespan, +# time_shift=subscribed, +# lookup=lookup, +# error=error, +# ) +# return scheduler.create_hot_observable(records) + hot_obs = hot( string, timespan=timespan, - time_shift=subscribed, + start_time=subscribed, lookup=lookup, error=error, + scheduler=scheduler, ) - return scheduler.create_hot_observable(records) + return hot_obs - return TestContext(start, cold, hot, expected) + return TestContext(test_start, test_cold, test_hot, test_expected) From 812aba37404274ee3f46e78c1f587c136d3a6645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Wed, 30 Jan 2019 18:35:10 +0100 Subject: [PATCH 06/17] move from_marbles(), to_iterable(), hot(), parse() from rx.testing.marbles to rx.core.observable.marbles + update tests & examples --- examples/marbles/frommarbles_error.py | 5 +- examples/marbles/frommarbles_flatmap.py | 12 +- examples/marbles/frommarbles_lookup.py | 5 +- examples/marbles/frommarbles_merge.py | 5 +- examples/marbles/hot.py | 5 +- rx/__init__.py | 69 +++++ rx/core/observable/marbles.py | 319 +++++++++++++++++++++++ rx/testing/marbles.py | 321 +----------------------- tests/test_testing/test_marbles.py | 50 ++-- 9 files changed, 435 insertions(+), 356 deletions(-) create mode 100644 rx/core/observable/marbles.py diff --git a/examples/marbles/frommarbles_error.py b/examples/marbles/frommarbles_error.py index 4c7e9956a..47c083468 100644 --- a/examples/marbles/frommarbles_error.py +++ b/examples/marbles/frommarbles_error.py @@ -2,12 +2,11 @@ import rx from rx import concurrency as ccy -from rx.testing import marbles err = ValueError("I don't like 5!") -src0 = marbles.from_marbles('12-----4-----67--|', timespan=0.2) -src1 = marbles.from_marbles('----3----5-# ', timespan=0.2, error=err) +src0 = rx.from_marbles('12-----4-----67--|', timespan=0.2) +src1 = rx.from_marbles('----3----5-# ', timespan=0.2, error=err) source = rx.merge(src0, src1) source.subscribe( diff --git a/examples/marbles/frommarbles_flatmap.py b/examples/marbles/frommarbles_flatmap.py index 051ecdd95..2f36152b2 100644 --- a/examples/marbles/frommarbles_flatmap.py +++ b/examples/marbles/frommarbles_flatmap.py @@ -1,14 +1,14 @@ import time +import rx from rx import operators as ops from rx import concurrency as ccy -from rx.testing.marbles import from_marbles -a = from_marbles(' ---a---a----------------a-|') -b = from_marbles(' ---b---b---| ') -c = from_marbles(' ---c---c---| ') -d = from_marbles(' --d---d---| ') -e1 = from_marbles('a--b--------c--d-------| ') +a = rx.cold(' ---a---a----------------a-|') +b = rx.cold(' ---b---b---| ') +c = rx.cold(' ---c---c---| ') +d = rx.cold(' --d---d---| ') +e1 = rx.cold('a--b--------c--d-------| ') observableLookup = {"a": a, "b": b, "c": c, "d": d} diff --git a/examples/marbles/frommarbles_lookup.py b/examples/marbles/frommarbles_lookup.py index 9a642bf99..285b79848 100644 --- a/examples/marbles/frommarbles_lookup.py +++ b/examples/marbles/frommarbles_lookup.py @@ -2,12 +2,11 @@ import rx from rx import concurrency as ccy -from rx.testing import marbles lookup0 = {'a': 1, 'b': 3, 'c': 5} lookup1 = {'x': 2, 'y': 4, 'z': 6} -source0 = marbles.from_marbles('a---b----c----|', timespan=0.2, lookup=lookup0) -source1 = marbles.from_marbles('---x---y---z--|', timespan=0.2, lookup=lookup1) +source0 = rx.cold('a---b----c----|', timespan=0.2, lookup=lookup0) +source1 = rx.cold('---x---y---z--|', timespan=0.2, lookup=lookup1) observable = rx.merge(source0, source1) diff --git a/examples/marbles/frommarbles_merge.py b/examples/marbles/frommarbles_merge.py index c6d3edf11..acfb244b6 100644 --- a/examples/marbles/frommarbles_merge.py +++ b/examples/marbles/frommarbles_merge.py @@ -2,10 +2,9 @@ import rx from rx import concurrency as ccy -from rx.testing import marbles -source0 = marbles.from_marbles('a-----d---1--------4-|', timespan=0.1) -source1 = marbles.from_marbles('--b-c-------2---3-| ', timespan=0.1) +source0 = rx.cold('a-----d---1--------4-|', timespan=0.1) +source1 = rx.cold('--b-c-------2---3-| ', timespan=0.1) observable = rx.merge(source0, source1) diff --git a/examples/marbles/hot.py b/examples/marbles/hot.py index 74792f1cd..17a46ef12 100644 --- a/examples/marbles/hot.py +++ b/examples/marbles/hot.py @@ -3,11 +3,12 @@ from rx.testing import marbles import rx.concurrency as ccy import datetime +import rx #start_time = 5 now = datetime.datetime.utcnow() -start_time = now + datetime.timedelta(seconds=1.0) -hot = marbles.hot('--a--b--c--|', +start_time = now + datetime.timedelta(seconds=3.0) +hot = rx.hot('--a--b--c--|', # timespan=0.3, start_time=start_time, # scheduler=ccy.timeout_scheduler, diff --git a/rx/__init__.py b/rx/__init__.py index 9210b8fef..3529bcf7f 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -232,6 +232,75 @@ def from_iterable(iterable: Iterable) -> Observable: from_list = from_iterable +def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typing.Scheduler = None, + lookup = None, error: Exception = None) -> Observable: + """Convert a marble diagram string to a cold observable sequence, using + an optional scheduler to enumerate the events. + + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted. numbers will be cast + to int or float. + + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time.| + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + + Examples: + >>> from_marbles("--1--(42)-3--|") + >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) + >>> from_marbles("a--b---#", error=ValueError("foo")) + + Args: + string: String with marble diagram + + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + + scheduler: [Optional] Scheduler to run the the input sequence + on. + + Returns: + The observable sequence whose elements are pulled from the + given marble diagram string. + """ + + from .core.observable.marbles import from_marbles as _from_marbles + return _from_marbles(string, timespan=timespan, lookup=lookup, error=error, scheduler=scheduler) + +cold = from_marbles + +# TODO: need to move hot() operator (not in alphabetic order) +# TODO: write the doc +def hot(string, timespan: typing.RelativeTime=0.1, start_time = 0.0, scheduler: typing.Scheduler = None, + lookup = None, error: Exception = None) -> Observable: + + from .core.observable.marbles import hot as _hot + return _hot(string, timespan=timespan, start_time=start_time, lookup=lookup, error=error, scheduler=scheduler) + + + def generate_with_relative_time(initial_state, condition, iterate, time_mapper) -> Observable: """Generates an observable sequence by iterating a state from an initial state until the condition fails. diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py new file mode 100644 index 000000000..50ff6b83a --- /dev/null +++ b/rx/core/observable/marbles.py @@ -0,0 +1,319 @@ +from typing import List, Dict + +from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler +from rx import Observable +from rx.disposable import CompositeDisposable +from rx.concurrency import NewThreadScheduler + +# TODO: hot() should not rely on operators since it could be used for testing +import rx +from rx import operators as ops + +# TODO: remove dependency to testing +from rx.testing.recorded import Recorded +from rx.testing.reactivetest import ReactiveTest + +new_thread_scheduler = NewThreadScheduler() + +# TODO: rename start_time to duetime +# TODO: use a plain impelementation instead of operators +def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, + lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: + + + scheduler_ = scheduler or new_thread_scheduler + + cold_observable = from_marbles( + string, + timespan=timespan, + lookup=lookup, + error=error, + scheduler=scheduler_, + ) + values = rx.timer(start_time, scheduler=scheduler_).pipe( + ops.flat_map(lambda _: cold_observable), + ops.publish(), + ) + + values.connect() + return values + + +def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, + error: Exception = None, scheduler: Scheduler = None) -> Observable: + """Convert a marble diagram string to a cold observable sequence, using + an optional scheduler to enumerate the events. + + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted. Digit 0-9 will be cast + to int. + + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time.| + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + + Examples: + >>> from_marbles("--1--(42)-3--|") + >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) + >>> from_marbles("a--b---#", error=ValueError("foo")) + + Args: + string: String with marble diagram + + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + + scheduler: [Optional] Scheduler to run the the input sequence + on. + + Returns: + The observable sequence whose elements are pulled from the + given marble diagram string. + """ + + disp = CompositeDisposable() + records = parse(string, timespan=timespan, lookup=lookup, error=error) + + def schedule_msg(record, observer, scheduler): + timespan = record.time + notification = record.value + + def action(scheduler, state=None): + notification.accept(observer) + + disp.add(scheduler.schedule_relative(timespan, action)) + + def subscribe(observer, scheduler_): + _scheduler = scheduler or scheduler_ or new_thread_scheduler + + for record in records: + # Don't make closures within a loop + schedule_msg(record, observer, _scheduler) + return disp + return Observable(subscribe) + + +def to_marbles(scheduler=None, timespan=0.1): + """Convert an observable sequence into a marble diagram string + + Args: + scheduler: [Optional] The scheduler used to run the the input + sequence on. + + Returns: + Observable stream. + """ + def _to_marbles(source: Observable) -> Observable: + def subscribe(observer, scheduler=None): + scheduler = scheduler or new_thread_scheduler + + result: List[str] = [] + last = scheduler.now + + def add_timespan(): + nonlocal last + + now = scheduler.now + diff = now - last + last = now + secs = scheduler.to_seconds(diff) + dashes = "-" * int((secs + timespan / 2.0) * (1.0 / timespan)) + result.append(dashes) + + def on_next(value): + add_timespan() + result.append(stringify(value)) + + def on_error(exception): + add_timespan() + result.append(stringify(exception)) + observer.on_next("".join(n for n in result)) + observer.on_completed() + + def on_completed(): + add_timespan() + result.append("|") + observer.on_next("".join(n for n in result)) + observer.on_completed() + + return source.subscribe_(on_next, on_error, on_completed) + return Observable(subscribe) + return _to_marbles + + +def stringify(value): + """Utility for stringifying an event. + """ + string = str(value) + if len(string) > 1: + string = "(%s)" % string + + return string + +# TODO: remove support of subscription symbol ^ +# TODO: consecutive characters should be considered as one element +# TODO: add support of comma , to split elements in group +def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, + lookup: Dict = None, error: Exception = None) -> List[Recorded]: + """Convert a marble diagram string to a list of records of type + :class:`rx.testing.recorded.Recorded`. + + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted according to their horizontal + position in the diagram. Digit 0-9 will be cast to :class:`int`. + + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | `^` | subscription (hot observable only) | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time.| + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + + If a subscription symbol `^` is specified (hot observable), each marble + will be emitted at a time relative to their position from the '^'symbol. + E.g. if subscription time is set to 0, Every marbles + that appears before the `^` will have negative timestamp. + + Examples: + >>> res = parse("1-2-3-|") + >>> res = parse("--1--^-(42)-3--|") + >>> res = parse("a--B---c-", lookup={'a': 1, 'B': 2, 'c': 3}) + + Args: + string: String with marble diagram + + timespan: [Optional] duration of each character. + Default set to 1. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + time_shift: [Optional] absolute time of subscription. + If not specified, defaults to 0. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + + Returns: + A list of records of type :class:`rx.testing.recorded.Recorded` + containing time and :class:`Notification` as value. + """ + + error = error or Exception('error') + lookup = lookup or {} + + string = string.replace(' ', '') + + isub = string.find('^') + if isub > 0: + time_shift -= isub * timespan + + records = [] + group_frame = 0 + in_group = False + has_subscribe = False + + def check_group_opening(): + if in_group: + raise ValueError( + "A group of items must be closed before opening a new one. " + 'Got "{}..."'.format(string[:char_frame+1])) + + def check_group_closing(): + if not in_group: + raise ValueError( + "A Group of items have already been closed before. " + "Got {} ...".format(string[:char_frame+1])) + + def check_subscription(): + if has_subscribe: + raise ValueError( + "Only one subscription is allowed for a hot observable. " + "Got {} ...".format(string[:char_frame+1])) + + def shift(frame): + return frame + time_shift + + for i, char in enumerate(string): + char_frame = i * timespan + + if char == '(': + check_group_opening() + in_group = True + group_frame = char_frame + + elif char == ')': + check_group_closing() + in_group = False + + elif char == '-': + pass + + elif char == '|': + frame = group_frame if in_group else char_frame + record = ReactiveTest.on_completed(shift(frame)) + records.append(record) + + elif char == '#': + frame = group_frame if in_group else char_frame + record = ReactiveTest.on_error(shift(frame), error) + records.append(record) + + elif char == '^': + check_subscription() + + else: + frame = group_frame if in_group else char_frame + try: + char = int(char) + except ValueError: + pass + value = lookup.get(char, char) + record = ReactiveTest.on_next(shift(frame), value) + records.append(record) + + if in_group: + raise ValueError( + "The last group of items has been opened but never closed. " + "Missing a ')." + ) + + return records + diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index 9ce271670..86595b189 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -1,324 +1,15 @@ from typing import List, Union, Dict from collections import namedtuple +import rx from rx.core import Observable -from rx.disposable import CompositeDisposable from rx.concurrency import NewThreadScheduler -from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Callable, Scheduler -from rx.testing import ReactiveTest, TestScheduler, Recorded +from rx.core.typing import Callable +from rx.testing import TestScheduler, Recorded +from rx.core.observable.marbles import parse new_thread_scheduler = NewThreadScheduler() -from rx import operators as ops -from datetime import datetime -import rx - -def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, - lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: - - scheduler_ = scheduler or new_thread_scheduler - - cold_observable = from_marbles( - string, - timespan=timespan, - lookup=lookup, - error=error, - scheduler=scheduler_, - ) - values = rx.timer(start_time, scheduler=scheduler_).pipe( - ops.flat_map(lambda _: cold_observable), - ops.publish(), - ) - -# duetime = start_time -# if isinstance(start_time, datetime): -# duetime = scheduler_.to_timedelta(start_time - scheduler_.now) -# values = cold_observable.pipe( -# ops.delay(duetime, scheduler=scheduler_), -# ops.publish(), -# ) - - values.connect() - return values - - -def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, - error: Exception = None, scheduler: Scheduler = None) -> Observable: - """Convert a marble diagram string to a cold observable sequence, using - an optional scheduler to enumerate the events. - - Each character in the string will advance time by timespan - (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted. Digit 0-9 will be cast - to int. - - Special characters: - +--------+--------------------------------------------------------+ - | `-` | advance time by timespan | - +--------+--------------------------------------------------------+ - | `#` | on_error() | - +--------+--------------------------------------------------------+ - | `|` | on_completed() | - +--------+--------------------------------------------------------+ - | `(` | open a group of marbles sharing the same timestamp | - +--------+--------------------------------------------------------+ - | `)` | close a group of marbles | - +--------+--------------------------------------------------------+ - | space | used to align multiple diagrams, does not advance time.| - +--------+--------------------------------------------------------+ - - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. - - Examples: - >>> from_marbles("--1--(42)-3--|") - >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) - >>> from_marbles("a--b---#", error=ValueError("foo")) - - Args: - string: String with marble diagram - - timespan: [Optional] duration of each character in second. - If not specified, defaults to 0.1s. - - lookup: [Optional] dict used to convert a marble into a specified - value. If not specified, defaults to {}. - - error: [Optional] exception that will be use in place of the # symbol. - If not specified, defaults to Exception('error'). - - scheduler: [Optional] Scheduler to run the the input sequence - on. - - Returns: - The observable sequence whose elements are pulled from the - given marble diagram string. - """ - - disp = CompositeDisposable() - records = parse(string, timespan=timespan, lookup=lookup, error=error) - - def schedule_msg(record, observer, scheduler): - timespan = record.time - notification = record.value - - def action(scheduler, state=None): - notification.accept(observer) - - disp.add(scheduler.schedule_relative(timespan, action)) - - def subscribe(observer, scheduler_): - _scheduler = scheduler or scheduler_ or new_thread_scheduler - - for record in records: - # Don't make closures within a loop - schedule_msg(record, observer, _scheduler) - return disp - return Observable(subscribe) - - -def to_marbles(scheduler=None, timespan=0.1): - """Convert an observable sequence into a marble diagram string - - Args: - scheduler: [Optional] The scheduler used to run the the input - sequence on. - - Returns: - Observable stream. - """ - def _to_marbles(source: Observable) -> Observable: - def subscribe(observer, scheduler=None): - scheduler = scheduler or new_thread_scheduler - - result: List[str] = [] - last = scheduler.now - - def add_timespan(): - nonlocal last - - now = scheduler.now - diff = now - last - last = now - secs = scheduler.to_seconds(diff) - dashes = "-" * int((secs + timespan / 2.0) * (1.0 / timespan)) - result.append(dashes) - - def on_next(value): - add_timespan() - result.append(stringify(value)) - - def on_error(exception): - add_timespan() - result.append(stringify(exception)) - observer.on_next("".join(n for n in result)) - observer.on_completed() - - def on_completed(): - add_timespan() - result.append("|") - observer.on_next("".join(n for n in result)) - observer.on_completed() - - return source.subscribe_(on_next, on_error, on_completed) - return Observable(subscribe) - return _to_marbles - - -def stringify(value): - """Utility for stringifying an event. - """ - string = str(value) - if len(string) > 1: - string = "(%s)" % string - - return string - - -def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, - lookup: Dict = None, error: Exception = None) -> List[Recorded]: - """Convert a marble diagram string to a list of records of type - :class:`rx.testing.recorded.Recorded`. - - Each character in the string will advance time by timespan - (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted according to their horizontal - position in the diagram. Digit 0-9 will be cast to :class:`int`. - - Special characters: - +--------+--------------------------------------------------------+ - | `-` | advance time by timespan | - +--------+--------------------------------------------------------+ - | `#` | on_error() | - +--------+--------------------------------------------------------+ - | `|` | on_completed() | - +--------+--------------------------------------------------------+ - | `(` | open a group of marbles sharing the same timestamp | - +--------+--------------------------------------------------------+ - | `)` | close a group of marbles | - +--------+--------------------------------------------------------+ - | `^` | subscription (hot observable only) | - +--------+--------------------------------------------------------+ - | space | used to align multiple diagrams, does not advance time.| - +--------+--------------------------------------------------------+ - - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. - - If a subscription symbol `^` is specified (hot observable), each marble - will be emitted at a time relative to their position from the '^'symbol. - E.g. if subscription time is set to 0, Every marbles - that appears before the `^` will have negative timestamp. - - Examples: - >>> res = parse("1-2-3-|") - >>> res = parse("--1--^-(42)-3--|") - >>> res = parse("a--B---c-", lookup={'a': 1, 'B': 2, 'c': 3}) - - Args: - string: String with marble diagram - - timespan: [Optional] duration of each character. - Default set to 1. - - lookup: [Optional] dict used to convert a marble into a specified - value. If not specified, defaults to {}. - - time_shift: [Optional] absolute time of subscription. - If not specified, defaults to 0. - - error: [Optional] exception that will be use in place of the # symbol. - If not specified, defaults to Exception('error'). - - Returns: - A list of records of type :class:`rx.testing.recorded.Recorded` - containing time and :class:`Notification` as value. - """ - - error = error or Exception('error') - lookup = lookup or {} - - string = string.replace(' ', '') - - isub = string.find('^') - if isub > 0: - time_shift -= isub * timespan - - records = [] - group_frame = 0 - in_group = False - has_subscribe = False - - def check_group_opening(): - if in_group: - raise ValueError( - "A group of items must be closed before opening a new one. " - 'Got "{}..."'.format(string[:char_frame+1])) - - def check_group_closing(): - if not in_group: - raise ValueError( - "A Group of items have already been closed before. " - "Got {} ...".format(string[:char_frame+1])) - - def check_subscription(): - if has_subscribe: - raise ValueError( - "Only one subscription is allowed for a hot observable. " - "Got {} ...".format(string[:char_frame+1])) - - def shift(frame): - return frame + time_shift - - for i, char in enumerate(string): - char_frame = i * timespan - - if char == '(': - check_group_opening() - in_group = True - group_frame = char_frame - - elif char == ')': - check_group_closing() - in_group = False - - elif char == '-': - pass - - elif char == '|': - frame = group_frame if in_group else char_frame - record = ReactiveTest.on_completed(shift(frame)) - records.append(record) - - elif char == '#': - frame = group_frame if in_group else char_frame - record = ReactiveTest.on_error(shift(frame), error) - records.append(record) - - elif char == '^': - check_subscription() - - else: - frame = group_frame if in_group else char_frame - try: - char = int(char) - except ValueError: - pass - value = lookup.get(char, char) - record = ReactiveTest.on_next(shift(frame), value) - records.append(record) - - if in_group: - raise ValueError( - "The last group of items has been opened but never closed. " - "Missing a ')." - ) - - return records - - TestContext = namedtuple('TestContext', 'start, cold, hot, exp') @@ -406,7 +97,7 @@ def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Obse 'Cold observable does not support subscription symbol "^".' 'Got "{}"'.format(string)) - return from_marbles( + return rx.from_marbles( string, timespan=timespan, lookup=lookup, @@ -422,7 +113,7 @@ def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Obser # error=error, # ) # return scheduler.create_hot_observable(records) - hot_obs = hot( + hot_obs = rx.hot( string, timespan=timespan, start_time=subscribed, diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 9f93111c7..07b580882 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -1,7 +1,9 @@ import unittest +import rx +from rx.core.observable.marbles import parse from rx import Observable -from rx.testing import marbles, TestScheduler +from rx.testing import TestScheduler, marbles from rx.testing.reactivetest import ReactiveTest @@ -48,26 +50,26 @@ class TestParse(unittest.TestCase): def test_parse_on_error(self): string = "#" - results = marbles.parse(string) + results = parse(string) expected = [ReactiveTest.on_error(0, Exception('error'))] assert results == expected def test_parse_on_error_specified(self): string = "#" ex = Exception('Foo') - results = marbles.parse(string, error=ex) + results = parse(string, error=ex) expected = [ReactiveTest.on_error(0, ex)] assert results == expected def test_parse_on_complete(self): string = "|" - results = marbles.parse(string) + results = parse(string) expected = [ReactiveTest.on_completed(0)] assert results == expected def test_parse_on_next(self): string = "a" - results = marbles.parse(string) + results = parse(string) expected = [ReactiveTest.on_next(0, 'a')] assert results == expected @@ -75,7 +77,7 @@ def test_parse_timespan(self): string = "a--b---c" " 012345678901234567890" ts = 0.1 - results = marbles.parse(string, timespan=ts) + results = parse(string, timespan=ts) expected = [ ReactiveTest.on_next(0 * ts, 'a'), ReactiveTest.on_next(3 * ts, 'b'), @@ -86,7 +88,7 @@ def test_parse_timespan(self): def test_parse_marble_completed(self): string = "-ab-c--|" " 012345678901234567890" - results = marbles.parse(string) + results = parse(string) expected = [ ReactiveTest.on_next(1, 'a'), ReactiveTest.on_next(2, 'b'), @@ -99,7 +101,7 @@ def test_parse_marble_with_error(self): string = "-ab-c--#--" " 012345678901234567890" ex = Exception('ex') - results = marbles.parse(string, error=ex) + results = parse(string, error=ex) expected = [ ReactiveTest.on_next(1, 'a'), ReactiveTest.on_next(2, 'b'), @@ -111,7 +113,7 @@ def test_parse_marble_with_error(self): def test_parse_marble_with_space(self): string = " -a b- c- - |" " 012345678901234567890" - results = marbles.parse(string) + results = parse(string) expected = [ ReactiveTest.on_next(1, 'a'), ReactiveTest.on_next(2, 'b'), @@ -124,7 +126,7 @@ def test_parse_marble_with_space(self): def test_parse_marble_with_group(self): string = "-(ab)-c--|" " 012345678901234567890" - results = marbles.parse(string) + results = parse(string) expected = [ ReactiveTest.on_next(1, 'a'), ReactiveTest.on_next(1, 'b'), @@ -147,7 +149,7 @@ def test_parse_marble_lookup(self): 3: 33, } - results = marbles.parse(string, lookup=lookup) + results = parse(string, lookup=lookup) expected = [ ReactiveTest.on_next(1, 'aa'), ReactiveTest.on_next(2, 'bb'), @@ -163,7 +165,7 @@ def test_parse_marble_lookup(self): def test_parse_marble_subscribe(self): string = "-ab--^-c-d-|" " 8654321012345678901234567890" - results = marbles.parse(string) + results = parse(string) expected = [ ReactiveTest.on_next(-4, 'a'), ReactiveTest.on_next(-3, 'b'), @@ -177,7 +179,7 @@ def test_parse_marble_time_shift(self): string = "-ab----c-d-|" " 012345678901234567890" offset = 10 - results = marbles.parse(string, time_shift=offset) + results = parse(string, time_shift=offset) expected = [ ReactiveTest.on_next(1 + offset, 'a'), ReactiveTest.on_next(2 + offset, 'b'), @@ -192,7 +194,7 @@ def test_parse_marble_time_shift_and_subscribe(self): string = "-ab--^--c-d-|" " 654321012345678901234567890" offset = 10 - results = marbles.parse(string, time_shift=offset) + results = parse(string, time_shift=offset) expected = [ ReactiveTest.on_next(-4 + offset, 'a'), ReactiveTest.on_next(-3 + offset, 'b'), @@ -211,7 +213,7 @@ def create(): def test_from_marbles_on_error(self): string = "#" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages @@ -221,7 +223,7 @@ def test_from_marbles_on_error(self): def test_from_marbles_on_error_specified(self): string = "#" ex = Exception('Foo') - obs = marbles.from_marbles(string, error=ex) + obs = rx.from_marbles(string, error=ex) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages @@ -230,7 +232,7 @@ def test_from_marbles_on_error_specified(self): def test_from_marbles_on_complete(self): string = "|" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ReactiveTest.on_completed(200)] @@ -238,7 +240,7 @@ def test_from_marbles_on_complete(self): def test_from_marbles_on_next(self): string = "a" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ReactiveTest.on_next(200, 'a')] @@ -248,7 +250,7 @@ def test_from_marbles_timespan(self): string = "a--b---c" " 012345678901234567890" ts = 0.5 - obs = marbles.from_marbles(string, timespan=ts) + obs = rx.from_marbles(string, timespan=ts) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ @@ -261,7 +263,7 @@ def test_from_marbles_timespan(self): def test_from_marbles_marble_completed(self): string = "-ab-c--|" " 012345678901234567890" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ @@ -276,7 +278,7 @@ def test_from_marbles_marble_with_error(self): string = "-ab-c--#--" " 012345678901234567890" ex = Exception('ex') - obs = marbles.from_marbles(string, error=ex) + obs = rx.from_marbles(string, error=ex) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ @@ -290,7 +292,7 @@ def test_from_marbles_marble_with_error(self): def test_from_marbles_marble_with_space(self): string = " -a b- c- - |" " 012 34 56 7 8901234567890" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ @@ -304,7 +306,7 @@ def test_from_marbles_marble_with_space(self): def test_from_marbles_marble_with_group(self): string = "-(ab)-c--|" " 012345678901234567890" - obs = marbles.from_marbles(string) + obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ @@ -326,7 +328,7 @@ def test_from_marbles_marble_lookup(self): 2: '22', 3: 33, } - obs = marbles.from_marbles(string, lookup=lookup) + obs = rx.from_marbles(string, lookup=lookup) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ From b7b6dfe9806f2445642256c31bf06232041bfe1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Wed, 30 Jan 2019 22:32:19 +0100 Subject: [PATCH 07/17] remove dependency to rx.testing.recorded.Recorded() + update test --- rx/core/observable/marbles.py | 51 +++++------ tests/test_testing/test_marbles.py | 142 +++++++++++------------------ 2 files changed, 73 insertions(+), 120 deletions(-) diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index 50ff6b83a..4ceea97f7 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -1,20 +1,18 @@ -from typing import List, Dict +from typing import List, Dict, Tuple -from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler from rx import Observable +from rx.core import notification from rx.disposable import CompositeDisposable from rx.concurrency import NewThreadScheduler +from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler # TODO: hot() should not rely on operators since it could be used for testing import rx from rx import operators as ops -# TODO: remove dependency to testing -from rx.testing.recorded import Recorded -from rx.testing.reactivetest import ReactiveTest - new_thread_scheduler = NewThreadScheduler() + # TODO: rename start_time to duetime # TODO: use a plain impelementation instead of operators def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, @@ -94,11 +92,10 @@ def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, """ disp = CompositeDisposable() - records = parse(string, timespan=timespan, lookup=lookup, error=error) + messages = parse(string, timespan=timespan, lookup=lookup, error=error) - def schedule_msg(record, observer, scheduler): - timespan = record.time - notification = record.value + def schedule_msg(message, observer, scheduler): + timespan, notification = message def action(scheduler, state=None): notification.accept(observer) @@ -108,9 +105,10 @@ def action(scheduler, state=None): def subscribe(observer, scheduler_): _scheduler = scheduler or scheduler_ or new_thread_scheduler - for record in records: - # Don't make closures within a loop - schedule_msg(record, observer, _scheduler) + for message in messages: + + # Don't make closures within a loop + schedule_msg(message, observer, _scheduler) return disp return Observable(subscribe) @@ -175,8 +173,9 @@ def stringify(value): # TODO: remove support of subscription symbol ^ # TODO: consecutive characters should be considered as one element # TODO: add support of comma , to split elements in group +# TODO: complete the definition of the return type List[tuple] def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, - lookup: Dict = None, error: Exception = None) -> List[Recorded]: + lookup: Dict = None, error: Exception = None) -> List[Tuple]: """Convert a marble diagram string to a list of records of type :class:`rx.testing.recorded.Recorded`. @@ -197,8 +196,6 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativ +--------+--------------------------------------------------------+ | `)` | close a group of marbles | +--------+--------------------------------------------------------+ - | `^` | subscription (hot observable only) | - +--------+--------------------------------------------------------+ | space | used to align multiple diagrams, does not advance time.| +--------+--------------------------------------------------------+ @@ -206,11 +203,6 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativ timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. - If a subscription symbol `^` is specified (hot observable), each marble - will be emitted at a time relative to their position from the '^'symbol. - E.g. if subscription time is set to 0, Every marbles - that appears before the `^` will have negative timestamp. - Examples: >>> res = parse("1-2-3-|") >>> res = parse("--1--^-(42)-3--|") @@ -245,7 +237,7 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativ if isub > 0: time_shift -= isub * timespan - records = [] + messages = [] group_frame = 0 in_group = False has_subscribe = False @@ -288,14 +280,13 @@ def shift(frame): elif char == '|': frame = group_frame if in_group else char_frame - record = ReactiveTest.on_completed(shift(frame)) - records.append(record) + message = (shift(frame), notification.OnCompleted()) + messages.append(message) elif char == '#': frame = group_frame if in_group else char_frame - record = ReactiveTest.on_error(shift(frame), error) - records.append(record) - + message = (shift(frame), notification.OnError(error)) + messages.append(message) elif char == '^': check_subscription() @@ -306,8 +297,8 @@ def shift(frame): except ValueError: pass value = lookup.get(char, char) - record = ReactiveTest.on_next(shift(frame), value) - records.append(record) + message = (shift(frame), notification.OnNext(value)) + messages.append(message) if in_group: raise ValueError( @@ -315,5 +306,5 @@ def shift(frame): "Missing a ')." ) - return records + return messages diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 07b580882..a65208774 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -5,7 +5,7 @@ from rx import Observable from rx.testing import TestScheduler, marbles from rx.testing.reactivetest import ReactiveTest - +from rx.core import notification #from rx.concurrency import timeout_scheduler, new_thread_scheduler @@ -46,31 +46,43 @@ # expected = [t.replace('-', '') for t in tested_marbles] # self._run_test(expected, new_thread_scheduler, TestScheduler()) +def mess_on_next(time, value): + return (time, notification.OnNext(value)) + + +def mess_on_error(time, error): + return (time, notification.OnError(error)) + + +def mess_on_completed(time): + return (time, notification.OnCompleted()) + + class TestParse(unittest.TestCase): def test_parse_on_error(self): string = "#" results = parse(string) - expected = [ReactiveTest.on_error(0, Exception('error'))] + expected = [mess_on_error(0, Exception('error'))] assert results == expected def test_parse_on_error_specified(self): string = "#" ex = Exception('Foo') results = parse(string, error=ex) - expected = [ReactiveTest.on_error(0, ex)] + expected = [mess_on_error(0, ex)] assert results == expected def test_parse_on_complete(self): string = "|" results = parse(string) - expected = [ReactiveTest.on_completed(0)] + expected = [mess_on_completed(0)] assert results == expected def test_parse_on_next(self): string = "a" results = parse(string) - expected = [ReactiveTest.on_next(0, 'a')] + expected = [mess_on_next(0, 'a')] assert results == expected def test_parse_timespan(self): @@ -79,9 +91,9 @@ def test_parse_timespan(self): ts = 0.1 results = parse(string, timespan=ts) expected = [ - ReactiveTest.on_next(0 * ts, 'a'), - ReactiveTest.on_next(3 * ts, 'b'), - ReactiveTest.on_next(7 * ts, 'c'), + mess_on_next(0 * ts, 'a'), + mess_on_next(3 * ts, 'b'), + mess_on_next(7 * ts, 'c'), ] assert results == expected @@ -90,10 +102,10 @@ def test_parse_marble_completed(self): " 012345678901234567890" results = parse(string) expected = [ - ReactiveTest.on_next(1, 'a'), - ReactiveTest.on_next(2, 'b'), - ReactiveTest.on_next(4, 'c'), - ReactiveTest.on_completed(7), + mess_on_next(1, 'a'), + mess_on_next(2, 'b'), + mess_on_next(4, 'c'), + mess_on_completed(7), ] assert results == expected @@ -103,10 +115,10 @@ def test_parse_marble_with_error(self): ex = Exception('ex') results = parse(string, error=ex) expected = [ - ReactiveTest.on_next(1, 'a'), - ReactiveTest.on_next(2, 'b'), - ReactiveTest.on_next(4, 'c'), - ReactiveTest.on_error(7, ex), + mess_on_next(1, 'a'), + mess_on_next(2, 'b'), + mess_on_next(4, 'c'), + mess_on_error(7, ex), ] assert results == expected @@ -115,28 +127,25 @@ def test_parse_marble_with_space(self): " 012345678901234567890" results = parse(string) expected = [ - ReactiveTest.on_next(1, 'a'), - ReactiveTest.on_next(2, 'b'), - ReactiveTest.on_next(4, 'c'), - ReactiveTest.on_completed(7), + mess_on_next(1, 'a'), + mess_on_next(2, 'b'), + mess_on_next(4, 'c'), + mess_on_completed(7), ] assert results == expected - def test_parse_marble_with_group(self): string = "-(ab)-c--|" " 012345678901234567890" results = parse(string) expected = [ - ReactiveTest.on_next(1, 'a'), - ReactiveTest.on_next(1, 'b'), - ReactiveTest.on_next(6, 'c'), - ReactiveTest.on_completed(9), + mess_on_next(1, 'a'), + mess_on_next(1, 'b'), + mess_on_next(6, 'c'), + mess_on_completed(9), ] assert results == expected - - def test_parse_marble_lookup(self): string = "-ab-c-12-3-|" " 012345678901234567890" @@ -151,27 +160,13 @@ def test_parse_marble_lookup(self): results = parse(string, lookup=lookup) expected = [ - ReactiveTest.on_next(1, 'aa'), - ReactiveTest.on_next(2, 'bb'), - ReactiveTest.on_next(4, 'cc'), - ReactiveTest.on_next(6, '11'), - ReactiveTest.on_next(7, '22'), - ReactiveTest.on_next(9, 33), - ReactiveTest.on_completed(11), - ] - assert results == expected - - - def test_parse_marble_subscribe(self): - string = "-ab--^-c-d-|" - " 8654321012345678901234567890" - results = parse(string) - expected = [ - ReactiveTest.on_next(-4, 'a'), - ReactiveTest.on_next(-3, 'b'), - ReactiveTest.on_next(2, 'c'), - ReactiveTest.on_next(4, 'd'), - ReactiveTest.on_completed(6), + mess_on_next(1, 'aa'), + mess_on_next(2, 'bb'), + mess_on_next(4, 'cc'), + mess_on_next(6, '11'), + mess_on_next(7, '22'), + mess_on_next(9, 33), + mess_on_completed(11), ] assert results == expected @@ -181,26 +176,11 @@ def test_parse_marble_time_shift(self): offset = 10 results = parse(string, time_shift=offset) expected = [ - ReactiveTest.on_next(1 + offset, 'a'), - ReactiveTest.on_next(2 + offset, 'b'), - ReactiveTest.on_next(7 + offset, 'c'), - ReactiveTest.on_next(9 + offset, 'd'), - ReactiveTest.on_completed(11 + offset), - ] - assert results == expected - - - def test_parse_marble_time_shift_and_subscribe(self): - string = "-ab--^--c-d-|" - " 654321012345678901234567890" - offset = 10 - results = parse(string, time_shift=offset) - expected = [ - ReactiveTest.on_next(-4 + offset, 'a'), - ReactiveTest.on_next(-3 + offset, 'b'), - ReactiveTest.on_next(3 + offset, 'c'), - ReactiveTest.on_next(5 + offset, 'd'), - ReactiveTest.on_completed(7 + offset), + mess_on_next(1 + offset, 'a'), + mess_on_next(2 + offset, 'b'), + mess_on_next(7 + offset, 'c'), + mess_on_next(9 + offset, 'd'), + mess_on_completed(11 + offset), ] assert results == expected @@ -400,7 +380,6 @@ def test_start_with_cold_no_create_function(self): ] assert results == expected - def test_start_with_hot_never(self): start, cold, hot, exp = marbles.test_context() obs = hot("------") @@ -422,7 +401,7 @@ def create(): return obs results = start(create) - expected = [ReactiveTest.on_completed(203),] + expected = [ReactiveTest.on_completed(203), ] assert results == expected def test_start_with_hot_normal(self): @@ -442,23 +421,6 @@ def create(): ] assert results == expected - def test_start_with_hot_subscribe(self): - start, cold, hot, exp = marbles.test_context() - obs = hot("12-^-3--4--5-|") - " 012345678901234567890" - - def create(): - return obs - - results = start(create) - expected = [ - ReactiveTest.on_next(202, 3), - ReactiveTest.on_next(205, 4), - ReactiveTest.on_next(208, 5), - ReactiveTest.on_completed(210), - ] - assert results == expected - def test_exp(self): start, cold, hot, exp = marbles.test_context() results = exp("12--3--4--5-|") @@ -477,9 +439,9 @@ def test_exp(self): def test_start_with_hot_and_exp(self): start, cold, hot, exp = marbles.test_context() - obs = hot(" 12-^-3--4--5-|") - expected = exp(" --3--4--5-|") - " 012345678901234567890" + obs = hot(" --3--4--5-|") + expected = exp("--3--4--5-|") + " 012345678901234567890" def create(): return obs From 39d9131390ca30c9aa3a6a69344a355dccdc9834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Thu, 31 Jan 2019 19:14:53 +0100 Subject: [PATCH 08/17] fix exp() function --- rx/testing/marbles.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index 86595b189..ab3094b42 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -1,11 +1,11 @@ -from typing import List, Union, Dict +from typing import List, Tuple, Union, Dict from collections import namedtuple import rx from rx.core import Observable from rx.concurrency import NewThreadScheduler from rx.core.typing import Callable -from rx.testing import TestScheduler, Recorded +from rx.testing import TestScheduler, Recorded, ReactiveTest from rx.core.observable.marbles import parse new_thread_scheduler = NewThreadScheduler() @@ -83,13 +83,15 @@ def test_expected(string: str, lookup: Dict = None, error: Exception = None) -> 'Expected function does not support subscription symbol "^".' 'Got "{}"'.format(string)) - return parse( + messages = parse( string, timespan=timespan, time_shift=subscribed, lookup=lookup, error=error, ) + return messages_to_records(messages) + def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Observable: if string.find('^') >= 0: @@ -105,14 +107,6 @@ def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Obse ) def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: -# records = parse( -# string, -# timespan=timespan, -# time_shift=subscribed, -# lookup=lookup, -# error=error, -# ) -# return scheduler.create_hot_observable(records) hot_obs = rx.hot( string, timespan=timespan, @@ -125,3 +119,24 @@ def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Obser return TestContext(test_start, test_cold, test_hot, test_expected) + +def messages_to_records(messages: List[Tuple]) -> List[Recorded]: + """ + Helper function to convert messages returned by parse() to a list of + Recorded. + """ + records = [] + + dispatcher = dict( + N=lambda t, n: ReactiveTest.on_next(t, n.value), + E=lambda t, n: ReactiveTest.on_error(t, n.exception), + C=lambda t, n: ReactiveTest.on_completed(t) + ) + + for message in messages: + time, notification = message + kind = notification.kind + record = dispatcher[kind](time, notification) + records.append(record) + + return records From 3f5224bfbb7829329e22953499dcf51ca1e045ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Thu, 31 Jan 2019 19:15:31 +0100 Subject: [PATCH 09/17] add to_marbles in rx.__init__.py --- rx/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rx/__init__.py b/rx/__init__.py index 3529bcf7f..7e48542a3 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -300,6 +300,10 @@ def hot(string, timespan: typing.RelativeTime=0.1, start_time = 0.0, scheduler: return _hot(string, timespan=timespan, start_time=start_time, lookup=lookup, error=error, scheduler=scheduler) +def to_marbles(scheduler: typing.Scheduler = None, timespan = 0.1) -> str: + from .core.observable.marbles import to_marbles as _to_marbles + return _to_marbles(scheduler=scheduler, timespan=timespan) + def generate_with_relative_time(initial_state, condition, iterate, time_mapper) -> Observable: """Generates an observable sequence by iterating a state from an From 1efbea67d6d53ffcd8a710d6adece615b9122639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Fri, 1 Feb 2019 20:09:38 +0100 Subject: [PATCH 10/17] implement the marbles syntax as discussed in #299 + update tests --- rx/core/observable/marbles.py | 134 ++++++++++++------------- tests/test_testing/test_marbles.py | 154 ++++++++++++++++++----------- 2 files changed, 156 insertions(+), 132 deletions(-) diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index 4ceea97f7..95a42c205 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -1,4 +1,5 @@ from typing import List, Dict, Tuple +import re from rx import Observable from rx.core import notification @@ -12,13 +13,30 @@ new_thread_scheduler = NewThreadScheduler() +# tokens will be search with pipe in the order below +# group of elements: match any characters surrounding by () +pattern_group = r"(\(.*?\))" +# timespan: match one or multiple hyphens +pattern_ticks = r"(-+)" +# comma err: match any comma which is not in a group +pattern_comma_error = r"(,)" +# element: match | or # or one or more characters which are not - | # ( ) , +pattern_element = r"(#|\||[^-,()#\|]+)" + +pattern = r'|'.join([ + pattern_group, + pattern_ticks, + pattern_comma_error, + pattern_element, + ]) +tokens = re.compile(pattern) + # TODO: rename start_time to duetime # TODO: use a plain impelementation instead of operators def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: - scheduler_ = scheduler or new_thread_scheduler cold_observable = from_marbles( @@ -59,6 +77,8 @@ def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, +--------+--------------------------------------------------------+ | `)` | close a group of marbles | +--------+--------------------------------------------------------+ + | `,` | separate elements in a group | + +--------+--------------------------------------------------------+ | space | used to align multiple diagrams, does not advance time.| +--------+--------------------------------------------------------+ @@ -107,7 +127,7 @@ def subscribe(observer, scheduler_): for message in messages: - # Don't make closures within a loop + # Don't make closures within a loop schedule_msg(message, observer, _scheduler) return disp return Observable(subscribe) @@ -170,9 +190,6 @@ def stringify(value): return string -# TODO: remove support of subscription symbol ^ -# TODO: consecutive characters should be considered as one element -# TODO: add support of comma , to split elements in group # TODO: complete the definition of the return type List[tuple] def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, lookup: Dict = None, error: Exception = None) -> List[Tuple]: @@ -196,6 +213,8 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativ +--------+--------------------------------------------------------+ | `)` | close a group of marbles | +--------+--------------------------------------------------------+ + | `,` | separate elements in a group | + +--------+--------------------------------------------------------+ | space | used to align multiple diagrams, does not advance time.| +--------+--------------------------------------------------------+ @@ -233,78 +252,47 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativ string = string.replace(' ', '') - isub = string.find('^') - if isub > 0: - time_shift -= isub * timespan + # try to cast a string to an int, then to a float + def try_number(element): + try: + return int(element) + except ValueError: + try: + return float(element) + except ValueError: + return element + + def map_element(time, element): + if element == '|': + return (time, notification.OnCompleted()) + elif element == '#': + return (time, notification.OnError(error)) + else: + value = try_number(element) + value = lookup.get(value, value) + return (time, notification.OnNext(value)) + iframe = 0 messages = [] - group_frame = 0 - in_group = False - has_subscribe = False - - def check_group_opening(): - if in_group: - raise ValueError( - "A group of items must be closed before opening a new one. " - 'Got "{}..."'.format(string[:char_frame+1])) - - def check_group_closing(): - if not in_group: - raise ValueError( - "A Group of items have already been closed before. " - "Got {} ...".format(string[:char_frame+1])) - - def check_subscription(): - if has_subscribe: - raise ValueError( - "Only one subscription is allowed for a hot observable. " - "Got {} ...".format(string[:char_frame+1])) - - def shift(frame): - return frame + time_shift - - for i, char in enumerate(string): - char_frame = i * timespan - - if char == '(': - check_group_opening() - in_group = True - group_frame = char_frame - - elif char == ')': - check_group_closing() - in_group = False - - elif char == '-': - pass - - elif char == '|': - frame = group_frame if in_group else char_frame - message = (shift(frame), notification.OnCompleted()) - messages.append(message) + for results in tokens.findall(string): + timestamp = iframe * timespan + time_shift + group, ticks, comma_error, element = results - elif char == '#': - frame = group_frame if in_group else char_frame - message = (shift(frame), notification.OnError(error)) - messages.append(message) - elif char == '^': - check_subscription() + if group: + elements = group[1:-1].split(',') + grp_messages = [map_element(timestamp, elm) for elm in elements if elm !=''] + messages.extend(grp_messages) + iframe += len(group) - else: - frame = group_frame if in_group else char_frame - try: - char = int(char) - except ValueError: - pass - value = lookup.get(char, char) - message = (shift(frame), notification.OnNext(value)) - messages.append(message) + if ticks: + iframe += len(ticks) - if in_group: - raise ValueError( - "The last group of items has been opened but never closed. " - "Missing a ')." - ) + if comma_error: + raise ValueError("Comma is only allowed in group of elements.") - return messages + if element: + message = map_element(timestamp, element) + messages.append(message) + iframe += len(element) + return messages diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index a65208774..594832cad 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -60,32 +60,32 @@ def mess_on_completed(time): class TestParse(unittest.TestCase): - def test_parse_on_error(self): + def test_parse_just_on_error(self): string = "#" results = parse(string) expected = [mess_on_error(0, Exception('error'))] assert results == expected - def test_parse_on_error_specified(self): + def test_parse_just_on_error_specified(self): string = "#" ex = Exception('Foo') results = parse(string, error=ex) expected = [mess_on_error(0, ex)] assert results == expected - def test_parse_on_complete(self): + def test_parse_just_on_completed(self): string = "|" results = parse(string) expected = [mess_on_completed(0)] assert results == expected - def test_parse_on_next(self): + def test_parse_just_on_next(self): string = "a" results = parse(string) expected = [mess_on_next(0, 'a')] assert results == expected - def test_parse_timespan(self): + def test_parse_marble_timespan(self): string = "a--b---c" " 012345678901234567890" ts = 0.1 @@ -97,52 +97,89 @@ def test_parse_timespan(self): ] assert results == expected + def test_parse_marble_multiple_digits(self): + string = "-ab-cde--" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 'ab'), + mess_on_next(4, 'cde'), + ] + assert results == expected + + def test_parse_marble_multiple_digits_int(self): + string = "-1-22-333-" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 1), + mess_on_next(3, 22), + mess_on_next(6, 333), + ] + assert results == expected + + def test_parse_marble_multiple_digits_float(self): + string = "-1.0--2.345--6.7e8-" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, float('1.0')), + mess_on_next(6, float('2.345')), + mess_on_next(13, float('6.7e8')), + ] + assert results == expected + def test_parse_marble_completed(self): string = "-ab-c--|" " 012345678901234567890" results = parse(string) expected = [ - mess_on_next(1, 'a'), - mess_on_next(2, 'b'), + mess_on_next(1, 'ab'), mess_on_next(4, 'c'), mess_on_completed(7), ] assert results == expected def test_parse_marble_with_error(self): - string = "-ab-c--#--" + string = "-a-b-c--#--" " 012345678901234567890" ex = Exception('ex') results = parse(string, error=ex) expected = [ mess_on_next(1, 'a'), - mess_on_next(2, 'b'), - mess_on_next(4, 'c'), - mess_on_error(7, ex), + mess_on_next(3, 'b'), + mess_on_next(5, 'c'), + mess_on_error(8, ex), ] assert results == expected def test_parse_marble_with_space(self): - string = " -a b- c- - |" - " 012345678901234567890" + string = " -a b- c- de |" + " 01 23 45 67 8901234567890" results = parse(string) expected = [ - mess_on_next(1, 'a'), - mess_on_next(2, 'b'), + mess_on_next(1, 'ab'), mess_on_next(4, 'c'), - mess_on_completed(7), + mess_on_next(6, 'de'), + mess_on_completed(8), ] assert results == expected def test_parse_marble_with_group(self): - string = "-(ab)-c--|" - " 012345678901234567890" + string = "-x(ab,12,1.5)-c--(de)-|" + " 012345678901234567890123" + " 0 1 2 " results = parse(string) expected = [ - mess_on_next(1, 'a'), - mess_on_next(1, 'b'), - mess_on_next(6, 'c'), - mess_on_completed(9), + mess_on_next(1, 'x'), + mess_on_next(2, 'ab'), + mess_on_next(2, 12), + mess_on_next(2, float('1.5')), + + mess_on_next(14, 'c'), + mess_on_next(17, 'de'), + + mess_on_completed(22), ] assert results == expected @@ -150,21 +187,17 @@ def test_parse_marble_lookup(self): string = "-ab-c-12-3-|" " 012345678901234567890" lookup = { - 'a': 'aa', - 'b': 'bb', + 'ab': 'aabb', 'c': 'cc', - 1: '11', - 2: '22', + 12: '1122', 3: 33, } results = parse(string, lookup=lookup) expected = [ - mess_on_next(1, 'aa'), - mess_on_next(2, 'bb'), + mess_on_next(1, 'aabb'), mess_on_next(4, 'cc'), - mess_on_next(6, '11'), - mess_on_next(7, '22'), + mess_on_next(6, '1122'), mess_on_next(9, 33), mess_on_completed(11), ] @@ -176,8 +209,7 @@ def test_parse_marble_time_shift(self): offset = 10 results = parse(string, time_shift=offset) expected = [ - mess_on_next(1 + offset, 'a'), - mess_on_next(2 + offset, 'b'), + mess_on_next(1 + offset, 'ab'), mess_on_next(7 + offset, 'c'), mess_on_next(9 + offset, 'd'), mess_on_completed(11 + offset), @@ -247,8 +279,7 @@ def test_from_marbles_marble_completed(self): scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(200.1, 'a'), - ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.1, 'ab'), ReactiveTest.on_next(200.4, 'c'), ReactiveTest.on_completed(200.7), ] @@ -262,8 +293,7 @@ def test_from_marbles_marble_with_error(self): scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(200.1, 'a'), - ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.1, 'ab'), ReactiveTest.on_next(200.4, 'c'), ReactiveTest.on_error(200.7, ex), ] @@ -271,29 +301,30 @@ def test_from_marbles_marble_with_error(self): def test_from_marbles_marble_with_space(self): string = " -a b- c- - |" - " 012 34 56 7 8901234567890" + " 01 23 45 6 78901234567890" obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(200.1, 'a'), - ReactiveTest.on_next(200.2, 'b'), + ReactiveTest.on_next(200.1, 'ab'), ReactiveTest.on_next(200.4, 'c'), ReactiveTest.on_completed(200.7), ] assert results == expected def test_from_marbles_marble_with_group(self): - string = "-(ab)-c--|" + string = "-(ab)-c-(12.5,def)--(6,|)" " 012345678901234567890" obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(200.1, 'a'), - ReactiveTest.on_next(200.1, 'b'), + ReactiveTest.on_next(200.1, 'ab'), ReactiveTest.on_next(200.6, 'c'), - ReactiveTest.on_completed(200.9), + ReactiveTest.on_next(200.8, str(12.5)), + ReactiveTest.on_next(200.8, 'def'), + ReactiveTest.on_next(202.0, 6), + ReactiveTest.on_completed(202.0), ] assert results == expected @@ -301,22 +332,18 @@ def test_from_marbles_marble_lookup(self): string = "-ab-c-12-3-|" " 012345678901234567890" lookup = { - 'a': 'aa', - 'b': 'bb', + 'ab': 'aabb', 'c': 'cc', - 1: '11', - 2: '22', + 12: '1122', 3: 33, } obs = rx.from_marbles(string, lookup=lookup) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(200.1, 'aa'), - ReactiveTest.on_next(200.2, 'bb'), + ReactiveTest.on_next(200.1, 'aabb'), ReactiveTest.on_next(200.4, 'cc'), - ReactiveTest.on_next(200.6, '11'), - ReactiveTest.on_next(200.7, '22'), + ReactiveTest.on_next(200.6, '1122'), ReactiveTest.on_next(200.9, 33), ReactiveTest.on_completed(201.1), ] @@ -359,8 +386,7 @@ def create(): results = start(create) expected = [ - ReactiveTest.on_next(200, 1), - ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(200, 12), ReactiveTest.on_next(204, 3), ReactiveTest.on_completed(206), ] @@ -373,8 +399,7 @@ def test_start_with_cold_no_create_function(self): results = start(obs) expected = [ - ReactiveTest.on_next(200, 1), - ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(200, 12), ReactiveTest.on_next(204, 3), ReactiveTest.on_completed(206), ] @@ -414,8 +439,7 @@ def create(): results = start(create) expected = [ - ReactiveTest.on_next(201, 1), - ReactiveTest.on_next(202, 2), + ReactiveTest.on_next(201, 12), ReactiveTest.on_next(205, 3), ReactiveTest.on_completed(207), ] @@ -427,8 +451,7 @@ def test_exp(self): " 012345678901234567890" expected = [ - ReactiveTest.on_next(200, 1), - ReactiveTest.on_next(201, 2), + ReactiveTest.on_next(200, 12), ReactiveTest.on_next(204, 3), ReactiveTest.on_next(207, 4), ReactiveTest.on_next(210, 5), @@ -461,3 +484,16 @@ def create(): results = start(create) assert results == expected + + def test_start_with_cold_and_exp_group(self): + + start, cold, hot, exp = marbles.test_context() + obs = cold(" 12--(3,6.5)----(5,#)-e-|") + expected = exp(" 12--(3,6.5)----(5,#) ") + " 012345678901234567890" + + def create(): + return obs + + results = start(create) + assert results == expected From bc5cef08dbb98545676c90bc610dee3a1cdf0f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Fri, 1 Feb 2019 21:44:29 +0100 Subject: [PATCH 11/17] add tests for rx.hot --- tests/test_testing/test_marbles.py | 134 +++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 594832cad..3b4ef5305 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -350,6 +350,140 @@ def test_from_marbles_marble_lookup(self): assert results == expected +class TestHot(unittest.TestCase): + def create_factory(self, observable): + def create(): + return observable + return create + + def test_hot_on_error(self): + string = "#" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, Exception('error'))] + assert results == expected + + def test_hot_on_error_specified(self): + string = "#" + ex = Exception('Foo') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, ex)] + assert results == expected + + def test_hot_on_complete(self): + string = "|" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_completed(200.0)] + assert results == expected + + def test_hot_on_next(self): + string = "a" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_next(200.0, 'a')] + assert results == expected + + def test_hot_timespan(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.5 + scheduler = TestScheduler() + obs = rx.hot(string, ts, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(0 * ts + 200.0, 'a'), + ReactiveTest.on_next(3 * ts + 200.0, 'b'), + ReactiveTest.on_next(7 * ts + 200.0, 'c'), + ] + assert results == expected + + def test_hot_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_hot_marble_with_error(self): + string = "-ab-c--#--" + " 012345678901234567890" + ex = Exception('ex') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_hot_marble_with_space(self): + string = " -a b- c- - |" + " 01 23 45 6 78901234567890" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_hot_marble_with_group(self): + string = "-(ab)-c-(12.5,def)--(6,|)" + " 01234567890123456789012345" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.6, 'c'), + ReactiveTest.on_next(200.8, str(12.5)), + ReactiveTest.on_next(200.8, 'def'), + ReactiveTest.on_next(202.0, 6), + ReactiveTest.on_completed(202.0), + ] + assert results == expected + + def test_hot_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'ab': 'aabb', + 'c': 'cc', + 12: '1122', + 3: 33, + } + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, lookup=lookup, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'aabb'), + ReactiveTest.on_next(200.4, 'cc'), + ReactiveTest.on_next(200.6, '1122'), + ReactiveTest.on_next(200.9, 33), + ReactiveTest.on_completed(201.1), + ] + assert results == expected + + + class TestTestContext(unittest.TestCase): def test_start_with_cold_never(self): From 3c6c97c145750b15a656f82ae0f74f2c598501e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Fri, 1 Feb 2019 23:38:17 +0100 Subject: [PATCH 12/17] implement hot with no operators --- rx/__init__.py | 20 ++++++++--- rx/core/observable/marbles.py | 57 +++++++++++++++++++++++------- rx/testing/marbles.py | 2 +- tests/test_testing/test_marbles.py | 54 +++++++++++++++++++++++----- 4 files changed, 107 insertions(+), 26 deletions(-) diff --git a/rx/__init__.py b/rx/__init__.py index 7e48542a3..034985583 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -287,17 +287,29 @@ def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typ """ from .core.observable.marbles import from_marbles as _from_marbles - return _from_marbles(string, timespan=timespan, lookup=lookup, error=error, scheduler=scheduler) + return _from_marbles( + string, + timespan=timespan, + lookup=lookup, + error=error, + scheduler=scheduler) + cold = from_marbles # TODO: need to move hot() operator (not in alphabetic order) # TODO: write the doc -def hot(string, timespan: typing.RelativeTime=0.1, start_time = 0.0, scheduler: typing.Scheduler = None, - lookup = None, error: Exception = None) -> Observable: +def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRelativeTime = 0.0, + scheduler: typing.Scheduler = None, lookup = None, error: Exception = None) -> Observable: from .core.observable.marbles import hot as _hot - return _hot(string, timespan=timespan, start_time=start_time, lookup=lookup, error=error, scheduler=scheduler) + return _hot( + string, + timespan=timespan, + duetime=duetime, + lookup=lookup, + error=error, + scheduler=scheduler) def to_marbles(scheduler: typing.Scheduler = None, timespan = 0.1) -> str: diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index 95a42c205..a5348937f 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -1,8 +1,11 @@ from typing import List, Dict, Tuple import re +import threading +from datetime import datetime, timedelta from rx import Observable from rx.core import notification +from rx.disposable import Disposable from rx.disposable import CompositeDisposable from rx.concurrency import NewThreadScheduler from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler @@ -32,27 +35,57 @@ tokens = re.compile(pattern) -# TODO: rename start_time to duetime # TODO: use a plain impelementation instead of operators -def hot(string: str, timespan: RelativeTime = 0.1, start_time:AbsoluteOrRelativeTime=0, +def hot(string: str, timespan: RelativeTime = 0.1, duetime:AbsoluteOrRelativeTime=0, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: - scheduler_ = scheduler or new_thread_scheduler + _scheduler = scheduler or new_thread_scheduler - cold_observable = from_marbles( + messages = parse( string, + time_shift=duetime, timespan=timespan, lookup=lookup, error=error, - scheduler=scheduler_, - ) - values = rx.timer(start_time, scheduler=scheduler_).pipe( - ops.flat_map(lambda _: cold_observable), - ops.publish(), ) - values.connect() - return values + lock = threading.RLock() + is_completed = False + observers = [] + + def subscribe(observer, scheduler=None): + if not is_completed: + with lock: + observers.append(observer) + # should a hot observable already completed or on error + # re-push on_completed/on_error at subscription time? + + def dispose(): + with lock: + try: + observers.remove(observer) + except ValueError: + pass + + return Disposable(dispose) + + def create_action(notification): + def action(scheduler, state=None): + with lock: + for observer in observers: + notification.accept(observer) + return action + + for message in messages: + timespan, notification = message + action = create_action(notification) + + # Don't make closures within a loop + _scheduler.schedule_relative(timespan, action) + + return Observable(subscribe) + + def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, @@ -191,7 +224,7 @@ def stringify(value): return string # TODO: complete the definition of the return type List[tuple] -def parse(string: str, timespan: RelativeTime = 1, time_shift: AbsoluteOrRelativeTime = 0, +def parse(string: str, timespan: RelativeTime = 1, time_shift: RelativeTime = 0, lookup: Dict = None, error: Exception = None) -> List[Tuple]: """Convert a marble diagram string to a list of records of type :class:`rx.testing.recorded.Recorded`. diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index ab3094b42..e0729551b 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -110,7 +110,7 @@ def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Obser hot_obs = rx.hot( string, timespan=timespan, - start_time=subscribed, + duetime=subscribed, lookup=lookup, error=error, scheduler=scheduler, diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 3b4ef5305..d4bd92c6d 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -299,6 +299,20 @@ def test_from_marbles_marble_with_error(self): ] assert results == expected + def test_from_marbles_marble_with_consecutive_symbols(self): + string = "-ab(12)#--" + " 012345678901234567890" + ex = Exception('ex') + obs = rx.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.3, 12), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + def test_from_marbles_marble_with_space(self): string = " -a b- c- - |" " 01 23 45 6 78901234567890" @@ -359,47 +373,55 @@ def create(): def test_hot_on_error(self): string = "#" scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_error(200, Exception('error'))] + expected = [ReactiveTest.on_error(200.1, Exception('error'))] assert results == expected def test_hot_on_error_specified(self): string = "#" ex = Exception('Foo') scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + obs = rx.hot(string, 0.1, 200.1, error=ex, scheduler=scheduler) results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_error(200, ex)] + expected = [ReactiveTest.on_error(200.1, ex)] assert results == expected def test_hot_on_complete(self): string = "|" scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_completed(200.0)] + expected = [ReactiveTest.on_completed(200.1)] assert results == expected def test_hot_on_next(self): + string = "a" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_next(200.1, 'a')] + assert results == expected + + def test_hot_skipped_at_200(self): string = "a" scheduler = TestScheduler() obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_next(200.0, 'a')] + expected = [] assert results == expected def test_hot_timespan(self): - string = "a--b---c" + string = "-a-b---c" " 012345678901234567890" ts = 0.5 scheduler = TestScheduler() obs = rx.hot(string, ts, 200.0, scheduler=scheduler) results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(0 * ts + 200.0, 'a'), + ReactiveTest.on_next(1 * ts + 200.0, 'a'), ReactiveTest.on_next(3 * ts + 200.0, 'b'), ReactiveTest.on_next(7 * ts + 200.0, 'c'), ] @@ -432,6 +454,20 @@ def test_hot_marble_with_error(self): ] assert results == expected + def test_from_marbles_marble_with_consecutive_symbols(self): + string = "-ab(12)#--" + " 012345678901234567890" + ex = Exception('ex') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.3, 12), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + def test_hot_marble_with_space(self): string = " -a b- c- - |" " 01 23 45 6 78901234567890" From aaaeeb11ecd4039ae2ac361f11dd5a3ee6423121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Fri, 1 Feb 2019 23:47:30 +0100 Subject: [PATCH 13/17] move tests relative to from_marbles()/cold() and hot() to tests/test_observable/test_marbles.py --- tests/test_observable/test_marbles.py | 481 ++++++++++++++++++++++++++ tests/test_testing/test_marbles.py | 479 +------------------------ 2 files changed, 482 insertions(+), 478 deletions(-) create mode 100644 tests/test_observable/test_marbles.py diff --git a/tests/test_observable/test_marbles.py b/tests/test_observable/test_marbles.py new file mode 100644 index 000000000..cae05fd26 --- /dev/null +++ b/tests/test_observable/test_marbles.py @@ -0,0 +1,481 @@ +import unittest + +import rx +from rx.core import notification +from rx.core.observable.marbles import parse +from rx.testing import TestScheduler +from rx.testing.reactivetest import ReactiveTest + + +def mess_on_next(time, value): + return (time, notification.OnNext(value)) + + +def mess_on_error(time, error): + return (time, notification.OnError(error)) + + +def mess_on_completed(time): + return (time, notification.OnCompleted()) + + +class TestParse(unittest.TestCase): + + def test_parse_just_on_error(self): + string = "#" + results = parse(string) + expected = [mess_on_error(0, Exception('error'))] + assert results == expected + + def test_parse_just_on_error_specified(self): + string = "#" + ex = Exception('Foo') + results = parse(string, error=ex) + expected = [mess_on_error(0, ex)] + assert results == expected + + def test_parse_just_on_completed(self): + string = "|" + results = parse(string) + expected = [mess_on_completed(0)] + assert results == expected + + def test_parse_just_on_next(self): + string = "a" + results = parse(string) + expected = [mess_on_next(0, 'a')] + assert results == expected + + def test_parse_marble_timespan(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.1 + results = parse(string, timespan=ts) + expected = [ + mess_on_next(0 * ts, 'a'), + mess_on_next(3 * ts, 'b'), + mess_on_next(7 * ts, 'c'), + ] + assert results == expected + + def test_parse_marble_multiple_digits(self): + string = "-ab-cde--" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 'ab'), + mess_on_next(4, 'cde'), + ] + assert results == expected + + def test_parse_marble_multiple_digits_int(self): + string = "-1-22-333-" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 1), + mess_on_next(3, 22), + mess_on_next(6, 333), + ] + assert results == expected + + def test_parse_marble_multiple_digits_float(self): + string = "-1.0--2.345--6.7e8-" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, float('1.0')), + mess_on_next(6, float('2.345')), + mess_on_next(13, float('6.7e8')), + ] + assert results == expected + + def test_parse_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 'ab'), + mess_on_next(4, 'c'), + mess_on_completed(7), + ] + assert results == expected + + def test_parse_marble_with_error(self): + string = "-a-b-c--#--" + " 012345678901234567890" + ex = Exception('ex') + results = parse(string, error=ex) + expected = [ + mess_on_next(1, 'a'), + mess_on_next(3, 'b'), + mess_on_next(5, 'c'), + mess_on_error(8, ex), + ] + assert results == expected + + def test_parse_marble_with_space(self): + string = " -a b- c- de |" + " 01 23 45 67 8901234567890" + results = parse(string) + expected = [ + mess_on_next(1, 'ab'), + mess_on_next(4, 'c'), + mess_on_next(6, 'de'), + mess_on_completed(8), + ] + assert results == expected + + def test_parse_marble_with_group(self): + string = "-x(ab,12,1.5)-c--(de)-|" + " 012345678901234567890123" + " 0 1 2 " + results = parse(string) + expected = [ + mess_on_next(1, 'x'), + mess_on_next(2, 'ab'), + mess_on_next(2, 12), + mess_on_next(2, float('1.5')), + + mess_on_next(14, 'c'), + mess_on_next(17, 'de'), + + mess_on_completed(22), + ] + assert results == expected + + def test_parse_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'ab': 'aabb', + 'c': 'cc', + 12: '1122', + 3: 33, + } + + results = parse(string, lookup=lookup) + expected = [ + mess_on_next(1, 'aabb'), + mess_on_next(4, 'cc'), + mess_on_next(6, '1122'), + mess_on_next(9, 33), + mess_on_completed(11), + ] + assert results == expected + + def test_parse_marble_time_shift(self): + string = "-ab----c-d-|" + " 012345678901234567890" + offset = 10 + results = parse(string, time_shift=offset) + expected = [ + mess_on_next(1 + offset, 'ab'), + mess_on_next(7 + offset, 'c'), + mess_on_next(9 + offset, 'd'), + mess_on_completed(11 + offset), + ] + assert results == expected + + +class TestFromMarble(unittest.TestCase): + def create_factory(self, observable): + def create(): + return observable + return create + + def test_from_marbles_on_error(self): + string = "#" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, Exception('error'))] + assert results == expected + + def test_from_marbles_on_error_specified(self): + string = "#" + ex = Exception('Foo') + obs = rx.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200, ex)] + assert results == expected + + def test_from_marbles_on_complete(self): + string = "|" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_completed(200)] + assert results == expected + + def test_from_marbles_on_next(self): + string = "a" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_next(200, 'a')] + assert results == expected + + def test_from_marbles_timespan(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.5 + obs = rx.from_marbles(string, timespan=ts) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(0 * ts + 200, 'a'), + ReactiveTest.on_next(3 * ts + 200, 'b'), + ReactiveTest.on_next(7 * ts + 200, 'c'), + ] + assert results == expected + + def test_from_marbles_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_from_marbles_marble_with_error(self): + string = "-ab-c--#--" + " 012345678901234567890" + ex = Exception('ex') + obs = rx.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_from_marbles_marble_with_consecutive_symbols(self): + string = "-ab(12)#--" + " 012345678901234567890" + ex = Exception('ex') + obs = rx.from_marbles(string, error=ex) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.3, 12), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_from_marbles_marble_with_space(self): + string = " -a b- c- - |" + " 01 23 45 6 78901234567890" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_from_marbles_marble_with_group(self): + string = "-(ab)-c-(12.5,def)--(6,|)" + " 012345678901234567890" + obs = rx.from_marbles(string) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.6, 'c'), + ReactiveTest.on_next(200.8, str(12.5)), + ReactiveTest.on_next(200.8, 'def'), + ReactiveTest.on_next(202.0, 6), + ReactiveTest.on_completed(202.0), + ] + assert results == expected + + def test_from_marbles_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'ab': 'aabb', + 'c': 'cc', + 12: '1122', + 3: 33, + } + obs = rx.from_marbles(string, lookup=lookup) + scheduler = TestScheduler() + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'aabb'), + ReactiveTest.on_next(200.4, 'cc'), + ReactiveTest.on_next(200.6, '1122'), + ReactiveTest.on_next(200.9, 33), + ReactiveTest.on_completed(201.1), + ] + assert results == expected + + +class TestHot(unittest.TestCase): + def create_factory(self, observable): + def create(): + return observable + return create + + def test_hot_on_error(self): + string = "#" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200.1, Exception('error'))] + assert results == expected + + def test_hot_on_error_specified(self): + string = "#" + ex = Exception('Foo') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.1, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + + expected = [ReactiveTest.on_error(200.1, ex)] + assert results == expected + + def test_hot_on_complete(self): + string = "|" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_completed(200.1)] + assert results == expected + + def test_hot_on_next(self): + string = "a" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ReactiveTest.on_next(200.1, 'a')] + assert results == expected + + def test_hot_skipped_at_200(self): + string = "a" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [] + assert results == expected + + def test_hot_timespan(self): + string = "-a-b---c" + " 012345678901234567890" + ts = 0.5 + scheduler = TestScheduler() + obs = rx.hot(string, ts, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(1 * ts + 200.0, 'a'), + ReactiveTest.on_next(3 * ts + 200.0, 'b'), + ReactiveTest.on_next(7 * ts + 200.0, 'c'), + ] + assert results == expected + + def test_hot_marble_completed(self): + string = "-ab-c--|" + " 012345678901234567890" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_hot_marble_with_error(self): + string = "-ab-c--#--" + " 012345678901234567890" + ex = Exception('ex') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_from_marbles_marble_with_consecutive_symbols(self): + string = "-ab(12)#--" + " 012345678901234567890" + ex = Exception('ex') + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.3, 12), + ReactiveTest.on_error(200.7, ex), + ] + assert results == expected + + def test_hot_marble_with_space(self): + string = " -a b- c- - |" + " 01 23 45 6 78901234567890" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.4, 'c'), + ReactiveTest.on_completed(200.7), + ] + assert results == expected + + def test_hot_marble_with_group(self): + string = "-(ab)-c-(12.5,def)--(6,|)" + " 01234567890123456789012345" + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'ab'), + ReactiveTest.on_next(200.6, 'c'), + ReactiveTest.on_next(200.8, str(12.5)), + ReactiveTest.on_next(200.8, 'def'), + ReactiveTest.on_next(202.0, 6), + ReactiveTest.on_completed(202.0), + ] + assert results == expected + + def test_hot_marble_lookup(self): + string = "-ab-c-12-3-|" + " 012345678901234567890" + lookup = { + 'ab': 'aabb', + 'c': 'cc', + 12: '1122', + 3: 33, + } + scheduler = TestScheduler() + obs = rx.hot(string, 0.1, 200.0, lookup=lookup, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(200.1, 'aabb'), + ReactiveTest.on_next(200.4, 'cc'), + ReactiveTest.on_next(200.6, '1122'), + ReactiveTest.on_next(200.9, 33), + ReactiveTest.on_completed(201.1), + ] + assert results == expected + diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index d4bd92c6d..c8ace224c 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -1,11 +1,7 @@ import unittest -import rx -from rx.core.observable.marbles import parse -from rx import Observable -from rx.testing import TestScheduler, marbles +from rx.testing import marbles from rx.testing.reactivetest import ReactiveTest -from rx.core import notification #from rx.concurrency import timeout_scheduler, new_thread_scheduler @@ -46,479 +42,6 @@ # expected = [t.replace('-', '') for t in tested_marbles] # self._run_test(expected, new_thread_scheduler, TestScheduler()) -def mess_on_next(time, value): - return (time, notification.OnNext(value)) - - -def mess_on_error(time, error): - return (time, notification.OnError(error)) - - -def mess_on_completed(time): - return (time, notification.OnCompleted()) - - -class TestParse(unittest.TestCase): - - def test_parse_just_on_error(self): - string = "#" - results = parse(string) - expected = [mess_on_error(0, Exception('error'))] - assert results == expected - - def test_parse_just_on_error_specified(self): - string = "#" - ex = Exception('Foo') - results = parse(string, error=ex) - expected = [mess_on_error(0, ex)] - assert results == expected - - def test_parse_just_on_completed(self): - string = "|" - results = parse(string) - expected = [mess_on_completed(0)] - assert results == expected - - def test_parse_just_on_next(self): - string = "a" - results = parse(string) - expected = [mess_on_next(0, 'a')] - assert results == expected - - def test_parse_marble_timespan(self): - string = "a--b---c" - " 012345678901234567890" - ts = 0.1 - results = parse(string, timespan=ts) - expected = [ - mess_on_next(0 * ts, 'a'), - mess_on_next(3 * ts, 'b'), - mess_on_next(7 * ts, 'c'), - ] - assert results == expected - - def test_parse_marble_multiple_digits(self): - string = "-ab-cde--" - " 012345678901234567890" - results = parse(string) - expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'cde'), - ] - assert results == expected - - def test_parse_marble_multiple_digits_int(self): - string = "-1-22-333-" - " 012345678901234567890" - results = parse(string) - expected = [ - mess_on_next(1, 1), - mess_on_next(3, 22), - mess_on_next(6, 333), - ] - assert results == expected - - def test_parse_marble_multiple_digits_float(self): - string = "-1.0--2.345--6.7e8-" - " 012345678901234567890" - results = parse(string) - expected = [ - mess_on_next(1, float('1.0')), - mess_on_next(6, float('2.345')), - mess_on_next(13, float('6.7e8')), - ] - assert results == expected - - def test_parse_marble_completed(self): - string = "-ab-c--|" - " 012345678901234567890" - results = parse(string) - expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'c'), - mess_on_completed(7), - ] - assert results == expected - - def test_parse_marble_with_error(self): - string = "-a-b-c--#--" - " 012345678901234567890" - ex = Exception('ex') - results = parse(string, error=ex) - expected = [ - mess_on_next(1, 'a'), - mess_on_next(3, 'b'), - mess_on_next(5, 'c'), - mess_on_error(8, ex), - ] - assert results == expected - - def test_parse_marble_with_space(self): - string = " -a b- c- de |" - " 01 23 45 67 8901234567890" - results = parse(string) - expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'c'), - mess_on_next(6, 'de'), - mess_on_completed(8), - ] - assert results == expected - - def test_parse_marble_with_group(self): - string = "-x(ab,12,1.5)-c--(de)-|" - " 012345678901234567890123" - " 0 1 2 " - results = parse(string) - expected = [ - mess_on_next(1, 'x'), - mess_on_next(2, 'ab'), - mess_on_next(2, 12), - mess_on_next(2, float('1.5')), - - mess_on_next(14, 'c'), - mess_on_next(17, 'de'), - - mess_on_completed(22), - ] - assert results == expected - - def test_parse_marble_lookup(self): - string = "-ab-c-12-3-|" - " 012345678901234567890" - lookup = { - 'ab': 'aabb', - 'c': 'cc', - 12: '1122', - 3: 33, - } - - results = parse(string, lookup=lookup) - expected = [ - mess_on_next(1, 'aabb'), - mess_on_next(4, 'cc'), - mess_on_next(6, '1122'), - mess_on_next(9, 33), - mess_on_completed(11), - ] - assert results == expected - - def test_parse_marble_time_shift(self): - string = "-ab----c-d-|" - " 012345678901234567890" - offset = 10 - results = parse(string, time_shift=offset) - expected = [ - mess_on_next(1 + offset, 'ab'), - mess_on_next(7 + offset, 'c'), - mess_on_next(9 + offset, 'd'), - mess_on_completed(11 + offset), - ] - assert results == expected - - -class TestFromMarble(unittest.TestCase): - def create_factory(self, observable): - def create(): - return observable - return create - - def test_from_marbles_on_error(self): - string = "#" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - - expected = [ReactiveTest.on_error(200, Exception('error'))] - assert results == expected - - def test_from_marbles_on_error_specified(self): - string = "#" - ex = Exception('Foo') - obs = rx.from_marbles(string, error=ex) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - - expected = [ReactiveTest.on_error(200, ex)] - assert results == expected - - def test_from_marbles_on_complete(self): - string = "|" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_completed(200)] - assert results == expected - - def test_from_marbles_on_next(self): - string = "a" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_next(200, 'a')] - assert results == expected - - def test_from_marbles_timespan(self): - string = "a--b---c" - " 012345678901234567890" - ts = 0.5 - obs = rx.from_marbles(string, timespan=ts) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(0 * ts + 200, 'a'), - ReactiveTest.on_next(3 * ts + 200, 'b'), - ReactiveTest.on_next(7 * ts + 200, 'c'), - ] - assert results == expected - - def test_from_marbles_marble_completed(self): - string = "-ab-c--|" - " 012345678901234567890" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_completed(200.7), - ] - assert results == expected - - def test_from_marbles_marble_with_error(self): - string = "-ab-c--#--" - " 012345678901234567890" - ex = Exception('ex') - obs = rx.from_marbles(string, error=ex) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_error(200.7, ex), - ] - assert results == expected - - def test_from_marbles_marble_with_consecutive_symbols(self): - string = "-ab(12)#--" - " 012345678901234567890" - ex = Exception('ex') - obs = rx.from_marbles(string, error=ex) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.3, 12), - ReactiveTest.on_error(200.7, ex), - ] - assert results == expected - - def test_from_marbles_marble_with_space(self): - string = " -a b- c- - |" - " 01 23 45 6 78901234567890" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_completed(200.7), - ] - assert results == expected - - def test_from_marbles_marble_with_group(self): - string = "-(ab)-c-(12.5,def)--(6,|)" - " 012345678901234567890" - obs = rx.from_marbles(string) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.6, 'c'), - ReactiveTest.on_next(200.8, str(12.5)), - ReactiveTest.on_next(200.8, 'def'), - ReactiveTest.on_next(202.0, 6), - ReactiveTest.on_completed(202.0), - ] - assert results == expected - - def test_from_marbles_marble_lookup(self): - string = "-ab-c-12-3-|" - " 012345678901234567890" - lookup = { - 'ab': 'aabb', - 'c': 'cc', - 12: '1122', - 3: 33, - } - obs = rx.from_marbles(string, lookup=lookup) - scheduler = TestScheduler() - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'aabb'), - ReactiveTest.on_next(200.4, 'cc'), - ReactiveTest.on_next(200.6, '1122'), - ReactiveTest.on_next(200.9, 33), - ReactiveTest.on_completed(201.1), - ] - assert results == expected - - -class TestHot(unittest.TestCase): - def create_factory(self, observable): - def create(): - return observable - return create - - def test_hot_on_error(self): - string = "#" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - - expected = [ReactiveTest.on_error(200.1, Exception('error'))] - assert results == expected - - def test_hot_on_error_specified(self): - string = "#" - ex = Exception('Foo') - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.1, error=ex, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - - expected = [ReactiveTest.on_error(200.1, ex)] - assert results == expected - - def test_hot_on_complete(self): - string = "|" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_completed(200.1)] - assert results == expected - - def test_hot_on_next(self): - string = "a" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.1, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_next(200.1, 'a')] - assert results == expected - - def test_hot_skipped_at_200(self): - string = "a" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [] - assert results == expected - - def test_hot_timespan(self): - string = "-a-b---c" - " 012345678901234567890" - ts = 0.5 - scheduler = TestScheduler() - obs = rx.hot(string, ts, 200.0, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(1 * ts + 200.0, 'a'), - ReactiveTest.on_next(3 * ts + 200.0, 'b'), - ReactiveTest.on_next(7 * ts + 200.0, 'c'), - ] - assert results == expected - - def test_hot_marble_completed(self): - string = "-ab-c--|" - " 012345678901234567890" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_completed(200.7), - ] - assert results == expected - - def test_hot_marble_with_error(self): - string = "-ab-c--#--" - " 012345678901234567890" - ex = Exception('ex') - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_error(200.7, ex), - ] - assert results == expected - - def test_from_marbles_marble_with_consecutive_symbols(self): - string = "-ab(12)#--" - " 012345678901234567890" - ex = Exception('ex') - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, error=ex, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.3, 12), - ReactiveTest.on_error(200.7, ex), - ] - assert results == expected - - def test_hot_marble_with_space(self): - string = " -a b- c- - |" - " 01 23 45 6 78901234567890" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.4, 'c'), - ReactiveTest.on_completed(200.7), - ] - assert results == expected - - def test_hot_marble_with_group(self): - string = "-(ab)-c-(12.5,def)--(6,|)" - " 01234567890123456789012345" - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'ab'), - ReactiveTest.on_next(200.6, 'c'), - ReactiveTest.on_next(200.8, str(12.5)), - ReactiveTest.on_next(200.8, 'def'), - ReactiveTest.on_next(202.0, 6), - ReactiveTest.on_completed(202.0), - ] - assert results == expected - - def test_hot_marble_lookup(self): - string = "-ab-c-12-3-|" - " 012345678901234567890" - lookup = { - 'ab': 'aabb', - 'c': 'cc', - 12: '1122', - 3: 33, - } - scheduler = TestScheduler() - obs = rx.hot(string, 0.1, 200.0, lookup=lookup, scheduler=scheduler) - results = scheduler.start(self.create_factory(obs)).messages - expected = [ - ReactiveTest.on_next(200.1, 'aabb'), - ReactiveTest.on_next(200.4, 'cc'), - ReactiveTest.on_next(200.6, '1122'), - ReactiveTest.on_next(200.9, 33), - ReactiveTest.on_completed(201.1), - ] - assert results == expected - - class TestTestContext(unittest.TestCase): From b774bfd3f7d7ff3a1bc41ec4c6a78d42a27c5bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Sat, 2 Feb 2019 18:36:21 +0100 Subject: [PATCH 14/17] fix elements should be skipped after on_completed or on_error --- rx/core/observable/marbles.py | 18 ++++++++++++++++- tests/test_observable/test_marbles.py | 28 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index a5348937f..cf81473fa 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -51,10 +51,11 @@ def hot(string: str, timespan: RelativeTime = 0.1, duetime:AbsoluteOrRelativeTim lock = threading.RLock() is_completed = False + is_on_error = False observers = [] def subscribe(observer, scheduler=None): - if not is_completed: + if not is_completed and not is_on_error: with lock: observers.append(observer) # should a hot observable already completed or on error @@ -71,9 +72,18 @@ def dispose(): def create_action(notification): def action(scheduler, state=None): + nonlocal is_completed + nonlocal is_on_error + with lock: for observer in observers: notification.accept(observer) + + if notification.kind == 'C': + is_completed = True + elif notification.kind == 'E': + is_on_error = True + return action for message in messages: @@ -315,6 +325,9 @@ def map_element(time, element): elements = group[1:-1].split(',') grp_messages = [map_element(timestamp, elm) for elm in elements if elm !=''] messages.extend(grp_messages) + kinds = [m[1].kind for m in grp_messages] + if 'E' in kinds or 'C' in kinds: + break iframe += len(group) if ticks: @@ -326,6 +339,9 @@ def map_element(time, element): if element: message = map_element(timestamp, element) messages.append(message) + kind = message[1].kind + if kind == 'E' or kind == 'C': + break iframe += len(element) return messages diff --git a/tests/test_observable/test_marbles.py b/tests/test_observable/test_marbles.py index cae05fd26..980fe4de2 100644 --- a/tests/test_observable/test_marbles.py +++ b/tests/test_observable/test_marbles.py @@ -114,6 +114,32 @@ def test_parse_marble_with_error(self): ] assert results == expected + def test_parse_marble_with_error_skip_next_elements(self): + string = "-a-b-c--#--(de,#,|)-f-" + " 012345678901234567890" + ex = Exception('ex') + results = parse(string, error=ex) + expected = [ + mess_on_next(1, 'a'), + mess_on_next(3, 'b'), + mess_on_next(5, 'c'), + mess_on_error(8, ex), + ] + assert results == expected + + def test_parse_marble_with_on_completed_skip_next_elements(self): + string = "-a-b-c--|--(de,#,|)-f-" + " 012345678901234567890" + ex = Exception('ex') + results = parse(string, error=ex) + expected = [ + mess_on_next(1, 'a'), + mess_on_next(3, 'b'), + mess_on_next(5, 'c'), + mess_on_completed(8), + ] + assert results == expected + def test_parse_marble_with_space(self): string = " -a b- c- de |" " 01 23 45 67 8901234567890" @@ -415,7 +441,7 @@ def test_hot_marble_with_error(self): ] assert results == expected - def test_from_marbles_marble_with_consecutive_symbols(self): + def test_hot_marble_with_consecutive_symbols(self): string = "-ab(12)#--" " 012345678901234567890" ex = Exception('ex') From c44e948462d974e0f72a00ff20476e6a01482617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Sat, 2 Feb 2019 19:57:57 +0100 Subject: [PATCH 15/17] handle timedelta/datetime + enforce time value as float + cleaning --- rx/__init__.py | 106 ++++++++++++++---- rx/core/observable/marbles.py | 123 ++++++--------------- tests/test_observable/test_marbles.py | 152 ++++++++++++++++---------- tests/test_testing/test_marbles.py | 30 ++--- 4 files changed, 227 insertions(+), 184 deletions(-) diff --git a/rx/__init__.py b/rx/__init__.py index 034985583..8974f93d4 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -232,7 +232,7 @@ def from_iterable(iterable: Iterable) -> Observable: from_list = from_iterable -def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typing.Scheduler = None, +def from_marbles(string: str, timespan: typing.RelativeTime = 0.1, scheduler: typing.Scheduler = None, lookup = None, error: Exception = None) -> Observable: """Convert a marble diagram string to a cold observable sequence, using an optional scheduler to enumerate the events. @@ -254,7 +254,9 @@ def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typ +--------+--------------------------------------------------------+ | `)` | close a group of marbles | +--------+--------------------------------------------------------+ - | space | used to align multiple diagrams, does not advance time.| + | `,` | separate elements in a group | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time | +--------+--------------------------------------------------------+ In a group of marbles, the position of the initial `(` determines the @@ -262,8 +264,8 @@ def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typ emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. Examples: - >>> from_marbles("--1--(42)-3--|") - >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) + >>> from_marbles("--1--(2,3)-4--|") + >>> from_marbles("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) >>> from_marbles("a--b---#", error=ValueError("foo")) Args: @@ -279,7 +281,8 @@ def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typ If not specified, defaults to Exception('error'). scheduler: [Optional] Scheduler to run the the input sequence - on. + on. If not specified, defaults to the downstream scheduler, + else to NewThreadScheduler. Returns: The observable sequence whose elements are pulled from the @@ -297,25 +300,6 @@ def from_marbles(string: str, timespan:typing.RelativeTime = 0.1, scheduler: typ cold = from_marbles -# TODO: need to move hot() operator (not in alphabetic order) -# TODO: write the doc -def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRelativeTime = 0.0, - scheduler: typing.Scheduler = None, lookup = None, error: Exception = None) -> Observable: - - from .core.observable.marbles import hot as _hot - return _hot( - string, - timespan=timespan, - duetime=duetime, - lookup=lookup, - error=error, - scheduler=scheduler) - - -def to_marbles(scheduler: typing.Scheduler = None, timespan = 0.1) -> str: - from .core.observable.marbles import to_marbles as _to_marbles - return _to_marbles(scheduler=scheduler, timespan=timespan) - def generate_with_relative_time(initial_state, condition, iterate, time_mapper) -> Observable: """Generates an observable sequence by iterating a state from an @@ -361,6 +345,75 @@ def generate(initial_state, condition, iterate) -> Observable: return _generate(initial_state, condition, iterate) +def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRelativeTime = 0.0, + scheduler: typing.Scheduler = None, lookup = None, error: Exception = None) -> Observable: + """Convert a marble diagram string to a hot observable sequence, using + an optional scheduler to enumerate the events. + + Each character in the string will advance time by timespan + (exept for space). Characters that are not special (see the table below) + will be interpreted as a value to be emitted. numbers will be cast + to int or float. + + Special characters: + +--------+--------------------------------------------------------+ + | `-` | advance time by timespan | + +--------+--------------------------------------------------------+ + | `#` | on_error() | + +--------+--------------------------------------------------------+ + | `|` | on_completed() | + +--------+--------------------------------------------------------+ + | `(` | open a group of marbles sharing the same timestamp | + +--------+--------------------------------------------------------+ + | `)` | close a group of marbles | + +--------+--------------------------------------------------------+ + | `,` | separate elements in a group | + +--------+--------------------------------------------------------+ + | space | used to align multiple diagrams, does not advance time | + +--------+--------------------------------------------------------+ + + In a group of marbles, the position of the initial `(` determines the + timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will + emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + + Examples: + >>> from_marbles("--1--(2,3)-4--|") + >>> from_marbles("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) + >>> from_marbles("a--b---#", error=ValueError("foo")) + + Args: + string: String with marble diagram + + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + + duetime: [Optional] Absolute datetime or timedelta relative to + the calling time that defines when to start the emission of elements. + + lookup: [Optional] dict used to convert a marble into a specified + value. If not specified, defaults to {}. + + error: [Optional] exception that will be use in place of the # symbol. + If not specified, defaults to Exception('error'). + + scheduler: [Optional] Scheduler to run the the input sequence + on. If not specified, defaults to NewThreadScheduler. + + Returns: + The observable sequence whose elements are pulled from the + given marble diagram string. + """ + + from .core.observable.marbles import hot as _hot + return _hot( + string, + timespan=timespan, + duetime=duetime, + lookup=lookup, + error=error, + scheduler=scheduler) + + def if_then(condition: Callable[[], bool], then_source: Observable, else_source: Observable = None) -> Observable: """Determines whether an observable collection contains values. @@ -644,6 +697,11 @@ def to_async(func: Callable, scheduler=None) -> Callable: return _to_async(func, scheduler) +def to_marbles(scheduler: typing.Scheduler = None, timespan=0.1) -> str: + from .core.observable.marbles import to_marbles as _to_marbles + return _to_marbles(scheduler=scheduler, timespan=timespan) + + def using(resource_factory: Callable[[], typing.Disposable], observable_factory: Callable[[typing.Disposable], Observable] ) -> Observable: """Constructs an observable sequence that depends on a resource diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index cf81473fa..a79e356bf 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -10,9 +10,6 @@ from rx.concurrency import NewThreadScheduler from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler -# TODO: hot() should not rely on operators since it could be used for testing -import rx -from rx import operators as ops new_thread_scheduler = NewThreadScheduler() @@ -35,31 +32,32 @@ tokens = re.compile(pattern) -# TODO: use a plain impelementation instead of operators -def hot(string: str, timespan: RelativeTime = 0.1, duetime:AbsoluteOrRelativeTime=0, +def hot(string: str, timespan: RelativeTime = 0.1, duetime: AbsoluteOrRelativeTime = 0.0, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: _scheduler = scheduler or new_thread_scheduler + if isinstance(duetime, datetime): + duetime = duetime - _scheduler.now + messages = parse( string, - time_shift=duetime, timespan=timespan, + time_shift=duetime, lookup=lookup, error=error, ) lock = threading.RLock() - is_completed = False - is_on_error = False + is_stopped = False observers = [] def subscribe(observer, scheduler=None): - if not is_completed and not is_on_error: + # should a hot observable already completed or on error + # re-push on_completed/on_error at subscription time? + if not is_stopped: with lock: observers.append(observer) - # should a hot observable already completed or on error - # re-push on_completed/on_error at subscription time? def dispose(): with lock: @@ -72,17 +70,14 @@ def dispose(): def create_action(notification): def action(scheduler, state=None): - nonlocal is_completed - nonlocal is_on_error + nonlocal is_stopped with lock: for observer in observers: notification.accept(observer) - if notification.kind == 'C': - is_completed = True - elif notification.kind == 'E': - is_on_error = True + if notification.kind in ('C', 'E'): + is_stopped = True return action @@ -92,67 +87,11 @@ def action(scheduler, state=None): # Don't make closures within a loop _scheduler.schedule_relative(timespan, action) - return Observable(subscribe) - - def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: - """Convert a marble diagram string to a cold observable sequence, using - an optional scheduler to enumerate the events. - - Each character in the string will advance time by timespan - (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted. Digit 0-9 will be cast - to int. - - Special characters: - +--------+--------------------------------------------------------+ - | `-` | advance time by timespan | - +--------+--------------------------------------------------------+ - | `#` | on_error() | - +--------+--------------------------------------------------------+ - | `|` | on_completed() | - +--------+--------------------------------------------------------+ - | `(` | open a group of marbles sharing the same timestamp | - +--------+--------------------------------------------------------+ - | `)` | close a group of marbles | - +--------+--------------------------------------------------------+ - | `,` | separate elements in a group | - +--------+--------------------------------------------------------+ - | space | used to align multiple diagrams, does not advance time.| - +--------+--------------------------------------------------------+ - - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. - - Examples: - >>> from_marbles("--1--(42)-3--|") - >>> from_marbles("a--B--c-", lookup={'a': 1, 'B': 2, 'c': 3}) - >>> from_marbles("a--b---#", error=ValueError("foo")) - - Args: - string: String with marble diagram - - timespan: [Optional] duration of each character in second. - If not specified, defaults to 0.1s. - - lookup: [Optional] dict used to convert a marble into a specified - value. If not specified, defaults to {}. - - error: [Optional] exception that will be use in place of the # symbol. - If not specified, defaults to Exception('error'). - - scheduler: [Optional] Scheduler to run the the input sequence - on. - - Returns: - The observable sequence whose elements are pulled from the - given marble diagram string. - """ disp = CompositeDisposable() messages = parse(string, timespan=timespan, lookup=lookup, error=error) @@ -233,16 +172,15 @@ def stringify(value): return string -# TODO: complete the definition of the return type List[tuple] -def parse(string: str, timespan: RelativeTime = 1, time_shift: RelativeTime = 0, - lookup: Dict = None, error: Exception = None) -> List[Tuple]: - """Convert a marble diagram string to a list of records of type - :class:`rx.testing.recorded.Recorded`. + +def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = 0.0, lookup: Dict = None, + error: Exception = None) -> List[Tuple[RelativeTime, notification.Notification]]: + """Convert a marble diagram string to a list of messages. Each character in the string will advance time by timespan (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted according to their horizontal - position in the diagram. Digit 0-9 will be cast to :class:`int`. + will be interpreted as a value to be emitted. numbers will be cast + to int or float. Special characters: +--------+--------------------------------------------------------+ @@ -258,7 +196,7 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: RelativeTime = 0, +--------+--------------------------------------------------------+ | `,` | separate elements in a group | +--------+--------------------------------------------------------+ - | space | used to align multiple diagrams, does not advance time.| + | space | used to align multiple diagrams, does not advance time | +--------+--------------------------------------------------------+ In a group of marbles, the position of the initial `(` determines the @@ -266,33 +204,38 @@ def parse(string: str, timespan: RelativeTime = 1, time_shift: RelativeTime = 0, emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. Examples: - >>> res = parse("1-2-3-|") - >>> res = parse("--1--^-(42)-3--|") - >>> res = parse("a--B---c-", lookup={'a': 1, 'B': 2, 'c': 3}) + >>> from_marbles("--1--(2,3)-4--|") + >>> from_marbles("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) + >>> from_marbles("a--b---#", error=ValueError("foo")) Args: string: String with marble diagram - timespan: [Optional] duration of each character. - Default set to 1. + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. lookup: [Optional] dict used to convert a marble into a specified value. If not specified, defaults to {}. - time_shift: [Optional] absolute time of subscription. - If not specified, defaults to 0. + time_shift: [Optional] time used to delay every elements. + If not specified, defaults to 0.0s. error: [Optional] exception that will be use in place of the # symbol. If not specified, defaults to Exception('error'). Returns: - A list of records of type :class:`rx.testing.recorded.Recorded` - containing time and :class:`Notification` as value. + A list of messages defined as a tuple of (timespan, notification). + """ error = error or Exception('error') lookup = lookup or {} + if isinstance(timespan, timedelta): + timespan = timespan.total_seconds() + if isinstance(time_shift, timedelta): + time_shift = time_shift.total_seconds() + string = string.replace(' ', '') # try to cast a string to an int, then to a float diff --git a/tests/test_observable/test_marbles.py b/tests/test_observable/test_marbles.py index 980fe4de2..51dbf1205 100644 --- a/tests/test_observable/test_marbles.py +++ b/tests/test_observable/test_marbles.py @@ -5,6 +5,7 @@ from rx.core.observable.marbles import parse from rx.testing import TestScheduler from rx.testing.reactivetest import ReactiveTest +import datetime def mess_on_next(time, value): @@ -24,26 +25,26 @@ class TestParse(unittest.TestCase): def test_parse_just_on_error(self): string = "#" results = parse(string) - expected = [mess_on_error(0, Exception('error'))] + expected = [mess_on_error(0.0, Exception('error'))] assert results == expected def test_parse_just_on_error_specified(self): string = "#" ex = Exception('Foo') results = parse(string, error=ex) - expected = [mess_on_error(0, ex)] + expected = [mess_on_error(0.0, ex)] assert results == expected def test_parse_just_on_completed(self): string = "|" results = parse(string) - expected = [mess_on_completed(0)] + expected = [mess_on_completed(0.0)] assert results == expected def test_parse_just_on_next(self): string = "a" results = parse(string) - expected = [mess_on_next(0, 'a')] + expected = [mess_on_next(0.0, 'a')] assert results == expected def test_parse_marble_timespan(self): @@ -58,13 +59,25 @@ def test_parse_marble_timespan(self): ] assert results == expected + def test_parse_marble_timedelta(self): + string = "a--b---c" + " 012345678901234567890" + ts = 0.1 + results = parse(string, timespan=datetime.timedelta(seconds=ts)) + expected = [ + mess_on_next(0 * ts, 'a'), + mess_on_next(3 * ts, 'b'), + mess_on_next(7 * ts, 'c'), + ] + assert results == expected + def test_parse_marble_multiple_digits(self): string = "-ab-cde--" " 012345678901234567890" results = parse(string) expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'cde'), + mess_on_next(1.0, 'ab'), + mess_on_next(4.0, 'cde'), ] assert results == expected @@ -73,9 +86,9 @@ def test_parse_marble_multiple_digits_int(self): " 012345678901234567890" results = parse(string) expected = [ - mess_on_next(1, 1), - mess_on_next(3, 22), - mess_on_next(6, 333), + mess_on_next(1.0, 1), + mess_on_next(3.0, 22), + mess_on_next(6.0, 333), ] assert results == expected @@ -84,9 +97,9 @@ def test_parse_marble_multiple_digits_float(self): " 012345678901234567890" results = parse(string) expected = [ - mess_on_next(1, float('1.0')), - mess_on_next(6, float('2.345')), - mess_on_next(13, float('6.7e8')), + mess_on_next(1.0, float('1.0')), + mess_on_next(6.0, float('2.345')), + mess_on_next(13.0, float('6.7e8')), ] assert results == expected @@ -95,9 +108,9 @@ def test_parse_marble_completed(self): " 012345678901234567890" results = parse(string) expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'c'), - mess_on_completed(7), + mess_on_next(1.0, 'ab'), + mess_on_next(4.0, 'c'), + mess_on_completed(7.0), ] assert results == expected @@ -107,10 +120,10 @@ def test_parse_marble_with_error(self): ex = Exception('ex') results = parse(string, error=ex) expected = [ - mess_on_next(1, 'a'), - mess_on_next(3, 'b'), - mess_on_next(5, 'c'), - mess_on_error(8, ex), + mess_on_next(1.0, 'a'), + mess_on_next(3.0, 'b'), + mess_on_next(5.0, 'c'), + mess_on_error(8.0, ex), ] assert results == expected @@ -120,10 +133,10 @@ def test_parse_marble_with_error_skip_next_elements(self): ex = Exception('ex') results = parse(string, error=ex) expected = [ - mess_on_next(1, 'a'), - mess_on_next(3, 'b'), - mess_on_next(5, 'c'), - mess_on_error(8, ex), + mess_on_next(1.0, 'a'), + mess_on_next(3.0, 'b'), + mess_on_next(5.0, 'c'), + mess_on_error(8.0, ex), ] assert results == expected @@ -133,10 +146,10 @@ def test_parse_marble_with_on_completed_skip_next_elements(self): ex = Exception('ex') results = parse(string, error=ex) expected = [ - mess_on_next(1, 'a'), - mess_on_next(3, 'b'), - mess_on_next(5, 'c'), - mess_on_completed(8), + mess_on_next(1.0, 'a'), + mess_on_next(3.0, 'b'), + mess_on_next(5.0, 'c'), + mess_on_completed(8.0), ] assert results == expected @@ -145,10 +158,10 @@ def test_parse_marble_with_space(self): " 01 23 45 67 8901234567890" results = parse(string) expected = [ - mess_on_next(1, 'ab'), - mess_on_next(4, 'c'), - mess_on_next(6, 'de'), - mess_on_completed(8), + mess_on_next(1.0, 'ab'), + mess_on_next(4.0, 'c'), + mess_on_next(6.0, 'de'), + mess_on_completed(8.0), ] assert results == expected @@ -158,15 +171,15 @@ def test_parse_marble_with_group(self): " 0 1 2 " results = parse(string) expected = [ - mess_on_next(1, 'x'), - mess_on_next(2, 'ab'), - mess_on_next(2, 12), - mess_on_next(2, float('1.5')), + mess_on_next(1.0, 'x'), + mess_on_next(2.0, 'ab'), + mess_on_next(2.0, 12), + mess_on_next(2.0, float('1.5')), - mess_on_next(14, 'c'), - mess_on_next(17, 'de'), + mess_on_next(14.0, 'c'), + mess_on_next(17.0, 'de'), - mess_on_completed(22), + mess_on_completed(22.0), ] assert results == expected @@ -182,24 +195,24 @@ def test_parse_marble_lookup(self): results = parse(string, lookup=lookup) expected = [ - mess_on_next(1, 'aabb'), - mess_on_next(4, 'cc'), - mess_on_next(6, '1122'), - mess_on_next(9, 33), - mess_on_completed(11), + mess_on_next(1.0, 'aabb'), + mess_on_next(4.0, 'cc'), + mess_on_next(6.0, '1122'), + mess_on_next(9.0, 33), + mess_on_completed(11.0), ] assert results == expected def test_parse_marble_time_shift(self): string = "-ab----c-d-|" " 012345678901234567890" - offset = 10 + offset = 10.0 results = parse(string, time_shift=offset) expected = [ - mess_on_next(1 + offset, 'ab'), - mess_on_next(7 + offset, 'c'), - mess_on_next(9 + offset, 'd'), - mess_on_completed(11 + offset), + mess_on_next(1.0 + offset, 'ab'), + mess_on_next(7.0 + offset, 'c'), + mess_on_next(9.0 + offset, 'd'), + mess_on_completed(11.0 + offset), ] assert results == expected @@ -216,7 +229,7 @@ def test_from_marbles_on_error(self): scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_error(200, Exception('error'))] + expected = [ReactiveTest.on_error(200.0, Exception('error'))] assert results == expected def test_from_marbles_on_error_specified(self): @@ -226,7 +239,7 @@ def test_from_marbles_on_error_specified(self): scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_error(200, ex)] + expected = [ReactiveTest.on_error(200.0, ex)] assert results == expected def test_from_marbles_on_complete(self): @@ -234,7 +247,7 @@ def test_from_marbles_on_complete(self): obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_completed(200)] + expected = [ReactiveTest.on_completed(200.0)] assert results == expected def test_from_marbles_on_next(self): @@ -242,7 +255,7 @@ def test_from_marbles_on_next(self): obs = rx.from_marbles(string) scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages - expected = [ReactiveTest.on_next(200, 'a')] + expected = [ReactiveTest.on_next(200.0, 'a')] assert results == expected def test_from_marbles_timespan(self): @@ -253,9 +266,9 @@ def test_from_marbles_timespan(self): scheduler = TestScheduler() results = scheduler.start(self.create_factory(obs)).messages expected = [ - ReactiveTest.on_next(0 * ts + 200, 'a'), - ReactiveTest.on_next(3 * ts + 200, 'b'), - ReactiveTest.on_next(7 * ts + 200, 'c'), + ReactiveTest.on_next(0 * ts + 200.0, 'a'), + ReactiveTest.on_next(3 * ts + 200.0, 'b'), + ReactiveTest.on_next(7 * ts + 200.0, 'c'), ] assert results == expected @@ -505,3 +518,32 @@ def test_hot_marble_lookup(self): ] assert results == expected + def test_hot_marble_with_datetime(self): + string = "-ab-c--|" + " 012345678901234567890" + scheduler = TestScheduler() + duetime = scheduler.now + datetime.timedelta(seconds=300.0) + + obs = rx.hot(string, 0.1, duetime, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(300.1, 'ab'), + ReactiveTest.on_next(300.4, 'c'), + ReactiveTest.on_completed(300.7), + ] + assert results == expected + + def test_hot_marble_with_timedelta(self): + string = "-ab-c--|" + " 012345678901234567890" + scheduler = TestScheduler() + duetime = datetime.timedelta(seconds=300.0) + + obs = rx.hot(string, 0.1, duetime, scheduler=scheduler) + results = scheduler.start(self.create_factory(obs)).messages + expected = [ + ReactiveTest.on_next(300.1, 'ab'), + ReactiveTest.on_next(300.4, 'c'), + ReactiveTest.on_completed(300.7), + ] + assert results == expected diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index c8ace224c..97d64520a 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -79,9 +79,9 @@ def create(): results = start(create) expected = [ - ReactiveTest.on_next(200, 12), - ReactiveTest.on_next(204, 3), - ReactiveTest.on_completed(206), + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_completed(206.0), ] assert results == expected @@ -92,9 +92,9 @@ def test_start_with_cold_no_create_function(self): results = start(obs) expected = [ - ReactiveTest.on_next(200, 12), - ReactiveTest.on_next(204, 3), - ReactiveTest.on_completed(206), + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_completed(206.0), ] assert results == expected @@ -119,7 +119,7 @@ def create(): return obs results = start(create) - expected = [ReactiveTest.on_completed(203), ] + expected = [ReactiveTest.on_completed(203.0), ] assert results == expected def test_start_with_hot_normal(self): @@ -132,9 +132,9 @@ def create(): results = start(create) expected = [ - ReactiveTest.on_next(201, 12), - ReactiveTest.on_next(205, 3), - ReactiveTest.on_completed(207), + ReactiveTest.on_next(201.0, 12), + ReactiveTest.on_next(205.0, 3), + ReactiveTest.on_completed(207.0), ] assert results == expected @@ -144,11 +144,11 @@ def test_exp(self): " 012345678901234567890" expected = [ - ReactiveTest.on_next(200, 12), - ReactiveTest.on_next(204, 3), - ReactiveTest.on_next(207, 4), - ReactiveTest.on_next(210, 5), - ReactiveTest.on_completed(212), + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_next(207.0, 4), + ReactiveTest.on_next(210.0, 5), + ReactiveTest.on_completed(212.0), ] assert results == expected From 1df10559098ead49cab5c6e2fe9b5ada83bcce5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Sun, 3 Feb 2019 18:37:26 +0100 Subject: [PATCH 16/17] move to_iterable in rx/operators instead of rx/observable make marble context a true python context update tests update examples --- examples/marbles/frommarbles_error.py | 19 +-- examples/marbles/frommarbles_flatmap.py | 23 +-- examples/marbles/frommarbles_lookup.py | 24 ++- examples/marbles/frommarbles_merge.py | 22 +-- examples/marbles/hot.py | 17 -- examples/marbles/hot_datetime.py | 20 +++ examples/marbles/testing_debounce.py | 28 ++-- examples/marbles/testing_flatmap.py | 33 ++-- examples/marbles/tomarbles.py | 12 ++ rx/__init__.py | 56 +++---- rx/core/observable/marbles.py | 109 ++++-------- rx/core/operators/tomarbles.py | 69 ++++++++ rx/operators/__init__.py | 16 ++ rx/testing/marbles.py | 79 +++++---- tests/test_observable/test_marbles.py | 50 +++--- tests/test_testing/test_marbles.py | 211 ++++++++++++------------ 16 files changed, 404 insertions(+), 384 deletions(-) delete mode 100644 examples/marbles/hot.py create mode 100644 examples/marbles/hot_datetime.py create mode 100644 examples/marbles/tomarbles.py create mode 100644 rx/core/operators/tomarbles.py diff --git a/examples/marbles/frommarbles_error.py b/examples/marbles/frommarbles_error.py index 47c083468..d4589603d 100644 --- a/examples/marbles/frommarbles_error.py +++ b/examples/marbles/frommarbles_error.py @@ -1,19 +1,14 @@ -import time - import rx -from rx import concurrency as ccy +from rx import operators as ops + +""" +Specify the error to be raised in place of the # symbol. +""" err = ValueError("I don't like 5!") src0 = rx.from_marbles('12-----4-----67--|', timespan=0.2) src1 = rx.from_marbles('----3----5-# ', timespan=0.2, error=err) -source = rx.merge(src0, src1) -source.subscribe( - on_next=print, - on_error=lambda e: print('boom!! {}'.format(e)), - on_completed=lambda: print('good job!'), - scheduler=ccy.timeout_scheduler, - ) - -time.sleep(3) +source = rx.merge(src0, src1).pipe(ops.do_action(print)) +source.run() diff --git a/examples/marbles/frommarbles_flatmap.py b/examples/marbles/frommarbles_flatmap.py index 2f36152b2..f25483ee4 100644 --- a/examples/marbles/frommarbles_flatmap.py +++ b/examples/marbles/frommarbles_flatmap.py @@ -1,26 +1,17 @@ -import time - import rx from rx import operators as ops -from rx import concurrency as ccy -a = rx.cold(' ---a---a----------------a-|') -b = rx.cold(' ---b---b---| ') -c = rx.cold(' ---c---c---| ') -d = rx.cold(' --d---d---| ') -e1 = rx.cold('a--b--------c--d-------| ') +a = rx.cold(' ---a0---a1----------------a2-| ') +b = rx.cold(' ---b1---b2---| ') +c = rx.cold(' ---c1---c2---| ') +d = rx.cold(' -----d1---d2---|') +e1 = rx.cold('a--b--------c-----d-------| ') observableLookup = {"a": a, "b": b, "c": c, "d": d} source = e1.pipe( ops.flat_map(lambda value: observableLookup[value]), + ops.do_action(lambda v: print(v)), ) -source.subscribe_( - on_next=print, - on_error=lambda e: print('boom!! {}'.format(e)), - on_completed=lambda: print('good job!'), - scheduler=ccy.timeout_scheduler, - ) - -time.sleep(3) +source.run() diff --git a/examples/marbles/frommarbles_lookup.py b/examples/marbles/frommarbles_lookup.py index 285b79848..b436c5dfd 100644 --- a/examples/marbles/frommarbles_lookup.py +++ b/examples/marbles/frommarbles_lookup.py @@ -1,20 +1,16 @@ -import time import rx -from rx import concurrency as ccy +import rx.operators as ops +""" +Use a dictionnary to convert elements declared in the marbles diagram to +the specified values. +""" lookup0 = {'a': 1, 'b': 3, 'c': 5} lookup1 = {'x': 2, 'y': 4, 'z': 6} -source0 = rx.cold('a---b----c----|', timespan=0.2, lookup=lookup0) -source1 = rx.cold('---x---y---z--|', timespan=0.2, lookup=lookup1) +source0 = rx.cold('a---b----c----|', timespan=0.01, lookup=lookup0) +source1 = rx.cold('---x---y---z--|', timespan=0.01, lookup=lookup1) -observable = rx.merge(source0, source1) - -observable.subscribe_( - on_next=print, - on_error=lambda e: print('boom!! {}'.format(e)), - on_completed=lambda: print('good job!'), - scheduler=ccy.timeout_scheduler, - ) - -time.sleep(3) +observable = rx.merge(source0, source1).pipe(ops.to_iterable()) +elements = observable.run() +print('received {}'.format(list(elements))) diff --git a/examples/marbles/frommarbles_merge.py b/examples/marbles/frommarbles_merge.py index acfb244b6..e44e0ac7c 100644 --- a/examples/marbles/frommarbles_merge.py +++ b/examples/marbles/frommarbles_merge.py @@ -1,18 +1,14 @@ -import time import rx -from rx import concurrency as ccy +from rx import operators as ops -source0 = rx.cold('a-----d---1--------4-|', timespan=0.1) -source1 = rx.cold('--b-c-------2---3-| ', timespan=0.1) +""" +simple example that merges two cold observables. +""" -observable = rx.merge(source0, source1) +source0 = rx.cold('a-----d---1--------4-|', timespan=0.01) +source1 = rx.cold('--b-c-------2---3-| ', timespan=0.01) -observable.subscribe( - on_next=print, - on_error=lambda e: print('boom!! {}'.format(e)), - on_completed=lambda: print('good job!'), - scheduler=ccy.timeout_scheduler, - ) - -time.sleep(3) +observable = rx.merge(source0, source1).pipe(ops.to_iterable()) +elements = observable.run() +print('received {}'.format(list(elements))) diff --git a/examples/marbles/hot.py b/examples/marbles/hot.py deleted file mode 100644 index 17a46ef12..000000000 --- a/examples/marbles/hot.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- - -from rx.testing import marbles -import rx.concurrency as ccy -import datetime -import rx - -#start_time = 5 -now = datetime.datetime.utcnow() -start_time = now + datetime.timedelta(seconds=3.0) -hot = rx.hot('--a--b--c--|', -# timespan=0.3, - start_time=start_time, -# scheduler=ccy.timeout_scheduler, - ) - -hot.subscribe(print, print, lambda: print('completed')) diff --git a/examples/marbles/hot_datetime.py b/examples/marbles/hot_datetime.py new file mode 100644 index 000000000..7603d7d57 --- /dev/null +++ b/examples/marbles/hot_datetime.py @@ -0,0 +1,20 @@ +import datetime + +import rx +import rx.operators as ops + +""" +Delay the emission of elements to the specified datetime. +""" + +now = datetime.datetime.utcnow() +dt = datetime.timedelta(seconds=3.0) +duetime = now + dt + +print('{} -> now\n' + '{} -> start of emission in {}s'.format(now, duetime, dt.total_seconds())) + +hot = rx.hot('10--11--12--13--(14,|)', timespan=0.2, duetime=duetime) + +source = hot.pipe(ops.do_action(print)) +source.run() diff --git a/examples/marbles/testing_debounce.py b/examples/marbles/testing_debounce.py index 8f0ae920c..c6d514930 100644 --- a/examples/marbles/testing_debounce.py +++ b/examples/marbles/testing_debounce.py @@ -1,5 +1,5 @@ from rx import operators as ops -from rx.testing import marbles +from rx.testing.marbles import marbles_testing """ Tests debounceTime from rxjs @@ -7,24 +7,20 @@ it should delay all element by the specified time """ +with marbles_testing(timespan=1.0) as (start, cold, hot, exp): -start, cold, hot, exp = marbles.test_context(timespan=1) + e1 = cold('-a--------b------c----|') + ex = exp( '------a--------b------(c,|)') + expected = ex -e1 = hot('-a--------b------c----|') -ex = exp('------a--------b------(c|)') -expected = ex + def create(): + return e1.pipe( + ops.debounce(5), + ) + results = start(create) + assert results == expected -def create(): - return e1.pipe( - ops.debounce(5), - ) - - -results = start(create) -assert results == expected - -print('\ndebounce: results vs expected') +print('debounce: results vs expected') for r, e in zip(results, expected): print(r, e) - diff --git a/examples/marbles/testing_flatmap.py b/examples/marbles/testing_flatmap.py index 4fc22a356..9d34592c0 100644 --- a/examples/marbles/testing_flatmap.py +++ b/examples/marbles/testing_flatmap.py @@ -1,5 +1,5 @@ from rx import operators as ops -from rx.testing import marbles +from rx.testing.marbles import marbles_testing """ Tests MergeMap from rxjs @@ -7,29 +7,26 @@ it should flat_map many regular interval inners """ -start, cold, hot, exp = marbles.test_context(timespan=1) +with marbles_testing(timespan=1.0) as context: + start, cold, hot, exp = context -a = cold(' ----a---a---a---(a|) ') -b = cold(' ----1---b---(b|) ') -c = cold(' ----c---c---c---c---(c|)') -d = cold(' ----(d|) ') -e1 = hot('-a---b-----------c-------d-------| ') -ex = exp('-----a---(a1)(ab)(ab)c---c---(cd)c---(c|)') -expected = ex + a = cold(' ----a---a----a----(a,|) ') + b = cold(' ----1----b----(b,|) ') + c = cold(' -------c---c---c----c---(c,|)') + d = cold(' -------(d,|) ') + e1 = hot('-a---b-----------c-------d------------| ') + ex = exp('-----a---(a,1)(a,b)(a,b)c---c---(c,d)c---(c,|)') + expected = ex -observableLookup = {"a": a, "b": b, "c": c, "d": d} + observableLookup = {"a": a, "b": b, "c": c, "d": d} - -def create(): - return e1.pipe( + obs = e1.pipe( ops.flat_map(lambda value: observableLookup[value]) ) + results = start(obs) + assert results == expected -results = start(create) -assert results == expected - - -print('\nflat_map: results vs expected') +print('flat_map: results vs expected') for r, e in zip(results, expected): print(r, e) diff --git a/examples/marbles/tomarbles.py b/examples/marbles/tomarbles.py new file mode 100644 index 000000000..ecb327e1b --- /dev/null +++ b/examples/marbles/tomarbles.py @@ -0,0 +1,12 @@ +import rx +from rx import concurrency as ccy +from rx import operators as ops + +source0 = rx.cold('a-----d---1--------4-|', timespan=0.1) +source1 = rx.cold('--b-c-------2---3-| ', timespan=0.1) + +print("to_marbles() is a blocking operator, we need to wait for completion...") +print('expecting "a-b-c-d---1-2---3--4-|"') +observable = rx.merge(source0, source1).pipe(ops.to_marbles(timespan=0.1)) +diagram = observable.run() +print('got "{}"'.format(diagram)) diff --git a/rx/__init__.py b/rx/__init__.py index 8974f93d4..f58771d18 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -239,7 +239,7 @@ def from_marbles(string: str, timespan: typing.RelativeTime = 0.1, scheduler: ty Each character in the string will advance time by timespan (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted. numbers will be cast + will be interpreted as a value to be emitted. Numbers will be cast to int or float. Special characters: @@ -259,9 +259,10 @@ def from_marbles(string: str, timespan: typing.RelativeTime = 0.1, scheduler: ty | space | used to align multiple diagrams, does not advance time | +--------+--------------------------------------------------------+ - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + In a group of elements, the position of the initial `(` determines the + timestamp at which grouped elements will be emitted. E.g. `--(12,3,4)--` + will emit 12, 3, 4 at 2 * timespan and then advance virtual time + by 8 * timespan. Examples: >>> from_marbles("--1--(2,3)-4--|") @@ -274,15 +275,15 @@ def from_marbles(string: str, timespan: typing.RelativeTime = 0.1, scheduler: ty timespan: [Optional] duration of each character in second. If not specified, defaults to 0.1s. - lookup: [Optional] dict used to convert a marble into a specified + lookup: [Optional] dict used to convert an element into a specified value. If not specified, defaults to {}. error: [Optional] exception that will be use in place of the # symbol. If not specified, defaults to Exception('error'). scheduler: [Optional] Scheduler to run the the input sequence - on. If not specified, defaults to the downstream scheduler, - else to NewThreadScheduler. + on. If not specified, defaults to the subscribe scheduler + if defined, else to NewThreadScheduler. Returns: The observable sequence whose elements are pulled from the @@ -290,12 +291,7 @@ def from_marbles(string: str, timespan: typing.RelativeTime = 0.1, scheduler: ty """ from .core.observable.marbles import from_marbles as _from_marbles - return _from_marbles( - string, - timespan=timespan, - lookup=lookup, - error=error, - scheduler=scheduler) + return _from_marbles(string, timespan, lookup=lookup, error=error, scheduler=scheduler) cold = from_marbles @@ -346,13 +342,13 @@ def generate(initial_state, condition, iterate) -> Observable: def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRelativeTime = 0.0, - scheduler: typing.Scheduler = None, lookup = None, error: Exception = None) -> Observable: + scheduler: typing.Scheduler = None, lookup=None, error: Exception = None) -> Observable: """Convert a marble diagram string to a hot observable sequence, using an optional scheduler to enumerate the events. Each character in the string will advance time by timespan (exept for space). Characters that are not special (see the table below) - will be interpreted as a value to be emitted. numbers will be cast + will be interpreted as a value to be emitted. Numbers will be cast to int or float. Special characters: @@ -363,18 +359,19 @@ def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRela +--------+--------------------------------------------------------+ | `|` | on_completed() | +--------+--------------------------------------------------------+ - | `(` | open a group of marbles sharing the same timestamp | + | `(` | open a group of elemets sharing the same timestamp | +--------+--------------------------------------------------------+ - | `)` | close a group of marbles | + | `)` | close a group of elements | +--------+--------------------------------------------------------+ | `,` | separate elements in a group | +--------+--------------------------------------------------------+ | space | used to align multiple diagrams, does not advance time | +--------+--------------------------------------------------------+ - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + In a group of elements, the position of the initial `(` determines the + timestamp at which grouped elements will be emitted. E.g. `--(12,3,4)--` + will emit 12, 3, 4 at 2 * timespan and then advance virtual time + by 8 * timespan. Examples: >>> from_marbles("--1--(2,3)-4--|") @@ -387,10 +384,10 @@ def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRela timespan: [Optional] duration of each character in second. If not specified, defaults to 0.1s. - duetime: [Optional] Absolute datetime or timedelta relative to - the calling time that defines when to start the emission of elements. + duetime: [Optional] Absolute datetime or timedelta from now that + determines when to start the emission of elements. - lookup: [Optional] dict used to convert a marble into a specified + lookup: [Optional] dict used to convert an element into a specified value. If not specified, defaults to {}. error: [Optional] exception that will be use in place of the # symbol. @@ -405,13 +402,7 @@ def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRela """ from .core.observable.marbles import hot as _hot - return _hot( - string, - timespan=timespan, - duetime=duetime, - lookup=lookup, - error=error, - scheduler=scheduler) + return _hot(string, timespan, duetime, lookup=lookup, error=error, scheduler=scheduler) def if_then(condition: Callable[[], bool], then_source: Observable, @@ -697,11 +688,6 @@ def to_async(func: Callable, scheduler=None) -> Callable: return _to_async(func, scheduler) -def to_marbles(scheduler: typing.Scheduler = None, timespan=0.1) -> str: - from .core.observable.marbles import to_marbles as _to_marbles - return _to_marbles(scheduler=scheduler, timespan=timespan) - - def using(resource_factory: Callable[[], typing.Disposable], observable_factory: Callable[[typing.Disposable], Observable] ) -> Observable: """Constructs an observable sequence that depends on a resource diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index a79e356bf..c7013c888 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -5,16 +5,15 @@ from rx import Observable from rx.core import notification -from rx.disposable import Disposable -from rx.disposable import CompositeDisposable +from rx.disposable import CompositeDisposable, Disposable from rx.concurrency import NewThreadScheduler from rx.core.typing import RelativeTime, AbsoluteOrRelativeTime, Scheduler new_thread_scheduler = NewThreadScheduler() -# tokens will be search with pipe in the order below -# group of elements: match any characters surrounding by () +# tokens will be searched in the order below using pipe +# group of elements: match any characters surrounded by () pattern_group = r"(\(.*?\))" # timespan: match one or multiple hyphens pattern_ticks = r"(-+)" @@ -46,6 +45,7 @@ def hot(string: str, timespan: RelativeTime = 0.1, duetime: AbsoluteOrRelativeTi time_shift=duetime, lookup=lookup, error=error, + raise_stopped=True, ) lock = threading.RLock() @@ -87,6 +87,7 @@ def action(scheduler, state=None): # Don't make closures within a loop _scheduler.schedule_relative(timespan, action) + return Observable(subscribe) @@ -94,7 +95,7 @@ def from_marbles(string: str, timespan: RelativeTime = 0.1, lookup: Dict = None, error: Exception = None, scheduler: Scheduler = None) -> Observable: disp = CompositeDisposable() - messages = parse(string, timespan=timespan, lookup=lookup, error=error) + messages = parse(string, timespan=timespan, lookup=lookup, error=error, raise_stopped=True) def schedule_msg(message, observer, scheduler): timespan, notification = message @@ -108,73 +109,15 @@ def subscribe(observer, scheduler_): _scheduler = scheduler or scheduler_ or new_thread_scheduler for message in messages: - # Don't make closures within a loop schedule_msg(message, observer, _scheduler) + return disp return Observable(subscribe) -def to_marbles(scheduler=None, timespan=0.1): - """Convert an observable sequence into a marble diagram string - - Args: - scheduler: [Optional] The scheduler used to run the the input - sequence on. - - Returns: - Observable stream. - """ - def _to_marbles(source: Observable) -> Observable: - def subscribe(observer, scheduler=None): - scheduler = scheduler or new_thread_scheduler - - result: List[str] = [] - last = scheduler.now - - def add_timespan(): - nonlocal last - - now = scheduler.now - diff = now - last - last = now - secs = scheduler.to_seconds(diff) - dashes = "-" * int((secs + timespan / 2.0) * (1.0 / timespan)) - result.append(dashes) - - def on_next(value): - add_timespan() - result.append(stringify(value)) - - def on_error(exception): - add_timespan() - result.append(stringify(exception)) - observer.on_next("".join(n for n in result)) - observer.on_completed() - - def on_completed(): - add_timespan() - result.append("|") - observer.on_next("".join(n for n in result)) - observer.on_completed() - - return source.subscribe_(on_next, on_error, on_completed) - return Observable(subscribe) - return _to_marbles - - -def stringify(value): - """Utility for stringifying an event. - """ - string = str(value) - if len(string) > 1: - string = "(%s)" % string - - return string - - def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = 0.0, lookup: Dict = None, - error: Exception = None) -> List[Tuple[RelativeTime, notification.Notification]]: + error: Exception = None, raise_stopped: bool = False) -> List[Tuple[RelativeTime, notification.Notification]]: """Convert a marble diagram string to a list of messages. Each character in the string will advance time by timespan @@ -190,18 +133,19 @@ def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = +--------+--------------------------------------------------------+ | `|` | on_completed() | +--------+--------------------------------------------------------+ - | `(` | open a group of marbles sharing the same timestamp | + | `(` | open a group of elements sharing the same timestamp | +--------+--------------------------------------------------------+ - | `)` | close a group of marbles | + | `)` | close a group of elements | +--------+--------------------------------------------------------+ | `,` | separate elements in a group | +--------+--------------------------------------------------------+ | space | used to align multiple diagrams, does not advance time | +--------+--------------------------------------------------------+ - In a group of marbles, the position of the initial `(` determines the - timestamp at which grouped marbles will be emitted. E.g. `--(abc)--` will - emit a, b, c at 2 * timespan and then advance virtual time by 5 * timespan. + In a group of elements, the position of the initial `(` determines the + timestamp at which grouped elements will be emitted. E.g. `--(12,3,4)--` + will emit 12, 3, 4 at 2 * timespan and then advance virtual time + by 8 * timespan. Examples: >>> from_marbles("--1--(2,3)-4--|") @@ -214,7 +158,7 @@ def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = timespan: [Optional] duration of each character in second. If not specified, defaults to 0.1s. - lookup: [Optional] dict used to convert a marble into a specified + lookup: [Optional] dict used to convert an element into a specified value. If not specified, defaults to {}. time_shift: [Optional] time used to delay every elements. @@ -222,6 +166,8 @@ def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = error: [Optional] exception that will be use in place of the # symbol. If not specified, defaults to Exception('error'). + raise_finished: [optional] raise ValueError if elements are + declared after on_completed or on_error symbol. Returns: A list of messages defined as a tuple of (timespan, notification). @@ -258,19 +204,30 @@ def map_element(time, element): value = lookup.get(value, value) return (time, notification.OnNext(value)) + is_stopped = False + def check_stopped(element): + nonlocal is_stopped + if raise_stopped: + if is_stopped: + raise ValueError('Elements cannot be declared after a # or | symbol.') + + if element in ('#', '|'): + is_stopped = True + + iframe = 0 messages = [] + for results in tokens.findall(string): timestamp = iframe * timespan + time_shift group, ticks, comma_error, element = results if group: elements = group[1:-1].split(',') + for elm in elements: + check_stopped(elm) grp_messages = [map_element(timestamp, elm) for elm in elements if elm !=''] messages.extend(grp_messages) - kinds = [m[1].kind for m in grp_messages] - if 'E' in kinds or 'C' in kinds: - break iframe += len(group) if ticks: @@ -280,11 +237,9 @@ def map_element(time, element): raise ValueError("Comma is only allowed in group of elements.") if element: + check_stopped(element) message = map_element(timestamp, element) messages.append(message) - kind = message[1].kind - if kind == 'E' or kind == 'C': - break iframe += len(element) return messages diff --git a/rx/core/operators/tomarbles.py b/rx/core/operators/tomarbles.py new file mode 100644 index 000000000..869317526 --- /dev/null +++ b/rx/core/operators/tomarbles.py @@ -0,0 +1,69 @@ +from typing import List + +from rx.core import Observable +from rx.core.typing import Scheduler, RelativeTime +from rx.concurrency import NewThreadScheduler + +new_thread_scheduler = NewThreadScheduler() + + +def _to_marbles(scheduler: Scheduler = None, timespan: RelativeTime = 0.1): + + def to_marbles(source: Observable) -> Observable: + """Convert an observable sequence into a marble diagram string. + + Args: + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + scheduler: [Optional] The scheduler used to run the the input + sequence on. + + Returns: + Observable stream. + """ + + def subscribe(observer, scheduler=None): + scheduler = scheduler or new_thread_scheduler + + result: List[str] = [] + last = scheduler.now + + def add_timespan(): + nonlocal last + + now = scheduler.now + diff = now - last + last = now + secs = scheduler.to_seconds(diff) + dashes = "-" * int((secs + timespan / 2.0) * (1.0 / timespan)) + result.append(dashes) + + def on_next(value): + add_timespan() + result.append(stringify(value)) + + def on_error(exception): + add_timespan() + result.append(stringify(exception)) + observer.on_next("".join(n for n in result)) + observer.on_completed() + + def on_completed(): + add_timespan() + result.append("|") + observer.on_next("".join(n for n in result)) + observer.on_completed() + + return source.subscribe_(on_next, on_error, on_completed) + return Observable(subscribe) + return to_marbles + + +def stringify(value): + """Utility for stringifying an event. + """ + string = str(value) + if len(string) > 1: + string = "(%s)" % string + + return string \ No newline at end of file diff --git a/rx/operators/__init__.py b/rx/operators/__init__.py index d018e20a5..8ef5002fc 100644 --- a/rx/operators/__init__.py +++ b/rx/operators/__init__.py @@ -2456,6 +2456,22 @@ def to_iterable() -> Callable[[Observable], Observable]: return _to_iterable() +def to_marbles(timespan: typing.RelativeTime = 0.1, scheduler: typing.Scheduler = None ) -> Callable[[Observable], Observable]: + """Convert an observable sequence into a marble diagram string. + + Args: + timespan: [Optional] duration of each character in second. + If not specified, defaults to 0.1s. + scheduler: [Optional] The scheduler used to run the the input + sequence on. + + Returns: + Observable stream. + """ + from rx.core.operators.tomarbles import _to_marbles + return _to_marbles(scheduler=scheduler, timespan=timespan) + + def to_set() -> Callable[[Observable], Observable]: """Converts the observable sequence to a set. diff --git a/rx/testing/marbles.py b/rx/testing/marbles.py index e0729551b..24260b834 100644 --- a/rx/testing/marbles.py +++ b/rx/testing/marbles.py @@ -1,5 +1,7 @@ from typing import List, Tuple, Union, Dict from collections import namedtuple +from contextlib import contextmanager +from warnings import warn import rx from rx.core import Observable @@ -10,13 +12,14 @@ new_thread_scheduler = NewThreadScheduler() -TestContext = namedtuple('TestContext', 'start, cold, hot, exp') +MarblesContext = namedtuple('MarblesContext', 'start, cold, hot, exp') -def test_context(timespan=1): +@contextmanager +def marbles_testing(timespan=1.0): """ - Initialize a :class:`TestScheduler` and return a namedtuple containing the - following functions that wrap its methods. + Initialize a :class:`rx.testing.TestScheduler` and return a namedtuple + containing the following functions that wrap its methods. :func:`cold()`: Parse a marbles string and return a cold observable @@ -25,7 +28,7 @@ def test_context(timespan=1): Parse a marbles string and return a hot observable :func:`start()`: - Start the test scheduler and invoke the create function, + Start the test scheduler, invoke the create function, subscribe to the resulting sequence, dispose the subscription and return the resulting records @@ -33,33 +36,48 @@ def test_context(timespan=1): Parse a marbles string and return a list of records Examples: - >>> start, cold, hot, exp = test_context() - - >>> context = test_context() - >>> context.cold("--a--b--#", error=Exception("foo")) - - >>> e0 = hot("a---^-b---c-|") - >>> ex = exp(" --b---c-|") - >>> results = start(e1) - >>> assert results.messages == ex + >>> with marbles_testing() as (start, cold, hot, exp): + ... obs = hot("-a-----b---c-|") + ... ex = exp( "-a-----b---c-|") + ... results = start(obs) + ... assert results == ex The underlying test scheduler is initialized with the following parameters: - - created time = 100 - - subscribed = 200 - - disposed = 1000 + - created time = 100.0s + - subscribed = 200.0s + - disposed = 1000.0s **IMPORTANT**: regarding :func:`hot()`, a marble declared as the first character will be skipped by the test scheduler. - E.g. `hot("a--b--")` will only emit `b`. + E.g. hot("a--b--") will only emit b. """ scheduler = TestScheduler() - created = 100 - disposed = 1000 - subscribed = 200 + created = 100.0 + disposed = 1000.0 + subscribed = 200.0 + start_called = False + outside_of_context = False + + def check(): + if outside_of_context: + warn('context functions should not be called outside of ' + 'with statement.', + UserWarning, + stacklevel=3, + ) + + if start_called: + warn('start() should only be called one time inside ' + 'a with statement.', + UserWarning, + stacklevel=3, + ) def test_start(create: Union[Observable, Callable[[], Observable]]) -> List[Recorded]: + nonlocal start_called + check() def default_create(): return create @@ -75,14 +93,10 @@ def default_create(): subscribed=subscribed, disposed=disposed, ) + start_called = True return mock_observer.messages def test_expected(string: str, lookup: Dict = None, error: Exception = None) -> List[Recorded]: - if string.find('^') >= 0: - raise ValueError( - 'Expected function does not support subscription symbol "^".' - 'Got "{}"'.format(string)) - messages = parse( string, timespan=timespan, @@ -92,13 +106,8 @@ def test_expected(string: str, lookup: Dict = None, error: Exception = None) -> ) return messages_to_records(messages) - def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Observable: - if string.find('^') >= 0: - raise ValueError( - 'Cold observable does not support subscription symbol "^".' - 'Got "{}"'.format(string)) - + check() return rx.from_marbles( string, timespan=timespan, @@ -107,6 +116,7 @@ def test_cold(string: str, lookup: Dict = None, error: Exception = None) -> Obse ) def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Observable: + check() hot_obs = rx.hot( string, timespan=timespan, @@ -117,7 +127,10 @@ def test_hot(string: str, lookup: Dict = None, error: Exception = None) -> Obser ) return hot_obs - return TestContext(test_start, test_cold, test_hot, test_expected) + try: + yield MarblesContext(test_start, test_cold, test_hot, test_expected) + finally: + outside_of_context = True def messages_to_records(messages: List[Tuple]) -> List[Recorded]: diff --git a/tests/test_observable/test_marbles.py b/tests/test_observable/test_marbles.py index 51dbf1205..e38395a8b 100644 --- a/tests/test_observable/test_marbles.py +++ b/tests/test_observable/test_marbles.py @@ -127,32 +127,6 @@ def test_parse_marble_with_error(self): ] assert results == expected - def test_parse_marble_with_error_skip_next_elements(self): - string = "-a-b-c--#--(de,#,|)-f-" - " 012345678901234567890" - ex = Exception('ex') - results = parse(string, error=ex) - expected = [ - mess_on_next(1.0, 'a'), - mess_on_next(3.0, 'b'), - mess_on_next(5.0, 'c'), - mess_on_error(8.0, ex), - ] - assert results == expected - - def test_parse_marble_with_on_completed_skip_next_elements(self): - string = "-a-b-c--|--(de,#,|)-f-" - " 012345678901234567890" - ex = Exception('ex') - results = parse(string, error=ex) - expected = [ - mess_on_next(1.0, 'a'), - mess_on_next(3.0, 'b'), - mess_on_next(5.0, 'c'), - mess_on_completed(8.0), - ] - assert results == expected - def test_parse_marble_with_space(self): string = " -a b- c- de |" " 01 23 45 67 8901234567890" @@ -216,6 +190,30 @@ def test_parse_marble_time_shift(self): ] assert results == expected + def test_parse_marble_raise_with_elements_after_error(self): + string = "-a-b-c--#-1-" + " 012345678901234567890" + with self.assertRaises(ValueError): + parse(string, raise_stopped=True) + + def test_parse_marble_raise_with_elements_after_completed(self): + string = "-a-b-c--|-1-" + " 012345678901234567890" + with self.assertRaises(ValueError): + parse(string, raise_stopped=True) + + def test_parse_marble_raise_with_elements_after_completed_group(self): + string = "-a-b-c--(|,1)-" + " 012345678901234567890" + with self.assertRaises(ValueError): + parse(string, raise_stopped=True) + + def test_parse_marble_raise_with_elements_after_error_group(self): + string = "-a-b-c--(#,1)-" + " 012345678901234567890" + with self.assertRaises(ValueError): + parse(string, raise_stopped=True) + class TestFromMarble(unittest.TestCase): def create_factory(self, observable): diff --git a/tests/test_testing/test_marbles.py b/tests/test_testing/test_marbles.py index 97d64520a..0e6dc92ba 100644 --- a/tests/test_testing/test_marbles.py +++ b/tests/test_testing/test_marbles.py @@ -1,6 +1,6 @@ import unittest -from rx.testing import marbles +from rx.testing.marbles import marbles_testing from rx.testing.reactivetest import ReactiveTest #from rx.concurrency import timeout_scheduler, new_thread_scheduler @@ -46,147 +46,144 @@ class TestTestContext(unittest.TestCase): def test_start_with_cold_never(self): - start, cold, hot, exp = marbles.test_context() - obs = cold("----") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = cold("----") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [] - assert results == expected + results = start(create) + expected = [] + assert results == expected def test_start_with_cold_empty(self): - start, cold, hot, exp = marbles.test_context() - obs = cold("------|") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = cold("------|") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [ReactiveTest.on_completed(206)] - assert results == expected + results = start(create) + expected = [ReactiveTest.on_completed(206)] + assert results == expected def test_start_with_cold_normal(self): - start, cold, hot, exp = marbles.test_context() - obs = cold("12--3-|") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = cold("12--3-|") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [ - ReactiveTest.on_next(200.0, 12), - ReactiveTest.on_next(204.0, 3), - ReactiveTest.on_completed(206.0), - ] - assert results == expected + results = start(create) + expected = [ + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_completed(206.0), + ] + assert results == expected def test_start_with_cold_no_create_function(self): - start, cold, hot, exp = marbles.test_context() - obs = cold("12--3-|") - " 012345678901234567890" - - results = start(obs) - expected = [ - ReactiveTest.on_next(200.0, 12), - ReactiveTest.on_next(204.0, 3), - ReactiveTest.on_completed(206.0), - ] - assert results == expected + with marbles_testing() as (start, cold, hot, exp): + obs = cold("12--3-|") + " 012345678901234567890" + + results = start(obs) + expected = [ + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_completed(206.0), + ] + assert results == expected def test_start_with_hot_never(self): - start, cold, hot, exp = marbles.test_context() - obs = hot("------") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = hot("------") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [] - assert results == expected + results = start(create) + expected = [] + assert results == expected def test_start_with_hot_empty(self): - start, cold, hot, exp = marbles.test_context() - obs = hot("---|") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = hot("---|") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [ReactiveTest.on_completed(203.0), ] - assert results == expected + results = start(create) + expected = [ReactiveTest.on_completed(203.0), ] + assert results == expected def test_start_with_hot_normal(self): - start, cold, hot, exp = marbles.test_context() - obs = hot("-12--3-|") - " 012345678901234567890" + with marbles_testing() as (start, cold, hot, exp): + obs = hot("-12--3-|") + " 012345678901234567890" - def create(): - return obs + def create(): + return obs - results = start(create) - expected = [ - ReactiveTest.on_next(201.0, 12), - ReactiveTest.on_next(205.0, 3), - ReactiveTest.on_completed(207.0), - ] - assert results == expected + results = start(create) + expected = [ + ReactiveTest.on_next(201.0, 12), + ReactiveTest.on_next(205.0, 3), + ReactiveTest.on_completed(207.0), + ] + assert results == expected def test_exp(self): - start, cold, hot, exp = marbles.test_context() - results = exp("12--3--4--5-|") - " 012345678901234567890" - - expected = [ - ReactiveTest.on_next(200.0, 12), - ReactiveTest.on_next(204.0, 3), - ReactiveTest.on_next(207.0, 4), - ReactiveTest.on_next(210.0, 5), - ReactiveTest.on_completed(212.0), - ] - assert results == expected + with marbles_testing() as (start, cold, hot, exp): + results = exp("12--3--4--5-|") + " 012345678901234567890" + + expected = [ + ReactiveTest.on_next(200.0, 12), + ReactiveTest.on_next(204.0, 3), + ReactiveTest.on_next(207.0, 4), + ReactiveTest.on_next(210.0, 5), + ReactiveTest.on_completed(212.0), + ] + assert results == expected def test_start_with_hot_and_exp(self): + with marbles_testing() as (start, cold, hot, exp): + obs = hot(" --3--4--5-|") + expected = exp("--3--4--5-|") + " 012345678901234567890" - start, cold, hot, exp = marbles.test_context() - obs = hot(" --3--4--5-|") - expected = exp("--3--4--5-|") - " 012345678901234567890" + def create(): + return obs - def create(): - return obs - - results = start(create) - assert results == expected + results = start(create) + assert results == expected def test_start_with_cold_and_exp(self): + with marbles_testing() as (start, cold, hot, exp): + obs = cold(" 12--3--4--5-|") + expected = exp(" 12--3--4--5-|") + " 012345678901234567890" - start, cold, hot, exp = marbles.test_context() - obs = cold(" 12--3--4--5-|") - expected = exp(" 12--3--4--5-|") - " 012345678901234567890" - - def create(): - return obs + def create(): + return obs - results = start(create) - assert results == expected + results = start(create) + assert results == expected def test_start_with_cold_and_exp_group(self): + with marbles_testing() as (start, cold, hot, exp): + obs = cold(" 12--(3,6.5)----(5,#)") + expected = exp(" 12--(3,6.5)----(5,#)") + " 012345678901234567890" - start, cold, hot, exp = marbles.test_context() - obs = cold(" 12--(3,6.5)----(5,#)-e-|") - expected = exp(" 12--(3,6.5)----(5,#) ") - " 012345678901234567890" - - def create(): - return obs + def create(): + return obs - results = start(create) - assert results == expected + results = start(create) + assert results == expected From 8fbe13a0a91ed052235da7cae0cc066eaeb65c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Fache?= Date: Sun, 3 Feb 2019 22:41:36 +0100 Subject: [PATCH 17/17] typo --- rx/__init__.py | 6 +++--- rx/core/observable/marbles.py | 7 ++++--- rx/core/operators/tomarbles.py | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rx/__init__.py b/rx/__init__.py index f58771d18..dc2e2c9ab 100644 --- a/rx/__init__.py +++ b/rx/__init__.py @@ -374,9 +374,9 @@ def hot(string, timespan: typing.RelativeTime=0.1, duetime:typing.AbsoluteOrRela by 8 * timespan. Examples: - >>> from_marbles("--1--(2,3)-4--|") - >>> from_marbles("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) - >>> from_marbles("a--b---#", error=ValueError("foo")) + >>> hot("--1--(2,3)-4--|") + >>> hot("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) + >>> hot("a--b---#", error=ValueError("foo")) Args: string: String with marble diagram diff --git a/rx/core/observable/marbles.py b/rx/core/observable/marbles.py index c7013c888..15663dabe 100644 --- a/rx/core/observable/marbles.py +++ b/rx/core/observable/marbles.py @@ -148,9 +148,9 @@ def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = by 8 * timespan. Examples: - >>> from_marbles("--1--(2,3)-4--|") - >>> from_marbles("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) - >>> from_marbles("a--b---#", error=ValueError("foo")) + >>> parse("--1--(2,3)-4--|") + >>> parse("a--b--c-", lookup={'a': 1, 'b': 2, 'c': 3}) + >>> parse("a--b---#", error=ValueError("foo")) Args: string: String with marble diagram @@ -166,6 +166,7 @@ def parse(string: str, timespan: RelativeTime = 1.0, time_shift: RelativeTime = error: [Optional] exception that will be use in place of the # symbol. If not specified, defaults to Exception('error'). + raise_finished: [optional] raise ValueError if elements are declared after on_completed or on_error symbol. diff --git a/rx/core/operators/tomarbles.py b/rx/core/operators/tomarbles.py index 869317526..dfe20a535 100644 --- a/rx/core/operators/tomarbles.py +++ b/rx/core/operators/tomarbles.py @@ -66,4 +66,4 @@ def stringify(value): if len(string) > 1: string = "(%s)" % string - return string \ No newline at end of file + return string