Source code for lighttree.interactive

import re
import unicodedata

from typing import Optional, Union, List, Any, Dict, TypeVar, Generic
from lighttree.tree import Tree


[docs]def is_valid_attr_name(item: Any) -> bool: if not isinstance(item, str): return False if item.startswith("__"): return False if re.search(string=item, pattern=r"^[a-zA-Z_]+[a-zA-Z0-9_]*$") is None: return False if re.search(string=item, pattern=r"[^_]") is None: return False return True
def _coerce_attr(attr: Any) -> Union[str, None]: if not len(attr): return None new_attr = unicodedata.normalize("NFD", attr).encode("ASCII", "ignore").decode() new_attr = re.sub(string=new_attr, pattern=r"[^a-zA-Z_0-9]", repl="_") if new_attr[0].isdigit(): new_attr = "_" + new_attr if is_valid_attr_name(new_attr): return new_attr return None
[docs]class Obj: """Object class that allows to get items both by attribute `__getattribute__` access: `obj.attribute` or by dict `__getitem__` access: >>> obj = Obj(key='value') >>> obj.key 'value' >>> obj['key'] 'value' In Ipython interpreter, attributes will be available in autocomplete (except private ones): >>> obj = Obj(key='value', key2='value2') >>> obj.k # press tab for autocompletion key key2 Items names that are not compliant with python attributes (accepted characters are [a-zA-Z0-9\_] without beginning with a figure), will be only available through dict `__getitem__` access. """ _REPR_NAME: Optional[str] = None _STRING_KEY_CONSTRAINT: bool = True _COERCE_ATTR: bool = False def __init__(self, **kwargs: Any) -> None: # will store non-valid names self.__d: Dict[str, Any] = dict() for k, v in kwargs.items(): if not is_valid_attr_name(k) or k == "_Obj__d": raise ValueError( "Attribute <%s> of type <%s> is not valid." % (k, type(k)) ) self[k] = v def __getitem__(self, item: Any) -> Any: # when calling d[key] if is_valid_attr_name(item): return self.__getattribute__(item) else: return self.__d[item] def __setitem__(self, key: Any, value: Any) -> None: # d[key] = value if not isinstance(key, str): if self._STRING_KEY_CONSTRAINT: raise ValueError( "Key <%s> of type <%s> cannot be set as attribute on <%s> instance." % (key, type(key), self.__class__.__name__) ) self.__d[key] = value return if is_valid_attr_name(key): super(Obj, self).__setattr__(key, value) return if self._COERCE_ATTR: # if coerc_attr is set to True, try to coerce n_key = _coerce_attr(key) if n_key is not None: super(Obj, self).__setattr__(n_key, value) return self.__d[key] = value def __keys(self) -> List[Any]: return list(self.__d.keys()) + [ k for k in self.__dict__.keys() if k != "_Obj__d" ] def __contains__(self, item: Any) -> bool: return item in self.__keys() def __str__(self) -> str: return "<%s> %s" % ( str(self.__class__._REPR_NAME or self.__class__.__name__), str(sorted(map(str, self.__keys()))), ) def __repr__(self) -> str: return self.__str__()
GenericTree = TypeVar("GenericTree", bound=Tree)
[docs]class TreeBasedObj(Obj, Generic[GenericTree]): """ Recursive Obj whose structure is defined by a lighttree.Tree object. The main purpose of this object is to iteratively expand the tree as attributes of this object. To avoid creating useless instances, only direct children of accessed nodes are expanded. """ _COERCE_ATTR: bool = False _ATTR: Optional[str] = None def __init__( self, tree: GenericTree, root_path: Optional[str] = None, depth: int = 1, initial_tree: Optional[GenericTree] = None, ) -> None: super(TreeBasedObj, self).__init__() self._tree: GenericTree = tree self._root_path: Optional[str] = root_path self._initial_tree: GenericTree = ( initial_tree if initial_tree is not None else tree ) self._expand_attrs(depth) def _clone(self, nid: str, root_path: str, depth: int) -> "TreeBasedObj": _, st = self._tree.subtree(nid) return self.__class__( tree=st, root_path=root_path, depth=depth, initial_tree=self._initial_tree, ) def _expand_attrs(self, depth: int) -> None: if depth: r = self._tree.root if r is None: return for child_key, child_node in self._tree.children(nid=r): if self._ATTR: child_key = getattr(child_node, self._ATTR) str_child_key: str if isinstance(child_key, str): if self._COERCE_ATTR: # if invalid coercion, coerce returns None, in this case we keep inital naming str_child_key = _coerce_attr(child_key) or child_key else: str_child_key = child_key else: str_child_key = "i%s" % child_key child_root: str if self._root_path is not None: child_root = "%s.%s" % (self._root_path, child_key) else: child_root = str(child_key) if child_key else "" self[str_child_key] = self._clone( child_node.identifier, root_path=child_root, depth=depth - 1 ) def __getattribute__(self, item: Any) -> Any: # called by __getattribute__ will always refer to valid attribute item r = super(TreeBasedObj, self).__getattribute__(item) if isinstance(r, TreeBasedObj): r._expand_attrs(depth=1) return r def __getitem__(self, item: Any) -> Any: if is_valid_attr_name(item): r = super(TreeBasedObj, self).__getattribute__(item) else: r = super(TreeBasedObj, self).__getitem__(item) if isinstance(r, TreeBasedObj): r._expand_attrs(depth=1) return r def _show(self, *args: Any, **kwargs: Any) -> str: tree_repr = self._tree.show(*args, **kwargs) if self._root_path is None: return "<%s>\n%s" % ( str(self.__class__._REPR_NAME or self.__class__.__name__), str(tree_repr), ) current_path = self._root_path return "<%s subpart: %s>\n%s" % ( str(self.__class__._REPR_NAME or self.__class__.__name__), str(current_path), str(tree_repr), ) def __call__(self, *args: Any, **kwargs: Any) -> GenericTree: return self._tree def __str__(self) -> str: return self._show()