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