Passed
Push — master ( de34e0...cab643 )
by Max
56s
created

structured_data.adt._set_hash()   A

Complexity

Conditions 2

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nop 2
dl 0
loc 3
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.
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
- Decorate the class with the ``adt`` function. Optionally, pass keyword-only
13
  arguments to control the generated functions.
14
15
To look inside an ADT instance, use the functions from the
16
:mod:`structured_data.match` module.
17
18
Putting it together:
19
20
>>> from structured_data import match
21
>>> class Example(Sum):
22
...     FirstConstructor: Ctor[int, str]
23
...     SecondConstructor: Ctor[bytes]
24
...     ThirdConstructor: Ctor
25
...     def __iter__(self):
26
...         matchable = match.Matchable(self)
27
...         if matchable(Example.FirstConstructor(match.pat.count, match.pat.string)):
28
...             count, string = matchable[match.pat.count, match.pat.string]
29
...             for _ in range(count):
30
...                 yield string
31
...         elif matchable(Example.SecondConstructor(match.pat.bytes)):
32
...             bytes_ = matchable[match.pat.bytes]
33
...             for byte in bytes_:
34
...                 yield chr(byte)
35
...         elif matchable(Example.ThirdConstructor()):
36
...             yield "Third"
37
...             yield "Constructor"
38
>>> list(Example.FirstConstructor(5, "abc"))
39
['abc', 'abc', 'abc', 'abc', 'abc']
40
>>> list(Example.SecondConstructor(b"abc"))
41
['a', 'b', 'c']
42
>>> list(Example.ThirdConstructor())
43
['Third', 'Constructor']
44
"""
45
46
import sys
47
import typing
48
49
from ._adt_constructor import ADTConstructor
50
from ._adt_constructor import make_constructor
51
from ._ctor import get_args
52
from ._prewritten_methods import SUBCLASS_ORDER
53
from ._prewritten_methods import PrewrittenMethods
54
55
_T = typing.TypeVar("_T")
56
57
58
if typing.TYPE_CHECKING:  # pragma: nocover
59
60
    class Ctor:
61
        """Dummy class for type-checking purposes."""
62
63
    class ConcreteCtor(typing.Generic[_T]):
64
        """Wrapper class for type-checking purposes.
65
66
        The type parameter should be a Tuple type of fixed size.
67
        Classes containing this annotation (meaning they haven't been
68
        processed by the ``adt`` decorator) should not be instantiated.
69
        """
70
71
72
else:
73
    from ._ctor import Ctor
74
75
76
def _name(cls: typing.Type[_T], function) -> str:
77
    """Return the name of a function accessed through a descriptor."""
78
    return function.__get__(None, cls).__name__
79
80
81
def _set_new_functions(cls: typing.Type[_T], *functions) -> typing.Optional[str]:
82
    """Attempt to set the attributes corresponding to the functions on cls.
83
84
    If any attributes are already defined, fail *before* setting any, and
85
    return the already-defined name.
