Source code for rpyc.core.netref
"""*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__', '____hash__', '____member__', '____bind_instance__',
'__class__', '__cmp__', '__del__', '__delattr__',
'__dir__', '__getattr__', '__getattribute__', '__hash__',
'__instancecheck__', '__subclasscheck__', '__subclasses__',
'__init__', '__metaclass__', '__module__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__slots__', '__str__', '__bool__',
'__weakref__', '__dict__', '__methods__', '__exit__',
'__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__',
'mro', '__mro__', '__bases__', '__base__',
]) | 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
"""
if type(proxy) is NetrefMetaclass:
base = type
else:
base = object
conn = base.__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
"""
if type(proxy) is NetrefMetaclass:
base = type
else:
base = object
conn = base.__getattribute__(proxy, "____conn__")
return conn.async_request(handler, proxy, *args)
class Member:
__slots__ = "____conn__", "____id_pack__", "____refcount__", "____hash__"
class MemberDescriptor:
__slots__ = '_name', '_owner', '_default'
def __init__(self, default=None, /):
self._default = default
self._owner = None
self._name = None
def __set_name__(self, owner, name, /):
self._owner = owner
self._name = name
def __get__(self, instance, owner, /):
if instance is None:
return self._default
return getattr(instance.____member__, self._name, self._default)
def __set__(self, instance, value, /):
setattr(instance.____member__, self._name, value)
def __delete__(self, instance):
delattr(instance.____member__, self._name)
[docs]
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 __new__(metacls, name, bases, dct, id_pack=None):
alldct = {}
for b in reversed(bases):
alldct.update(b.__dict__)
alldct.update(dct)
for attr in LOCAL_ATTRS:
if attr not in alldct and attr not in (
'__class__',
'__methods__',
'__metaclass__',
'__weakref__',
'__array_interface__',
'__array_struct__',
'__repr__',
'__dict__',
'__reduce__',
'__instancecheck__',
'__subclasscheck__',
'__subclasses__',
'__init__',
'__new__',
'____id_pack__',
'____refcount__',
'____conn__',
'____hash__',
'____member__',
'____bind_instance__',
'mro',
'__mro__',
'__bases__',
'__base__',
):
dct[attr] = metacls.__dict__[attr]
for attr in (
'____refcount__',
'____conn__',
'____hash__',
):
if attr not in alldct:
dct[attr] = None
dct['____id_pack__'] = id_pack
undefined = object()
for attr in (
'____refcount__',
'____conn__',
'____id_pack__',
'____hash__',
):
value = dct.get(attr, undefined)
if value is not undefined and not isinstance(value, MemberDescriptor):
dct[attr] = MemberDescriptor(dct[attr])
return super(NetrefMetaclass, metacls).__new__(metacls, name, bases, dct)
def __call__(cls, *args, **kwargs):
kwargs = tuple(kwargs.items())
return syncreq(cls, consts.HANDLE_CALL, args, kwargs)
def ____bind_instance__(cls, conn, id_pack):
obj = cls.__new__(cls, conn, id_pack)
if isinstance(obj, cls):
cls.__init__(obj, conn, id_pack)
return obj
def __del__(self):
# this is called from garbage collection
# garbage collection might kick in at any moment
# Therefore we must be very careful what we call
# from here
conn = getattr(self, "____conn__", None)
if conn is not None:
id_pack = self.____id_pack__
try:
conn.async_request(consts.HANDLE_DEL, id_pack, self.____refcount__)
except BaseException:
# 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 type(self) is NetrefMetaclass:
base = type
else:
base = object
if base is type and name in ("__name__", ):
return base.__getattribute__(self, name)
if name in LOCAL_ATTRS:
if name == "__class__":
cls = base.__getattribute__(self, "__class__")
if cls is None:
cls = self.__getattr__("__class__")
return cls
if name in DELETED_ATTRS:
raise AttributeError(
f'{type(self).__name__!r} object has no attribute {name!r}'
)
return base.__getattribute__(self, name)
if name in ("__call__", "__array__"):
return base.__getattribute__(self, name)
return syncreq(self, consts.HANDLE_GETATTR, name)
def __getattr__(self, name):
if name in DELETED_ATTRS or (
name != '__class__' and name in LOCAL_ATTRS):
raise AttributeError(
f'{type(self).__name__!r} object has no attribute {name!r}'
)
return syncreq(self, consts.HANDLE_GETATTR, name)
def __delattr__(self, name):
if name in LOCAL_ATTRS:
super(type(self), self).__delattr__(name)
else:
syncreq(self, consts.HANDLE_DELATTR, name)
def __setattr__(self, name, value):
if name in LOCAL_ATTRS:
if (name in (
'____conn__',
'____id_pack__',
'____refcount__',
'____hash__'
) and type(self) is NetrefMetaclass and
not isinstance(value, MemberDescriptor)):
value = MemberDescriptor(value)
value.__set_name__(self, name)
if type(self) is NetrefMetaclass:
base = type
else:
base = object
base.__setattr__(self, name, value)
else:
syncreq(self, consts.HANDLE_SETATTR, name, value)
def __dir__(self):
return list(syncreq(self, consts.HANDLE_DIR))
def __repr__(self):
if self.____conn__ is None:
if self.__module__:
return f"<netref class '{self.__module__}.{self.__name__}'>"
return f"<netref class '{self.__name__}'>"
return syncreq(self, consts.HANDLE_REPR)
def __str__(self):
return syncreq(self, consts.HANDLE_STR)
def __bool__(self):
return syncreq(self, consts.HANDLE_BOOL)
def __exit__(self, typ, exc, tb):
if exc is None:
boxed_exc = None
else:
boxed_exc = self.____conn__._box_exc(typ, exc, tb)
return syncreq(self, consts.HANDLE_CTXEXIT, boxed_exc)
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 super(type(self), self).__instancecheck__(other):
if self is BaseNetref:
return True
if self.____id_pack__[2] != 0:
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
if self.____id_pack__[1] == other.____id_pack__[1]:
return other.____id_pack__[2] != 0
# seems dubious if each netref proxies to a different address spaces
return syncreq(self, consts.HANDLE_INSTANCECHECK, other.____id_pack__)
if self.____id_pack__ is None:
return False
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, self.__dict__['__class__'].instance)
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
def __subclasscheck__(self, other):
# support for checking cached instances across connections
if super(type(self), self).__subclasscheck__(other):
if self is BaseNetref:
return True
if self.____id_pack__[2] != 0:
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
if self.____id_pack__[1] == other.____id_pack__[1]:
return other.____id_pack__[2] == 0
# seems dubious if each netref proxies to a different address spaces
return syncreq(self, consts.HANDLE_SUBCLASSCHECK, other.____id_pack__)
if self.____id_pack__ is None:
return False
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 issubclass(other, self.__dict__['__class__'].instance)
raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
def __hash__(self):
# cache hashes for performance reasons
if self.____hash__ is None:
if self.____conn__ is None:
return super(type(self), self).__hash__()
try:
self.____hash__ = syncreq(self, consts.HANDLE_HASH)
except BaseException as ex:
self.____hash__ = ex
if isinstance(self.____hash__, BaseException):
raise self.____hash__ from None
return self.____hash__
class BaseNetref(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__ = "__weakref__", "____member__"
____refcount__ = 1
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 __init__(self, conn, id_pack):
self.____member__ = Member()
self.____conn__ = conn
self.____id_pack__ = id_pack
self.____refcount__ = 1
self.____hash__ = None
class Method:
__slots__ = ('__name__', '__doc__')
def __init__(self, name, doc):
self.__name__ = name
self.__doc__ = doc
def __get__(self, instance, owner=None):
if instance is None:
instance = owner
return syncreq(instance, consts.HANDLE_GETATTR, self.__name__)
def __set__(self, instance, value):
return syncreq(instance, consts.HANDLE_SETATTR, self.__name__)
def __delete__(self, instance):
return syncreq(instance, consts.HANDLE_DELATTR, self.__name__)
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__
if 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
if name == "__array__":
def __array__(self, *args, **kwargs):
# Note that protocol=-1 will only work between python
# interpreters of the same version.
if type(self) is NetrefMetaclass:
base = type
else:
base = object
if not base.__getattribute__(self, '____conn__')._config["allow_pickle"]:
# Security check that server side allows pickling per #551
raise ValueError("pickling is disabled")
array = pickle.loads(syncreq(self, consts.HANDLE_PICKLE, -1))
return array.__array__(*args, **kwargs)
__array__.__doc__ = doc
return __array__
return Method(name, doc)
[docs]
class NetrefClass:
"""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"""
if netref_instance is None:
return self.owner
return self.instance
[docs]
def class_factory(id_pack, methods, conn=None):
"""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
if class_descriptor is not None:
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.rsplit('.', 1)[-1], (BaseNetref, ), ns)
netref_cls.____id_pack__ = id_pack
netref_cls.____conn__ = conn
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)
