1"""Refactored 'safe reference from dispatcher.py"""
2
3import collections
4import traceback
5import weakref
6from functools import total_ordering
7
8
9def safe_ref(target, on_delete=None):
10    """Return a *safe* weak reference to a callable target.
11
12    - ``target``: The object to be weakly referenced, if it's a bound
13      method reference, will create a BoundMethodWeakref, otherwise
14      creates a simple weakref.
15
16    - ``on_delete``: If provided, will have a hard reference stored to
17      the callable to be called after the safe reference goes out of
18      scope with the reference object, (either a weakref or a
19      BoundMethodWeakref) as argument.
20    """
21    if hasattr(target, "im_self"):
22        if target.__self__ is not None:
23            # Turn a bound method into a BoundMethodWeakref instance.
24            # Keep track of these instances for lookup by disconnect().
25            assert hasattr(target, "im_func"), (
26                f"safe_ref target {target!r} has im_self, but no im_func, "
27                "don't know how to create reference"
28            )
29            reference = BoundMethodWeakref(target=target, on_delete=on_delete)
30            return reference
31
32    if hasattr(target, "__self__"):
33        if target.__self__ is not None:
34            assert hasattr(target, "__func__"), (
35                f"safe_ref target {target!r} has __self__, but no __func__, "
36                "don't know how to create reference"
37            )
38            reference = BoundMethodWeakref(target=target, on_delete=on_delete)
39            return reference
40
41    if hasattr(on_delete, "__call__"):
42        return weakref.ref(target, on_delete)
43    else:
44        return weakref.ref(target)
45
46
47@total_ordering
48class BoundMethodWeakref(object):
49    """'Safe' and reusable weak references to instance methods.
50
51    BoundMethodWeakref objects provide a mechanism for referencing a
52    bound method without requiring that the method object itself
53    (which is normally a transient object) is kept alive.  Instead,
54    the BoundMethodWeakref object keeps weak references to both the
55    object and the function which together define the instance method.
56
57    Attributes:
58
59    - ``key``: The identity key for the reference, calculated by the
60      class's calculate_key method applied to the target instance method.
61
62    - ``deletion_methods``: Sequence of callable objects taking single
63      argument, a reference to this object which will be called when
64      *either* the target object or target function is garbage
65      collected (i.e. when this object becomes invalid).  These are
66      specified as the on_delete parameters of safe_ref calls.
67
68    - ``weak_self``: Weak reference to the target object.
69
70    - ``weak_func``: Weak reference to the target function.
71
72    Class Attributes:
73
74    - ``_all_instances``: Class attribute pointing to all live
75      BoundMethodWeakref objects indexed by the class's
76      calculate_key(target) method applied to the target objects.
77      This weak value dictionary is used to short-circuit creation so
78      that multiple references to the same (object, function) pair
79      produce the same BoundMethodWeakref instance.
80    """
81
82    _all_instances = weakref.WeakValueDictionary()
83
84    def __new__(cls, target, on_delete=None, *arguments, **named):
85        """Create new instance or return current instance.
86
87        Basically this method of construction allows us to
88        short-circuit creation of references to already- referenced
89        instance methods.  The key corresponding to the target is
90        calculated, and if there is already an existing reference,
91        that is returned, with its deletion_methods attribute updated.
92        Otherwise the new instance is created and registered in the
93        table of already-referenced methods.
94        """
95        key = cls.calculate_key(target)
96        current = cls._all_instances.get(key)
97        if current is not None:
98            current.deletion_methods.append(on_delete)
99            return current
100        else:
101            base = super(BoundMethodWeakref, cls).__new__(cls)
102            cls._all_instances[key] = base
103            base.__init__(target, on_delete, *arguments, **named)
104            return base
105
106    def __init__(self, target, on_delete=None):
107        """Return a weak-reference-like instance for a bound method.
108
109        - ``target``: The instance-method target for the weak reference,
110          must have im_self and im_func attributes and be
111          reconstructable via the following, which is true of built-in
112          instance methods::
113
114            target.im_func.__get__( target.im_self )
115
116        - ``on_delete``: Optional callback which will be called when
117          this weak reference ceases to be valid (i.e. either the
118          object or the function is garbage collected).  Should take a
119          single argument, which will be passed a pointer to this
120          object.
121        """
122
123        def remove(weak, self_=self):
124            """Set self.isDead to True when method or instance is destroyed."""
125            methods = self_.deletion_methods[:]
126            del self_.deletion_methods[:]
127            try:
128                del self_.__class__._all_instances[self_.key]
129            except KeyError:
130                pass
131            for function in methods:
132                try:
133                    if isinstance(function, collections.Callable):
134                        function(self_)
135                except Exception:
136                    try:
137                        traceback.print_exc()
138                    except AttributeError as e:
139                        print(
140                            f"Exception during saferef {self_} "
141                            f"cleanup function {function}: {e}"
142                        )
143
144        self.deletion_methods = [on_delete]
145        self.key = self.calculate_key(target)
146        try:
147            self.weak_self = weakref.ref(target.__self__, remove)
148            self.weak_func = weakref.ref(target.__func__, remove)
149            self.self_name = str(target.__self__)
150            self.__name__ = str(target.__func__.__name__)
151        except AttributeError:
152            self.weak_self = weakref.ref(target.__self__, remove)
153            self.weak_func = weakref.ref(target.__func__, remove)
154            self.self_name = str(target.__self__)
155            self.__name__ = str(target.__func__.__name__)
156
157    @classmethod
158    def calculate_key(cls, target):
159        """Calculate the reference key for this reference.
160
161        Currently this is a two-tuple of the id()'s of the target
162        object and the target function respectively.
163        """
164        return id(target.__self__), id(target.__func__)
165
166    def __str__(self):
167        """Give a friendly representation of the object."""
168        return f"{self.__class__.__name__}({self.self_name}.{self.__name__})"
169
170    __repr__ = __str__
171
172    def __bool__(self):
173        """Whether we are still a valid reference."""
174        return self() is not None
175
176    def __eq__(self, other):
177        """Compare with another reference."""
178        if not isinstance(other, self.__class__):
179            return self.__class__ is type(other)
180        else:
181            return self.key == other.key
182
183    def __ne__(self, other):
184        """Compare with another reference."""
185        if not isinstance(other, self.__class__):
186            return self.__class__ is not type(other)
187        else:
188            return self.key != other.key
189
190    def __lt__(self, other):
191        """Compare with another reference."""
192        if not isinstance(other, self.__class__):
193            return self.__class__ < type(other)
194        else:
195            return self.key < other.key
196
197    def __call__(self):
198        """Return a strong reference to the bound method.
199
200        If the target cannot be retrieved, then will return None,
201        otherwise returns a bound instance method for our object and
202        function.
203
204        Note: You may call this method any number of times, as it does
205        not invalidate the reference.
206        """
207        target = self.weak_self()
208        if target is not None:
209            function = self.weak_func()
210            if function is not None:
211                return function.__get__(target)
212        return None
213
214    def __hash__(self):
215        return hash(self.key)
216