# SPDX-License-Identifier: GPL-2.0-or-later OR AGPL-3.0-or-later OR CERN-OHL-S-2.0+
"""_util module with private helper functions
API Notes:
* This is an internal module and none of the functions or classes should be
called or instantiated in user code. No backward compatibility is provided
unless stated otherwise in specific autodoc.
"""
import abc
from collections.abc import Hashable
from itertools import islice
from typing import (
Any, Dict, List, Tuple,
Optional, Union, Generic, TypeVar, Type,
Iterable, Iterator, Generator, MutableSequence, Mapping, MutableMapping,
cast, overload,
)
from .typing import SingleOrMulti, IntFloat
# Typevars used for Generic Collection classes
_elem_typevar_ = TypeVar("_elem_typevar_")
_index_typevar_ = TypeVar("_index_typevar_", bound=Hashable)
_childelem_typevar_ = TypeVar("_childelem_typevar_")
_child_class_ = TypeVar("_child_class_")
_iter_typevar_ = TypeVar("_iter_typevar_")
@overload
def i2f(i: None) -> None:
... # pragma: no cover
@overload
def i2f(i: IntFloat) -> float:
... # pragma: no cover
[docs]def i2f(i):
if type(i) == bool:
raise ValueError("Use of bool as float not allowed")
return i if i is None else float(i)
[docs]def i2f_recursive(values: Any) -> Any:
"""Recursively convert int and bool elements of an iterable.
Iterables will be converted to tuples"""
if is_iterable(values):
return tuple(i2f_recursive(v) for v in values)
else:
return i2f(values)
[docs]def v2t(
value: SingleOrMulti[_elem_typevar_].T, *, n: Optional[int]=None,
) -> Tuple[_elem_typevar_, ...]:
"""Convert a single value or an iterable of value to a tuple.
Arguments:
value: a single value or an iterable of value
n: length specification for return value
Returns:
* If n is not specified and value is single value, a tuple with the
value will be returned.
* If n specified and value is a single value, a tuple with
n times the value will be returns.
* If n is specified and value is an iterable, the length of
iterable will be checked to correspond with given length
"""
if is_iterable(value) and (not isinstance(value, str)):
v = tuple(cast(Iterable[_elem_typevar_], value))
if n is not None:
assert n == len(v)
return v
else:
t = (cast(_elem_typevar_, value),)
if n is None:
return t
else:
return n*t
[docs]def is_iterable(it: Any) -> bool:
"""Check if a value is Iterable"""
if type(it) is str:
return False
try:
iter(it)
except:
return False
else:
return True
[docs]def nth(it: Iterable[_elem_typevar_], n) -> _elem_typevar_:
"""Return nth element from an iterable.
Arguments:
n: element to return starting from 0.
All values up to the element will have been consumed from the
iterable.
Raises:
StopIteration: if iterable has less than n+1 elements
"""
return next(islice(it, n, None))
[docs]def first(it: Iterable[_elem_typevar_]) -> _elem_typevar_:
"""Get first element of an iterable
This function will consume the first element of the iterator
Raises:
StopIteration: if iterable is empty
"""
return nth(it, 0)
[docs]def last(it: Iterable[_iter_typevar_]) -> _iter_typevar_:
"""Get last elemeent from an iterator.
The iterator will be exhausted after calling this function.
Raises:
StopIteration: if iterable is empty
"""
for _v in it:
v = _v
try:
return v # type: ignore
except NameError:
raise StopIteration
[docs]def strip_literal(s: str) -> str:
"""Strip surrounding '"' of a string.
Strip head and tail only if they are both '"'
"""
if (s[0] == '"') and (s[-1] == '"'):
return s[1:-1]
else:
return s
[docs]class IterableOverride(Iterable[_iter_typevar_], Generic[_iter_typevar_]):
"""Mixin class to allow to override element with Iterable element with more
specific element. This is for static typing support to have right element
for an iterator whose elements are a subclass of
Example:
.. code-block:: python
class Elem:
pass
class ElemChild(Elem):
pass
T = TypeVar("T")
class MyList(List[T], Generic[T]):
pass
class ElemList(MyList[Elem]):
pass
class ElemChildList(IterableOverride[ElemChild], ElemList):
pass
"""
def __iter__(self) -> Iterator[_iter_typevar_]:
return cast(Iterator[_iter_typevar_], super().__iter__())
[docs]class IterTypeMixin(Iterable[_elem_typevar_], Generic[_elem_typevar_]):
"""Internal collection support
TODO: Extended internal API documentation.
"""
@overload
def __iter_type__(self,
type_: Tuple[Type[_iter_typevar_], ...],
) -> Generator[_iter_typevar_, None, None]:
... # pragma: no cover
@overload
def __iter_type__(self,
type_: Type[_iter_typevar_],
) -> Generator[_iter_typevar_, None, None]:
... # pragma: no cover
def __iter_type__(self, type_):
"""Iterate over elems of an Iterable of certain type
Arguments:
type_: type of the element from the Iterable to iterate over
"""
for elem in self:
if isinstance(elem, type_):
yield cast(_iter_typevar_, elem)
[docs]class TypedList(
List[_elem_typevar_], IterTypeMixin[_elem_typevar_],
Generic[_elem_typevar_],
):
"""An internal list class that allow only elements of certain type.
TODO: Extended internal API documentation.
"""
def __init__(self, iterable: Iterable[_elem_typevar_]=tuple()):
super().__init__(iterable)
self._frozen__: bool = False
@property
@abc.abstractmethod
def _elem_type_(self) -> Union[
Type[_childelem_typevar_],
Tuple[Type[_childelem_typevar_], ...],
]:
... # pragma: no cover
def __add__(self, # type: ignore[override]
x: Union[_elem_typevar_, List[_elem_typevar_]],
) -> "TypedList[_elem_typevar_]":
if isinstance(x, self._elem_type_):
ret = super().__add__(cast(List[_elem_typevar_], [x]))
else:
ret = super().__add__(cast(List[_elem_typevar_], x))
return cast("TypedList[_elem_typevar_]", ret)
def __delitem__(self, i: Union[int, slice]) -> None:
if self._frozen_:
raise TypeError("Can't delete from a frozen list")
return super().__delitem__(i)
def __iadd__(self: _child_class_,
x: SingleOrMulti[_elem_typevar_].T,
) -> _child_class_:
cself = cast(TypedList[_elem_typevar_], self)
if isinstance(x, cself._elem_type_):
cself.append(cast(_elem_typevar_, x))
else:
cself.extend(cast(Iterable[_elem_typevar_], x))
return self
def __imul__(self: "TypedList[_elem_typevar_]", n: int) -> "TypedList[_elem_typevar_]":
if self._frozen_:
raise TypeError("Can't extend frozen list")
return cast("TypedList[_elem_typevar_]", super().__imul__(n))
def __setitem__(self, i: Union[int, slice], value) -> None:
if self._frozen_:
raise TypeError("Can't replace item from a frozen list")
return super().__setitem__(i, value)
[docs] def append(self, __object: _elem_typevar_) -> None:
if self._frozen_:
raise TypeError("Can't append to frozen list")
return super().append(__object)
[docs] def clear(self) -> None:
if self._frozen_:
raise TypeError("Can't clear frozen list")
return super().clear()
[docs] def extend(self, __iterable: Iterable[_elem_typevar_]) -> None:
if self._frozen_:
raise TypeError("Can't extend frozen list")
return super().extend(__iterable)
[docs] def insert(self, __index: int, __object: _elem_typevar_) -> None:
if self._frozen_:
raise TypeError("Can't insert in a frozen list")
return super().insert(__index, __object)
[docs] def pop(self, __index: int=-1) -> _elem_typevar_:
if self._frozen_:
raise TypeError("Can't pop from frozen list")
return super().pop(__index)
[docs] def remove(self, __value: _elem_typevar_) -> None:
if self._frozen_:
raise TypeError("Can't remove from frozen list")
return super().remove(__value)
[docs] def reverse(self) -> None:
if self._frozen_:
raise TypeError("Can't reverse frozen list")
return super().reverse()
[docs] def sort(self, *, key=None, reverse: bool=False) -> None:
if self._frozen_:
raise TypeError("Can't sort a frozen list")
return super().sort(key=key, reverse=reverse)
[docs] def _freeze_(self) -> None:
self._frozen__ = True
@property
def _frozen_(self) -> bool:
return self._frozen__
[docs] def _reorder_(self, neworder: Iterable[int]) -> None:
if self._frozen_:
raise TypeError("Can't reorder a frozen list")
neworder = tuple(neworder)
if set(neworder) != set(range(len(self))):
raise ValueError("neworder has to be iterable of indices with value from 'range(len(self))'")
newlist = [self[i] for i in neworder]
self.clear()
self.extend(newlist)
def __hash__(self) -> int:
if not self._frozen_:
raise TypeError(
f"'{self.__class__.__name__}' objects need to be frozen to be hashable",
)
else:
return hash(tuple(self))
def __eq__(self, o: object) -> bool:
return (
isinstance(o, TypedList)
and (len(self) == len(o))
and all(self[i] == o[i] for i in range(len(self)))
)
[docs]class TypedListMapping(
MutableSequence[_elem_typevar_], MutableMapping[_index_typevar_, _elem_typevar_],
IterTypeMixin[_elem_typevar_], Generic[_elem_typevar_, _index_typevar_],
):
"""An internal collection class that combines a `MutableSequence` with
`MutableMapping`
TODO: Extended internal API documentation.
API Notes:
TypedListMapping assumes not isinstance(Iterable[_elem_typevar], _elem_typevar)
_elem_type_ has to be valid when _typedListMapping.__init__() is called.
"""
T = TypeVar("T")
[docs] class _List(TypedList):
def __init__(self,
iterable: Iterable["TypedListMapping.T"], parent: "TypedListMapping",
):
super().__init__(iterable)
self._parent_ = parent
@property
def _elem_type_(self):
return self._parent_._elem_type_
def __init__(self, iterable: SingleOrMulti[_elem_typevar_].T=tuple()):
self._list_: TypedList[_elem_typevar_]
if isinstance(iterable, self._elem_type_):
self._list_ = self.__class__._List(
(cast(_elem_typevar_, iterable),), self,
)
else:
self._list_ = self.__class__._List(
cast(Iterable[_elem_typevar_], iterable), self,
)
attr_name = self._index_attribute_
assert isinstance(attr_name, str)
self._map_: Dict[_index_typevar_, _elem_typevar_] = {}
for elem in self._list_:
if not hasattr(elem, attr_name):
raise ValueError(f"elem {elem!r} has no attribute '{attr_name}'")
attr: _index_typevar_ = getattr(elem, attr_name)
self._map_[attr] = elem
@property
@abc.abstractmethod
def _elem_type_(self) -> Union[
Type[_childelem_typevar_],
Tuple[Type[_childelem_typevar_], ...],
]:
... # pragma: no cover
@property
@abc.abstractmethod
def _index_type_(self) -> Type[_index_typevar_]:
... # pragma: no cover
@property
@abc.abstractmethod
def _index_attribute_(self) -> str:
... # pragma: no cover
@overload
def __getitem__(self, key: Union[int, _index_typevar_]) -> _elem_typevar_:
... # pragma: no cover
@overload
def __getitem__(self: _child_class_, key: slice) -> _child_class_:
... # pragma: no cover
def __getitem__(self: _child_class_, # type: ignore[override]
key: Union[int, slice, _index_typevar_],
) -> Union[_elem_typevar_, _child_class_]:
cself = cast(TypedListMapping[_elem_typevar_, _index_typevar_], self)
if isinstance(key, int):
return cself._list_[key]
elif isinstance(key, slice):
o = cself.__class__(
cself._list_.__getitem__(key),
)
return cast(_child_class_, o)
elif isinstance(key, cself._index_type_):
return cself._map_[key]
raise TypeError(
f"idx has to be of type 'int', 'slice' or {cself._index_type_}, "
f"not {type(key)}"
)
@overload
def __setitem__(self,
key: Union[int, _index_typevar_], value: _elem_typevar_,
) -> None:
... # pragma: no cover
@overload
def __setitem__(self, key: slice, value: Iterable[_elem_typevar_]) -> None:
... # pragma: no cover
def __setitem__(self,
key: Union[int, slice, _index_typevar_],
value: SingleOrMulti[_elem_typevar_].T,
) -> None:
if self._frozen_:
raise TypeError("Can't change a frozen list")
if isinstance(key, int):
old = self._list_[key]
try:
self._map_.pop(getattr(old, self._index_attribute_))
except: # pragma: no cover
# If the elem is in _list_ it should also be in _map_
raise RuntimeError("Internal error")
# Todo: can we avoid cast to please Pylance
if not isinstance(value, cast(Any, self._elem_type_)):
raise TypeError(
f"value has to be of type '{self._elem_type_}', "
f"not {type(value)}"
)
self._list_[key] = value
key2 = getattr(value, self._index_attribute_)
self._map_[key2] = cast(_elem_typevar_, value)
elif isinstance(key, slice): # pragma: no cover
raise NotImplementedError(
"Assigning to slice of TypedListMapping"
)
elif isinstance(key, self._index_type_):
assert isinstance(value, self._elem_type_)
value = cast(_elem_typevar_, value)
for i, elem in enumerate(self._list_):
if getattr(elem, self._index_attribute_) == key:
self._list_[i] = value
self._map_[key] = cast(_elem_typevar_, value)
break
else:
self += value
def __delitem__(self,
key: Union[int, slice, _index_typevar_],
) -> None:
if self._frozen_:
raise TypeError("Can't change a frozen list")
if isinstance(key, int):
old = self._list_[key]
self._map_.pop(getattr(old, self._index_attribute_))
self._list_.__delitem__(key)
elif isinstance(key, slice): # pragma: no cover
raise NotImplementedError(
"Deketing slice of TypedListMapping"
)
elif isinstance(key, self._index_type_):
v = self._map_.pop(key)
self._list_.remove(v)
else:
raise TypeError(
f"key has to be of type 'int', 'slice' or '{self._index_type_}'"
)
[docs] def clear(self) -> None:
if self._frozen_:
raise TypeError("Can't change a frozen list")
self._list_.clear()
self._map_.clear()
[docs] def pop(self, # type: ignore[override]
key: Optional[Union[_index_typevar_, int]]=None,
) -> _elem_typevar_:
if self._frozen_:
raise TypeError("Can't change a frozen list")
if key is None:
elem = self._list_.pop()
self._map_.pop(getattr(elem, self._index_attribute_))
elif isinstance(key, self._index_type_):
elem = self._map_.pop(key)
self._list_.remove(elem)
else:
assert isinstance(key, int)
elem = self._list_.pop(key)
self._map_.pop(getattr(elem, self._index_attribute_))
return elem
[docs] def popitem(self):
raise NotImplementedError("TypedListMapping.popitem()")
[docs] def update(self, # type: ignore[override]
__m: Mapping[_index_typevar_, _elem_typevar_]
) -> None:
raise NotImplementedError("TypedListMapping.update()")
def __iter__(self) -> Iterator[_elem_typevar_]:
return iter(self._list_)
def __len__(self) -> int:
return len(self._list_)
def __contains__(self, elem: Any) -> bool:
return getattr(elem, self._index_attribute_) in self._map_
[docs] def index(self, elem: _elem_typevar_) -> int: # type: ignore[override]
"""
API Notes:
* Specifying start/end is currently not supported
"""
return self._list_.index(elem)
[docs] def insert(self, index: int, value: _elem_typevar_) -> None:
if self._frozen_:
raise TypeError("Can't change a frozen list")
self._list_.insert(index, value)
self._map_[getattr(value, self._index_attribute_)] = value
[docs] def keys(self):
return self._map_.keys()
[docs] def items(self):
return self._map_.items()
[docs] def values(self):
return self._map_.values()
def __iadd__(self: _child_class_,
x: SingleOrMulti[_elem_typevar_].T,
) -> _child_class_:
if cast("TypedListMapping", self)._frozen_:
raise TypeError("Can't change a frozen list")
cself = cast(TypedListMapping[_elem_typevar_, _index_typevar_], self)
new: Tuple[_elem_typevar_, ...]
if isinstance(x, cself._elem_type_):
new = (cast(_elem_typevar_, x),)
else:
new = tuple(cast(Iterable[_elem_typevar_], x))
cself._list_ += new
for e in new:
cself._map_[getattr(e, cself._index_attribute_)] = e
return self
[docs] def _freeze_(self) -> None:
self._list_._freeze_()
@property
def _frozen_(self) -> bool:
return self._list_._frozen_
[docs] def _reorder_(self, neworder: Iterable[int]) -> None:
if self._frozen_:
raise TypeError("Can't reorder a frozen list")
self._list_._reorder_(neworder)
[docs]class _ListMappingOverride(
MutableSequence[_elem_typevar_], MutableMapping[_index_typevar_, _elem_typevar_],
Generic[_elem_typevar_, _index_typevar_],
):
"""A support class for helping subclassing of ListMapping. It allows to subclass
a ListMapping class with an element that is a subclass of the parent ListMapping
element.
TODO: Extended internal API documentation.
"""
@overload
def __getitem__(self, key: Union[int, _index_typevar_]) -> _elem_typevar_:
... # pragma: no cover
@overload
def __getitem__(self: _child_class_, key: slice) -> _child_class_:
... # pragma: no cover
def __getitem__(self: _child_class_, # type: ignore[override]
key: Union[int, slice, _index_typevar_],
) -> Union[_elem_typevar_, _child_class_]:
return cast(Any, super()).__getitem__(key)
@overload
def __setitem__(self,
key: Union[int, _index_typevar_], value: _elem_typevar_,
) -> None:
... # pragma: no cover
@overload
def __setitem__(self, key: slice, value: Iterable[_elem_typevar_]) -> None:
... # pragma: no cover
def __setitem__(self,
key: Union[int, slice, _index_typevar_],
value: SingleOrMulti[_elem_typevar_].T,
) -> None:
return cast(Any, super()).__setitem__(key, value)
def __delitem__(self,
key: Union[int, slice, _index_typevar_],
) -> None:
return cast(Any, super()).__delitem__(key)
[docs] def pop(self, # type: ignore[override]
key: Optional[_index_typevar_]=None,
) -> _elem_typevar_:
return cast(Any, super()).pop(key)
[docs] def update(self, # type: ignore[override]
__m: Mapping[_index_typevar_, _elem_typevar_]
) -> None:
return cast(Any, super()).update(__m)
def __iter__(self) -> Iterator[_elem_typevar_]:
return cast(Any, super()).__iter__()
[docs] def index(self, elem: _elem_typevar_) -> int: # type: ignore[override]
return cast(Any, super()).index(elem)
[docs] def insert(self, index: int, value: _elem_typevar_) -> None:
return cast(Any, super()).insert(index, value)
def __iadd__(self: _child_class_,
x: SingleOrMulti[_elem_typevar_].T,
) -> _child_class_:
return cast(Any, super()).__iadd__(x)
[docs]class TypedListStrMapping(TypedListMapping[_elem_typevar_, str], Generic[_elem_typevar_]):
"""TypeListMapping where the _index_type_ is `str`. By default this also take 'name'
as default attribute name for the index. This can be overloaded in a subclass if
needed.
TODO: Extended internal API documentation.
"""
@property
def _index_type_(self):
return str
# Set default attribute name to 'name'
@property
def _index_attribute_(self):
return "name"
def __getattr__(self, name: str) -> _elem_typevar_:
return self._map_[name]
[docs]class ListStrMappingOverride(
_ListMappingOverride[_elem_typevar_, str], Generic[_elem_typevar_],
):
"""A support class for helping subclassing of TypeListStrMapping analog to
`_ListMappingOverride`.
TODO: Extended internal API documentation.
"""
def __getattr__(self, name: str) -> _elem_typevar_:
return cast(Any, super()).__getattr__(name)