Coverage for foxplot / node.py: 100%

54 statements  

« 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 

5 

6"""Internal node used to access data in interactive mode.""" 

7 

8from typing import Any, Dict, List, Union, cast 

9 

10from .exceptions import FoxplotError 

11from .hot_series import HotSeries 

12from .series import Series 

13 

14 

15class Node: 

16 """Series data unpacked from input dictionaries.""" 

17 

18 _label: str 

19 

20 def __init__(self, label: str): 

21 """Initialize node with a label. 

22 

23 Args: 

24 label: Node label. 

25 """ 

26 self._label = label 

27 

28 def __getitem__(self, key): 

29 """Get item from node, either a child node or an indexed series (leaf). 

30 

31 Args: 

32 key: Key that identifies the child item. 

33 """ 

34 return self.__dict__[key] 

35 

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}]" 

44 

45 def _get_child(self, keys: List[str]) -> Series: 

46 """Get leaf descendant in the tree from a list of keys. 

47 

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 

57 

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) 

63 

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 

72 

73 def _update(self, index: int, unpacked: Union[None, dict, list]) -> None: 

74 """Update node from a new unpacked dictionary. 

75 

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) 

99 

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)