Coverage for foxplot / node.py: 100%
54 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-15 10:53 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-15 10:53 +0000
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# SPDX-License-Identifier: Apache-2.0
6"""Internal node used to access data in interactive mode."""
8from typing import Any, Dict, List, Union, cast
10from .exceptions import FoxplotError
11from .hot_series import HotSeries
12from .series import Series
15class Node:
16 """Series data unpacked from input dictionaries."""
18 _label: str
20 def __init__(self, label: str):
21 """Initialize node with a label.
23 Args:
24 label: Node label.
25 """
26 self._label = label
28 def __getitem__(self, key):
29 """Get item from node, either a child node or an indexed series (leaf).
31 Args:
32 key: Key that identifies the child item.
33 """
34 return self.__dict__[key]
36 def __repr__(self):
37 """String representation of the node."""
38 keys = ", ".join(
39 str(key)
40 for key in self.__dict__
41 if isinstance(key, int) or not key.startswith("_")
42 )
43 return f"{self._label}: [{keys}]"
45 def _get_child(self, keys: List[str]) -> Series:
46 """Get leaf descendant in the tree from a list of keys.
48 Args:
49 keys: List of keys uniquely identifying the leaf descendant.
50 """
51 child = self.__dict__[keys[0]]
52 if len(keys) > 1:
53 return child._get_child(keys[1:])
54 if not isinstance(child, Series):
55 raise FoxplotError(f"{child._label} is not a time series")
56 return child
58 def _items(self):
59 for key, child in self.__dict__.items():
60 if isinstance(key, str) and key.startswith("_"):
61 continue
62 yield (key, child)
64 def _list_labels(self) -> List[str]:
65 """List all labels reachable from this node."""
66 labels = []
67 for key, child in self.__dict__.items():
68 if isinstance(key, int) or key.startswith("_"):
69 continue
70 labels.extend(child._list_labels())
71 return labels
73 def _update(self, index: int, unpacked: Union[None, dict, list]) -> None:
74 """Update node from a new unpacked dictionary.
76 Args:
77 index: Index of the unpacked dictionary in the sequential input.
78 unpacked: Unpacked dictionary.
79 """
80 if unpacked is None:
81 return
82 items = (
83 unpacked.items()
84 if isinstance(unpacked, dict)
85 else enumerate(unpacked)
86 )
87 for key, value in items:
88 # Explicitly signal to the type checker that keys can be integers
89 self_dict = cast(Dict[Union[str, int], Any], self.__dict__)
90 if key in self.__dict__:
91 child = self_dict[key]
92 else: # key not in self.__dict__
93 sep = "/" if not self._label.endswith("/") else ""
94 is_primitive = not isinstance(value, (dict, list))
95 ChildClass = HotSeries if is_primitive else Node
96 child = ChildClass(label=f"{self._label}{sep}{key}")
97 self_dict[key] = child
98 child._update(index, value)
100 def _freeze(self, max_index: int) -> None:
101 update = {}
102 for key, child in self.__dict__.items():
103 if isinstance(child, HotSeries):
104 update[key] = child._freeze(max_index)
105 elif isinstance(child, Node):
106 child._freeze(max_index)
107 self.__dict__.update(update)