86
    """
87
    for function in functions:
88
        name = _name(cls, function)
89
        if getattr(object, name, None) is not getattr(cls, name, None):
90
            return name
91
    for function in functions:
92
        setattr(cls, _name(cls, function), function)
93
    return None
94
95
96
def _adt_super(_cls: typing.Type[_T]):
97
    def base(cls, args):
98
        return super(_cls, cls).__new__(cls, args)
99
100
    return staticmethod(base)
101
102
103
def _make_nested_new(_cls: typing.Type[_T], subclasses, base__new__):
104
    def __new__(cls, args):
105
        if cls not in subclasses:
106
            raise TypeError
107
        return base__new__.__get__(None, cls)(cls, args)
108
109
    return staticmethod(__new__)
110
111
112
_K = typing.TypeVar("_K")
113
_V = typing.TypeVar("_V")
114
115
116
def _nillable_write(dct: typing.Dict[_K, _V], key: _K, value: typing.Optional[_V]):
117
    if value is None:
118
        dct.pop(key, typing.cast(_V, None))
119
    else:
120
        dct[key] = value
121
122
123
def _add_methods(cls: typing.Type[_T], do_set, *methods):
124
    methods_were_set = False
125
    if do_set:
126
        methods_were_set = not _set_new_functions(cls, *methods)
127
    return methods_were_set
128
129
130
def _set_hash(cls: typing.Type[_T], set_hash):
131
    if set_hash:
132
        cls.__hash__ = PrewrittenMethods.__hash__  # type: ignore
133
134
135
def _add_order(cls: typing.Type[_T], set_order, equality_methods_were_set):
136
    if set_order:
137
        if not equality_methods_were_set:
138
            raise ValueError(
139
                "Can't add ordering methods if equality methods are provided."
140
            )
141
        collision = _set_new_functions(
142
            cls,
143
            PrewrittenMethods.__lt__,
144
            PrewrittenMethods.__le__,
145
            PrewrittenMethods.__gt__,
146
            PrewrittenMethods.__ge__,
147
        )
148
        if collision:
149
            raise TypeError(
150
                "Cannot overwrite attribute {collision} in class "
151
                "{name}. Consider using functools.total_ordering".format(
152
                    collision=collision, name=cls.__name__
153
                )
154
            )
155
156
157
def _custom_new(cls: typing.Type[_T], subclasses):
158
    new = cls.__dict__.get("__new__", _adt_super(cls))
159
    cls.__new__ = _make_nested_new(cls, subclasses, new)  # type: ignore
160
161
162
def _args_from_annotations(cls: typing.Type[_T]) -> typing.Dict[str, typing.Tuple]:
163
    args: typing.Dict[str, typing.Tuple] = {}
164
    for superclass in reversed(cls.__mro__):
165
        for key, value in getattr(superclass, "__annotations__", {}).items():
166
            _nillable_write(
167
                args, key, get_args(value, vars(sys.modules[superclass.__module__]))
168
            )
169
    return args
170
171
172
def _process_class(_cls: typing.Type[_T], _repr, eq, order) -> typing.Type[_T]:
173
    if order and not eq:
174
        raise ValueError("eq must be true if order is true")
175
176
    subclass_order: typing.List[typing.Type[_T]] = []
177
178
    for name, args in _args_from_annotations(_cls).items():
179
        make_constructor(_cls, name, args, subclass_order)
180
181
    SUBCLASS_ORDER[_cls] = tuple(subclass_order)
182
183
    _cls.__init_subclass__ = PrewrittenMethods.__init_subclass__  # type: ignore
184
185
    _custom_new(_cls, frozenset(subclass_order))
186
187
    _set_new_functions(
188
        _cls, PrewrittenMethods.__setattr__, PrewrittenMethods.__delattr__
189
    )
190
    _set_new_functions(_cls, PrewrittenMethods.__bool__)
191
192
    _add_methods(_cls, _repr, PrewrittenMethods.__repr__)
193
194
    equality_methods_were_set = _add_methods(
195
        _cls, eq, PrewrittenMethods.__eq__, PrewrittenMethods.__ne__
196
    )
197
198
    _set_hash(_cls, equality_methods_were_set)
199
200
    _add_order(_cls, order, equality_methods_were_set)
201
202
    return _cls
203
204
205
class Sum:
206
    """Base class of classes with disjoint constructors.
207
208
    Examines PEP 526 __annotations__ to determine subclasses.
209
210
    If repr is true, a __repr__() method is added to the class.
211
    If order is true, rich comparison dunder methods are added.
212
213
    The Sum class examines the class to find Ctor annotations.
214
    A Ctor annotation is the adt.Ctor class itself, or the result of indexing
215
    the class, either with a single type hint, or a tuple of type hints.
216
    All other annotations are ignored.
217
218
    The subclass is not subclassable, but has subclasses at each of the
219
    names that had Ctor annotations. Each subclass takes a fixed number of
220
    arguments, corresponding to the type hints given to its annotation, if any.
221
    """
222
223
    __slots__ = ()
224
225
    def __init_subclass__(cls, **kwargs):
226
        if not issubclass(cls, ADTConstructor):
227
            repr_ = kwargs.pop("repr", True)
228
            eq = kwargs.pop("eq", True)
229
            order = kwargs.pop("order", False)
230
        super().__init_subclass__(**kwargs)
231
        if not issubclass(cls, ADTConstructor):
232
            _process_class(cls, repr_, eq, order)
0 ignored issues
show
introduced by
The variable eq does not seem to be defined in case BooleanNotNode on line 226 is False. Are you sure this can never be the case?
Loading history...
introduced by
The variable repr_ does not seem to be defined in case BooleanNotNode on line 226 is False. Are you sure this can never be the case?
Loading history...
introduced by
The variable order does not seem to be defined in case BooleanNotNode on line 226 is False. Are you sure this can never be the case?
Loading history...
233
234
235
__all__ = ["Ctor", "Sum"]
236