phml.utilities.misc.inspect

phml.utilities.misc.inspect

Logic to inspect any phml node. Outputs a tree representation of the node as a string.

  1"""phml.utilities.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, JSONEncoder
  8
  9from phml.core.nodes import AST, NODE, Comment, Element, Root, Text
 10
 11__all__ = ["inspect", "normalize_indent"]
 12
 13
 14def inspect(start: AST | NODE, 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 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: NODE, 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
 87class ComplexEncoder(JSONEncoder):
 88    def default(self, obj):
 89        try:
 90            return JSONEncoder.default(self, obj)
 91        except:
 92            return repr(obj)
 93
 94def stringify_props(node: Element) -> list[str]:
 95    """Generate a list of lines from strigifying the nodes properties."""
 96
 97    if len(node.properties.keys()) > 0:
 98        lines = dumps(node.properties, indent=2, cls=ComplexEncoder).split("\n")
 99        lines[0] = f"properties: {lines[0]}"
100        return lines
101    return []
102
103
104def build_literal_value(node: Text | Comment) -> list[str]:
105    """Build the lines for the string value of a literal node."""
106
107    lines = normalize_indent(node.value).split("\n")
108
109    if len(lines) == 1:
110        lines[0] = f'"{lines[0]}"'
111    else:
112        lines[0] = f'"{lines[0]}'
113        lines[-1] = f' {lines[-1]}"'
114        if len(lines) > 2:
115            for idx in range(1, len(lines) - 1):
116                lines[idx] = f' {lines[idx]}'
117    return lines
118
119
120def normalize_indent(text: str) -> str:
121    """Remove extra prefix whitespace while preserving relative indenting.
122
123    Example:
124    ```python
125        if True:
126            print("Hello World")
127    ```
128
129    becomes
130
131    ```python
132    if True:
133        print("Hello World")
134    ```
135    """
136    lines = text.split("\n")
137
138    # Get min offset
139    if len(lines) > 1:
140        min_offset = len(lines[0])
141        for line in lines:
142            offset = len(line) - len(line.lstrip())
143            if offset < min_offset:
144                min_offset = offset
145    else:
146        return lines[0]
147
148    # Remove min_offset from each line
149    return "\n".join([line[min_offset:] for line in lines])
15def inspect(start: AST | NODE, indent: int = 2):
16    """Recursively inspect the passed node or ast."""
17
18    if isinstance(start, AST):
19        start = start.tree
20
21    def recursive_inspect(node: Element | Root, indent: int) -> list[str]:
22        """Generate signature for node then for each child recursively."""
23        from phml import visit_children  # pylint: disable=import-outside-toplevel
24
25        results = [*signature(node)]
26
27        for idx, child in enumerate(visit_children(node)):
28            if isinstance(child, (Element, Root)):
29                lines = recursive_inspect(child, indent)
30
31                child_prefix = "└" if idx == len(node.children) - 1 else "├"
32                nested_prefix = " " if idx == len(node.children) - 1 else "│"
33
34                lines[0] = f"{child_prefix}{idx} {lines[0]}"
35                if len(lines) > 1:
36                    for line in range(1, len(lines)):
37                        lines[line] = f"{nested_prefix}  {lines[line]}"
38                results.extend(lines)
39            else:
40                lines = signature(child, indent)
41
42                child_prefix = "└" if idx == len(node.children) - 1 else "├"
43                nested_prefix = " " if idx == len(node.children) - 1 else "│"
44
45                lines[0] = f"{child_prefix}{idx} {lines[0]}"
46                if len(lines) > 1:
47                    for line in range(1, len(lines)):
48                        lines[line] = f"{nested_prefix}  {lines[line]}"
49
50                results.extend(lines)
51        return results
52
53    if isinstance(start, (Element, Root)):
54        return "\n".join(recursive_inspect(start, indent))
55
56    return "\n".join(signature(start))

Recursively inspect the passed node or ast.

def normalize_indent(text: str) -> str:
121def normalize_indent(text: str) -> str:
122    """Remove extra prefix whitespace while preserving relative indenting.
123
124    Example:
125    ```python
126        if True:
127            print("Hello World")
128    ```
129
130    becomes
131
132    ```python
133    if True:
134        print("Hello World")
135    ```
136    """
137    lines = text.split("\n")
138
139    # Get min offset
140    if len(lines) > 1:
141        min_offset = len(lines[0])
142        for line in lines:
143            offset = len(line) - len(line.lstrip())
144            if offset < min_offset:
145                min_offset = offset
146    else:
147        return lines[0]
148
149    # Remove min_offset from each line
150    return "\n".join([line[min_offset:] for line in lines])

Remove extra prefix whitespace while preserving relative indenting.

Example:

if True:
        print("Hello World")
    

becomes

if True:
    print("Hello World")