_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)