structured_data._adt.ctor   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 33
eloc 99
dl 0
loc 169
rs 9.76
c 0
b 0
f 0

9 Functions

Rating   Name   Duplication   Size   Complexity  
A annotation_is_classvar() 0 16 3
A _str_is_classvar() 0 15 5
A _extract_tuple_ast() 0 18 5
A _interpret_args_from_non_string() 0 7 2
A _interpret_classvar_from_non_string() 0 7 3
A _checked_eval() 0 11 2
A _parse_constructor() 0 5 2
A get_args() 0 16 3
A _get_args_from_index() 0 4 2

3 Methods

Rating   Name   Duplication   Size   Complexity  
A Ctor.__init_subclass__() 0 2 1
A Ctor.__new__() 0 6 2
A Ctor.__class_getitem__() 0 7 3
1
"""Internal implementation of helper types for Sum annotations."""
2
3
from __future__ import annotations
4
5
import ast
6
import types
7
import typing
8
import weakref
9
10
import astor  # type: ignore
11
12
_CTOR_CACHE: typing.Dict[typing.Tuple, "Ctor"] = {}
13
14
15
AnyCtor = typing.Union["Ctor", typing.Type["Ctor"]]
16
17
ARGS: typing.MutableMapping[AnyCtor, typing.Tuple] = weakref.WeakKeyDictionary()
18
19
20
class Ctor:
21
    """Marker class for adt constructors.
22
23
    To use, index with a sequence of types, and annotate a variable in an
24
    adt-decorated class with it.
25
    """
26
27
    __slots__ = ("__weakref__",)
28
29
    def __new__(cls, args: typing.Tuple[typing.Any, ...]) -> Ctor:
30
        if args == ():
31
            raise ValueError
32
        self = object.__new__(cls)
33
        ARGS[self] = args
34
        return _CTOR_CACHE.setdefault(args, self)
35
36
    def __init_subclass__(cls, **kwargs: typing.Any) -> None:
37
        raise TypeError
38
39
    def __class_getitem__(cls, args: typing.Any) -> AnyCtor:
40
        if not isinstance(args, tuple):
41
            args = (args,)
42
        if not args:
43
            return cls
44
        # Yes it is.
45
        return cls(args)  # pylint: disable=not-callable
46
47
48
ARGS[Ctor] = ()
49
50
51
def _interpret_args_from_non_string(
52
    constructor: typing.Any,
53
) -> typing.Optional[typing.Tuple]:
54
    try:
55
        return ARGS.get(constructor)
56
    except TypeError:
57
        return None
58
59
60
def _interpret_classvar_from_non_string(annotation: typing.Any) -> bool:
61
    if annotation is typing.ClassVar:
62
        return True
63
    try:
64
        return annotation.__origin__ is typing.ClassVar
65
    except AttributeError:
66
        return False
67
68
69
def _parse_constructor(constructor: str) -> ast.Expression:
70
    try:
71
        return typing.cast(ast.Expression, ast.parse(constructor, mode="eval"))
72
    except Exception:
73
        raise ValueError("parsing annotation failed")
74
75
76
def _get_args_from_index(index: ast.AST) -> typing.Tuple:
77
    if isinstance(index, ast.Tuple):
78
        return tuple(astor.to_source(elt) for elt in index.elts)
79
    return (astor.to_source(index),)
80
81
82
def _checked_eval(
83
    source: typing.Union[str, types.CodeType], global_ns: typing.Dict[str, typing.Any]
84
) -> typing.Any:
85
    try:
86
        # Oh no, the user might end up executing arbitrary code that they wrote
87
        # in the first place.
88
        return eval(source, global_ns)  # pylint: disable=eval-used
89
    # If we hit this, anything could have gone wrong, but just assume it's not
90
    # anything we care about.
91
    except Exception:  # pylint: disable=broad-except
92
        return None
93
94
95
NO_VALUE = object()
96
97
98
def _extract_tuple_ast(
99
    constructor: str, global_ns: typing.Dict[str, typing.Any]
100
) -> typing.Optional[typing.Tuple]:
101
    ctor_ast = _parse_constructor(constructor)
102
    value = index = NO_VALUE
103
    if isinstance(ctor_ast.body, ast.Subscript) and isinstance(
104
        ctor_ast.body.slice, ast.Index
105
    ):
106
        index = ctor_ast.body.slice.value
107
        ctor_ast.body = ctor_ast.body.value
108
        value = _checked_eval(compile(ctor_ast, "<annotation>", "eval"), global_ns)
109
    if value is Ctor:
110
        # If value is Ctor, then value was set, which is only possible if the
111
        # previous block executed, which will set "index" to an AST.
112
        return _get_args_from_index(typing.cast(ast.AST, index))
113
    if value is None:
114
        return None
115
    return _interpret_args_from_non_string(_checked_eval(constructor, global_ns))
116
117
118
def _str_is_classvar(annotation: str, global_ns: typing.Dict[str, typing.Any]) -> bool:
119
    annotation_ast = _parse_constructor(annotation)
120
    value = NO_VALUE
121
    if isinstance(annotation_ast.body, ast.Subscript) and isinstance(
122
        annotation_ast.body.slice, ast.Index
123
    ):
124
        annotation_ast.body = annotation_ast.body.value
125
        value = _checked_eval(
126
            compile(annotation_ast, "<annotation>", "eval"), global_ns
127
        )
128
    if value is typing.ClassVar:
129
        return True
130
    if value is None:
131
        return False
132
    return _interpret_classvar_from_non_string(_checked_eval(annotation, global_ns))
133
134
135
def get_args(
136
    constructor: typing.Any, global_ns: typing.Dict[str, typing.Any]
137
) -> typing.Optional[typing.Tuple]:
138
    """Given annotation value and module namespace, return Ctor args, if any.
139
140
    The function first checks if the value is a string. If so, it tries to
141
    parse it.
142
    Otherwise, if the value is a Ctor instance, it extracts the annotation, and
143
    if not, it returns ``None``.
144
    """
145
    if isinstance(constructor, str):
146
        try:
147
            return _extract_tuple_ast(constructor, global_ns)
148
        except ValueError:
149
            return None
150
    return _interpret_args_from_non_string(constructor)
151
152
153
def annotation_is_classvar(
154
    annotation: typing.Any, global_ns: typing.Dict[str, typing.Any]
155
) -> bool:
156
    """Given annotation value and module namespace, return whether it's a ClassVar.
157
158
    The function first checks if the value is a string. If so, it tries to
159
    parse it.
160
    Otherwise, if the value is a ClassVar instance, it returns ``True``.
161
    If not, it returns ``False``.
162
    """
163
    if isinstance(annotation, str):
164
        try:
165
            return _str_is_classvar(annotation, global_ns)
166
        except ValueError:
167
            return False
168
    return _interpret_classvar_from_non_string(annotation)
169