"""*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, with_metaclass
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 = {}
[docs]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)
[docs]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)
[docs]class BaseNetref(with_metaclass(NetrefMetaclass, object)):
"""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.protocl.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.
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
[docs]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
[docs]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)
break
ns['__class__'] = class_descriptor
netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack
# create methods that must perform a syncreq
for name, doc in methods:
name = str(name) # IronPython issue #10
# only create methods that wont shadow BaseNetref during merge for mro
if name not in LOCAL_ATTRS: # i.e. `name != __class__`
ns[name] = _make_method(name, doc)
return type(netref_name, (BaseNetref,), ns)
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)