Skip to content

Releases: x42005e1f/aiologic

0.16.0

27 Nov 23:48
f386059

Choose a tag to compare

Added

  • aiologic.__version__ and aiologic.__version_tuple__ as a way to retrieve the package version at runtime.
  • aiologic.meta.SingletonEnum as a base class that encapsulates common logic of all type-checker-friendly singleton classes (such as aiologic.meta.DefaultType and aiologic.meta.MissingType).
  • aiologic.meta.resolve_name() as an alternative to importlib.util.resolve_name() with more consistent behavior.
  • aiologic.meta.import_module() as an alternative to importlib.import_module() with more consistent behavior.
  • aiologic.meta.import_from() as a way to import attributes from modules. It differs from more naive solutions in that it raises an ImportError instead of an AttributeError, while not allowing names like *, and also in that it attempts to import submodules to achieve the expected behavior.
  • aiologic.meta.export_dynamic() as an alternative to aiologic.meta.export_deprecated() that does not raise a DeprecationWarning. Useful for optional features that may be missing at runtime but should be available at the package level.
  • aiologic.meta.export() and aiologic.meta.export_deprecated() now support passing module objects, which should simplify their use for manually created module objects.
  • Final classes from aiologic.meta now support runtime introspection via the __final__ attribute on all supported versions of Python.

Changed

  • aiologic.meta.export() has been redesigned:
    • It now updates not only the __module__ attribute, but also name-related attributes. This provides the expected representation in cases where functions are created dynamically in a different context with a different name.
    • Attributes are only updated if the object type matches one of the supported types, which protects against undefined behavior due to problematic types (such as singletons that provide a read-only __module__ attribute).
    • When a class is encountered, its members are also updated (recursively). This allows its functions to be referenced safely. In addition, properties and class/static methods are now also processed.
    • All non-public attributes are now skipped, which should reduce the number of operations performed.
    • For each package, it now builds a human-readable __all__, which gives the expected behavior of from package import * (in particular, public subpackages are now excluded) and also simplifies analysis.
  • aiologic.meta.export_deprecated() now does additional checks before registering a link, and also relies on the new importlib-like functions, which should make it safer to use.
  • Non-public functions for importing original objects now implement aiologic.meta.import_from()-like behavior, making them more predictable (relevant for a green-patched world).
  • aiologic.Queue and its derivatives now raise a ValueError when attempting to pass a maxsize less than 1.
  • All timeouts now raise a ValueError for values less than zero.

Removed

  • All deprecated features (see 0.15.0).

Fixed

  • aiologic.meta.replaces() performed replacement by the name of the replaced function, which could lead to replacing an arbitrary object in the case of a different __name__ attribute value.
  • aiologic.meta.replaces() did not handle cases of parallel application to the same function, which could lead to an AttributeError being raised in such cases.
  • aiologic.meta.copies() used a function copying technique that was incompatible with Nuitka, resulting in a RuntimeError when attempting to call the copied function after compilation.
  • aiologic.meta.copies() used the default keyword argument values of the replaced function, which could lead to unexpected behavior when the default values were different.

0.15.0

05 Nov 11:28
422bb19

Choose a tag to compare

