Source code for phantom.sized

"""
Types describing collections with size boundaries. These types should only be used with
immutable collections. There is a naive check that eliminates some of the most common
mutable collections in the instance check. However, a guaranteed check is probably
impossible to implement, so some amount of developer discipline is required.

Sized types are created by subclassing :py:class:`PhantomSized` and providing a
predicate that will be called with the size of the tested collection. For instance,
:py:class:`NonEmpty` is implemented using ``len=numeric.greater(0)``.

This made-up type would describe sized collections with between 5 and 10 ints:

.. code-block:: python

    class SpecificSize(PhantomSized[int], len=interval.open(5, 10)):
        ...
"""
from typing import Any
from typing import Generic
from typing import Iterable
from typing import Sized
from typing import TypeVar

# We attempt to import _ProtocolMeta from typing_extensions to support Python 3.7 but
# fall back the typing module to support Python 3.8+. This is the closest I could find
# to documentation of _ProtocolMeta.
# https://github.com/python/cpython/commit/74d7f76e2c953fbfdb7ce01b7319d91d471cc5ef
try:
    from typing_extensions import _ProtocolMeta  # type: ignore[attr-defined]
except ImportError:
    from typing import _ProtocolMeta

from typing_extensions import Protocol
from typing_extensions import runtime_checkable

from . import Phantom
from . import PhantomMeta
from . import Predicate
from ._utils.misc import is_not_mutable_instance
from .predicates import boolean
from .predicates import collection
from .predicates import generic
from .predicates import numeric
from .schema import Schema

__all__ = (
    "SizedIterable",
    "PhantomSized",
    "NonEmpty",
    "Empty",
)


T = TypeVar("T", bound=object, covariant=True)


[docs]@runtime_checkable class SizedIterable(Sized, Iterable[T], Protocol[T]): """Intersection of :py:class:`typing.Sized` and :py:class:`typing.Iterable`."""
class SizedIterablePhantomMeta(PhantomMeta, _ProtocolMeta): # type: ignore[misc] ...
[docs]class PhantomSized( Phantom[Sized], SizedIterable[T], Generic[T], metaclass=SizedIterablePhantomMeta, bound=SizedIterable, abstract=True, ): """Takes class argument ``len: Predicate[int]``.""" def __init_subclass__(cls, len: Predicate[int], **kwargs: Any) -> None: super().__init_subclass__( predicate=boolean.both( is_not_mutable_instance, collection.count(len), ), **kwargs, ) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), # type: ignore[misc] "type": "array", }
[docs]class NonEmpty(PhantomSized[T], Generic[T], len=numeric.greater(0)): """A sized collection with at least one item.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), # type: ignore[misc] "description": "A non-empty array.", "minItems": 1, }
[docs]class Empty(PhantomSized[T], Generic[T], len=generic.equal(0)): """A sized collection with exactly zero items.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), # type: ignore[misc] "description": "An empty array.", "maxItems": 0, }