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