blob: b6624c720ab0326a6816a0fec568bfa91d0d2ddc [file] [log] [blame]
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed 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.
"""Dictionary types that work for object identity, not defined equality"""
import weakref
from contextlib import contextmanager
from collections.abc import MutableMapping, MutableSet
from threading import RLock
_NO_RELOAD = True
class IdentityDictionary(MutableMapping):
"""Identity version of a dict"""
def __init__(self):
self.__data = {}
def __repr__(self):
return "ID{%s}" % " ,".join(
"{!r}: {!r}".format(*item) for item in self.items())
# Container
def __contains__(self, key):
return id(key) in self.__data
# Mapping
def __getitem__(self, key):
return self.__data[id(key)][1]
def __iter__(self):
return (v[0] for v in self.__data.values())
def __len__(self):
return len(self.__data)
def keys(self):
return iter(self)
def values(self):
return (v[1] for v in self.items())
def items(self):
return self.__data.values()
# MutableMapping
def __setitem__(self, key, value):
self.__data[id(key)] = (key, value)
def __delitem__(self, key):
del self.__data[id(key)]
def clear(self):
self.__data.clear()
class IdentitySet(MutableSet):
"""Identity version of a set"""
def __init__(self):
self.__data = {}
# Container
def __contains__(self, item):
return id(item) in self.__data
# Set
def __iter__(self):
return iter(self.__data.values())
def __len__(self):
return len(self.__data)
# MutableSet
def add(self, value):
self.__data[id(value)] = value
def discard(self, value):
self.__data.pop(id(value), None)
class WeakKeyIdentityDictionary(MutableMapping):
"""Weakly-keyed dictionary that stores objects by identity"""
# This class is complicated by how weakref defers to object __hash__
# instead of object identity. It works, essentially, by
# implementing a "hash table" at the Python level, using the object
# ID as a hash code. We can collide: we can store an object A with
# id I that later dies. In the meantime, we can construct an object
# B that reuses id I. So we have two weak refs keyed with id I and
# have to disambiguate them. The set of weakrefs to objects with id
# I is the bucket for I. The cookie is a unique integer that
# associates an object with a value. Each object gets its own
# cookie, so two objects with the same ID can have different
# associated data.
def __init__(self):
self.__clean_lock = RLock()
self.__clean_block_depth = 0
self.__to_clean = set()
self.__buckets_by_id = {}
self.__values_by_cookie = {}
self.__next_cookie = 1 # True in bool context
@contextmanager
def __clean_blocked(self):
with self.__clean_lock:
self.__clean_block_depth += 1
yield
with self.__clean_lock:
self.__clean_block_depth -= 1
if not self.__clean_block_depth:
self.__flush_clean_locked()
def __flush_clean_locked(self):
while self.__to_clean:
bucket_id, cookie = self.__to_clean.pop()
bucket = self.__buckets_by_id[bucket_id]
del self.__values_by_cookie[cookie]
del bucket[cookie]
if not bucket:
del self.__buckets_by_id[bucket_id]
def __weakref_callback(self, wref):
with self.__clean_lock:
self.__to_clean.add(wref.key)
if not self.__clean_block_depth:
self.__flush_clean_locked()
def __find(self, key):
assert key is not None
assert self.__clean_block_depth > 0 # pylint: disable=compare-to-zero
bucket = self.__buckets_by_id.get(id(key))
cookie = None
for b_cookie, b_ref in bucket.items() if bucket else ():
candidate = b_ref()
if candidate is None:
assert b_ref.key == (id(key), b_cookie)
self.__to_clean.add(b_ref.key)
elif candidate is key:
cookie = b_cookie
break
return cookie, bucket
# Container
def __contains__(self, key):
with self.__clean_blocked():
cookie, _ = self.__find(key)
return bool(cookie)
# Mapping
def __getitem__(self, key):
with self.__clean_blocked():
cookie, _ = self.__find(key)
if cookie:
return self.__values_by_cookie[cookie]
raise KeyError(key)
def get(self, key, default=None):
with self.__clean_blocked():
cookie, _ = self.__find(key)
if cookie:
return self.__values_by_cookie[cookie]
return default
def __iter__(self):
return iter(self.keys())
def __len__(self):
return len(self.items())
def keys(self):
return (v[0] for v in self.items())
def values(self):
return (v[1] for v in self.items())
def items(self):
# Weak-keyed collections are inherently unstable, so just return a
# snapshot of the object contents.
with self.__clean_blocked():
items = []
for bucket in self.__buckets_by_id.values():
for b_cookie, b_ref in bucket.items():
obj = b_ref()
if obj is None:
self.__to_clean.add(b_ref.key)
else:
item = (obj, self.__values_by_cookie[b_cookie])
items.append(item)
return items
# MutableMapping
def __setitem__(self, key, value):
with self.__clean_blocked():
cookie, bucket = self.__find(key)
if not cookie:
if not bucket:
bucket = {}
self.__buckets_by_id[id(key)] = bucket
cookie = self.__next_cookie
bucket[cookie] = weakref.KeyedRef(
key,
self.__weakref_callback,
(id(key), cookie))
self.__next_cookie += 1
self.__values_by_cookie[cookie] = value
def __delitem__(self, key):
with self.__clean_blocked():
cookie, _ = self.__find(key)
if not cookie:
raise KeyError(key)
self.__to_clean.add((id(key), cookie))