| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from weakref import ref |
| from typing import MutableMapping |
| |
| |
| class IdentityWeakRef(ref): |
| __slots__ = "object_id" |
| |
| def __init__(self, obj, callback=None): |
| super(IdentityWeakRef, self).__init__(obj, callback) |
| self.object_id = id(obj) |
| |
| def __hash__(self): |
| return self.object_id |
| |
| def __eq__(self, other): |
| if type(other) is not IdentityWeakRef: |
| return False |
| return self.object_id == other.object_id |
| |
| |
| class WeakIdentityKeyDictionary(MutableMapping): |
| """Mapping class that references keys weakly. |
| |
| Entries in the dictionary will be discarded when there is no |
| longer a strong reference to the key. This can be used to |
| associate additional data with an object owned by other parts of |
| an application without adding attributes to those objects. This |
| can be especially useful with objects that override attribute |
| accesses. |
| """ |
| |
| def __init__(self, dict=None): |
| self.data = {} |
| |
| def remove(k, selfref=IdentityWeakRef(self)): |
| self = selfref() |
| if self is not None: |
| if self._iterating: |
| self._pending_removals.append(k) |
| else: |
| del self.data[k] |
| |
| self._remove = remove |
| # A list of dead weakrefs (keys to be removed) |
| self._pending_removals = [] |
| self._iterating = set() |
| self._dirty_len = False |
| if dict is not None: |
| self.update(dict) |
| |
| def _scrub_removals(self): |
| d = self.data |
| self._pending_removals = [k for k in self._pending_removals if k in d] |
| self._dirty_len = False |
| |
| def __delitem__(self, key): |
| self._dirty_len = True |
| del self.data[IdentityWeakRef(key)] |
| |
| def __getitem__(self, key): |
| return self.data[IdentityWeakRef(key)] |
| |
| def __len__(self): |
| if self._dirty_len and self._pending_removals: |
| # self._pending_removals may still contain keys which were |
| # explicitly removed, we have to scrub them (see issue #21173). |
| self._scrub_removals() |
| return len(self.data) - len(self._pending_removals) |
| |
| def __repr__(self): |
| return "<%s at %#x>" % (self.__class__.__name__, id(self)) |
| |
| def __setitem__(self, key, value): |
| self.data[IdentityWeakRef(key, self._remove)] = value |
| |
| def get(self, key, default=None): |
| return self.data.get(IdentityWeakRef(key), default) |
| |
| def __contains__(self, key): |
| try: |
| wr = IdentityWeakRef(key) |
| except TypeError: |
| return False |
| return wr in self.data |
| |
| def items(self): |
| with _IterationGuard(self): |
| for wr, value in self.data.items(): |
| key = wr() |
| if key is not None: |
| yield key, value |
| |
| def keys(self): |
| with _IterationGuard(self): |
| for wr in self.data: |
| obj = wr() |
| if obj is not None: |
| yield obj |
| |
| __iter__ = keys |
| |
| def values(self): |
| with _IterationGuard(self): |
| for wr, value in self.data.items(): |
| if wr() is not None: |
| yield value |
| |
| def keyrefs(self): |
| """Return a list of weak references to the keys. |
| |
| The references are not guaranteed to be 'live' at the time |
| they are used, so the result of calling the references needs |
| to be checked before being used. This can be used to avoid |
| creating references that will cause the garbage collector to |
| keep the keys around longer than needed. |
| |
| """ |
| return list(self.data) |
| |
| def popitem(self): |
| self._dirty_len = True |
| while True: |
| key, value = self.data.popitem() |
| o = key() |
| if o is not None: |
| return o, value |
| |
| def pop(self, key, *args): |
| self._dirty_len = True |
| return self.data.pop(IdentityWeakRef(key), *args) |
| |
| def setdefault(self, key, default=None): |
| return self.data.setdefault(IdentityWeakRef(key, self._remove), default) |
| |
| def update(self, dict=None, **kwargs): |
| d = self.data |
| if dict is not None: |
| if not hasattr(dict, "items"): |
| dict = type({})(dict) |
| for key, value in dict.items(): |
| d[IdentityWeakRef(key, self._remove)] = value |
| if len(kwargs): |
| self.update(kwargs) |
| |
| |
| class _IterationGuard: |
| # This context manager registers itself in the current iterators of the |
| # weak container, such as to delay all removals until the context manager |
| # exits. |
| # This technique should be relatively thread-safe (since sets are). |
| |
| def __init__(self, weakcontainer): |
| # Don't create cycles |
| self.weakcontainer = IdentityWeakRef(weakcontainer) |
| |
| def __enter__(self): |
| w = self.weakcontainer() |
| if w is not None: |
| w._iterating.add(self) |
| return self |
| |
| def __exit__(self, e, t, b): |
| w = self.weakcontainer() |
| if w is not None: |
| s = w._iterating |
| s.remove(self) |
| if not s: |
| w._commit_removals() |