Source code for typetree.typetree

# -*- coding: utf-8 -*-
"""Object type tree analyzer. The GUI is handled by a separate module."""

import dataclasses
import enum
import functools
import json
import re

from collections.abc import Callable
from typing import Any, Type

try:
    from viewer import tree_viewer
except (ModuleNotFoundError, ImportError):
    from .viewer import tree_viewer

_DEFAULT_MAX_LINES = 1000
_DEFAULT_MAX_SEARCH = 100_000
_DEFAULT_MAX_DEPTH = 20
_DEFAULT_MAX_BRANCHES = float('inf')

# Pre-compiled format for _KeyType.INDEX and _KeyType.SLICE
_RANGE_REGEX = re.compile(r'^\[(\d+)(?::(\d+))?]$')


[docs]@functools.total_ordering class KeyType(enum.Enum): """Node key types. Each key will be displayed differently based on their type. """ NONE = 0, 'None' """For the root node, which has no key.""" ATTR = 1, 'Attribute' """For object attributes (starts with a dot). Examples: .attr, ._protected, .__special__. """ MAP = 2, 'Mapping' """For dict-like Mapping keys. Examples: ['key'], [datetime.date(1970, 1, 1)]. """ INDEX = 3, 'Sequence' """For array-like Sequence indices or slices. The indices must be consecutive integers. Examples: [2], [4:7], [:3], [:]. """ SET = 4, 'Collection' """For set-like Collections, which have item counters, but no key. Example: (×3). The path to the item will be set to .copy().pop() for convenience, even though not all Collections accept these methods. """
[docs] @classmethod def path(cls, key_type: 'KeyType', value: Any = None) -> str: """Key path from the parent object to the current one.""" match key_type, value: case cls.NONE, None: return '' case cls.NONE, str(x): return x case cls.ATTR, _: return f'.{value!s}' case cls.MAP, _: return f'[{value!r}]' case cls.SET, int(): return '.copy().pop()' case cls.INDEX, int(x): return f'[{x}]' case cls.INDEX, None: return '[:]' case cls.INDEX, (int(x), int(y)): if x + 1 == y: return f'[{x}]' if x == 0: return f'[:{y}]' return f'[{x}:{y}]' raise TypeError(f"Invalid key type '{key_type}' or value '{value}'")
[docs] @classmethod def str(cls, key_type: 'KeyType', value: Any = None) -> str: """Key label to be displayed in the tree view.""" match key_type, value: case cls.NONE, None: return '' case cls.SET, int(x): return '(×{:d}) '.format(x) case _: return f'{cls.path(key_type, value)}: '
def __lt__(self, other: 'KeyType'): """For sorting. Respect the declaration order.""" if not isinstance(other, type(self)): return TypeError return self.value < other.value
@functools.total_ordering class _NodeKey: """Node key for string representation and for sorting.""" def __init__(self, key_type: KeyType, value: Any = None): self._str: str = KeyType.str(key_type, value) self._path: str = KeyType.path(key_type, value) self._type: KeyType = key_type self._counter: int = 1 if key_type == KeyType.SET: if not isinstance(value, int): raise TypeError self._counter = value self._slice: tuple[int, int] | None = None if key_type == KeyType.INDEX: if isinstance(value, int): self._slice = value, value + 1 else: self._slice = value # Hash is unique for Sets self._hash: int = hash(( type(self), self._type, self._slice, id(self)*(self._type == KeyType.SET) )) @property def path(self) -> str: return self._path @property def type(self) -> KeyType: return self._type @property def counter(self) -> int: return self._counter @property def slice(self) -> tuple[int, int] | None: return self._slice def reset_counter(self): self._counter = 1 self._str = KeyType.str(self.type, self._counter) def increment_counter(self): self._counter += 1 self._str = KeyType.str(self.type, self._counter) def __str__(self) -> str: return self._str def __repr__(self) -> str: return f'{type(self).__name__}({self})' def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): raise NotImplementedError if self._type == other._type == KeyType.SET: return False return self._str == other._str def __lt__(self, other: object) -> bool: if not isinstance(other, type(self)): raise NotImplementedError if self._str == other._str: return False if self._slice is not None: if other._slice is None: return self._type < other._type return self._slice < other._slice if self._type == other._type: if self._type == KeyType.SET: if self._counter != other._counter: return self._counter < other._counter return self._hash < other._hash return self._str < other._str return self._type < other._type def __hash__(self) -> int: return self._hash def get_itself(var: Any) -> Any: """Do nothing, return itself.""" return var def get_type_name(var: Any) -> str: """Return the input's type name.""" return str(type(var).__name__) def getattr_maker(attr: str) -> Callable[[Any], Any]: """Build a partial `getattr` for a fixed attribute.""" def attr_get(var: Any, attr_copy: str = attr) -> Any: return getattr(var, attr_copy) return attr_get
[docs]@dataclasses.dataclass(slots=True, frozen=True) class Template: """Default template for configuration properties of :class:`Tree`.""" items_lookup: Callable[[Any], Any] = get_itself type_name_lookup: Callable[[Any], str] = get_type_name value_lookup: Callable[[Any], Any] = get_itself sort_keys: bool = True show_lengths: bool = True include_attributes: bool = True include_dir: bool = False include_protected: bool = False include_special: bool = False max_search: float = _DEFAULT_MAX_SEARCH max_depth: float = _DEFAULT_MAX_DEPTH max_branches: float = _DEFAULT_MAX_BRANCHES
[docs]@dataclasses.dataclass(slots=True, frozen=True) class DOM(Template): """Template for generating a tree view of DOM etree objects.""" items_lookup: Callable[[Any], Any] = getattr_maker('childNodes') type_name_lookup: Callable[[Any], str] = getattr_maker('nodeName') value_lookup: Callable[[Any], Any] = getattr_maker('text')
[docs]@dataclasses.dataclass(slots=True, frozen=True) class HTML(Template): """Template for generating a tree view of HTML etree objects.""" type_name_lookup: Callable[[Any], str] = getattr_maker('tag') value_lookup: Callable[[Any], Any] = getattr_maker('text')
[docs]@dataclasses.dataclass(slots=True, frozen=True) class XML(Template): """Template for generating a tree view of XML etree objects.""" type_name_lookup: Callable[[Any], str] = getattr_maker('tag') value_lookup: Callable[[Any], Any] = getattr_maker('text')
class _MaxSearchError(Exception): """Reached maximum number of nodes to be searched.""" pass class _NodeInfo: """Non-recursive tree node info.""" def __init__(self, obj: Any, node_key: _NodeKey, config: Template, nodes_searched: int, ancestors_ids: set[int], depth: int): self.key: _NodeKey = node_key self.path: str = node_key.path self.config: Template = config self.nodes_searched: int = nodes_searched self.maxed_search: bool = False # For displaying an ellipsis indicating possible inner content self.maxed_depth: bool = depth >= config.max_depth is_infinite_recursion: bool = id(obj) in ancestors_ids self.type_name: str try: self.type_name = self.config.type_name_lookup(obj) except AttributeError: self.type_name = '?' original_var: Any = obj obj = self.config.items_lookup(obj) # These refer to the contents of Maps or Sequences self.items_key_type: KeyType = KeyType.NONE self.items_len: int | None = None self.update_items_info(obj, original_var) self.branches: dict[_NodeKey, Any] = {} if not is_infinite_recursion: try: self.add_branches(obj) except _MaxSearchError: pass self.var_repr: str = f'<{self.type_name}>' if is_infinite_recursion: self.var_repr = f'<...> {self.var_repr}' if self.config.show_lengths and self.items_len is not None: self.var_repr = f'{self.var_repr}[{self.items_len}]' def update_items_info(self, var: Any, original_var: Any): """Check which kind of iterable var is, if any, and update. Update `self.items_key_type` and `self.items_len` if needed. """ # Check for Collections try: var, size = self.get_var_len(var, original_var) # If TypeError, var is not a Collection of items # Also ignore possible attribute lookup error except (TypeError, AttributeError): return # Check for Mappings if self.is_mapping(var): self.items_key_type = KeyType.MAP self.items_len = size return # Check for Sequences if self.is_sequence(var): self.items_key_type = KeyType.INDEX self.items_len = size return # Check for set-like Collections if self.is_collection(var): self.items_key_type = KeyType.SET self.items_len = size def get_var_len(self, var: Any, original_var: Any) -> tuple[Any, int]: """Check for contents in `var`. If no content is found, use the value lookup for tree leaves. Return the value and the number of items it contains. Raise :exc:`TypeError` if not a Collection. """ size: int = len(var) # Raises TypeError if not a Collection if not size: var = self.config.value_lookup(original_var) # value_lookup might return itself and var might be empty size = len(var) if isinstance(var, str | bytes | bytearray): raise TypeError return var, size @staticmethod def is_mapping(var: Any) -> bool: try: # Since Maps are usually also Sequences, the priority is # to access the Map items. But if the keys do not match # their Sequence values, the priority inverts. if (all(key1 == key2 for key1, key2 in zip(var, var.keys())) and all(var[key] == value for key, value in var.items()) and len(var) == len(var.keys()) == len(var.items())): return True except (AttributeError, TypeError, KeyError): pass return False @staticmethod def is_sequence(var: Any) -> bool: try: if all(var[index] == value for index, value in enumerate(var)): return True except (KeyError, TypeError): pass return False @staticmethod def is_collection(var: Any) -> bool: try: next(iter(var)) # Unknown type: has len, but is not an iterable. Ignore contents. except (TypeError, KeyError): return False # Is an empty Collection except StopIteration: pass return True def add_branches(self, var: Any): if self.config.include_attributes and hasattr(var, '__dict__'): for key, value in vars(var).items(): if self.include_attr(key): self.add_branch(KeyType.ATTR, key, value) if self.config.include_dir: for key in dir(var): if self.include_attr(key): try: value = getattr(var, key) except AttributeError: continue self.add_branch(KeyType.ATTR, key, value) match self.items_key_type: case KeyType.MAP: for key, value in var.items(): self.add_branch(KeyType.MAP, key, value) case KeyType.INDEX: for index, value in enumerate(var): self.add_branch(KeyType.INDEX, index, value) case KeyType.SET: for value in var: self.add_branch(KeyType.SET, 1, value) # Success -- do not display ellipsis indicating max depth exceeded self.maxed_depth = False def add_branch(self, key_type: KeyType, key: Any, value: Any): if self.nodes_searched >= self.config.max_search: self.maxed_search = True if not self.branches: # empty # For displaying an ellipsis indicating possible inner content self.maxed_depth = True raise _MaxSearchError if self.maxed_depth: raise _MaxSearchError node_key = _NodeKey(key_type, key) self.branches[node_key] = value self.nodes_searched += 1 def include_attr(self, key: str) -> bool: if key.startswith('__') and key.endswith('__'): return self.config.include_special if key.startswith('_'): return self.config.include_protected return True def __lt__(self, other): if self.key.type == other.key.type == KeyType.SET: if self.key.counter != other.key.counter: return self.key.counter < other.key.counter return self.path < other.path return self.key < other.key def __str__(self) -> str: return f'{self.key}{self.var_repr}' def __repr__(self) -> str: return f'{type(self).__name__}({self})' class _InfoTree: """A recursive tree builder of :class:`_NodeInfo`.""" def __init__(self, obj: Any, node_key: _NodeKey, config: Template, nodes_searched: int, ancestors_ids: set[int], depth: int = 0): self.config: Template = config self.ancestors_ids: set[int] = ancestors_ids.copy() self.depth: int = depth self.node_info: _NodeInfo = _NodeInfo( obj, node_key, config, nodes_searched, ancestors_ids, self.depth ) self.nodes_searched: int = self.node_info.nodes_searched self.ancestors_ids.add(id(obj)) self.is_complete: bool = not self.node_info.branches self.maxed_depth: bool = (bool(self.depth >= config.max_depth and self.node_info.branches) or self.node_info.maxed_depth) self.has_maxed_depth: bool = self.maxed_depth self.branches: list[_InfoTree] = [] self.updated: bool = False def update(self): """Recursively call to update the deepest :class:`_InfoTree`.""" if self.is_complete: return elif self.updated: for info_tree in self.branches: info_tree.nodes_searched = self.nodes_searched info_tree.update() self._update_info_tree(info_tree) else: self.updated = True for key, var in self.node_info.branches.items(): info_tree: _InfoTree = _InfoTree( var, key, self.config, self.nodes_searched, self.ancestors_ids, self.depth + 1, ) self.branches.append(info_tree) self._update_info_tree(info_tree) self.is_complete = all(info_tree.is_complete for info_tree in self.branches) def _update_info_tree(self, info_tree: '_InfoTree'): if info_tree.has_maxed_depth: self.has_maxed_depth = True if self.nodes_searched == info_tree.nodes_searched: info_tree.is_complete = True self.nodes_searched = info_tree.nodes_searched class _SubtreeCreator: """Create a :class:`Subtree` instance. Used for pre-computing and analysing the branches. Needed because Subtree inherits from tuple, which is immutable, and the creation process is too complex to be done inside :meth:`_SubtreeCreator.__new__`. """ def __init__(self, cls: Type['Subtree'], info_tree: _InfoTree): self.config: Template = info_tree.config info: _NodeInfo = info_tree.node_info node_key: _NodeKey = info.key self.all_branches: tuple[Subtree, ...] = tuple( Subtree(sub_info_tree) for sub_info_tree in info_tree.branches ) self.group_branches() max_branches: int = int(min(float(len(self.all_branches)), self.config.max_branches)) overflowed: bool = info.maxed_search or (len(self.all_branches) > self.config.max_branches) self.subtree: Subtree = tuple.__new__( cls, self.all_branches[:max_branches] ) self.subtree._node_key = node_key self.subtree._config = self.config self.subtree._info = info self.subtree._label = str(info) self.subtree._overflowed = overflowed self.subtree._maxed_depth = info.maxed_depth @staticmethod def group_to_map(v: list[set[int]]) -> dict[tuple[int, int], int]: """Convert groups of index mappings to consecutive range maps. Argument `v` is a list of indices grouped in sets that map to the same structure in a Sequence tree. Their positions indicate where they map to. Example: `branches = [A, B, A, A, C, A, B]` `unique_branches = [A, B, C]` `v = [{0, 2, 3, 5}, {1, 6}, {4}]` means that `A` shows in indices `v[0] = {0, 2, 3, 5}`, `B` shows in indices `v[1] = {1, 6}`, and `C` shows in `v[2] = {4}`. The return value is a dict of sequential ranges of indices as keys and their mapping to unique_branches. In the previous case it will return `{(0, 1): 0, (1, 2): 1, (2, 4): 0, (4, 5): 2, (5, 6): 0, (6, 7): 1}`. """ if not v: return {} u: dict[tuple[int, int], int] = {} for k, s in enumerate(v): if not s: continue sv: list[int] = list(sorted(s)) su: list[tuple[int, int]] = [(sv[0], sv[0] + 1)] for x in sv[1:]: # type: int if x == su[-1][1]: su[-1] = (su[-1][0], x + 1) else: su.append((x, x + 1)) for t in su: # type: tuple[int, int] u[t] = k # noinspection PyTypeChecker return dict(sorted(u.items())) # noinspection PyProtectedMember def group_branches(self): """Group equivalent branches (with the same type structure).""" if not self.all_branches: return # Group unique consecutive Sequence branches and # unique Collection branches unique_index_branches: list[Subtree] = [] index_branch_groups: list[list[Subtree]] = [] index_key_groups: list[set[int]] = [] unique_set_branches: list[Subtree] = [] added_branches: list[Subtree] = [] for branch in self.all_branches: # Process Sequences if branch._node_key.type == KeyType.INDEX: range_key = range(*branch._node_key.slice) try: index = unique_index_branches.index(branch) except ValueError: unique_index_branches.append(branch) index_branch_groups.append([branch]) index_key_groups.append(set(range_key)) else: index_branch_groups[index].append(branch) index_key_groups[index].update(range_key) # Process Collections elif branch._node_key.type == KeyType.SET: try: index = unique_set_branches.index(branch) except ValueError: branch._node_key.reset_counter() unique_set_branches.append(branch) else: unique_set_branches[index]._node_key.increment_counter() # Do not group other types else: added_branches.append(branch) # Update node keys unique_index_branches = self.update_index_keys(index_branch_groups, index_key_groups) for branch in unique_set_branches: branch._update_key(branch._node_key) # Join and sort self.all_branches = tuple( added_branches + unique_index_branches + unique_set_branches ) self.sort_branches() # noinspection PyProtectedMember def update_index_keys(self, branch_groups: list[list['Subtree']], key_groups: list[set[int]]) -> list['Subtree']: updated_branches: list[Subtree] = [] for _range, index in self.group_to_map(key_groups).items(): branch = branch_groups[index].pop() if self.config.show_lengths: branch._update_key(_NodeKey(KeyType.INDEX, _range)) else: branch._update_key(_NodeKey(KeyType.INDEX, None)) updated_branches.append(branch) return list(sorted(updated_branches, key=lambda x: x.key)) # noinspection PyProtectedMember def sort_branches(self): self.all_branches = tuple(sorted( self.all_branches, key=((lambda x: x._info) if self.config.sort_keys else lambda x: x._node_key._type) ))
[docs]@functools.total_ordering class Subtree(tuple): """A recursive object tree structure.""" # Initialized by _SubtreeCreator _node_key: _NodeKey _config: Template _info: _NodeInfo _label: str _overflowed: bool _maxed_depth: bool # Initialized by __init__ _nodes: int _hash: int _path: str _is_expandable: bool def __new__(cls, info_tree: _InfoTree): """Create an immutable nested subtree.""" return _SubtreeCreator(cls, info_tree).subtree
[docs] def __init__(self, *_args, **_kwargs): """Initialize the remaining subtree attributes.""" self._update_paths() node: Subtree self._nodes = len(self) + sum(node._nodes for node in self) # Hash is unique if overflowed or max depth reached because # the tree is incomplete and equality between incomplete trees # cannot be established. self._hash = hash(( self._info.var_repr, self._info.key.type, id(self)*(self._overflowed or self._maxed_depth), tuple(map(hash, self)), )) self._path = self._node_key.path self._update_paths() self._is_expandable = bool(self or self._overflowed or self._maxed_depth)
@property def key(self) -> str: """Key path from previous node to the current one.""" return self._node_key.path @property def key_type(self) -> KeyType: """Return the key type used. Can be a Sequence index, a Mapping key, an attribute, a Set (empty key), or none (also empty). """ return self._node_key.type @property def path(self) -> str: """Return the key path from the root node to the current one.""" return self._path @property def type(self) -> str: """Return the type name of the object the node corresponds to.""" return self._info.type_name @property def label(self) -> str: """Return the full text displayed for the node.""" return self._label @property def nodes(self) -> int: """Return the number of inner nodes indexed.""" return self._nodes @property def config(self) -> Template: """Return the settings used for generating the tree.""" return self._config @property def is_expandable(self) -> bool: """Return a flag for whether the node has inner content. If true, the node will show as expandable in the tree view. """ return self._is_expandable @property def maxed_depth(self) -> bool: """Return a flag for maximum depth reached. Only evaluate to `True` if the object the node refers to has inner content that was not indexed. """ return self._maxed_depth @property def overflowed(self) -> bool: """Return a flag for maximum branch exceeded. If `True`, the node has more branches than indexed. Might be from reaching either `max_search` or `max_branches`. """ return self._overflowed def _update_key(self, new_key: _NodeKey): self._node_key = new_key self._info.key = new_key self._label = str(self._info) self._update_paths() def _update_paths(self, parent_path: str = ''): if self._node_key.type == KeyType.SET: self._path = f'{parent_path}.copy().pop()' else: self._path = f'{parent_path}{self._node_key.path}' for branch in self: # type: Subtree branch._update_paths(self._path) def _get_tree_lines(self, max_lines: float, root_pad: str = '', branch_pad: str = '') -> list[str]: lines: list[str] = [f'{root_pad}{self._label}'] if self._maxed_depth and self._is_expandable: lines.append(f'{branch_pad}└── ...') return lines if not self: # empty return lines end: int = len(self) if not self._overflowed: end -= 1 for branch in self[:end]: # type: Subtree lines.extend(branch._get_tree_lines( max_lines=max_lines, root_pad=f'{branch_pad}├── ', branch_pad=f'{branch_pad}│ ', )) if self._overflowed: lines.append(f'{branch_pad}...') else: last_branch: Subtree = self[-1] lines.extend(last_branch._get_tree_lines( max_lines=max_lines, root_pad=f'{branch_pad}└── ', branch_pad=f'{branch_pad} ', )) if len(lines) > max_lines: del lines[int(max_lines) - 1:] lines.append(' ...') return lines
[docs] def to_dict(self, max_lines: float) -> str | dict[str, str | dict]: """Create a nested dict representing the type tree structure.""" if not self: # empty return str(self._info) branch: Subtree return { str(branch._info): branch.to_dict(max_lines) for branch in self }
def __eq__(self, other: object) -> bool: """Compare two subtrees. Subtrees containing the same type structure are equivalent, except for set-like nodes. """ if not isinstance(other, type(self)): raise NotImplementedError if self._hash != other._hash: return False if self._info.var_repr != other._info.var_repr: return False if self._info.key.type != other._info.key.type: return False if self._overflowed or self._maxed_depth: return False return super().__eq__(other) def __lt__(self, other: object) -> bool: """Compare two subtrees for sorting.""" if not isinstance(other, type(self)): raise NotImplementedError if self == other: return False if self._info.key.type != other._info.key.type: if self._info.key.type is None: return True if other._info.key.type is None: return False return self._info.key.type < other._info.key.type if self._info.var_repr != other._info.var_repr: return self._info.var_repr < other._info.var_repr if len(self) != len(other): return len(self) < len(other) for x, y in zip(self, other): if x != y: return x < y return self._hash < other._hash def __str__(self) -> str: """Return the string representation of the subtree.""" return f'{self._info!s}{{...}}' def __repr__(self) -> str: """Return the repr of the subtree.""" return f'{type(self).__name__}({self!s})' def __hash__(self) -> int: """Return the hash of the subtree. Equivalent (equal) subtrees always evaluate to the same hash. """ return self._hash
[docs]class Tree(Subtree): """Root node of the type tree representation of a Python object. Root-only (:class:`Tree`) attributes: :param depth: Maximum depth reached :type depth: int :param searches: Total number of nodes searches :type searches: int Inherited (:class:`Subtree`) attributes: :param key: Key path from previous node to the current one :type key: str :param key_type: The key type used. Type `help(KeyType)` for more info :type key_type: KeyType :param path: The key path from the root node to the current one :type path: str :param type: The type name of the object the node corresponds to :type type: str :param label: The full text displayed for the node :type label: str :param nodes: The number of inner nodes indexed :type nodes: int :param config: The settings used for generating the tree :type config: Template :param is_expandable: Flag for whether the node has inner content. If `True`, the node will show as expandable in the tree view :type is_expandable: bool :param maxed_depth: Flag for maximum depth reached. If `True`, the object the node refers to has inner content that was not indexed :type maxed_depth: bool :param overflowed: Flag indicating maximum branch exceeded. If `True`, the node has more branches than indexed. Might be from reaching either `max_search` or `max_branches` :type overflowed: bool """ _depth: int _searches: int max_lines: float def __new__(cls, obj: Any, *, key_text: str | None = None, max_lines: float = _DEFAULT_MAX_LINES, template: Type[Template] = Template, **kwargs): """Use breadth-first search to construct a new instance.""" # Since Tree inherits from tuple, which is immutable, the # instance size must be pre-computed before the instance # initialization. This process is done recursively with a # breadth-first algorithm. # noinspection PyArgumentList config: Template = template(**kwargs) info_tree: _InfoTree = _InfoTree( obj, _NodeKey(KeyType.NONE, key_text), config, nodes_searched=0, ancestors_ids=set(), depth=0 ) depth: int = 0 while not info_tree.is_complete: info_tree.update() depth += 1 tree: Tree = super().__new__(cls, info_tree) tree._depth = depth tree._searches = info_tree.nodes_searched tree.max_lines = max_lines return tree
[docs] def __init__(self, obj: Any, **kwargs): """Build a recursive object tree structure. :param obj: Any Python object to be analysed :type obj: Any :param key_text: Placeholder text for the root key node. Defaults to None :type key_text: str, optional :param template: A configuration template for common object types. Currently supported: `Template` (default), `DOM`, `HTML`, and `XML` :param items_lookup: Function used to access the node's content. Defaults to `lambda var: var` :type items_lookup: Callable[[Any], Any], optional :param type_name_lookup: Function used to get the type name. Defaults to `lambda var: type(var).__name__` :type type_name_lookup: Callable[[Any], Any], optional :param value_lookup: Function used to get the value when the node's content is empty (tree leaves). Defaults to `lambda var: var` :type value_lookup: Callable[[Any], Any], optional :param sort_keys: Flag for sorting keys alphabetically. Defaults to `True` :type sort_keys: bool, optional :param show_lengths: Flag for displaying sizes of iterables. This affects how subtrees are grouped together, since Sequences of different sizes but same content types are considered equivalent. Defaults to `True` :type show_lengths: bool, optional :param include_attributes: Flag for including the mutable attributes returned by `vars()`. Defaults to `True` :type include_attributes: bool, optional :param include_dir: Flag for including the attributes returned by `dir()`, except the protected (`_protected`) and special (`__special__`) ones. Defaults to `False` :type include_dir: bool, optional :param include_protected: Flag for including the protected (`_protected`) attributes. Defaults to `False` :type include_protected: bool, optional :param include_special`: Flag for including the special (`__special__`) attributes. Defaults to `False` :type include_special: bool, optional :param max_lines: Maximum number of lines to be printed For the GUI, it is the maximum number of rows to be displayed, not including the extra ellipsis at the end. Can be disabled by setting it to infinity (`float('inf')` or `math.inf`). Defaults to 1000 :type max_lines: float, optional :param max_search: Maximum number of nodes searched. Defaults to 100,000 :type max_search: float, optional :param max_depth: Maximum search depth. Defaults to 20 :type max_depth: float, optional :param max_branches: Maximum number of branches displayed on each node. This only applies after grouping. Defaults to infinity :type max_branches: float, optional """ super().__init__(obj, **kwargs)
@property def depth(self) -> int: """Maximum depth reached.""" return self._depth @property def searches(self) -> int: """Total number of nodes searched.""" return self._searches
[docs] def to_dict(self, max_lines: float | None = None ) -> str | dict[str, str | dict]: """Return a nested `dict` representation of the type tree.""" if max_lines is None: max_lines = self.max_lines return super().to_dict(max_lines)
[docs] def to_json(self, *args, max_lines: float | None = None, **kwargs) -> str: """Return a JSON representation of the type tree.""" if max_lines is None: max_lines = self.max_lines return json.dumps(self.to_dict(max_lines), *args, **kwargs)
[docs] def save_as_json(self, file_path: str, *args, max_lines: float | None = None, encoding: str = 'utf-8', ensure_ascii: bool = False, indent: int = 4, **kwargs): """Save a JSON representation of the type tree to a file.""" if max_lines is None: max_lines = self.max_lines with open(file_path, 'w', encoding=encoding) as file: json.dump(self.to_dict(max_lines), file, *args, ensure_ascii=ensure_ascii, indent=indent, **kwargs)
[docs] def save_as_text(self, file_path: str, encoding: str = 'utf-8', **kwargs): """Save the tree view as a text file.""" with open(file_path, 'w', encoding=encoding) as f: f.write(self.to_string(**kwargs))
[docs] def to_string(self, max_lines: float | None = None, verbose: bool = False) -> str: """Get a tree view of the object's type structure as a string. :param max_lines: Maximum number of lines to be printed. Can be disabled by setting it to infinity :type max_lines: float, optional :param verbose: Flag for printing extra information before printing the tree view. Defaults to False :type verbose: bool, optional """ lines: list[str] = [] if verbose: lines.append('Nodes (indexed/searched): ' f'{self.nodes}/{self.searches}') lines.append(f'Maximum depth: {self.depth}') if max_lines is None: max_lines = self.max_lines lines.extend(self._get_tree_lines(max_lines, ' ', ' ')) return '\n'.join(lines)
[docs] def print(self, max_lines: float | None = None, verbose: bool = False): """Print a tree view of the object's type structure. :param max_lines: Maximum number of lines to be printed. Can be disabled by setting it to infinity :type max_lines: float, optional :param verbose: Flag for printing extra information before printing the tree view. Defaults to False :type verbose: bool, optional """ print(self.to_string(max_lines=max_lines, verbose=verbose))
[docs] def view(self, spawn_thread: bool = True, spawn_process: bool = False, max_lines: float | None = None): """Show a tree view of the object's type structure in a GUI. :param spawn_thread: Run the GUI in a separate thread :type spawn_thread: bool :param spawn_process: Run the GUI in a separate process :type spawn_process: bool :param max_lines: Maximum number of rows to be displayed, not including the extra ellipsis at the end. Can be disabled by setting it to infinity """ if max_lines is None: max_lines = self.max_lines tree_viewer(self, max_lines, spawn_thread=spawn_thread, spawn_process=spawn_process)
[docs]def view_tree(obj: Any, *, spawn_thread: bool = True, spawn_process: bool = False, **kwargs): """Show a tree view of the object's type structure in a GUI. :param obj: Any Python object to be analysed :type obj: Any :param spawn_thread: Run the GUI in a separate thread :type spawn_thread: bool :param spawn_process: Run the GUI in a separate process :type spawn_process: bool :param kwargs: Same as :class:`Tree`. Type `help(Tree.__init__)` for the full list """ Tree(obj, **kwargs).view(spawn_thread=spawn_thread, spawn_process=spawn_process)