File: _hintable.py

package info (click to toggle)
python-typish 1.9.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 332 kB
  • sloc: python: 1,636; makefile: 2
file content (109 lines) | stat: -rw-r--r-- 3,864 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import inspect
import re
from functools import wraps
from typing import Dict, Optional, Callable, List

_DEFAULT_PARAM_NAME = 'hint'


class _Hintable:
    _hints_per_frame = {}

    def __init__(
            self,
            decorated: Callable,
            param: str,
            stack_index: int) -> None:
        self._decorated = decorated
        self._param = param
        self._stack_index = stack_index

    def __call__(self, *args, **kwargs):
        stack = inspect.stack()

        previous_frame = stack[self._stack_index]
        frame_id = id(previous_frame.frame)

        if not self._hints_per_frame.get(frame_id):
            code_context = previous_frame.code_context[0].strip()
            hint_strs = self._extract_hints(code_context)
            globals_ = previous_frame.frame.f_globals
            # Store the type hint if any, otherwise the string, otherwise None.
            hints = [self._to_cls(hint_str, globals_) or hint_str or None
                     for hint_str in hint_strs]
            self._hints_per_frame[frame_id] = hints

        hint = (self._hints_per_frame.get(frame_id) or [None]).pop()

        kwargs_ = {**kwargs, self._param: kwargs.get(self._param, hint)}
        return self._decorated(*args, **kwargs_)

    def _extract_hints(self, code_context: str) -> List[str]:
        result = []
        regex = (
            r'.+?(:(.+?))?=\s*'  # e.g. 'x: int = ', $2 holds hint
            r'.*?{}\s*\(.*?\)\s*'  # e.g. 'func(...) '
            r'(#\s*type\s*:\s*(\w+))?\s*'  # e.g. '# type: int ', $4 holds hint
        ).format(self._decorated.__name__)

        # Find all matches and store them (reversed) in the resulting list.
        for _, group2, _, group4 in re.findall(regex, code_context):
            raw_hint = (group2 or group4).strip()
            if self._is_between(raw_hint, '\'') or self._is_between(raw_hint, '"'):
                # Remove any quotes that surround the hint.
                raw_hint = raw_hint[1:-1].strip()
            result.insert(0, raw_hint)

        return result

    def _is_between(self, subject: str, character: str) -> bool:
        return subject.startswith(character) and subject.endswith(character)

    def _to_cls(self, hint: str, f_globals: Dict[str, type]) -> Optional[type]:
        return __builtins__.get(hint, f_globals.get(hint))


def _get_wrapper(decorated, param: str, stack_index: int):
    @wraps(decorated)
    def _wrapper(*args, **kwargs):
        return _Hintable(decorated, param, stack_index)(*args, **kwargs)

    if isinstance(decorated, type):
        raise TypeError('Only functions and methods should be decorated with '
                        '\'hintable\', not classes.')

    if param not in inspect.signature(decorated).parameters:
        raise TypeError('The decorated \'{}\' must accept a parameter with '
                        'the name \'{}\'.'
                        .format(decorated.__name__, param))

    return _wrapper


def hintable(decorated=None, *, param: str = _DEFAULT_PARAM_NAME) -> Callable:
    """
    Allow a function or method to receive the type hint of a receiving
    variable.

    Example:

    >>> @hintable
    ... def cast(value, hint):
    ...     return hint(value)
    >>> x: int = cast('42')
    42

    Use this decorator wisely. If a variable was hinted with a type (e.g. int
    in the above example), your function should return a value of that type
    (in the above example, that would be an int value).

    :param decorated: a function or method.
    :param param: the name of the parameter that receives the type hint.
    :return: the decorated function/method wrapped into a new function.
    """
    if decorated is not None:
        wrapper = _get_wrapper(decorated, param, 2)
    else:
        wrapper = lambda decorated_: _get_wrapper(decorated_, param, 2)

    return wrapper