From 21ae4e93bb4200521f033d8bd3f81f90b6b766e5 Mon Sep 17 00:00:00 2001 From: Zeeland Date: Tue, 10 Jan 2023 02:11:53 +0800 Subject: [PATCH 1/2] test: optimize tests and examples --- example/demo1_base.py | 90 +++++++++++++++++++++ example/demo2.py | 31 -------- example/{demo4.py => demo2_class.py} | 0 example/demo3.py | 27 ------- example/{demo1.py => demo3_async.py} | 8 +- {test => tests}/__init__.py | 0 tests/test_async.py | 43 ++++++++++ tests/test_base.py | 114 +++++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 63 deletions(-) create mode 100644 example/demo1_base.py delete mode 100644 example/demo2.py rename example/{demo4.py => demo2_class.py} (100%) delete mode 100644 example/demo3.py rename example/{demo1.py => demo3_async.py} (94%) rename {test => tests}/__init__.py (100%) create mode 100644 tests/test_async.py create mode 100644 tests/test_base.py diff --git a/example/demo1_base.py b/example/demo1_base.py new file mode 100644 index 0000000..cca8a08 --- /dev/null +++ b/example/demo1_base.py @@ -0,0 +1,90 @@ +# Copyright 2022 Zeeland(https://github.com/Undertone0809/). All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from broadcast_service import broadcast_service, BroadcastService + + +def handle_no_msg(): + print("handle_no_msg func") + + +def callback_of_no_params(): + """ + callback of no parameters + """ + # listen topic + broadcast_service.subscribe('no_params', handle_no_msg) + + # publish broadcast + broadcast_service.publish('no_params') + """ + other way: + bc = BroadcastService() + bc.listen('no_params', handle_no_msg) + bc.broadcast('no_params') + """ + + +@broadcast_service.on_listen(["decorator", "lambda"]) +def handle_decorator(*args, **kwargs): + print("handle_no_msg func") + + +def callback_of_decorator(): + """ + callback of decorator + """ + broadcast_service.broadcast("decorator") + + +def callback_of_lambda(): + """ + callback of lambda + """ + # listen topic + broadcast_service.listen('lambda', lambda x,y: print(f"the params is {x} and {y}")) + + # publish broadcast + broadcast_service.broadcast('lambda', 11, 22) + + +def handle_2msg(info, info2): + print("handle_2msg func") + print(info) + print(info2) + + +def callback_of_2params(): + """ + callback of 2 parameters + """ + info = 'info' + info2 = 'info2' + + # listen topic + broadcast_service.listen('2_params', handle_2msg) + + # publish broadcast + broadcast_service.broadcast('2_params', info, info2) + + +def main(): + callback_of_no_params() + # callback_of_decorator() + # callback_of_lambda() + # callback_of_2params() + + +if __name__ == '__main__': + main() diff --git a/example/demo2.py b/example/demo2.py deleted file mode 100644 index e0cee74..0000000 --- a/example/demo2.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022 Zeeland(https://github.com/Undertone0809/). All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from broadcast_service import broadcast_service - - -def handle_msg(info, info2): - print(info) - print(info2) - - -if __name__ == '__main__': - info = 'This is very important msg' - info2 = 'This is also a very important msg.' - - # listen topic - broadcast_service.listen('Test', handle_msg) - - # publish broadcast - broadcast_service.broadcast('Test', info, info2) diff --git a/example/demo4.py b/example/demo2_class.py similarity index 100% rename from example/demo4.py rename to example/demo2_class.py diff --git a/example/demo3.py b/example/demo3.py deleted file mode 100644 index 9c8e7fb..0000000 --- a/example/demo3.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2022 Zeeland(https://github.com/Undertone0809/). All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from broadcast_service import broadcast_service - - -def handle_msg(): - print('handle_msg callback') - - -if __name__ == '__main__': - # listen topic - broadcast_service.listen('Test', handle_msg) - - # publish broadcast - broadcast_service.broadcast('Test') diff --git a/example/demo1.py b/example/demo3_async.py similarity index 94% rename from example/demo1.py rename to example/demo3_async.py index 9c3bdbb..5e5888e 100644 --- a/example/demo1.py +++ b/example/demo3_async.py @@ -19,7 +19,7 @@ class Application: """ - This demo aim to show how to use broadcast-service. + This demo shows how to use async. Scene: One day, leader Tom arrive the company but find not one staff in company because all staff are playing outside. Therefor, Tom send a message @@ -64,19 +64,17 @@ def notice_go_back(self): class Staff: def __init__(self, name): self.name = name - self.rec_msg() - - def rec_msg(self): broadcast_service.listen('meeting', self.go_back) def go_back(self, info): print("[{2}] {0}(staff) receive msg '{1}' and go back now.".format( self.name, info, print_time())) - time.sleep(random.randint(1, 5)) + time.sleep(2) print('[{1}] {0}(staff) is back now.'.format(self.name, print_time())) def main(): + broadcast_service.enable_async = False app = Application() app.run() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 0000000..833b2ac --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,43 @@ +# Copyright 2022 Zeeland(https://github.com/Undertone0809/). All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import unittest +from broadcast_service import broadcast_service + + +def handle(): + time.sleep(2) + + +class TestAsync(unittest.TestCase): + def test_async(self): + start_time = time.time() + broadcast_service.enable_async = True + broadcast_service.listen("test_topic", handle) + broadcast_service.broadcast("test_topic") + used_time = time.time() - start_time + self.assertLessEqual(used_time, 2) + + def test_sync(self): + start_time = time.time() + broadcast_service.enable_async = False + broadcast_service.listen("test_topic", handle) + broadcast_service.broadcast("test_topic") + used_time = time.time() - start_time + self.assertEqual(int(used_time), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 0000000..8137cbd --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,114 @@ +# Copyright 2022 Zeeland(https://github.com/Undertone0809/). All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from unittest import TestCase +from broadcast_service import broadcast_service + + +def wait(seconds=0.5): + time.sleep(seconds) + + +class TestBroadcast(TestCase): + def test_listen_of_common(self): + self.test_listen_of_common_no_params = False + self.test_listen_of_common_specify_params = False + + def handle_topic_no_params(): + self.test_listen_of_common_no_params = True + + def handle_topic_specify_params(a, b, c): + self.assertEqual(11, a) + self.assertEqual(22, b) + self.assertEqual(33, c) + self.test_listen_of_common_specify_params = True + + broadcast_service.subscribe( + "test_listen_of_common_no_params", handle_topic_no_params) + broadcast_service.publish("test_listen_of_common_no_params") + wait() + self.assertTrue(self.test_listen_of_common_no_params) + + broadcast_service.subscribe( + "test_listen_of_common_specify_params", handle_topic_specify_params) + broadcast_service.publish( + "test_listen_of_common_specify_params", 11, 22, 33) + wait() + self.assertTrue(self.test_listen_of_common_specify_params) + + def test_listen_of_decorator(self): + self.test_listen_of_decorator_no_params = False + self.test_listen_of_decorator_specify_params = False + self.test_listen_of_decorator_listen_all = False + self.counter = 0 + + @broadcast_service.on_listen(["test_listen_of_decorator_no_params"]) + def handle_topic_no_params(): + self.test_listen_of_decorator_no_params = True + + @broadcast_service.on_listen(["test_listen_of_decorator_specify_params"]) + def handle_topic_specify_params(a, b, c): + self.assertEqual(11, a) + self.assertEqual(22, b) + self.assertEqual(33, c) + self.test_listen_of_decorator_specify_params = True + + @broadcast_service.on_listen() + def handle_listen_all_topics(*args, **kwargs): + self.counter += 1 + self.test_listen_of_decorator_listen_all = True + + broadcast_service.publish("test_listen_of_decorator_no_params") + wait() + self.assertTrue(self.test_listen_of_decorator_no_params) + + broadcast_service.publish( + "test_listen_of_decorator_specify_params", 11, 22, 33) + wait() + self.assertTrue(self.test_listen_of_decorator_specify_params) + + broadcast_service.publish("test_listen_of_decorator_listen_all") + wait() + self.assertTrue(self.test_listen_of_decorator_listen_all) + print(broadcast_service.pubsub_channels) + self.assertEqual(3, self.counter) + + def test_listen_of_lambda(self): + self.test_listen_of_lambda_no_params = False + self.test_listen_of_lambda_specify_params = False + + def handle_topic_no_params(): + self.test_listen_of_lambda_no_params = True + + def handle_topic_specify_params(params: bool): + self.test_listen_of_lambda_specify_params = params + + broadcast_service.subscribe( + "test_listen_of_lambda_no_params", lambda: handle_topic_no_params()) + broadcast_service.publish("test_listen_of_lambda_no_params") + wait() + self.assertTrue(self.test_listen_of_lambda_no_params) + + broadcast_service.subscribe( + "test_listen_of_lambda_specify_params", lambda: handle_topic_specify_params(True)) + broadcast_service.publish("test_listen_of_lambda_specify_params") + wait() + self.assertTrue(self.test_listen_of_lambda_no_params) + + def test_broadcast(self): + pass + + def test_close(self): + pass From eaa225c9debdfcdbbee66da813e7c46e59e967c7 Mon Sep 17 00:00:00 2001 From: Zeeland Date: Tue, 10 Jan 2023 02:12:35 +0800 Subject: [PATCH 2/2] feat: add decorator, optimize syntactic expression --- README.md | 14 ++-- broadcast_service/_core.py | 130 +++++++++++++++++++++++++------------ setup.py | 2 +- 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 6f35e06..b7f0bd9 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ if __name__ == '__main__': info = 'This is very important msg' # listen topic - broadcast_service.listen('Test', handle_msg) + broadcast_service.subscribe('Test', handle_msg) # publish broadcast - broadcast_service.broadcast('Test', info) + broadcast_service.publish('Test', info) ``` @@ -64,10 +64,10 @@ if __name__ == '__main__': info2 = 'This is also a very important msg.' # listen topic - broadcast_service.listen('Test', handle_msg) + broadcast_service.subscribe('Test', handle_msg) # publish broadcast - broadcast_service.broadcast('Test', info, info2) + broadcast_service.publish('Test', info, info2) ``` ```python from broadcast_service import broadcast_service @@ -77,12 +77,12 @@ def handle_msg(): if __name__ == '__main__': # listen topic - broadcast_service.listen('Test', handle_msg) + broadcast_service.subscribe('Test', handle_msg) # publish broadcast - broadcast_service.broadcast('Test') + broadcast_service.publish('Test') ``` -Actually, you can see more example in [example](/example). +Actually, you can see more example in [example](/example) and [tests](/tests). ## TODO - optimize documents and show more examples. diff --git a/broadcast_service/_core.py b/broadcast_service/_core.py index 3710f55..ff287a2 100644 --- a/broadcast_service/_core.py +++ b/broadcast_service/_core.py @@ -13,8 +13,11 @@ # limitations under the License. from concurrent.futures import ThreadPoolExecutor +from functools import wraps +from typing import Optional, List, Callable + +__all__ = ['broadcast_service', 'BroadcastService'] -__all__ = ['broadcast_service'] class BroadcastService: """ @@ -23,7 +26,6 @@ class BroadcastService: callback function if some classes subscribe the topic. example: - app.py --------------------------------------------------------------------------------- from broadcast_service import broadcast_service @@ -34,67 +36,111 @@ def handle_msg(params): info = 'This is very important msg' # listen topic - broadcast_service.listen('Test', handle_msg) + broadcast_service.subscribe('Test', handle_msg) # publish broadcast - broadcast_service.broadcast('Test', info) + broadcast_service.publish('Test', info) --------------------------------------------------------------------------------- - """ def __init__(self): """ - subscribe_info example: - - subscribe_info = { - 'my_topic': [{ - 'callback_function': function1, - 'params': { - 'name':'jack', - 'age':20 - } - },{ - 'callback_function': function2, - 'params': 666 - }] + pubsub_channels is the dict to store publish/subscribe data. + pubsub_channels example: + + pubsub_channels = { + 'my_topic': [callback_function1: Callable,callback_function2: Callable] + '__all__': [callback_function3: Callable] } """ - self.topic_list = [] - self.subscribe_info = {} - self.enable_async = True + self.pubsub_channels: dict = { + '__all__': [] + } + self.enable_async: bool = True self.thread_pool = ThreadPoolExecutor(max_workers=5) - def listen(self, topic_name, callback): + # function renaming + self.subscribe = self.listen + self.publish = self.broadcast + self.unsubscribe = self.stop_listen + + self.on = self.listen + self.emit = self.broadcast + self.off = self.stop_listen + + def listen(self, topic_name: str, callback: Callable): """ listen topic """ - if topic_name not in self.subscribe_info.keys(): - self.subscribe_info[topic_name] = [] + if topic_name not in self.pubsub_channels.keys(): + self.pubsub_channels[topic_name] = [] - if callback not in self.subscribe_info[topic_name]: - options = { - 'callback_function': callback, - } - self.subscribe_info[topic_name].append(options) + if callback not in self.pubsub_channels[topic_name]: + # options = { + # 'callback_function': callback, + # } + self.pubsub_channels[topic_name].append(callback) - def broadcast(self, topic_name, *args, **kwargs): - """ Launch broadcast on the specifide topic """ - if topic_name not in self.topic_list: - self.topic_list.append(topic_name) + def broadcast(self, topic_name: str, *args, **kwargs): + """ + Launch broadcast on the specify topic + """ + if topic_name not in self.pubsub_channels.keys(): + self.pubsub_channels[topic_name] = [] - if topic_name not in self.subscribe_info.keys(): - return + for item in self.pubsub_channels[topic_name]: + if self.enable_async: + self.thread_pool.submit( + item, *args, **kwargs) + else: + item(*args, **kwargs) - for item in self.subscribe_info[topic_name]: + for item in self.pubsub_channels['__all__']: if self.enable_async: - self.thread_pool.submit(item['callback_function'], *args, **kwargs) + self.thread_pool.submit( + item, *args, **kwargs) else: - item['callback_function'](*args, **kwargs) + item(*args, **kwargs) - def stop_listen(self, topic_name, callback): - if topic_name not in self.subscribe_info.keys(): + def stop_listen(self, topic_name: str, callback: Callable): + if topic_name not in self.pubsub_channels.keys(): raise RuntimeError("you didn't listen the topic:", topic_name) - if callback not in self.subscribe_info[topic_name]: + if callback not in self.pubsub_channels[topic_name]: pass else: - self.subscribe_info[topic_name].remove(callback) + self.pubsub_channels[topic_name].remove(callback) + + def on_listen(self, topics: Optional[List[str]] = None) -> Callable: + """ + Decorator to listen specify topic. If topics is none, then listen all topics has exits. + :param topics: topic list, you can input topic like: ["topic1", "topic2"]. + + Usage:: + @broadcast_service.on_listen(['topic1']) + def handle_all_msg(): + # your code + + @broadcast_service.on_listen(['topic1','topic2']) + def handle_all_msg(): + # your code + + @broadcast_service.on_listen() + def handle_all_msg(*args, **kwargs): + # your code + + Attention: Your params should keep '*args, **kwargs'. If you publish a topic take arguments, + the callback function you handle should take arguments, otherwise it will not be called back. + """ + def decorator(fn: Callable) -> Callable: + if topics is not None: + for topic in topics: + self.listen(topic, fn) + else: + self.listen('__all__', fn) + + def inner(*args, **kwargs) -> Callable: + ret = fn(*args, **kwargs) + return ret + return inner + return decorator + broadcast_service = BroadcastService() diff --git a/setup.py b/setup.py index f309ac8..1a4ef62 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setuptools.setup( name="broadcast_service", - version="1.1.6", + version="1.1.7", author="Zeeland", author_email="zeeland@foxmail.com", description="A lightweight third-party broadcast library",