Implement RFC 30: Component metadata.

Co-authored-by: Catherine <whitequark@whitequark.org>
This commit is contained in:
Jean-François Nguyen 2023-12-01 17:00:48 +01:00 committed by Catherine
parent 1d2b9c309e
commit 496432edaa
12 changed files with 1024 additions and 9 deletions

87
tests/test_lib_meta.py Normal file
View file

@ -0,0 +1,87 @@
import unittest
from amaranth import *
from amaranth.lib.meta import *
class AnnotationTestCase(unittest.TestCase):
def test_init_subclass(self):
class MyAnnotation(Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
"type": "string",
}
@property
def origin(self):
return "foo"
@property
def as_json(self):
return "foo"
self.assertRegex(repr(MyAnnotation()), r"<.+\.MyAnnotation for 'foo'>")
def test_init_subclass_wrong_schema(self):
with self.assertRaisesRegex(TypeError, r"Annotation schema must be a dict, not 'foo'"):
class MyAnnotation(Annotation):
schema = "foo"
def test_init_subclass_schema_missing_id(self):
with self.assertRaisesRegex(InvalidSchema, r"'\$id' keyword is missing from Annotation schema: {}"):
class MyAnnotation(Annotation):
schema = {}
def test_init_subclass_schema_missing_schema(self):
with self.assertRaises(InvalidSchema):
class MyAnnotation(Annotation):
schema = {
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
}
def test_init_subclass_schema_error(self):
with self.assertRaises(InvalidSchema):
class MyAnnotation(Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
"type": "foo",
}
def test_validate(self):
class MyAnnotation(Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
"type": "object",
"properties": {
"foo": {
"enum": [ "bar" ],
},
},
"additionalProperties": False,
"required": [
"foo",
],
}
MyAnnotation.validate({"foo": "bar"})
def test_validate_error(self):
class MyAnnotation(Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
"type": "object",
"properties": {
"foo": {
"enum": [ "bar" ],
},
},
"additionalProperties": False,
"required": [
"foo",
],
}
with self.assertRaises(InvalidAnnotation):
MyAnnotation.validate({"foo": "baz"})

View file

@ -8,8 +8,9 @@ from amaranth.lib import data, enum
from amaranth.lib.wiring import Flow, In, Out, Member
from amaranth.lib.wiring import SignatureError, SignatureMembers, FlippedSignatureMembers
from amaranth.lib.wiring import Signature, FlippedSignature, PureInterface, FlippedInterface
from amaranth.lib.wiring import Component
from amaranth.lib.wiring import Component, ComponentMetadata, InvalidMetadata
from amaranth.lib.wiring import ConnectionError, connect, flipped
from amaranth.lib.meta import Annotation
class FlowTestCase(unittest.TestCase):
@ -336,6 +337,11 @@ class SignatureTestCase(unittest.TestCase):
sig = Signature({"a": In(1)})
self.assertEqual(sig.members, SignatureMembers({"a": In(1)}))
def test_annotations_empty(self):
sig = Signature({"a": In(1)})
iface = PureInterface(sig)
self.assertEqual(sig.annotations(iface), ())
def test_eq(self):
self.assertEqual(Signature({"a": In(1)}),
Signature({"a": In(1)}))
@ -1156,3 +1162,174 @@ class ComponentTestCase(unittest.TestCase):
with self.assertRaisesRegex(TypeError,
r"^Object 4 is not a signature nor a dict$"):
C(2)
def test_metadata_origin(self):
class A(Component):
clk: In(1)
a = A()
self.assertIsInstance(a.metadata, ComponentMetadata)
self.assertIs(a.metadata.origin, a)
class ComponentMetadataTestCase(unittest.TestCase):
def test_as_json(self):
class Annotation1(Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schema/foo/0.1/bar.json",
"type": "object",
"properties": {
"hello": { "type": "boolean" },
},
}
def origin(self):
return object()
def as_json(self):
instance = { "hello": True }
self.validate(instance)
return instance
class Signature1(Signature):
def __init__(self):
super().__init__({
"i": In(unsigned(8), init=42).array(2).array(3),
"o": Out(signed(4))
})
def annotations(self, obj):
return (*Signature.annotations(self, obj), Annotation1())
class Signature2(Signature):
def __init__(self):
super().__init__({
"clk": In(1),
"foo": Out(Signature1()).array(4),
"oof": In(Signature1())
})
def annotations(self, obj):
return (*Signature.annotations(self, obj), Annotation1())
class A(Component):
def __init__(self):
super().__init__(Signature2())
metadata = ComponentMetadata(A())
self.assertEqual(metadata.as_json(), {
"interface": {
"members": {
"clk": {
"type": "port",
"name": "clk",
"dir": "in",
"width": 1,
"signed": False,
"init": "0",
},
"foo": [{
"type": "interface",
"members": {
"i": [[{
"type": "port",
"name": f"foo__{x}__i__{y}__{z}",
"dir": "in",
"width": 8,
"signed": False,
"init": "42",
} for z in range(2)] for y in range(3)],
"o": {
"type": "port",
"name": f"foo__{x}__o",
"dir": "out",
"width": 4,
"signed": True,
"init": "0",
},
},
"annotations": {
"https://example.com/schema/foo/0.1/bar.json": {
"hello": True,
},
},
} for x in range(4)],
"oof": {
"type": "interface",
"members": {
"i": [[{
"type": "port",
"name": f"oof__i__{y}__{z}",
"dir": "out",
"width": 8,
"signed": False,
"init": "42",
} for z in range(2)] for y in range(3)],
"o": {
"type": "port",
"name": "oof__o",
"dir": "in",
"width": 4,
"signed": True,
"init": "0",
},
},
"annotations": {
"https://example.com/schema/foo/0.1/bar.json": {
"hello": True,
},
},
},
},
"annotations": {
"https://example.com/schema/foo/0.1/bar.json": {
"hello": True,
},
},
},
})
def test_validate(self):
ComponentMetadata.validate({
"interface": {
"members": {
"i": {
"type": "port",
"name": "i",
"dir": "in",
"width": 1,
"signed": False,
"init": "0",
},
"o": {
"type": "port",
"name": "o",
"dir": "out",
"width": 1,
"signed": False,
"init": "0",
},
},
"annotations": {
"https://example.com/schema/foo/0/foo.json": {
"foo": True,
},
},
},
})
def test_validate_error(self):
with self.assertRaises(InvalidMetadata):
ComponentMetadata.validate({
"interface": {
"members": {
"foo": True,
},
"annotations": {},
},
})
def test_wrong_origin(self):
with self.assertRaisesRegex(TypeError, r"Origin must be a component, not 'foo'"):
ComponentMetadata("foo")