Skip to content

Commit 27f1c91

Browse files
authored
Inherit slots instead of redefining them (#3105)
1 parent ded5f4c commit 27f1c91

File tree

3 files changed

+100
-66
lines changed

3 files changed

+100
-66
lines changed

ibis/expr/operations/core.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import collections
22
import functools
33
import itertools
4-
from typing import Any as AnyT
5-
from typing import Dict
64

75
import toolz
86
from public import public
@@ -95,37 +93,6 @@ def _pp(x):
9593

9694
return '{}({})'.format(opname, ', '.join(pprint_args))
9795

98-
def __getstate__(self) -> Dict[str, AnyT]:
99-
"""The attributes _expr_cached and _hash are
100-
used as caches; they can be excluded from
101-
serialization without affecting correctness.
102-
103-
Excluding _expr_cached and _hash from serialization
104-
will allow the serialized bytes to be the same for
105-
equivalent Node objets.
106-
107-
Returns
108-
-------
109-
Dict[str, Any]
110-
A dictionary storing the objects attributes.
111-
"""
112-
excluded_slots = {'_expr_cached', '_hash'}
113-
return {
114-
slot: getattr(self, slot)
115-
for slot in self.__slots__
116-
if slot not in excluded_slots
117-
}
118-
119-
def __setstate__(self, state: Dict[str, AnyT]) -> None:
120-
"""
121-
Parameters
122-
----------
123-
state: Dict[str, Any]
124-
A dictionary storing the objects attributes.
125-
"""
126-
for slot in state:
127-
setattr(self, slot, state[slot])
128-
12996
@property
13097
def inputs(self):
13198
return tuple(self.args)

ibis/expr/signature.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import copy
22
import inspect
3-
import typing
3+
from typing import Any, Callable, Dict
44

55
import ibis.expr.rules as rlz
66
from ibis import util
77

8-
try:
9-
from cytoolz import unique
10-
except ImportError:
11-
from toolz import unique
12-
13-
148
EMPTY = inspect.Parameter.empty # marker for missing argument
159

1610

17-
class Validator(typing.Callable):
11+
class Validator(Callable):
1812
pass
1913

2014

@@ -112,37 +106,30 @@ def Argument(validator, default=EMPTY):
112106

113107
class AnnotableMeta(type):
114108
def __new__(metacls, clsname, bases, dct):
115-
slots, params = [], {}
116-
109+
params = {}
117110
for parent in bases:
118-
# inherit parent slots
119-
if hasattr(parent, '__slots__'):
120-
slots += parent.__slots__
121111
# inherit from parent signatures
122112
if hasattr(parent, '__signature__'):
123113
params.update(parent.__signature__.parameters)
124114

115+
slots = list(dct.pop('__slots__', []))
125116
attribs = {}
126117
for name, attrib in dct.items():
127118
if isinstance(attrib, Validator):
128119
# so we can set directly
129120
params[name] = Parameter(name, validator=attrib)
121+
slots.append(name)
130122
else:
131123
attribs[name] = attrib
132124

133-
# if slots or signature are defined no inheritance happens
134-
slots = attribs.get('__slots__', tuple(slots))
135-
slots += tuple(params.keys())
136-
137125
# mandatory fields without default values must preceed the optional
138126
# ones in the function signature, the partial ordering will be kept
139127
params = sorted(
140128
params.values(), key=lambda p: p.default is EMPTY, reverse=True
141129
)
142-
signature = attribs.get('__signature__', inspect.Signature(params))
143130

144-
attribs['__slots__'] = tuple(unique(slots))
145-
attribs['__signature__'] = signature
131+
attribs['__slots__'] = tuple(slots)
132+
attribs['__signature__'] = inspect.Signature(params)
146133

147134
return super().__new__(metacls, clsname, bases, attribs)
148135

@@ -162,11 +149,27 @@ def __init__(self, *args, **kwargs):
162149
def _validate(self):
163150
pass
164151

165-
def __eq__(self, other):
152+
def __eq__(self, other) -> bool:
166153
if self is other:
167154
return True
168155
return type(self) == type(other) and self.args == other.args
169156

157+
def __getstate__(self) -> Dict[str, Any]:
158+
return {
159+
key: getattr(self, key)
160+
for key in self.__signature__.parameters.keys()
161+
}
162+
163+
def __setstate__(self, state: Dict[str, Any]) -> None:
164+
"""
165+
Parameters
166+
----------
167+
state: Dict[str, Any]
168+
A dictionary storing the objects attributes.
169+
"""
170+
for key, value in state.items():
171+
setattr(self, key, value)
172+
170173
@property
171174
def argnames(self):
172175
return tuple(self.__signature__.parameters.keys())

ibis/tests/expr/test_signature.py

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pickle
12
from inspect import Signature
23

34
import pytest
@@ -28,6 +29,24 @@ def __call__(self, arg, **kwargs):
2829
return arg
2930

3031

32+
class Op(Annotable):
33+
__slots__ = ('_cache', '_hash')
34+
35+
36+
class ValueOp(Op):
37+
arg = Argument(object)
38+
39+
40+
class StringOp(ValueOp):
41+
arg = Argument(str)
42+
43+
44+
class MagicString(StringOp):
45+
foo = Argument(str)
46+
bar = Argument(bool)
47+
baz = Argument(int)
48+
49+
3150
@pytest.mark.parametrize('validator', [3, 'coerce'])
3251
def test_invalid_validator(validator):
3352
with pytest.raises(TypeError):
@@ -198,32 +217,77 @@ class NoHooves(Farm):
198217
assert g1.goats == 0
199218

200219

220+
def test_copy_default():
221+
default = []
222+
223+
class Op(Annotable):
224+
arg = rlz.optional(rlz.instance_of(list), default=default)
225+
226+
op = Op()
227+
assert op.arg is not default
228+
229+
201230
def test_slots_are_inherited_and_overridable():
202231
class Op(Annotable):
203232
__slots__ = ('_cache',) # first definition
204233
arg = Argument(lambda x: x)
205234

206235
class StringOp(Op):
207-
arg = Argument(str) # inherit
236+
arg = Argument(str) # new overridden slot
208237

209238
class StringSplit(StringOp):
210-
sep = Argument(str) # inherit
239+
sep = Argument(str) # new slot
211240

212241
class StringJoin(StringOp):
213-
__slots__ = ('_memoize',) # override
214-
sep = Argument(str)
242+
__slots__ = ('_memoize',) # new slot
243+
sep = Argument(str) # new overridden slot
215244

216245
assert Op.__slots__ == ('_cache', 'arg')
217-
assert StringOp.__slots__ == ('_cache', 'arg')
218-
assert StringSplit.__slots__ == ('_cache', 'arg', 'sep')
219-
assert StringJoin.__slots__ == ('_memoize', 'arg', 'sep')
246+
assert StringOp.__slots__ == ('arg',)
247+
assert StringSplit.__slots__ == ('sep',)
248+
assert StringJoin.__slots__ == ('_memoize', 'sep')
220249

221250

222-
def test_copy_default():
223-
default = []
251+
def test_multiple_inheritance():
252+
# multiple inheritance is allowed only if one of the parents has non-empty
253+
# __slots__ definition, otherwise python will raise lay-out conflict
224254

225255
class Op(Annotable):
226-
arg = rlz.optional(rlz.instance_of(list), default=default)
256+
__slots__ = ('_hash',)
227257

228-
op = Op()
229-
assert op.arg is not default
258+
class ValueOp(Annotable):
259+
arg = Argument(object)
260+
261+
class Reduction(ValueOp):
262+
_reduction = True
263+
264+
class UDF(ValueOp):
265+
func = Argument(lambda fn, this: fn)
266+
267+
class UDAF(UDF, Reduction):
268+
arity = Argument(int)
269+
270+
class A(Annotable):
271+
a = Argument(int)
272+
273+
class B(Annotable):
274+
b = Argument(int)
275+
276+
msg = "multiple bases have instance lay-out conflict"
277+
with pytest.raises(TypeError, match=msg):
278+
279+
class AB(A, B):
280+
ab = Argument(int)
281+
282+
assert UDAF.__slots__ == ('arity',)
283+
strlen = UDAF(arg=2, func=lambda value: len(str(value)), arity=1)
284+
assert strlen.arg == 2
285+
assert strlen.arity == 1
286+
assert strlen._reduction is True
287+
288+
289+
def test_pickling_support():
290+
op = MagicString(arg="something", foo="magic", bar=True, baz=8)
291+
raw = pickle.dumps(op)
292+
loaded = pickle.loads(raw)
293+
assert op == loaded

0 commit comments

Comments
 (0)