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.utilities 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 = (
 31                    "\x1b[38;5;8m└\x1b[39m"
 32                    if idx == len(node.children) - 1
 33                    else "\x1b[38;5;8m├\x1b[39m"
 34                )
 35                nested_prefix = (
 36                    " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m"
 37                )
 38
 39                lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}"
 40                if len(lines) > 1:
 41                    for line in range(1, len(lines)):
 42                        lines[line] = f"{nested_prefix}  {lines[line]}"
 43                results.extend(lines)
 44            else:
 45                lines = signature(child, indent)
 46
 47                child_prefix = (
 48                    "\x1b[38;5;8m└\x1b[39m"
 49                    if idx == len(node.children) - 1
 50                    else "\x1b[38;5;8m├\x1b[39m"
 51                )
 52                nested_prefix = (
 53                    " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m"
 54                )
 55
 56                lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}"
 57                if len(lines) > 1:
 58                    for line in range(1, len(lines)):
 59                        lines[line] = f"{nested_prefix}  {lines[line]}"
 60
 61                results.extend(lines)
 62        return results
 63
 64    if isinstance(start, (Element, Root)):
 65        return "\n".join(recursive_inspect(start, indent))
 66
 67    return "\n".join(signature(start))
 68
 69
 70def signature(node: NODE, indent: int = 2):
 71    """Generate the signature or base information for a single node."""
 72    sig = ""
 73    # element node's tag
 74    if isinstance(node, Element):
 75        sig += f"\x1b[34m{node.tag}\x1b[39m"
 76    else:
 77        sig = f"\x1b[34m{node.type}\x1b[39m"
 78
 79    # count of children in parent node
 80    if isinstance(node, (Element, Root)):
 81        sig += f" \x1b[38;5;8m[{len(node.children) if len(node.children) > 0 else '/'}]\x1b[39m"
 82
 83    # position of non generated nodes
 84    if node.position is not None:
 85        sig += f" {node.position}"
 86
 87    result = [sig]
 88
 89    # element node's properties
 90    if hasattr(node, "properties"):
 91        for line in stringify_props(node):
 92            result.append(f"\x1b[38;5;8m│\x1b[39m{' '*indent}{line}")
 93
 94    # literal node's value
 95    if isinstance(node, (Text, Comment)):
 96        for line in build_literal_value(node):
 97            result.append(f"\x1b[38;5;8m│\x1b[39m{' '*indent}\x1b[32m{line}\x1b[39m")
 98
 99    return result
100
101
102class ComplexEncoder(JSONEncoder):
103    def default(self, obj):
104        try:
105            return JSONEncoder.default(self, obj)
106        except:
107            return repr(obj)
108
109
110def stringify_props(node: Element) -> list[str]:
111    """Generate a list of lines from strigifying the nodes properties."""
112
113    if len(node.properties.keys()) > 0:
114        lines = ["properties: \x1b[38;5;8m{\x1b[39m"]
115        for key, value in node.properties.items():
116            if value is None or isinstance(value, bool):
117                value = f"\x1b[38;5;104m{value}\x1b[39m"
118            elif isinstance(value, str):
119                value = f'\x1b[32m"{value}"\x1b[39m'
120            lines.append(f'  \x1b[32m"{key}"\x1b[39m: {value},')
121        lines.append("\x1b[38;5;8m}\x1b[39m")
122        return lines
123    return []
124
125
126def build_literal_value(node: Text | Comment) -> list[str]:
127    """Build the lines for the string value of a literal node."""
128
129    lines = normalize_indent(node.value).split("\n")
130
131    if len(lines) == 1:
132        lines[0] = f'"{lines[0]}"'
133    else:
134        lines[0] = f'"{lines[0]}'
135        lines[-1] = f' {lines[-1]}"'
136        if len(lines) > 2:
137            for idx in range(1, len(lines) - 1):
138                lines[idx] = f" {lines[idx]}"
139    return lines
140
141
142def normalize_indent(text: str) -> str:
143    """Remove extra prefix whitespace while preserving relative indenting.
144
145    Example:
146    ```python
147        if True:
148            print("Hello World")
149    ```
150
151    becomes
152
153    ```python
154    if True:
155        print("Hello World")
156    ```
157    """
158    lines = text.split("\n")
159
160    # Get min offset
161    if len(lines) > 1:
162        min_offset = len(lines[0])
163        for line in lines:
164            offset = len(line) - len(line.lstrip())
165            if offset < min_offset:
166                min_offset = offset
167    else:
168        return lines[0]
169
170    # Remove min_offset from each line
171    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.utilities 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 = (
32                    "\x1b[38;5;8m└\x1b[39m"
33                    if idx == len(node.children) - 1
34                    else "\x1b[38;5;8m├\x1b[39m"
35                )
36                nested_prefix = (
37                    " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m"
38                )
39
40                lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}"
41                if len(lines) > 1:
42                    for line in range(1, len(lines)):
43                        lines[line] = f"{nested_prefix}  {lines[line]}"
44                results.extend(lines)
45            else:
46                lines = signature(child, indent)
47
48                child_prefix = (
49                    "\x1b[38;5;8m└\x1b[39m"
50                    if idx == len(node.children) - 1
51                    else "\x1b[38;5;8m├\x1b[39m"
52                )
53                nested_prefix = (
54                    " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m"
55                )
56
57                lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}"
58                if len(lines) > 1:
59                    for line in range(1, len(lines)):
60                        lines[line] = f"{nested_prefix}  {lines[line]}"
61
62                results.extend(lines)
63        return results
64
65    if isinstance(start, (Element, Root)):
66        return "\n".join(recursive_inspect(start, indent))
67
68    return "\n".join(signature(start))

Recursively inspect the passed node or ast.

def normalize_indent(text: str) -> str:
143def normalize_indent(text: str) -> str:
144    """Remove extra prefix whitespace while preserving relative indenting.
145
146    Example:
147    ```python
148        if True:
149            print("Hello World")
150    ```
151
152    becomes
153
154    ```python
155    if True:
156        print("Hello World")
157    ```
158    """
159    lines = text.split("\n")
160
161    # Get min offset
162    if len(lines) > 1:
163        min_offset = len(lines[0])
164        for line in lines:
165            offset = len(line) - len(line.lstrip())
166            if offset < min_offset:
167                min_offset = offset
168    else:
169        return lines[0]
170
171    # Remove min_offset from each line
172    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")