Passed
Push — master ( 49cecb...9cd713 )
by Max
01:11
created

structured_data.adt   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 206
dl 0
loc 397
rs 8.48
c 0
b 0
f 0
wmc 49

9 Functions

Rating   Name   Duplication   Size   Complexity  
A _name() 0 3 1
A _cant_set_new_functions() 0 12 3
A _sum_new() 0 12 2
A _set_new_functions() 0 12 3
A cant_modify() 0 9 2
A _set_ordering() 0 9 2
A _product_new() 0 23 3
A _can_set_ordering() 0 3 2
A _ordering_options_are_valid() 0 5 3

12 Methods

Rating   Name   Duplication   Size   Complexity  
A Sum.__new__() 0 5 2
A Product.__getattribute__() 0 5 2
A Sum.__bool__() 0 2 1
A Product.__dir__() 0 2 1
A Product.__new__() 0 10 2
B Sum.__init_subclass__() 0 36 5
B Product.__init_subclass__() 0 46 6
A Sum.__delattr__() 0 4 2
A Product.__bool__() 0 2 1
A Product.__delattr__() 0 4 2
A Product.__setattr__() 0 4 2
A Sum.__setattr__() 0 4 2

How to fix   Complexity   

Complexity

Complex classes like structured_data.adt 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
"""Base classes for defining abstract data types.
2
3
This module provides three public members, which are used together.
4
5
Given a structure, possibly a choice of different structures, that you'd like
6
to associate with a type:
7
8
- First, create a class, that subclasses the Sum class.
9
- Then, for each possible structure, add an attribute annotation to the class
10
  with the desired name of the constructor, and a type of ``Ctor``, with the
11
  types within the constructor as arguments.
12
13
To look inside an ADT instance, use the functions from the
14
:mod:`structured_data.match` module.
15
16
Putting it together:
17
18
>>> from structured_data import match
19
>>> class Example(Sum):
20
...     FirstConstructor: Ctor[int, str]
21
...     SecondConstructor: Ctor[bytes]
22
...     ThirdConstructor: Ctor
23
...     def __iter__(self):
24
...         matchable = match.Matchable(self)
25
...         if matchable(Example.FirstConstructor(match.pat.count, match.pat.string)):
26
...             count, string = matchable[match.pat.count, match.pat.string]
27
...             for _ in range(count):
28
...                 yield string
29
...         elif matchable(Example.SecondConstructor(match.pat.bytes)):
30
...             bytes_ = matchable[match.pat.bytes]
31
...             for byte in bytes_:
32
...                 yield chr(byte)
33
...         elif matchable(Example.ThirdConstructor()):
34
...             yield "Third"
35
...             yield "Constructor"
36
>>> list(Example.FirstConstructor(5, "abc"))
37
['abc', 'abc', 'abc', 'abc', 'abc']
38
>>> list(Example.SecondConstructor(b"abc"))
39
['a', 'b', 'c']
40
>>> list(Example.ThirdConstructor())
41
['Third', 'Constructor']
42
"""
43
44
import inspect
45
import typing
46
47
from . import _adt_constructor
48
from . import _annotations
49
from . import _conditional_method
50
from . import _prewritten_methods
51
52
_T = typing.TypeVar("_T")
53
54
55
if typing.TYPE_CHECKING:  # pragma: nocover
56
57
    class Ctor:
58
        """Dummy class for type-checking purposes."""
59
60
    class ConcreteCtor(typing.Generic[_T]):
61
        """Wrapper class for type-checking purposes.
62
63
        The type parameter should be a Tuple type of fixed size.
64
        Classes containing this annotation (meaning they haven't been
65
        processed by the ``adt`` decorator) should not be instantiated.
66
        """
67
68
69
else:
70
    from ._ctor import Ctor
71
72
73
# This is fine.
74
def _name(cls: type, function) -> str:
75
    """Return the name of a function accessed through a descriptor."""
76
    return function.__get__(None, cls).__name__
