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:
parent
79adbed313
commit
74e613b49d
|
@ -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})"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue