-
Notifications
You must be signed in to change notification settings - Fork 243
/
netref.py
345 lines (287 loc) · 13.6 KB
/
netref.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
"""*NetRef*: a transparent *network reference*. This module contains quite a lot
of *magic*, so beware.
"""
import sys
import types
from rpyc.lib import get_methods, get_id_pack
from rpyc.lib.compat import pickle, maxint
from rpyc.core import consts
builtin_id_pack_cache = {} # name_pack -> id_pack
builtin_classes_cache = {} # id_pack -> class
# If these can be accessed, numpy will try to load the array from local memory,
# resulting in exceptions and/or segfaults, see #236:
DELETED_ATTRS = frozenset([
'__array_struct__', '__array_interface__',
])
"""the set of attributes that are local to the netref object"""
LOCAL_ATTRS = frozenset([
'____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__',
'__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__',
'__init__', '__metaclass__', '__module__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__slots__', '__str__',
'__weakref__', '__dict__', '__methods__', '__exit__',
'__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__',
]) | DELETED_ATTRS
"""a list of types considered built-in (shared between connections)
this is needed because iterating the members of the builtins module is not enough,
some types (e.g NoneType) are not members of the builtins module.
TODO: this list is not complete.
"""
_builtin_types = [
type, object, bool, complex, dict, float, int, list, slice, str, tuple, set,
frozenset, BaseException, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType,
types.MethodType, types.CodeType, types.FrameType, types.TracebackType,
types.ModuleType, types.FunctionType, types.MappingProxyType,
type(int.__add__), # wrapper_descriptor
type((1).__add__), # method-wrapper
type(iter([])), # listiterator
type(iter(())), # tupleiterator
type(iter(set())), # setiterator
bytes, bytearray, type(iter(range(10))), memoryview
]
_normalized_builtin_types = {}
def syncreq(proxy, handler, *args):
"""Performs a synchronous request on the given proxy object.
Not intended to be invoked directly.
:param proxy: the proxy on which to issue the request
:param handler: the request handler (one of the ``HANDLE_XXX`` members of
``rpyc.protocol.consts``)
:param args: arguments to the handler
:raises: any exception raised by the operation will be raised
:returns: the result of the operation
"""
conn = object.__getattribute__(proxy, "____conn__")
return conn.sync_request(handler, proxy, *args)
def asyncreq(proxy, handler, *args):
"""Performs an asynchronous request on the given proxy object.
Not intended to be invoked directly.
:param proxy: the proxy on which to issue the request
:param handler: the request handler (one of the ``HANDLE_XXX`` members of
``rpyc.protocol.consts``)
:param args: arguments to the handler
:returns: an :class:`~rpyc.core.async_.AsyncResult` representing
the operation
"""
conn = object.__getattribute__(proxy, "____conn__")
return conn.async_request(handler, proxy, *args)
class NetrefMetaclass(type):
"""A *metaclass* used to customize the ``__repr__`` of ``netref`` classes.
It is quite useless, but it makes debugging and interactive programming
easier"""
__slots__ = ()
def __repr__(self):
if self.__module__:
return f"<netref class '{self.__module__}.{self.__name__}'>"
else:
return f"<netref class '{self.__name__}'>"
class BaseNetref(object, metaclass=NetrefMetaclass):
"""The base netref class, from which all netref classes derive. Some netref
classes are "pre-generated" and cached upon importing this module (those
defined in the :data:`_builtin_types`), and they are shared between all
connections.
The rest of the netref classes are created by :meth:`rpyc.core.protocol.Connection._unbox`,
and are private to the connection.
Do not use this class directly; use :func:`class_factory` instead.
:param conn: the :class:`rpyc.core.protocol.Connection` instance
:param id_pack: id tuple for an object ~ (name_pack, remote-class-id, remote-instance-id)
(cont.) name_pack := __module__.__name__ (hits or misses on builtin cache and sys.module)
remote-class-id := id of object class (hits or misses on netref classes cache and instance checks)
remote-instance-id := id object instance (hits or misses on proxy cache)
id_pack is usually created by rpyc.lib.get_id_pack
"""
__slots__ = ["____conn__", "____id_pack__", "__weakref__", "____refcount__"]
def __init__(self, conn, id_pack):
self.____conn__ = conn
self.____id_pack__ = id_pack
self.____refcount__ = 1
def __del__(self):
try:
asyncreq(self, consts.HANDLE_DEL, self.____refcount__)
except Exception:
# raised in a destructor, most likely on program termination,
# when the connection might have already been closed.
# it's safe to ignore all exceptions here
pass
def __getattribute__(self, name):
if name in LOCAL_ATTRS:
if name == "__class__":
cls = object.__getattribute__(self, "__class__")
if cls is None:
cls = self.__getattr__("__class__")
return cls
elif name == "__doc__":
return self.__getattr__("__doc__")
elif name in DELETED_ATTRS:
raise AttributeError()
else:
return object.__getattribute__(self, name)
elif name == "__call__": # IronPython issue #10
return object.__getattribute__(self, "__call__")
elif name == "__array__":
return object.__getattribute__(self, "__array__")
else:
return syncreq(self, consts.HANDLE_GETATTR, name)
def __getattr__(self, name):
if name in DELETED_ATTRS:
raise AttributeError()
return syncreq(self, consts.HANDLE_GETATTR, name)
def __delattr__(self, name):
if name in LOCAL_ATTRS:
object.__delattr__(self, name)
else:
syncreq(self, consts.HANDLE_DELATTR, name)
def __setattr__(self, name, value):
if name in LOCAL_ATTRS:
object.__setattr__(self, name, value)
else:
syncreq(self, consts.HANDLE_SETATTR, name, value)
def __dir__(self):
return list(syncreq(self, consts.HANDLE_DIR))
# support for metaclasses
def __hash__(self):
return syncreq(self, consts.HANDLE_HASH)
def __cmp__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__cmp__')
def __eq__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__eq__')
def __ne__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__ne__')
def __lt__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__lt__')
def __gt__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__gt__')
def __le__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__le__')
def __ge__(self, other):
return syncreq(self, consts.HANDLE_CMP, other, '__ge__')
def __repr__(self):
return syncreq(self, consts.HANDLE_REPR)
def __str__(self):
return syncreq(self, consts.HANDLE_STR)
def __exit__(self, exc, typ, tb):
return syncreq(self, consts.HANDLE_CTXEXIT, exc) # can't pass type nor traceback
def __reduce_ex__(self, proto):
# support for pickling netrefs
return pickle.loads, (syncreq(self, consts.HANDLE_PICKLE, proto),)
def __instancecheck__(self, other):
# support for checking cached instances across connections
if isinstance(other, BaseNetref):
if self.____id_pack__[2] != 0:
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
elif self.____id_pack__[1] == other.____id_pack__[1]:
if other.____id_pack__[2] == 0:
return False
elif other.____id_pack__[2] != 0:
return True
else:
# seems dubious if each netref proxies to a different address spaces
return syncreq(self, consts.HANDLE_INSTANCECHECK, other.____id_pack__)
else:
if self.____id_pack__[2] == 0:
# outside the context of `__instancecheck__`, `__class__` is expected to be type(self)
# within the context of `__instancecheck__`, `other` should be compared to the proxied class
return isinstance(other, type(self).__dict__['__class__'].instance)
else:
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
def _make_method(name, doc):
"""creates a method with the given name and docstring that invokes
:func:`syncreq` on its `self` argument"""
slicers = {"__getslice__": "__getitem__", "__delslice__": "__delitem__", "__setslice__": "__setitem__"}
name = str(name) # IronPython issue #10
if name == "__call__":
def __call__(_self, *args, **kwargs):
kwargs = tuple(kwargs.items())
return syncreq(_self, consts.HANDLE_CALL, args, kwargs)
__call__.__doc__ = doc
return __call__
elif name in slicers: # 32/64 bit issue #41
def method(self, start, stop, *args):
if stop == maxint:
stop = None
return syncreq(self, consts.HANDLE_OLDSLICING, slicers[name], name, start, stop, args)
method.__name__ = name
method.__doc__ = doc
return method
elif name == "__array__":
def __array__(self):
# Note that protocol=-1 will only work between python
# interpreters of the same version.
if not object.__getattribute__(self,'____conn__')._config["allow_pickle"]:
# Security check that server side allows pickling per #551
raise ValueError("pickling is disabled")
return pickle.loads(syncreq(self, consts.HANDLE_PICKLE, -1))
__array__.__doc__ = doc
return __array__
else:
def method(_self, *args, **kwargs):
kwargs = tuple(kwargs.items())
return syncreq(_self, consts.HANDLE_CALLATTR, name, args, kwargs)
method.__name__ = name
method.__doc__ = doc
return method
class NetrefClass(object):
"""a descriptor of the class being proxied
Future considerations:
+ there may be a cleaner alternative but lib.compat.with_metaclass prevented using __new__
+ consider using __slot__ for this class
+ revisit the design choice to use properties here
"""
def __init__(self, class_obj):
self._class_obj = class_obj
@property
def instance(self):
"""accessor to class object for the instance being proxied"""
return self._class_obj
@property
def owner(self):
"""accessor to the class object for the instance owner being proxied"""
return self._class_obj.__class__
def __get__(self, netref_instance, netref_owner):
"""the value returned when accessing the netref class is dictated by whether or not an instance is proxied"""
return self.owner if netref_instance.____id_pack__[2] == 0 else self.instance
def class_factory(id_pack, methods):
"""Creates a netref class proxying the given class
:param id_pack: the id pack used for proxy communication
:param methods: a list of ``(method name, docstring)`` tuples, of the methods that the class defines
:returns: a netref class
"""
ns = {"__slots__": (), "__class__": None}
name_pack = id_pack[0]
class_descriptor = None
if name_pack is not None:
# attempt to resolve __class__ using normalized builtins first
_builtin_class = _normalized_builtin_types.get(name_pack)
if _builtin_class is not None:
class_descriptor = NetrefClass(_builtin_class)
# then by imported modules (this also tries all builtins under "builtins")
else:
_module = None
cursor = len(name_pack)
while cursor != -1:
_module = sys.modules.get(name_pack[:cursor])
if _module is None:
cursor = name_pack[:cursor].rfind('.')
continue
_class_name = name_pack[cursor + 1:]
_class = getattr(_module, _class_name, None)
if _class is not None and hasattr(_class, '__class__'):
class_descriptor = NetrefClass(_class)
elif _class is None:
class_descriptor = NetrefClass(type(_module))
break
ns['__class__'] = class_descriptor
# create methods that must perform a syncreq
for name, doc in methods:
name = str(name) # IronPython issue #10
# only create methods that won't shadow BaseNetref during merge for mro
if name not in LOCAL_ATTRS: # i.e. `name != __class__`
ns[name] = _make_method(name, doc)
netref_cls = type(name_pack, (BaseNetref, ), ns)
return netref_cls
for _builtin in _builtin_types:
_id_pack = get_id_pack(_builtin)
_name_pack = _id_pack[0]
_normalized_builtin_types[_name_pack] = _builtin
_builtin_methods = get_methods(LOCAL_ATTRS, _builtin)
# assume all normalized builtins are classes
builtin_classes_cache[_name_pack] = class_factory(_id_pack, _builtin_methods)