diff --git a/cpl-workspace.json b/cpl-workspace.json index e00ea987..706e879e 100644 --- a/cpl-workspace.json +++ b/cpl-workspace.json @@ -6,6 +6,7 @@ "cpl-core": "src/cpl_core/cpl-core.json", "cpl-discord": "src/cpl_discord/cpl-discord.json", "cpl-query": "src/cpl_query/cpl-query.json", + "cpl-reactive-extensions": "src/cpl_reactive_extensions/cpl-reactive-extensions.json", "cpl-translation": "src/cpl_translation/cpl-translation.json", "set-version": "tools/set_version/set-version.json", "set-pip-urls": "tools/set_pip_urls/set-pip-urls.json", @@ -14,7 +15,8 @@ "unittests_core": "unittests/unittests_core/unittests_core.json", "unittests_query": "unittests/unittests_query/unittests_query.json", "unittests_shared": "unittests/unittests_shared/unittests_shared.json", - "unittests_translation": "unittests/unittests_translation/unittests_translation.json" + "unittests_translation": "unittests/unittests_translation/unittests_translation.json", + "unittests_reactive_extenstions": "unittests/unittests_reactive_extenstions/unittests_reactive_extenstions.json" }, "Scripts": { "hello-world": "echo 'Hello World'", diff --git a/src/cpl_reactive_extensions/__init__.py b/src/cpl_reactive_extensions/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/src/cpl_reactive_extensions/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/src/cpl_reactive_extensions/cpl-reactive-extensions.json b/src/cpl_reactive_extensions/cpl-reactive-extensions.json new file mode 100644 index 00000000..bd851781 --- /dev/null +++ b/src/cpl_reactive_extensions/cpl-reactive-extensions.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "cpl-reactive-extensions", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2023.4.0" + ], + "DevDependencies": [ + "cpl-cli>=2023.4.0" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "cpl_reactive_extensions.main", + "EntryPoint": "cpl-reactive-extensions", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/cpl_reactive_extensions/observable.py b/src/cpl_reactive_extensions/observable.py new file mode 100644 index 00000000..88cefa3b --- /dev/null +++ b/src/cpl_reactive_extensions/observable.py @@ -0,0 +1,19 @@ +from typing import Callable + +from cpl_reactive_extensions.observer import Observer + + +class Observable: + def __init__(self, callback: Callable): + self._callback = callback + self._subscriptions: list[Callable] = [] + + def _run_subscriptions(self): + for callback in self._subscriptions: + callback() + + def subscribe(self, observer: Observer): + try: + self._callback(observer) + except Exception as e: + observer.error(e) diff --git a/src/cpl_reactive_extensions/observer.py b/src/cpl_reactive_extensions/observer.py new file mode 100644 index 00000000..895b6a66 --- /dev/null +++ b/src/cpl_reactive_extensions/observer.py @@ -0,0 +1,24 @@ +from typing import Callable + +from cpl_core.type import T + + +class Observer: + def __init__(self, on_next: Callable, on_error: Callable = None, on_complete: Callable = None): + self._on_next = on_next + self._on_error = on_error if on_error is not None else lambda err: err + self._on_complete = on_complete if on_complete is not None else lambda x: x + + def next(self, value: T): + self._on_next(value) + + def error(self, ex: Exception): + if self._on_error is None: + return + self._on_error(ex) + + def complete(self): + if self._on_complete is None: + return + + self._on_complete() diff --git a/src/cpl_reactive_extensions/subject.py b/src/cpl_reactive_extensions/subject.py new file mode 100644 index 00000000..679f8ac1 --- /dev/null +++ b/src/cpl_reactive_extensions/subject.py @@ -0,0 +1,17 @@ +from cpl_core.type import T +from cpl_reactive_extensions.observable import Observable + + +class Subject(Observable): + def __init__(self): + Observable.__init__(self) + + self._value: T = None + + @property + def value(self) -> T: + return self._value + + def emit(self, value: T): + self._value = value + self._subscriptions() diff --git a/unittests/unittests/application.py b/unittests/unittests/application.py index 932fb34b..95a4854f 100644 --- a/unittests/unittests/application.py +++ b/unittests/unittests/application.py @@ -6,6 +6,7 @@ from cpl_core.dependency_injection import ServiceProviderABC from unittests_cli.cli_test_suite import CLITestSuite from unittests_core.core_test_suite import CoreTestSuite from unittests_query.query_test_suite import QueryTestSuite +from unittests_reactive_extenstions.reactive_test_suite import ReactiveTestSuite from unittests_translation.translation_test_suite import TranslationTestSuite @@ -21,4 +22,5 @@ class Application(ApplicationABC): runner.run(CoreTestSuite()) runner.run(CLITestSuite()) runner.run(QueryTestSuite()) + runner.run(ReactiveTestSuite()) runner.run(TranslationTestSuite()) diff --git a/unittests/unittests_reactive_extenstions/__init__.py b/unittests/unittests_reactive_extenstions/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/unittests/unittests_reactive_extenstions/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/unittests/unittests_reactive_extenstions/reactive_test_case.py b/unittests/unittests_reactive_extenstions/reactive_test_case.py new file mode 100644 index 00000000..e0ee498f --- /dev/null +++ b/unittests/unittests_reactive_extenstions/reactive_test_case.py @@ -0,0 +1,71 @@ +import unittest +from threading import Timer + +from cpl_reactive_extensions.observable import Observable +from cpl_reactive_extensions.observer import Observer + + +class ReactiveTestCase(unittest.TestCase): + def setUp(self): + pass + + def test_observer(self): + called = 0 + has_error = False + completed = False + test_x = 1 + + def callback(observer: Observer): + nonlocal test_x + observer.next(test_x) + test_x += 1 + observer.next(test_x) + test_x += 1 + observer.next(test_x) + + def complete(): + nonlocal test_x + test_x += 1 + observer.next(test_x) + observer.complete() + + Timer(1.0, complete).start() + + observable = Observable(callback) + + def on_next(x): + nonlocal called + called += 1 + self.assertEqual(test_x, x) + + def on_err(): + nonlocal has_error + has_error = True + + def on_complete(): + nonlocal completed + completed = True + + self.assertEqual(called, 0) + self.assertFalse(has_error) + self.assertFalse(completed) + observable.subscribe( + Observer( + on_next, + on_err, + on_complete, + ) + ) + self.assertEqual(called, 3) + self.assertFalse(has_error) + self.assertFalse(completed) + + def complete(): + self.assertEqual(called, 4) + self.assertFalse(has_error) + self.assertTrue(completed) + + Timer(1.0, complete).start() + + def test_subject(self): + pass diff --git a/unittests/unittests_reactive_extenstions/reactive_test_suite.py b/unittests/unittests_reactive_extenstions/reactive_test_suite.py new file mode 100644 index 00000000..050700d3 --- /dev/null +++ b/unittests/unittests_reactive_extenstions/reactive_test_suite.py @@ -0,0 +1,24 @@ +import unittest + +from unittests_query.enumerable_query_test_case import EnumerableQueryTestCase +from unittests_query.enumerable_test_case import EnumerableTestCase +from unittests_query.iterable_query_test_case import IterableQueryTestCase +from unittests_query.iterable_test_case import IterableTestCase +from unittests_query.sequence_test_case import SequenceTestCase +from unittests_reactive_extenstions.reactive_test_case import ReactiveTestCase + + +class ReactiveTestSuite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self) + + loader = unittest.TestLoader() + self.addTests(loader.loadTestsFromTestCase(ReactiveTestCase)) + + def run(self, *args): + super().run(*args) + + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + runner.run(ReactiveTestSuite()) diff --git a/unittests/unittests_reactive_extenstions/unittests_reactive_extenstions.json b/unittests/unittests_reactive_extenstions/unittests_reactive_extenstions.json new file mode 100644 index 00000000..8919eb59 --- /dev/null +++ b/unittests/unittests_reactive_extenstions/unittests_reactive_extenstions.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "unittests_reactive_extenstions", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2023.4.0" + ], + "DevDependencies": [ + "cpl-cli>=2023.4.0" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "unittest", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "unittests_reactive_extenstions.main", + "EntryPoint": "unittests_reactive_extenstions", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file