/ pdkmaster / _util.py
_util.py
  1  # SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
  2  """_util module with private helper functions
  3  
  4  API Notes:
  5      * This is an internal module and none of the functions or classes should be
  6        called or instantiated in user code. No backward compatibility is provided
  7        unless stated otherwise in specific autodoc.
  8  """
  9  import abc
 10  from collections.abc import Hashable
 11  from itertools import islice
 12  from typing import (
 13      Any, Dict, List, Tuple,
 14      Optional, Union, Generic, TypeVar, Type,
 15      Iterable, Iterator, MutableSequence, Mapping, MutableMapping,
 16      cast, overload,
 17  )
 18  from .typing import MultiT, cast_MultiT
 19  
 20  # Typevars used for Generic Collection classes
 21  _elem_typevar_ = TypeVar("_elem_typevar_")
 22  _index_typevar_ = TypeVar("_index_typevar_", bound=Hashable)
 23  _child_class_ = TypeVar("_child_class_")
 24  _iter_typevar_ = TypeVar("_iter_typevar_")
 25  
 26  
 27  def i2f_recursive(values: Any) -> Any:
 28      """Recursively convert int and bool elements of an iterable.
 29      Iterables will be converted to tuples"""
 30      if is_iterable(values):
 31          return tuple(i2f_recursive(v) for v in values)
 32      else:
 33          return float(values)
 34  
 35  def is_iterable(it: Any) -> bool:
 36      """Check if a value is Iterable"""
 37      if type(it) is str:
 38          return False
 39      try:
 40          iter(it)
 41      except:
 42          return False
 43      else:
 44          return True
 45  
 46  def get_nth_of(it: Iterable[_elem_typevar_], *, n: int) -> _elem_typevar_:
 47      """Return nth element from an iterable.
 48  
 49      Arguments:
 50          n: element to return starting from 0.  
 51             All values up to the element will have been consumed from the
 52             iterable.
 53  
 54      Raises:
 55          StopIteration: if iterable has less than n+1 elements
 56      """
 57      return next(islice(it, n, None))
 58  
 59  def get_first_of(it: Iterable[_elem_typevar_]) -> _elem_typevar_:
 60      """Get first element of an iterable
 61  
 62      This function will consume the first element of the iterator
 63  
 64      Raises:
 65          StopIteration: if iterable is empty
 66      """
 67      return get_nth_of(it, n=0)
 68  
 69  def get_last_of(it: Iterable[_iter_typevar_]) -> _iter_typevar_:
 70      """Get last elemeent from an iterator.
 71  
 72      The iterator will be exhausted after calling this function.
 73  
 74      Raises:
 75          StopIteration: if iterable is empty
 76      """
 77      for _v in it:
 78          v = _v
 79      try:
 80          return v # type: ignore
 81      except NameError:
 82          raise StopIteration
 83  
 84  def strip_literal(s: str) -> str:
 85      """Strip surrounding '"' of a string.
 86  
 87      Strip head and tail only if they are both '"'
 88      """
 89      if (s[0] == '"') and (s[-1] == '"'):
 90          return s[1:-1]
 91      else:
 92          return s
 93  
 94  
 95  class IterableOverride(Iterable[_iter_typevar_], Generic[_iter_typevar_]):
 96      """Mixin class to allow to override element with Iterable element with more
 97      specific element. This is for static typing support to have right element
 98      for an iterator whose elements are a subclass of parent's element type.
 99  
100      Example:
101  
102          .. code-block:: python
103  
104              class Elem:
105                  pass
106  
107              class ElemChild(Elem):
108                  pass
109  
110              T = TypeVar("T")
111              class MyList(List[T], Generic[T]):
112                  pass
113  
114              class ElemList(MyList[Elem]):
115                  pass
116  
117              class ElemChildList(IterableOverride[ElemChild], ElemList):
118                  pass
119      """
120  
121      def __iter__(self) -> Iterator[_iter_typevar_]:
122          return cast(Iterator[_iter_typevar_], super().__iter__())
123  
124  
125  class IterTypeMixin(Iterable[_elem_typevar_], Generic[_elem_typevar_]):
126      """Internal collection support
127  
128      TODO: Extended internal API documentation.
129      """
130      def __iter_type__(self,
131          type_: Union[Type[_iter_typevar_], Tuple[Type[_iter_typevar_], ...]],
132      ) -> Iterable[_iter_typevar_]:
133          """Iterate over elems of an Iterable of certain type
134  
135          Arguments:
136              type_: type of the element from the Iterable to iterate over
137          """
138          for elem in self:
139              if isinstance(elem, type_):
140                  yield elem
141  
142  
143  class ExtendedList(
144      List[_elem_typevar_], IterTypeMixin[_elem_typevar_],
145      Generic[_elem_typevar_],
146  ):
147      """An internal list class that allow only elements of certain type.
148  
149      TODO: Extended internal API documentation.
150      """
151      def __init__(self, iterable: Iterable[_elem_typevar_]=tuple()):
152          super().__init__(iterable)
153  
154          self._frozen__: bool = False
155  
156      def __add__(self,  # type: ignore[override]
157          x: Union[_elem_typevar_, List[_elem_typevar_]],
158      ) -> "ExtendedList[_elem_typevar_]":
159          if isinstance(x, list):
160              ret = super().__add__(cast(List[_elem_typevar_], x))
161          else:
162              ret = super().__add__(cast(List[_elem_typevar_], [x]))
163          return cast("ExtendedList[_elem_typevar_]", ret)
164  
165      def __delitem__(self, i: Union[int, slice]) -> None:
166          if self._frozen_:
167              raise TypeError("Can't delete from a frozen list")
168          return super().__delitem__(i)
169  
170      def __iadd__(self: _child_class_,
171          x: MultiT[_elem_typevar_],
172      ) -> _child_class_:
173          cself = cast(ExtendedList[_elem_typevar_], self)
174          if cself._frozen_:
175              raise TypeError("Can't extend frozen list")
176          cself.extend(cast_MultiT(x))
177          return self
178  
179      def __imul__(self: "ExtendedList[_elem_typevar_]", n: int) -> "ExtendedList[_elem_typevar_]":
180          if self._frozen_:
181              raise TypeError("Can't extend frozen list")
182          return cast("ExtendedList[_elem_typevar_]", super().__imul__(n))
183  
184      def __setitem__(self, i: Union[int, slice], value) -> None:
185          if self._frozen_:
186              raise TypeError("Can't replace item from a frozen list")
187          return super().__setitem__(i, value)
188  
189      def append(self, __object: _elem_typevar_) -> None:
190          if self._frozen_:
191              raise TypeError("Can't append to frozen list")
192          return super().append(__object)
193  
194      def clear(self) -> None:
195          if self._frozen_:
196              raise TypeError("Can't clear frozen list")
197          return super().clear()
198  
199      def extend(self, __iterable: Iterable[_elem_typevar_]) -> None:
200          if self._frozen_:
201              raise TypeError("Can't extend frozen list")
202          return super().extend(__iterable)
203  
204      def insert(self, __index: int, __object: _elem_typevar_) -> None:
205          if self._frozen_:
206              raise TypeError("Can't insert in a frozen list")
207          return super().insert(__index, __object)
208  
209      def pop(self, __index: int=-1) -> _elem_typevar_:
210          if self._frozen_:
211              raise TypeError("Can't pop from frozen list")
212          return super().pop(__index)
213  
214      def remove(self, __value: _elem_typevar_) -> None:
215          if self._frozen_:
216              raise TypeError("Can't remove from frozen list")
217          return super().remove(__value)
218  
219      def reverse(self) -> None:
220          if self._frozen_:
221              raise TypeError("Can't reverse frozen list")
222          return super().reverse()
223  
224      def sort(self, *, key=None, reverse: bool=False) -> None:
225          if self._frozen_:
226              raise TypeError("Can't sort a frozen list")
227          return super().sort(key=key, reverse=reverse)
228  
229      def _freeze_(self) -> None:
230          self._frozen__ = True
231  
232      @property
233      def _frozen_(self) -> bool:
234          return self._frozen__
235  
236      def _reorder_(self, *, neworder: Iterable[int]) -> None:
237          if self._frozen_:
238              raise TypeError("Can't reorder a frozen list")
239          neworder = tuple(neworder)
240          if set(neworder) != set(range(len(self))):
241              raise ValueError("neworder has to be iterable of indices with value from 'range(len(self))'")
242          newlist = [self[i] for i in neworder]
243          self.clear()
244          self.extend(newlist)
245  
246      def __hash__(self) -> int:
247          if not self._frozen_:
248              raise TypeError(
249                  f"'{self.__class__.__name__}' objects need to be frozen to be hashable",
250              )
251          else:
252              return hash(tuple(self))
253  
254      def __eq__(self, o: object) -> bool:
255          return (
256              isinstance(o, ExtendedList)
257              and (len(self) == len(o))
258              and all(self[i] == o[i] for i in range(len(self)))
259          )
260  
261  
262  class ExtendedListMapping(
263      MutableSequence[_elem_typevar_], MutableMapping[_index_typevar_, _elem_typevar_],
264      IterTypeMixin[_elem_typevar_], Generic[_elem_typevar_, _index_typevar_],
265  ):
266      """An internal collection class that combines a `MutableSequence` with
267      `MutableMapping`
268  
269      TODO: Extended internal API documentation.
270  
271      API Notes:
272          ExtendedListMapping assumes not isinstance(Iterable[_elem_typevar], _elem_typevar)
273      """
274      def __init__(self, iterable: MultiT[_elem_typevar_]=tuple()):
275          self._list_ = ExtendedList[_elem_typevar_](cast_MultiT(iterable))
276  
277          attr_name = self._index_attribute_
278          assert isinstance(attr_name, str)
279  
280          self._map_: Dict[_index_typevar_, _elem_typevar_] = {}
281          for elem in self._list_:
282              if not hasattr(elem, attr_name):
283                  raise ValueError(f"elem {elem!r} has no attribute '{attr_name}'")
284              attr: _index_typevar_ = getattr(elem, attr_name)
285              self._map_[attr] = elem
286  
287      @property
288      @abc.abstractmethod
289      def _index_attribute_(self) -> str:
290          ... # pragma: no cover
291  
292      @overload
293      def __getitem__(self, key: Union[int, _index_typevar_]) -> _elem_typevar_:
294          ... # pragma: no cover
295      @overload
296      def __getitem__(self: _child_class_, key: slice) -> _child_class_:
297          ... # pragma: no cover
298      def __getitem__(self: _child_class_, # type: ignore[override]
299          key: Union[int, slice, _index_typevar_],
300      ) -> Union[_elem_typevar_, _child_class_]:
301          cself = cast(ExtendedListMapping[_elem_typevar_, _index_typevar_], self)
302          if isinstance(key, int):
303              return cself._list_[key]
304          elif isinstance(key, slice):
305              o = cself.__class__(
306                  cself._list_.__getitem__(key),
307              )
308              return cast(_child_class_, o)
309          else:
310              # type(key) is _index_typevar_
311              return cself._map_[key]
312  
313      @overload
314      def __setitem__(self,
315          key: Union[int, _index_typevar_], value: _elem_typevar_,
316      ) -> None:
317          ... # pragma: no cover
318      @overload
319      def __setitem__(self, key: slice, value: Iterable[_elem_typevar_]) -> None:
320          ... # pragma: no cover
321      def __setitem__(self,
322          key: Union[int, slice, _index_typevar_],
323          value: MultiT[_elem_typevar_],
324      ) -> None:
325          if self._frozen_:
326              raise TypeError("Can't change a frozen list")
327          if isinstance(key, int):
328              old = self._list_[key]
329              try:
330                  self._map_.pop(getattr(old, self._index_attribute_))
331              except: # pragma: no cover
332                  # If the elem is in _list_ it should also be in _map_
333                  raise RuntimeError("Internal error")
334              key2 = getattr(value, self._index_attribute_)
335              self._list_[key] = cast(_elem_typevar_, value)
336              self._map_[key2] = cast(_elem_typevar_, value)
337          elif isinstance(key, slice): # pragma: no cover
338              raise NotImplementedError(
339                  "Assigning to slice of ExtendedListMapping"
340              )
341          else:
342              value = cast(_elem_typevar_, value)
343              for i, elem in enumerate(self._list_):
344                  if getattr(elem, self._index_attribute_) == key:
345                      self._list_[i] = value
346                      self._map_[key] = cast(_elem_typevar_, value)
347                      break
348              else:
349                  self += value
350  
351      def __delitem__(self,
352          key: Union[int, slice, _index_typevar_],
353      ) -> None:
354          if self._frozen_:
355              raise TypeError("Can't change a frozen list")
356          if isinstance(key, int):
357              old = self._list_[key]
358              self._map_.pop(getattr(old, self._index_attribute_))
359              self._list_.__delitem__(key)
360          elif isinstance(key, slice): # pragma: no cover
361              raise NotImplementedError(
362                  "Deleting slice of ExtendedListMapping"
363              )
364          else:
365              v = self._map_.pop(key)
366              self._list_.remove(v)
367  
368      def clear(self) -> None:
369          if self._frozen_:
370              raise TypeError("Can't change a frozen list")
371          self._list_.clear()
372          self._map_.clear()
373  
374      def pop(self, # type: ignore[override]
375          key: Optional[Union[_index_typevar_, int]]=None,
376      ) -> _elem_typevar_:
377          if self._frozen_:
378              raise TypeError("Can't change a frozen list")
379          if key is None:
380              elem = self._list_.pop()
381              self._map_.pop(getattr(elem, self._index_attribute_))
382          elif isinstance(key, int):
383              elem = self._list_.pop(key)
384              self._map_.pop(getattr(elem, self._index_attribute_))
385          else:
386              elem = self._map_.pop(key)
387              self._list_.remove(elem)
388          return elem
389  
390      def popitem(self):
391          raise NotImplementedError("ExtendedListMapping.popitem()")
392  
393      def update(self, # type: ignore[override]
394          __m: Mapping[_index_typevar_, _elem_typevar_]
395      ) -> None:
396          raise NotImplementedError("ExtendedListMapping.update()")
397  
398      def __iter__(self) -> Iterator[_elem_typevar_]:
399          return iter(self._list_)
400  
401      def __len__(self) -> int:
402          return len(self._list_)
403  
404      def __contains__(self, elem: Any) -> bool:
405          return getattr(elem, self._index_attribute_) in self._map_
406  
407      def index(self, elem: _elem_typevar_) -> int: # type: ignore[override]
408          """
409          API Notes:
410              * Specifying start/end is currently not supported
411          """
412          return self._list_.index(elem)
413  
414      def insert(self, index: int, value: _elem_typevar_) -> None:
415          if self._frozen_:
416              raise TypeError("Can't change a frozen list")
417          self._list_.insert(index, value)
418          self._map_[getattr(value, self._index_attribute_)] = value
419  
420      def keys(self):
421          return self._map_.keys()
422  
423      def items(self):
424          return self._map_.items()
425  
426      def values(self):
427          return self._map_.values()
428  
429      def __iadd__(self: _child_class_,
430          x: MultiT[_elem_typevar_],
431      ) -> _child_class_:
432          if cast("ExtendedListMapping", self)._frozen_:
433              raise TypeError("Can't change a frozen list")
434          cself = cast(ExtendedListMapping[_elem_typevar_, _index_typevar_], self)
435          new = cast_MultiT(x)
436          cself._list_ += new
437          for e in new:
438              cself._map_[getattr(e, cself._index_attribute_)] = e
439          return self
440  
441      def _freeze_(self) -> None:
442          self._list_._freeze_()
443  
444      @property
445      def _frozen_(self) -> bool:
446          return self._list_._frozen_
447  
448      def _reorder_(self, neworder: Iterable[int]) -> None:
449          if self._frozen_:
450              raise TypeError("Can't reorder a frozen list")
451          self._list_._reorder_(neworder=neworder)
452  
453  
454  class _ListMappingOverride(
455      MutableSequence[_elem_typevar_], MutableMapping[_index_typevar_, _elem_typevar_],
456      Generic[_elem_typevar_, _index_typevar_],
457  ):
458      """A support class for helping subclassing of ListMapping. It allows to subclass
459      a ListMapping class with an element that is a subclass of the parent ListMapping
460      element.
461  
462      TODO: Extended internal API documentation.
463      """
464      @overload
465      def __getitem__(self, key: Union[int, _index_typevar_]) -> _elem_typevar_:
466          ... # pragma: no cover
467      @overload
468      def __getitem__(self: _child_class_, key: slice) -> _child_class_:
469          ... # pragma: no cover
470      def __getitem__(self: _child_class_, # type: ignore[override]
471          key: Union[int, slice, _index_typevar_],
472      ) -> Union[_elem_typevar_, _child_class_]:
473          return cast(Any, super()).__getitem__(key)
474  
475      @overload
476      def __setitem__(self,
477          key: Union[int, _index_typevar_], value: _elem_typevar_,
478      ) -> None:
479          ... # pragma: no cover
480      @overload
481      def __setitem__(self, key: slice, value: Iterable[_elem_typevar_]) -> None:
482          ... # pragma: no cover
483      def __setitem__(self,
484          key: Union[int, slice, _index_typevar_],
485          value: MultiT[_elem_typevar_],
486      ) -> None:
487          return cast(Any, super()).__setitem__(key, value)
488  
489      def __delitem__(self,
490          key: Union[int, slice, _index_typevar_],
491      ) -> None:
492          return cast(Any, super()).__delitem__(key)
493  
494      def pop(self, # type: ignore[override]
495          key: Optional[_index_typevar_]=None,
496      ) -> _elem_typevar_:
497          return cast(Any, super()).pop(key)
498  
499      def update(self, # type: ignore[override]
500          __m: Mapping[_index_typevar_, _elem_typevar_]
501      ) -> None:
502          return cast(Any, super()).update(__m)
503  
504      def __iter__(self) -> Iterator[_elem_typevar_]:
505          return cast(Any, super()).__iter__()
506  
507      def index(self, elem: _elem_typevar_) -> int: # type: ignore[override]
508          return cast(Any, super()).index(elem)
509  
510      def insert(self, index: int, value: _elem_typevar_) -> None:
511          return cast(Any, super()).insert(index, value)
512  
513      def __iadd__(self: _child_class_,
514          x: MultiT[_elem_typevar_],
515      ) -> _child_class_:
516          return cast(Any, super()).__iadd__(x)
517  
518  
519  class ExtendedListStrMapping(ExtendedListMapping[_elem_typevar_, str], Generic[_elem_typevar_]):
520      """TypeListMapping where the index `type_` is `str`. By default this also take 'name'
521      as default attribute name for the index. This can be overloaded in a subclass if
522      needed.
523  
524      TODO: Extended internal API documentation.
525      """
526      @property
527      def _index_attribute_(self):
528          return "name"
529  
530      def __getattr__(self, name: str) -> _elem_typevar_:
531          return self._map_[name]
532  
533  
534  class ListStrMappingOverride(
535      _ListMappingOverride[_elem_typevar_, str], Generic[_elem_typevar_],
536  ):
537      """A support class for helping subclassing of TypeListStrMapping analog to
538      `_ListMappingOverride`.
539  
540      TODO: Extended internal API documentation.
541      """
542      def __getattr__(self, name: str) -> _elem_typevar_:
543          return cast(Any, super()).__getattr__(name)