Source code for biocutils.Names

from copy import deepcopy
from typing import Any, Callable, Iterable, List, Optional, Sequence, Union

from .assign_sequence import assign_sequence
from .combine_sequences import combine_sequences
from .normalize_subscript import NormalizedSubscript, normalize_subscript
from .reverse_index import build_reverse_index
from .subset_sequence import subset_sequence

SubscriptTypes = Union[slice, range, Sequence, int, bool, NormalizedSubscript]


[docs] class Names: """ List of strings containing names. Typically used to decorate sequences, such that callers can get or set elements by name instead of position. """ def __init__(self, names: Optional[Iterable] = None, _validate: bool = True): """ Args: names: Some iterable object containing strings, or values that can be coerced into strings. _validate: Internal use only. """ if _validate: if names is None: names = [] elif isinstance(names, Names): names = names._names else: names = list(str(y) for y in names) self._names = names self._reverse = None # Enable fast indexing by name, but only on demand. This reverse mapping # field is strictly internal and should be completely transparent to the # user; so, calls to map() can be considered as 'non-mutating', as it # shouldn't manifest in any visible changes to the Names object. I guess # that things become a little hairy in multi-threaded contexts where I # should probably protect the final assignment to _reverse. But then # again, Python is single-threaded anyway, so maybe it doesn't matter. def _populate_reverse_index(self): if self._reverse is None: self._reverse = build_reverse_index(self._names) def _wipe_reverse_index(self): self._reverse = None ################################### #####>>>> Bits and pieces <<<<##### ###################################
[docs] def __len__(self) -> int: """ Returns: Length of the list. """ return len(self._names)
[docs] def __iter__(self) -> "list_iterator": """ Returns: An iterator on the underlying list of names. """ return iter(self._names)
[docs] def __repr__(self) -> str: """ Returns: A stringified representation of this object. """ return type(self).__name__ + "(" + repr(self._names) + ")"
def __str__(self) -> str: """ Returns: A pretty-printed representation of this object. """ return str(self._names)
[docs] def __eq__(self, other: "Names") -> bool: """ Args: other: Another ``Names`` object. Returns: Whether the current object is the same as ``other``. """ if not isinstance(other, Names): return False return self._names == other._names
[docs] def as_list(self) -> List[str]: """ Returns: List of strings containing the names. This should be treated as a read-only reference. Modifications should be performed by creating a new ``Names`` object instead. """ return self._names
[docs] def map(self, name: str) -> int: """ Args: name: Name of interest. Returns: Index containing the position of the first occurrence of ``name``; or -1, if ``name`` is not present in this object. """ self._populate_reverse_index() if name in self._reverse: return self._reverse[name] else: return -1
################################# #####>>>> Get/set items <<<<##### #################################
[docs] def get_value(self, index: int) -> str: """ Args: index: Position of interest. Returns: The name at the specified position. """ return self._names[index]
[docs] def get_slice(self, index: SubscriptTypes) -> "Names": """ Args: index: Positions of interest, see the allowed indices in :py:func:`~biocutils.normalize_subscript.normalize_subscript` for more details. Scalars are treated as length-1 sequences. Returns: A ``Names`` object containing the names at the specified positions. """ index, scalar = normalize_subscript(index, len(self), None) return type(self)(subset_sequence(self._names, index), _validate=False)
[docs] def __getitem__(self, index: SubscriptTypes) -> Union[str, "Names"]: """ If ``index`` is a scalar, this is an alias for :py:attr:`~get_value`. If ``index`` is a sequence, this is an alias for :py:attr:`~get_slice`. """ index, scalar = normalize_subscript(index, len(self), None) if scalar: return self.get_value(index[0]) else: return self.get_slice(NormalizedSubscript(index))
[docs] def set_value(self, index: int, value: str, in_place: bool = False) -> "Names": """ Args: index: Position of interest. value: Replacement name. in_place: Whether to perform the modification in-place. Returns: A modified ``Names`` object with the replacement name, either as a new object or as a reference to the current object. """ if in_place: self._wipe_reverse_index() output = self else: output = self.copy() output._names[index] = str(value) return output
[docs] def set_slice( self, index: SubscriptTypes, value: Sequence[str], in_place: bool = False ) -> "Names": """ Args: index: Positions of interest. value: Replacement names. in_place: Whether to perform the modification in-place. Returns: A modified ``Names`` object with the replacement name, either as a new object or as a reference to the current object. """ if in_place: self._wipe_reverse_index() output = self else: output = self.copy() if isinstance(value, Names): value = value.as_list() index, scalar = normalize_subscript(index, len(self), None) output._wipe_reverse_index() for i, j in enumerate(index): output._names[j] = str(value[i]) return output
[docs] def __setitem__(self, index: SubscriptTypes, value: Any): """ If ``index`` is a scalar, this is an alias for :py:attr:`~set_value` with ``in_place = True``. If ``index`` is a sequence, this is an alias for :py:attr:`~set_slice` with ``in_place = True``. """ index, scalar = normalize_subscript(index, len(self), self._names) if scalar: self.set_value(index[0], value, in_place=True) else: self.set_slice(NormalizedSubscript(index), value, in_place=True)
################################ #####>>>> List methods <<<<##### ################################ def _define_output(self, in_place: bool) -> "Names": if in_place: return self else: return self.copy()
[docs] def safe_append(self, value: str, in_place: bool = False) -> "Names": """ Args: value: Name to be added. in_place: Whether to perform this appending in-place. Returns: A ``Names`` object is returned with the added name. This may be a new object or a reference to the current object. """ output = self._define_output(in_place) name = str(value) if output._reverse is not None and name not in output._reverse: output._reverse[name] = len(output) output._names.append(name) return output
[docs] def append(self, value: str): """Alias for :py:attr:`~safe_append` with ``in_place = True``.""" self.safe_append(value, in_place=True)
[docs] def safe_insert(self, index: int, value: str, in_place: bool = False) -> "Names": """ Args: index: Position on the object to insert at. value: Name to be added. in_place: Whether to perform this insertion in-place. Returns: A ``Names`` object is returned with the inserted name. This may be a new object or a reference to the current object. """ output = self._define_output(in_place) output._wipe_reverse_index() output._names.insert(index, str(value)) return output
[docs] def insert(self, index: int, value: str): """Alias for :py:attr:`~safe_insert` with ``in_place = True``.""" self.safe_insert(index, value, in_place=True)
[docs] def safe_extend(self, value: Sequence[str], in_place: bool = False) -> "Names": """ Args: value: Names to be added. in_place: Whether to perform this extension in-place. Returns: A ``Names`` object is returned with the extension. This may be a new object or a reference to the current object. """ output = self._define_output(in_place) if output._reverse is not None: for i, n in enumerate(value): n = str(n) if n not in output._reverse: output._reverse[n] = len(output._names) output._names.append(n) elif isinstance(value, Names): output._names.extend(value._names) else: output._names.extend(str(y) for y in value) return output
[docs] def extend(self, value: Sequence[str]): """Alias for :py:attr:`~safe_extend` with ``in_place = True``.""" self.safe_extend(value, in_place=True)
[docs] def __add__(self, other: list): """ Args: other: List of names. Returns: A new ``Names`` containing the combined contents of the current object and ``other``. """ return self.safe_extend(other)
[docs] def __iadd__(self, other: list): """ Args: other: List of names. Returns: The current object is modified by adding ``other`` to its names. """ self.extend(other) return self
################################ #####>>>> Copy methods <<<<##### ################################
[docs] def copy(self) -> "Names": """ Returns: A shallow copy of the current object. This will copy the underlying list so that any in-place operations like :py:attr:`~append`, etc., on the new object will not change the original object. """ return type(self)(self._names.copy(), _validate=False)
[docs] def __copy__(self) -> "Names": """Alias for :py:attr:`~copy`.""" return self.copy()
[docs] def __deepcopy__(self, memo=None, _nil=[]) -> "Names": """ Args: memo: See :py:func:`~copy.deepcopy` for details. _nil: See :py:func:`~copy.deepcopy` for details. Returns: A deep copy of this ``Names`` object with the same contents. """ return type(self)(deepcopy(self._names, memo, _nil), _validate=False)
@subset_sequence.register def _subset_sequence_Names(x: Names, indices: Sequence[int]) -> Names: return x.get_slice(NormalizedSubscript(indices)) @assign_sequence.register def _assign_sequence_Names(x: Names, indices: Sequence[int], other: Sequence) -> Names: return x.set_slice(NormalizedSubscript(indices), other) @combine_sequences.register def _combine_sequences_Names(*x: Names) -> Names: output = x[0].copy() for i in range(1, len(x)): output.extend(x[i]) return output def _name_to_position(names: Optional[Names], index: str) -> int: i = -1 if names is not None: i = names.map(index) if i < 0: raise KeyError("failed to find entry with name '" + index + "'") return i def _sanitize_names(names: Optional[Names], length: int) -> Union[None, Names]: if names is None: return names if not isinstance(names, Names): names = Names(names) if len(names) != length: raise ValueError( "length of 'names' must be equal to number of entries (" + str(length) + ")" ) return names def _combine_names(*x: Any, get_names: Callable) -> Union[Names, None]: all_names = [] has_names = False for y in x: n = get_names(y) if n is None: all_names.append(len(y)) else: has_names = True all_names.append(n) if not has_names: return None else: output = Names() for i, n in enumerate(all_names): if not isinstance(n, Names): output.extend([""] * n) else: output.extend(n) return output