Implement RFC 37: Make `Signature` immutable.

Fixes #995.
This commit is contained in:
Wanda 2023-12-11 19:18:53 +01:00 committed by Catherine
parent b9c2404f22
commit 6ad0d21cc9
3 changed files with 51 additions and 175 deletions

View file

@ -151,8 +151,12 @@ class SignatureError(Exception):
class SignatureMembers(Mapping): class SignatureMembers(Mapping):
def __init__(self, members=()): def __init__(self, members=()):
self._dict = dict() self._dict = dict()
self._frozen = False for name, member in dict(members).items():
self += members self._check_name(name)
if type(member) is not Member:
raise TypeError(f"Value {member!r} must be a member; "
f"did you mean In({member!r}) or Out({member!r})?")
self._dict[name] = member
def flip(self): def flip(self):
return FlippedSignatureMembers(self) return FlippedSignatureMembers(self)
@ -179,16 +183,7 @@ class SignatureMembers(Mapping):
return self._dict[name] return self._dict[name]
def __setitem__(self, name, member): def __setitem__(self, name, member):
self._check_name(name) raise SignatureError("Members cannot be added to a signature once constructed")
if name in self._dict:
raise SignatureError(f"Member '{name}' already exists in the signature and cannot "
f"be replaced")
if type(member) is not Member:
raise TypeError(f"Assigned value {member!r} must be a member; "
f"did you mean In({member!r}) or Out({member!r})?")
if self._frozen:
raise SignatureError("Cannot add members to a frozen signature")
self._dict[name] = member
def __delitem__(self, name): def __delitem__(self, name):
raise SignatureError("Members cannot be removed from a signature") raise SignatureError("Members cannot be removed from a signature")
@ -199,21 +194,6 @@ class SignatureMembers(Mapping):
def __len__(self): def __len__(self):
return len(self._dict) return len(self._dict)
def __iadd__(self, members):
for name, member in dict(members).items():
self[name] = member
return self
@property
def frozen(self):
return self._frozen
def freeze(self):
self._frozen = True
for member in self.values():
if member.is_signature:
member.signature.freeze()
def flatten(self, *, path=()): def flatten(self, *, path=()):
for name, member in self.items(): for name, member in self.items():
yield ((*path, name), member) yield ((*path, name), member)
@ -244,8 +224,7 @@ class SignatureMembers(Mapping):
return attrs return attrs
def __repr__(self): def __repr__(self):
frozen_repr = ".freeze()" if self._frozen else "" return f"SignatureMembers({self._dict})"
return f"SignatureMembers({self._dict}){frozen_repr}"
@final @final
@ -277,17 +256,6 @@ class FlippedSignatureMembers(Mapping):
def __len__(self): def __len__(self):
return self.__unflipped.__len__() return self.__unflipped.__len__()
def __iadd__(self, members):
self.__unflipped.__iadd__({name: member.flip() for name, member in members.items()})
return self
@property
def frozen(self):
return self.__unflipped.frozen
def freeze(self):
self.__unflipped.freeze()
# These methods do not access instance variables and so their implementation can be shared # These methods do not access instance variables and so their implementation can be shared
# between the normal and the flipped member collections. # between the normal and the flipped member collections.
flatten = SignatureMembers.flatten flatten = SignatureMembers.flatten
@ -358,12 +326,6 @@ class Signature(metaclass=SignatureMeta):
def members(self): def members(self):
return self.__members return self.__members
@members.setter
def members(self, new_members):
# The setter is called when `sig.members += ...` is used.
if new_members is not self.__members:
raise AttributeError("property 'members' of 'Signature' object cannot be set")
def __eq__(self, other): def __eq__(self, other):
other_unflipped = other.flip() if type(other) is FlippedSignature else other other_unflipped = other.flip() if type(other) is FlippedSignature else other
if type(self) is type(other_unflipped) is Signature: if type(self) is type(other_unflipped) is Signature:
@ -374,14 +336,6 @@ class Signature(metaclass=SignatureMeta):
# usually be overridden in a derived class. # usually be overridden in a derived class.
return self is other return self is other
@property
def frozen(self):
return self.members.frozen
def freeze(self):
self.members.freeze()
return self
def flatten(self, obj): def flatten(self, obj):
for name, member in self.members.items(): for name, member in self.members.items():
path = (name,) path = (name,)
@ -544,11 +498,6 @@ class FlippedSignature:
def members(self): def members(self):
return FlippedSignatureMembers(self.__unflipped.members) return FlippedSignatureMembers(self.__unflipped.members)
@members.setter
def members(self, new_members):
if new_members.flip() is not self.__unflipped.members:
raise AttributeError("property 'members' of 'FlippedSignature' object cannot be set")
def __eq__(self, other): def __eq__(self, other):
if type(other) is FlippedSignature: if type(other) is FlippedSignature:
# Trivial case. # Trivial case.
@ -561,10 +510,8 @@ class FlippedSignature:
# in infinite recursion. # in infinite recursion.
return NotImplemented return NotImplemented
# These methods do not access instance variables and so their implementation can be shared # This method does not access instance variables and so its implementation can be shared
# between the normal and the flipped member collections. # between the normal and the flipped member collections.
frozen = Signature.frozen
freeze = Signature.freeze
is_compliant = Signature.is_compliant is_compliant = Signature.is_compliant
# FIXME: document this logic # FIXME: document this logic
@ -581,16 +528,10 @@ class FlippedSignature:
return getattr(self.__unflipped, name) return getattr(self.__unflipped, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == "members": try: # descriptor first
# Although `sig.flip().members` does not call `__getattr__` but directly invokes _gettypeattr(self.__unflipped, name).__set__(self, value)
# the descriptor of the `FlippedSignature.members` property, `sig.flip().members +=` except AttributeError:
# does call `__setattr__`, and this must be special-cased for the setter to work. setattr(self.__unflipped, name, value)
FlippedSignature.members.__set__(self, value)
else:
try: # descriptor first
_gettypeattr(self.__unflipped, name).__set__(self, value)
except AttributeError:
setattr(self.__unflipped, name, value)
def __delattr__(self, name): def __delattr__(self, name):
try: # descriptor first try: # descriptor first
@ -703,7 +644,7 @@ def connect(m, *args, **kwargs):
reasons_as_string = "".join("\n- " + reason for reason in reasons) reasons_as_string = "".join("\n- " + reason for reason in reasons)
raise ConnectionError(f"Argument {handle!r} does not match its signature:" + raise ConnectionError(f"Argument {handle!r} does not match its signature:" +
reasons_as_string) reasons_as_string)
signatures[handle] = obj.signature.freeze() signatures[handle] = obj.signature
# Collate signatures and build connections. # Collate signatures and build connections.
flattens = {handle: signature.members.flatten() flattens = {handle: signature.members.flatten()
@ -875,8 +816,8 @@ class Component(Elaboratable):
@property @property
def signature(self): def signature(self):
cls = type(self) cls = type(self)
signature = Signature({}) members = {}
for base in cls.mro()[:cls.mro().index(Component)]: for base in reversed(cls.mro()[:cls.mro().index(Component)]):
for name, annot in base.__dict__.get("__annotations__", {}).items(): for name, annot in base.__dict__.get("__annotations__", {}).items():
if name.startswith("_"): if name.startswith("_"):
continue continue
@ -897,9 +838,11 @@ class Component(Elaboratable):
category=SyntaxWarning, category=SyntaxWarning,
stacklevel=2) stacklevel=2)
elif type(annot) is Member: elif type(annot) is Member:
signature.members[name] = annot if name in members:
if not signature.members: raise SignatureError(f"Member '{name}' is redefined in {base.__module__}.{base.__qualname__}")
members[name] = annot
if not members:
raise NotImplementedError( raise NotImplementedError(
f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature member " f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature member "
f"annotations") f"annotations")
return signature return Signature(members)

View file

@ -63,6 +63,7 @@ Implemented RFCs
.. _RFC 31: https://amaranth-lang.org/rfcs/0031-enumeration-type-safety.html .. _RFC 31: https://amaranth-lang.org/rfcs/0031-enumeration-type-safety.html
.. _RFC 34: https://amaranth-lang.org/rfcs/0034-interface-rename.html .. _RFC 34: https://amaranth-lang.org/rfcs/0034-interface-rename.html
.. _RFC 35: https://amaranth-lang.org/rfcs/0035-shapelike-valuelike.html .. _RFC 35: https://amaranth-lang.org/rfcs/0035-shapelike-valuelike.html
.. _RFC 37: https://amaranth-lang.org/rfcs/0037-make-signature-immutable.html
* `RFC 1`_: Aggregate data structure library * `RFC 1`_: Aggregate data structure library
@ -83,6 +84,7 @@ Implemented RFCs
* `RFC 31`_: Enumeration type safety * `RFC 31`_: Enumeration type safety
* `RFC 34`_: Rename ``amaranth.lib.wiring.Interface`` to ``PureInterface`` * `RFC 34`_: Rename ``amaranth.lib.wiring.Interface`` to ``PureInterface``
* `RFC 35`_: Add ``ShapeLike``, ``ValueLike`` * `RFC 35`_: Add ``ShapeLike``, ``ValueLike``
* `RFC 37`_: Make ``Signature`` immutable
Language changes Language changes

View file

@ -182,29 +182,9 @@ class SignatureMembersTestCase(unittest.TestCase):
def test_setitem(self): def test_setitem(self):
members = SignatureMembers() members = SignatureMembers()
members["a"] = In(1)
self.assertEqual(members["a"], In(1))
def test_setitem_existing(self):
members = SignatureMembers({"a": In(1)})
with self.assertRaisesRegex(SignatureError, with self.assertRaisesRegex(SignatureError,
r"^Member 'a' already exists in the signature and cannot be replaced$"): r"^Members cannot be added to a signature once constructed$"):
members["a"] = Out(2) members["a"] = In(1)
def test_setitem_wrong(self):
members = SignatureMembers()
with self.assertRaisesRegex(TypeError,
r"^Member name must be a string, not 1$"):
members[1] = Out(1)
with self.assertRaisesRegex(TypeError,
r"^Assigned value 1 must be a member; did you mean In\(1\) or Out\(1\)\?$"):
members["a"] = 1
with self.assertRaisesRegex(NameError,
r"^Member name '_a' must be a valid, public Python attribute name$"):
members["_a"] = Out(1)
with self.assertRaisesRegex(NameError,
r"^Member name cannot be 'signature'$"):
members["signature"] = Out(1)
def test_delitem(self): def test_delitem(self):
members = SignatureMembers() members = SignatureMembers()
@ -216,7 +196,7 @@ class SignatureMembersTestCase(unittest.TestCase):
members = SignatureMembers() members = SignatureMembers()
self.assertEqual(list(iter(members)), []) self.assertEqual(list(iter(members)), [])
self.assertEqual(len(members), 0) self.assertEqual(len(members), 0)
members["a"] = In(1) members = SignatureMembers({"a": In(1)})
self.assertEqual(list(iter(members)), ["a"]) self.assertEqual(list(iter(members)), ["a"])
self.assertEqual(len(members), 1) self.assertEqual(len(members), 1)
@ -226,35 +206,6 @@ class SignatureMembersTestCase(unittest.TestCase):
self.assertEqual(list(iter(SignatureMembers({"b": In(1), "a": Out(1)}))), self.assertEqual(list(iter(SignatureMembers({"b": In(1), "a": Out(1)}))),
["a", "b"]) ["a", "b"])
def test_iadd(self):
members = SignatureMembers()
members += {"a": In(1)}
members += [("b", Out(1))]
self.assertEqual(members, SignatureMembers({"a": In(1), "b": Out(1)}))
def test_freeze(self):
members = SignatureMembers({"a": In(1)})
self.assertEqual(members.frozen, False)
members.freeze()
self.assertEqual(members.frozen, True)
with self.assertRaisesRegex(SignatureError,
r"^Cannot add members to a frozen signature$"):
members += {"b": Out(1)}
def test_freeze_rec(self):
sig = Signature({})
members = SignatureMembers({
"a": In(1),
"s": Out(sig)
})
self.assertEqual(members.frozen, False)
self.assertEqual(sig.frozen, False)
self.assertEqual(sig.members.frozen, False)
members.freeze()
self.assertEqual(members.frozen, True)
self.assertEqual(sig.frozen, True)
self.assertEqual(sig.members.frozen, True)
def test_flatten(self): def test_flatten(self):
sig = Signature({ sig = Signature({
"b": Out(1), "b": Out(1),
@ -307,15 +258,19 @@ class SignatureMembersTestCase(unittest.TestCase):
self.assertIsInstance(y, Signal) self.assertIsInstance(y, Signal)
self.assertEqual(members["a"][1][2].name, "members__a__1__2") self.assertEqual(members["a"][1][2].name, "members__a__1__2")
def test_create_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Value 1 must be a member; did you mean In\(1\) or Out\(1\)\?$"):
SignatureMembers({"a": 1})
def test_repr(self): def test_repr(self):
self.assertEqual(repr(SignatureMembers({})), self.assertEqual(repr(SignatureMembers({})),
"SignatureMembers({})") "SignatureMembers({})")
self.assertEqual(repr(SignatureMembers({"a": In(1)})), self.assertEqual(repr(SignatureMembers({"a": In(1)})),
"SignatureMembers({'a': In(1)})") "SignatureMembers({'a': In(1)})")
members = SignatureMembers({"b": Out(2)}) members = SignatureMembers({"b": Out(2)})
members.freeze()
self.assertEqual(repr(members), self.assertEqual(repr(members),
"SignatureMembers({'b': Out(2)}).freeze()") "SignatureMembers({'b': Out(2)})")
class FlippedSignatureMembersTestCase(unittest.TestCase): class FlippedSignatureMembersTestCase(unittest.TestCase):
@ -325,12 +280,14 @@ class FlippedSignatureMembersTestCase(unittest.TestCase):
self.assertIsInstance(fmembers, FlippedSignatureMembers) self.assertIsInstance(fmembers, FlippedSignatureMembers)
self.assertIn("a", fmembers) self.assertIn("a", fmembers)
self.assertEqual(fmembers["a"], Out(1)) self.assertEqual(fmembers["a"], Out(1))
fmembers["b"] = Out(2) members = SignatureMembers({"a": In(1), "b": In(2)})
fmembers = members.flip()
self.assertEqual(len(fmembers), 2) self.assertEqual(len(fmembers), 2)
self.assertEqual(members["b"], In(2)) self.assertEqual(fmembers["b"], Out(2))
self.assertEqual(list(fmembers), ["a", "b"]) self.assertEqual(list(fmembers), ["a", "b"])
fmembers += {"c": In(2)} members = SignatureMembers({"a": In(1), "b": In(2), "c": Out(2)})
self.assertEqual(members["c"], Out(2)) fmembers = members.flip()
self.assertEqual(fmembers["c"], In(2))
self.assertIs(fmembers.flip(), members) self.assertIs(fmembers.flip(), members)
def test_eq(self): def test_eq(self):
@ -345,14 +302,6 @@ class FlippedSignatureMembersTestCase(unittest.TestCase):
r"^Members cannot be removed from a signature$"): r"^Members cannot be removed from a signature$"):
del fmembers["a"] del fmembers["a"]
def test_freeze(self):
members = SignatureMembers({"a": In(1)})
fmembers = members.flip()
self.assertEqual(fmembers.frozen, False)
fmembers.freeze()
self.assertEqual(members.frozen, True)
self.assertEqual(fmembers.frozen, True)
def test_repr(self): def test_repr(self):
fmembers = SignatureMembers({"a": In(1)}).flip() fmembers = SignatureMembers({"a": In(1)}).flip()
self.assertEqual(repr(fmembers), "SignatureMembers({'a': In(1)}).flip()") self.assertEqual(repr(fmembers), "SignatureMembers({'a': In(1)}).flip()")
@ -369,25 +318,9 @@ class SignatureTestCase(unittest.TestCase):
self.assertNotEqual(Signature({"a": In(1)}), self.assertNotEqual(Signature({"a": In(1)}),
Signature({"a": Out(1)})) Signature({"a": Out(1)}))
def test_freeze(self):
sig = Signature({"a": In(1)})
self.assertEqual(sig.frozen, False)
sig.freeze()
self.assertEqual(sig.frozen, True)
with self.assertRaisesRegex(SignatureError,
r"^Cannot add members to a frozen signature$"):
sig.members += {"b": Out(1)}
def test_members_plus_equal(self):
sig = Signature({})
# This invokes the setter of Signature.members.
sig.members += {"a": In(1)}
self.assertIn("a", sig.members)
def test_members_equal_wrong(self): def test_members_equal_wrong(self):
sig = Signature({}) sig = Signature({})
with self.assertRaisesRegex(AttributeError, with self.assertRaises(AttributeError):
r"property 'members' of 'Signature' object cannot be set"):
sig.members = SignatureMembers({}) sig.members = SignatureMembers({})
def assertFlattenedSignature(self, actual, expected): def assertFlattenedSignature(self, actual, expected):
@ -643,16 +576,9 @@ class FlippedSignatureTestCase(unittest.TestCase):
fsig.x() fsig.x()
self.assertEqual(x_type, S) self.assertEqual(x_type, S)
def test_members_plus_equal(self):
sig = Signature({})
# This invokes the setter of FlippedSignature.members.
sig.flip().members += {"a": In(1)}
self.assertIn("a", sig.members)
def test_members_equal_wrong(self): def test_members_equal_wrong(self):
sig = Signature({}) sig = Signature({})
with self.assertRaisesRegex(AttributeError, with self.assertRaises(AttributeError):
r"property 'members' of 'FlippedSignature' object cannot be set"):
sig.flip().members = SignatureMembers({}) sig.flip().members = SignatureMembers({})
@ -843,12 +769,6 @@ class ConnectTestCase(unittest.TestCase):
r"- 'arg0' does not have an attribute 'a'$"): r"- 'arg0' does not have an attribute 'a'$"):
connect(m, NS(signature=Signature({"a": In(1)}))) connect(m, NS(signature=Signature({"a": In(1)})))
def test_signature_freeze(self):
m = Module()
intf = NS(signature=Signature({}))
connect(m, intf)
self.assertTrue(intf.signature.frozen)
def test_member_missing(self): def test_member_missing(self):
m = Module() m = Module()
with self.assertRaisesRegex(ConnectionError, with self.assertRaisesRegex(ConnectionError,
@ -1123,3 +1043,14 @@ class ComponentTestCase(unittest.TestCase):
c = C() c = C()
self.assertEqual(c.signature, Signature({"clk": In(1), "rst": In(1)})) self.assertEqual(c.signature, Signature({"clk": In(1), "rst": In(1)}))
def test_inherit_wrong(self):
class A(Component):
a: In(1)
class B(A):
a: Out(1)
with self.assertRaisesRegex(SignatureError,
r"^Member 'a' is redefined in .*<locals>.B$"):
B()