Test Failed
Push — master ( 201ca1...2e0d62 )
by Max
01:54
created

structured_data.enum   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 204
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 120
dl 0
loc 204
rs 9.36
c 0
b 0
f 0
wmc 38

7 Functions

Rating   Name   Duplication   Size   Complexity  
A _set_new_functions() 0 12 4
A enum() 0 11 2
A _name() 0 3 1
C _args() 0 46 9
F _process_class() 0 60 14
A _make_nested_new() 0 7 2
A _enum_super() 0 4 1

3 Methods

Rating   Name   Duplication   Size   Complexity  
A Ctor.__new__() 0 6 2
A Ctor.__init_subclass__() 0 2 1
A Ctor.__class_getitem__() 0 4 2
1
"""Class decorator for defining abstract data types."""
2
3
import ast
4
import sys
5
import typing
6
import weakref
7
8
import astor
9
10
from ._enum_constructor import make_constructor
11
from ._prewritten_methods import SUBCLASS_ORDER
12
from ._prewritten_methods import PrewrittenMethods
13
14
_CTOR_CACHE = {}
15
16
17
ARGS = weakref.WeakKeyDictionary()
18
19
20
class Ctor:
21
    """Marker class for enum constructors.
22
23
    To use, index with a sequence of types, and annotate a variable in an
24
    enum-decorated class with it.
25
    """
26
27
    def __new__(cls, args):
28
        if args == ():
29
            return cls
30
        self = object.__new__(cls)
31
        ARGS[self] = args
32
        return _CTOR_CACHE.setdefault(args, self)
33
34
    def __init_subclass__(cls, **kwargs):
35
        raise TypeError
36
37
    def __class_getitem__(cls, args):
38
        if not isinstance(args, tuple):
39
            args = (args,)
40
        return cls(args)
41
42
43
ARGS[Ctor] = ()
44
45
46
def _args(constructor, global_ns):
47
    # Handle forward declarations. Needed for 3.7 compatinility.
48
    if isinstance(constructor, str):
49
        # Leverage Python's parser instead of trying to parse by hand.
50
        ctor_ast = ast.parse(constructor, mode='eval')
51
        # The top-level operation must be a subscript, with normal indexing.
52
        if (
53
                isinstance(ctor_ast.body, ast.Subscript)
54
                and isinstance(ctor_ast.body.slice, ast.Index)):
55
            # Pull out the index argument
56
            index = ctor_ast.body.slice.value
57
            # This basically gets rid of the brackets and index.
58
            # Next, try to evaluate the result.
59
            # This relies on Ctor being globally visible at definition time,
60
            # which seems like a reasonable requirement.
61
            ctor_ast.body = ctor_ast.body.value
62
            try:
63
                value = eval(
64
                    compile(ctor_ast, '<annotation>', 'eval'), global_ns)
65
            except Exception:
66
                # We couldn't tell what it was, so it's probably not Ctor.
67
                return None
68
            if value is Ctor:
69
                # Pull the information directly off a Tuple node.
70
                if isinstance(index, ast.Tuple):
71
                    return tuple(astor.to_source(elt) for elt in index.elts)
72
                # Otherwise, assume it's a sequence of length 1.
73
                # It's possible for this heuristic to return false answers, BUT
74
                # it seems like everywhere that AST inspection and evaluation
75
                # disagree, it needs syntax that breaks mypy, so it's sort of
76
                # like it doesn't matter.
77
                return (astor.to_source(index),)
78
        try:
79
            # If the above conditions didn't hold, maybe it's an alias.
80
            # We could try to validate the AST, but we need to know the value
81
            # anyway if it's valid.
82
            constructor = eval(constructor, global_ns)
83
        except Exception:
84
            # This return is a little concerning. It's basically for "We tried
85
            # to evaluate a forward reference of some kind, and failed."
86
            # This might reject input at runtime that mypy would accept.
87
            # The basic solution is to document that you can have forward
88
            # references FROM a Ctor, but not within a decorated class.
