Coverage for phml\utils\misc\inspect.py: 88%

73 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 11:07 -0600

1"""phml.utils.misc.inspect 

2 

3Logic to inspect any phml node. Outputs a tree representation 

4of the node as a string. 

5""" 

6 

7from json import dumps 

8 

9from phml.nodes import AST, All_Nodes, Comment, Element, Root, Text 

10 

11__all__ = ["inspect", "normalize_indent"] 

12 

13 

14def inspect(start: AST | All_Nodes, indent: int = 2): 

15 """Recursively inspect the passed node or ast.""" 

16 

17 if isinstance(start, AST): 

18 start = start.tree 

19 

20 def recursive_inspect(node: Element | Root, indent: int) -> list[str]: 

21 """Generate signature for node then for each child recursively.""" 

22 from phml.utils import visit_children # pylint: disable=import-outside-toplevel 

23 

24 results = [*signature(node)] 

25 

26 for idx, child in enumerate(visit_children(node)): 

27 if isinstance(child, (Element, Root)): 

28 lines = recursive_inspect(child, indent) 

29 

30 child_prefix = "└" if idx == len(node.children) - 1 else "├" 

31 nested_prefix = " " if idx == len(node.children) - 1 else "│" 

32 

33 lines[0] = f"{child_prefix}{idx} {lines[0]}" 

34 if len(lines) > 1: 

35 for line in range(1, len(lines)): 

36 lines[line] = f"{nested_prefix} {lines[line]}" 

37 results.extend(lines) 

38 else: 

39 lines = signature(child, indent) 

40 

41 child_prefix = "└" if idx == len(node.children) - 1 else "├" 

42 nested_prefix = " " if idx == len(node.children) - 1 else "│" 

43 

44 lines[0] = f"{child_prefix}{idx} {lines[0]}" 

45 if len(lines) > 1: 

46 for line in range(1, len(lines)): 

47 lines[line] = f"{nested_prefix} {lines[line]}" 

48 

49 results.extend(lines) 

50 return results 

51 

52 if isinstance(start, (Element, Root)): 

53 return "\n".join(recursive_inspect(start, indent)) 

54 

55 return "\n".join(signature(start)) 

56 

57 

58def signature(node: All_Nodes, indent: int = 2): 

59 """Generate the signature or base information for a single node.""" 

60 sig = f"{node.type}" 

61 # element node's tag 

62 if isinstance(node, Element): 

63 sig += f"<{node.tag}{'/' if node.startend else ''}>" 

64 

65 # count of children in parent node 

66 if isinstance(node, (Element, Root)) and len(node.children) > 0: 

67 sig += f" [{len(node.children)}]" 

68 

69 # position of non generated nodes 

70 if node.position is not None: 

71 sig += f" {node.position}" 

72 

73 result = [sig] 

74 

75 # element node's properties 

76 if hasattr(node, "properties"): 

77 for line in stringify_props(node): 

78 result.append(f"│{' '*indent}{line}") 

79 

80 # literal node's value 

81 if isinstance(node, (Text, Comment)): 

82 for line in build_literal_value(node): 

83 result.append(f"│{' '*indent}{line}") 

84 

85 return result 

86 

87 

88def stringify_props(node: Element) -> list[str]: 

89 """Generate a list of lines from strigifying the nodes properties.""" 

90 

91 if len(node.properties.keys()) > 0: 

92 lines = dumps(node.properties, indent=2).split("\n") 

93 lines[0] = f"properties: {lines[0]}" 

94 return lines 

95 return [] 

96 

97 

98def build_literal_value(node: Text | Comment) -> list[str]: 

99 """Build the lines for the string value of a literal node.""" 

100 

101 lines = normalize_indent(node.value).split("\n") 

102 

103 if len(lines) == 1: 

104 lines[0] = f'"{lines[0]}"' 

105 else: 

106 lines[0] = f'"{lines[0]}' 

107 lines[-1] = f' {lines[-1]}"' 

108 if len(lines) > 2: 

109 for idx in range(1, len(lines) - 1): 

110 lines[idx] = f' {lines[idx]}' 

111 return lines 

112 

113 

114def normalize_indent(text: str) -> str: 

115 """Remove extra prefix whitespac while preserving relative indenting. 

116 

117 Example: 

118 ```python 

119 if True: 

120 print("Hello World") 

121 ``` 

122 

123 becomes 

124 

125 ```python 

126 if True: 

127 print("Hello World") 

128 ``` 

129 """ 

130 lines = text.split("\n") 

131 

132 # Get min offset 

133 if len(lines) > 1: 

134 min_offset = len(lines[0]) 

135 for line in lines: 

136 offset = len(line) - len(line.lstrip()) 

137 if offset < min_offset: 

138 min_offset = offset 

139 else: 

140 return lines[0] 

141 

142 # Remove min_offset from each line 

143 return "\n".join([line[min_offset:] for line in lines])