From 592869bca3cde7b63dd1c2c13266ca4baef0d5e4 Mon Sep 17 00:00:00 2001 From: Yangwook Ian Jeong Date: Sun, 6 Nov 2022 14:45:56 +0900 Subject: [PATCH 01/27] docs: fix miss typed operator name --- docs/operators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operators.rst b/docs/operators.rst index c3130a79a..63001ebf3 100644 --- a/docs/operators.rst +++ b/docs/operators.rst @@ -114,7 +114,7 @@ Operator :func:`skip_until ` Discard items emitted by an Observable until a second Observable emits an item. :func:`skip_while ` Discard items emitted by an Observable until a specified condition becomes false. :func:`take_until ` Discard items emitted by an Observable after a second Observable emits an item or terminates. -:func:`take_whle ` Discard items emitted by an Observable after a specified condition becomes false. +:func:`take_while ` Discard items emitted by an Observable after a specified condition becomes false. ================================================================= ================================================ Mathematical and Aggregate Operators From a138cca5f0ef4fbda7dc4071c0e91459032dd3c1 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 7 Dec 2022 09:54:24 +0100 Subject: [PATCH 02/27] Fix flake8 in pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 463b263d4..d24cee0dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - hooks: - id: flake8 exclude: (^docs/|^examples/|^notebooks/|^tests/) - repo: https://gitlab.com/pycqa/flake8 + repo: https://github.com/PyCQA/flake8 rev: 3.9.2 - hooks: - id: pyright From 21ba043636462a3883ddd1f28da01135005488ca Mon Sep 17 00:00:00 2001 From: Robert Stewart Date: Fri, 23 Dec 2022 16:51:23 -0700 Subject: [PATCH 03/27] fix(docs): IO Concurrency for 3.10+ The current example returns errors when run on Python 3.10+. The new example should work on Python 3.7+. The function `asyncio.create_server` lost its loop parameter in Python 3.10 as part of a cleanup of the high-level asyncio api. The function `asyncio.create_server` has been implicitly retrieving the current loop since Python 3.7. [Python 3.10 Removed](https://docs.python.org/3/whatsnew/3.10.html#removed) [Python 3.10 Changes in the Python API](https://docs.python.org/3/whatsnew/3.10.html#changes-in-the-python-api) The result of `asyncio.get_event_loop()` appears to be system dependent. The [documentation](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop) reads _If there is no running event loop set, the function will return the result of `get_event_loop_policy().get_event_loop()` call._ _Because this function has rather complex behavior (especially when custom event loop policies are in use), using the [get_running_loop()](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop) function is preferred to [get_event_loop()](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop) in coroutines and callbacks._ Since the example did not start an event loop on my system, I took the liberty of writing `asyncio.new_event_loop()` instead. ``` Current code returns: /tmp/io.py:47: DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop() starting server Task exception was never retrieved future: exception=TypeError("BaseEventLoop.create_server() got an unexpected keyword argument 'loop'")> Traceback (most recent call last): File "/usr/local/lib/python3.11/asyncio/streams.py", line 85, in start_server return await loop.create_server(factory, host, port, **kwds) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: BaseEventLoop.create_server() got an unexpected keyword argument 'loop' ``` --- docs/get_started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/get_started.rst b/docs/get_started.rst index 7af47dc62..cd9c4f370 100644 --- a/docs/get_started.rst +++ b/docs/get_started.rst @@ -379,7 +379,7 @@ the coroutine. i.future.set_result(i.data) print("starting server") - server = asyncio.start_server(handle_echo, '127.0.0.1', 8888, loop=loop) + server = asyncio.start_server(handle_echo, '127.0.0.1', 8888) loop.create_task(server) sink.subscribe( @@ -390,7 +390,7 @@ the coroutine. return reactivex.create(on_subscribe) - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() proxy = Subject() source = tcp_server(proxy, loop) aio_scheduler = AsyncIOScheduler(loop=loop) From a86f596a07c8f2dbe969241a63b9506c2c63b65a Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 27 Dec 2022 05:46:05 +0800 Subject: [PATCH 04/27] Concat map implementation --- reactivex/operators/__init__.py | 31 ++++ reactivex/operators/_concatmap.py | 20 +++ tests/test_observable/test_concatmap.py | 212 ++++++++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 reactivex/operators/_concatmap.py create mode 100644 tests/test_observable/test_concatmap.py diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 0851b22fd..9a692a371 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -454,6 +454,37 @@ def concat(*sources: Observable[_T]) -> Callable[[Observable[_T]], Observable[_T return concat_(*sources) +def concat_map( + project: Mapper[_T1, Observable[_T2]] +) -> Callable[[Observable[_T1]], Observable[_T2]]: + """Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next. + + Warning: if source values arrive endlessly and faster than their corresponding inner Observables can complete, it will result in memory issues as inner Observables amass in an unbounded buffer waiting for their turn to be subscribed to. + + Note: concatMap is equivalent to mergeMap with concurrency parameter set to 1. + + .. marble:: + :alt: concat_map + + ---1------2--3-------------------| + [ concat_map(i: timer(5).map(i)) ] + --------1------2----3------------| + + Examples: + >>> op = concat(lambda i: reactivex.timer(1.0).pipe(take(3))) + + Args: + project: Projecting function which takes the outer observable value and emits the inner observable + + Returns: + An operator function that maps each value to the inner observable and emits its values in order. + + """ + from ._concatmap import concatmap_ + + return concatmap_(project) + + def contains( value: _T, comparer: Optional[typing.Comparer[_T]] = None ) -> Callable[[Observable[_T]], Observable[bool]]: diff --git a/reactivex/operators/_concatmap.py b/reactivex/operators/_concatmap.py new file mode 100644 index 000000000..c5479864c --- /dev/null +++ b/reactivex/operators/_concatmap.py @@ -0,0 +1,20 @@ +from typing import Callable, TypeVar + +from reactivex import Observable +from reactivex import operators +from reactivex.typing import Mapper + +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") + + +def concatmap_( + project: Mapper[_T1, Observable[_T2]] +) -> Callable[[Observable[_T1]], Observable[_T2]]: + def _concat_map(source: Observable[_T1]) -> Observable[_T2]: + return source.pipe(operators.map(project), operators.merge(max_concurrent=1)) + + return _concat_map + + +__all__ = ["concatmap_"] diff --git a/tests/test_observable/test_concatmap.py b/tests/test_observable/test_concatmap.py new file mode 100644 index 000000000..43eeabc1a --- /dev/null +++ b/tests/test_observable/test_concatmap.py @@ -0,0 +1,212 @@ +import unittest + +from reactivex import operators, Observable +from reactivex.testing import ReactiveTest +from reactivex.testing.subscription import Subscription +from reactivex.testing.testscheduler import TestScheduler + +on_next = ReactiveTest.on_next +on_completed = ReactiveTest.on_completed +on_error = ReactiveTest.on_error +subscribe = ReactiveTest.subscribe +subscribed = ReactiveTest.subscribed +disposed = ReactiveTest.disposed +created = ReactiveTest.created + + +class RxException(Exception): + pass + + +class TestConcatMap(unittest.TestCase): + def test_concat_map_and_flatten_each_item(self): + scheduler = TestScheduler() + e1 = scheduler.create_hot_observable( + on_next(220, 1), + on_next(300, 3), + on_next(330, 5), + on_completed(500), + ) + e2 = scheduler.create_cold_observable( + on_next(0, 10), on_next(10, 10), on_next(20, 10), on_completed(30) + ) + + def create_inner(x: int) -> Observable[int]: + return e2.pipe(operators.map(lambda i: i * x)) + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + assert results.messages == [ + on_next(220, 10), + on_next(230, 10), + on_next(240, 10), + on_next(300, 30), + on_next(310, 30), + on_next(320, 30), + on_next(330, 50), + on_next(340, 50), + on_next(350, 50), + on_completed(500), + ] + assert e1.subscriptions == [Subscription(200, 500)] + assert e2.subscriptions == [ + Subscription(220, 250), + Subscription(300, 330), + Subscription(330, 360), + ] + + def test_concat_map_many_inner_inner_never_completes(self): + """should concat_ap many outer to many inner, inner never completes""" + scheduler = TestScheduler() + e1 = scheduler.create_hot_observable( + on_next(210, 1), + on_next(300, 4), # take 4 will never complete + on_next(400, 5), + ) + e2 = scheduler.create_cold_observable( + on_next(0, 5), on_next(10, 55), on_next(20, 555) + ) + + def create_inner(x: int) -> Observable[int]: + return e2.pipe(operators.take(x)) + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + assert results.messages == [ + on_next(210, 5), + on_next(300, 5), + on_next(310, 55), + on_next(320, 555), + ] + assert e1.subscriptions == [Subscription(200, 1000)] + assert e2.subscriptions == [ + Subscription(210, 210), + Subscription(300, 1000), # unsubs when we dispose of e1 at end of test + ] + + def test_concat_map_finalize_before_next(self): + """should finalize before moving to the next observable""" + scheduler = TestScheduler() + e1 = scheduler.create_hot_observable( + on_next(210, 2), + on_next(220, 4), + on_next(600, 6), + ) + e2 = scheduler.create_cold_observable( + on_next(50, 5), on_next(100, 55), on_completed(100) + ) + + def create_inner(x: int) -> Observable[int]: + return e2.pipe(operators.map(lambda i: i * x)) + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + assert results.messages == [ + on_next(260, 10), + on_next(310, 110), + on_next(310 + 50, 20), + on_next(410, 220), + on_next(650, 30), + on_next(700, 330), + ] + assert e1.subscriptions == [Subscription(200, 1000)] + assert e2.subscriptions == [ + Subscription(210, 310), + Subscription(310, 410), + Subscription(600, 700), + ] + + def test_concat_map_inner_errors(self): + """should propagate errors if the mapped inner throws""" + scheduler = TestScheduler() + e1 = scheduler.create_cold_observable( + on_next(0, 0), + on_next(50, 1), + on_next(100, 2), + ) + + inners = [ + scheduler.create_cold_observable( + on_next(10, 1), on_next(100, 2), on_completed(100) + ), + scheduler.create_cold_observable( + on_next(10, 50), on_error(80, Exception("no")) + ), + scheduler.create_cold_observable( + on_next(10, 1), on_next(100, 2), on_completed(100) + ), + ] + + def create_inner(x: int) -> Observable[int]: + return inners[x] + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + assert results.messages == [ + on_next(210, 1), + on_next(300, 2), + on_next(310, 50), + on_error(380, Exception("no")), + ] + assert e1.subscriptions == [Subscription(200, 380)] + e2, e3, e4 = inners + assert e2.subscriptions == [ + Subscription(200, 300), + ] + assert e3.subscriptions == [ + Subscription(300, 380), + ] + assert e4.subscriptions == [] + + def test_concat_map_outer_errors(self): + scheduler = TestScheduler() + e1 = scheduler.create_hot_observable( + on_next(210, 2), + on_next(220, 4), + on_error(230, Exception("a")), + ) + e2 = scheduler.create_cold_observable(on_next(0, 5), on_completed(100)) + + def create_inner(x: int) -> Observable[int]: + return e2.pipe(operators.map(lambda i: i * x)) + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + + assert results.messages == [on_next(210, 10), on_error(230, Exception("a"))] + assert e1.subscriptions == [Subscription(200, 230)] + assert e2.subscriptions == [ + Subscription(210, 230) + ] # should not be any further sub and should unsub from e2 on outer error + + def test_readme_example(self): + scheduler = TestScheduler() + e1 = scheduler.create_hot_observable( + on_next(300, 1), on_next(400, 2), on_next(430, 3), on_completed(800) + ) + e2 = scheduler.create_cold_observable(on_next(50, 0), on_completed(50)) + + def create_inner(x: int) -> Observable[int]: + return e2.pipe(operators.map(lambda _i: x)) + + def test_create(): + return e1.pipe(operators.concat_map(create_inner)) + + results = scheduler.start(test_create) + + assert results.messages == [ + on_next(350, 1), + on_next(450, 2), + on_next(500, 3), + on_completed(800), + ] From 167d371aa0c086743d11aea30b4306b07e270fe9 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 27 Dec 2022 05:56:07 +0800 Subject: [PATCH 05/27] Change readme example to follow RxJS --- reactivex/operators/__init__.py | 6 +++--- tests/test_observable/test_concatmap.py | 22 ---------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 9a692a371..d329e5ab8 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -466,9 +466,9 @@ def concat_map( .. marble:: :alt: concat_map - ---1------2--3-------------------| - [ concat_map(i: timer(5).map(i)) ] - --------1------2----3------------| + ---1------------2-----3----------------------------| + [ concat_map(i: 10*i---10*i---10*i|) ] + ---10---10---10-20---20---(20,30)---30---30--------| Examples: >>> op = concat(lambda i: reactivex.timer(1.0).pipe(take(3))) diff --git a/tests/test_observable/test_concatmap.py b/tests/test_observable/test_concatmap.py index 43eeabc1a..29425b5fc 100644 --- a/tests/test_observable/test_concatmap.py +++ b/tests/test_observable/test_concatmap.py @@ -188,25 +188,3 @@ def test_create(): assert e2.subscriptions == [ Subscription(210, 230) ] # should not be any further sub and should unsub from e2 on outer error - - def test_readme_example(self): - scheduler = TestScheduler() - e1 = scheduler.create_hot_observable( - on_next(300, 1), on_next(400, 2), on_next(430, 3), on_completed(800) - ) - e2 = scheduler.create_cold_observable(on_next(50, 0), on_completed(50)) - - def create_inner(x: int) -> Observable[int]: - return e2.pipe(operators.map(lambda _i: x)) - - def test_create(): - return e1.pipe(operators.concat_map(create_inner)) - - results = scheduler.start(test_create) - - assert results.messages == [ - on_next(350, 1), - on_next(450, 2), - on_next(500, 3), - on_completed(800), - ] From 35d4003aae02ca01135e220591dcc014d8b904dc Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 29 Dec 2022 17:15:47 +0800 Subject: [PATCH 06/27] Fix flake8 and isort --- reactivex/operators/__init__.py | 15 +++++++++++---- reactivex/operators/_concatmap.py | 3 +-- tests/test_observable/test_concatmap.py | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index d329e5ab8..3172dee97 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -457,9 +457,14 @@ def concat(*sources: Observable[_T]) -> Callable[[Observable[_T]], Observable[_T def concat_map( project: Mapper[_T1, Observable[_T2]] ) -> Callable[[Observable[_T1]], Observable[_T2]]: - """Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next. + """Projects each source value to an Observable which is merged in the + output Observable, in a serialized fashion waiting for each one to complete + before merging the next. - Warning: if source values arrive endlessly and faster than their corresponding inner Observables can complete, it will result in memory issues as inner Observables amass in an unbounded buffer waiting for their turn to be subscribed to. + Warning: if source values arrive endlessly and faster than their corresponding + inner Observables can complete, it will result in memory issues as inner + Observables amass in an unbounded buffer waiting + for their turn to be subscribed to. Note: concatMap is equivalent to mergeMap with concurrency parameter set to 1. @@ -474,10 +479,12 @@ def concat_map( >>> op = concat(lambda i: reactivex.timer(1.0).pipe(take(3))) Args: - project: Projecting function which takes the outer observable value and emits the inner observable + project: Projecting function which takes the outer observable value + and emits the inner observable Returns: - An operator function that maps each value to the inner observable and emits its values in order. + An operator function that maps each value to the inner observable + and emits its values in order. """ from ._concatmap import concatmap_ diff --git a/reactivex/operators/_concatmap.py b/reactivex/operators/_concatmap.py index c5479864c..c47f271d5 100644 --- a/reactivex/operators/_concatmap.py +++ b/reactivex/operators/_concatmap.py @@ -1,7 +1,6 @@ from typing import Callable, TypeVar -from reactivex import Observable -from reactivex import operators +from reactivex import Observable, operators from reactivex.typing import Mapper _T1 = TypeVar("_T1") diff --git a/tests/test_observable/test_concatmap.py b/tests/test_observable/test_concatmap.py index 29425b5fc..c8a6d7a26 100644 --- a/tests/test_observable/test_concatmap.py +++ b/tests/test_observable/test_concatmap.py @@ -1,6 +1,6 @@ import unittest -from reactivex import operators, Observable +from reactivex import Observable, operators from reactivex.testing import ReactiveTest from reactivex.testing.subscription import Subscription from reactivex.testing.testscheduler import TestScheduler From b403943e6cf5e3128fa4d9763c9eb9892df142e2 Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 30 Dec 2022 17:42:29 +0800 Subject: [PATCH 07/27] Move concat_map to __init__ file --- reactivex/operators/__init__.py | 12 +++++++++++- reactivex/operators/_concatmap.py | 19 ------------------- 2 files changed, 11 insertions(+), 20 deletions(-) delete mode 100644 reactivex/operators/_concatmap.py diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 3172dee97..84db2fbc8 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -26,6 +26,7 @@ Observable, abc, compose, + operators, typing, ) from reactivex.internal.basic import identity @@ -487,7 +488,16 @@ def concat_map( and emits its values in order. """ - from ._concatmap import concatmap_ + + def concatmap_( + project: Mapper[_T1, Observable[_T2]] + ) -> Callable[[Observable[_T1]], Observable[_T2]]: + def _concat_map(source: Observable[_T1]) -> Observable[_T2]: + return source.pipe( + operators.map(project), operators.merge(max_concurrent=1) + ) + + return _concat_map return concatmap_(project) diff --git a/reactivex/operators/_concatmap.py b/reactivex/operators/_concatmap.py deleted file mode 100644 index c47f271d5..000000000 --- a/reactivex/operators/_concatmap.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Callable, TypeVar - -from reactivex import Observable, operators -from reactivex.typing import Mapper - -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") - - -def concatmap_( - project: Mapper[_T1, Observable[_T2]] -) -> Callable[[Observable[_T1]], Observable[_T2]]: - def _concat_map(source: Observable[_T1]) -> Observable[_T2]: - return source.pipe(operators.map(project), operators.merge(max_concurrent=1)) - - return _concat_map - - -__all__ = ["concatmap_"] From 6c87398dd8286568a942332611caf63a807b1bd3 Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 30 Dec 2022 17:44:38 +0800 Subject: [PATCH 08/27] Simplify definition of concat_map --- reactivex/operators/__init__.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 84db2fbc8..61f82b526 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -489,17 +489,7 @@ def concat_map( """ - def concatmap_( - project: Mapper[_T1, Observable[_T2]] - ) -> Callable[[Observable[_T1]], Observable[_T2]]: - def _concat_map(source: Observable[_T1]) -> Observable[_T2]: - return source.pipe( - operators.map(project), operators.merge(max_concurrent=1) - ) - - return _concat_map - - return concatmap_(project) + return compose(map(project), merge(max_concurrent=1)) def contains( From d0c24d638011dc4b714a831cd8f43569bd01c18d Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 30 Dec 2022 17:45:21 +0800 Subject: [PATCH 09/27] Remove unused import --- reactivex/operators/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 61f82b526..43a126fda 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -26,7 +26,6 @@ Observable, abc, compose, - operators, typing, ) from reactivex.internal.basic import identity From 80df6cf4118cc131ebb7a39be5266a337d524d4b Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 22 Dec 2022 15:59:55 +0800 Subject: [PATCH 10/27] Initial content --- docs/index.rst | 1 + docs/testing.rst | 184 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 docs/testing.rst diff --git a/docs/index.rst b/docs/index.rst index ee37f97f1..8c2468767 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ operators in Python. installation rationale get_started + testing migration operators additional_reading diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 000000000..b25b90271 --- /dev/null +++ b/docs/testing.rst @@ -0,0 +1,184 @@ +Testing +------- + +Using the tools provided in `reactivex.testing`, it is possible to create tests for +your own observables, custom operators and subscriptions. + +Additionally, tests can be used to help understand the behaviors of existing operators. + +Basic example +............. + +.. code:: python + + # Import the testing tools + from reactivex.testing import ReactiveTest, TestScheduler + from reactivex import operators + + # setting up aliases for more concise code + on_next = ReactiveTest.on_next + on_error = ReactiveTest.on_error + on_completed = ReactiveTest.on_completed + subscribe = ReactiveTest.subscribe + + + # This assumes that you are using pytest but unittest or others would work just as well + + from reactivex.testing import ReactiveTest, TestScheduler + from reactivex import operators + + on_next = ReactiveTest.on_next + on_error = ReactiveTest.on_error + on_completed = ReactiveTest.on_completed + subscribe = ReactiveTest.subscribe + + + def test_double(): + # Create a scheduler + scheduler = TestScheduler() + # Define one or more source + source = scheduler.create_hot_observable( + on_next(250, 3), + on_next(350, 5), + ) + + # Define how the observable/operator is used on the source + def create(): + return source.pipe(operators.map(lambda x: 2 * x)) + + # trigger subscription and record emissions + results = scheduler.start(create) + + # check the messages and potentially subscriptions + assert results.messages == [ + on_next(250, 6), + on_next(350, 10), + ] + + +Testing a custom operator +......................... + +Whether your custom operator is created using a *composition* of operators +or with full control, you can easily test various situations and combinations + +.. _in_sequence_or_throw: + +.. code:: python + + def test_operator(): + # Code to test; takes a sequence of integers and passes through, + # unless they are not in sequence in which case it errors + def in_sequence_or_throw(): + return reactivex.compose( + operators.start_with(None), + operators.pairwise(), + operators.flat_map(lambda x: reactivex.of(x[1]) if ( + x[0] is None or x[1] == x[0] + 1 + ) else reactivex.throw(ValueError('Sequence error'))) + ) + ## End of code to test + + scheduler = TestScheduler() + # Create source + source = scheduler.create_cold_observable( + on_next(300, 1), on_next(400, 2), on_next(500, 3), on_completed(600) + ) + # Here is another way to create the same observable, + # as long as we set the correct scheduler + source = reactivex.from_marbles('------1-2-3-|', timespan=50, scheduler=scheduler) + # You can shorten the "create" function from the basic example to a lambda with no arguments + result = scheduler.start(lambda: source.pipe( + in_sequence_or_throw(), + )) + assert result.messages == [ + on_next(500, 1), on_next(600, 2), on_next(700, 3), on_completed(800) + ] + +Surprised about the timestamps (@500, @600, ...) for the result messages? Read below about the timeline + +Timeline +........ + +When `scheduler.start` is called, the test scheduler starts moving its virtual clock forward. +Some important timestamps are however hidden as defaults, as listed below. +These values can be modified using kwargs in the `scheduler.start(...)` call: + +1. ``created`` [100]: When is the observable created. +That is when the ``create`` function seen in the basic example, or the lambda above is called. +2. `subscribed` [200]: When does the subscription occur. +This explains the above emission timestamps: +consider the first emission @500; given that we are using a cold observable, +and subscribe to it at 200, the "source"'s timeline starts at 200 and only 300 ticks later, it emits. +1. `disposed` [1000]: When the subscription is disposed + +Keep the following in mind when modifying these values: + +1. Do not use `0` as values since the code ignores that +2. If you change `subscribed` to be lower than 100, you need to change `created` as well +otherwise nothing will happen. + + +Testing an observable factory +............................. + +An observable created from `Observable(subscribe)` can be just as easily tested. +Let's use this example to additionally test a disposal case + +.. code:: python + def test_my_observable_factory(): + from reactivex.disposable import Disposable, CompositeDisposable + a = 42 + def factory(observer: Observer, scheduler=None): + def increment(): + nonlocal a + a += 1 + sub = Disposable(action=increment) + return CompositeDisposable( + sub, + reactivex.timer(20, scheduler=scheduler).subscribe(observer) + ) + + scheduler = TestScheduler() + result = scheduler.start(lambda: Observable(factory)) + assert result.messages == [ + on_next(220, 0), + on_completed(220) + ] + assert a == 43 + + +Testing errors +.............. + +Going back to the in_sequence_or_throw_ operator, we did not test the error case + +.. code:: python + + def test_in_sequence_or_throw_error(): + scheduler = TestScheduler() + source = reactivex.from_marbles('--1-4-3-', timespan=50, scheduler=scheduler) + result = scheduler.start(lambda: source.pipe( + in_sequence_or_throw(), + ), created=1, subscribed=30) + + assert result.messages == [ + on_next(30+100, 1), + on_error(230, ValueError('Sequence error')) + ] + # Often it's better not to test the exact exception; we can test a specific emit as follows: + message, err = result.messages + assert message.time == 130 + assert err.time == 230 + assert message.value.kind == 'N' # Notification + assert err.value.kind == 'E' # E for errors + assert message.value.value == 1 + assert type(err.value.exception) == ValueError # look at .exception for errors + + +Testing subscriptions, multiple observables, hot observables +.............................................. + +``scheduler.start`` only allows for a single subscription. +Some cases like e.g. `operators.partition` require more. +The examples below showcase some less commonly needed testing tools. From 824d148fde716e9d149e91240b3757b74d8e8b80 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 22 Dec 2022 16:29:19 +0800 Subject: [PATCH 11/27] Draft 1 --- docs/testing.rst | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index b25b90271..54981f969 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -182,3 +182,78 @@ Testing subscriptions, multiple observables, hot observables ``scheduler.start`` only allows for a single subscription. Some cases like e.g. `operators.partition` require more. The examples below showcase some less commonly needed testing tools. + +.. code:: python + + def test_multiple(): + scheduler = TestScheduler() + source = reactivex.from_marbles('-1-4-3-|', timespan=50, scheduler=scheduler) + odd, even = source.pipe( + operators.partition(lambda x: x % 2), + ) + steven = scheduler.create_observer() + todd = scheduler.create_observer() + + even.subscribe(steven) + odd.subscribe(todd) + + # Note! Since it's not "start" which creates the subscription, they actually occur at t=0 + scheduler.start() + + assert steven.messages == [ + on_next(150, 4), + on_completed(350) + ] + assert steven.messages == [ + on_next(150, 4), + on_completed(350) + ] + + +.. code:: python + + from reactivex.testing.subscription import Subscription + def test_subscriptions(): + scheduler = TestScheduler() + source = scheduler.create_cold_observable() # "infinite" + subs = [] + shared = source.pipe( + operators.share() + ) + """first sub""" + scheduler.schedule_relative(200, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) + # second sub, should not sub to source itself + scheduler.schedule_relative(300, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) + scheduler.schedule_relative(500, lambda *_: subs[1].dispose()) + scheduler.schedule_relative(600, lambda *_: subs[0].dispose()) + """end first sub""" + # no existing sub should sub again onto source - we never dispose of it + scheduler.schedule_relative(900, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) + + scheduler.start() + # Check that the submissions on the source are as expected + assert source.subscriptions == [ + Subscription(200, 600), + Subscription(900), # represents an infinite subscription + ] + + +.. code:: python + + def test_hot(): + scheduler = TestScheduler() + # hot starts at 0 but sub starts at 200 so we'll miss 190 + source = scheduler.create_hot_observable( + on_next(190, 5), + on_next(300, 42), + on_completed(500) + ) + result = scheduler.start(lambda: source.pipe( + operators.to_marbles(timespan=20, scheduler=scheduler) + )) + + message = result.messages[0] + # sub starts at 200 and we emit at 300 - since this is a hot observable, aka 5 ticks of 20 (timespan=20 in to_marbles) + # then we get the 42 emit and then blank until 500, so 10 ticks*20 + assert message.value.value == '-----(42)----------|' + From 6fcc7bca1e62504ced3d90bbfa9c4293a495564c Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 22 Dec 2022 16:48:22 +0800 Subject: [PATCH 12/27] Draft 1.1 --- docs/testing.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index 54981f969..c96ade676 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -257,3 +257,45 @@ The examples below showcase some less commonly needed testing tools. # then we get the 42 emit and then blank until 500, so 10 ticks*20 assert message.value.value == '-----(42)----------|' + +Gotchas +....... + +Directly using observables in code +********************************** + +If your code creates observables directly in the code you wish to test e.g. `timeout = reactivex.timer(3)` +you will not be able to test properly as it will *actually* attempt to wait 3 real life seconds. + +Some suggestions: + +.. code:: python + + # Difficult to test because reactivex.timer is real time + def do_or_timeout(doer_observable: Observable[int]): + reactivex.merge( + doer_observable, + reactivex.timer(5).pipe( + operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) + ) + ) + + # option 1: accept scheduler as arg, and pass the TestScheduler + def do_or_timeout(doer_observable: Observable[int], scheduler=None): + reactivex.merge( + doer_observable, + reactivex.timer(5.0, scheduler=scheduler).pipe( + operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) + ) + ) + + # option 2: dependency injection: optional timeout + def do_or_timeout(doer_observable: Observable[int], timeout=None): + timeout = timeout or reactivex.timer(5.0) + reactivex.merge( + doer_observable, + timeout.pipe( + operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) + ) + ) + From a79f969e05aa501297c891fac20be33742c4cb91 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 22 Dec 2022 17:03:29 +0800 Subject: [PATCH 13/27] Minor fixes --- docs/testing.rst | 53 +++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index c96ade676..f77f8584c 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -11,6 +11,7 @@ Basic example .. code:: python + # This assumes that you are using pytest but unittest or others would work just as well # Import the testing tools from reactivex.testing import ReactiveTest, TestScheduler from reactivex import operators @@ -19,19 +20,6 @@ Basic example on_next = ReactiveTest.on_next on_error = ReactiveTest.on_error on_completed = ReactiveTest.on_completed - subscribe = ReactiveTest.subscribe - - - # This assumes that you are using pytest but unittest or others would work just as well - - from reactivex.testing import ReactiveTest, TestScheduler - from reactivex import operators - - on_next = ReactiveTest.on_next - on_error = ReactiveTest.on_error - on_completed = ReactiveTest.on_completed - subscribe = ReactiveTest.subscribe - def test_double(): # Create a scheduler @@ -95,37 +83,39 @@ or with full control, you can easily test various situations and combinations on_next(500, 1), on_next(600, 2), on_next(700, 3), on_completed(800) ] -Surprised about the timestamps (@500, @600, ...) for the result messages? Read below about the timeline +Surprised about the timestamps (@500, @600, ...) for the result messages? +Then read below about the timeline. Timeline ........ -When `scheduler.start` is called, the test scheduler starts moving its virtual clock forward. +When ``scheduler.start`` is called, the test scheduler starts moving its virtual clock forward. Some important timestamps are however hidden as defaults, as listed below. -These values can be modified using kwargs in the `scheduler.start(...)` call: +These values can be modified using kwargs in the ``scheduler.start(...)`` call: 1. ``created`` [100]: When is the observable created. -That is when the ``create`` function seen in the basic example, or the lambda above is called. -2. `subscribed` [200]: When does the subscription occur. -This explains the above emission timestamps: -consider the first emission @500; given that we are using a cold observable, -and subscribe to it at 200, the "source"'s timeline starts at 200 and only 300 ticks later, it emits. -1. `disposed` [1000]: When the subscription is disposed + That is when the ``create`` function seen in the basic example. +2. ``subscribed`` [200]: When does the subscription occur. + This explains the above emission timestamps: + consider the first emission @500; given that we are using a cold observable, + and subscribe to it at 200, the "source"'s timeline starts at 200 and only 300 ticks later, it emits. +3. ``disposed`` [1000]: When the subscription is disposed Keep the following in mind when modifying these values: 1. Do not use `0` as values since the code ignores that -2. If you change `subscribed` to be lower than 100, you need to change `created` as well -otherwise nothing will happen. +2. If you change ``subscribed`` to be lower than 100, you need to change ``created`` as well + otherwise nothing will happen. Testing an observable factory ............................. An observable created from `Observable(subscribe)` can be just as easily tested. -Let's use this example to additionally test a disposal case +Let's use this example to additionally test a disposal case. .. code:: python + def test_my_observable_factory(): from reactivex.disposable import Disposable, CompositeDisposable a = 42 @@ -151,7 +141,8 @@ Let's use this example to additionally test a disposal case Testing errors .............. -Going back to the in_sequence_or_throw_ operator, we did not test the error case +Going back to the in_sequence_or_throw_ operator, we did not test the error case; +Let's remedy that below. .. code:: python @@ -177,7 +168,7 @@ Going back to the in_sequence_or_throw_ operator, we did not test the error case Testing subscriptions, multiple observables, hot observables -.............................................. +............................................................ ``scheduler.start`` only allows for a single subscription. Some cases like e.g. `operators.partition` require more. @@ -204,8 +195,9 @@ The examples below showcase some less commonly needed testing tools. on_next(150, 4), on_completed(350) ] - assert steven.messages == [ - on_next(150, 4), + assert todd.messages == [ + on_next(50, 1), + on_next(250, 3), on_completed(350) ] @@ -253,7 +245,8 @@ The examples below showcase some less commonly needed testing tools. )) message = result.messages[0] - # sub starts at 200 and we emit at 300 - since this is a hot observable, aka 5 ticks of 20 (timespan=20 in to_marbles) + # sub starts at 200 and we emit at 300 - since this is a hot observable, + # aka 5 ticks of 20 (timespan=20 in to_marbles) # then we get the 42 emit and then blank until 500, so 10 ticks*20 assert message.value.value == '-----(42)----------|' From d82b7ab6e5d2aaf7171eb68c2d556ce6699fec4f Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 23 Dec 2022 03:24:22 +0800 Subject: [PATCH 14/27] Remove gotchas Fix comment on test_hot marbles --- docs/testing.rst | 51 ++++++------------------------------------------ 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index f77f8584c..745240ffa 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -245,50 +245,11 @@ The examples below showcase some less commonly needed testing tools. )) message = result.messages[0] - # sub starts at 200 and we emit at 300 - since this is a hot observable, - # aka 5 ticks of 20 (timespan=20 in to_marbles) - # then we get the 42 emit and then blank until 500, so 10 ticks*20 + # sub starts at 200 and we emit at 300 + # since `source` is a hot observable, the emission @190 will not be caught + # instead on our subscription, it will look like emission at 300-200=100 ticks + # aka 5 "-" each representing 20 ticks (timespan=20 in to_marbles) + # then the 42 is received emit + # and then nothing for another 500-300 ticks 500, so 10 "-" before complete assert message.value.value == '-----(42)----------|' - -Gotchas -....... - -Directly using observables in code -********************************** - -If your code creates observables directly in the code you wish to test e.g. `timeout = reactivex.timer(3)` -you will not be able to test properly as it will *actually* attempt to wait 3 real life seconds. - -Some suggestions: - -.. code:: python - - # Difficult to test because reactivex.timer is real time - def do_or_timeout(doer_observable: Observable[int]): - reactivex.merge( - doer_observable, - reactivex.timer(5).pipe( - operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) - ) - ) - - # option 1: accept scheduler as arg, and pass the TestScheduler - def do_or_timeout(doer_observable: Observable[int], scheduler=None): - reactivex.merge( - doer_observable, - reactivex.timer(5.0, scheduler=scheduler).pipe( - operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) - ) - ) - - # option 2: dependency injection: optional timeout - def do_or_timeout(doer_observable: Observable[int], timeout=None): - timeout = timeout or reactivex.timer(5.0) - reactivex.merge( - doer_observable, - timeout.pipe( - operators.flat_map(lambda _: reactivex.throw(Exception('abc'))) - ) - ) - From cde46734ed68bde62df1409b0b5b1f154e2607f2 Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 23 Dec 2022 03:28:11 +0800 Subject: [PATCH 15/27] Fix comment on marbles --- docs/testing.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 745240ffa..15bae77f2 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -245,11 +245,12 @@ The examples below showcase some less commonly needed testing tools. )) message = result.messages[0] - # sub starts at 200 and we emit at 300 + # subscriptions starts at 200 # since `source` is a hot observable, the emission @190 will not be caught - # instead on our subscription, it will look like emission at 300-200=100 ticks + # the next emit is at 300 ticks, + # which, on our subscription, will look like 300-200=100 ticks # aka 5 "-" each representing 20 ticks (timespan=20 in to_marbles) - # then the 42 is received emit + # then the 42 is received # and then nothing for another 500-300 ticks 500, so 10 "-" before complete assert message.value.value == '-----(42)----------|' From 5a0080c5fd28a09629fe9d720d6add357242b251 Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 23 Dec 2022 06:02:23 +0800 Subject: [PATCH 16/27] Add marble testing section --- docs/testing.rst | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 15bae77f2..a4c27e890 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -107,12 +107,40 @@ Keep the following in mind when modifying these values: 2. If you change ``subscribed`` to be lower than 100, you need to change ``created`` as well otherwise nothing will happen. +An alternative using marbles +............................ + +As we saw in the previous section, we can use `reactivex.from_marbles` +to create observables for our tests. + +An example of using `to_marbles` for the assertion is shown in test_hot_ + +There is a simplified flow available in `reactivex.testing.marbles` and here's an example: + +.. code:: python + + def test_start_with(): + from reactivex.testing.marbles import marbles_testing + with marbles_testing() as (start, cold, hot, exp): + source = cold('------1-2-3-|') + outcome = exp('a-----1-2-3-|', {"a": None}) # can use lookups if needed + obs = source.pipe( + operators.start_with(None) + ) + # Note that start accepts the observable directly, + # without the need for a "create" function + results = start(obs) + + assert results == outcome + +This method makes for very quick to write, and easy to read, tests. + Testing an observable factory ............................. An observable created from `Observable(subscribe)` can be just as easily tested. -Let's use this example to additionally test a disposal case. +Let's use this example to additionally test a Disposable case. .. code:: python @@ -157,7 +185,9 @@ Let's remedy that below. on_next(30+100, 1), on_error(230, ValueError('Sequence error')) ] - # Often it's better not to test the exact exception; we can test a specific emit as follows: + # At times it's better not to test the exact exception, + # maybe its message changes with time or other reasons + # We can test a specific notification's details as follows: message, err = result.messages assert message.time == 130 assert err.time == 230 @@ -229,6 +259,7 @@ The examples below showcase some less commonly needed testing tools. Subscription(900), # represents an infinite subscription ] +.. _test_hot: .. code:: python @@ -245,12 +276,12 @@ The examples below showcase some less commonly needed testing tools. )) message = result.messages[0] - # subscriptions starts at 200 - # since `source` is a hot observable, the emission @190 will not be caught - # the next emit is at 300 ticks, - # which, on our subscription, will look like 300-200=100 ticks - # aka 5 "-" each representing 20 ticks (timespan=20 in to_marbles) + # the subscription starts at 200; + # since `source` is a hot observable, the notification @190 will not be caught + # the next notification is at 300 ticks, + # which, on our subscription, will show at 100 ticks (300-200 from subscribed) + # or 5 "-" each representing 20 ticks (timespan=20 in to_marbles) # then the 42 is received - # and then nothing for another 500-300 ticks 500, so 10 "-" before complete + # and then nothing for another 200 ticks, so 10 "-" before complete assert message.value.value == '-----(42)----------|' From 15f09951247ba461151ef3e4d09c9cf31fea3d72 Mon Sep 17 00:00:00 2001 From: matiboy Date: Tue, 6 Dec 2022 10:09:35 +0800 Subject: [PATCH 17/27] [DOCS] Getting started: fix example Fix example for custom operator which still uses v3's rx.pipe --- docs/get_started.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/get_started.rst b/docs/get_started.rst index cd9c4f370..e838cd588 100644 --- a/docs/get_started.rst +++ b/docs/get_started.rst @@ -165,7 +165,8 @@ other operators, then the implementation is straightforward, thanks to the from reactivex import operators as ops def length_more_than_5(): - return rx.pipe( + # In v4 rx.pipe has been renamed to `compose` + return reactivex.compose( ops.map(lambda s: len(s)), ops.filter(lambda i: i >= 5), ) From 78870b29201362ca026098b84f11d41d43c6ad61 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 22 Dec 2022 13:20:16 +0800 Subject: [PATCH 18/27] Fix `timer` in examples --- reactivex/operators/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 43a126fda..ca3868801 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -685,7 +685,7 @@ def delay_with_mapper( Examples: >>> # with mapper only - >>> res = source.delay_with_mapper(lambda x: Scheduler.timer(5.0)) + >>> res = source.delay_with_mapper(lambda x: reactivex.timer(5.0)) >>> # with delay and mapper >>> res = source.delay_with_mapper( reactivex.timer(2.0), lambda x: reactivex.timer(x) @@ -3690,7 +3690,7 @@ def throttle_with_mapper( another value within a computed throttle duration. Example: - >>> op = throttle_with_mapper(lambda x: rx.Scheduler.timer(x+x)) + >>> op = throttle_with_mapper(lambda x: reactivex.timer(x+x)) Args: throttle_duration_mapper: Mapper function to retrieve an From 8263b1e36231aeec956e825a991f3350ee3e74a7 Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 23 Dec 2022 04:50:09 +0800 Subject: [PATCH 19/27] Observable spelling --- reactivex/operators/__init__.py | 2 +- reactivex/operators/_partition.py | 2 +- reactivex/operators/_sequenceequal.py | 2 +- reactivex/operators/_timeout.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index ca3868801..cbf9b551f 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -3889,7 +3889,7 @@ def to_iterable() -> Callable[[Observable[_T]], Observable[List[_T]]]: There is also an alias called ``to_list``. Returns: - An operator function that takes an obserable source and + An operator function that takes an observable source and returns an observable sequence containing a single element with an iterable containing all the elements of the source sequence. """ diff --git a/reactivex/operators/_partition.py b/reactivex/operators/_partition.py index 2f005e7db..26b032966 100644 --- a/reactivex/operators/_partition.py +++ b/reactivex/operators/_partition.py @@ -23,7 +23,7 @@ def partition(source: Observable[_T]) -> List[Observable[_T]]: when the source completes. Args: - source: Source obserable to partition. + source: Source observable to partition. Returns: A list of observables. The first triggers when the diff --git a/reactivex/operators/_sequenceequal.py b/reactivex/operators/_sequenceequal.py index d79a531b8..a3019d6a7 100644 --- a/reactivex/operators/_sequenceequal.py +++ b/reactivex/operators/_sequenceequal.py @@ -31,7 +31,7 @@ def sequence_equal(source: Observable[_T]) -> Observable[bool]: ) Args: - source: Source obserable to compare. + source: Source observable to compare. Returns: An observable sequence that contains a single element which diff --git a/reactivex/operators/_timeout.py b/reactivex/operators/_timeout.py index 2e2d6e196..585f0b7b5 100644 --- a/reactivex/operators/_timeout.py +++ b/reactivex/operators/_timeout.py @@ -36,7 +36,7 @@ def timeout(source: Observable[_T]) -> Observable[_T]: source: Source observable to timeout Returns: - An obserable sequence switching to the other sequence in + An observable sequence switching to the other sequence in case of a timeout. """ From 26f927bf5f4ff84243e5197f405dfd64f9ceec0c Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 6 Nov 2022 22:56:10 +0100 Subject: [PATCH 20/27] Update pyright to 1.1.278 --- .pre-commit-config.yaml | 2 +- reactivex/operators/_distinct.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d24cee0dc..3d83fa772 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: language: node pass_filenames: false types: [python] - additional_dependencies: ["pyright@1.1.254"] + additional_dependencies: ["pyright@1.1.278"] repo: local - hooks: - id: mypy diff --git a/reactivex/operators/_distinct.py b/reactivex/operators/_distinct.py index 6c9cc07a5..09b863bde 100644 --- a/reactivex/operators/_distinct.py +++ b/reactivex/operators/_distinct.py @@ -32,7 +32,9 @@ def distinct_( key_mapper: Optional[typing.Mapper[_T, _TKey]] = None, comparer: Optional[typing.Comparer[_TKey]] = None, ) -> Callable[[Observable[_T]], Observable[_T]]: - comparer_ = comparer or default_comparer + comparer_: typing.Comparer[_TKey] = comparer or cast( + typing.Comparer[_TKey], default_comparer + ) def distinct(source: Observable[_T]) -> Observable[_T]: """Returns an observable sequence that contains only distinct From f64f8304df6c0ba1766f46b94e873c3896e8422f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 29 Dec 2022 11:00:23 +0100 Subject: [PATCH 21/27] Update to pyright v286 --- .pre-commit-config.yaml | 2 +- reactivex/operators/_distinct.py | 4 +--- reactivex/operators/_timestamp.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d83fa772..5122b45e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: language: node pass_filenames: false types: [python] - additional_dependencies: ["pyright@1.1.278"] + additional_dependencies: ["pyright@1.1.286"] repo: local - hooks: - id: mypy diff --git a/reactivex/operators/_distinct.py b/reactivex/operators/_distinct.py index 09b863bde..918d0ee78 100644 --- a/reactivex/operators/_distinct.py +++ b/reactivex/operators/_distinct.py @@ -32,9 +32,7 @@ def distinct_( key_mapper: Optional[typing.Mapper[_T, _TKey]] = None, comparer: Optional[typing.Comparer[_TKey]] = None, ) -> Callable[[Observable[_T]], Observable[_T]]: - comparer_: typing.Comparer[_TKey] = comparer or cast( - typing.Comparer[_TKey], default_comparer - ) + comparer_ = comparer or cast(typing.Comparer[_TKey], default_comparer) def distinct(source: Observable[_T]) -> Observable[_T]: """Returns an observable sequence that contains only distinct diff --git a/reactivex/operators/_timestamp.py b/reactivex/operators/_timestamp.py index 1d51b204f..2ae406078 100644 --- a/reactivex/operators/_timestamp.py +++ b/reactivex/operators/_timestamp.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import Any, Callable, Generic, Optional, TypeVar +from typing import Callable, Generic, Optional, TypeVar from reactivex import Observable, abc, defer, operators from reactivex.scheduler import TimeoutScheduler @@ -17,7 +17,7 @@ class Timestamp(Generic[_T]): def timestamp_( scheduler: Optional[abc.SchedulerBase] = None, ) -> Callable[[Observable[_T]], Observable[Timestamp[_T]]]: - def timestamp(source: Observable[Any]) -> Observable[Timestamp[_T]]: + def timestamp(source: Observable[_T]) -> Observable[Timestamp[_T]]: """Records the timestamp for each value in an observable sequence. Examples: From a10cc1cfd31c5fb1582207d44aaa53b920163f6e Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 29 Dec 2022 10:50:36 +0100 Subject: [PATCH 22/27] Add CI check for Python-3.11 - Update dependencies --- .github/workflows/python-package.yml | 2 +- poetry.lock | 523 +++++++++++++-------------- 2 files changed, 258 insertions(+), 267 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b3bd0c742..7e808d85d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, "3.10", pypy-3.8] + python-version: [3.7, 3.8, 3.9, "3.10", 3.11, pypy-3.8] runs-on: ${{ matrix.platform }} steps: diff --git a/poetry.lock b/poetry.lock index d6a228ebd..81f077545 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,50 +1,45 @@ -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "attrs" -version = "21.4.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests_no_zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] [[package]] name = "autoflake" -version = "1.4" +version = "1.7.8" description = "Removes unused imports and unused variables" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] -pyflakes = ">=1.1.0" +pyflakes = ">=1.1.0,<3" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [[package]] name = "black" -version = "22.1.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=1.1.0" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} @@ -56,11 +51,11 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "cfgv" @@ -72,22 +67,22 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -95,15 +90,15 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" -version = "6.3.2" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -130,7 +125,7 @@ yaml = ["PyYAML (>=3.10)"] [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false @@ -146,7 +141,7 @@ python-versions = "*" [[package]] name = "dunamai" -version = "1.9.0" +version = "1.15.0" description = "Dynamic version generation" category = "dev" optional = false @@ -156,6 +151,17 @@ python-versions = ">=3.5,<4.0" importlib-metadata = {version = ">=1.6.0", markers = "python_version < \"3.8\""} packaging = ">=20.9" +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "1.9.0" @@ -169,15 +175,15 @@ testing = ["pre-commit"] [[package]] name = "filelock" -version = "3.6.0" +version = "3.9.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -195,7 +201,7 @@ pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "identify" -version = "2.4.11" +version = "2.5.11" description = "File identification library for Python" category = "dev" optional = false @@ -206,7 +212,7 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false @@ -225,8 +231,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] [[package]] name = "iniconfig" @@ -238,17 +244,17 @@ python-versions = "*" [[package]] name = "isort" -version = "5.10.1" +version = "5.11.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.7.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "mccabe" @@ -286,42 +292,45 @@ python-versions = "*" [[package]] name = "nodeenv" -version = "1.6.0" +version = "1.7.0" description = "Node.js virtual environment builder" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[package.dependencies] +setuptools = "*" [[package]] name = "packaging" -version = "21.3" +version = "22.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.5.1" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -340,11 +349,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -352,8 +361,7 @@ identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "py" @@ -379,20 +387,9 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pyright" -version = "0.0.13" +version = "0.0.13.post0" description = "Command line wrapper for pyright" category = "dev" optional = false @@ -408,29 +405,28 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.0.1" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.18.1" +version = "0.18.3" description = "Pytest support for asyncio" category = "dev" optional = false @@ -441,7 +437,7 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] +testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-forked" @@ -474,7 +470,7 @@ setproctitle = ["setproctitle"] testing = ["filelock"] [[package]] -name = "pyyaml" +name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" @@ -483,37 +479,34 @@ python-versions = ">=3.6" [[package]] name = "requests" -version = "2.27.1" +version = "2.28.1" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" +name = "setuptools" +version = "65.6.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.7" -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" @@ -525,7 +518,7 @@ python-versions = ">=3.7" [[package]] name = "typed-ast" -version = "1.5.2" +version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -533,195 +526,193 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.2" +version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" -six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "zipp" -version = "3.7.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" -python-versions = ">= 3.7, < 3.11" -content-hash = "b7c131a52cd9cb1d14dadc1b8f1b0d9044ae592674cd9918f227b993a5474de9" +python-versions = ">= 3.7, < 4.0" +content-hash = "c9b5104ed99e8620363724beecc42b0253d00e7bc8262279e82cf20351027dcb" [metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] autoflake = [ - {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, + {file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"}, + {file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"}, ] black = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] coveralls = [ {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, ] distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] dunamai = [ - {file = "dunamai-1.9.0-py3-none-any.whl", hash = "sha256:89213a9c1176313cdfda7091a15c6d94be8c17b25e57a8c1e2ecf10d902b25d2"}, - {file = "dunamai-1.9.0.tar.gz", hash = "sha256:284dc9acbd3d6b749440332e46164f64b207c56eaf4af412d856cf9f08978932"}, + {file = "dunamai-1.15.0-py3-none-any.whl", hash = "sha256:646d7a6c07d0ec8412ac43c1a3546f1d1b52f3ed918f4bfbefe32439b49dec7e"}, + {file = "dunamai-1.15.0.tar.gz", hash = "sha256:d6088922df3226a8234c228803bb01902a83766d04c2cddb33d4dd3bf85ec9a5"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] identify = [ - {file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"}, - {file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"}, + {file = "identify-2.5.11-py2.py3-none-any.whl", hash = "sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db"}, + {file = "identify-2.5.11.tar.gz", hash = "sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, @@ -732,8 +723,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -766,28 +757,28 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] nodeenv = [ - {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, - {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, ] pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -801,21 +792,18 @@ pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] pyright = [ - {file = "pyright-0.0.13-py3-none-any.whl", hash = "sha256:520458c87dc456ed0d405a08a565066aaf5b76281d23861759cfaa3c73f5bda0"}, - {file = "pyright-0.0.13.tar.gz", hash = "sha256:ca97f9121927847d0aea5d2e757a0bf6d3c25c116c90a3ce6263b4167a12dfc4"}, + {file = "pyright-0.0.13.post0-py3-none-any.whl", hash = "sha256:47c29623f208226f4437bf02276475b816beff6f539ea166e3cade57d6717598"}, + {file = "pyright-0.0.13.post0.tar.gz", hash = "sha256:dda7602f3692f07fdff8c6d30d704f1b1ed20b5b3bb55c069599988d1ec9bd5a"}, ] pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, - {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-forked = [ {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, @@ -825,7 +813,7 @@ pytest-xdist = [ {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, ] -pyyaml = [ +PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -833,6 +821,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -861,60 +856,56 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +setuptools = [ + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, ] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typed-ast = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] virtualenv = [ - {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"}, - {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"}, + {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, + {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] From 5ddfde9f80edee6f44474242e1080e9cf2cb6ca7 Mon Sep 17 00:00:00 2001 From: Mat Date: Sat, 7 Jan 2023 14:17:28 +0800 Subject: [PATCH 23/27] Switchmap operator Minor docs amendments --- docs/operators.rst | 22 +-- docs/testing.rst | 85 +++++----- reactivex/operators/__init__.py | 39 +++++ tests/test_observable/test_switchmap.py | 196 ++++++++++++++++++++++++ 4 files changed, 293 insertions(+), 49 deletions(-) create mode 100644 tests/test_observable/test_switchmap.py diff --git a/docs/operators.rst b/docs/operators.rst index 63001ebf3..d15eb3c0a 100644 --- a/docs/operators.rst +++ b/docs/operators.rst @@ -26,16 +26,18 @@ Operator Description Transforming Observables ------------------------ -================================================ ================================================ -Operator Description -================================================ ================================================ -:func:`buffer ` Periodically gather items from an Observable into bundles and emit these bundles rather than emitting the items one at a time. -:func:`flat_map ` Transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable. -:func:`group_by ` Divide an Observable into a set of Observables that each emit a different group of items from the original Observable, organized by key. -:func:`map ` Transform the items emitted by an Observable by applying a function to each item. -:func:`scan ` Apply a function to each item emitted by an Observable, sequentially, and emit each successive value. -:func:`window ` Periodically subdivide items from an Observable into Observable windows and emit these windows rather than emitting the items one at a time. -================================================ ================================================ +================================================ ================================================ +Operator Description +================================================ ================================================ +:func:`buffer ` Periodically gather items from an Observable into bundles and emit these bundles rather than emitting the items one at a time. +:func:`flat_map ` Transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable. +:func:`concat_map ` Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next. +:func:`switch_map ` Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable. +:func:`group_by ` Divide an Observable into a set of Observables that each emit a different group of items from the original Observable, organized by key. +:func:`map ` Transform the items emitted by an Observable by applying a function to each item. +:func:`scan ` Apply a function to each item emitted by an Observable, sequentially, and emit each successive value. +:func:`window ` Periodically subdivide items from an Observable into Observable windows and emit these windows rather than emitting the items one at a time. +================================================ ================================================ Filtering Observables ---------------------- diff --git a/docs/testing.rst b/docs/testing.rst index a4c27e890..f51bb72a3 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -16,18 +16,13 @@ Basic example from reactivex.testing import ReactiveTest, TestScheduler from reactivex import operators - # setting up aliases for more concise code - on_next = ReactiveTest.on_next - on_error = ReactiveTest.on_error - on_completed = ReactiveTest.on_completed - def test_double(): # Create a scheduler scheduler = TestScheduler() # Define one or more source source = scheduler.create_hot_observable( - on_next(250, 3), - on_next(350, 5), + ReactiveTest.on_next(250, 3), + ReactiveTest.on_next(350, 5), ) # Define how the observable/operator is used on the source @@ -39,8 +34,8 @@ Basic example # check the messages and potentially subscriptions assert results.messages == [ - on_next(250, 6), - on_next(350, 10), + ReactiveTest.on_next(250, 6), + ReactiveTest.on_next(350, 10), ] @@ -53,6 +48,10 @@ or with full control, you can easily test various situations and combinations .. _in_sequence_or_throw: .. code:: python + # setting up aliases for more concise code + on_next = ReactiveTest.on_next + on_error = ReactiveTest.on_error + on_completed = ReactiveTest.on_completed def test_operator(): # Code to test; takes a sequence of integers and passes through, @@ -72,9 +71,8 @@ or with full control, you can easily test various situations and combinations source = scheduler.create_cold_observable( on_next(300, 1), on_next(400, 2), on_next(500, 3), on_completed(600) ) - # Here is another way to create the same observable, - # as long as we set the correct scheduler - source = reactivex.from_marbles('------1-2-3-|', timespan=50, scheduler=scheduler) + # Here is another way to create the same observable + source = reactivex.from_marbles('------1-2-3-|', timespan=50) # You can shorten the "create" function from the basic example to a lambda with no arguments result = scheduler.start(lambda: source.pipe( in_sequence_or_throw(), @@ -91,20 +89,20 @@ Timeline When ``scheduler.start`` is called, the test scheduler starts moving its virtual clock forward. Some important timestamps are however hidden as defaults, as listed below. -These values can be modified using kwargs in the ``scheduler.start(...)`` call: +These values can be modified using `kwargs` in the ``scheduler.start(...)`` call: 1. ``created`` [100]: When is the observable created. - That is when the ``create`` function seen in the basic example. + That is when the ``create`` function seen in the basic example is called. 2. ``subscribed`` [200]: When does the subscription occur. This explains the above emission timestamps: consider the first emission @500; given that we are using a cold observable, - and subscribe to it at 200, the "source"'s timeline starts at 200 and only 300 ticks later, it emits. + and subscribe to it at 200, the `source`'s timeline starts at 200 and only 300 ticks later, it emits. 3. ``disposed`` [1000]: When the subscription is disposed -Keep the following in mind when modifying these values: +Gotchas when modifying these values: -1. Do not use `0` as values since the code ignores that -2. If you change ``subscribed`` to be lower than 100, you need to change ``created`` as well +1. Do not use `0` as values for created/subscribed since the code would ignore it. +2. If you change ``subscribed`` to be lower than 100, you need to change ``created`` as well, otherwise nothing will happen. An alternative using marbles @@ -134,13 +132,17 @@ There is a simplified flow available in `reactivex.testing.marbles` and here's a assert results == outcome This method makes for very quick to write, and easy to read, tests. +At this moment however, it does not allow for testing subscriptions. Testing an observable factory ............................. -An observable created from `Observable(subscribe)` can be just as easily tested. -Let's use this example to additionally test a Disposable case. +An observable created directly from :class:`Observable ` +can be just as easily tested. + +In this example, we will additionally test a case where a +:class:`Disposable ` is used. .. code:: python @@ -163,7 +165,7 @@ Let's use this example to additionally test a Disposable case. on_next(220, 0), on_completed(220) ] - assert a == 43 + assert a == 43 # shows that our Disposable's action was as expected Testing errors @@ -188,20 +190,20 @@ Let's remedy that below. # At times it's better not to test the exact exception, # maybe its message changes with time or other reasons # We can test a specific notification's details as follows: - message, err = result.messages - assert message.time == 130 - assert err.time == 230 - assert message.value.kind == 'N' # Notification - assert err.value.kind == 'E' # E for errors - assert message.value.value == 1 - assert type(err.value.exception) == ValueError # look at .exception for errors + first_notification, error_notification = result.messages + assert first_notification.time == 130 + assert error_notification.time == 230 + assert first_notification.value.kind == 'N' # Notification + assert error_notification.value.kind == 'E' # E for errors + assert first_notification.value.value == 1 + assert type(error_notification.value.exception) == ValueError # look at .exception for errors Testing subscriptions, multiple observables, hot observables ............................................................ ``scheduler.start`` only allows for a single subscription. -Some cases like e.g. `operators.partition` require more. +Some cases like e.g. ``operators.partition`` require more. The examples below showcase some less commonly needed testing tools. .. code:: python @@ -218,7 +220,9 @@ The examples below showcase some less commonly needed testing tools. even.subscribe(steven) odd.subscribe(todd) - # Note! Since it's not "start" which creates the subscription, they actually occur at t=0 + # Note! Since the subscription is not created within + # `scheduler.start` below, the usual `subscribed` delay of t=200 + # is not in effect. The subscriptions therefore occur at t=0 scheduler.start() assert steven.messages == [ @@ -242,20 +246,23 @@ The examples below showcase some less commonly needed testing tools. shared = source.pipe( operators.share() ) - """first sub""" + # Creating our story: + # first sub is set to occur at t=200; this creates a sub on source scheduler.schedule_relative(200, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) - # second sub, should not sub to source itself + # second sub does not create a new sub on source, due to the `share` operator scheduler.schedule_relative(300, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) + # second sub ends scheduler.schedule_relative(500, lambda *_: subs[1].dispose()) + # first sub ends… and since there is no sub remaining, the only sub on source should be disposed too scheduler.schedule_relative(600, lambda *_: subs[0].dispose()) - """end first sub""" - # no existing sub should sub again onto source - we never dispose of it + # no existing sub on source, therefore this will create a new one + # we never dispose of it; we will test that infinite sub in the assertions scheduler.schedule_relative(900, lambda *_: subs.append(shared.subscribe(scheduler=scheduler))) scheduler.start() # Check that the submissions on the source are as expected assert source.subscriptions == [ - Subscription(200, 600), + Subscription(200, 600), # only one sub from 200 to 600 Subscription(900), # represents an infinite subscription ] @@ -279,9 +286,9 @@ The examples below showcase some less commonly needed testing tools. # the subscription starts at 200; # since `source` is a hot observable, the notification @190 will not be caught # the next notification is at 300 ticks, - # which, on our subscription, will show at 100 ticks (300-200 from subscribed) - # or 5 "-" each representing 20 ticks (timespan=20 in to_marbles) - # then the 42 is received - # and then nothing for another 200 ticks, so 10 "-" before complete + # which, on our subscription, will show at 100 ticks (300-200 from subscription delay) + # or 5 "-" each representing 20 ticks (timespan=20 in `to_marbles`). + # Then the "42" notification is received + # and then nothing for another 200 ticks, which is equal to 10 "-", before complete assert message.value.value == '-----(42)----------|' diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index cbf9b551f..bc8a23466 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -3362,6 +3362,45 @@ def switch_latest() -> Callable[ return switch_latest_() +def switch_map( + project: Callable[[_T1, int], Observable[_T2]] +) -> Callable[[Observable[_T1]], Observable[_T2]]: + """Projects each source value to an Observable which is merged in + the output Observable, emitting values only from the most recently + projected Observable. + + + .. marble:: + :alt: switch_map + + ---a----------b-------c---------------| + [ switch_map(x,i: x*i---x*i---x*i|) ] + ---a---a---a--bb---bb-ccc---ccc---ccc-| + + Examples: + >>> op = switch_map(lambda x, i: reactivex.timer(1.0).pipe(map(x*i))) + + Args: + project: Projecting function which takes the outer observable value + and the emission index and emits the inner observable + + Returns: + An operator function that maps each value to the inner observable + and emits its values in order, emitting values only from the + most recently projected Observable. + + + If an inner observable complete, the resulting sequence does *not* + complete. + If an inner observable errors, the resulting sequence errors as well. + If the outer observable completes/errors, the resulting sequence + completes/errors. + + """ + + return compose(map_indexed(project), switch_latest()) + + def take(count: int) -> Callable[[Observable[_T]], Observable[_T]]: """Returns a specified number of contiguous elements from the start of an observable sequence. diff --git a/tests/test_observable/test_switchmap.py b/tests/test_observable/test_switchmap.py new file mode 100644 index 000000000..6cf93f8dd --- /dev/null +++ b/tests/test_observable/test_switchmap.py @@ -0,0 +1,196 @@ +import unittest + +from reactivex import operators as ops, interval +from reactivex.testing import ReactiveTest, TestScheduler +from reactivex.testing.subscription import Subscription + +on_next = ReactiveTest.on_next +on_completed = ReactiveTest.on_completed +on_error = ReactiveTest.on_error +subscribe = ReactiveTest.subscribe +subscribed = ReactiveTest.subscribed +disposed = ReactiveTest.disposed +created = ReactiveTest.created + + +class TestSwitchMap(unittest.TestCase): + def test_switch_map_uses_index(self): + scheduler = TestScheduler() + xs = scheduler.create_hot_observable( + on_next( + 300, + 'a' + ), + on_next( + 400, + 'b' + ), + on_next( + 500, + 'c' + ), + ) + + def create_inner(x: str, i: int): + def create_changing(j: int): + return (i, j, x) + return interval(20).pipe(ops.map(create_changing)) + + def create(): + return xs.pipe(ops.switch_map( + project=create_inner + )) + + results = scheduler.start(create, disposed=580) + # (i, j, x): i is the index of the outer emit; + # j is the value of the inner interval; + # x is the value of the outer emission + assert results.messages == [ + on_next(320, (0, 0, 'a')), + on_next(340, (0, 1, 'a')), + on_next(360, (0, 2, 'a')), + on_next(380, (0, 3, 'a')), + on_next(420, (1, 0, 'b')), + on_next(440, (1, 1, 'b')), + on_next(460, (1, 2, 'b')), + on_next(480, (1, 3, 'b')), + on_next(520, (2, 0, 'c')), + on_next(540, (2, 1, 'c')), + on_next(560, (2, 2, 'c')), + ] + assert xs.subscriptions == [ + Subscription(200, 580) + ] + + def test_switch_map_inner_throws(self): + """Inner throwing causes outer to throw""" + ex = "ex" + scheduler = TestScheduler() + sources = [ + scheduler.create_cold_observable(on_next(100, 'a'), on_next(300, 'aa')), + scheduler.create_cold_observable(on_next(50, 'b'), on_error(120, ex)), + scheduler.create_cold_observable(on_next(50, 'wont happen'), on_error(120, "no")), + ] + xs = scheduler.create_hot_observable( + on_next( + 250, + 0, + ), + on_next( + 400, + 1 + ), + on_next( + 550, + 2, + ), + ) + + def create_inner(x: int, _i: int): + return sources[x] + + def create(): + return xs.pipe(ops.switch_map(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(350, 'a'), + on_next(450, 'b'), + on_error(520, ex), + ] + assert sources[0].subscriptions == [ + Subscription(250, 400) + ] + assert sources[1].subscriptions == [ + Subscription(400, 520) + ] + assert sources[2].subscriptions == [] + + def test_switch_map_outer_throws(self): + """Outer throwing unsubscribes from all""" + ex = "ABC" + scheduler = TestScheduler() + sources = [ + scheduler.create_cold_observable(on_next(100, 'a'), on_next(300, 'aa')), + scheduler.create_cold_observable(on_next(50, 'b'), on_error(120, ex)), + scheduler.create_cold_observable(on_next(50, 'wont happen'), on_error(120, "no")), + ] + xs = scheduler.create_hot_observable( + on_next( + 250, + 0, + ), + on_next( + 400, + 1 + ), + on_error(430, ex), + ) + + def create_inner(x: int, _i: int): + return sources[x] + + def create(): + return xs.pipe(ops.switch_map(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(350, 'a'), + on_error(430, ex), + ] + assert sources[0].subscriptions == [ + Subscription(250, 400) + ] + assert sources[1].subscriptions == [ + Subscription(400, 430) + ] + assert sources[2].subscriptions == [] + + def test_switch_map_no_inner(self): + scheduler = TestScheduler() + xs = scheduler.create_hot_observable(on_completed(500)) + # Fake inner which should never be subscribed to + sources = [ + scheduler.create_cold_observable(on_next(20, 2)) + ] + + def create_inner(_x: int, i: int): + return sources[i] + + def create(): + return xs.pipe(ops.switch_map(create_inner)) + + results = scheduler.start(create) + assert results.messages == [on_completed(500)] + assert xs.subscriptions == [Subscription(200, 500)] + assert sources[0].subscriptions == [] + + def test_switch_map_inner_completes(self): + """Inner completions do not affect outer""" + scheduler = TestScheduler() + xs = scheduler.create_hot_observable( + on_next( + 300, + 'd' + ), + on_next( + 330, + 'f' + ), + on_completed(540), + ) + + def create_inner(x: str, i: int): + """An observable which will complete after 40 ticks""" + return interval(20).pipe(ops.map(lambda j: (i, j, x)), ops.take(2)) + + def create(): + return xs.pipe(ops.switch_map(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(320, (0,0,'d')), + on_next(350, (1,0,'f')), + on_next(370, (1,1,'f')), # here the current inner is unsubscribed but not the outer + on_completed(540), # only outer completion affects + ] From fdfd23834fc069a06a9592a975427b0c616ff32e Mon Sep 17 00:00:00 2001 From: Mat Date: Sat, 7 Jan 2023 14:23:59 +0800 Subject: [PATCH 24/27] Black --- reactivex/operators/__init__.py | 4 +- tests/test_observable/test_switchmap.py | 125 ++++++++++-------------- 2 files changed, 51 insertions(+), 78 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index bc8a23466..02d678c72 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -3389,11 +3389,11 @@ def switch_map( and emits its values in order, emitting values only from the most recently projected Observable. - + If an inner observable complete, the resulting sequence does *not* complete. If an inner observable errors, the resulting sequence errors as well. - If the outer observable completes/errors, the resulting sequence + If the outer observable completes/errors, the resulting sequence completes/errors. """ diff --git a/tests/test_observable/test_switchmap.py b/tests/test_observable/test_switchmap.py index 6cf93f8dd..ba33eba17 100644 --- a/tests/test_observable/test_switchmap.py +++ b/tests/test_observable/test_switchmap.py @@ -1,6 +1,7 @@ import unittest -from reactivex import operators as ops, interval +from reactivex import interval +from reactivex import operators as ops from reactivex.testing import ReactiveTest, TestScheduler from reactivex.testing.subscription import Subscription @@ -17,69 +18,56 @@ class TestSwitchMap(unittest.TestCase): def test_switch_map_uses_index(self): scheduler = TestScheduler() xs = scheduler.create_hot_observable( - on_next( - 300, - 'a' - ), - on_next( - 400, - 'b' - ), - on_next( - 500, - 'c' - ), + on_next(300, "a"), + on_next(400, "b"), + on_next(500, "c"), ) def create_inner(x: str, i: int): def create_changing(j: int): return (i, j, x) + return interval(20).pipe(ops.map(create_changing)) def create(): - return xs.pipe(ops.switch_map( - project=create_inner - )) + return xs.pipe(ops.switch_map(project=create_inner)) results = scheduler.start(create, disposed=580) - # (i, j, x): i is the index of the outer emit; - # j is the value of the inner interval; + # (i, j, x): i is the index of the outer emit; + # j is the value of the inner interval; # x is the value of the outer emission assert results.messages == [ - on_next(320, (0, 0, 'a')), - on_next(340, (0, 1, 'a')), - on_next(360, (0, 2, 'a')), - on_next(380, (0, 3, 'a')), - on_next(420, (1, 0, 'b')), - on_next(440, (1, 1, 'b')), - on_next(460, (1, 2, 'b')), - on_next(480, (1, 3, 'b')), - on_next(520, (2, 0, 'c')), - on_next(540, (2, 1, 'c')), - on_next(560, (2, 2, 'c')), - ] - assert xs.subscriptions == [ - Subscription(200, 580) + on_next(320, (0, 0, "a")), + on_next(340, (0, 1, "a")), + on_next(360, (0, 2, "a")), + on_next(380, (0, 3, "a")), + on_next(420, (1, 0, "b")), + on_next(440, (1, 1, "b")), + on_next(460, (1, 2, "b")), + on_next(480, (1, 3, "b")), + on_next(520, (2, 0, "c")), + on_next(540, (2, 1, "c")), + on_next(560, (2, 2, "c")), ] + assert xs.subscriptions == [Subscription(200, 580)] def test_switch_map_inner_throws(self): """Inner throwing causes outer to throw""" ex = "ex" scheduler = TestScheduler() sources = [ - scheduler.create_cold_observable(on_next(100, 'a'), on_next(300, 'aa')), - scheduler.create_cold_observable(on_next(50, 'b'), on_error(120, ex)), - scheduler.create_cold_observable(on_next(50, 'wont happen'), on_error(120, "no")), + scheduler.create_cold_observable(on_next(100, "a"), on_next(300, "aa")), + scheduler.create_cold_observable(on_next(50, "b"), on_error(120, ex)), + scheduler.create_cold_observable( + on_next(50, "wont happen"), on_error(120, "no") + ), ] xs = scheduler.create_hot_observable( on_next( 250, 0, ), - on_next( - 400, - 1 - ), + on_next(400, 1), on_next( 550, 2, @@ -94,36 +82,31 @@ def create(): results = scheduler.start(create) assert results.messages == [ - on_next(350, 'a'), - on_next(450, 'b'), + on_next(350, "a"), + on_next(450, "b"), on_error(520, ex), ] - assert sources[0].subscriptions == [ - Subscription(250, 400) - ] - assert sources[1].subscriptions == [ - Subscription(400, 520) - ] + assert sources[0].subscriptions == [Subscription(250, 400)] + assert sources[1].subscriptions == [Subscription(400, 520)] assert sources[2].subscriptions == [] - + def test_switch_map_outer_throws(self): """Outer throwing unsubscribes from all""" ex = "ABC" scheduler = TestScheduler() sources = [ - scheduler.create_cold_observable(on_next(100, 'a'), on_next(300, 'aa')), - scheduler.create_cold_observable(on_next(50, 'b'), on_error(120, ex)), - scheduler.create_cold_observable(on_next(50, 'wont happen'), on_error(120, "no")), + scheduler.create_cold_observable(on_next(100, "a"), on_next(300, "aa")), + scheduler.create_cold_observable(on_next(50, "b"), on_error(120, ex)), + scheduler.create_cold_observable( + on_next(50, "wont happen"), on_error(120, "no") + ), ] xs = scheduler.create_hot_observable( on_next( 250, 0, ), - on_next( - 400, - 1 - ), + on_next(400, 1), on_error(430, ex), ) @@ -135,24 +118,18 @@ def create(): results = scheduler.start(create) assert results.messages == [ - on_next(350, 'a'), + on_next(350, "a"), on_error(430, ex), ] - assert sources[0].subscriptions == [ - Subscription(250, 400) - ] - assert sources[1].subscriptions == [ - Subscription(400, 430) - ] + assert sources[0].subscriptions == [Subscription(250, 400)] + assert sources[1].subscriptions == [Subscription(400, 430)] assert sources[2].subscriptions == [] def test_switch_map_no_inner(self): scheduler = TestScheduler() xs = scheduler.create_hot_observable(on_completed(500)) # Fake inner which should never be subscribed to - sources = [ - scheduler.create_cold_observable(on_next(20, 2)) - ] + sources = [scheduler.create_cold_observable(on_next(20, 2))] def create_inner(_x: int, i: int): return sources[i] @@ -169,14 +146,8 @@ def test_switch_map_inner_completes(self): """Inner completions do not affect outer""" scheduler = TestScheduler() xs = scheduler.create_hot_observable( - on_next( - 300, - 'd' - ), - on_next( - 330, - 'f' - ), + on_next(300, "d"), + on_next(330, "f"), on_completed(540), ) @@ -189,8 +160,10 @@ def create(): results = scheduler.start(create) assert results.messages == [ - on_next(320, (0,0,'d')), - on_next(350, (1,0,'f')), - on_next(370, (1,1,'f')), # here the current inner is unsubscribed but not the outer + on_next(320, (0, 0, "d")), + on_next(350, (1, 0, "f")), + on_next( + 370, (1, 1, "f") + ), # here the current inner is unsubscribed but not the outer on_completed(540), # only outer completion affects ] From 95e2924d553f3f98a842ea9e42e8276b3e20f2af Mon Sep 17 00:00:00 2001 From: matiboy Date: Sun, 8 Jan 2023 07:45:14 +0800 Subject: [PATCH 25/27] Apply suggestions from code review Co-authored-by: Dag Brattli --- reactivex/operators/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 02d678c72..7c2cdeb3f 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -3362,8 +3362,8 @@ def switch_latest() -> Callable[ return switch_latest_() -def switch_map( - project: Callable[[_T1, int], Observable[_T2]] +def switch_map_indexed( + project: Optional[MapperIndexed[_T1, Observable[_T2]]] ) -> Callable[[Observable[_T1]], Observable[_T2]]: """Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently From 9ee7f19db182a8eae9e6ace5220b7cc33b01eb63 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 8 Jan 2023 17:55:04 +0800 Subject: [PATCH 26/27] Change to switch_map and switch_map_index Add test for default projector --- reactivex/operators/__init__.py | 51 ++++- tests/test_observable/test_switchmap.py | 62 +++--- .../test_observable/test_switchmapindexed.py | 185 ++++++++++++++++++ 3 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 tests/test_observable/test_switchmapindexed.py diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 7c2cdeb3f..7a1a5b45e 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -26,6 +26,7 @@ Observable, abc, compose, + just, typing, ) from reactivex.internal.basic import identity @@ -3362,8 +3363,48 @@ def switch_latest() -> Callable[ return switch_latest_() +def switch_map( + project: Optional[Mapper[_T1, Observable[_T2]]] = None +) -> Callable[[Observable[_T1]], Observable[_T2]]: + """Projects each source value to an Observable which is merged in + the output Observable, emitting values only from the most recently + projected Observable. + + + .. marble:: + :alt: switch_map + + ---a----------b-------c---------| + [ switch_map(x: x---x---x|) ] + ---a---a---a--b---b---c---c---c-| + + Examples: + >>> op = switch_map(lambda x: reactivex.timer(1.0).pipe(map(lambda x: x))) + >>> op = switch_map() + + Args: + project: Projecting function which takes the outer observable value + and the emission index and emits the inner observable; defaults to `identity` + + Returns: + An operator function that maps each value to the inner observable + and emits its values in order, emitting values only from the + most recently projected Observable. + + + If an inner observable complete, the resulting sequence does *not* + complete. + If an inner observable errors, the resulting sequence errors as well. + If the outer observable completes/errors, the resulting sequence + completes/errors. + + """ + + return compose(map(project), switch_latest()) + + def switch_map_indexed( - project: Optional[MapperIndexed[_T1, Observable[_T2]]] + project: Optional[MapperIndexed[_T1, Observable[_T2]]] = None ) -> Callable[[Observable[_T1]], Observable[_T2]]: """Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently @@ -3373,12 +3414,12 @@ def switch_map_indexed( .. marble:: :alt: switch_map - ---a----------b-------c---------------| - [ switch_map(x,i: x*i---x*i---x*i|) ] - ---a---a---a--bb---bb-ccc---ccc---ccc-| + ---a----------b-------c---------------------| + [ switch_map_indexed(x,i: x*i---x*i---x*i|) ] + ---a---a---a--bb---bb-ccc---ccc---ccc-------| Examples: - >>> op = switch_map(lambda x, i: reactivex.timer(1.0).pipe(map(x*i))) + >>> op = switch_map_indexed(lambda x, i: reactivex.timer(1.0).pipe(map(x*i))) Args: project: Projecting function which takes the outer observable value diff --git a/tests/test_observable/test_switchmap.py b/tests/test_observable/test_switchmap.py index ba33eba17..44a1433ed 100644 --- a/tests/test_observable/test_switchmap.py +++ b/tests/test_observable/test_switchmap.py @@ -3,6 +3,7 @@ from reactivex import interval from reactivex import operators as ops from reactivex.testing import ReactiveTest, TestScheduler +from reactivex.testing.marbles import marbles_testing from reactivex.testing.subscription import Subscription on_next = ReactiveTest.on_next @@ -15,7 +16,7 @@ class TestSwitchMap(unittest.TestCase): - def test_switch_map_uses_index(self): + def test_switch_map(self): scheduler = TestScheduler() xs = scheduler.create_hot_observable( on_next(300, "a"), @@ -23,9 +24,9 @@ def test_switch_map_uses_index(self): on_next(500, "c"), ) - def create_inner(x: str, i: int): + def create_inner(x: str): def create_changing(j: int): - return (i, j, x) + return (j, x) return interval(20).pipe(ops.map(create_changing)) @@ -37,17 +38,17 @@ def create(): # j is the value of the inner interval; # x is the value of the outer emission assert results.messages == [ - on_next(320, (0, 0, "a")), - on_next(340, (0, 1, "a")), - on_next(360, (0, 2, "a")), - on_next(380, (0, 3, "a")), - on_next(420, (1, 0, "b")), - on_next(440, (1, 1, "b")), - on_next(460, (1, 2, "b")), - on_next(480, (1, 3, "b")), - on_next(520, (2, 0, "c")), - on_next(540, (2, 1, "c")), - on_next(560, (2, 2, "c")), + on_next(320, (0, "a")), + on_next(340, (1, "a")), + on_next(360, (2, "a")), + on_next(380, (3, "a")), + on_next(420, (0, "b")), + on_next(440, (1, "b")), + on_next(460, (2, "b")), + on_next(480, (3, "b")), + on_next(520, (0, "c")), + on_next(540, (1, "c")), + on_next(560, (2, "c")), ] assert xs.subscriptions == [Subscription(200, 580)] @@ -74,7 +75,7 @@ def test_switch_map_inner_throws(self): ), ) - def create_inner(x: int, _i: int): + def create_inner(x: int): return sources[x] def create(): @@ -110,7 +111,7 @@ def test_switch_map_outer_throws(self): on_error(430, ex), ) - def create_inner(x: int, _i: int): + def create_inner(x: int): return sources[x] def create(): @@ -131,8 +132,8 @@ def test_switch_map_no_inner(self): # Fake inner which should never be subscribed to sources = [scheduler.create_cold_observable(on_next(20, 2))] - def create_inner(_x: int, i: int): - return sources[i] + def create_inner(_x: int): + return sources.pop(0) def create(): return xs.pipe(ops.switch_map(create_inner)) @@ -151,19 +152,34 @@ def test_switch_map_inner_completes(self): on_completed(540), ) - def create_inner(x: str, i: int): + def create_inner(x: str): """An observable which will complete after 40 ticks""" - return interval(20).pipe(ops.map(lambda j: (i, j, x)), ops.take(2)) + return interval(20).pipe(ops.map(lambda j: (j, x)), ops.take(2)) def create(): return xs.pipe(ops.switch_map(create_inner)) results = scheduler.start(create) assert results.messages == [ - on_next(320, (0, 0, "d")), - on_next(350, (1, 0, "f")), + on_next(320, (0, "d")), + on_next(350, (0, "f")), on_next( - 370, (1, 1, "f") + 370, (1, "f") ), # here the current inner is unsubscribed but not the outer on_completed(540), # only outer completion affects ] + + def test_switch_map_default_mapper(self): + with marbles_testing(timespan=10) as (start, cold, hot, exp): + xs = hot( + " ---a---b------c-----", + { + "a": cold(" --1--2"), + "b": cold(" --1-2-3-4-5|"), + "c": cold(" --1--2"), + }, + None, + ) + expected = exp(" -----1---1-2-3--1--2", None, None) + result = start(xs.pipe(ops.switch_map())) + assert result == expected diff --git a/tests/test_observable/test_switchmapindexed.py b/tests/test_observable/test_switchmapindexed.py new file mode 100644 index 000000000..09736d5f8 --- /dev/null +++ b/tests/test_observable/test_switchmapindexed.py @@ -0,0 +1,185 @@ +import unittest + +from reactivex import interval +from reactivex import operators as ops +from reactivex.testing import ReactiveTest, TestScheduler +from reactivex.testing.subscription import Subscription +from reactivex.testing.marbles import marbles_testing + +on_next = ReactiveTest.on_next +on_completed = ReactiveTest.on_completed +on_error = ReactiveTest.on_error +subscribe = ReactiveTest.subscribe +subscribed = ReactiveTest.subscribed +disposed = ReactiveTest.disposed +created = ReactiveTest.created + + +class TestSwitchMapIndex(unittest.TestCase): + def test_switch_map_indexed_uses_index(self): + scheduler = TestScheduler() + xs = scheduler.create_hot_observable( + on_next(300, "a"), + on_next(400, "b"), + on_next(500, "c"), + ) + + def create_inner(x: str, i: int): + def create_changing(j: int): + return (i, j, x) + + return interval(20).pipe(ops.map(create_changing)) + + def create(): + return xs.pipe(ops.switch_map_indexed(project=create_inner)) + + results = scheduler.start(create, disposed=580) + # (i, j, x): i is the index of the outer emit; + # j is the value of the inner interval; + # x is the value of the outer emission + assert results.messages == [ + on_next(320, (0, 0, "a")), + on_next(340, (0, 1, "a")), + on_next(360, (0, 2, "a")), + on_next(380, (0, 3, "a")), + on_next(420, (1, 0, "b")), + on_next(440, (1, 1, "b")), + on_next(460, (1, 2, "b")), + on_next(480, (1, 3, "b")), + on_next(520, (2, 0, "c")), + on_next(540, (2, 1, "c")), + on_next(560, (2, 2, "c")), + ] + assert xs.subscriptions == [Subscription(200, 580)] + + def test_switch_map_indexed_inner_throws(self): + """Inner throwing causes outer to throw""" + ex = "ex" + scheduler = TestScheduler() + sources = [ + scheduler.create_cold_observable(on_next(100, "a"), on_next(300, "aa")), + scheduler.create_cold_observable(on_next(50, "b"), on_error(120, ex)), + scheduler.create_cold_observable( + on_next(50, "wont happen"), on_error(120, "no") + ), + ] + xs = scheduler.create_hot_observable( + on_next( + 250, + 0, + ), + on_next(400, 1), + on_next( + 550, + 2, + ), + ) + + def create_inner(x: int, _i: int): + return sources[x] + + def create(): + return xs.pipe(ops.switch_map_indexed(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(350, "a"), + on_next(450, "b"), + on_error(520, ex), + ] + assert sources[0].subscriptions == [Subscription(250, 400)] + assert sources[1].subscriptions == [Subscription(400, 520)] + assert sources[2].subscriptions == [] + + def test_switch_map_indexed_outer_throws(self): + """Outer throwing unsubscribes from all""" + ex = "ABC" + scheduler = TestScheduler() + sources = [ + scheduler.create_cold_observable(on_next(100, "a"), on_next(300, "aa")), + scheduler.create_cold_observable(on_next(50, "b"), on_error(120, ex)), + scheduler.create_cold_observable( + on_next(50, "wont happen"), on_error(120, "no") + ), + ] + xs = scheduler.create_hot_observable( + on_next( + 250, + 0, + ), + on_next(400, 1), + on_error(430, ex), + ) + + def create_inner(x: int, _i: int): + return sources[x] + + def create(): + return xs.pipe(ops.switch_map_indexed(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(350, "a"), + on_error(430, ex), + ] + assert sources[0].subscriptions == [Subscription(250, 400)] + assert sources[1].subscriptions == [Subscription(400, 430)] + assert sources[2].subscriptions == [] + + def test_switch_map_indexed_no_inner(self): + scheduler = TestScheduler() + xs = scheduler.create_hot_observable(on_completed(500)) + # Fake inner which should never be subscribed to + sources = [scheduler.create_cold_observable(on_next(20, 2))] + + def create_inner(_x: int, i: int): + return sources[i] + + def create(): + return xs.pipe(ops.switch_map_indexed(create_inner)) + + results = scheduler.start(create) + assert results.messages == [on_completed(500)] + assert xs.subscriptions == [Subscription(200, 500)] + assert sources[0].subscriptions == [] + + def test_switch_map_indexed_inner_completes(self): + """Inner completions do not affect outer""" + scheduler = TestScheduler() + xs = scheduler.create_hot_observable( + on_next(300, "d"), + on_next(330, "f"), + on_completed(540), + ) + + def create_inner(x: str, i: int): + """An observable which will complete after 40 ticks""" + return interval(20).pipe(ops.map(lambda j: (i, j, x)), ops.take(2)) + + def create(): + return xs.pipe(ops.switch_map_indexed(create_inner)) + + results = scheduler.start(create) + assert results.messages == [ + on_next(320, (0, 0, "d")), + on_next(350, (1, 0, "f")), + on_next( + 370, (1, 1, "f") + ), # here the current inner is unsubscribed but not the outer + on_completed(540), # only outer completion affects + ] + + def test_switch_map_default_mapper(self): + with marbles_testing(timespan=10) as (start, cold, hot, exp): + xs = hot( + " ---a---b------c-----", + { + "a": cold(" --1--2"), + "b": cold(" --1-2-3-4-5|"), + "c": cold(" --1--2"), + }, + None, + ) + expected = exp(" -----1---1-2-3--1--2", None, None) + result = start(xs.pipe(ops.switch_map_indexed())) + assert result == expected From af1663d35810fdcd4c25a3ed2e8f0d71b55c341d Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 8 Jan 2023 18:02:08 +0800 Subject: [PATCH 27/27] Black fixes --- reactivex/operators/__init__.py | 1 - tests/test_observable/test_switchmap.py | 6 +++--- tests/test_observable/test_switchmapindexed.py | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/reactivex/operators/__init__.py b/reactivex/operators/__init__.py index 7a1a5b45e..76bd13c0c 100644 --- a/reactivex/operators/__init__.py +++ b/reactivex/operators/__init__.py @@ -26,7 +26,6 @@ Observable, abc, compose, - just, typing, ) from reactivex.internal.basic import identity diff --git a/tests/test_observable/test_switchmap.py b/tests/test_observable/test_switchmap.py index 44a1433ed..363b818a0 100644 --- a/tests/test_observable/test_switchmap.py +++ b/tests/test_observable/test_switchmap.py @@ -174,9 +174,9 @@ def test_switch_map_default_mapper(self): xs = hot( " ---a---b------c-----", { - "a": cold(" --1--2"), - "b": cold(" --1-2-3-4-5|"), - "c": cold(" --1--2"), + "a": cold(" --1--2", None, None), + "b": cold(" --1-2-3-4-5|", None, None), + "c": cold(" --1--2", None, None), }, None, ) diff --git a/tests/test_observable/test_switchmapindexed.py b/tests/test_observable/test_switchmapindexed.py index 09736d5f8..5d5b3d15e 100644 --- a/tests/test_observable/test_switchmapindexed.py +++ b/tests/test_observable/test_switchmapindexed.py @@ -3,8 +3,8 @@ from reactivex import interval from reactivex import operators as ops from reactivex.testing import ReactiveTest, TestScheduler -from reactivex.testing.subscription import Subscription from reactivex.testing.marbles import marbles_testing +from reactivex.testing.subscription import Subscription on_next = ReactiveTest.on_next on_completed = ReactiveTest.on_completed @@ -174,9 +174,9 @@ def test_switch_map_default_mapper(self): xs = hot( " ---a---b------c-----", { - "a": cold(" --1--2"), - "b": cold(" --1-2-3-4-5|"), - "c": cold(" --1--2"), + "a": cold(" --1--2", None, None), + "b": cold(" --1-2-3-4-5|", None, None), + "c": cold(" --1--2", None, None), }, None, )