From 9f8e45c63dc76e2cb080516a9500ca0a6528cffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 5 Nov 2023 22:53:29 +0200 Subject: [PATCH] Exposed ResourceGuard in the public API (#630) Closes #627. --- docs/api.rst | 1 + docs/synchronization.rst | 19 +++++++++++++++++++ docs/versionhistory.rst | 1 + src/anyio/__init__.py | 1 + src/anyio/_core/_synchronization.py | 15 +++++++++++++-- 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5520d764..9af315ba 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -151,6 +151,7 @@ Synchronization .. autoclass:: anyio.Condition .. autoclass:: anyio.Semaphore .. autoclass:: anyio.CapacityLimiter +.. autoclass:: anyio.ResourceGuard .. autoclass:: anyio.LockStatistics .. autoclass:: anyio.EventStatistics diff --git a/docs/synchronization.rst b/docs/synchronization.rst index 5aa089bd..12ed2aa3 100644 --- a/docs/synchronization.rst +++ b/docs/synchronization.rst @@ -163,3 +163,22 @@ Example:: You can adjust the total number of tokens by setting a different value on the limiter's ``total_tokens`` property. + +Resource guards +--------------- + +Some resources, such as sockets, are very sensitive about concurrent use and should not +allow even attempts to be used concurrently. For such cases, :class:`ResourceGuard` is +the appropriate solution:: + + class Resource: + def __init__(self): + self._guard = ResourceGuard() + + async def do_something() -> None: + with self._guard: + ... + +Now, if another task tries calling the ``do_something()`` method on the same +``Resource`` instance before the first call has finished, that will raise a +:exc:`BusyResourceError`. diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index f2e1c2ef..906babfc 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -11,6 +11,7 @@ This library adheres to `Semantic Versioning 2.0 `_. instead of ``cancellable`` - Removed a checkpoint when exiting a task group - Bumped minimum version of trio to v0.23 +- Exposed the ``ResourceGuard`` class in the public API **4.0.0** diff --git a/src/anyio/__init__.py b/src/anyio/__init__.py index b84b7971..7bfe2316 100644 --- a/src/anyio/__init__.py +++ b/src/anyio/__init__.py @@ -51,6 +51,7 @@ from ._core._synchronization import EventStatistics as EventStatistics from ._core._synchronization import Lock as Lock from ._core._synchronization import LockStatistics as LockStatistics +from ._core._synchronization import ResourceGuard as ResourceGuard from ._core._synchronization import Semaphore as Semaphore from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED diff --git a/src/anyio/_core/_synchronization.py b/src/anyio/_core/_synchronization.py index ad8c5256..fdd4f5fb 100644 --- a/src/anyio/_core/_synchronization.py +++ b/src/anyio/_core/_synchronization.py @@ -483,10 +483,21 @@ def statistics(self) -> CapacityLimiterStatistics: class ResourceGuard: + """ + A context manager for ensuring that a resource is only used by a single task at a + time. + + Entering this context manager while the previous has not exited it yet will trigger + :exc:`BusyResourceError`. + + :param action: the action to guard against (visible in the :exc:`BusyResourceError` + when triggered, e.g. "Another task is already {action} this resource") + """ + __slots__ = "action", "_guarded" - def __init__(self, action: str): - self.action = action + def __init__(self, action: str = "using"): + self.action: str = action self._guarded = False def __enter__(self) -> None: