Passed
Push — master ( c3ccf9...0a4dd5 )
by Max
50s
created

structured_data.enum   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 137
dl 0
loc 207
rs 8.72
c 0
b 0
f 0
wmc 46

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

12 Functions

Rating   Name   Duplication   Size   Complexity  
A _parse_constructor() 0 5 2
A _set_new_functions() 0 12 4
A enum() 0 11 2
F _process_class() 0 60 14
A _name() 0 3 1
A _make_nested_new() 0 7 2
A _get_args_from_index() 0 4 3
A _extract_tuple_ast() 0 13 5
A _interpret_args_from_non_string() 0 5 2
A _checked_eval() 0 5 2
A _args() 0 7 3
A _enum_super() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like structured_data.enum often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 _interpret_args_from_non_string(constructor):
47
    try:
48
        return ARGS.get(constructor)
49
    except TypeError:
50
        return None
51
52
53
def _parse_constructor(constructor):
54
    try:
55
        return ast.parse(constructor, mode='eval')
56
    except Exception as err:
57
        raise ValueError('parsing annotation failed')
58
59
60
def _get_args_from_index(index):
61
    if isinstance(index, ast.Tuple):
62
        return tuple(astor.to_source(elt) for elt in index.elts)
63
    return (astor.to_source(index),)
64
65
66
def _checked_eval(source, global_ns):
67
    try:
68
        return eval(source, global_ns)
69
    except Exception:
70
        return None
71
72
73
def _extract_tuple_ast(constructor, global_ns):
74
    ctor_ast = _parse_constructor(constructor)
75
    if (
76
            isinstance(ctor_ast.body, ast.Subscript)
77
            and isinstance(ctor_ast.body.slice, ast.Index)):
78
        index = ctor_ast.body.slice.value
79
        ctor_ast.body = ctor_ast.body.value
80
        value = _checked_eval(compile(ctor_ast, '<annotation>', 'eval'), global_ns)
81
        if value is Ctor:
82
            return _get_args_from_index(index)
83
        if value is None:
84
            return None
85
    return _interpret_args_from_non_string(_checked_eval(constructor, global_ns))
86
87
88
def _args(constructor, global_ns):
89
    if isinstance(constructor, str):
90
        try:
91
            return _extract_tuple_ast(constructor, global_ns)
92
        except ValueError:
93
            return None
94
    return _interpret_args_from_non_string(constructor)
95
96
97
def _name(cls, function) -> str:
98
    """Return the name of a function accessed through a descriptor."""
99
    return function.__get__(None, cls).__name__
100
101
102
def _set_new_functions(cls, *functions) -> typing.Optional[str]:
103
    """Attempt to set the attributes corresponding to the functions on cls.
104
105
    If any attributes are already defined, fail *before* setting any, and
106
    return the already-defined name.
107
    """
108
    for function in functions:
109
        if _name(cls, function) in cls.__dict__:
110
            return _name(cls, function)
111
    for function in functions:
112
        setattr(cls, _name(cls, function), function)
113
    return None
114
115
116
def _enum_super(_cls):
117
    def base(cls, args):
118
        return super(_cls, cls).__new__(cls, args)
119
    return base
120
121
122
def _make_nested_new(_cls, subclasses, base__new__):
123
    @staticmethod
124
    def __new__(cls, args):
125
        if cls not in subclasses:
126
            raise TypeError
127
        return base__new__(cls, args)
128
    return __new__
129
130
131
def _process_class(_cls, _repr, eq, order):
132
    if order and not eq:
133
        raise ValueError('eq must be true if order is true')
134
135
    argses = {}
136
    subclasses = set()
137
    subclass_order = []
138
    for cls in reversed(_cls.__mro__):
139
        for key, value in getattr(cls, '__annotations__', {}).items():
140
            args = _args(value, vars(sys.modules[cls.__module__]))
141
            # Shadow redone annotations.
142
            if args is None:
143
                argses.pop(key, None)
144
            else:
145
                argses[key] = args
146
147
    for name, args in argses.items():
148
        make_constructor(_cls, name, args, subclasses, subclass_order)
149
150
    _cls.__init_subclass__ = PrewrittenMethods.__init_subclass__
151
152
    if _set_new_functions(_cls, _make_nested_new(_cls, subclasses, _enum_super(_cls))):
153
        _cls.__new__ = _make_nested_new(_cls, subclasses, _cls.__new__)
154
155
    _set_new_functions(
156
        _cls, PrewrittenMethods.__setattr__, PrewrittenMethods.__delattr__)
157
    _set_new_functions(_cls, PrewrittenMethods.__bool__)
158
159
    if _repr:
160
        _set_new_functions(_cls, PrewrittenMethods.__repr__)
161
162
    equality_methods_were_set = False
163
164
    if eq:
165
        equality_methods_were_set = not _set_new_functions(
166
            _cls, PrewrittenMethods.__eq__, PrewrittenMethods.__ne__)
167
168
    if equality_methods_were_set:
169
        _cls.__hash__ = PrewrittenMethods.__hash__
170
171
    if order:
172
        if not equality_methods_were_set:
173
            raise ValueError(
174
                "Can't add ordering methods if equality methods are provided.")
175
        collision = _set_new_functions(
176
            _cls,
177
            PrewrittenMethods.__lt__,
178
            PrewrittenMethods.__le__,
179
            PrewrittenMethods.__gt__,
180
            PrewrittenMethods.__ge__
181
            )
182
        if collision:
183
            raise TypeError(
184
                'Cannot overwrite attribute {collision} in class '
185
                '{name}. Consider using functools.total_ordering'.format(
186
                    collision=collision, name=_cls.__name__))
187
188
    SUBCLASS_ORDER[_cls] = tuple(subclass_order)
189
190
    return _cls
191
192
193
def enum(_cls=None, *, repr=True, eq=True, order=False):
194
    """Decorate a class to be an algebraic data type."""
195
196
    def wrap(cls):
197
        """Return the processed class."""
198
        return _process_class(cls, repr, eq, order)
199
200
    if _cls is None:
201
        return wrap
202
203
    return wrap(_cls)
204
205
206
__all__ = ['Ctor', 'enum']
207