Added

  • aiologic.synchronized() as an async-aware alternative to wrapt.synchronized(). Related: GrahamDumpleton/wrapt#236.
  • aiologic.SimpleLifoQueue as a simplified LIFO queue, i.e. a lightweight alternative to aiologic.LifoQueue without maxsize support.
  • aiologic.BinarySemaphore and aiologic.BoundedBinarySemaphore as binary semaphores, i.e. semaphores restricted to the values 0 and 1 and using a more efficient implementation.
  • aiologic.RBarrier as a reusable barrier, i.e. a barrier that can be reset to its initial state (async-aware alternative to threading.Barrier).
  • aiologic.lowlevel.ThreadLock class (for typing purposes) and aiologic.lowlevel.create_thread_lock() factory function as a new way to obtain unpatched threading.Lock.
  • aiologic.lowlevel.ThreadRLock class (for typing purposes) and aiologic.lowlevel.create_thread_rlock() factory function as a unique way to obtain unpatched threading.RLock. Solves the problem of using reentrant thread-level locks in the gevent-patched world (due to the fact that threading._PyRLock.__globals__ referenced the patched namespace, which made it impossible to use the original object because it used the patched threading.Lock). Note: like threading._PyRLock, the fallback pure Python implementation is not signal-safe.
  • aiologic.lowlevel.ThreadOnceLock class (for typing purposes) and aiologic.lowlevel.create_thread_oncelock() factory function as a way to obtain a one-time reentrant lock. The interface mimics that of aiologic.lowlevel.ThreadRLock, but the semantics are different: the first successful release() call, which sets the internal counter to zero, wakes up all threads at once (just like aiologic.Event), and all further acquire() calls become no-ops, which effectively turns the lock into a dummy primitive. And unlike aiologic.lowlevel.ThreadRLock, this primitive is signal-safe, which, combined with the described semantics, makes the primitive suitable for protecting initialization sections.
  • aiologic.lowlevel.ThreadDummyLock class (for typing purposes) and aiologic.lowlevel.THREAD_DUMMY_LOCK singleton object as a way to obtain a dummy lock.
  • aiologic.lowlevel.once decorator to ensure that a function is executed only once (inspired by std::sync::Once from Rust). It uses aiologic.lowlevel.ThreadOnceLock under the hood and stores the result in the wrapper's closure, which makes the function both thread-safe and signal-safe when reentrant=True is passed (note: this does not apply to side effects!).
  • aiologic.lowlevel.lazydeque as a thread-safe/signal-safe wrapper for collections.deque with lazy initialization. It solves the problem of deques' high memory usage: one empty instance of collections.deque takes up 760 bytes on Python 3.11+ (for comparison, one empty list takes up only 56 bytes!). In contrast, one empty instance of aiologic.lowlevel.lazydeque takes up 128 bytes in total, and after initialization (first addition) takes up 832 bytes on Python 3.11+. Free-threading adds an additional 16 bytes in all cases (due to the internal use of aiologic.lowlevel.ThreadOnceLock).
  • aiologic.lowlevel.lazyqueue as a thread-safe/signal-safe wrapper for _queue.SimpleQueue (when available) or collections.deque with lazy initialization. It provides a non-blocking queue and differs from aiologic.lowlevel.lazydeque in that it is more memory efficient at the cost of less functionality. Instead of 128 and 832 bytes, it takes up 120 and 200 bytes on Python 3.13+.
  • aiologic.lowlevel.create_green_waiter() and aiologic.lowlevel.create_async_waiter() as functions to create waiters, i.e. new low-level primitives that encapsulate library-specific wait-wake logic. Unlike low-level events, they have no state, and thus have less efficiency for multiple notifications (in particular, they schedule calls regardless of whether the wait has been completed or not). And, of course, for the same reason, they are even less safe (they require more specific conditions for their correct operation).
  • aiologic.lowlevel.enable_signal_safety() and aiologic.lowlevel.disable_signal_safety() universal decorators to enable and disable signal-safety in the current thread's context. They support awaitable objects, coroutine functions, and green functions, and can be used directly as context managers.
  • aiologic.lowlevel.signal_safety_enabled() to determine if signal-safety is enabled.
  • aiologic.lowlevel.create_green_event() and aiologic.lowlevel.create_async_event() as a new way to create low-level events.
  • aiologic.lowlevel.enable_checkpoints() and aiologic.lowlevel.disable_checkpoints() universal decorators to enable and disable checkpoints in the current thread's context. They support awaitable objects, coroutine functions, and green functions, and can be used directly as context managers.
  • aiologic.lowlevel.green_checkpoint_enabled() and aiologic.lowlevel.async_checkpoint_enabled() to determine if checkpoints are enabled for the current library.
  • aiologic.lowlevel.green_checkpoint_if_cancelled() and aiologic.lowlevel.async_checkpoint_if_cancelled(). Currently, aiologic.lowlevel.async_checkpoint_if_cancelled() is equivalent to removed aiologic.lowlevel.checkpoint_if_cancelled(), and aiologic.lowlevel.green_checkpoint_if_cancelled() does nothing. However, these methods have a slightly different meaning: they are intended to accompany aiologic.lowlevel.shield() calls to pre-check for cancellation, and do not guarantee actual checking.
  • aiologic.lowlevel.green_clock() and aiologic.lowlevel.async_clock() as a way to get the current time according to the current library's internal monotonic clock (useful for sleep-until functions).
  • aiologic.lowlevel.green_sleep() and aiologic.lowlevel.async_sleep() to suspend the current task for the given number of seconds.
  • aiologic.lowlevel.green_sleep_until() and aiologic.lowlevel.async_sleep_until() to suspend the current task until the given deadline (relative to the current library's internal monotonic clock).
  • aiologic.lowlevel.green_sleep_forever() and aiologic.lowlevel.async_sleep_forever() to suspend the current task until an exception occurs.
  • aiologic.lowlevel.green_seconds_per_sleep() and aiologic.lowlevel.async_seconds_per_sleep() as a way to get the number of seconds during which sleep guarantees exactly one checkpoint. If sleep exceeds this time, it will use multiple calls (to bypass library limitations).
  • aiologic.lowlevel.green_seconds_per_timeout() and aiologic.lowlevel.async_seconds_per_timeout() that are the same as their sleep equivalents, but for timeouts (applies to low-level waiters/events, as well as all high-level primitives).
  • copy() method to flags and queues as a way to create a shallow copy without additional imports.
  • async_borrowed() and green_borrowed() methods to capacity limiters, green_owned() and async_owned() methods to locks. They allow to reliably check if the current task is holding the primitive (or any of its tokens) without importing additional functions.
  • async_count() and green_count() to reentrant primitives for the same purpose, but returning how many releases need to be made before the primitive is actually released by the current task.
  • Reentrant primitives can now be acquired and released multiple times in a single call.
  • Multi-use barriers (cyclic and reusable) can now be used as context managers, which simplifies error handling.
  • for_() method to condition variables as an async analog of wait_for().
  • Conditional variables now support user-defined timers. They can be passed to the constructor, called via the timer property, and used to pass the deadline to the notification methods.
  • Low-level events can now support aiologic.lowlevel.ThreadOnceLock methods by passing locking=True. This allows to synchronize related one-time operations with less memory overhead.
  • Low-level events can now be shielded from external cancellation by passing shield=True. This allows to implement efficient finalization strategies while preserving the one-time nature of low-level events.
  • Low-level events can now be forced (like checkpoints) by passing force=True. This allows to use existing event objects instead of checkpoints to minimize possible overhead.
  • aiologic.lowlevel.SET_EVENT and aiologic.lowlevel.CANCELLED_EVENT as variants of aiologic.lowlevel.DUMMY_EVENT for a set event and a cancelled event respectively. In fact, aiologic.lowlevel.SET_EVENT is just a copy of aiologic.lowlevel.DUMMY_EVENT, but to avoid confusion both variants will coexist (maybe temporarily, maybe not).
  • aiologic.meta subpackage for metaprogramming purposes.
  • aiologic.meta.MISSING as a marker for parameters that, when not passed, specify special default behavior.
  • aiologic.meta.DEFAULT as a marker for parameters with default values.
  • aiologic.meta.copies() to replace a function with a copy of another.
  • aiologic.meta.replaces() to replace a function of the same name in a certain namespace.
  • aiologic.meta.export() to export all module content on behalf of the module itself (by updating __module__).
  • aiologic.meta.export_deprecated() to export deprecated content via custom __getattr__().
  • aiologic.meta.await_for() to use awaitable primitives via functions that only accept asynchronous functions.
  • AIOLOGIC_GREEN_CHECKPOINTS and AIOLOGIC_ASYNC_CHECKPOINTS environment variables.
  • AIOLOGIC_PERFECT_FAIRNESS environment variable.

Changed

  • In previous versions, aiologic implicitly provided a strong fairness guarantee, informally called "perfect fairness". The point of this guarantee is to ensure the fairness of wakeups when they are parallel in nature. This had strong effects such as resuming all threads at once (no one is slee...
Read more

0.14.0

12 Feb 14:44
0f7dcad

Choose a tag to compare

Added

  • Experimental curio support.

Changed

  • Support for libraries has been redefined with wrapt via post import hooks. Previously, available libraries were determined after the first use, which could lead to unexpected behavior in interactive scenarios. Now support for a library is activated when the corresponding library is imported.
  • Support for greenlet-based libraries has been simplified. Detecting the current green library is now done by hub: if any eventlet or gevent function was called in the current thread, the current thread starts using the corresponding library. This eliminates the need to specify a green library both before and after monkey patching.
  • Corrected type annotations:
    • aiologic.BoundedSemaphore extends aiologic.Semaphore, and aiologic.Semaphore returns an instance of aiologic.BoundedSemaphore when passing max_value. Previously, the classes were independent in stubs, which was inconsistent with the behavior added in 0.2.0.
    • aiologic.lowlevel.current_thread() returns Optional[threading.Thread]. Previously, threading.Thread was returned, which was inconsistent with the special handling of threading._DummyThread.

Removed

  • Aliases to old modules (affects objects pickled before 0.13.0).
  • Patcher-related exports (such as aiologic.lowlevel.start_new_thread()).
  • aiologic.lowlevel.current_async_library_cvar: the sniffio equivalent is deprecated and not used by modern libraries, and the performance impact of using it is only negative.

Fixed

  • asyncio was not considered running when the current task was None. This resulted in the inability to use any async functions in asyncio REPR without explicitly setting the current async library. Related: python-trio/sniffio#35.
  • There was a missing branch for the optimistic case of non-waiting lock acquiring during race condition, which caused hangs in a free-threaded mode (0.13.1 regression).

0.13.1

24 Jan 15:52
517b4fd

Choose a tag to compare

Fixed

  • Optimized the event removal in locks and semaphores. Previously, removing an event from the waiting queue was performed even when it was successfully set, which caused serious slowdown due to expensive O(n) operations. Now the removal is only performed in case of failure — on cancellation, in particular timeouts, and exceptions. (#5).

0.13.0

19 Jan 13:15
039f6ae

Choose a tag to compare

Added

  • Type annotations via stubs. They are tested via mypy and pyright, but may cause ambiguities in some IDEs that do not support overloading well.

Changed

  • The source code tree has been significantly changed for better IDEs support. The same applies to exports, which are now checker-friendly. Objects pickled in previous versions refer to the old tree, so aliases to old modules have been temporarily added. If you have these, it is strongly recommended to repickle them with this version to support future versions.

0.12.0

14 Dec 05:30
5042457

Choose a tag to compare

Changed

  • Support for cancellation and timeouts has been dramatically improved. The cancel() method (and accompanying is_cancelled()) has been added to low-level events, which always returns True after the first successful call before set() and False otherwise. Previously, cancellation handling used the same set() call, which resulted in false negatives, in particular unnecessary wakes.

Fixed

  • Added missing await keyword in aiologic.Condition, without which it did not restore the state of the wrapped aiologic.RLock (0.11.0 regression).
  • Priority queue initialization now ensures the heap invariant for the passed list. Previously, passing a list with an incorrect order of elements violated the order in which the priority queue elements were returned.
  • Low-level event classes are now created in exclusive mode, eliminating possible slowdown in method resolution.

0.11.0

08 Nov 14:44
54d0b3d

Choose a tag to compare

Added

  • aiologic.lowlevel.async_checkpoint() as an alias for aiologic.lowlevel.checkpoint().

Changed

  • The capabilities of aiologic.Condition have been extended. Since there is no clear definition of which locks it should wrap, support for sync-only locks such as threading.Lock has been added. Passing another instance of aiologic.Condition is now supported too, and implies copying a reference to its lock. Also, passing None now specifies a different behavior whereby aiologic.Condition acts as lockless: its methods ignore the lock, which should help to use aiologic.Condition as a simple replacement for removed aiologic.ParkingLot.

0.10.0

04 Nov 01:36
e871010

Choose a tag to compare

Changed

  • aiologic.lowlevel.shield() function, which protects the call from cancellation, has been replaced by aiologic.lowlevel.repeat_if_cancelled(), which, depending on the library, either has the same action or repeats the call until it completes (successfully or unsuccessfully). Previously anyio.CancelScope was used, which did not really shield the call from cancellation in ways outside of anyio, such as task.cancel(), so its use was abandoned. However, asyncio.shield() starts a new task, which has a negative impact on performance and does not match the expected single-switch behavior. This is why repeat instead of shield was chosen. This change directly affects aiologic.Condition.

0.9.0

02 Nov 18:27
9e5df22

Choose a tag to compare

Changed

  • aiologic.CountdownEvent is significantly improved. Instead of using the initial value for the representation, it now uses the current value, so that its state is saved when the pickle module is used, just like the other events. Also added the ability to increase the current value by more than one per call, which should help with performance in some scenarios. The new clear() method, which atomically resets the current value, has the same purpose.
  • Changed the detection of the current green library after applying the monkey patch. Now the library that applied the patch is set only for the main thread, and the default library (usually threading) is used for all others. This should eliminate redundancy when using worker threads, which previously had to explicitly set threading to avoid accidentally creating a new hub and hence an event loop.

0.8.1

30 Oct 16:37
c3df65c

Choose a tag to compare

Fixed

  • The future used by the asyncio event could be canceled due to the event loop shutdown, causing InvalidStateError to be raised due to the callback execution. In particular, this was detected when using call_soon() for notification methods.