89
            return None
90
    # Try to interpret the current value as a Ctor
91
    return ARGS.get(constructor)
92
93
94
def _name(cls, function) -> str:
95
    """Return the name of a function accessed through a descriptor."""
96
    return function.__get__(None, cls).__name__
97
98
99
def _set_new_functions(cls, *functions) -> typing.Optional[str]:
100
    """Attempt to set the attributes corresponding to the functions on cls.
101
102
    If any attributes are already defined, fail *before* setting any, and
103
    return the already-defined name.
104
    """
105
    for function in functions:
106
        if _name(cls, function) in cls.__dict__:
107
            return _name(cls, function)
108
    for function in functions:
109
        setattr(cls, _name(cls, function), function)
110
    return None
111
112
113
def _enum_super(_cls):
114
    def base(cls, args):
115
        return super(_cls, cls).__new__(cls, args)
116
    return base
117
118
119
def _make_nested_new(_cls, subclasses, base__new__):
120
    @staticmethod
121
    def __new__(cls, args):
122
        if cls not in subclasses:
123
            raise TypeError
124
        return base__new__(cls, args)
125
    return __new__
126
127
128
def _process_class(_cls, _repr, eq, order):
129
    if order and not eq:
130
        raise ValueError('eq must be true if order is true')
131
132
    argses = {}
133
    subclasses = set()
134
    subclass_order = []
135
    for cls in reversed(_cls.__mro__):
136
        for key, value in getattr(cls, '__annotations__', {}).items():
137
            args = _args(value, vars(sys.modules[cls.__module__]))
138
            # Shadow redone annotations.
139
            if args is None:
140
                argses.pop(key, None)
141
            else:
142
                argses[key] = args
143
144
    for name, args in argses.items():
145
        make_constructor(_cls, name, args, subclasses, subclass_order)
146
147
    _cls.__init_subclass__ = PrewrittenMethods.__init_subclass__
148
149
    if _set_new_functions(_cls, _make_nested_new(_cls, subclasses, _enum_super(_cls))):
150
        _cls.__new__ = _make_nested_new(_cls, subclasses, _cls.__new__)
151
152
    _set_new_functions(
153
        _cls, PrewrittenMethods.__setattr__, PrewrittenMethods.__delattr__)
154
    _set_new_functions(_cls, PrewrittenMethods.__bool__)
155
156
    if _repr:
157
        _set_new_functions(_cls, PrewrittenMethods.__repr__)
158
159
    equality_methods_were_set = False
160
161
    if eq:
162
        equality_methods_were_set = not _set_new_functions(
163
            _cls, PrewrittenMethods.__eq__, PrewrittenMethods.__ne__)
164
165
    if equality_methods_were_set:
166
        _cls.__hash__ = PrewrittenMethods.__hash__
167
168
    if order:
169
        if not equality_methods_were_set:
170
            raise ValueError(
171
                "Can't add ordering methods if equality methods are provided.")
172
        collision = _set_new_functions(
173
            _cls,
174
            PrewrittenMethods.__lt__,
175
            PrewrittenMethods.__le__,
176
            PrewrittenMethods.__gt__,
177
            PrewrittenMethods.__ge__
178
            )
179
        if collision:
180
            raise TypeError(
181
                'Cannot overwrite attribute {collision} in class '
182
                '{name}. Consider using functools.total_ordering'.format(
183
                    collision=collision, name=_cls.__name__))
184
185
    SUBCLASS_ORDER[_cls] = tuple(subclass_order)
186
187
    return _cls
188
189
190
def enum(_cls=None, *, repr=True, eq=True, order=False):
191
    """Decorate a class to be an algebraic data type."""
192
193
    def wrap(cls):
194
        """Return the processed class."""
195
        return _process_class(cls, repr, eq, order)
196
197
    if _cls is None:
198
        return wrap
199
200
    return wrap(_cls)
201
202
203
__all__ = ['Ctor', 'enum']
204