Passed
Push — master ( 601bca...fcc3bd )
by Max
57s
created

structured_data.adt._sum_super()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
"""Class decorator for defining abstract data types.
2
3
This module provides two 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 sys
45
import typing
46
47
from ._adt_constructor import ADTConstructor
48
from ._adt_constructor import make_constructor
49
from ._ctor import get_args
50
from ._prewritten_methods import SUBCLASS_ORDER
51
from ._prewritten_methods import PrewrittenMethods
52
53
_T = typing.TypeVar("_T")
54
55
56
if typing.TYPE_CHECKING:  # pragma: nocover
57
58
    class Ctor:
59
        """Dummy class for type-checking purposes."""
60
61
    class ConcreteCtor(typing.Generic[_T]):
62
        """Wrapper class for type-checking purposes.
63
64
        The type parameter should be a Tuple type of fixed size.
65
        Classes containing this annotation (meaning they haven't been
66
        processed by the ``adt`` decorator) should not be instantiated.
67
        """
68
69
70
else:
71
    from ._ctor import Ctor
72
73
74
def _name(cls: typing.Type[_T], function) -> str:
75
    """Return the name of a function accessed through a descriptor."""
76
    return function.__get__(None, cls).__name__
77
78
79
def _set_new_functions(cls: typing.Type[_T], *functions) -> typing.Optional[str]:
80
    """Attempt to set the attributes corresponding to the functions on cls.
81
82
    If any attributes are already defined, fail *before* setting any, and
83
    return the already-defined name.
84
    """
85
    for function in functions:
86
        name = _name(cls, function)
87
        if getattr(object, name, None) is not getattr(cls, name, None):
88
            return name
89
    for function in functions:
90
        setattr(cls, _name(cls, function), function)
91
    return None
92
93
94
def _sum_super(_cls: typing.Type[_T]):
95
    def base(cls, args):
96
        return super(_cls, cls).__new__(cls, args)
97
98
    return staticmethod(base)
99
100
101
def _make_nested_new(_cls: typing.Type[_T], subclasses, base__new__):
102
    def __new__(cls, args):
103
        if cls not in subclasses:
104
            raise TypeError
105
        return base__new__.__get__(None, cls)(cls, args)
106
107
    return staticmethod(__new__)
108
109
110
_K = typing.TypeVar("_K")
111
_V = typing.TypeVar("_V")
112
113
114
def _nillable_write(dct: typing.Dict[_K, _V], key: _K, value: typing.Optional[_V]):
115
    if value is None:
116
        dct.pop(key, typing.cast(_V, None))
117
    else:
118
        dct[key] = value
119
120
121
def _add_methods(cls: typing.Type[_T], do_set, *methods):
122
    methods_were_set = False
123
    if do_set:
124
        methods_were_set = not _set_new_functions(cls, *methods)
125
    return methods_were_set
126
127
128
def _set_hash(cls: typing.Type[_T], set_hash):
129
    if set_hash:
130
        cls.__hash__ = PrewrittenMethods.__hash__  # type: ignore
131
132
133
def _add_order(cls: typing.Type[_T], set_order, equality_methods_were_set):
134
    if set_order:
135
        if not equality_methods_were_set:
136
            raise ValueError(
137
                "Can't add ordering methods if equality methods are provided."
138
            )
139
        collision = _set_new_functions(
140
            cls,
141
            PrewrittenMethods.__lt__,
142
            PrewrittenMethods.__le__,
143
            PrewrittenMethods.__gt__,
144
            PrewrittenMethods.__ge__,
145
        )
146
        if collision:
147
            raise TypeError(
148
                "Cannot overwrite attribute {collision} in class "
149
                "{name}. Consider using functools.total_ordering".format(
150
                    collision=collision, name=cls.__name__
151
                )
152
            )
153
154
155
def _custom_new(cls: typing.Type[_T], subclasses):
156
    new = cls.__dict__.get("__new__", _sum_super(cls))
157
    cls.__new__ = _make_nested_new(cls, subclasses, new)  # type: ignore
158
159
160
def _args_from_annotations(cls: typing.Type[_T]) -> typing.Dict[str, typing.Tuple]:
161
    args: typing.Dict[str, typing.Tuple] = {}
162
    for superclass in reversed(cls.__mro__):
163
        for key, value in getattr(superclass, "__annotations__", {}).items():
164
            _nillable_write(
165
                args, key, get_args(value, vars(sys.modules[superclass.__module__]))
166
            )
167
    return args
168
169
170
def _process_class(_cls: typing.Type[_T], _repr, eq, order) -> typing.Type[_T]:
171
    if order and not eq:
172
        raise ValueError("eq must be true if order is true")
173
174
    subclass_order: typing.List[typing.Type[_T]] = []
175
176
    for name, args in _args_from_annotations(_cls).items():
177
        make_constructor(_cls, name, args, subclass_order)
178
179
    SUBCLASS_ORDER[_cls] = tuple(subclass_order)
180
181
    _cls.__init_subclass__ = PrewrittenMethods.__init_subclass__  # type: ignore
182
183
    _custom_new(_cls, frozenset(subclass_order))
184
185
    _set_new_functions(
186
        _cls, PrewrittenMethods.__setattr__, PrewrittenMethods.__delattr__
187
    )
188
    _set_new_functions(_cls, PrewrittenMethods.__bool__)
189
190
    _add_methods(_cls, _repr, PrewrittenMethods.__repr__)
191
192
    equality_methods_were_set = _add_methods(
193
        _cls, eq, PrewrittenMethods.__eq__, PrewrittenMethods.__ne__
194
    )
195
196
    _set_hash(_cls, equality_methods_were_set)
197
198
    _add_order(_cls, order, equality_methods_were_set)
199
200
    return _cls
201
202
203
class Sum:
204
    """Base class of classes with disjoint constructors.
205
206
    Examines PEP 526 __annotations__ to determine subclasses.
207
208
    If repr is true, a __repr__() method is added to the class.
209
    If order is true, rich comparison dunder methods are added.
210
211
    The Sum class examines the class to find Ctor annotations.
212
    A Ctor annotation is the adt.Ctor class itself, or the result of indexing
213
    the class, either with a single type hint, or a tuple of type hints.
214
    All other annotations are ignored.
215
216
    The subclass is not subclassable, but has subclasses at each of the
217
    names that had Ctor annotations. Each subclass takes a fixed number of
218
    arguments, corresponding to the type hints given to its annotation, if any.
219
    """
220
221
    __slots__ = ()
222
223
    def __init_subclass__(cls, *, repr=True, eq=True, order=False, **kwargs):
224
        super().__init_subclass__(**kwargs)
225
        if not issubclass(cls, ADTConstructor):
226
            _process_class(cls, repr, eq, order)
227
228
229
__all__ = ["Ctor", "Sum"]
230