Source code for spinn_utilities.ranged.range_dictionary

# Copyright (c) 2017-2019 The University of Manchester
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from spinn_utilities.overrides import overrides
from .abstract_dict import AbstractDict
from .abstract_list import AbstractList
from .abstract_sized import AbstractSized
from .ids_view import _IdsView
from .ranged_list import RangedList
from .single_view import _SingleView
from .slice_view import _SliceView


[docs]class RangeDictionary(AbstractSized, AbstractDict): """ Main holding class for a range of similar Dictionary object. \ Keys in the dictionary must be str object and can not be removed. \ New keys can be added using the ``dict[str] = value`` format. \ The size (length of the list) is fixed and set at initialisation time. """ __slots__ = [ "_value_lists"] def __init__(self, size, defaults=None): """ The Object is set up initially where every ID in the range will share the same value for each key. All keys must be of type str. The default Values can be anything including None. :param size: Fixed number of IDs / Length of lists :type size: int :param defaults: Default dictionary where all keys must be str :type defaults: dict """ super().__init__(size) self._value_lists = dict() if defaults is not None: for key, value in defaults.items(): self._value_lists[key] = self.list_factory( size=size, value=value, key=key)
[docs] def list_factory(self, size, value, key): """ Defines which class or subclass of :py:class:`RangedList` to use. Main purpose is for subclasses to use a subclass or RangedList. All parameters are pass through ones to the List constructor :param size: Fixed length of the list :param value: value to given to all elements in the list :param key: The dict key this list covers. :return: AbstractList in this case a RangedList """ return RangedList(size, value, key)
[docs] def view_factory(self, key): """ Main function for creating views.\ This is the preferred way of creating new views as it checks\ parameters and returns the most efficient view. Note the ``__getitem__`` methods called by Object[id] and similar defer to this method so are fine to use. The ID(s) used are the actual IDs in the range and not indexes on the list of IDs :param key: A single int ID, a Slice object, or an iterable of int IDs :return: A view over the range """ # Key is an int - return single view if isinstance(key, int): self._check_id_in_range(key) return _SingleView(range_dict=self, the_id=key) # Key is a slice - return a sliced view if isinstance(key, slice): slice_start, slice_stop = self._check_slice_in_range( key.start, key.stop) if slice_start >= slice_stop: msg = "{} would result in an empty view".format(key) raise KeyError(msg) # Slice is really just one item - return a single view if slice_start == slice_stop - 1: return _SingleView(range_dict=self, the_id=slice_start) # Slice is continuous - return a slice view elif key.step is None or key.step == 1: return _SliceView(range_dict=self, start=slice_start, stop=slice_stop) # Slice is really a list of integers - change it and continue below key = range(self._size)[key] # Key can only now be an iterable of ints so make it a list and check key = list(key) if not all(isinstance(x, int) for x in key): raise KeyError("Only list/tuple of int are supported") # Key is really just a single int - return single view if len(key) == 1: return _SingleView(range_dict=self, the_id=key[0]) # Key is really just a slice (i.e. one of each key in order without # holes) - return a slice view if len(key) == key[-1] - key[0] + 1: in_order = sorted(key) if in_order == key: return _SliceView( range_dict=self, start=key[0], stop=key[-1] + 1) # Random jumble of ints - return an IDs view return _IdsView(range_dict=self, ids=key)
def __getitem__(self, key): """ Support for the view[x] based the type of the key If key is a str, a list type object of ``AbstractList`` is returned. Otherwise a view (AbstractView) over part of the IDs in the dict is returned. Multiple str objects or None are not supported as keys here. :param key: a str, int, or iterable of int values :return: An AbstractList or AbstractView """ if isinstance(key, str): return self._value_lists[key] return self.view_factory(key=key)
[docs] @overrides(AbstractDict.get_value, extend_defaults=True) def get_value(self, key=None): if isinstance(key, str): return self._value_lists[key].get_single_value_all() if key is None: key = self.keys() results = dict() for a_key in key: results[a_key] = self._value_lists[a_key].get_single_value_all() return results
[docs] def get_values_by_id(self, key, the_id): """ Same as :py:meth:`get_value` but limited to a single ID. :param key: as :py:meth:`get_value` :param the_id: single int ID :return: See :py:meth:`get_value` """ if isinstance(key, str): return self._value_lists[key].get_value_by_id(the_id) if key is None: key = self.keys() results = dict() for a_key in key: results[a_key] = self._value_lists[a_key].get_value_by_id(the_id) return results
[docs] def get_list(self, key): """ Gets the storage unit for a single key. .. note:: Mainly intended by Views to access the data for one key directly. :param key: a key which must be present in the dict :type key: str :rtype: :py:class:`.ranged_list.RangedList` """ return self._value_lists[key]
[docs] def update_safe_iter_all_values(self, key, ids): """ Same as :py:meth:`iter_all_values` \ but limited to a collection of IDs and update-safe. """ for id_value in ids: yield self.get_values_by_id(key=key, the_id=id_value)
[docs] @overrides(AbstractDict.iter_all_values, extend_defaults=True) def iter_all_values(self, key=None, update_save=False): if isinstance(key, str): if update_save: return self._value_lists[key].iter() return self._value_lists[key].__iter__() else: # Sub methods will check key type if update_save: return self.update_safe_iter_all_values( key, range(self._size)) return self._values_from_ranges(self.iter_ranges(key))
[docs] def iter_values_by_slice( self, slice_start, slice_stop, key=None, update_save=False): """ Same as :py:meth:`iter_all_values` but limited to a simple slice. """ if isinstance(key, str) and not update_save: return self._value_lists[key].iter_by_slice( slice_start=slice_start, slice_stop=slice_stop) # Sub methods will check key type if update_save: return self.update_safe_iter_all_values( key, range(slice_start, slice_stop)) return self._values_from_ranges(self.iter_ranges_by_slice( slice_start=slice_start, slice_stop=slice_stop, key=key))
[docs] def iter_values_by_ids(self, ids, key=None, update_save=False): """ Same as :py:meth:`iter_all_values` but limited to a simple slice. """ if update_save: return self.update_safe_iter_all_values(key, ids) return self._values_from_ranges(self.iter_ranges_by_ids( key=key, ids=ids))
def _values_from_ranges(self, ranges): for (start, stop, value) in ranges: for _ in range(start, stop): yield value
[docs] @overrides(AbstractDict.set_value) def set_value(self, key, value, use_list_as_value=False): self._value_lists[key].set_value(value, use_list_as_value)
def __setitem__(self, key, value): """ Wrapper around set_value to support ``range["key"] =`` .. note: ``range[int] =`` is not supported ``value`` can be a single object or ``None`` in which case every value in the list is set to that. ``value`` can be a collection but then it must be exactly the size of all lists in this dictionary. ``value`` can be an ``AbstractList`` :param key: Existing or NEW str dictionary key :type key: str :param value: List or value to create list based on. :return: """ if isinstance(key, str): if key in self: self.set_value(key=key, value=value) elif isinstance(value, AbstractList): assert self._size == len(value) self._value_lists[key] = value else: new_list = self.list_factory( size=self._size, value=value, key=key) self._value_lists[key] = new_list elif isinstance(key, (slice, int, tuple, list)): raise KeyError("Setting of a slice/ids not supported") else: raise KeyError("Unexpected key type: {}".format(type(key)))
[docs] @overrides(AbstractDict.ids) def ids(self): """ Returns a list of the IDs in this Range :return: a list of the IDs in this Range :rtype: list(int) """ return list(range(self._size))
[docs] @overrides(AbstractDict.has_key) def has_key(self, key): return key in self._value_lists
[docs] @overrides(AbstractDict.keys) def keys(self): return self._value_lists.keys()
def _merge_ranges(self, range_iters): current = dict() ranges = dict() start = 0 stop = self._size keys = range_iters.keys() for key in keys: ranges[key] = next(range_iters[key]) start = ranges[key][0] current[key] = ranges[key][2] stop = min(ranges[key][1], stop) yield (start, stop, current) while stop < self._size: current = dict() start = self._size next_stop = self._size for key in keys: try: if ranges[key][1] == stop: ranges[key] = next(range_iters[key]) except StopIteration: return start = min(max(ranges[key][0], stop), start) next_stop = min(ranges[key][1], next_stop) current[key] = ranges[key][2] stop = next_stop yield (start, stop, current)
[docs] @overrides(AbstractDict.iter_ranges) def iter_ranges(self, key=None): if isinstance(key, str): return self._value_lists[key].iter_ranges() if key is None: key = self.keys() ranges = dict() for a_key in key: ranges[a_key] = self._value_lists[a_key].iter_ranges() return self._merge_ranges(ranges)
[docs] def iter_ranges_by_id(self, key=None, the_id=None): """ Same as :py:meth:`iter_ranges` but limited to one ID. :param key: see :py:meth:`iter_ranges` parameter key :param the_id: single ID which is the actual ID and not an index into IDs :type the_id: int """ if isinstance(key, str): return self._value_lists[key].iter_ranges_by_id(the_id=the_id) if key is None: key = self.keys() ranges = dict() for a_key in key: ranges[a_key] = self._value_lists[a_key].iter_ranges_by_id( the_id=the_id) return self._merge_ranges(ranges)
[docs] def iter_ranges_by_slice(self, key, slice_start, slice_stop): """ Same as :py:meth:`iter_ranges` but limited to a simple slice. ``slice_start`` and ``slice_stop`` are actual ID values and not indexes into the IDs. They must also be actual values, so ``None``, ``max_int``, and negative numbers are not supported. :param key: see :py:meth:`iter_ranges` parameter ``key`` :param slice_start: Inclusive i.e. first ID :param slice_stop: Exclusive to last ID + 1 :return: see :py:meth:`iter_ranges` """ if isinstance(key, str): return self._value_lists[key].iter_ranges_by_slice( slice_start=slice_start, slice_stop=slice_stop) if key is None: key = self.keys() ranges = dict() for a_key in key: ranges[a_key] = self._value_lists[a_key].iter_ranges_by_slice( slice_start=slice_start, slice_stop=slice_stop) return self._merge_ranges(ranges)
[docs] def iter_ranges_by_ids(self, ids, key=None): """ Same as :py:meth:`iter_ranges` but limited to a\ collection of IDs. The IDs are actual ID values and not indexes into the IDs :param key: see :py:meth:`iter_ranges` parameter ``key`` :param ids: Collection of IDs in the range :return: see :py:meth:`iter_ranges` """ if isinstance(key, str): return self._value_lists[key].iter_ranges_by_ids(ids=ids) if key is None: key = self.keys() ranges = dict() for a_key in key: ranges[a_key] = self._value_lists[a_key].\ iter_ranges_by_ids(ids=ids) return self._merge_ranges(ranges)
[docs] def set_default(self, key, default): """ Sets the default value for a single key. .. note:: Does not change any values but only changes what ``reset_value`` would do .. warning:: If called on a View it sets the default for the *whole* range and not just the view. :param key: Existing dict key :type key: str :param default: Value to be used by reset """ self._value_lists[key].set_default(default)
[docs] @overrides(AbstractDict.get_default) def get_default(self, key): return self._value_lists[key].get_default()
[docs] def copy_into(self, other): """ Turns this dict into a copy of the other dict but keep its id :param RangedDict other: Another Ranged Dictionary assumed created by cloning this one """ for key in other.keys(): value = other[key] if isinstance(value, RangedList): if key in self: self._value_lists[key].copy_into(value) else: self._value_lists[key] = value.copy() else: self._value_lists[key] = RangedList( len(value), key=key) self._value_lists[key].copy_into(value)
[docs] def copy(self): copy = RangeDictionary(self._size) copy.copy_into(self) return copy