diff --git a/README.md b/README.md index 3e27c3c1..8862663d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ - [async modifiers](#async-modifiers) - [sync modifiers](#sync-modifiers) - [Default Modifiers](#default-modifiers) + - [Other Helpful Classes](#other-helpful-classes) + - [ASyncIterable](#asynciterable) + - [ASyncIterator](#asynciterator) + - [ASyncFilter](#asyncfilter) + - [ASyncSorter](#asyncsorter) ## Introduction @@ -195,3 +200,87 @@ Instead of setting modifiers one by one in functions, you can set a default valu - `RAM_CACHE_TTL` - `RUNS_PER_MINUTE` - `SEMAPHORE` + +### Other Helpful Classes +#### ASyncIterable +The `ASyncIterable` class allows objects to be iterated over using either a standard `for` loop or an `async for` loop. This is particularly useful in scenarios where the mode of iteration needs to be flexible or is determined at runtime. + +```python +from a_sync import ASyncIterable + +async_iterable = ASyncIterable(some_async_iterable) + +# Asynchronous iteration +async for item in async_iterable: + ... + +# Synchronous iteration +for item in async_iterable: + ... +``` + +#### ASyncIterator + +The `ASyncIterator` class provides a unified interface for iteration that can operate in both synchronous and asynchronous contexts. It allows the wrapping of asynchronous iterable objects or async generator functions. + +```python +from a_sync import ASyncIterator + +async_iterator = ASyncIterator(some_async_iterator) + +# Asynchronous iteration +async for item in async_iterator: + ... + +# Synchronous iteration +for item in async_iterator: + ... +``` + +#### ASyncFilter + +The `ASyncFilter` class filters items of an async iterable based on a provided function. It can handle both synchronous and asynchronous filter functions. + +```python +from a_sync import ASyncFilter + +async def is_even(x): + return x % 2 == 0 + +filtered_iterable = ASyncFilter(is_even, some_async_iterable) + +# or use the alias +import a_sync + +filtered_iterable = a_sync.filter(is_even, some_async_iterable) + +# Asynchronous iteration +async for item in filtered_iterable: + ... + +# Synchronous iteration +for item in filtered_iterable: + ... +``` + +#### ASyncSorter + +The `ASyncSorter` class sorts items of an async iterable based on a provided key function. It supports both synchronous and asynchronous key functions. + +```python +from a_sync import ASyncSorter + +sorted_iterable = ASyncSorter(some_async_iterable, key=lambda x: x.value) + +# or use the alias +import a_sync + +sorted_iterable = a_sync.sort(some_async_iterable, key=lambda x: x.value) + +# Asynchronous iteration +async for item in sorted_iterable: + ... + +# Synchronous iteration +for item in sorted_iterable: + ... \ No newline at end of file diff --git a/a_sync/_smart.py b/a_sync/_smart.py index caad3160..f526585a 100644 --- a/a_sync/_smart.py +++ b/a_sync/_smart.py @@ -37,6 +37,18 @@ class _SmartFutureMixin(Generic[T]): def __await__(self: Union["SmartFuture", "SmartTask"]) -> Generator[Any, None, T]: """ Await the smart future or task, handling waiters and logging. + + Yields: + The result of the future or task. + + Raises: + RuntimeError: If await wasn't used with future. + + Example: + ```python + future = SmartFuture() + result = await future + ``` """ if self.done(): return self.result() # May raise too. @@ -54,6 +66,15 @@ def num_waiters(self: Union["SmartFuture", "SmartTask"]) -> int: # NOTE: we check .done() because the callback may not have ran yet and its very lightweight """ Get the number of waiters currently awaiting the future or task. + + Returns: + int: The number of waiters. + + Example: + ```python + future = SmartFuture() + print(future.num_waiters) + ``` """ if self.done(): # if there are any waiters left, there won't be once the event loop runs once @@ -67,6 +88,9 @@ def _waiter_done_cleanup_callback( Callback to clean up waiters when a waiter task is done. Removes the waiter from _waiters, and _queue._futs if applicable + + Args: + waiter: The waiter task to clean up. """ if not self.done(): self._waiters.remove(waiter) @@ -86,6 +110,12 @@ class SmartFuture(_SmartFutureMixin[T], asyncio.Future): Inherits from both _SmartFutureMixin and asyncio.Future, providing additional functionality for tracking waiters and integrating with a smart processing queue. + + Example: + ```python + future = SmartFuture() + await future + ``` """ _queue = None @@ -105,6 +135,11 @@ def __init__( queue: Optional; a smart processing queue. key: Optional; a key identifying the future. loop: Optional; the event loop. + + Example: + ```python + future = SmartFuture(queue=my_queue, key=my_key) + ``` """ super().__init__(loop=loop) if queue: @@ -127,6 +162,13 @@ def __lt__(self, other: "SmartFuture[T]") -> bool: Returns: True if self has more waiters than other. + + Example: + ```python + future1 = SmartFuture() + future2 = SmartFuture() + print(future1 < future2) + ``` """ return self.num_waiters > other.num_waiters @@ -147,6 +189,11 @@ def create_future( Returns: A SmartFuture instance. + + Example: + ```python + future = create_future(queue=my_queue, key=my_key) + ``` """ return SmartFuture(queue=queue, key=key, loop=loop or asyncio.get_event_loop()) @@ -157,6 +204,12 @@ class SmartTask(_SmartFutureMixin[T], asyncio.Task): Inherits from both _SmartFutureMixin and asyncio.Task, providing additional functionality for tracking waiters and integrating with a smart processing queue. + + Example: + ```python + task = SmartTask(coro=my_coroutine()) + await task + ``` """ def __init__( @@ -173,6 +226,11 @@ def __init__( coro: The coroutine to run in the task. loop: Optional; the event loop. name: Optional; the name of the task. + + Example: + ```python + task = SmartTask(coro=my_coroutine(), name="my_task") + ``` """ super().__init__(coro, loop=loop, name=name) self._waiters: Set["asyncio.Task[T]"] = set() @@ -193,6 +251,12 @@ def smart_task_factory( Returns: A SmartTask instance running the provided coroutine. + + Example: + ```python + loop = asyncio.get_event_loop() + task = smart_task_factory(loop, my_coroutine()) + ``` """ return SmartTask(coro, loop=loop) @@ -203,6 +267,14 @@ def set_smart_task_factory(loop: asyncio.AbstractEventLoop = None) -> None: Args: loop: Optional; the event loop. If None, the current event loop is used. + + Example: + ```python + set_smart_task_factory() + ``` + + See Also: + - :func:`smart_task_factory` """ if loop is None: loop = a_sync.asyncio.get_event_loop() @@ -241,6 +313,14 @@ def shield( Args: arg: The awaitable to shield from cancellation. loop: Optional; the event loop. Deprecated since Python 3.8. + + Example: + ```python + result = await shield(my_coroutine()) + ``` + + See Also: + - :func:`asyncio.shield` """ if loop is not None: warnings.warn( @@ -291,4 +371,4 @@ def _outer_done_callback(outer): "SmartTask", "smart_task_factory", "set_smart_task_factory", -] +] \ No newline at end of file diff --git a/a_sync/a_sync/_descriptor.py b/a_sync/a_sync/_descriptor.py index c36189d0..2c66379e 100644 --- a/a_sync/a_sync/_descriptor.py +++ b/a_sync/a_sync/_descriptor.py @@ -1,8 +1,14 @@ """ -This module contains the ASyncDescriptor class, which is used to create sync/async methods +This module contains the :class:`ASyncDescriptor` class, which is used to create dual-function sync/async methods and properties. -It also includes utility methods for mapping operations across multiple instances. +The :class:`ASyncDescriptor` class provides functionality for mapping operations across multiple instances +and includes utility methods for common operations such as checking if all or any results are truthy, +and finding the minimum, maximum, or sum of results of the method or property mapped across multiple instances. + +See Also: + - :class:`~a_sync.a_sync.function.ASyncFunction` + - :class:`~a_sync.a_sync.method.ASyncMethodDescriptor` """ import functools @@ -23,6 +29,27 @@ class ASyncDescriptor(_ModifiedMixin, Generic[I, P, T]): and includes utility methods for common operations such as checking if all or any results are truthy, and finding the minimum, maximum, or sum of results of the method or property mapped across multiple instances. + + Examples: + To create a dual-function method or property, subclass :class:`ASyncDescriptor` and implement + the desired functionality. You can then use the provided utility methods to perform operations + across multiple instances. + + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x * 2) + + instance = MyClass() + result = instance.my_method.map([1, 2, 3]) + ``` + + See Also: + - :class:`~a_sync.a_sync.function.ASyncFunction` + - :class:`~a_sync.a_sync.method.ASyncMethodDescriptor` """ __wrapped__: AnyFn[Concatenate[I, P], T] @@ -37,7 +64,7 @@ def __init__( **modifiers: ModifierKwargs, ) -> None: """ - Initialize the {cls}. + Initialize the :class:`ASyncDescriptor`. Args: _fget: The function to be wrapped. @@ -61,7 +88,7 @@ def __init__( self.__wrapped__ = _fget self.field_name = field_name or _fget.__name__ - """The name of the field the {cls} is bound to.""" + """The name of the field the :class:`ASyncDescriptor` is bound to.""" functools.update_wrapper(self, self.__wrapped__) @@ -70,7 +97,7 @@ def __repr__(self) -> str: def __set_name__(self, owner, name): """ - Set the field name when the {cls} is assigned to a class. + Set the field name when the :class:`ASyncDescriptor` is assigned to a class. Args: owner: The class owning this descriptor. @@ -82,14 +109,25 @@ def map( self, *instances: AnyIterable[I], **bound_method_kwargs: P.kwargs ) -> "TaskMapping[I, T]": """ - Create a TaskMapping for the given instances. + Create a :class:`TaskMapping` for the given instances. Args: *instances: Iterable of instances to map over. **bound_method_kwargs: Additional keyword arguments for the bound method. Returns: - A TaskMapping object. + A :class:`TaskMapping` object. + + Examples: + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x * 2) + + instance = MyClass() + result = instance.my_method.map([1, 2, 3]) """ from a_sync.task import TaskMapping @@ -101,7 +139,18 @@ def all(self) -> ASyncFunction[Concatenate[AnyIterable[I], P], bool]: Create an :class:`~ASyncFunction` that checks if all results are truthy. Returns: - An ASyncFunction object. + An :class:`ASyncFunction` object. + + Examples: + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x > 0) + + instance = MyClass() + result = await instance.my_method.all([1, 2, 3]) """ return decorator.a_sync(default=self.default)(self._all) @@ -111,7 +160,18 @@ def any(self) -> ASyncFunction[Concatenate[AnyIterable[I], P], bool]: Create an :class:`~ASyncFunction` that checks if any result is truthy. Returns: - An ASyncFunction object. + An :class:`ASyncFunction` object. + + Examples: + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x > 0) + + instance = MyClass() + result = await instance.my_method.any([-1, 0, 1]) """ return decorator.a_sync(default=self.default)(self._any) @@ -121,7 +181,20 @@ def min(self) -> ASyncFunction[Concatenate[AnyIterable[I], P], T]: Create an :class:`~ASyncFunction` that returns the minimum result. Returns: - An ASyncFunction object. + An :class:`ASyncFunction` object. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method.min([3, 1, 2]) + ``` """ return decorator.a_sync(default=self.default)(self._min) @@ -131,7 +204,18 @@ def max(self) -> ASyncFunction[Concatenate[AnyIterable[I], P], T]: Create an :class:`~ASyncFunction` that returns the maximum result. Returns: - An ASyncFunction object. + An :class:`ASyncFunction` object. + + Examples: + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method.max([3, 1, 2]) """ return decorator.a_sync(default=self.default)(self._max) @@ -141,7 +225,20 @@ def sum(self) -> ASyncFunction[Concatenate[AnyIterable[I], P], T]: Create an :class:`~ASyncFunction` that returns the sum of results. Returns: - An ASyncFunction object. + An :class:`ASyncFunction` object. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method.sum([1, 2, 3]) + ``` """ return decorator.a_sync(default=self.default)(self._sum) @@ -163,6 +260,19 @@ async def _all( Returns: A boolean indicating if all results are truthy. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x > 0) + + instance = MyClass() + result = await instance.my_method._all([1, 2, 3]) + ``` """ return await self.map( *instances, concurrency=concurrency, name=name, **kwargs @@ -186,6 +296,19 @@ async def _any( Returns: A boolean indicating if any result is truthy. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x > 0) + + instance = MyClass() + result = await instance.my_method._any([-1, 0, 1]) + ``` """ return await self.map( *instances, concurrency=concurrency, name=name, **kwargs @@ -209,6 +332,19 @@ async def _min( Returns: The minimum result. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method._min([3, 1, 2]) + ``` """ return await self.map( *instances, concurrency=concurrency, name=name, **kwargs @@ -232,6 +368,19 @@ async def _max( Returns: The maximum result. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method._max([3, 1, 2]) + ``` """ return await self.map( *instances, concurrency=concurrency, name=name, **kwargs @@ -255,6 +404,19 @@ async def _sum( Returns: The sum of the results. + + Examples: + ```python + class MyDescriptor(ASyncDescriptor): + def __init__(self, func): + super().__init__(func) + + class MyClass: + my_method = MyDescriptor(lambda x: x) + + instance = MyClass() + result = await instance.my_method._sum([1, 2, 3]) + ``` """ return await self.map( *instances, concurrency=concurrency, name=name, **kwargs @@ -264,4 +426,4 @@ def __init_subclass__(cls) -> None: for attr in cls.__dict__.values(): if attr.__doc__ and "{cls}" in attr.__doc__: attr.__doc__ = attr.__doc__.replace("{cls}", f":class:`{cls.__name__}`") - return super().__init_subclass__() + return super().__init_subclass__() \ No newline at end of file diff --git a/a_sync/a_sync/_helpers.py b/a_sync/a_sync/_helpers.py index 39e0c063..baa41729 100644 --- a/a_sync/a_sync/_helpers.py +++ b/a_sync/a_sync/_helpers.py @@ -1,3 +1,5 @@ +/home/ubuntu/libs/a-sync/a_sync/a_sync/_helpers.py + """ This module provides utility functions for handling asynchronous operations and converting synchronous functions to asynchronous ones. @@ -21,6 +23,17 @@ def _await(awaitable: Awaitable[T]) -> T: Raises: exceptions.SyncModeInAsyncContextError: If the event loop is already running. + + Examples: + >>> async def example_coroutine(): + ... return 42 + ... + >>> result = _await(example_coroutine()) + >>> print(result) + 42 + + See Also: + - :func:`asyncio.run`: For running the main entry point of an asyncio program. """ try: return a_sync.asyncio.get_event_loop().run_until_complete(awaitable) @@ -39,10 +52,23 @@ def _asyncify(func: SyncFn[P, T], executor: Executor) -> CoroFn[P, T]: # type: executor: The executor used to run the synchronous function. Returns: - A coroutine function wrapping the input function. + CoroFn[P, T]: A coroutine function wrapping the input function. Raises: exceptions.FunctionNotSync: If the input function is a coroutine function or an instance of ASyncFunction. + + Examples: + >>> def sync_function(x): + ... return x * 2 + ... + >>> async_function = _asyncify(sync_function, executor) + >>> result = await async_function(3) + >>> print(result) + 6 + + See Also: + - :class:`concurrent.futures.Executor`: For managing pools of threads or processes. + - :func:`asyncio.to_thread`: For running blocking code in a separate thread. """ from a_sync.a_sync.function import ASyncFunction @@ -56,4 +82,4 @@ async def _asyncify_wrap(*args: P.args, **kwargs: P.kwargs) -> T: loop=a_sync.asyncio.get_event_loop(), ) - return _asyncify_wrap + return _asyncify_wrap \ No newline at end of file diff --git a/a_sync/a_sync/config.py b/a_sync/a_sync/config.py index 987279e3..a15d65ed 100644 --- a/a_sync/a_sync/config.py +++ b/a_sync/a_sync/config.py @@ -1,3 +1,5 @@ +# /home/ubuntu/libs/a-sync/a_sync/a_sync/config.py + """ Configuration module for the a_sync library. @@ -7,17 +9,23 @@ Environment Variables: A_SYNC_EXECUTOR_TYPE: Specifies the type of executor to use. Valid values are - strings that start with 'p' for ProcessPoolExecutor (e.g., 'processes') - or 't' for ThreadPoolExecutor (e.g., 'threads'). Defaults to 'threads'. + strings that start with 'p' for :class:`~concurrent.futures.ProcessPoolExecutor` + (e.g., 'processes') or 't' for :class:`~concurrent.futures.ThreadPoolExecutor` + (e.g., 'threads'). Defaults to 'threads'. A_SYNC_EXECUTOR_VALUE: Specifies the number of workers for the executor. Defaults to 8. A_SYNC_DEFAULT_MODE: Sets the default mode for a_sync functions if not specified. A_SYNC_CACHE_TYPE: Sets the default cache type. If not specified, defaults to None. A_SYNC_CACHE_TYPED: Boolean flag to determine if cache keys should consider types. A_SYNC_RAM_CACHE_MAXSIZE: Sets the maximum size for the RAM cache. Defaults to -1. - A_SYNC_RAM_CACHE_TTL: Sets the time-to-live for cache entries. Defaults to None. + A_SYNC_RAM_CACHE_TTL: Sets the time-to-live for cache entries. Defaults to 0, + which is interpreted as None. A_SYNC_RUNS_PER_MINUTE: Sets the rate limit for function execution. A_SYNC_SEMAPHORE: Sets the semaphore limit for function execution. + +See Also: + - :mod:`concurrent.futures`: For more details on executors. + - :mod:`functools`: For caching mechanisms. """ import functools @@ -36,11 +44,25 @@ def get_default_executor() -> Executor: """Get the default executor based on the EXECUTOR_TYPE environment variable. Returns: - An instance of either ProcessPoolExecutor or ThreadPoolExecutor. + An instance of either :class:`~concurrent.futures.ProcessPoolExecutor` + or :class:`~concurrent.futures.ThreadPoolExecutor`. Raises: ValueError: If an invalid EXECUTOR_TYPE is specified. Valid values are - strings that start with 'p' for ProcessPoolExecutor or 't' for ThreadPoolExecutor. + strings that start with 'p' for :class:`~concurrent.futures.ProcessPoolExecutor` + or 't' for :class:`~concurrent.futures.ThreadPoolExecutor`. + + Examples: + >>> import os + >>> os.environ["A_SYNC_EXECUTOR_TYPE"] = "threads" + >>> executor = get_default_executor() + >>> isinstance(executor, ThreadPoolExecutor) + True + + >>> os.environ["A_SYNC_EXECUTOR_TYPE"] = "processes" + >>> executor = get_default_executor() + >>> isinstance(executor, ProcessPoolExecutor) + True """ if EXECUTOR_TYPE.lower().startswith("p"): # p, P, proc, Processes, etc return ProcessPoolExecutor(EXECUTOR_VALUE) @@ -104,4 +126,4 @@ def get_default_executor() -> Executor: runs_per_minute=RUNS_PER_MINUTE, semaphore=SEMAPHORE, executor=default_sync_executor, -) +) \ No newline at end of file diff --git a/a_sync/a_sync/decorator.py b/a_sync/a_sync/decorator.py index 133658b5..723ea99e 100644 --- a/a_sync/a_sync/decorator.py +++ b/a_sync/a_sync/decorator.py @@ -35,6 +35,18 @@ def a_sync( Args: default: Specifies the default execution mode as 'async'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync(default='async') + ... async def my_function(): + ... return True + >>> await my_function() + True + >>> my_function(sync=True) + True + + See Also: + :class:`ASyncDecoratorAsyncDefault` """ @@ -49,6 +61,18 @@ def a_sync( Args: default: Specifies the default execution mode as 'sync'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync(default='sync') + ... def my_function(): + ... return True + >>> my_function() + True + >>> await my_function(sync=False) + True + + See Also: + :class:`ASyncDecoratorSyncDefault` """ @@ -61,6 +85,18 @@ def a_sync( Args: **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync + ... async def my_function(): + ... return True + >>> await my_function() + True + >>> my_function(sync=True) + True + + See Also: + :class:`ASyncDecorator` """ @@ -77,6 +113,18 @@ def a_sync( coro_fn: The coroutine function to be decorated. default: Specifies no default execution mode. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> async def my_function(): + ... return True + >>> decorated_function = a_sync(my_function) + >>> await decorated_function() + True + >>> decorated_function(sync=True) + True + + See Also: + :class:`ASyncFunctionAsyncDefault` """ @@ -93,18 +141,19 @@ def a_sync( coro_fn: The synchronous function to be decorated. default: Specifies no default execution mode. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. - """ - -# @a_sync(default='async') -# def some_fn(): -# pass -# -# @a_sync(default='async') -# async def some_fn(): -# pass -# -# NOTE These should output a decorator that will be applied to 'some_fn' + Examples: + >>> def my_function(): + ... return True + >>> decorated_function = a_sync(my_function) + >>> decorated_function() + True + >>> await decorated_function(sync=False) + True + + See Also: + :class:`ASyncFunctionSyncDefault` + """ @overload @@ -120,6 +169,18 @@ def a_sync( coro_fn: Specifies no function. default: Specifies the default execution mode as 'async'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync(default='async') + ... async def my_function(): + ... return True + >>> await my_function() + True + >>> my_function(sync=True) + True + + See Also: + :class:`ASyncDecoratorAsyncDefault` """ @@ -136,10 +197,19 @@ def a_sync( coro_fn: Specifies the default execution mode as 'async'. default: Specifies no default execution mode. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. - """ - -# a_sync(some_fn, default='async') + Examples: + >>> @a_sync('async') + ... async def my_function(): + ... return True + >>> await my_function() + True + >>> my_function(sync=True) + True + + See Also: + :class:`ASyncDecoratorAsyncDefault` + """ @overload # async def, async default @@ -155,6 +225,18 @@ def a_sync( coro_fn: The coroutine function to be decorated. default: Specifies the default execution mode as 'async'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> async def my_function(): + ... return True + >>> decorated_function = a_sync(my_function, default='async') + >>> await decorated_function() + True + >>> decorated_function(sync=True) + True + + See Also: + :class:`ASyncFunctionAsyncDefault` """ @@ -171,10 +253,19 @@ def a_sync( coro_fn: The synchronous function to be decorated. default: Specifies the default execution mode as 'async'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. - """ - -# a_sync(some_fn, default='sync') + Examples: + >>> def my_function(): + ... return True + >>> decorated_function = a_sync(my_function, default='async') + >>> await decorated_function() + True + >>> decorated_function(sync=True) + True + + See Also: + :class:`ASyncFunctionAsyncDefault` + """ @overload # async def, sync default @@ -190,6 +281,18 @@ def a_sync( coro_fn: The coroutine function to be decorated. default: Specifies the default execution mode as 'sync'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> async def my_function(): + ... return True + >>> decorated_function = a_sync(my_function, default='sync') + >>> decorated_function() + True + >>> await decorated_function(sync=False) + True + + See Also: + :class:`ASyncFunctionSyncDefault` """ @@ -206,18 +309,19 @@ def a_sync( coro_fn: The synchronous function to be decorated. default: Specifies the default execution mode as 'sync'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. - """ - -# @a_sync(default='sync') -# def some_fn(): -# pass -# -# @a_sync(default='sync') -# async def some_fn(): -# pass -# -# NOTE These should output a decorator that will be applied to 'some_fn' + Examples: + >>> def my_function(): + ... return True + >>> decorated_function = a_sync(my_function, default='sync') + >>> decorated_function() + True + >>> await decorated_function(sync=False) + True + + See Also: + :class:`ASyncFunctionSyncDefault` + """ @overload @@ -233,6 +337,18 @@ def a_sync( coro_fn: Specifies no function. default: Specifies the default execution mode as 'sync'. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync(default='sync') + ... def my_function(): + ... return True + >>> my_function() + True + >>> await my_function(sync=False) + True + + See Also: + :class:`ASyncDecoratorSyncDefault` """ @@ -249,6 +365,18 @@ def a_sync( coro_fn: Specifies the default execution mode as 'sync'. default: Specifies no default execution mode. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync('sync') + ... def my_function(): + ... return True + >>> my_function() + True + >>> await my_function(sync=False) + True + + See Also: + :class:`ASyncDecoratorSyncDefault` """ @@ -265,6 +393,18 @@ def a_sync( coro_fn: Specifies the default execution mode as 'sync'. default: Specifies no default execution mode. **modifiers: Additional keyword arguments to modify the behavior of the decorated function. + + Examples: + >>> @a_sync('sync') + ... def my_function(): + ... return True + >>> my_function() + True + >>> await my_function(sync=False) + True + + See Also: + :class:`ASyncDecoratorSyncDefault` """ @@ -313,9 +453,9 @@ def a_sync( >>> @a_sync ... async def some_async_fn(): ... return True - >>> await some_fn() + >>> await some_async_fn() True - >>> some_fn(sync=True) + >>> some_async_fn(sync=True) True >>> @a_sync @@ -324,7 +464,7 @@ def a_sync( >>> some_sync_fn() True >>> some_sync_fn(sync=False) - + 2. As a decorator with default mode specified: >>> @a_sync(default='sync') @@ -333,6 +473,17 @@ def a_sync( ... >>> some_fn() True + >>> some_fn(sync=False) + + + >>> @a_sync('async') + ... def some_fn(): + ... return True + ... + >>> some_fn() + + >>> some_fn(asynchronous=False) + True 3. As a decorator with modifiers: >>> @a_sync(cache_type='memory', runs_per_minute=60) @@ -348,18 +499,19 @@ def a_sync( "some return value" The decorated function can then be called either synchronously or asynchronously: - - result = some_fn() # Synchronous call - result = await some_fn() # Asynchronous call + >>> result = some_fn() # Synchronous call + >>> result = await some_fn() # Asynchronous call The execution mode can also be explicitly specified during the call: - - result = some_fn(sync=True) # Force synchronous execution - result = await some_fn(sync=False) # Force asynchronous execution + >>> result = some_fn(sync=True) # Force synchronous execution + >>> result = await some_fn(sync=False) # Force asynchronous execution This decorator is particularly useful for libraries that need to support both synchronous and asynchronous usage, or for gradually migrating synchronous code to asynchronous without breaking existing interfaces. + + See Also: + :class:`ASyncFunction`, :class:`ASyncDecorator` """ # If the dev tried passing a default as an arg instead of a kwarg, ie: @a_sync('sync')... @@ -376,4 +528,4 @@ def a_sync( return deco if coro_fn is None else deco(coro_fn) # type: ignore [arg-type] -# TODO: in a future release, I will make this usable with sync functions as well +# TODO: in a future release, I will make this usable with sync functions as well \ No newline at end of file diff --git a/a_sync/a_sync/function.py b/a_sync/a_sync/function.py index 87754d25..49b532e2 100644 --- a/a_sync/a_sync/function.py +++ b/a_sync/a_sync/function.py @@ -30,6 +30,10 @@ class _ModifiedMixin: `ASyncFunctionAsyncDefault` and `ASyncFunctionSyncDefault`, to handle the application of async and sync modifiers to functions. Modifiers can alter the behavior of functions, such as converting sync functions to async, applying caching, or rate limiting. + + See Also: + - :class:`~ASyncFunction` + - :class:`~ModifierManager` """ modifiers: ModifierManager @@ -44,6 +48,10 @@ def _asyncify(self, func: SyncFn[P, T]) -> CoroFn[P, T]: Returns: The asynchronous version of the function with applied modifiers. + + See Also: + - :func:`_helpers._asyncify` + - :meth:`ModifierManager.apply_async_modifiers` """ coro_fn = _helpers._asyncify(func, self.modifiers.executor) return self.modifiers.apply_async_modifiers(coro_fn) @@ -55,6 +63,10 @@ def _await(self) -> Callable[[Awaitable[T]], T]: Returns: The modified _await function. + + See Also: + - :func:`_helpers._await` + - :meth:`ModifierManager.apply_sync_modifiers` """ return self.modifiers.apply_sync_modifiers(_helpers._await) @@ -65,6 +77,9 @@ def default(self) -> DefaultMode: Returns: The default execution mode. + + See Also: + - :attr:`ModifierManager.default` """ return self.modifiers.default @@ -78,6 +93,9 @@ def _validate_wrapped_fn(fn: Callable) -> None: Raises: TypeError: If the input is not callable. RuntimeError: If the function has arguments with names that conflict with viable flags. + + See Also: + - :func:`_check_not_genfunc` """ if isinstance(fn, (AsyncPropertyDescriptor, AsyncCachedPropertyDescriptor)): return # These are always valid @@ -116,20 +134,26 @@ async def my_coroutine(x: int) -> str: # Asynchronous call result = await func(5) # returns "5" + + See Also: + - :class:`_ModifiedMixin` + - :class:`ModifierManager` """ # NOTE: We can't use __slots__ here because it breaks functools.update_wrapper @overload - def __init__(self, fn: CoroFn[P, T], **modifiers: Unpack[ModifierKwargs]) -> None: + def __init__( + self, fn: CoroFn[P, T], **modifiers: Unpack[ModifierKwargs] + ) -> None: ... # TODO write specific docs for this overload - @overload - def __init__(self, fn: SyncFn[P, T], **modifiers: Unpack[ModifierKwargs]) -> None: + def __init__( + self, fn: SyncFn[P, T], **modifiers: Unpack[ModifierKwargs] + ) -> None: ... # TODO write specific docs for this overload - def __init__(self, fn: AnyFn[P, T], **modifiers: Unpack[ModifierKwargs]) -> None: """ Initializes an ASyncFunction instance. @@ -137,6 +161,10 @@ def __init__(self, fn: AnyFn[P, T], **modifiers: Unpack[ModifierKwargs]) -> None Args: fn: The function to wrap. **modifiers: Keyword arguments for function modifiers. + + See Also: + - :func:`_validate_wrapped_fn` + - :class:`ModifierManager` """ _validate_wrapped_fn(fn) @@ -158,33 +186,28 @@ def __init__(self, fn: AnyFn[P, T], **modifiers: Unpack[ModifierKwargs]) -> None def __call__(self, *args: P.args, sync: Literal[True], **kwargs: P.kwargs) -> T: ... # TODO write specific docs for this overload - @overload def __call__( self, *args: P.args, sync: Literal[False], **kwargs: P.kwargs ) -> Coroutine[Any, Any, T]: ... # TODO write specific docs for this overload - @overload def __call__( self, *args: P.args, asynchronous: Literal[False], **kwargs: P.kwargs ) -> T: ... # TODO write specific docs for this overload - @overload def __call__( self, *args: P.args, asynchronous: Literal[True], **kwargs: P.kwargs ) -> Coroutine[Any, Any, T]: ... # TODO write specific docs for this overload - @overload - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: ... # TODO write specific docs for this overload - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: """ Calls the wrapped function either synchronously or asynchronously. @@ -198,6 +221,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: Raises: Exception: Any exception that may be raised by the wrapped function. + + See Also: + - :attr:`default` + - :meth:`_run_sync` """ logger.debug( "calling %s fn: %s with args: %s kwargs: %s", self, self.fn, args, kwargs @@ -209,13 +236,16 @@ def __repr__(self) -> str: @functools.cached_property def fn( - self, ): # -> Union[SyncFn[[CoroFn[P, T]], MaybeAwaitable[T]], SyncFn[[SyncFn[P, T]], MaybeAwaitable[T]]]: """ Returns the final wrapped version of :attr:`ASyncFunction._fn` decorated with all of the a_sync goodness. Returns: The final wrapped function. + + See Also: + - :meth:`_async_wrap` + - :meth:`_sync_wrap` """ return self._async_wrap if self._async_def else self._sync_wrap @@ -239,6 +269,9 @@ def map( Returns: A TaskMapping object for managing concurrent execution. + + See Also: + - :class:`TaskMapping` """ from a_sync import TaskMapping @@ -268,6 +301,9 @@ async def any( Returns: True if any result is truthy, otherwise False. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -294,6 +330,9 @@ async def all( Returns: True if all results are truthy, otherwise False. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -320,6 +359,9 @@ async def min( Returns: The minimum result. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -346,6 +388,9 @@ async def max( Returns: The maximum result. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -372,6 +417,9 @@ async def sum( Returns: The sum of the results. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -400,6 +448,9 @@ def map( Returns: A TaskMapping object for managing concurrent execution. + + See Also: + - :class:`TaskMapping` """ from a_sync import TaskMapping @@ -429,6 +480,9 @@ async def any( Returns: True if any result is truthy, otherwise False. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -455,6 +509,9 @@ async def all( Returns: True if all results are truthy, otherwise False. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -481,6 +538,9 @@ async def min( Returns: The minimum result. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -507,6 +567,9 @@ async def max( Returns: The maximum result. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -533,6 +596,9 @@ async def sum( Returns: The sum of the results. + + See Also: + - :meth:`map` """ return await self.map( *iterables, @@ -551,6 +617,9 @@ def _sync_default(self) -> bool: Returns: True if the default is sync, False if async. + + See Also: + - :attr:`default` """ return ( True @@ -565,6 +634,9 @@ def _async_def(self) -> bool: Returns: True if the function is asynchronous, otherwise False. + + See Also: + - :func:`asyncio.iscoroutinefunction` """ return asyncio.iscoroutinefunction(self.__wrapped__) @@ -580,6 +652,10 @@ def _run_sync(self, kwargs: dict) -> bool: Returns: True if the function should run synchronously, otherwise False. + + See Also: + - :func:`_kwargs.get_flag_name` + - :func:`_kwargs.is_sync` """ if flag := _kwargs.get_flag_name(kwargs): # If a flag was specified in the kwargs, we will defer to it. @@ -598,6 +674,9 @@ def _asyncified(self) -> CoroFn[P, T]: Returns: The asynchronous version of the wrapped function. + + See Also: + - :meth:`_asyncify` """ if self._async_def: raise TypeError( @@ -615,6 +694,10 @@ def _modified_fn(self) -> AnyFn[P, T]: Returns: The modified function. + + See Also: + - :meth:`ModifierManager.apply_async_modifiers` + - :meth:`ModifierManager.apply_sync_modifiers` """ if self._async_def: return self.modifiers.apply_async_modifiers(self.__wrapped__) # type: ignore [arg-type] @@ -629,6 +712,10 @@ def _async_wrap(self): # -> SyncFn[[CoroFn[P, T]], MaybeAwaitable[T]]: Returns: The wrapped function with async handling. + + See Also: + - :meth:`_run_sync` + - :meth:`_await` """ @functools.wraps(self._modified_fn) @@ -650,6 +737,10 @@ def _sync_wrap(self): # -> SyncFn[[SyncFn[P, T]], MaybeAwaitable[T]]: Returns: The wrapped function with sync handling. + + See Also: + - :meth:`_run_sync` + - :meth:`_asyncified` """ @functools.wraps(self._modified_fn) @@ -679,6 +770,9 @@ def __init__(self, **modifiers: Unpack[ModifierKwargs]) -> None: Raises: ValueError: If 'default' is not 'sync', 'async', or None. + + See Also: + - :class:`ModifierManager` """ assert "default" in modifiers, modifiers self.modifiers = ModifierManager(modifiers) @@ -690,6 +784,9 @@ def validate_inputs(self) -> None: Raises: ValueError: If 'default' is not 'sync', 'async', or None. + + See Also: + - :attr:`ModifierManager.default` """ if self.modifiers.default not in ["sync", "async", None]: raise ValueError( @@ -713,6 +810,9 @@ def __call__(self, func: AnyFn[P, T]) -> ASyncFunction[P, T]: # type: ignore [o Returns: An ASyncFunction instance with the appropriate default behavior. + + See Also: + - :class:`ASyncFunction` """ if self.default == "async": return ASyncFunctionAsyncDefault(func, **self.modifiers) @@ -732,6 +832,10 @@ def _check_not_genfunc(func: Callable) -> None: Raises: ValueError: If the function is a generator or async generator. + + See Also: + - :func:`inspect.isasyncgenfunction` + - :func:`inspect.isgeneratorfunction` """ if inspect.isasyncgenfunction(func) or inspect.isgeneratorfunction(func): raise ValueError("unable to decorate generator functions with this decorator") @@ -797,6 +901,9 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: Returns: The result of the function call. + + See Also: + - :meth:`ASyncFunction.__call__` """ return self.fn(*args, **kwargs) @@ -867,6 +974,9 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: Returns: The result of the function call. + + See Also: + - :meth:`ASyncFunction.__call__` """ return self.fn(*args, **kwargs) diff --git a/a_sync/a_sync/method.py b/a_sync/a_sync/method.py index 981cb3f9..3388105c 100644 --- a/a_sync/a_sync/method.py +++ b/a_sync/a_sync/method.py @@ -31,8 +31,27 @@ class ASyncMethodDescriptor(ASyncDescriptor[I, P, T]): """ - This class provides the core functionality for creating :class:`ASyncBoundMethod` objects, + Provides the core functionality for creating :class:`ASyncBoundMethod` objects, which can be used to define methods that can be called both synchronously and asynchronously. + + It handles the binding of methods to instances and determines the default mode + ("sync" or "async") based on the instance type or the `default` attribute. + + Examples: + >>> class MyClass: + ... @ASyncMethodDescriptor + ... async def my_method(self): + ... return "Hello, World!" + ... + >>> obj = MyClass() + >>> await obj.my_method() + 'Hello, World!' + >>> obj.my_method(sync=True) + 'Hello, World!' + + See Also: + - :class:`ASyncBoundMethod` + - :class:`ASyncFunction` """ __wrapped__: AnyFn[P, T] @@ -47,8 +66,9 @@ async def __call__(self, instance: I, *args: P.args, **kwargs: P.kwargs) -> T: *args: Positional arguments. **kwargs: Keyword arguments. - Returns: - The result of the method call. + Examples: + >>> descriptor = ASyncMethodDescriptor(my_async_function) + >>> await descriptor(instance, arg1, arg2, kwarg1=value1) """ # NOTE: This is only used by TaskMapping atm # TODO: use it elsewhere logger.debug( @@ -74,8 +94,9 @@ def __get__( instance: The instance to bind the method to, or None. owner: The owner class. - Returns: - The descriptor or bound method. + Examples: + >>> descriptor = ASyncMethodDescriptor(my_function) + >>> bound_method = descriptor.__get__(instance, MyClass) """ if instance is None: return self @@ -136,7 +157,12 @@ def __set__(self, instance, value): value: The value to set. Raises: - :class:`RuntimeError`: Always raised to prevent setting. + RuntimeError: Always raised to prevent setting. + + Examples: + >>> descriptor = ASyncMethodDescriptor(my_function) + >>> descriptor.__set__(instance, value) + RuntimeError: cannot set field_name, descriptor is what you get. sorry. """ raise RuntimeError( f"cannot set {self.field_name}, {self} is what you get. sorry." @@ -150,7 +176,12 @@ def __delete__(self, instance): instance: The instance. Raises: - :class:`RuntimeError`: Always raised to prevent deletion. + RuntimeError: Always raised to prevent deletion. + + Examples: + >>> descriptor = ASyncMethodDescriptor(my_function) + >>> descriptor.__delete__(instance) + RuntimeError: cannot delete field_name, you're stuck with descriptor forever. sorry. """ raise RuntimeError( f"cannot delete {self.field_name}, you're stuck with {self} forever. sorry." @@ -161,8 +192,10 @@ def __is_async_def__(self) -> bool: """ Check if the wrapped function is a coroutine function. - Returns: - True if the wrapped function is a coroutine function, False otherwise. + Examples: + >>> descriptor = ASyncMethodDescriptor(my_function) + >>> descriptor.__is_async_def__ + True """ return asyncio.iscoroutinefunction(self.__wrapped__) @@ -175,6 +208,10 @@ def _get_cache_handle(self, instance: I) -> asyncio.TimerHandle: Returns: A timer handle for cache management. + + Examples: + >>> descriptor = ASyncMethodDescriptor(my_function) + >>> cache_handle = descriptor._get_cache_handle(instance) """ # NOTE: use `instance.__dict__.pop` instead of `delattr` so we don't create a strong ref to `instance` return asyncio.get_event_loop().call_later( @@ -189,6 +226,22 @@ class ASyncMethodDescriptorSyncDefault(ASyncMethodDescriptor[I, P, T]): This class extends ASyncMethodDescriptor to provide a synchronous default behavior for method calls. + + Examples: + >>> class MyClass: + ... @ASyncMethodDescriptorSyncDefault + ... def my_method(self): + ... return "Hello, World!" + ... + >>> obj = MyClass() + >>> obj.my_method() + 'Hello, World!' + >>> await obj.my_method(sync=False) + 'Hello, World!' + + See Also: + - :class:`ASyncBoundMethodSyncDefault` + - :class:`ASyncFunctionSyncDefault` """ default = "sync" @@ -229,8 +282,9 @@ def __get__( instance: The instance to bind the method to, or None. owner: The owner class. - Returns: - The descriptor or bound method with synchronous default. + Examples: + >>> descriptor = ASyncMethodDescriptorSyncDefault(my_function) + >>> bound_method = descriptor.__get__(instance, MyClass) """ if instance is None: return self @@ -256,6 +310,22 @@ class ASyncMethodDescriptorAsyncDefault(ASyncMethodDescriptor[I, P, T]): This class extends ASyncMethodDescriptor to provide an asynchronous default behavior for method calls. + + Examples: + >>> class MyClass: + ... @ASyncMethodDescriptorAsyncDefault + ... async def my_method(self): + ... return "Hello, World!" + ... + >>> obj = MyClass() + >>> await obj.my_method() + 'Hello, World!' + >>> obj.my_method(sync=True) + 'Hello, World!' + + See Also: + - :class:`ASyncBoundMethodAsyncDefault` + - :class:`ASyncFunctionAsyncDefault` """ default = "async" @@ -294,8 +364,9 @@ def __get__( instance: The instance to bind the method to, or None. owner: The owner class. - Returns: - The descriptor or bound method with asynchronous default. + Examples: + >>> descriptor = ASyncMethodDescriptorAsyncDefault(my_function) + >>> bound_method = descriptor.__get__(instance, MyClass) """ if instance is None: return self @@ -320,15 +391,34 @@ class ASyncBoundMethod(ASyncFunction[P, T], Generic[I, P, T]): This class represents a method bound to an instance, which can be called either synchronously or asynchronously based on various conditions. + + Examples: + >>> class MyClass: + ... def __init__(self, value): + ... self.value = value + ... + ... @ASyncMethodDescriptor + ... async def my_method(self): + ... return self.value + ... + >>> obj = MyClass(42) + >>> await obj.my_method() + 42 + >>> obj.my_method(sync=True) + 42 + + See Also: + - :class:`ASyncMethodDescriptor` + - :class:`ASyncFunction` """ # NOTE: this is created by the Descriptor _cache_handle: asyncio.TimerHandle - "An asyncio handle used to pop the bound method from `instance.__dict__` 5 minutes after its last use." + """An asyncio handle used to pop the bound method from `instance.__dict__` 5 minutes after its last use.""" __weakself__: "weakref.ref[I]" - "A weak reference to the instance the function is bound to." + """A weak reference to the instance the function is bound to.""" __wrapped__: AnyFn[Concatenate[I, P], T] """The original unbound method that was wrapped.""" @@ -350,6 +440,18 @@ def __init__( unbound: The unbound function. async_def: Whether the original function is an async def. **modifiers: Additional modifiers for the function. + + Examples: + >>> class MyClass: + ... def __init__(self, value): + ... self.value = value + ... + ... @ASyncMethodDescriptor + ... async def my_method(self): + ... return self.value + ... + >>> obj = MyClass(42) + >>> bound_method = ASyncBoundMethod(obj, MyClass.my_method, True) """ self.__weakself__ = weakref.ref(instance, self.__cancel_cache_handle) # First we unwrap the coro_fn and rewrap it so overriding flag kwargs are handled automagically. @@ -358,15 +460,17 @@ def __init__( unbound = unbound.__wrapped__ ASyncFunction.__init__(self, unbound, **modifiers) self._is_async_def = async_def - "True if `self.__wrapped__` is a coroutine function, False otherwise." + """True if `self.__wrapped__` is a coroutine function, False otherwise.""" functools.update_wrapper(self, unbound) def __repr__(self) -> str: """ Return a string representation of the bound method. - Returns: - A string representation of the bound method. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> repr(bound_method) + '' """ try: instance_type = type(self.__self__) @@ -401,8 +505,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> MaybeCoro[T]: *args: Positional arguments. **kwargs: Keyword arguments. - Returns: - The result of the method call, which may be a coroutine. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> await bound_method(arg1, arg2, kwarg1=value1) + >>> bound_method(arg1, arg2, kwarg1=value1, sync=True) """ logger.debug("calling %s with args: %s kwargs: %s", self, args, kwargs) # This could either be a coroutine or a return value from an awaited coroutine, @@ -428,11 +534,13 @@ def __self__(self) -> I: """ Get the instance the method is bound to. - Returns: - The instance the method is bound to. - Raises: - :class:`ReferenceError`: If the instance has been garbage collected. + ReferenceError: If the instance has been garbage collected. + + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> bound_method.__self__ + """ instance = self.__weakself__() if instance is not None: @@ -444,8 +552,10 @@ def __bound_to_a_sync_instance__(self) -> bool: """ Check if the method is bound to an ASyncABC instance. - Returns: - True if bound to an ASyncABC instance, False otherwise. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> bound_method.__bound_to_a_sync_instance__ + True """ from a_sync.a_sync.abstract import ASyncABC @@ -469,6 +579,10 @@ def map( Returns: A TaskMapping instance for this method. + + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> task_mapping = bound_method.map(iterable1, iterable2, concurrency=5) """ from a_sync import TaskMapping @@ -492,8 +606,9 @@ async def any( task_name: Optional name for the task. **kwargs: Additional keyword arguments. - Returns: - True if any result is truthy, False otherwise. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> result = await bound_method.any(iterable1, iterable2) """ return await self.map( *iterables, concurrency=concurrency, task_name=task_name, **kwargs @@ -515,8 +630,9 @@ async def all( task_name: Optional name for the task. **kwargs: Additional keyword arguments. - Returns: - True if all results are truthy, False otherwise. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> result = await bound_method.all(iterable1, iterable2) """ return await self.map( *iterables, concurrency=concurrency, task_name=task_name, **kwargs @@ -538,8 +654,9 @@ async def min( task_name: Optional name for the task. **kwargs: Additional keyword arguments. - Returns: - The minimum result. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> result = await bound_method.min(iterable1, iterable2) """ return await self.map( *iterables, concurrency=concurrency, task_name=task_name, **kwargs @@ -561,8 +678,9 @@ async def max( task_name: Optional name for the task. **kwargs: Additional keyword arguments. - Returns: - The maximum result. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> result = await bound_method.max(iterable1, iterable2) """ return await self.map( *iterables, concurrency=concurrency, task_name=task_name, **kwargs @@ -584,8 +702,9 @@ async def sum( task_name: Optional name for the task. **kwargs: Additional keyword arguments. - Returns: - The sum of the results. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> result = await bound_method.sum(iterable1, iterable2) """ return await self.map( *iterables, concurrency=concurrency, task_name=task_name, **kwargs @@ -598,8 +717,9 @@ def _should_await(self, kwargs: dict) -> bool: Args: kwargs: Keyword arguments passed to the method. - Returns: - True if the method should be awaited, False otherwise. + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> should_await = bound_method._should_await(kwargs) """ if flag := _kwargs.get_flag_name(kwargs): return _kwargs.is_sync(flag, kwargs, pop_flag=True) # type: ignore [arg-type] @@ -616,6 +736,10 @@ def __cancel_cache_handle(self, instance: I) -> None: Args: instance: The instance associated with the cache handle. + + Examples: + >>> bound_method = ASyncBoundMethod(instance, my_function, True) + >>> bound_method.__cancel_cache_handle(instance) """ cache_handle: asyncio.TimerHandle = self._cache_handle cache_handle.cancel() @@ -624,6 +748,25 @@ def __cancel_cache_handle(self, instance: I) -> None: class ASyncBoundMethodSyncDefault(ASyncBoundMethod[I, P, T]): """ A bound method with synchronous default behavior. + + Examples: + >>> class MyClass: + ... def __init__(self, value): + ... self.value = value + ... + ... @ASyncMethodDescriptorSyncDefault + ... def my_method(self): + ... return self.value + ... + >>> obj = MyClass(42) + >>> obj.my_method() + 42 + >>> await obj.my_method(sync=False) + 42 + + See Also: + - :class:`ASyncBoundMethod` + - :class:`ASyncMethodDescriptorSyncDefault` """ def __get__( @@ -636,8 +779,9 @@ def __get__( instance: The instance to bind the method to, or None. owner: The owner class. - Returns: - The bound method with synchronous default behavior. + Examples: + >>> descriptor = ASyncMethodDescriptorSyncDefault(my_function) + >>> bound_method = descriptor.__get__(instance, MyClass) """ return ASyncBoundMethod.__get__(self, instance, owner) @@ -665,8 +809,9 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: *args: Positional arguments. **kwargs: Keyword arguments. - Returns: - The result of the method call. + Examples: + >>> bound_method = ASyncBoundMethodSyncDefault(instance, my_function, True) + >>> bound_method(arg1, arg2, kwarg1=value1) """ return ASyncBoundMethod.__call__(self, *args, **kwargs) @@ -674,6 +819,25 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: class ASyncBoundMethodAsyncDefault(ASyncBoundMethod[I, P, T]): """ A bound method with asynchronous default behavior. + + Examples: + >>> class MyClass: + ... def __init__(self, value): + ... self.value = value + ... + ... @ASyncMethodDescriptorAsyncDefault + ... async def my_method(self): + ... return self.value + ... + >>> obj = MyClass(42) + >>> await obj.my_method() + 42 + >>> obj.my_method(sync=True) + 42 + + See Also: + - :class:`ASyncBoundMethod` + - :class:`ASyncMethodDescriptorAsyncDefault` """ def __get__(self, instance: I, owner: Type[I]) -> ASyncFunctionAsyncDefault[P, T]: @@ -684,8 +848,9 @@ def __get__(self, instance: I, owner: Type[I]) -> ASyncFunctionAsyncDefault[P, T instance: The instance to bind the method to. owner: The owner class. - Returns: - The bound method with asynchronous default behavior. + Examples: + >>> descriptor = ASyncMethodDescriptorAsyncDefault(my_function) + >>> bound_method = descriptor.__get__(instance, MyClass) """ return ASyncBoundMethod.__get__(self, instance, owner) @@ -713,7 +878,8 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, T]: *args: Positional arguments. **kwargs: Keyword arguments. - Returns: - A coroutine representing the asynchronous method call. + Examples: + >>> bound_method = ASyncBoundMethodAsyncDefault(instance, my_function, True) + >>> await bound_method(arg1, arg2, kwarg1=value1) """ - return ASyncBoundMethod.__call__(self, *args, **kwargs) + return ASyncBoundMethod.__call__(self, *args, **kwargs) \ No newline at end of file diff --git a/a_sync/a_sync/modifiers/limiter.py b/a_sync/a_sync/modifiers/limiter.py index 042cca69..e34c3fff 100644 --- a/a_sync/a_sync/modifiers/limiter.py +++ b/a_sync/a_sync/modifiers/limiter.py @@ -17,8 +17,18 @@ def apply_rate_limit( ) -> AsyncDecorator[P, T]: """Decorator to apply a rate limit to an asynchronous function. + This overload allows specifying the number of allowed executions per minute. + Args: runs_per_minute: The number of allowed executions per minute. + + Examples: + >>> @apply_rate_limit(60) + ... async def my_function(): + ... pass + + See Also: + :class:`aiolimiter.AsyncLimiter` """ @@ -29,9 +39,21 @@ def apply_rate_limit( ) -> CoroFn[P, T]: """Decorator to apply a rate limit to an asynchronous function. + This overload allows specifying either the number of allowed executions per minute + or an :class:`aiolimiter.AsyncLimiter` instance. + Args: coro_fn: The coroutine function to be rate-limited. - runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance. + runs_per_minute: The number of allowed executions per minute or an :class:`aiolimiter.AsyncLimiter` instance. + + Examples: + >>> async_limiter = AsyncLimiter(60) + >>> @apply_rate_limit(async_limiter) + ... async def my_function(): + ... pass + + See Also: + :class:`aiolimiter.AsyncLimiter` """ @@ -44,15 +66,28 @@ def apply_rate_limit( This function can be used as a decorator to limit the number of times an asynchronous function can be called per minute. It can be configured with either an integer specifying the number of runs per minute or an - AsyncLimiter instance. + :class:`aiolimiter.AsyncLimiter` instance. Args: - coro_fn: The coroutine function to be rate-limited. If an integer is provided, it is treated as runs per minute, and runs_per_minute should be None. - runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance. If coro_fn is an integer, this should be None. + coro_fn: The coroutine function to be rate-limited. If an integer is provided, it is treated as runs per minute, and `runs_per_minute` should be None. + runs_per_minute: The number of allowed executions per minute or an :class:`aiolimiter.AsyncLimiter` instance. If `coro_fn` is an integer, this should be None. Raises: - TypeError: If 'runs_per_minute' is neither an integer nor an AsyncLimiter when 'coro_fn' is None. - exceptions.FunctionNotAsync: If 'coro_fn' is not an asynchronous function. + TypeError: If `runs_per_minute` is neither an integer nor an :class:`aiolimiter.AsyncLimiter` when `coro_fn` is None. + exceptions.FunctionNotAsync: If `coro_fn` is not an asynchronous function. + + Examples: + >>> @apply_rate_limit(60) + ... async def my_function(): + ... pass + + >>> async_limiter = AsyncLimiter(60) + >>> @apply_rate_limit(async_limiter) + ... async def my_function(): + ... pass + + See Also: + :class:`aiolimiter.AsyncLimiter` """ # Parse Inputs if isinstance(coro_fn, (int, AsyncLimiter)): @@ -82,4 +117,4 @@ async def rate_limit_wrap(*args: P.args, **kwargs: P.kwargs) -> T: return rate_limit_wrap - return rate_limit_decorator if coro_fn is None else rate_limit_decorator(coro_fn) + return rate_limit_decorator if coro_fn is None else rate_limit_decorator(coro_fn) \ No newline at end of file diff --git a/a_sync/a_sync/modifiers/manager.py b/a_sync/a_sync/modifiers/manager.py index 5ebee735..f07772a3 100644 --- a/a_sync/a_sync/modifiers/manager.py +++ b/a_sync/a_sync/modifiers/manager.py @@ -5,6 +5,7 @@ from a_sync.a_sync.config import user_set_default_modifiers, null_modifiers from a_sync.a_sync.modifiers import cache, limiter, semaphores +# TODO give me a docstring valid_modifiers = [ key for key in ModifierKwargs.__annotations__ @@ -16,9 +17,35 @@ class ModifierManager(Dict[str, Any]): """Manages modifiers for asynchronous and synchronous functions. This class is responsible for applying modifiers to functions, such as - caching, rate limiting, and semaphores for asynchronous functions. + caching, rate limiting, and semaphores for asynchronous functions. It also + handles synchronous functions, although no sync modifiers are currently + implemented. + + Examples: + Creating a ModifierManager with specific modifiers: + + >>> modifiers = ModifierKwargs(cache_type='memory', runs_per_minute=60) + >>> manager = ModifierManager(modifiers) + + Applying modifiers to an asynchronous function: + + >>> async def my_coro(): + ... pass + >>> modified_coro = manager.apply_async_modifiers(my_coro) + + Applying modifiers to a synchronous function (no sync modifiers applied): + + >>> def my_function(): + ... pass + >>> modified_function = manager.apply_sync_modifiers(my_function) + + See Also: + - :class:`a_sync.a_sync.modifiers.cache` + - :class:`a_sync.a_sync.modifiers.limiter` + - :class:`a_sync.a_sync.modifiers.semaphores` """ + # TODO give us docstrings default: DefaultMode cache_type: CacheType cache_typed: bool @@ -26,9 +53,13 @@ class ModifierManager(Dict[str, Any]): ram_cache_ttl: Optional[int] runs_per_minute: Optional[int] semaphore: SemaphoreSpec + # sync modifiers executor: Executor - """This is not applied like a typical modifier. The executor is used to run the sync function in an asynchronous context.""" + """ + This is not applied like a typical modifier but is still passed through the library with them for convenience. + The executor is used to run the sync function in an asynchronous context. + """ __slots__ = ("_modifiers",) @@ -95,6 +126,12 @@ def apply_async_modifiers(self, coro_fn: CoroFn[P, T]) -> CoroFn[P, T]: Returns: The modified coroutine function. + + Examples: + >>> async def my_coro(): + ... pass + >>> manager = ModifierManager(ModifierKwargs(runs_per_minute=60)) + >>> modified_coro = manager.apply_async_modifiers(my_coro) """ # NOTE: THESE STACK IN REVERSE ORDER if self.use_limiter: @@ -122,6 +159,12 @@ def apply_sync_modifiers(self, function: SyncFn[P, T]) -> SyncFn[P, T]: Returns: The wrapped synchronous function. + + Examples: + >>> def my_function(): + ... pass + >>> manager = ModifierManager(ModifierKwargs()) + >>> modified_function = manager.apply_sync_modifiers(my_function) """ @functools.wraps(function) @@ -167,5 +210,6 @@ def __getitem__(self, modifier_key: str): return self._modifiers[modifier_key] # type: ignore [literal-required] +# TODO give us docstrings nulls = ModifierManager(null_modifiers) -user_defaults = ModifierManager(user_set_default_modifiers) +user_defaults = ModifierManager(user_set_default_modifiers) \ No newline at end of file diff --git a/a_sync/a_sync/modifiers/semaphores.py b/a_sync/a_sync/modifiers/semaphores.py index 3aafa067..94d03a01 100644 --- a/a_sync/a_sync/modifiers/semaphores.py +++ b/a_sync/a_sync/modifiers/semaphores.py @@ -14,13 +14,24 @@ def apply_semaphore( # type: ignore [misc] semaphore: SemaphoreSpec, ) -> AsyncDecorator[P, T]: - """Applies a semaphore to a coroutine function. + """Create a decorator to apply a semaphore to a coroutine function. This overload is used when the semaphore is provided as a single argument, returning a decorator that can be applied to a coroutine function. Args: - semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object. + semaphore (Union[int, asyncio.Semaphore, primitives.ThreadsafeSemaphore]): + The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, + or a `primitives.ThreadsafeSemaphore` object. + + Examples: + >>> @apply_semaphore(2) + ... async def limited_concurrent_function(): + ... pass + + See Also: + - :class:`asyncio.Semaphore` + - :class:`primitives.ThreadsafeSemaphore` """ @@ -29,14 +40,25 @@ def apply_semaphore( coro_fn: CoroFn[P, T], semaphore: SemaphoreSpec, ) -> CoroFn[P, T]: - """Applies a semaphore to a coroutine function. + """Apply a semaphore directly to a coroutine function. This overload is used when both the coroutine function and semaphore are provided, directly applying the semaphore to the coroutine function. Args: - coro_fn: The coroutine function to which the semaphore will be applied. - semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object. + coro_fn (Callable): The coroutine function to which the semaphore will be applied. + semaphore (Union[int, asyncio.Semaphore, primitives.ThreadsafeSemaphore]): + The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, + or a `primitives.ThreadsafeSemaphore` object. + + Examples: + >>> async def my_coroutine(): + ... pass + >>> my_coroutine = apply_semaphore(my_coroutine, 3) + + See Also: + - :class:`asyncio.Semaphore` + - :class:`primitives.ThreadsafeSemaphore` """ @@ -44,21 +66,38 @@ def apply_semaphore( coro_fn: Optional[Union[CoroFn[P, T], SemaphoreSpec]] = None, semaphore: SemaphoreSpec = None, ) -> AsyncDecoratorOrCoroFn[P, T]: - """Applies a semaphore to a coroutine function or returns a decorator. + """Apply a semaphore to a coroutine function or return a decorator. This function can be used to apply a semaphore to a coroutine function either by passing the coroutine function and semaphore as arguments or by using the semaphore as a decorator. It raises exceptions if the inputs are not valid. Args: - coro_fn: The coroutine function to which the semaphore will be applied, or None - if the semaphore is to be used as a decorator. - semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object. + coro_fn (Optional[Callable]): The coroutine function to which the semaphore will be applied, + or None if the semaphore is to be used as a decorator. + semaphore (Union[int, asyncio.Semaphore, primitives.ThreadsafeSemaphore]): + The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, + or a `primitives.ThreadsafeSemaphore` object. Raises: - ValueError: If both coro_fn and semaphore are provided as invalid inputs. + ValueError: If both `coro_fn` and `semaphore` are provided as invalid inputs. exceptions.FunctionNotAsync: If the provided function is not a coroutine. - TypeError: If the semaphore is not an integer or an asyncio.Semaphore object. + TypeError: If the semaphore is not an integer, an `asyncio.Semaphore`, or a `primitives.ThreadsafeSemaphore` object. + + Examples: + Using as a decorator: + >>> @apply_semaphore(2) + ... async def limited_concurrent_function(): + ... pass + + Applying directly to a function: + >>> async def my_coroutine(): + ... pass + >>> my_coroutine = apply_semaphore(my_coroutine, 3) + + See Also: + - :class:`asyncio.Semaphore` + - :class:`primitives.ThreadsafeSemaphore` """ # Parse Inputs if isinstance(coro_fn, (int, asyncio.Semaphore)): diff --git a/a_sync/executor.py b/a_sync/executor.py index d0e174dd..297f5236 100644 --- a/a_sync/executor.py +++ b/a_sync/executor.py @@ -1,7 +1,9 @@ +# /home/ubuntu/libs/a-sync/a_sync/executor.py + """ With these executors, you can simply run sync functions in your executor with `await executor.run(fn, *args)`. -`executor.submit(fn, *args)` will work the same as the concurrent.futures implementation, but will return an asyncio.Future instead of a concurrent.futures.Future. +`executor.submit(fn, *args)` will work the same as the `concurrent.futures` implementation, but will return an `asyncio.Future` instead of a `concurrent.futures.Future`. This module provides several executor classes: - _AsyncExecutorMixin: A mixin providing asynchronous run and submit methods, with support for synchronous mode. @@ -45,12 +47,21 @@ async def run(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): A shorthand way to call `await asyncio.get_event_loop().run_in_executor(this_executor, fn, *args)`. Doesn't `await this_executor.run(fn, *args)` look so much better? - Oh, and you can also use kwargs! + In synchronous mode, the function is executed directly in the current thread. + In asynchronous mode, the function is submitted to the executor and awaited. Args: fn: The function to run. *args: Positional arguments for the function. **kwargs: Keyword arguments for the function. + + Examples: + >>> async def example(): + >>> result = await executor.run(some_function, arg1, arg2, kwarg1=value1) + >>> print(result) + + See Also: + - :meth:`submit` for submitting functions to the executor. """ return ( fn(*args, **kwargs) @@ -60,12 +71,20 @@ async def run(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): def submit(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> "asyncio.Future[T]": # type: ignore [override] """ - Submits a job to the executor and returns an asyncio.Future that can be awaited for the result without blocking. + Submits a job to the executor and returns an `asyncio.Future` that can be awaited for the result without blocking. Args: fn: The function to submit. *args: Positional arguments for the function. **kwargs: Keyword arguments for the function. + + Examples: + >>> future = executor.submit(some_function, arg1, arg2, kwarg1=value1) + >>> result = await future + >>> print(result) + + See Also: + - :meth:`run` for running functions with the executor. """ if self.sync_mode: fut = asyncio.get_event_loop().create_future() @@ -89,6 +108,10 @@ def __len__(self) -> int: def sync_mode(self) -> bool: """ Indicates if the executor is in synchronous mode (max_workers == 0). + + Examples: + >>> if executor.sync_mode: + >>> print("Executor is in synchronous mode.") """ return self._max_workers == 0 @@ -96,6 +119,9 @@ def sync_mode(self) -> bool: def worker_count_current(self) -> int: """ Returns the current number of workers. + + Examples: + >>> print(f"Current worker count: {executor.worker_count_current}") """ return len(getattr(self, f"_{self._workers}")) @@ -108,6 +134,9 @@ async def _debug_daemon(self, fut: asyncio.Future, fn, *args, **kwargs) -> None: fn: The function being executed. *args: Positional arguments for the function. **kwargs: Keyword arguments for the function. + + See Also: + - :meth:`_start_debug_daemon` to start the debug daemon. """ # TODO: make prettier strings for other types if type(fn).__name__ == "function": @@ -171,6 +200,11 @@ def __init__( mp_context: The multiprocessing context. Defaults to None. initializer: An initializer callable. Defaults to None. initargs: Arguments for the initializer. Defaults to (). + + Examples: + >>> executor = AsyncProcessPoolExecutor(max_workers=4) + >>> future = executor.submit(some_function, arg1, arg2) + >>> result = await future """ if max_workers == 0: super().__init__(1, mp_context, initializer, initargs) @@ -213,6 +247,11 @@ def __init__( thread_name_prefix: Prefix for thread names. Defaults to ''. initializer: An initializer callable. Defaults to None. initargs: Arguments for the initializer. Defaults to (). + + Examples: + >>> executor = AsyncThreadPoolExecutor(max_workers=10, thread_name_prefix="MyThread") + >>> future = executor.submit(some_function, arg1, arg2) + >>> result = await future """ if max_workers == 0: super().__init__(1, thread_name_prefix, initializer, initargs) @@ -240,6 +279,9 @@ def _worker( initializer: The initializer function. initargs: Arguments for the initializer. timeout: Timeout duration for pruning inactive threads. + + See Also: + - :class:`PruningThreadPoolExecutor` for more details on thread pruning. """ if initializer is not None: try: @@ -326,6 +368,11 @@ def __init__( initializer: An initializer callable. Defaults to None. initargs: Arguments for the initializer. Defaults to (). timeout: Timeout duration for pruning inactive threads. Defaults to TEN_MINUTES. + + Examples: + >>> executor = PruningThreadPoolExecutor(max_workers=5, timeout=300) + >>> future = executor.submit(some_function, arg1, arg2) + >>> result = await future """ self._timeout = timeout @@ -342,6 +389,9 @@ def __len__(self) -> int: def _adjust_thread_count(self): """ Adjusts the number of threads based on workload and idle threads. + + See Also: + - :func:`_worker` for the worker function that handles thread pruning. """ with self._adjusting_lock: # if idle threads are available, don't spin new threads @@ -379,4 +429,4 @@ def weakref_cb(_, q=self._work_queue): "AsyncThreadPoolExecutor", "AsyncProcessPoolExecutor", "PruningThreadPoolExecutor", -] +] \ No newline at end of file diff --git a/a_sync/future.py b/a_sync/future.py index 3c2bdaf2..d6b72d50 100644 --- a/a_sync/future.py +++ b/a_sync/future.py @@ -39,6 +39,17 @@ def future( Args: callable: The callable to convert. Defaults to None. **kwargs: Additional keyword arguments for the modifier. + + Returns: + A callable that returns either the result or an ASyncFuture. + + Example: + >>> @future + ... async def async_function(): + ... return 42 + >>> result = async_function() + >>> isinstance(result, ASyncFuture) + True """ return _ASyncFutureWrappedFn(callable, **kwargs) @@ -49,6 +60,15 @@ async def _gather_check_and_materialize(*things: Unpack[MaybeAwaitable[T]]) -> L Args: *things: Items to gather and materialize. + + Returns: + A list of materialized items. + + Example: + >>> async def async_fn(x): + ... return x + >>> await _gather_check_and_materialize(async_fn(1), 2, async_fn(3)) + [1, 2, 3] """ return await asyncio.gather(*[_check_and_materialize(thing) for thing in things]) @@ -59,6 +79,15 @@ async def _check_and_materialize(thing: T) -> T: Args: thing: The item to check and materialize. + + Returns: + The materialized item. + + Example: + >>> async def async_fn(): + ... return 42 + >>> await _check_and_materialize(async_fn()) + 42 """ return await thing if isawaitable(thing) else thing @@ -67,11 +96,22 @@ def _materialize(meta: "ASyncFuture[T]") -> T: """ Materializes the result of an ASyncFuture. + This function attempts to run the event loop until the ASyncFuture is complete. + If the event loop is already running, it raises a RuntimeError. + Args: meta: The ASyncFuture to materialize. Raises: RuntimeError: If the event loop is running and the result cannot be awaited. + + Example: + >>> future = ASyncFuture(asyncio.sleep(1, result=42)) + >>> _materialize(future) + 42 + + See Also: + :class:`ASyncFuture` """ try: return asyncio.get_event_loop().run_until_complete(meta) @@ -91,6 +131,13 @@ class ASyncFuture(concurrent.futures.Future, Awaitable[T]): A class representing an asynchronous future result. Inherits from both concurrent.futures.Future and Awaitable[T], allowing it to be used in both synchronous and asynchronous contexts. + + Example: + >>> async def async_fn(): + ... return 42 + >>> future = ASyncFuture(async_fn()) + >>> await future + 42 """ __slots__ = "__awaitable__", "__dependencies", "__dependants", "__task" @@ -139,6 +186,9 @@ def __list_dependencies(self, other) -> List["ASyncFuture"]: Args: other: The other dependency to list. + + Returns: + A list of ASyncFuture dependencies. """ if isinstance(other, ASyncFuture): return [self, other] @@ -150,6 +200,14 @@ def result(self) -> Union[Callable[[], T], Any]: If this future is not done, it will work like cf.Future.result. It will block, await the awaitable, and return the result when ready. If this future is done and the result has attribute `result`, will return `getattr(future_result, 'result')` If this future is done and the result does NOT have attribute `result`, will again work like cf.Future.result + + Returns: + The result of the future or a callable to get the result. + + Example: + >>> future = ASyncFuture(asyncio.sleep(1, result=42)) + >>> future.result() + 42 """ if self.done(): if hasattr(r := super().result(), "result"): @@ -180,6 +238,14 @@ def __contains__(self, key: Any) -> bool: def __await__(self) -> Generator[Any, None, T]: """ Makes the ASyncFuture awaitable. + + Returns: + A generator for awaiting the future. + + Example: + >>> future = ASyncFuture(asyncio.sleep(1, result=42)) + >>> await future + 42 """ return self.__await().__await__() @@ -192,6 +258,15 @@ async def __await(self) -> T: def __task__(self) -> "asyncio.Task[T]": """ Returns the asyncio task associated with the awaitable, creating it if necessary. + + Returns: + The asyncio task. + + Example: + >>> future = ASyncFuture(asyncio.sleep(1, result=42)) + >>> task = future.__task__ + >>> isinstance(task, asyncio.Task) + True """ if self.__task is None: self.__task = asyncio.create_task(self.__awaitable__) @@ -821,6 +896,9 @@ def __float__(self) -> float: def __dependants__(self) -> Set["ASyncFuture"]: """ Returns the set of dependants for this ASyncFuture, including nested dependants. + + Returns: + A set of ASyncFuture dependants. """ dependants = set() for dep in self.__dependants: @@ -832,6 +910,9 @@ def __dependants__(self) -> Set["ASyncFuture"]: def __dependencies__(self) -> Set["ASyncFuture"]: """ Returns the set of dependencies for this ASyncFuture, including nested dependencies. + + Returns: + A set of ASyncFuture dependencies. """ dependencies = set() for dep in self.__dependencies: @@ -853,6 +934,14 @@ def __sizeof__(self) -> int: class _ASyncFutureWrappedFn(Callable[P, ASyncFuture[T]]): """ A callable class to wrap functions and return ASyncFuture objects. + + Example: + >>> def sync_fn(): + ... return 42 + >>> wrapped_fn = _ASyncFutureWrappedFn(sync_fn) + >>> future = wrapped_fn() + >>> isinstance(future, ASyncFuture) + True """ __slots__ = "callable", "wrapped", "_callable_name" @@ -902,6 +991,16 @@ class _ASyncFutureInstanceMethod(Generic[I, P, T]): # NOTE: probably could just replace this with functools.partial """ A class to handle instance methods wrapped as ASyncFuture. + + Example: + >>> class MyClass: + ... @_ASyncFutureWrappedFn + ... def method(self): + ... return 42 + >>> instance = MyClass() + >>> future = instance.method() + >>> isinstance(future, ASyncFuture) + True """ __module__: str @@ -964,4 +1063,4 @@ def __call__(self, /, *fn_args: P.args, **fn_kwargs: P.kwargs) -> T: return self.__wrapper(self.__instance, *fn_args, **fn_kwargs) -__all__ = ["future", "ASyncFuture"] +__all__ = ["future", "ASyncFuture"] \ No newline at end of file diff --git a/a_sync/task.py b/a_sync/task.py index 82223a74..c7a67c56 100644 --- a/a_sync/task.py +++ b/a_sync/task.py @@ -45,24 +45,30 @@ class TaskMapping(DefaultDict[K, "asyncio.Task[V]"], AsyncIterable[Tuple[K, V]]) """ A mapping of keys to asynchronous tasks with additional functionality. - TaskMapping is a specialized dictionary that maps keys to asyncio Tasks. It provides + `TaskMapping` is a specialized dictionary that maps keys to `asyncio` Tasks. It provides convenient methods for creating, managing, and iterating over these tasks asynchronously. + Tasks are created automatically for each key using a provided function. You cannot manually set items in a `TaskMapping` using dictionary-like syntax. + Example: >>> async def fetch_data(url: str) -> str: ... async with aiohttp.ClientSession() as session: ... async with session.get(url) as response: ... return await response.text() ... - >>> tasks = TaskMapping(fetch_data, name='url_fetcher', concurrency=5) - >>> tasks['example.com'] - >>> tasks['python.org'] + >>> tasks = TaskMapping(fetch_data, ['http://example.com', 'https://www.python.org'], name='url_fetcher', concurrency=5) >>> async for key, result in tasks: ... print(f"Data for {key}: {result}") ... Data for python.org: http://python.org Data for example.com: http://example.com + Note: + You cannot manually set items in a `TaskMapping` using dictionary-like syntax. Tasks are created and managed internally. + + See Also: + - :class:`asyncio.Task` + - :func:`asyncio.create_task` """ concurrency: Optional[int] = None @@ -114,6 +120,13 @@ def __init__( name: An optional name for the tasks created by this mapping. concurrency: Maximum number of tasks to run concurrently. **wrapped_func_kwargs: Additional keyword arguments to be passed to wrapped_func. + + Example: + async def process_item(item: int) -> int: + await asyncio.sleep(1) + return item * 2 + + task_map = TaskMapping(process_item, [1, 2, 3], concurrency=2) """ if concurrency: @@ -296,6 +309,15 @@ async def map( Yields: Depending on `yields`, either keys, values, or tuples of key-value pairs representing the results of completed tasks. + + Example: + async def process_item(item: int) -> int: + await asyncio.sleep(1) + return item * 2 + + task_map = TaskMapping(process_item) + async for key, result in task_map.map([1, 2, 3]): + print(f"Processed {key}: {result}") """ self._if_pop_check_destroyed(pop) @@ -421,6 +443,15 @@ async def yield_completed(self, pop: bool = True) -> AsyncIterator[Tuple[K, V]]: Yields: Tuples of key-value pairs representing the results of completed tasks. + + Example: + async def process_item(item: int) -> int: + await asyncio.sleep(1) + return item * 2 + + task_map = TaskMapping(process_item, [1, 2, 3]) + async for key, result in task_map.yield_completed(): + print(f"Completed {key}: {result}") """ if pop: for k, task in dict(self).items(): @@ -832,4 +863,4 @@ async def __aiter__(self) -> AsyncIterator[V]: "TaskMappingKeys", "TaskMappingValues", "TaskMappingItems", -] +] \ No newline at end of file