77
78
79
# This is mostly fine, though the list of classes is somewhat ad-hoc, to say
80
# the least.
81
def _cant_set_new_functions(cls: type, *functions) -> typing.Optional[str]:
82
    for function in functions:
83
        name = _name(cls, function)
84
        existing = getattr(cls, name, None)
85
        if existing not in (
86
            getattr(object, name, None),
87
            getattr(Product, name, None),
88
            None,
89
            function,
90
        ):
91
            return name
92
    return None
93
94
95
MISSING = object()
96
97
98
def cant_modify(self, name):
99
    """Prevent attempts to modify an attr of the given name."""
100
    class_repr = repr(self.__class__.__name__)
101
    name_repr = repr(name)
102
    if inspect.getattr_static(self, name, MISSING) is MISSING:
103
        format_msg = "{class_repr} object has no attribute {name_repr}"
104
    else:
105
        format_msg = "{class_repr} object attribute {name_repr} is read-only"
106
    raise AttributeError(format_msg.format(class_repr=class_repr, name_repr=name_repr))
107
108
109
def _set_new_functions(cls: type, *functions) -> typing.Optional[str]:
110
    """Attempt to set the attributes corresponding to the functions on cls.
111
112
    If any attributes are already defined, fail *before* setting any, and
113
    return the already-defined name.
114
    """
115
    cant_set = _cant_set_new_functions(cls, *functions)
116
    if cant_set:
117
        return cant_set
118
    for function in functions:
119
        setattr(cls, _name(cls, function), function)
120
    return None
121
122
123
def _sum_new(_cls: typing.Type[_T], subclasses):
124
    def base(cls: typing.Type[_T], args):
125
        return super(_cls, cls).__new__(cls, args)  # type: ignore
126
127
    new = vars(_cls).get("__new__", staticmethod(base))
128
129
    def __new__(cls: typing.Type[_T], args):
130
        if cls not in subclasses:
131
            raise TypeError
132
        return new.__get__(None, cls)(cls, args)
133
134
    _cls.__new__ = staticmethod(__new__)  # type: ignore
135
136
137
def _product_new(_cls: typing.Type[_T], _signature):
138
    if "__new__" in vars(_cls):
139
        original_new = _cls.__new__
140
141
        def __new__(*args, **kwargs):
142
            cls, *args = args
143
            if cls is _cls:
144
                return original_new(cls, *args, **kwargs)
145
            return super(_cls, cls).__new__(cls, *args, **kwargs)
146
147
        signature = inspect.signature(original_new)
148
    else:
149
150
        def __new__(*args, **kwargs):
151
            cls, *args = args
152
            return super(_cls, cls).__new__(cls, *args, **kwargs)
153
154
        signature = _signature.replace(
155
            parameters=[inspect.Parameter("cls", inspect.Parameter.POSITIONAL_ONLY)]
156
            + list(_signature.parameters.values())
157
        )
158
    __new__.__signature__ = signature  # type: ignore
159
    _cls.__new__ = __new__  # type: ignore
160
161
162
def _ordering_options_are_valid(
163
    *, eq: bool, order: bool  # pylint: disable=invalid-name
164
):
165
    if order and not eq:
166
        raise ValueError("eq must be true if order is true")
167
168
169
def _can_set_ordering(*, can_set: bool):
170
    if not can_set:
171
        raise ValueError("Can't add ordering methods if equality methods are provided.")
172
173
174
def _set_ordering(*, setter, cls: type, source: type):
175
    collision = setter(
176
        cls, source.__lt__, source.__le__, source.__gt__, source.__ge__  # type: ignore
177
    )
178
    if collision:
179
        raise TypeError(
180
            "Cannot overwrite attribute {collision} in class "
181
            "{name}. Consider using functools.total_ordering".format(
182
                collision=collision, name=cls.__name__
183
            )
184
        )
