Passed
Push — master ( bb717c...d524ea )
by Guibert
01:01
created

async_btree.analyze.Node.__str__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
"""Analyze definition."""
2
from inspect import getclosurevars
3
from typing import Any, Callable, List, NamedTuple, Optional, Tuple, no_type_check
4
5
from .definition import CallableFunction, NodeMetadata
6
7
8
__all__ = ["analyze", "stringify_analyze", "Node"]
9
10
11
class Node(NamedTuple):
12
    """Node aggregate node definition implemented with NamedTuple.
13
14
    A Node is used to keep information on name, properties, and relations ship
15
    between a hierachical construct of functions.
16
    It's like an instance of NodeMetadata.
17
18
    Attributes:
19
        name (str): named operation.
20
        properties (List[Tuple[str, Any]]): a list of tuple (name, value) for definition.
21
        edges (List[Tuple[str, List[Any]]]): a list of tuple (name, node list) for
22
            definition.
23
24
    Notes:
25
        Edges attribut should be edges: ```List[Tuple[str, List['Node']]]```
26
        But it is impossible for now, see [mypy issues 731](https://github.com/python/mypy/issues/731)
27
    """
28
29
    name: str
30
    properties: List[Tuple[str, Any]]
31
    # edges: List[Tuple[str, List['Node']]]
32
    # https://github.com/python/mypy/issues/731
33
    edges: List[Tuple[str, List[Any]]]
34
35
    def __str__(self):
36
        return stringify_analyze(target=self)
37
38
39
def _get_analyze_edges(nonlocals) -> Callable:
40
41
    return _get_analyze_edges
42
43
44
@no_type_check
45
def _get_node_metadata(target: CallableFunction) -> NodeMetadata:
46
    """Returns node metadata instance associated with target."""
47
    node = target.__node_metadata
48
    if not isinstance(node, NodeMetadata):
49
        raise RuntimeError(f'attr __node_metadata of {target} is not a NodeMetadata!')
50
    return node
51
52
53
# pylint: disable=protected-access
54
@no_type_check  # it's a shortcut for hasattr ...
55
def analyze(target: CallableFunction) -> Node:
56
    """Analyze specified target and return a Node representation.
57
58
    Args:
59
        target (CallableFunction): async function to analyze.
60
61
    Returns:
62
        (Node): a node instance representation of target function
63
    """
64
65
    nonlocals = getclosurevars(target).nonlocals
66
67
    def _get_nonlocals_value_for(name):
68
        return nonlocals[name] if name in nonlocals else None
69
70
    def _analyze_property(p):
71
        """Return a tuple (name, value) or (name, function name) as property."""
72
        value = _get_nonlocals_value_for(name=p)
73
        return p, value.__name__ if value and callable(value) else value
74
75
    def _analyze_edges(egde_name):
76
        """Lookup children node from egde_name local var."""
77
        value = None
78
        edge = _get_nonlocals_value_for(name=egde_name)
79
        if edge:
80
            # it could be a collection of node
81
            if hasattr(edge, "__iter__"):
82
                value = list(map(analyze, edge))
83
            else:  # or a single node
84
                value = [analyze(edge)]
85
        return (egde_name, value)
86
87
    if hasattr(target, "__node_metadata"):
88
        node = _get_node_metadata(target=target)
89
        return Node(
90
            name=node.name,
91
            properties=list(map(_analyze_property, node.properties)),
92
            edges=list(filter(lambda p: p is not None, map(_analyze_edges, node.edges))),
93
        )
94
95
    # simple function
96
    return Node(
97
        name=target.__name__.lstrip("_") if hasattr(target, "__name__") else "anonymous",
98
        properties=list(map(_analyze_property, nonlocals.keys())),
99
        edges=[],
100
    )
101
102
103
def stringify_analyze(target: Node, indent: int = 0, label: Optional[str] = None) -> str:
104
    """Stringify node representation of specified target.
105
106
    Args:
107
        target (CallableFunction): async function to analyze.
108
        indent (int): level identation (default to zero).
109
        label (Optional[str]): label of current node (default None).
110
111
    Returns:
112
        (str): a string node representation.
113
    """
114
    _ident = '    '
115
    _space = f'{_ident * indent} '
116
    result: str = ''
117
    if label:
118
        result += f'{_space}--({label})--> {target.name}:\n'
119
        _space += f"{_ident}{' ' * len(label)}"
120
    else:
121
        result += f'{_space}--> {target.name}:\n'
122
123
    for k, v in target.properties:
124
        result += f'{_space}    {k}: {v}\n'
125
126
    for _label, children in target.edges:
127
        if children:
128
            for child in children:
129
                result += stringify_analyze(target=child, indent=indent + 1, label=_label)
130
    return result
131