Source code for zixy.container.base

# Copyright 2026 Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Base classes for containers with an ownership model and underlying Rust-bound data."""

from __future__ import annotations

import builtins
from abc import abstractmethod
from collections.abc import Callable, Iterator, Sequence, Sized
from functools import wraps
from typing import (
    Any,
    Concatenate,
    Generic,
    ParamSpec,
    Protocol,
    TypeVar,
    cast,
    overload,
)

from typing_extensions import Self

from zixy.utils import slice_index, slice_len

P = ParamSpec("P")
R = TypeVar("R")
OwnT = TypeVar("OwnT", bound="SupportsOwnership")
OutT = TypeVar("OutT", bound="ViewableBase[Any, Any]")


[docs] class SupportsOwnership(Protocol): """Protocol for classes supporting the ownership model."""
[docs] def is_owning(self) -> bool: """Check if ``self`` is owning (i.e. not a view).""" ...
[docs] def clone(self) -> Self: """Return a deep copy of ``self``.""" ...
[docs] def requires_ownership( method: Callable[Concatenate[OwnT, P], R], ) -> Callable[Concatenate[OwnT, P], R]: """Decorator for methods that can only be called on owning views. Args: method: Method to decorate. Returns: Decorated method that raises a ``ValueError`` if called on a non-owning view. """ @wraps(method) def wrapper(self: OwnT, *args: P.args, **kwargs: P.kwargs) -> R: if not self.is_owning(): raise ValueError( f"Cannot call {method.__name__} on a non-owning view. Consider cloning the object " "first using the .clone() method." ) return method(self, *args, **kwargs) return cast(Callable[Concatenate[OwnT, P], R], wrapper)
[docs] class Resizable(Sized, Protocol): """Protocol for :class:`Sized` classes that support resizing."""
[docs] def resize(self, n: int) -> None: """Resize the underlying container. Args: n: The new size of the container. Note: This method operates in-place. Raises: ValueError: If the container is a view. """ ...
T = TypeVar("T") ImplT = TypeVar("ImplT", bound=Resizable) IndexerT = TypeVar("IndexerT", int | None, slice)
[docs] class ViewableBase(Generic[ImplT, IndexerT]): """Abstract base class for classes with an ownership model and underlying Rust-bound data.""" _impl: ImplT @classmethod @abstractmethod def _create(cls, impl: ImplT, indexer: IndexerT) -> Self: """Create an instance of ``cls``. Args: impl: Rust-bound object containing the data for this instance. indexer: Index or slice of the data in ``impl`` that this instance should view. The default value of ``None`` for an ``int`` or ``slice(None)`` for a ``slice`` indicates that this instance is considered to be owning. Returns: A new instance of ``cls``. """ pass
[docs] @abstractmethod def is_owning(self) -> bool: """Check if ``self`` is owning (i.e. not a view).""" pass
[docs] @abstractmethod def clone(self) -> Self: """Return a deep copy of ``self``.""" pass
[docs] def into(self, t: type[OutT]) -> OutT: """Clone ``self`` into a new related container of type ``t``. Args: t: Type of the new container to create. Returns: A new instance of ``t`` containing the same data as ``self``. """ from zixy.container.convert import into # noqa: PLC0415 return into(self, t)
[docs] class ViewableItem(ViewableBase[ImplT, int | None]): """Abstract base class for items with an ownership model and underlying Rust-bound data.""" _index: int | None @classmethod @abstractmethod def _create(cls, impl: ImplT, indexer: int | None = None) -> Self: """Create an instance of ``cls``. Args: impl: Rust-bound object containing the data for this item. indexer: Index of the item within ``impl``. If ``None``, this instance is considered to be owning. Returns: A new instance of ``cls``. """ pass
[docs] def is_owning(self) -> bool: """Check if ``self`` is owning (i.e. not a view).""" return self._index is None
@property def index(self) -> int: """Get the index of ``self`` within its underlying data.""" return 0 if self._index is None else self._index
[docs] class ViewableSequence(ViewableBase[ImplT, slice], Generic[T, ImplT], Sequence[T]): """Abstract base class for sequences with an ownership model and underlying Rust-bound data.""" _slice: slice @classmethod @abstractmethod def _create(cls, impl: ImplT, indexer: slice = slice(None)) -> Self: """Create a new instance of ``cls``. Args: impl: Rust-bound object containing the data for this sequence. indexer: Slice of the data in ``impl`` that this instance should view. The default value of ``slice(None)`` indicates that this instance is considered to be owning. Returns: A new instance of ``cls``. """ pass @classmethod def _create_view(cls, impl: ImplT) -> Self: """Factory method to create a viewing instance of ``cls``. Args: impl: Rust-bound object containing the data for this sequence. Returns: A viewing instance of ``cls``. """ return cls._create(impl, slice(None, len(impl))) @property def slice(self) -> builtins.slice: """Get the slice of the underlying data that ``self`` views.""" return self._slice
[docs] def map_index(self, i: int) -> int: """Map an index in ``self`` to an index in the underlying data. Args: i: Index in ``self``. Returns: Corresponding index in the underlying data. """ return slice_index(self._slice, i, len(self._impl))
@overload def __getitem__(self, indexer: int) -> T: ... @overload def __getitem__(self, indexer: builtins.slice) -> Self: ...
[docs] def __getitem__(self, indexer: int | builtins.slice) -> T | Self: """Get the element or elements selected by ``indexer``. Args: indexer: Index or slice selecting the element(s) to return. Returns: Element or slice selected by ``indexer``. """ raise NotImplementedError
[docs] def __iter__(self) -> Iterator[T]: """Iterate over the elements of ``self``.""" for i in range(len(self)): yield self[i]
@abstractmethod def _empty_clone(self) -> Self: """Get an empty (owning, contiguous) clone of ``self``.""" pass
[docs] def __len__(self) -> int: """Get the number of elements in ``self``.""" return slice_len(self._slice, len(self._impl))
[docs] def as_view(self) -> Self: """Return a view of ``self``. Returns: If ``self`` is owning, a new view on the same underlying data, otherwise ``self``. """ if self.is_owning(): return self._create_view(self._impl) else: return self
[docs] def is_owning(self) -> bool: """Check if ``self`` is owning (i.e. not a view).""" return self.slice == slice(None)
[docs] @requires_ownership def resize(self, n: int) -> Self: """Resize the underlying container. Args: n: The new size of the container. Note: This method operates in-place. Raises: ValueError: If the container is a view. """ self._impl.resize(n) assert len(self) == n return self
[docs] @classmethod def from_view(cls, source: Self) -> Self: """Create a new instance of ``cls`` from a view. Args: source: View to clone into the new instance. Returns: An owning clone of ``source``. """ return source.clone()
[docs] @classmethod def from_size(cls, n: int) -> Self: """Create a new instance of ``cls`` with the given size. Args: n: The size of the new instance. Returns: An instance of ``cls`` with the given size. """ out = cls() out.resize(n) return out