185
186
187
class Sum:
188
    """Base class of classes with disjoint constructors.
189
190
    Examines PEP 526 __annotations__ to determine subclasses.
191
192
    If repr is true, a __repr__() method is added to the class.
193
    If order is true, rich comparison dunder methods are added.
194
195
    The Sum class examines the class to find Ctor annotations.
196
    A Ctor annotation is the adt.Ctor class itself, or the result of indexing
197
    the class, either with a single type hint, or a tuple of type hints.
198
    All other annotations are ignored.
199
200
    The subclass is not subclassable, but has subclasses at each of the
201
    names that had Ctor annotations. Each subclass takes a fixed number of
202
    arguments, corresponding to the type hints given to its annotation, if any.
203
    """
204
205
    __slots__ = ()
206
207
    def __new__(*args, **kwargs):  # pylint: disable=no-method-argument
208
        cls, *args = args
209
        if not issubclass(cls, _adt_constructor.ADTConstructor):
210
            raise TypeError
211
        return super(Sum, cls).__new__(cls, *args, **kwargs)
212
213
    # Both of these are for consistency with modules defined in the stdlib.
214
    # BOOM!
215
    def __init_subclass__(
216
        cls,
217
        *,
218
        repr: bool = True,  # pylint: disable=redefined-builtin
219
        eq: bool = True,  # pylint: disable=invalid-name
220
        order: bool = False,
221
        **kwargs,
222
    ):
223
        super().__init_subclass__(**kwargs)  # type: ignore
224
        if issubclass(cls, _adt_constructor.ADTConstructor):
225
            return
226
        _ordering_options_are_valid(eq=eq, order=order)
227
228
        _prewritten_methods.SUBCLASS_ORDER[cls] = _adt_constructor.make_constructors(
229
            cls
230
        )
231
232
        source = _prewritten_methods.PrewrittenSumMethods
233
234
        cls.__init_subclass__ = source.__init_subclass__  # type: ignore
235
236
        _sum_new(cls, frozenset(_prewritten_methods.SUBCLASS_ORDER[cls]))
237
238
        if repr:
239
            _set_new_functions(cls, source.__repr__)
240
241
        equality_methods_were_set = eq and not _set_new_functions(
242
            cls, source.__eq__, source.__ne__
243
        )
244
245
        if equality_methods_were_set:
246
            cls.__hash__ = source.__hash__  # type: ignore
247
248
        if order:
249
            _can_set_ordering(can_set=equality_methods_were_set)
250
            _set_ordering(setter=_set_new_functions, cls=cls, source=source)
251
252
    def __bool__(self):
253
        return True
254
255
    def __setattr__(self, name, value):
256
        if not inspect.isdatadescriptor(inspect.getattr_static(self, name, MISSING)):
257
            cant_modify(self, name)
258
        super().__setattr__(name, value)
259
260
    def __delattr__(self, name):
261
        if not inspect.isdatadescriptor(inspect.getattr_static(self, name, MISSING)):
262
            cant_modify(self, name)
263
        super().__delattr__(name)
264
265
266
class Product(_adt_constructor.ADTConstructor, tuple):
267
    """Base class of classes with typed fields.
268
269
    Examines PEP 526 __annotations__ to determine fields.
270
271
    If repr is true, a __repr__() method is added to the class.
272
    If order is true, rich comparison dunder methods are added.
273
274
    The Product class examines the class to find annotations.
275
    Annotations with a value of "None" are discarded.
276
    Fields may have default values, and can be set to inspect.empty to
277
    indicate "no default".
278
279
    The subclass is subclassable. The implementation was designed with a focus
280
    on flexibility over ideals of purity, and therefore provides various
281
    optional facilities that conflict with, for example, Liskov
282
    substitutability. For the purposes of matching, each class is considered
283
    distinct.
284
    """
285
286
    __slots__ = ()
287
288
    def __new__(*args, **kwargs):  # pylint: disable=no-method-argument
289
        cls, *args = args
290
        if cls is Product:
291
            raise TypeError
292
        # Probably a result of not having positional-only args.
293
        bound_arguments = cls.__signature.bind(
294
            *args, **kwargs
295
        )  # pylint: disable=protected-access
