Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-unsync for openSUSE:Factory checked in at 2025-01-07 20:53:59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-unsync (Old) and /work/SRC/openSUSE:Factory/.python-unsync.new.1881 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-unsync" Tue Jan 7 20:53:59 2025 rev:2 rq:1235527 version:1.3.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-unsync/python-unsync.changes 2021-01-02 21:31:21.975455548 +0100 +++ /work/SRC/openSUSE:Factory/.python-unsync.new.1881/python-unsync.changes 2025-01-07 20:54:36.719872107 +0100 @@ -1,0 +2,19 @@ +Tue Jan 7 08:46:22 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaubitz@suse.com> + +- Update to version 1.3.2 + * Delete print("DERP") (#39) +- from version 1.3.1 + * Add a multi-process example + * Cleanup README + * Modify async example in documentation (#36) + * Support setting custom event loops (#34) + * Refactor lazy initialized unsync class members into properties +- Switch package to modern Python Stack on SLE-15 + * Use Python 3.11 on SLE-15 by default + * Drop support for older Python versions +- Switch build system from setuptools to pyproject.toml + * Add python-pip and python-wheel to BuildRequires + * Replace %python_build with %pyproject_wheel + * Replace %python_install with %pyproject_install + +------------------------------------------------------------------- Old: ---- unsync-1.3.tar.gz New: ---- unsync-1.3.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-unsync.spec ++++++ --- /var/tmp/diff_new_pack.UF8nHN/_old 2025-01-07 20:54:37.675911649 +0100 +++ /var/tmp/diff_new_pack.UF8nHN/_new 2025-01-07 20:54:37.679911814 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-unsync # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2025 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -15,17 +15,19 @@ # Please submit bugfixes or comments via https://bugs.opensuse.org/ # -%define skip_python2 1 -%{?!python_module:%define python_module() python-%{**} python3-%{**}} + +%{?sle15_python_module_pythons} %define modname unsync Name: python-unsync -Version: 1.3 +Version: 1.3.2 Release: 0 Summary: Unsynchronize asyncio License: MIT URL: https://github.com/alex-sherman/unsync Source: https://github.com/alex-sherman/%{modname}/archive/v%{version}.tar.gz#/%{modname}-%{version}.tar.gz +BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros BuildArch: noarch @@ -62,10 +64,10 @@ %setup -q -n unsync-%{version} %build -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitelib} %check ++++++ unsync-1.3.tar.gz -> unsync-1.3.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unsync-1.3/README.md new/unsync-1.3.2/README.md --- old/unsync-1.3/README.md 2020-12-29 04:48:53.000000000 +0100 +++ new/unsync-1.3.2/README.md 2021-03-27 23:23:06.000000000 +0100 @@ -1,52 +1,64 @@ # unsync -Unsynchronize `asyncio` by using an ambient event loop in a separate thread. +Unsynchronize `asyncio` by using an ambient event loop, or executing in separate threads or processes. -# Rules for unsync -1. Mark all async functions with `@unsync`. May also mark regular functions to execute in a separate thread. - * All `@unsync` functions, async or not, return an `Unfuture` -2. All `Futures` must be `Unfutures` which includes the result of an `@unsync` function call, - or wrapping `Unfuture(asyncio.Future)` or `Unfuture(concurrent.Future)`. - `Unfuture` combines the behavior of `asyncio.Future` and `concurrent.Future`: - * `Unfuture.set_value` is threadsafe unlike `asyncio.Future` - * `Unfuture` instances can be awaited, even if made from `concurrent.Future` - * `Unfuture.result()` is a blocking operation *except* in `unsync.loop`/`unsync.thread` where - it behaves like `asyncio.Future.result` and will throw an exception if the future is not done -3. Functions will execute in different contexts: - * `@unsync` async functions will execute in an event loop in `unsync.thread` - * `@unsync` regular functions will execute in `unsync.thread_executor`, a `ThreadPoolExecutor` - * `@unsync(cpu_bound=True)` regular functions will execute in `unsync.process_executor`, a `ProcessPoolExecutor` +# Quick Overview +Functions marked with the `@unsync` decorator will behave in one of the following ways: +* `async` functions will run in the `unsync.loop` event loop executed from `unsync.thread` +* Regular functions will execute in `unsync.thread_executor`, a `ThreadPoolExecutor` + * Useful for IO bounded work that does not support `asyncio` +* Regular functions marked with `@unsync(cpu_bound=True)` will execute in `unsync.process_executor`, a `ProcessPoolExecutor` + * Useful for CPU bounded work + +All `@unsync` functions will return an `Unfuture` object. +This new future type combines the behavior of `asyncio.Future` and `concurrent.Future` with the following changes: +* `Unfuture.set_result` is threadsafe unlike `asyncio.Future` +* `Unfuture` instances can be awaited, even if made from `concurrent.Future` +* `Unfuture.result()` is a blocking operation *except* in `unsync.loop`/`unsync.thread` where + it behaves like `asyncio.Future.result` and will throw an exception if the future is not done # Examples ## Simple Sleep A simple sleeping example with `asyncio`: ```python async def sync_async(): - await asyncio.sleep(0.1) + await asyncio.sleep(1) return 'I hate event loops' -result = asyncio.run(sync_async()) -print(result) + +async def main(): + future1 = asyncio.create_task(sync_async()) + future2 = asyncio.create_task(sync_async()) + + await future1, future2 + + print(future1.result() + future2.result()) + +asyncio.run(main()) +# Takes 1 second to run ``` Same example with `unsync`: ```python @unsync async def unsync_async(): - await asyncio.sleep(0.1) + await asyncio.sleep(1) return 'I like decorators' -print(unsync_async().result()) +unfuture1 = unsync_async() +unfuture2 = unsync_async() +print(unfuture1.result() + unfuture2.result()) +# Takes 1 second to run ``` -## Threading a synchronous function +## Multi-threading an IO-bound function Synchronous functions can be made to run asynchronously by executing them in a `concurrent.ThreadPoolExecutor`. This can be easily accomplished by marking the regular function `@unsync`. ```python @unsync def non_async_function(seconds): time.sleep(seconds) - return 'Run in parallel!' + return 'Run concurrently!' start = time.time() tasks = [non_async_function(0.1) for _ in range(10)] @@ -55,11 +67,11 @@ ``` Which prints: - ['Run in parallel!', 'Run in parallel!', ...] + ['Run concurrently!', 'Run concurrently!', ...] Executed in 0.10807514190673828 seconds ## Continuations -Using Unfuture.then chains asynchronous calls and returns an Unfuture that wraps both the source, and continuation. +Using `Unfuture.then` chains asynchronous calls and returns an `Unfuture` that wraps both the source, and continuation. The continuation is invoked with the source Unfuture as the first argument. Continuations can be regular functions (which will execute synchronously), or `@unsync` functions. ```python @@ -120,7 +132,7 @@ {0: 2, 1: 4, 2: 6, 3: 8, 4: 10, 5: 12, 6: 14, 7: 16, 8: 18, 9: 20} Executed in 0.22115683555603027 seconds - + ## Preserving typing As far as we know it is not possible to change the return type of a method or function using a decorator. Therefore, we need a workaround to properly use IntelliSense. You have three options in general: @@ -143,4 +155,21 @@ future_result = function_name('b') self.assertEqual('ba', future_result.result()) - ``` \ No newline at end of file + ``` + +## Custom Event Loops +In order to use custom event loops, be sure to set the event loop policy before calling any `@unsync` methods. +For example, to use `uvloop` simply: + +```python +import unsync +import uvloop + +@unsync +async def main(): + # Main entry-point. + ... + +uvloop.install() # Equivalent to asyncio.set_event_loop_policy(EventLoopPolicy()) +main() +``` \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unsync-1.3/examples/process_executor.py new/unsync-1.3.2/examples/process_executor.py --- old/unsync-1.3/examples/process_executor.py 1970-01-01 01:00:00.000000000 +0100 +++ new/unsync-1.3.2/examples/process_executor.py 2021-03-27 23:23:06.000000000 +0100 @@ -0,0 +1,22 @@ +import time + +from unsync import unsync + + +def cpu_bound_function(operations): + for i in range(int(operations)): + pass + return 'Run in parallel!' + +# Could also be applied as a decorator above +unsync_cpu_bound_function = unsync(cpu_bound=True)(cpu_bound_function) + +if __name__ == "__main__": + start = time.time() + print([cpu_bound_function(5e7) for _ in range(10)]) + print('Non-unsync executed in {} seconds'.format(time.time() - start)) + + start = time.time() + tasks = [unsync_cpu_bound_function(5e7) for _ in range(10)] + print([task.result() for task in tasks]) + print('unsync executed in {} seconds'.format(time.time() - start)) \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unsync-1.3/setup.py new/unsync-1.3.2/setup.py --- old/unsync-1.3/setup.py 2020-12-29 04:48:53.000000000 +0100 +++ new/unsync-1.3.2/setup.py 2021-03-27 23:23:06.000000000 +0100 @@ -3,7 +3,7 @@ setup( name='unsync', - version='1.3', + version='1.3.2', packages=['unsync'], url='https://github.com/alex-sherman/unsync', license='MIT', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unsync-1.3/test/test_custom_event_loop.py new/unsync-1.3.2/test/test_custom_event_loop.py --- old/unsync-1.3/test/test_custom_event_loop.py 1970-01-01 01:00:00.000000000 +0100 +++ new/unsync-1.3.2/test/test_custom_event_loop.py 2021-03-27 23:23:06.000000000 +0100 @@ -0,0 +1,23 @@ +from unittest import TestCase +import asyncio + +from unsync import unsync + + +class TestEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + def new_event_loop(self): + loop = super().new_event_loop() + loop.derp = "faff" + return loop + +asyncio.set_event_loop_policy(TestEventLoopPolicy()) + + +class CustomEventTest(TestCase): + def test_custom_event_loop(self): + @unsync + async def method(): + return True + + self.assertTrue(method().result()) + self.assertEqual(unsync.loop.derp, "faff") \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unsync-1.3/unsync/unsync.py new/unsync-1.3.2/unsync/unsync.py --- old/unsync-1.3/unsync/unsync.py 2020-12-29 04:48:53.000000000 +0100 +++ new/unsync-1.3.2/unsync/unsync.py 2021-03-27 23:23:06.000000000 +0100 @@ -7,16 +7,39 @@ from threading import Thread from typing import Generic, TypeVar +class unsync_meta(type): -class unsync(object): + def _init_loop(cls): + cls._loop = asyncio.new_event_loop() + cls._thread = Thread(target=cls._thread_target, args=(cls._loop,), daemon=True) + cls._thread.start() + + @property + def loop(cls): + if getattr(cls, '_loop', None) is None: + unsync_meta._init_loop(cls) + return cls._loop + + @property + def thread(cls): + if getattr(cls, '_thread', None) is None: + unsync_meta._init_loop(cls) + return cls._thread + + @property + def process_executor(cls): + if getattr(cls, '_process_executor', None) is None: + cls._process_executor = concurrent.futures.ProcessPoolExecutor() + return cls._process_executor + + +class unsync(object, metaclass=unsync_meta): thread_executor = concurrent.futures.ThreadPoolExecutor() process_executor = None - loop = asyncio.new_event_loop() - thread = None unsync_functions = {} @staticmethod - def thread_target(loop): + def _thread_target(loop): asyncio.set_event_loop(loop) loop.run_forever() @@ -52,8 +75,6 @@ future = self.func(*args, **kwargs) else: if self.cpu_bound: - if unsync.process_executor is None: - unsync.process_executor = concurrent.futures.ProcessPoolExecutor() future = unsync.process_executor.submit( _multiprocess_target, (self.func.__module__, self.func.__name__), *args, **kwargs) else: @@ -136,6 +157,3 @@ return await result return result - -unsync.thread = Thread(target=unsync.thread_target, args=(unsync.loop,), daemon=True) -unsync.thread.start()