retracesoftware-proxy 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- retracesoftware/config.json +7 -0
- retracesoftware/install/install.py +2 -26
- retracesoftware/install/patcher.py +52 -9
- retracesoftware/install/record.py +15 -40
- retracesoftware/install/replay.py +4 -101
- retracesoftware/install/tracer.py +18 -16
- retracesoftware/proxy/__init__.py +1 -1
- retracesoftware/proxy/gateway.py +25 -76
- retracesoftware/proxy/proxyfactory.py +194 -194
- retracesoftware/proxy/proxysystem.py +192 -10
- retracesoftware/proxy/proxytype.py +235 -96
- retracesoftware/proxy/record.py +184 -96
- retracesoftware/proxy/replay.py +184 -60
- retracesoftware/proxy/stubfactory.py +141 -0
- retracesoftware/proxy/thread.py +40 -5
- {retracesoftware_proxy-0.1.0.dist-info → retracesoftware_proxy-0.1.2.dist-info}/METADATA +1 -6
- retracesoftware_proxy-0.1.2.dist-info/RECORD +27 -0
- retracesoftware_proxy-0.1.0.dist-info/RECORD +0 -26
- {retracesoftware_proxy-0.1.0.dist-info → retracesoftware_proxy-0.1.2.dist-info}/WHEEL +0 -0
- {retracesoftware_proxy-0.1.0.dist-info → retracesoftware_proxy-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import retracesoftware.utils as utils
|
|
2
2
|
import retracesoftware.functional as functional
|
|
3
3
|
|
|
4
4
|
import types
|
|
5
5
|
|
|
6
|
+
from retracesoftware.proxy.stubfactory import StubMethodDescriptor
|
|
7
|
+
|
|
6
8
|
class Proxy:
|
|
7
9
|
__slots__ = []
|
|
8
10
|
|
|
@@ -18,6 +20,44 @@ class ExtendingProxy(Proxy):
|
|
|
18
20
|
class InternalProxy:
|
|
19
21
|
__slots__ = []
|
|
20
22
|
|
|
23
|
+
@functional.memoize_one_arg
|
|
24
|
+
def unproxy_type(cls):
|
|
25
|
+
if not issubclass(cls, Proxy):
|
|
26
|
+
return cls
|
|
27
|
+
|
|
28
|
+
# Prefer attribute on the class, not on the mappingproxy
|
|
29
|
+
target = getattr(cls, '__retrace_target_class__', None)
|
|
30
|
+
if target is not None:
|
|
31
|
+
return unproxy_type(target)
|
|
32
|
+
|
|
33
|
+
# Rebuild with unproxied bases, preserving metaclass and critical dunder attrs
|
|
34
|
+
new_bases = tuple(map(unproxy_type, cls.__bases__))
|
|
35
|
+
|
|
36
|
+
# Copy attrs, but omit implementation-managed ones
|
|
37
|
+
attrs = {}
|
|
38
|
+
classcell = None
|
|
39
|
+
for k, v in cls.__dict__.items():
|
|
40
|
+
if k in ('__dict__', '__weakref__'):
|
|
41
|
+
continue
|
|
42
|
+
if k == '__classcell__':
|
|
43
|
+
# must be passed through at class creation for zero-arg super()
|
|
44
|
+
classcell = v
|
|
45
|
+
continue
|
|
46
|
+
attrs[k] = v
|
|
47
|
+
|
|
48
|
+
# Ensure module/doc are kept (in case they weren't in __dict__)
|
|
49
|
+
attrs.setdefault('__module__', cls.__module__)
|
|
50
|
+
attrs.setdefault('__doc__', cls.__doc__)
|
|
51
|
+
|
|
52
|
+
# Use types.new_class if you want full class-creation protocol
|
|
53
|
+
def exec_body(ns):
|
|
54
|
+
ns.update(attrs)
|
|
55
|
+
if classcell is not None:
|
|
56
|
+
ns['__classcell__'] = classcell
|
|
57
|
+
|
|
58
|
+
return types.new_class(cls.__name__, new_bases, {'metaclass': type(cls)}, exec_body)
|
|
59
|
+
|
|
60
|
+
|
|
21
61
|
def superdict(cls):
|
|
22
62
|
result = {}
|
|
23
63
|
for cls in list(reversed(cls.__mro__))[1:]:
|
|
@@ -27,7 +67,9 @@ def superdict(cls):
|
|
|
27
67
|
|
|
28
68
|
def is_method_descriptor(obj):
|
|
29
69
|
return isinstance(obj, types.FunctionType) or \
|
|
30
|
-
(isinstance(obj, (types.WrapperDescriptorType,
|
|
70
|
+
(isinstance(obj, (types.WrapperDescriptorType,
|
|
71
|
+
types.MethodDescriptorType,
|
|
72
|
+
StubMethodDescriptor)) and obj.__objclass__ != object)
|
|
31
73
|
|
|
32
74
|
def proxy_method_descriptors(cls, handler):
|
|
33
75
|
for name, target in cls.__dict__.items():
|
|
@@ -86,7 +128,7 @@ def dynamic_stubtype(handler, cls):
|
|
|
86
128
|
|
|
87
129
|
assert not issubclass(cls, BaseException)
|
|
88
130
|
|
|
89
|
-
blacklist = ['__getattribute__', '__hash__', '__del__', '
|
|
131
|
+
blacklist = ['__getattribute__', '__hash__', '__del__', '__call__']
|
|
90
132
|
|
|
91
133
|
to_proxy = [m for m in methods(cls) if m not in blacklist]
|
|
92
134
|
|
|
@@ -102,86 +144,203 @@ def dynamic_stubtype(handler, cls):
|
|
|
102
144
|
spec['__call__'] = handler
|
|
103
145
|
|
|
104
146
|
spec['__retrace_target_class__'] = cls
|
|
105
|
-
spec['__class__'] = property(functional.repeatedly(cls))
|
|
106
147
|
|
|
107
|
-
|
|
148
|
+
target_type = functional.sequence(utils.unwrap, functional.typeof)
|
|
149
|
+
spec['__class__'] = property(target_type)
|
|
150
|
+
|
|
151
|
+
spec['__module__'] = cls.__module__
|
|
152
|
+
spec['__qualname__'] = cls.__name__
|
|
153
|
+
|
|
154
|
+
# name = f'retrace.proxied.{cls.__module__}.{cls.__name__}'
|
|
155
|
+
|
|
156
|
+
return type(cls.__name__, (Stub, DynamicProxy), spec)
|
|
157
|
+
|
|
158
|
+
class DescriptorProxy:
|
|
159
|
+
__slots__ = ['target', 'handler']
|
|
160
|
+
|
|
161
|
+
def __init__(self, handler, target):
|
|
162
|
+
self.handler = handler
|
|
163
|
+
self.target = target
|
|
164
|
+
|
|
165
|
+
def __get__(self, obj, cls):
|
|
166
|
+
return self.handler(self.target.__get__, obj, cls)
|
|
167
|
+
|
|
168
|
+
def __set__(self, obj, value):
|
|
169
|
+
return self.handler(self.target.__set__, obj, value)
|
|
170
|
+
|
|
171
|
+
def __delete__(self, obj):
|
|
172
|
+
return self.handler(self.target.__delete__, obj)
|
|
173
|
+
|
|
174
|
+
class ExtendingDescriptorProxy:
|
|
175
|
+
|
|
176
|
+
__slots__ = ['handler', 'proxytype', 'name']
|
|
177
|
+
|
|
178
|
+
def __init__(self, proxytype, handler, name):
|
|
179
|
+
self.proxytype = proxytype
|
|
180
|
+
self.handler = handler
|
|
181
|
+
self.name = name
|
|
182
|
+
|
|
183
|
+
def __get__(self, instance, owner):
|
|
184
|
+
inst = owner if instance is None else instance
|
|
185
|
+
getter = functional.partial(getattr, super(self.proxytype, inst))
|
|
186
|
+
return self.handler(getter, self.name)
|
|
187
|
+
|
|
188
|
+
def __set__(self, instance, value):
|
|
189
|
+
setter = functional.partial(setattr, super(self.proxytype, instance))
|
|
190
|
+
return self.handler(setter, self.name, value)
|
|
191
|
+
|
|
192
|
+
def __delete__(self, instance):
|
|
193
|
+
deleter = functional.partial(delattr, super(self.proxytype, instance))
|
|
194
|
+
return self.handler(deleter, self.name)
|
|
108
195
|
|
|
109
|
-
return type(name, (Stub, DynamicProxy), spec)
|
|
110
196
|
|
|
111
197
|
def dynamic_proxytype(handler, cls):
|
|
112
198
|
|
|
199
|
+
print(f'Creating dynamic_proxytype from {cls} {cls.__dict__}')
|
|
200
|
+
|
|
201
|
+
assert not issubclass(cls, Proxy)
|
|
113
202
|
assert not issubclass(cls, BaseException)
|
|
114
203
|
|
|
115
|
-
blacklist = ['__getattribute__', '__hash__', '__del__', '
|
|
204
|
+
blacklist = ['__getattribute__', '__hash__', '__del__', '__call__']
|
|
116
205
|
|
|
117
|
-
|
|
206
|
+
spec = {}
|
|
207
|
+
|
|
208
|
+
for name in superdict(cls).keys():
|
|
209
|
+
if name not in blacklist:
|
|
210
|
+
value = getattr(cls, name)
|
|
211
|
+
if is_descriptor(value):
|
|
212
|
+
print(f'Adding: {name} {value} {type(value).__mro__}')
|
|
118
213
|
|
|
119
|
-
|
|
214
|
+
if utils.is_method_descriptor(value):
|
|
215
|
+
print(f'Adding method descriptor: {name} {value}')
|
|
216
|
+
spec[name] = utils.wrapped_function(handler = handler, target = value)
|
|
217
|
+
else:
|
|
218
|
+
spec[name] = DescriptorProxy(handler = handler, target = value)
|
|
219
|
+
|
|
220
|
+
# to_proxy = [m for m in methods(cls) if m not in blacklist]
|
|
221
|
+
|
|
222
|
+
# def wrap(target): return utils.wrapped_function(handler = handler, target = target)
|
|
120
223
|
|
|
121
|
-
spec = { name: wrap(getattr(cls, name)) for name in to_proxy }
|
|
224
|
+
# spec = { name: wrap(getattr(cls, name)) for name in to_proxy }
|
|
122
225
|
|
|
123
|
-
spec['__getattr__'] = wrap(getattr)
|
|
124
|
-
spec['__setattr__'] = wrap(setattr)
|
|
226
|
+
# spec['__getattr__'] = wrap(getattr)
|
|
227
|
+
# spec['__setattr__'] = wrap(setattr)
|
|
125
228
|
|
|
126
229
|
if utils.yields_callable_instances(cls):
|
|
127
230
|
spec['__call__'] = handler
|
|
128
231
|
|
|
129
232
|
spec['__retrace_target_class__'] = cls
|
|
130
233
|
|
|
131
|
-
target_type = functional.
|
|
234
|
+
target_type = functional.sequence(utils.unwrap, functional.typeof)
|
|
132
235
|
spec['__class__'] = property(target_type)
|
|
133
|
-
|
|
236
|
+
|
|
134
237
|
name = f'retrace.proxied.{cls.__module__}.{cls.__name__}'
|
|
135
238
|
|
|
136
239
|
return type(name, (utils.Wrapped, DynamicProxy), spec)
|
|
137
240
|
|
|
241
|
+
def dynamic_from_extended(cls):
|
|
242
|
+
|
|
243
|
+
base = cls.__base__
|
|
244
|
+
|
|
245
|
+
name = f'retrace.proxied.{base.__module__}.{base.__name__}'
|
|
246
|
+
|
|
247
|
+
spec = dict(cls.__dict__)
|
|
248
|
+
|
|
249
|
+
spec['__retrace_target_class__'] = base
|
|
250
|
+
|
|
251
|
+
del spec['__init_subclass__']
|
|
252
|
+
# del spec['__new__']
|
|
253
|
+
|
|
254
|
+
target_type = functional.sequence(utils.unwrap, functional.typeof)
|
|
255
|
+
spec['__class__'] = property(target_type)
|
|
256
|
+
|
|
257
|
+
print(f'dynamic_from_extended {spec}')
|
|
258
|
+
|
|
259
|
+
return type(name, (utils.Wrapped, DynamicProxy), spec)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def instantiable_dynamic_proxytype(handler, cls, thread_state, create_stub = False):
|
|
263
|
+
|
|
264
|
+
proxytype = dynamic_proxytype(handler = handler, cls = cls)
|
|
265
|
+
|
|
266
|
+
def create_original(proxytype, *args, **kwargs):
|
|
267
|
+
instance = cls(*args, **kwargs)
|
|
268
|
+
instance.__init__(*args, **kwargs)
|
|
269
|
+
return instance
|
|
270
|
+
|
|
271
|
+
def __new__(proxytype, *args, **kwargs):
|
|
272
|
+
print(f'instantiable_dynamic_proxytype: {cls}')
|
|
273
|
+
|
|
274
|
+
instance = utils.create_stub_object(cls) if create_stub else cls(*args, **kwargs)
|
|
275
|
+
return utils.create_wrapped(proxytype, instance)
|
|
276
|
+
|
|
277
|
+
proxytype.__new__ = thread_state.dispatch(create_original, internal = __new__)
|
|
278
|
+
|
|
279
|
+
return proxytype
|
|
280
|
+
|
|
138
281
|
def dynamic_int_proxytype(handler, cls, bind):
|
|
139
282
|
proxytype = dynamic_proxytype(handler = handler, cls = cls)
|
|
140
283
|
proxytype.__new__ = functional.sequence(proxytype.__new__, functional.side_effect(bind))
|
|
141
284
|
return proxytype
|
|
142
285
|
|
|
143
|
-
class DescriptorProxy:
|
|
144
286
|
|
|
145
|
-
|
|
287
|
+
blacklist = ['__getattribute__', '__hash__', '__del__', '__dict__']
|
|
146
288
|
|
|
147
|
-
|
|
148
|
-
self.proxytype = proxytype
|
|
149
|
-
self.handler = handler
|
|
150
|
-
self.name = name
|
|
289
|
+
# if the type can be patched, thats better, all new instances must be of correct type
|
|
151
290
|
|
|
152
|
-
|
|
153
|
-
inst = owner if instance is None else instance
|
|
154
|
-
getter = functional.partial(getattr, super(self.proxytype, inst))
|
|
155
|
-
return self.handler(getter, self.name)
|
|
291
|
+
# def make_extensible(thread_state, proxy_method_descriptor, cls):
|
|
156
292
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
293
|
+
# @functional.memoize_one_arg
|
|
294
|
+
# def proxy_subclass(subclass):
|
|
295
|
+
# if '__retrace_target_type__' in subclass.__dict__:
|
|
296
|
+
# return subclass
|
|
297
|
+
|
|
298
|
+
# attrs = {k: proxy_method_descriptor(v) for k, v in subclass.__dict__.items() if is_method_descriptor(v) }
|
|
160
299
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
300
|
+
# # Ensure module/doc are kept (in case they weren't in __dict__)
|
|
301
|
+
# attrs.setdefault('__module__', subclass.__module__)
|
|
302
|
+
# attrs.setdefault('__doc__', subclass.__doc__)
|
|
303
|
+
# attrs['__retrace_target_type__'] = subclass
|
|
164
304
|
|
|
305
|
+
# return type(subclass.__name__, (subclass,), attrs)
|
|
165
306
|
|
|
166
|
-
|
|
307
|
+
# orig__new__ = cls.__new__
|
|
167
308
|
|
|
168
|
-
#
|
|
309
|
+
# def proxied__new__(subclass, *args, **kwargs):
|
|
310
|
+
# nonlocal orig__new__
|
|
311
|
+
# return orig__new__(proxy_subclass(subclass), *args, **kwargs)
|
|
312
|
+
|
|
313
|
+
# def unproxied__new__(subclass, *args, **kwargs):
|
|
314
|
+
# return unproxy_type(subclass)(*args, **kwargs)
|
|
315
|
+
|
|
316
|
+
# # Dispatch which constructor to use based on your thread_state policy
|
|
317
|
+
# cls.__new__ = thread_state.dispatch(unproxied__new__, internal = proxied__new__)
|
|
318
|
+
|
|
319
|
+
def create_unproxied_type(cls):
|
|
169
320
|
|
|
170
|
-
def
|
|
321
|
+
def unproxy_type(cls):
|
|
322
|
+
return cls.__dict__.get('__retrace_unproxied__', cls)
|
|
323
|
+
|
|
324
|
+
return type(cls.__name__, tuple(map(unproxy_type, cls.__bases__)), dict(cls.__dict__))
|
|
325
|
+
|
|
326
|
+
def extending_proxytype(cls, base, thread_state, ext_handler, int_handler, on_subclass_new):
|
|
171
327
|
|
|
172
328
|
assert not issubclass(cls, BaseException)
|
|
173
329
|
|
|
174
330
|
def init_subclass(subclass, **kwargs):
|
|
175
|
-
print(f'In init_subclass: {subclass} {kwargs}')
|
|
176
|
-
|
|
331
|
+
# print(f'In init_subclass: {subclass} {kwargs}')
|
|
332
|
+
subclass.__retrace_unproxied__ = create_unproxied_type(subclass)
|
|
177
333
|
|
|
178
334
|
proxy_method_descriptors(cls = subclass, handler = int_handler)
|
|
179
335
|
|
|
180
336
|
if not issubclass(subclass, InternalProxy):
|
|
181
|
-
subclass.__new__ = functional.
|
|
337
|
+
subclass.__new__ = functional.sequence(subclass.__new__, functional.side_effect(on_subclass_new))
|
|
182
338
|
subclass.__bases__ = subclass.__bases__ + (InternalProxy,)
|
|
183
339
|
|
|
184
|
-
slots = { "__slots__": (),
|
|
340
|
+
slots = { "__slots__": (),
|
|
341
|
+
"__retrace_unproxied__": cls,
|
|
342
|
+
"__module__": cls.__module__,
|
|
343
|
+
"__init_subclass__": init_subclass }
|
|
185
344
|
|
|
186
345
|
def wrap(target): return utils.wrapped_function(handler = ext_handler, target = target)
|
|
187
346
|
|
|
@@ -194,87 +353,67 @@ def extending_proxytype(cls, thread_state, ext_handler, int_handler, on_subclass
|
|
|
194
353
|
elif is_descriptor(value):
|
|
195
354
|
descriptors.append(name)
|
|
196
355
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
extended = type(name, (cls, ExtendingProxy), slots)
|
|
356
|
+
extended = type(cls.__name__, (base, ExtendingProxy), slots)
|
|
200
357
|
|
|
201
358
|
for name in descriptors:
|
|
202
|
-
proxy =
|
|
359
|
+
proxy = ExtendingDescriptorProxy(handler = ext_handler, name = name, proxytype = extended)
|
|
203
360
|
setattr(extended, name, proxy)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# instance.__init__(*args, **kwargs)
|
|
210
|
-
# return instance
|
|
211
|
-
|
|
212
|
-
# __new__ = functional.compose(cls.__new__, functional.side_effect(on_new))
|
|
213
|
-
if is_stub:
|
|
214
|
-
def __new__(cls, *args, **kwargs):
|
|
215
|
-
instance = utils.create_stub_object(cls)
|
|
216
|
-
# print(instance is None)
|
|
217
|
-
# utils.sigtrap(None)
|
|
218
|
-
return instance
|
|
219
|
-
|
|
220
|
-
extended.__new__ = thread_state.dispatch(cls.__new__, internal = __new__, external = __new__)
|
|
221
|
-
|
|
222
|
-
# extended.__retrace_unproxied__ = cls
|
|
361
|
+
|
|
362
|
+
def unproxied__new__(subclass, *args, **kwargs):
|
|
363
|
+
return subclass.__retrace_unproxied__(*args, **kwargs)
|
|
364
|
+
|
|
365
|
+
extended.__new__ = thread_state.dispatch(unproxied__new__, internal = base.__new__)
|
|
223
366
|
|
|
224
367
|
return extended
|
|
225
368
|
|
|
226
369
|
|
|
227
|
-
def stubtype(cls, result, thread_state, handler):
|
|
370
|
+
# def stubtype(cls, result, thread_state, handler):
|
|
228
371
|
|
|
229
|
-
|
|
372
|
+
# name = f'retrace.stub.{cls.__module__}.{cls.__name__}'
|
|
230
373
|
|
|
231
|
-
|
|
374
|
+
# slots = {}
|
|
232
375
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
376
|
+
# def wrap(name):
|
|
377
|
+
# return utils.wrapped_function(
|
|
378
|
+
# handler = handler,
|
|
379
|
+
# target = StubMethodDescriptor(name = name, result = result))
|
|
237
380
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
381
|
+
# for name,value in superdict(cls).items():
|
|
382
|
+
# if name not in blacklist:
|
|
383
|
+
# if is_method_descriptor(value):
|
|
384
|
+
# slots[name] = wrap(name)
|
|
385
|
+
# elif is_descriptor(value):
|
|
386
|
+
# slots[name] = DescriptorStub(handler = handler, name = name)
|
|
244
387
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
388
|
+
# def disabled__new__(subcls, *args, **kwargs):
|
|
389
|
+
# instance = cls.__new__(subcls.__retrace_unproxied__, *args, **kwargs)
|
|
390
|
+
# instance.__init__(*args, **kwargs)
|
|
391
|
+
# return instance
|
|
249
392
|
|
|
250
|
-
|
|
393
|
+
# stub = type(name, (Stub,), slots)
|
|
251
394
|
|
|
252
|
-
|
|
395
|
+
# stub.__new__ = thread_state.dispatch(disabled__new__, internal = stub.__new__, external = stub.__new__)
|
|
253
396
|
|
|
254
|
-
|
|
397
|
+
# stub.__retrace_unproxied__ = cls
|
|
398
|
+
|
|
399
|
+
# return stub
|
|
255
400
|
|
|
256
|
-
return stub
|
|
257
401
|
|
|
258
|
-
def create_unproxied_type(cls):
|
|
259
|
-
name = f'{cls.__module__}.{cls.__name__}'
|
|
260
402
|
|
|
261
|
-
def unproxy_type(cls):
|
|
262
|
-
return cls.__retrace_unproxied__ if issubclass(cls, ExtendingProxy) else cls
|
|
263
403
|
|
|
264
|
-
return type(name, tuple(map(unproxy_type, cls.__bases__)), dict(cls.__dict__))
|
|
265
404
|
|
|
266
|
-
def make_extensible(cls, handler, on_new):
|
|
405
|
+
# def make_extensible(cls, handler, on_new):
|
|
267
406
|
|
|
268
|
-
|
|
407
|
+
# cls.__retrace_unproxied__ = cls.__base__
|
|
269
408
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
409
|
+
# def init_subclass(*args, **kwargs):
|
|
410
|
+
# print(f'In init_subclass: {args} {kwargs}')
|
|
411
|
+
# # subclass.__retrace_unproxied__ = create_unproxied_type(subclass)
|
|
273
412
|
|
|
274
|
-
|
|
413
|
+
# # proxy_method_descriptors(cls = subclass, handler = handler)
|
|
275
414
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
415
|
+
# # if not issubclass(subclass, InternalProxy):
|
|
416
|
+
# # cls.__new__ = functional.compose(cls.__new__, functional.side_effect(on_new))
|
|
417
|
+
# # cls.__bases__ = cls.__bases__ + (InternalProxy,)
|
|
279
418
|
|
|
280
|
-
|
|
419
|
+
# cls.__init_subclass__ = init_subclass
|