Source code for spinn_utilities.ranged.abstract_sized

# Copyright (c) 2017 The University of Manchester
#
# 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
#
#     https://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.

from collections.abc import Sized
import itertools
import logging
import sys
from typing import Any, Optional, Sequence, SupportsInt, Tuple, Union
from typing_extensions import TypeAlias, TypeGuard
import numpy

logger = logging.getLogger(__file__)

#: Type of integers for selectors
_Integer: TypeAlias = Union[int, numpy.integer, SupportsInt]
#: The type of selectors; properly, SupportsInt should exclude numpy.ndarray
#: because that's there just to handle special edge cases.
Selector: TypeAlias = Union[
    None, _Integer, slice, Sequence[Union[bool, numpy.bool_]],
    Sequence[_Integer]]


def _is_iterable_selector(selector: Selector) -> TypeGuard[
        Union[Sequence[Union[bool, numpy.bool_]], Sequence[_Integer]]]:
    # Check selector is an iterable using pythonic try
    try:
        iterator = iter(selector)  # type: ignore[arg-type]
    except TypeError:
        return False
    return iterator is not None


[docs] class AbstractSized(object): """ Base class for slice and ID checking against size. Subclasses of this support the `len()` built-in. """ __slots__ = ( "_size", ) def __init__(self, size: Union[int, float]): """ :param int size: Fixed length of the list. """ # Strictly doesn't need to be int, but really should be! self._size = max(int(round(size)), 0) def __len__(self) -> int: """ Size of the list, irrespective of actual values. :return: the initial and Fixed size of the list """ return self._size @staticmethod def _is_id_type(the_id: Any) -> TypeGuard[Union[int, SupportsInt]]: """ Check if the given ID has a type acceptable for IDs. """ return isinstance(the_id, (int, SupportsInt)) and ( not isinstance(the_id, (float, numpy.ndarray))) def _check_id_in_range(self, the_id: Union[int, SupportsInt]) -> int: if not self._is_id_type(the_id): raise TypeError(f"Invalid argument type {type(the_id)}.") the_id = int(the_id) if not 0 <= the_id < self._size: raise IndexError(f"The index {the_id} is out of range.") return the_id def _check_slice_in_range( self, slice_start: Optional[int], slice_stop: Optional[int]) -> Tuple[int, int]: # Fix types if slice_start is None: slice_start = 0 elif not self._is_id_type(slice_start): raise TypeError(f"Invalid argument type {type(slice_start)}.") else: slice_start = int(slice_start) if slice_stop is None or slice_stop == sys.maxsize: slice_stop = self._size elif not self._is_id_type(slice_stop): raise TypeError(f"Invalid argument type {type(slice_stop)}.") else: slice_stop = int(slice_stop) if slice_start < 0: slice_start = self._size + slice_start if slice_start < 0: logger.warning( "Specified slice start was %d while size is only %d. " "Therefore slice will start at index 0", int(slice_start) - self._size, self._size) slice_start = 0 elif slice_start >= len(self): logger.warning( "Specified slice start was %d while size is only %d. " "Therefore slice will be empty", slice_start - self._size, self._size) return (self._size, self._size) if slice_stop < 0: slice_stop = self._size + slice_stop if slice_start > slice_stop: logger.warning( "Specified slice has a start %d greater than its stop %d " "(based on size %d). Therefore slice will be empty", slice_start, slice_stop, self._size) return (self._size, self._size) if slice_stop > len(self): logger.warning( "Specified slice has a start %d equal to its stop %d " "(based on size %d). Therefore slice will be empty", slice_start, slice_stop, self._size) if slice_stop < 0: logger.warning( "Specified slice stop was %d while size is only %d. " "Therefore slice will be empty", int(slice_stop) - self._size, self._size) return (self._size, self._size) elif slice_start > slice_stop: logger.warning( "Specified slice has a start %d greater than its stop %d " "(based on size %d). Therefore slice will be empty", slice_start, slice_stop, self._size) return (self._size, self._size) elif slice_start == slice_stop: logger.warning( "Specified slice has a start %d equal to its stop %d " "(based on size %d). Therefore slice will be empty", slice_start, slice_stop, self._size) elif slice_stop > len(self): logger.warning( "Specified slice stop was %d while size is only %d. " "Therefore slice will be truncated", slice_stop, self._size) slice_stop = self._size return slice_start, slice_stop def _check_mask_size(self, selector: Sized) -> None: if len(selector) < self._size: logger.warning( "The boolean mask is too short. The expected length was %d " "but the length was only %d. All the missing entries will be " "treated as False!", self._size, len(selector)) elif len(selector) > self._size: logger.warning( "The boolean mask is too long. The expected length was %d " "but the length was only %d. All the missing entries will be " "ignored!", self._size, len(selector))
[docs] def selector_to_ids(self, selector: Selector, warn=False) -> Sequence[int]: """ Gets the list of IDs covered by this selector. The types of selector currently supported are: `None`: Returns all IDs. :py:class:`slice`: Standard python slice. Negative values and values larger than size are handled using slice's `indices` method. This could result in am empty list. :py:class:`int`: Handles negative values as normal. Checks if ID is within expected range. iterator(:py:class:`bool`): Used as a mask. If the length of the mask is longer or shorted than number of IDs the result is the shorter of the two, with the remainder of the longer ignored. iterator(:py:class:`int`) but not bool: Every value checked that it is with the range 0 to size. Negative values are *not* allowed. Original order and duplication is respected so result may be unordered and contain duplicates. :param selector: Some object that identifies a range of IDs. :param bool warn: If True, this method will warn about problems with the selector. :return: a (possibly sorted) list of IDs """ if _is_iterable_selector(selector): # bool is subclass of int so if any are bool all must be if any(isinstance(item, (bool, numpy.bool_)) for item in selector): if all(isinstance(item, (bool, numpy.bool_)) for item in selector): if warn: self._check_mask_size(selector) return list(itertools.compress( range(self._size), selector)) raise TypeError( "An iterable type must be all ints or all bools") elif all(isinstance(item, (int, numpy.integer, SupportsInt)) for item in selector): # list converts any specific numpy types # blows up if someone gives an iterable of ndarray; # serves them right for being too smart-ass ids = list(map(int, selector)) for _id in ids: if _id < 0: raise TypeError( f"Selector includes the ID {_id} which is " "less than zero") if _id >= self._size: raise TypeError( f"Selector includes the ID {_id} which not " f"less than the size {self._size}") return ids else: raise TypeError( "An iterable type must be all ints or all bools") # OK lets try for None, int and slice after all if selector is None: if warn: logger.warning("None selector taken as all IDs") return range(self._size) if isinstance(selector, slice): (slice_start, slice_stop, step) = selector.indices(self._size) return range(slice_start, slice_stop, step) if isinstance(selector, (int, numpy.integer)): selector = int(selector) # make sure it is a simple int if selector < 0: selector = self._size + selector if selector < 0 or selector >= self._size: raise TypeError( f"Selector {selector-self._size} is unsupported " f"for size {self._size}") return [selector] # Last throw of the dice... if isinstance(selector, SupportsInt): selector = int(selector) if selector < 0: selector = self._size + selector if selector < 0 or selector >= self._size: raise TypeError( f"Selector {selector-self._size} is unsupported " f"for size {self._size}") return [selector] raise TypeError(f"Unexpected selector type {type(selector)}")