diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py index a9ae082..53bfafd 100644 --- a/amaranth/lib/wiring.py +++ b/amaranth/lib/wiring.py @@ -806,43 +806,41 @@ def connect(m, *args, **kwargs): class Component(Elaboratable): - def __init__(self): - for name in self.signature.members: - if hasattr(self, name): - raise NameError(f"Cannot initialize attribute for signature member {name!r} " - f"because an attribute with the same name already exists") - self.__dict__.update(self.signature.members.create(path=())) - - @property - def signature(self): + def __init__(self, signature=None): cls = type(self) members = {} for base in reversed(cls.mro()[:cls.mro().index(Component)]): for name, annot in base.__dict__.get("__annotations__", {}).items(): if name.startswith("_"): continue - if (annot is Value or annot is Signal or annot is Const or - (isinstance(annot, type) and issubclass(annot, ValueCastable)) or - isinstance(annot, Signature)): - if isinstance(annot, type): - annot_repr = annot.__name__ - else: - annot_repr = repr(annot) - # To suppress this warning in the rare cases where it is necessary (and naming - # the field with a leading underscore is infeasible), override the property. - warnings.warn( - message=f"Component '{cls.__module__}.{cls.__qualname__}' has " - f"an annotation '{name}: {annot_repr}', which is not " - f"a signature member; did you mean '{name}: In({annot_repr})' " - f"or '{name}: Out({annot_repr})'?", - category=SyntaxWarning, - stacklevel=2) - elif type(annot) is Member: + if type(annot) is Member: if name in members: raise SignatureError(f"Member '{name}' is redefined in {base.__module__}.{base.__qualname__}") members[name] = annot if not members: - raise NotImplementedError( - f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature member " - f"annotations") - return Signature(members) + if signature is None: + raise NotImplementedError( + f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature " + f"member annotations") + if isinstance(signature, dict): + signature = Signature(signature) + elif not isinstance(signature, Signature): + raise TypeError(f"Object {signature!r} is not a signature nor a dict") + else: + if signature is not None: + raise TypeError( + f"Signature was passed as an argument, but component " + f"'{cls.__module__}.{cls.__qualname__}' already has signature " + f"member annotations") + signature = Signature(members) + + self.__signature = signature + for name in signature.members: + if hasattr(self, name): + raise NameError(f"Cannot initialize attribute for signature member {name!r} " + f"because an attribute with the same name already exists") + self.__dict__.update(signature.members.create(path=())) + + @property + def signature(self): + return self.__signature diff --git a/docs/changes.rst b/docs/changes.rst index 34cad61..b00c40f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -64,6 +64,7 @@ Implemented RFCs .. _RFC 34: https://amaranth-lang.org/rfcs/0034-interface-rename.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 38: https://amaranth-lang.org/rfcs/0038-component-signature-immutability.html * `RFC 1`_: Aggregate data structure library @@ -85,6 +86,7 @@ Implemented RFCs * `RFC 34`_: Rename ``amaranth.lib.wiring.Interface`` to ``PureInterface`` * `RFC 35`_: Add ``ShapeLike``, ``ValueLike`` * `RFC 37`_: Make ``Signature`` immutable +* `RFC 38`_: ``Component.signature`` immutability Language changes diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py index 7aeef86..b09664c 100644 --- a/tests/test_lib_wiring.py +++ b/tests/test_lib_wiring.py @@ -711,17 +711,15 @@ class FlippedInterfaceTestCase(unittest.TestCase): def test_propagate_flipped(self): class InterfaceWithFlippedSub(Component): - signature = Signature({ - "a": In(Signature({ - "b": Out(Signature({ - "c": Out(1) - })), - "d": In(Signature({ - "e": Out(1) - })), - "f": Out(1) - })) - }) + a: In(Signature({ + "b": Out(Signature({ + "c": Out(1) + })), + "d": In(Signature({ + "e": Out(1) + })), + "f": Out(1) + })) def __init__(self): super().__init__() @@ -984,53 +982,6 @@ class ComponentTestCase(unittest.TestCase): r"with the same name already exists$"): C() - def test_missing_in_out_warning(self): - class C1(Component): - prt1 : In(1) - sig2 : Signal - - with self.assertWarnsRegex(SyntaxWarning, - r"^Component '.+\.C1' has an annotation 'sig2: Signal', which is not a signature " - r"member; did you mean 'sig2: In\(Signal\)' or 'sig2: Out\(Signal\)'\?$"): - C1().signature - - class C2(Component): - prt1 : In(1) - sig2 : Signature({}) - - with self.assertWarnsRegex(SyntaxWarning, - r"^Component '.+\.C2' has an annotation 'sig2: Signature\({}\)', which is not " - r"a signature member; did you mean 'sig2: In\(Signature\({}\)\)' or " - r"'sig2: Out\(Signature\({}\)\)'\?$"): - C2().signature - - class MockValueCastable(ValueCastable): - def shape(self): pass - @ValueCastable.lowermethod - def as_value(self): pass - - class C3(Component): - prt1 : In(1) - val2 : MockValueCastable - - with self.assertWarnsRegex(SyntaxWarning, - r"^Component '.+\.C3' has an annotation 'val2: MockValueCastable', which is not " - r"a signature member; did you mean 'val2: In\(MockValueCastable\)' or " - r"'val2: Out\(MockValueCastable\)'\?$"): - C3().signature - - def test_bug_882(self): - class PageBuffer(Component): - rand: Signature({}).flip() - other: Out(1) - - with self.assertWarnsRegex(SyntaxWarning, - r"^Component '.+\.PageBuffer' has an annotation 'rand: Signature\({}\)\.flip\(\)', " - r"which is not a signature member; did you mean " - r"'rand: In\(Signature\({}\)\.flip\(\)\)' or " - r"'rand: Out\(Signature\({}\)\.flip\(\)\)'\?$"): - PageBuffer() - def test_inherit(self): class A(Component): clk: In(1) @@ -1054,3 +1005,49 @@ class ComponentTestCase(unittest.TestCase): with self.assertRaisesRegex(SignatureError, r"^Member 'a' is redefined in .*.B$"): B() + + def test_create(self): + class C(Component): + def __init__(self, width): + super().__init__(Signature({ + "a": In(width) + })) + + c = C(2) + self.assertEqual(c.signature, Signature({"a": In(2)})) + self.assertIsInstance(c.a, Signal) + self.assertEqual(c.a.shape(), unsigned(2)) + + def test_create_dict(self): + class C(Component): + def __init__(self, width): + super().__init__({ + "a": In(width) + }) + + c = C(2) + self.assertEqual(c.signature, Signature({"a": In(2)})) + self.assertIsInstance(c.a, Signal) + self.assertEqual(c.a.shape(), unsigned(2)) + + def test_create_wrong(self): + class C(Component): + a: In(2) + + def __init__(self, width): + super().__init__(Signature({ + "a": In(width) + })) + + with self.assertRaisesRegex(TypeError, + r"^Signature was passed as an argument, but component '.*.C' already has signature member annotations$"): + C(2) + + def test_create_wrong_type(self): + class C(Component): + def __init__(self, width): + super().__init__(4) + + with self.assertRaisesRegex(TypeError, + r"^Object 4 is not a signature nor a dict$"): + C(2)