lagom.util.reflection.FunctionSpec.__repr__()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 8.6667

Importance

Changes 0
Metric Value
cc 3
eloc 7
nop 1
dl 0
loc 9
ccs 1
cts 7
cp 0.1429
crap 8.6667
rs 10
c 0
b 0
f 0
1
"""Extra information about the reflection API
2
"""
3
4 1
import inspect
5 1
from functools import lru_cache
6 1
from typing import (
7
    Dict,
8
    Type,
9
    Callable,
10
    get_type_hints,
11
    Optional,
12
    Awaitable,
13
    Any,
14
    Mapping,
15
    Sequence,
16
)
17
18 1
import typing
19
20 1
RETURN_ANNOTATION = "return"
21
22
23 1
_TYPE_AWAITABLE = type(typing.Awaitable)
24
25
26 1
class FunctionSpec:
27
    """
28
    Describes the arguments of a function
29
    """
30
31 1
    args: Sequence[str]
32 1
    annotations: Mapping[str, Type]
33 1
    defaults: Mapping[str, Any]
34 1
    return_type: Optional[Type]
35 1
    arity: int
36
37 1
    def __init__(self, args, annotations, defaults, return_type):
38 1
        self.args = args
39 1
        self.annotations = annotations
40 1
        self.defaults = defaults
41 1
        self.return_type = return_type
42 1
        self.arity = len(args)
43
44 1
    def __repr__(self):
45
        def _arg_type_string(arg):
46
            return self.annotations[arg].__name__ if arg in self.annotations else "?"
47
48
        signature = ", ".join(_arg_type_string(arg) for arg in self.args)
49
        if self.return_type:
50
            return f"({signature}) -> {self.return_type.__name__}"
51
        else:
52
            return f"({signature})"
53
54 1
    def without_argument(self, arg_to_remove: str):
55
        """
56
        Returns the function spec with the specified argument removed
57
        :param arg_to_remove:
58
        :return:
59
        """
60 1
        new_args = [arg for arg in self.args if arg != arg_to_remove]
61 1
        return FunctionSpec(new_args, self.annotations, self.defaults, self.return_type)
62
63
64 1
class CachingReflector:
65
    """
66
    Takes a function and returns an object representing
67
    the function's type signature. Results are cached
68
    so subsequent calls do not need to call the reflection
69
    API.
70
    """
71
72 1
    @property
73 1
    def overview_of_cache(self) -> Dict[str, str]:
74
        """
75
        Gives a humanish readable representation of what has been reflected on.
76
        Removed since lru cache is now used
77
        :return:
78
        """
79 1
        return {"hidden": ""}
80
81 1
    @lru_cache(maxsize=1024)
82 1
    def get_function_spec(self, func) -> FunctionSpec:
83
        """
84
        Returns details about the function's signature
85
        :param func:
86
        :return:
87
        """
88 1
        return reflect(func)
89
90
91 1
def reflect(func: Callable) -> FunctionSpec:
92
    """
93
    Extension to inspect.getfullargspec with a little more.
94
    :param func:
95
    :return:
96
    """
97 1
    spec = inspect.getfullargspec(func)
98 1
    annotations = get_type_hints(func)
99 1
    defaults = _get_default_args(func)
100 1
    ret = annotations.pop(RETURN_ANNOTATION, None)
101 1
    if ret and inspect.iscoroutinefunction(func):
102 1
        ret = Awaitable[ret]  # type: ignore # todo: figure this out
103 1
    return FunctionSpec(spec.args, annotations, defaults, ret)
104
105
106 1
def _get_default_args(func):
107 1
    arguments = inspect.signature(func).parameters.items()
108 1
    return {
109
        name: argument.default
110
        for name, argument in arguments
111
        if argument.default is not inspect.Parameter.empty
112
    }
113
114
115 1
def remove_optional_type(dep_type) -> Optional[Type]:
116
    """if the Type is Optional[T] returns T else None
117
118
    :param dep_type:
119
    :return:
120
    """
121 1
    try:
122
        # Hacky: an optional type has [T, None] in __args__
123 1
        if len(dep_type.__args__) == 2 and dep_type.__args__[1] == None.__class__:
124 1
            return dep_type.__args__[0]
125 1
    except:
126 1
        pass
127 1
    return None
128
129
130 1
def remove_awaitable_type(dep_type) -> Optional[Type]:
131
    """if the Type is Awaitable[T] returns T else None
132
133
    :param dep_type:
134
    :return:
135
    """
136 1
    if isinstance(dep_type, _TYPE_AWAITABLE) or (
137
        hasattr(dep_type, "_name") and dep_type._name == "Awaitable"
138
    ):
139 1
        return dep_type.__args__[0]  # type: ignore
140
    return None
141