lib.wiring: expand flipped object forwarding to respect @property and del.

Although `@property` is the most common case, any descriptors are now
properly supported.

The special casing of methods goes away as they work by having functions
implement the descriptor protocol. (`__get__` has some special behavior
to make this possible.)

This is some of the most cursed code I have ever written, yet it is
obviously necessary.
This commit is contained in:
Catherine 2023-11-26 11:56:41 +00:00
parent 79adbed313
commit 74e613b49d
2 changed files with 176 additions and 24 deletions

View file

@ -491,6 +491,19 @@ class Signature(metaclass=SignatureMeta):
return super().__repr__() return super().__repr__()
def _gettypeattr(obj, attr):
# Resolve the attribute on the object's class, without triggering the descriptor protocol for
# attributes that are class methods, etc.
for cls in type(obj).__mro__:
try:
return cls.__dict__[attr]
except KeyError:
pass
# In case there is `__getattr__` on the metaclass, or just to generate an `AttributeError` with
# the standard message.
return type(obj).attr
# To simplify implementation and reduce API surface area `FlippedSignature` is made final. This # To simplify implementation and reduce API surface area `FlippedSignature` is made final. This
# restriction could be lifted if there is a compelling use case. # restriction could be lifted if there is a compelling use case.
@final @final
@ -524,15 +537,29 @@ class FlippedSignature:
is_compliant = Signature.is_compliant is_compliant = Signature.is_compliant
# FIXME: document this logic # FIXME: document this logic
# Because we would like to forward attribute access (other than what is explicitly overridden)
# to the unflipped signature, including access via e.g. @property-decorated functions, we have
# to reimplement the Python decorator protocol here. Note that in all of these functions, there
# are two possible exits via `except AttributeError`: from `getattr` and from `.__get__()`.
def __getattr__(self, name): def __getattr__(self, name):
value = getattr(self.__unflipped, name) try: # descriptor first
if inspect.ismethod(value): return _gettypeattr(self.__unflipped, name).__get__(self, type(self.__unflipped))
return types.MethodType(value.__func__, self) except AttributeError:
else: return getattr(self.__unflipped, name)
return value
def __setattr__(self, name, value): def __setattr__(self, name, value):
return setattr(self.__unflipped, name, value) try: # descriptor first
_gettypeattr(self.__unflipped, name).__set__(self, value)
except AttributeError:
setattr(self.__unflipped, name, value)
def __delattr__(self, name):
try: # descriptor first
_gettypeattr(self.__unflipped, name).__delete__(self)
except AttributeError:
delattr(self.__unflipped, name)
def create(self, *, path=()): def create(self, *, path=()):
return flipped(self.__unflipped.create(path=path)) return flipped(self.__unflipped.create(path=path))
@ -566,18 +593,35 @@ class FlippedInterface:
return type(self) is type(other) and self.__unflipped == other.__unflipped return type(self) is type(other) and self.__unflipped == other.__unflipped
# FIXME: document this logic # FIXME: document this logic
# See the note in ``FlippedSignature``. In addition, these accessors also handle flipping of
# an interface member.
def __getattr__(self, name): def __getattr__(self, name):
value = getattr(self.__unflipped, name) if (name in self.__unflipped.signature.members and
if inspect.ismethod(value): self.__unflipped.signature.members[name].is_signature):
return types.MethodType(value.__func__, self) return flipped(getattr(self.__unflipped, name))
elif name in self.__unflipped.signature.members and \
self.__unflipped.signature.members[name].is_signature:
return flipped(value)
else: else:
return value try: # descriptor first
return _gettypeattr(self.__unflipped, name).__get__(self, type(self.__unflipped))
except AttributeError:
return getattr(self.__unflipped, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
return setattr(self.__unflipped, name, value) if (name in self.__unflipped.signature.members and
self.__unflipped.signature.members[name].is_signature):
setattr(self.__unflipped, name, flipped(value))
else:
try: # descriptor first
_gettypeattr(self.__unflipped, name).__set__(self, value)
except AttributeError:
setattr(self.__unflipped, name, value)
def __delattr__(self, name):
try: # descriptor first
_gettypeattr(self.__unflipped, name).__delete__(self)
except AttributeError:
delattr(self.__unflipped, name)
def __repr__(self): def __repr__(self):
return f"flipped({self.__unflipped!r})" return f"flipped({self.__unflipped!r})"

View file

@ -530,7 +530,7 @@ class FlippedSignatureTestCase(unittest.TestCase):
sig = Signature({"a": In(1)}).flip() sig = Signature({"a": In(1)}).flip()
self.assertEqual(repr(sig), "Signature({'a': In(1)}).flip()") self.assertEqual(repr(sig), "Signature({'a': In(1)}).flip()")
def test_getattr_setattr(self): def test_getsetdelattr(self):
class S(Signature): class S(Signature):
def __init__(self): def __init__(self):
super().__init__({}) super().__init__({})
@ -539,12 +539,61 @@ class FlippedSignatureTestCase(unittest.TestCase):
def f(self2): def f(self2):
self.assertIsInstance(self2, FlippedSignature) self.assertIsInstance(self2, FlippedSignature)
return "f()" return "f()"
sig = S() sig = S()
fsig = sig.flip() fsig = sig.flip()
self.assertEqual(fsig.x, 1) self.assertEqual(fsig.x, 1)
self.assertEqual(fsig.f(), "f()") self.assertEqual(fsig.f(), "f()")
fsig.y = 2 fsig.y = 2
self.assertEqual(sig.y, 2) self.assertEqual(sig.y, 2)
del fsig.y
self.assertFalse(hasattr(sig, "y"))
def test_getsetdelattr_property(self):
class S(Signature):
def __init__(self):
super().__init__({})
self.x_get_type = None
self.x_set_type = None
self.x_set_val = None
self.x_del_type = None
@property
def x(self):
self.x_get_type = type(self)
@x.setter
def x(self, val):
self.x_set_type = type(self)
self.x_set_val = val
@x.deleter
def x(self):
self.x_del_type = type(self)
sig = S()
fsig = sig.flip()
fsig.x
fsig.x = 1
del fsig.x
# Tests both attribute access through the descriptor, and attribute setting without one!
self.assertEqual(sig.x_get_type, type(fsig))
self.assertEqual(sig.x_set_type, type(fsig))
self.assertEqual(sig.x_set_val, 1)
self.assertEqual(sig.x_del_type, type(fsig))
def test_classmethod(self):
x_type = None
class S(Signature):
@classmethod
def x(cls):
nonlocal x_type
x_type = cls
sig = S({})
fsig = sig.flip()
fsig.x()
self.assertEqual(x_type, S)
class InterfaceTestCase(unittest.TestCase): class InterfaceTestCase(unittest.TestCase):
@ -564,8 +613,8 @@ class FlippedInterfaceTestCase(unittest.TestCase):
r"^flipped\(<.+?\.Interface object at .+>\)$") r"^flipped\(<.+?\.Interface object at .+>\)$")
self.assertIs(flipped(tintf), intf) self.assertIs(flipped(tintf), intf)
def test_getattr_setattr(self): def test_getsetdelattr(self):
class I(Interface): class I:
signature = Signature({}) signature = Signature({})
def __init__(self): def __init__(self):
@ -574,27 +623,82 @@ class FlippedInterfaceTestCase(unittest.TestCase):
def f(self2): def f(self2):
self.assertIsInstance(self2, FlippedInterface) self.assertIsInstance(self2, FlippedInterface)
return "f()" return "f()"
intf = I() intf = I()
tintf = flipped(intf) fintf = flipped(intf)
self.assertEqual(tintf.x, 1) self.assertEqual(fintf.x, 1)
self.assertEqual(tintf.f(), "f()") self.assertEqual(fintf.f(), "f()")
tintf.y = 2 fintf.y = 2
self.assertEqual(intf.y, 2) self.assertEqual(intf.y, 2)
del fintf.y
self.assertFalse(hasattr(intf, "y"))
def test_getsetdelattr_property(self):
class I:
signature = Signature({})
def __init__(self):
self.x_get_type = None
self.x_set_type = None
self.x_set_val = None
self.x_del_type = None
@property
def x(self):
self.x_get_type = type(self)
@x.setter
def x(self, val):
self.x_set_type = type(self)
self.x_set_val = val
@x.deleter
def x(self):
self.x_del_type = type(self)
intf = I()
fintf = flipped(intf)
fintf.x
fintf.x = 1
del fintf.x
# Tests both attribute access through the descriptor, and attribute setting without one!
self.assertEqual(intf.x_get_type, type(fintf))
self.assertEqual(intf.x_set_type, type(fintf))
self.assertEqual(intf.x_set_val, 1)
self.assertEqual(intf.x_del_type, type(fintf))
def test_classmethod(self):
x_type = None
class I:
signature = Signature({})
def __init__(self):
pass
@classmethod
def x(cls):
nonlocal x_type
x_type = cls
intf = I()
fintf = flipped(intf)
fintf.x()
self.assertEqual(x_type, I)
def test_flipped_wrong(self): def test_flipped_wrong(self):
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
r"^flipped\(\) can only flip an interface object, not Signature\({}\)$"): r"^flipped\(\) can only flip an interface object, not Signature\({}\)$"):
flipped(Signature({})) flipped(Signature({}))
def test_create_subclass_flipped(self): def test_create_subclass_flipped(self):
class CustomInterface(Interface): class CustomInterface(Interface):
def custom_method(self): def custom_method(self):
return 69 return 69
class CustomSignature(Signature): class CustomSignature(Signature):
def create(self, *, path=()): def create(self, *, path=()):
return CustomInterface(self, path=path) return CustomInterface(self, path=path)
flipped_interface = CustomSignature({}).flip().create() flipped_interface = CustomSignature({}).flip().create()
self.assertTrue(hasattr(flipped_interface, "custom_method")) self.assertTrue(hasattr(flipped_interface, "custom_method"))
@ -630,6 +734,10 @@ class FlippedInterfaceTestCase(unittest.TestCase):
self.assertEqual(ifsub.g.members["h"].flow, In) self.assertEqual(ifsub.g.members["h"].flow, In)
self.assertEqual(flipped(ifsub).g.members["h"].flow, In) self.assertEqual(flipped(ifsub).g.members["h"].flow, In)
# This should be a no-op! That requires hooking ``__setattr__``.
flipped(ifsub).a = flipped(ifsub).a
self.assertEqual(ifsub.a.signature.members["f"].flow, In)
class ConnectTestCase(unittest.TestCase): class ConnectTestCase(unittest.TestCase):
def test_arg_handles_and_signature_attr(self): def test_arg_handles_and_signature_attr(self):