Source code for pantra.common

from __future__ import annotations

import types
import typing
import ctypes
import itertools

if typing.TYPE_CHECKING:
    from typing import *

__all__ = ['typename', 'UniNode', 'UniqueNode', 'DynamicClasses', 'DynamicStyles', 'WebUnits',
           'EmptyCaller', 'raise_exception_in_thread', 'HTML', 'DynamicDict', 'ScopeDict']

from .oid import OIDGenerator


def typename(t):
    return type(t).__name__

[docs] class UniNode: """Simple tree of nodes""" __slots__ = ['_children', '_parent'] def __init__(self, parent: Optional[Self] = None): self._parent: Self | None = parent if parent: parent.append(self) self._children: list[Self] = [] @property def parent(self) -> Self: """parent property""" return self._parent @parent.setter def parent(self, parent: Self | None): if self._parent: self._parent._children.remove(self) if parent: parent.append(self) self._parent = parent @property def children(self) -> list[Self]: """children property""" return self._children
[docs] def append(self, node: Self): """append node to children""" self._children.append(node)
[docs] def remove(self, node: Self): """remove node from children""" node.parent = None
[docs] def clear(self): """clear all children""" self._children.clear()
[docs] def index(self): """return index of current node""" return self._parent._children.index(self)
[docs] def move_child(self, from_i: int, to_i: int): """move child node from one index to another""" if from_i == to_i: return self._children.insert(to_i, self._children.pop(from_i))
[docs] def move(self, to_i: int): """move this node to another index""" if not self._parent: return self._parent.move_child(self.index(), to_i)
def __contains__(self, item: Self): return item in self._children def __getitem__(self, item: int) -> Self: return self._children[item] def __iter__(self) -> Iterable[Self]: yield from self._children
[docs] def path(self) -> str: """return path of current node""" if not self._parent: return str(self) return f'{self._parent.path()}/{self}'
@property def root(self): """return root node of this tree""" res = self while res.parent: res = res.parent return res
[docs] def select(self, predicate: Callable[[Self], bool], depth: int = None) -> Generator[Self]: """select all nodes by predicate condition Arguments: predicate: lambda expression, checking each item for condition depth: amount of levels depth for recursion Yields: selected node """ for child in self._children: if predicate(child): yield child if depth is None: yield from child.select(predicate) elif depth > 1: yield from child.select(predicate, depth-1)
[docs] def upto(self, predicate: Union[str, Callable[[Self], bool]]) -> Optional[Self]: """find the closest ascendant by specified predicate Arguments: predicate: lambda expression, checking each item for condition, or string representation of expected node Returns: first matched node or None """ node = self._parent while node: if isinstance(predicate, str): if str(node) == predicate: return node else: if predicate(node): return node node = node.parent return None
[docs] def downto(self, predicate: Callable[[Self], bool], depth: int = None) -> Optional[Self]: """find the closest descendant by specified predicate Arguments: predicate: lambda expression, checking each item for condition, or string representation of expected node depth: amount of levels depth for recursion Returns: first matched node or None """ return next(self.select(predicate, depth), None)
[docs] class UniqueNode(UniNode): """Extension to :class:`UniNode` with unique OID and :mod:`weakref` support Attributes: oid_gen: oid generator oid: unique object identifier (usually, within session) """ __slots__ = ['oid', 'oid_gen', '__weakref__'] def __init__(self, parent: typing.Self = None): self.oid_gen = parent.oid_gen if parent else OIDGenerator() self.oid = self.oid_gen.gen_id(self) super().__init__(parent)
[docs] class HTML(str): """Mark string as safe HTML content. Example:: refs["message"].set_text(HTML("<h1>Big one</h1>")) """
[docs] class DynamicDict(dict[str, typing.Any]): """Dictionary with dynamic (computable) values. It allows to set lambda functions for evaluation and repeated evaluations later:: x = 2 d = DynamicDict() d["f"] = lambda : x**2 print(d["f"]) # -> 4 x = 3 d.refresh() print(d["f"]) # -> 9 Arguments: _lazy_mode (bool): don't evaluate functions at the first assignment """ __slots__ = ['_lazy_mode', '_lambdas'] def __init__(self, *args, _lazy_mode: bool=False, **kwargs): super().__init__(*args, **kwargs) self._lambdas: dict[str, typing.Callable[[], typing.Any]] = {} self._lazy_mode: bool = _lazy_mode def __setitem__(self, key, value): if isinstance(value, types.LambdaType) and value.__name__ == '<lambda>': self._lambdas[key] = value if not self._lazy_mode and key not in self: dict.__setitem__(self, key, value()) else: """ try: del self._factories[key] except KeyError: pass """ dict.__setitem__(self, key, value) def __len__(self): if '$' in self.keys(): return dict.__len__(self) - 1 return dict.__len__(self)
[docs] def items(self): for key, value in super().items(): if key != '$': yield key, value
[docs] def refresh(self, attr_name: str | None = None): """repeated evaluations Affects all functions if `attr_name` is not specified. Arguments: attr_name (str): attribute name for repeated evaluations. """ if attr_name is None: for key, factory_func in self._lambdas.items(): dict.__setitem__(self, key, factory_func()) else: if attr_name in self._lambdas: dict.__setitem__(self, attr_name, self._lambdas[attr_name]())
[docs] def refresh_items(self) -> Iterable[tuple[str, typing.Any]]: """iterate through evaluated items only""" for key, factory_func in self._lambdas.items(): yield key, factory_func()
def __or__(self, other): return self.__class__(super().__or__(other))
[docs] class DynamicClasses(DynamicDict): """Extension to :class:`DynamicDict` to support HTML classes manipulation. It allows basic arithmetic operations intuitively:: c = DynamicClasses("colored bold animated") c['selected'] = lambda: True c += "hidden" c -= "bold" c *= "active" c *= "active" # switch class on and off again print(c) # -> "colored animated selected hidden" """ __slots__ = [] def __init__(self, *args, **kwargs): classes = args[0] if len(args) > 0 else None if isinstance(classes, str): super().__init__(*args[1:], **kwargs) for class_name in classes.split(' '): self[class_name] = True else: super().__init__(*args, **kwargs) @staticmethod def _check_args(other: str) -> bool | NoReturn: if not other: return False if not isinstance(other, str): raise TypeError('Expected string class name to switch on DynamicClasses') return True def __add__(self, other: str) -> Self: if self._check_args(other): for class_name in other.split(' '): self[class_name] = True return self def __sub__(self, other: str) -> Self: if self._check_args(other): for class_name in other.split(' '): self[class_name] = False return self def __mul__(self, other: str) -> Self: if self._check_args(other): for class_name in other.split(' '): self[class_name] = not self.get(class_name, False) return self def __str__(self): base = self.get('$', '') return (base + ' ' if base else '') + ' '.join(k for k, v in self.items() if v)
[docs] class DynamicStyles(DynamicDict): """Extension to :class:`DynamicDict` to support styles parsing Example:: s = DynamicStyles('left: 0; top: 0; width: 100%; height: 100%') print(s['left']) # -> "0" print(s['width']) # -> "100%" s -= "width" s["position"] = lambda: "absolute" s += "width: 80%" print(s) # -> "left: 0; top: 0; height: 100%; width: 80%; position: absolute" """ __slots__ = [] def __init__(self, style: dict | str | None = None): if style: if isinstance(style, dict): super().__init__(style) else: super().__init__() _ = self + style else: super().__init__() @staticmethod def _check_args(other: str) -> bool | NoReturn: if not other: return False if not isinstance(other, str): raise TypeError('Expected string style name to switch on DynamicStyles') return True def __add__(self, style: str) -> Self: if self._check_args(style): for expr in style.split(';'): key, value = expr.split(':', 1) self[key.strip()] = value.strip() return self def __sub__(self, other: str) -> Self: if self._check_args(other): for class_name in other.split(' '): del self[class_name] return self def __str__(self): base = self.get('$', '') if isinstance(base, dict): return ';'.join(f'{k}: {v}' for k, v in itertools.chain(base.items(), self.items()) if v) else: return base + ';'.join(f'{k}: {v}' for k, v in self.items() if v)
class ScopeDict(DynamicDict): """Extension to :class:`DynamicDict` to support scope variables Example:: layer1 = ScopeDict({'a': 1, 'b': 2}) layer2 = ScopeDict(layer1) layer2['b'] = 3 layer1['a'] = 5 print(layer2['a']) # -> 5 print(layer2['b']) # -> 3 """ __slots__ = ['_scope'] def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._scope: Self = args[0] if len(args) > 0 and isinstance(args[0], ScopeDict) else None def __getitem__(self, key: str): try: return super().__getitem__(key) except KeyError: if self._scope is None: raise return self._scope[key] def __contains__(self, item): return super().__contains__(item) or self._scope and item in self._scope
[docs] class WebUnits(str): """Helper type to work with `web units <https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length>`__ This time supports natural arithmetic operations: + - * / // Arguments: value (int | float | str): numeric value unit (str): unit name (px | em | % | ...) """ __slots__ = ['value', 'unit'] def __new__(cls, value: int | float | str, unit: str = 'px'): if not isinstance(value, str): return super().__new__(cls, f'{value}{unit}') else: return super().__new__(cls, value) def __init__(self, value: int | float | str, unit: str = 'px'): if not isinstance(value, str): self.value = value self.unit = unit elif not value: raise ValueError('value cannot be empty') else: pos = next((i for i, c in enumerate(value) if not c.isdigit()), len(value)) self.value = int(value[:pos]) self.unit = value[pos:] def __add__(self, other: int | float): if not other: return self return WebUnits(self.value + other, self.unit) def __sub__(self, other: int | float): if not other: return self return WebUnits(self.value - other, self.unit) def __mul__(self, other): if other == 1: return self return WebUnits(self.value * other, self.unit) def __truediv__(self, other): if other == 1: return self return WebUnits(self.value / other, self.unit) def __floordiv__(self, other): if other == 1: return self return WebUnits(self.value // other, self.unit) def __neg__(self): return WebUnits(-self.value, self.unit)
class EmptyCaller(str): __name__ = 'EmptyCaller' def __new__(cls): return super().__new__(cls, '') def __call__(self, *args, **kwargs): pass def raise_exception_in_thread(thread_id): res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit)) if res > 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) print('Exception raise failure')