"""Repro for Python 3.6 slots + weakref + typing.Generic bug.""" from typing import Iterator, Mapping, MutableMapping, TypeVar from weakref import ref KT1 = TypeVar("KT1") KT2 = TypeVar("KT2") class Invertible(Mapping[KT1, KT2]): """A one-element mapping that is generic in two key types with a reference to its inverse. ...which in turn holds a (weak) reference back to it. >>> element = Invertible("H", 1) >>> element >>> element.inverse >>> element.inverse.inverse >>> element.inverse.inverse is element True >>> dict(element.items()) {'H': 1} >>> dict(element.inverse.items()) {1: 'H'} >>> list(element) ['H'] Uses the __slots__ optimization, and uses weakrefs for references in one direction to avoid strong reference cycles. And still manages to support pickling to boot! >>> from pickle import dumps, loads >>> pickled = dumps(element) >>> roundtripped = loads(pickled) >>> roundtripped """ # Each instance has (either a strong or a weak) reference to its # inverse instance, which has a (weak or strong) reference back. __slots__ = ("_inverse_strong", "_inverse_weak", "__weakref__", "key1", "key2") def __init__(self, key1: KT1, key2: KT2) -> None: self._inverse_weak = None self._inverse_strong = inverse = self.__class__.__new__(self.__class__) self.key1 = inverse.key2 = key1 self.key2 = inverse.key1 = key2 inverse._inverse_strong = None inverse._inverse_weak = ref(self) def __len__(self) -> int: return 1 def __iter__(self) -> Iterator[KT1]: yield self.key1 def __getitem__(self, key: KT1) -> KT2: if key == self.key1: return self.key2 raise KeyError(key) def __repr__(self) -> str: return f"<{self.__class__.__name__} key1={self.key1!r} key2={self.key2!r}>" @property def inverse(self) -> "Invertible[KT2, KT1]": """The inverse instance.""" if self._inverse_strong is not None: return self._inverse_strong inverse = self._inverse_weak() if inverse is not None: return inverse # Refcount of referent must have dropped to zero, # as in `Invertible().inverse.inverse`, so init a new one. self._inverse_weak = None self._inverse_strong = inverse = self.__class__.__new__(self.__class__) inverse.key2 = self.key1 inverse.key1 = self.key2 inverse._inverse_strong = None inverse._inverse_weak = ref(self) return inverse def __getstate__(self) -> dict: """Needed to enable pickling due to use of __slots__ and weakrefs.""" state = {} for cls in self.__class__.__mro__: slots = getattr(cls, '__slots__', ()) for slot in slots: if hasattr(self, slot): state[slot] = getattr(self, slot) # weakrefs can't be pickled. state.pop('_inverse_weak', None) # Added back in __setstate__ below. state.pop('__weakref__', None) # Not added back in __setstate__. Python manages this one. return state def __setstate__(self, state) -> None: """Needed because use of __slots__ would prevent unpickling otherwise.""" for slot, value in state.items(): setattr(self, slot, value) self._inverse_weak = None self._inverse_strong = inverse = self.__class__.__new__(self.__class__) inverse.key2 = self.key1 inverse.key1 = self.key2 inverse._inverse_strong = None inverse._inverse_weak = ref(self) # So far so good, but now let's make a mutable version. # # The following class definition works on Python > 3.6, but fails on 3.6 with # TypeError: __weakref__ slot disallowed: either we already got one, or __itemsize__ != 0 class MutableInvertible(Invertible[KT1, KT2], MutableMapping[KT1, KT2]): """Works on > 3.6, but we don't even get this far on Python 3.6: >>> MutableInvertible("H", 1) """ __slots__ = () def __setitem__(self, key1: KT1, key2: KT2) -> None: self.key1 = self.inverse.key2 = key1 self.key2 = self.inverse.key1 = key2 def __delitem__(self, key: KT1) -> None: raise KeyError(key) if __name__ == "__main__": import doctest doctest.testmod()