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
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 11:07 -0600
1"""phml.utils.misc.inspect
3Logic to inspect any phml node. Outputs a tree representation
4of the node as a string.
5"""
7from json import dumps
9from phml.nodes import AST, All_Nodes, Comment, Element, Root, Text
11__all__ = ["inspect", "normalize_indent"]
14def inspect(start: AST | All_Nodes, indent: int = 2):
15 """Recursively inspect the passed node or ast."""
17 if isinstance(start, AST):
18 start = start.tree
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
24 results = [*signature(node)]
26 for idx, child in enumerate(visit_children(node)):
27 if isinstance(child, (Element, Root)):
28 lines = recursive_inspect(child, indent)
30 child_prefix = "└" if idx == len(node.children) - 1 else "├"
31 nested_prefix = " " if idx == len(node.children) - 1 else "│"
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)
41 child_prefix = "└" if idx == len(node.children) - 1 else "├"
42 nested_prefix = " " if idx == len(node.children) - 1 else "│"
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]}"
49 results.extend(lines)
50 return results
52 if isinstance(start, (Element, Root)):
53 return "\n".join(recursive_inspect(start, indent))
55 return "\n".join(signature(start))
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 ''}>"
65 # count of children in parent node
66 if isinstance(node, (Element, Root)) and len(node.children) > 0:
67 sig += f" [{len(node.children)}]"
69 # position of non generated nodes
70 if node.position is not None:
71 sig += f" {node.position}"
73 result = [sig]
75 # element node's properties
76 if hasattr(node, "properties"):
77 for line in stringify_props(node):
78 result.append(f"│{' '*indent}{line}")
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}")
85 return result
88def stringify_props(node: Element) -> list[str]:
89 """Generate a list of lines from strigifying the nodes properties."""
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 []
98def build_literal_value(node: Text | Comment) -> list[str]:
99 """Build the lines for the string value of a literal node."""
101 lines = normalize_indent(node.value).split("\n")
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
114def normalize_indent(text: str) -> str:
115 """Remove extra prefix whitespac while preserving relative indenting.
117 Example:
118 ```python
119 if True:
120 print("Hello World")
121 ```
123 becomes
125 ```python
126 if True:
127 print("Hello World")
128 ```
129 """
130 lines = text.split("\n")
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]
142 # Remove min_offset from each line
143 return "\n".join([line[min_offset:] for line in lines])