Skip to content

Commit

Permalink
chore: move all 'disposable' components into one module
Browse files Browse the repository at this point in the history
Included also are minor bug fixes and a few other improvements.
  • Loading branch information
kennedykori committed Sep 6, 2023
1 parent c12d52d commit 1227884
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 291 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sghi.disposable.disposable.Disposable
=====================================
sghi.disposable.Disposable
==========================

.. currentmodule:: sghi.disposable.disposable
.. currentmodule:: sghi.disposable

.. autoclass:: Disposable
:members:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sghi.disposable.exceptions.ResourceDisposedError
================================================
sghi.disposable.ResourceDisposedError
=====================================

.. currentmodule:: sghi.disposable.exceptions
.. currentmodule:: sghi.disposable

.. autoexception:: ResourceDisposedError
:members:
Expand Down

This file was deleted.

22 changes: 0 additions & 22 deletions app/sghi-commons/docs/source/api/sghi.disposable.decorators.rst

This file was deleted.

22 changes: 0 additions & 22 deletions app/sghi-commons/docs/source/api/sghi.disposable.disposable.rst

This file was deleted.

26 changes: 0 additions & 26 deletions app/sghi-commons/docs/source/api/sghi.disposable.exceptions.rst

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sghi.disposable.not\_disposed
=============================

.. currentmodule:: sghi.disposable

.. autofunction:: not_disposed
24 changes: 16 additions & 8 deletions app/sghi-commons/docs/source/api/sghi.disposable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,33 @@



.. rubric:: Functions

.. autosummary::
:toctree:

not_disposed





.. rubric:: Classes

.. autosummary::
:toctree:
:template: class.rst

Disposable



.. rubric:: Modules

.. autosummary::
:toctree:
:template: module.rst
:recursive:

sghi.disposable.decorators
sghi.disposable.disposable
sghi.disposable.exceptions
.. rubric:: Exceptions

.. autosummary::
:toctree:
:template: exception.rst

ResourceDisposedError
206 changes: 203 additions & 3 deletions app/sghi-commons/src/sghi/disposable/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,206 @@
from .decorators import not_disposed
from .disposable import Disposable
from .exceptions import ResourceDisposedError
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from contextlib import AbstractContextManager
from functools import wraps
from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar, overload

from ..exceptions import SGHIError

if TYPE_CHECKING:
from collections.abc import Callable
from types import TracebackType


# =============================================================================
# TYPES
# =============================================================================


_DE = TypeVar("_DE", bound="ResourceDisposedError")
_DT = TypeVar("_DT", bound="Disposable")
_P = ParamSpec("_P")
_RT = TypeVar("_RT")


# =============================================================================
# EXCEPTIONS
# =============================================================================


class ResourceDisposedError(SGHIError):
"""Indicates that a :class:`Disposable` item has already been disposed."""

def __init__(self, message: str | None = "Resource already disposed."):
"""
Initialize a new instance of `ResourceDisposedError`.
:param message: Optional custom error message. If not provided, a
default message indicating that the resource is already disposed
will be used.
"""
super().__init__(message=message)


# =============================================================================
# DISPOSABLE MIXIN
# =============================================================================


class Disposable(AbstractContextManager, metaclass=ABCMeta):
"""An entity that uses resources that need to be cleaned up.
As such, this interface supports the
:doc:`context manager protocol<python:library/contextlib>` making its
derivatives usable with Python's ``with`` statement. Implementors should
override the :meth:`dispose` method and define the resource clean up logic.
The :attr:`is_disposed` property can be used to check whether an instance
has been disposed.
"""

__slots__ = ()

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool | None:
"""Exit the context manager and call the :meth:`dispose` method.
:param exc_type: The type of the exception being handled. If an
exception was raised and is being propagated, this will be the
exception type. Otherwise, it will be ``None``.
:param exc_val: The exception instance. If no exception was raised,
this will be ``None``.
:param exc_tb: The traceback for the exception. If no exception was
raised, this will be ``None``.
:return: `False`.
"""
super().__exit__(exc_type, exc_val, exc_tb)
self.dispose()
return False

@property
@abstractmethod
def is_disposed(self) -> bool:
"""
Return ``True`` if this object has already been disposed, ``False``
otherwise.
:return: ``True`` if this object has been disposed, ``False``
otherwise.
"""
...

@abstractmethod
def dispose(self) -> None:
"""Release any underlying resources contained by this object.
After this method returns successfully, the :attr:`is_disposed`
property should return ``True``.
.. note::
Unless otherwise specified, trying to use methods of a
``Disposable`` instance decorated with the
:class:`~.decorators.not_disposed` decorator after this
method returns should generally be considered a programming error
and should result in a :exc:`ResourceDisposedError` being raised.
This method should be idempotent allowing it to be called more
than once; only the first call, however, should have an effect.
:return: None.
"""
...


# =============================================================================
# DECORATORS
# =============================================================================


@overload
def not_disposed(
f: Callable[Concatenate[_DT, _P], _RT],
*,
exc_factory: Callable[[], _DE] = ResourceDisposedError,
) -> Callable[Concatenate[_DT, _P], _RT]:
...


@overload
def not_disposed(
f: None = None,
*,
exc_factory: Callable[[], _DE] = ResourceDisposedError,
) -> Callable[
[Callable[Concatenate[_DT, _P], _RT]],
Callable[Concatenate[_DT, _P], _RT],
]:
...


def not_disposed(
f: Callable[Concatenate[_DT, _P], _RT] | None = None,
*,
exc_factory: Callable[[], _DE] = ResourceDisposedError,
) -> Callable[Concatenate[_DT, _P], _RT] | Callable[
[Callable[Concatenate[_DT, _P], _RT]],
Callable[Concatenate[_DT, _P], _RT],
]:
"""Decorate a function with the resource disposal check.
This decorator ensures a :class:`Disposable` item has not been disposed. If
the item is disposed, i.e. the :attr:`~Disposable.is_disposed` property
returns ``True``, then an instance of :exc:`ResourceDisposedError` or it's
derivatives is raised.
.. important::
This decorator *MUST* be used on methods bound to an instance of the
``Disposable`` interface. It requires that the first parameter of the
decorated method, i.e. ``self``, be an instance of ``Disposable``.
:param f: The function to be decorated. The first argument of the
function should be a ``Disposable`` instance.
:param exc_factory: An optional callable that creates instances of
``ResourceDisposedError`` or its subclasses. This is only called
if the resource is disposed. If not provided, a default factory
that creates ``ResourceDisposedError`` instances will be used.
:return: The decorated function.
"""
def wrap(
_f: Callable[Concatenate[_DT, _P], _RT],
) -> Callable[Concatenate[_DT, _P], _RT]:

@wraps(_f)
def wrapper(
disposable: _DT,
*args: _P.args,
**kwargs: _P.kwargs,
) -> _RT:
if disposable.is_disposed:
raise exc_factory()
return _f(disposable, *args, **kwargs)

return wrapper

# Whether `f` is None or not depends on the usage of the decorator. It's a
# method when used as `@not_disposed` and None when used as
# `@not_disposed()` or `@not_disposed(exc_factory=...)`.
if f is None:
return wrap

return wrap(f)


# =============================================================================
# MODULE EXPORTS
# =============================================================================


__all__ = [
"Disposable",
Expand Down
Loading

0 comments on commit 1227884

Please sign in to comment.