296
        bound_arguments.apply_defaults()
297
        return super(Product, cls).__new__(cls, bound_arguments.arguments.values())
298
299
    __repr: typing.ClassVar[bool] = True
300
    __eq: typing.ClassVar[bool] = True
301
    __order: typing.ClassVar[bool] = False
302
    __eq_succeeded = None
303
304
    # Both of these are for consistency with modules defined in the stdlib.
305
    # BOOM!
306
    def __init_subclass__(
307
        cls,
308
        *,
309
        repr: typing.Optional[bool] = None,  # pylint: disable=redefined-builtin
310
        eq: typing.Optional[bool] = None,  # pylint: disable=invalid-name
311
        order: typing.Optional[bool] = None,
312
        **kwargs,
313
    ):
314
        super().__init_subclass__(**kwargs)  # type: ignore
315
316
        if repr is not None:
317
            cls.__repr = repr
318
        if eq is not None:
319
            cls.__eq = eq
320
        if order is not None:
321
            cls.__order = order
322
323
        _ordering_options_are_valid(eq=cls.__eq, order=cls.__order)
324
325
        annotations = _annotations.product_args_from_annotations(cls)
326
        params = [
327
            inspect.Parameter(
328
                name,
329
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
330
                default=getattr(cls, name, inspect.Parameter.empty),
331
                annotation=annotation,
332
            )
333
            for (name, annotation) in annotations.items()
334
        ]
335
        try:
336
            cls.__signature = inspect.Signature(parameters=params, return_annotation=cls)
337
        except ValueError:
338
            raise TypeError
339
        cls.__fields = {field: index for (index, field) in enumerate(annotations)}
340
341
        _product_new(cls, cls.__signature)
342
343
        source = _prewritten_methods.PrewrittenProductMethods
344
345
        cls.__eq_succeeded = cls.__eq and not _cant_set_new_functions(
346
            cls, source.__eq__, source.__ne__
347
        )
348
349
        if cls.__order:
350
            _can_set_ordering(can_set=cls.__eq_succeeded)
351
            _set_ordering(setter=_cant_set_new_functions, cls=cls, source=source)
352
353
    def __dir__(self):
354
        return super().__dir__() + list(self.__fields)
355
356
    def __getattribute__(self, name):
357
        index = object.__getattribute__(self, "_Product__fields").get(name)
358
        if index is None:
359
            return super().__getattribute__(name)
360
        return tuple.__getitem__(self, index)
361
362
    def __setattr__(self, name, value):
363
        if not inspect.isdatadescriptor(inspect.getattr_static(self, name, MISSING)):
364
            cant_modify(self, name)
365
        super().__setattr__(name, value)
366
367
    def __delattr__(self, name):
368
        if not inspect.isdatadescriptor(inspect.getattr_static(self, name, MISSING)):
369
            cant_modify(self, name)
370
        super().__delattr__(name)
371
372
    def __bool__(self):
373
        return True
374
375
    source = _prewritten_methods.PrewrittenProductMethods
376
377
    # pylint: disable=protected-access
378
    __repr__ = _conditional_method.conditional_method(source).__repr  # type: ignore
379
    __hash__ = _conditional_method.conditional_method(  # type: ignore
380
        source
381
    ).__eq_succeeded
382
    __eq__ = _conditional_method.conditional_method(  # type: ignore
383
        source
384
    ).__eq_succeeded
385
    __ne__ = _conditional_method.conditional_method(  # type: ignore
386
        source
387
    ).__eq_succeeded
388
    __lt__ = _conditional_method.conditional_method(source).__order  # type: ignore
389
    __le__ = _conditional_method.conditional_method(source).__order  # type: ignore
390
    __gt__ = _conditional_method.conditional_method(source).__order  # type: ignore
391
    __ge__ = _conditional_method.conditional_method(source).__order  # type: ignore
392
393
    del source
394
395
396
__all__ = ["Ctor", "Product", "Sum"]
397