Source code for napari.utils.events.containers._typed

import logging
from typing import (
    Any,
    Callable,
    Dict,
    Iterable,
    List,
    MutableSequence,
    Sequence,
    Type,
    TypeVar,
    Union,
    overload,
)

# change on import from typing when drop python 3.10 support
from typing_extensions import Self

from napari.utils.translations import trans

logger = logging.getLogger(__name__)


Index = Union[int, slice]

_T = TypeVar("_T")
_L = TypeVar("_L")


[docs] class TypedMutableSequence(MutableSequence[_T]): """List mixin that enforces item type, and enables custom indexing. Parameters ---------- data : iterable, optional Elements to initialize the list with. basetype : type or sequence of types, optional Type of the elements in the list. If a basetype (or multiple) is provided, then a TypeError will be raised when attempting to add an item to this sequence if it is not an instance of one of the types in ``basetype``. lookup : dict of Type[L] : function(object) -> L Mapping between a type, and a function that converts items in the list to that type. This is used for custom indexing. For example, if a ``lookup`` of {str: lambda x: x.name} is provided, then you can index into the list using ``list['frank']`` and it will search for an object whos attribute ``.name`` equals ``'frank'``. """ def __init__( self, data: Iterable[_T] = (), *, basetype: Union[Type[_T], Sequence[Type[_T]]] = (), lookup: Dict[Type[_L], Callable[[_T], Union[_T, _L]]] = None, ) -> None: if lookup is None: lookup = {} self._list: List[_T] = [] self._basetypes = ( basetype if isinstance(basetype, Sequence) else (basetype,) ) self._lookup = lookup.copy() self.extend(data) def __len__(self) -> int: return len(self._list) def __repr__(self) -> str: return repr(self._list) def __eq__(self, other: object): return self._list == other def __hash__(self) -> int: # it's important to add this to allow this object to be hashable # given that we've also reimplemented __eq__ return id(self) @overload def __setitem__(self, key: int, value: _T): ... # pragma: no cover @overload def __setitem__(self, key: slice, value: Iterable[_T]): ... # pragma: no cover def __setitem__(self, key, value): if isinstance(key, slice): if not isinstance(value, Iterable): raise TypeError( trans._( 'Can only assign an iterable to slice', deferred=True, ) ) self._list[key] = [self._type_check(v) for v in value] else: self._list[key] = self._type_check(value)
[docs] def insert(self, index: int, value: _T): self._list.insert(index, self._type_check(value))
def __contains__(self, key): if type(key) in self._lookup: try: self[self.index(key)] except ValueError: return False else: return True return super().__contains__(key) @overload def __getitem__(self, key: int) -> _T: ... # pragma: no cover @overload def __getitem__(self, key: slice) -> 'TypedMutableSequence[_T]': ... # pragma: no cover def __getitem__(self, key): """Get an item from the list Parameters ---------- key : int, slice, or any type in self._lookup The key to get. Returns ------- The value at `key` Raises ------ IndexError: If ``type(key)`` is not in ``self._lookup`` (usually an int, like a regular list), and the index is out of range. KeyError: If type(key) is in self._lookup and the key is not in the list (after) applying the self._lookup[key] function to each item in the list """ if type(key) in self._lookup: try: return self.__getitem__(self.index(key)) except ValueError as e: raise KeyError(str(e)) from e result = self._list[key] return self.__newlike__(result) if isinstance(result, list) else result def __delitem__(self, key): _key = self.index(key) if type(key) in self._lookup else key del self._list[_key] def _type_check(self, e: Any) -> _T: if self._basetypes and not any( isinstance(e, t) for t in self._basetypes ): raise TypeError( trans._( 'Cannot add object with type {dtype!r} to TypedList expecting type {basetypes!r}', deferred=True, dtype=type(e), basetypes=self._basetypes, ) ) return e def __newlike__(self, iterable: Iterable[_T]): new = self.__class__() # seperating this allows subclasses to omit these from their `__init__` new._basetypes = self._basetypes new._lookup = self._lookup.copy() new.extend(iterable) return new
[docs] def copy(self) -> Self: """Return a shallow copy of the list.""" return self.__newlike__(self)
def __add__(self, other: Iterable[_T]) -> Self: """Add other to self, return new object.""" copy = self.copy() copy.extend(other) return copy def __iadd__(self, other: Iterable[_T]) -> Self: """Add other to self in place (self += other).""" self.extend(other) return self def __radd__(self, other: List) -> List: """Add other to self in place (self += other).""" return other + list(self)
[docs] def index(self, value: _L, start: int = 0, stop: int = None) -> int: """Return first index of value. Parameters ---------- value : Any A value to lookup. If `type(value)` is in the lookups functions provided for this class, then values in the list will be searched using the corresponding lookup converter function. start : int, optional The starting index to search, by default 0 stop : int, optional The ending index to search, by default None Returns ------- int The index of the value Raises ------ ValueError If the value is not present """ if start is not None and start < 0: start = max(len(self) + start, 0) if stop is not None and stop < 0: stop += len(self) convert = self._lookup.get(type(value), _noop) for i in self._iter_indices(start, stop): v = convert(self[i]) if v is value or v == value: return i raise ValueError( trans._( "{value!r} is not in list", deferred=True, value=value, ) )
def _iter_indices(self, start=0, stop=None): """Iter indices from start to stop. While this is trivial for this basic sequence type, this method lets subclasses (like NestableEventedList modify how they are traversed). """ yield from range(start, len(self) if stop is None else stop) def _ipython_key_completions_(self): if str in self._lookup: return (self._lookup[str](x) for x in self) return None # type: ignore
def _noop(x): return x