Skip to content

Commit 579dccf

Browse files
committed
wip
1 parent 6af7e05 commit 579dccf

File tree

7 files changed

+189
-45
lines changed

7 files changed

+189
-45
lines changed

amaranth/lib/meta.py

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
from abc import abstractmethod, ABCMeta
2-
from collections.abc import Mapping
3-
from urllib.parse import urlparse
4-
5-
import jsonschema
62

73

84
__all__ = ["Annotation"]
@@ -13,72 +9,76 @@ class Annotation(metaclass=ABCMeta):
139
1410
A container for metadata that can be retrieved from a :class:`~amaranth.lib.wiring.Signature`
1511
object. Annotation instances can be exported as JSON objects, whose structure is defined using
16-
the `JSON Schema <https://json-schema.org>`_ language.
17-
18-
Schema URLs
19-
-----------
20-
21-
An ``Annotation`` schema must have a ``"$id"`` property, which holds an URL that serves as its
22-
unique identifier. The suggested format of this URL is:
23-
24-
<protocol>://<domain>/schema/<package>/<version>/<path>.json
25-
26-
where:
27-
* ``<domain>`` is a domain name registered to the person or entity defining the annotation;
28-
* ``<package>`` is the name of the Python package providing the ``Annotation`` subclass;
29-
* ``<version>`` is the version of the aforementioned package;
30-
* ``<path>`` is a non-empty string specific to the annotation.
31-
32-
Attributes
33-
----------
34-
schema : :class`Mapping`
35-
Annotation schema.
12+
the `JSON Schema`_ language.
3613
"""
3714

38-
schema = property(abstractmethod(lambda: None)) # :nocov:
15+
#: :class:`dict`: Schema of this annotation, expressed in the `JSON Schema`_ language.
16+
#:
17+
#: Subclasses of :class:`Annotation` must implement this class attribute.
18+
schema = {}
3919

4020
def __init_subclass__(cls, **kwargs):
4121
super().__init_subclass__(**kwargs)
42-
if not isinstance(cls.schema, Mapping):
22+
23+
if not isinstance(cls.schema, dict):
4324
raise TypeError(f"Annotation schema must be a dict, not {cls.schema!r}")
4425

4526
# The '$id' keyword is optional in JSON schemas, but we require it.
4627
if "$id" not in cls.schema:
4728
raise ValueError(f"'$id' keyword is missing from Annotation schema: {cls.schema}")
48-
jsonschema.Draft202012Validator.check_schema(cls.schema)
29+
30+
try:
31+
import jsonschema
32+
jsonschema.Draft202012Validator.check_schema(cls.schema)
33+
except ImportError:
34+
# Amaranth was installed in some weird way and doesn't have jsonschema installed,
35+
# despite it being a mandatory dependency. The schema will eventually get checked
36+
# by the CI, so ignore the error here.
37+
pass # :nocov:
4938

5039
@property
5140
@abstractmethod
5241
def origin(self):
53-
"""Annotation origin.
42+
"""The Python object described by this :class:`Annotation` instance.
5443
55-
The Python object described by this :class:`Annotation` instance.
44+
Subclasses of :class:`Annotation` must implement this property.
5645
"""
5746
pass # :nocov:
5847

5948
@abstractmethod
6049
def as_json(self):
61-
"""Translate to JSON.
50+
"""Convert to a JSON representation.
51+
52+
Subclasses of :class:`Annotation` must implement this property.
53+
54+
The JSON representation returned by this method must adhere to :data:`schema` and pass
55+
validation by :meth:`validate`.
6256
6357
Returns
6458
-------
65-
:class:`Mapping`
66-
A JSON representation of this :class:`Annotation` instance.
59+
:class:`dict`
60+
JSON representation of this annotation, expressed in Python primitive types
61+
(:class:`dict`, :class:`list`, :class:`str`, :class:`int`, :class:`bool`).
6762
"""
6863
pass # :nocov:
6964

7065
@classmethod
7166
def validate(cls, instance):
72-
"""Validate a JSON object.
67+
"""Validate a JSON representation against :attr:`schema`.
7368
74-
Parameters
75-
----------
76-
instance : :class:`Mapping`
77-
The JSON object to validate.
69+
Arguments
70+
---------
71+
instance : :class:`dict`
72+
The JSON representation to validate, either previously returned by :meth:`as_json`
73+
or retrieved from an external source.
7874
7975
Raises
8076
------
8177
:exc:`jsonschema.exceptions.ValidationError`
82-
If `instance` doesn't comply with :attr:`Annotation.schema`.
78+
If :py:`instance` doesn't comply with :attr:`Annotation.schema`.
8379
"""
80+
import jsonschema
8481
jsonschema.validate(instance, schema=cls.schema)
82+
83+
def __repr__(self):
84+
return f"<{type(self).__module__}.{type(self).__qualname__} for {self.origin!r}>"

amaranth/lib/wiring.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ class Signature(metaclass=SignatureMeta):
672672
An interface object is a Python object that has a :py:`signature` attribute containing
673673
a :class:`Signature` object, as well as an attribute for every member of its signature.
674674
Signatures and interface objects are tightly linked: an interface object can be created out
675-
of a signature, and the signature is used when :func:`connect` ing two interface objects
675+
of a signature, and the signature is used when :func:`connect`\\ ing two interface objects
676676
together. See the :ref:`introduction to interfaces <wiring-intro1>` for a more detailed
677677
explanation of why this is useful.
678678
@@ -720,7 +720,7 @@ def annotations(self, obj, /):
720720
721721
Returns
722722
-------
723-
iterator of :class:`meta.Annotation`
723+
iterable of :class:`Annotation`
724724
"""
725725
return ()
726726

docs/conf.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
sys.path.insert(0, os.path.abspath("."))
33

44
import time
5-
import amaranth
5+
from importlib.metadata import version as package_version
6+
67

78
project = "Amaranth language & toolchain"
8-
version = amaranth.__version__.replace(".editable", "")
9+
version = package_version('amaranth').replace(".editable", "")
910
release = version.split("+")[0]
1011
copyright = time.strftime("2020—%Y, Amaranth project contributors")
1112

@@ -24,7 +25,10 @@
2425

2526
root_doc = "cover"
2627

27-
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
28+
intersphinx_mapping = {
29+
"python": ("https://docs.python.org/3", None),
30+
"jsonschema": (f"https://python-jsonschema.readthedocs.io/en/v{package_version('jsonschema')}/", None),
31+
}
2832

2933
todo_include_todos = True
3034

docs/stdlib.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Standard library
33

44
The :mod:`amaranth.lib` module, also known as the standard library, provides modules that falls into one of the three categories:
55

6-
1. Modules that will used by essentially all idiomatic Amaranth code, and are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), and :mod:`amaranth.lib.wiring` (interfaces and components).
6+
1. Modules that will used by essentially all idiomatic Amaranth code, or which are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), :mod:`amaranth.lib.wiring` (interfaces and components), and :mod:`amaranth.lib.meta` (metadata).
77
2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.cdc`.
88
3. Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:`amaranth.lib.coding`, :mod:`amaranth.lib.fifo`, and :mod:`amaranth.lib.crc`.
99

@@ -17,6 +17,7 @@ The Amaranth standard library is separate from the Amaranth language: everything
1717
stdlib/enum
1818
stdlib/data
1919
stdlib/wiring
20+
stdlib/meta
2021
stdlib/cdc
2122
stdlib/coding
2223
stdlib/fifo

docs/stdlib/meta.rst

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
Metadata
2+
########
3+
4+
.. py:module:: amaranth.lib.meta
5+
6+
The :mod:`amaranth.lib.meta` module provides a way to annotate objects in an Amaranth design and
7+
exchange these annotations with external tools in a standardized format.
8+
9+
.. _JSON Schema: https://json-schema.org
10+
11+
.. testsetup::
12+
13+
from amaranth import *
14+
from amaranth.lib import wiring
15+
from amaranth.lib.wiring import In, Out
16+
17+
18+
Introduction
19+
------------
20+
21+
Many Amaranth designs stay entirely within the Amaranth ecosystem, using the facilities it provides
22+
to define, test, and build hardware. In this case, the design is available for exploration using
23+
Python code, and metadata is not necessary. However, if an Amaranth design needs to fit into
24+
an existing ecosystem, or, conversely, to integrate components developed for another ecosystem,
25+
metadata can be used to exchange structured information about the design.
26+
27+
Consider a simple :ref:`component <wiring>`:
28+
29+
.. testcode::
30+
31+
class Adder(wiring.Component):
32+
a: In(unsigned(32))
33+
b: In(unsigned(32))
34+
o: Out(unsigned(33))
35+
36+
def elaborate(self, platform):
37+
m = Module()
38+
m.d.comb += self.o.eq(self.a + self.b)
39+
return m
40+
41+
..
42+
TODO: link to Verilog backend doc when we have it
43+
44+
While it can be easily converted to Verilog, external tools will find the interface of
45+
the resulting module opaque unless they parse its Verilog source (a difficult and unrewarding task),
46+
or are provided with a description of it. Components can describe their signature with JSON-based
47+
metadata:
48+
49+
.. doctest::
50+
51+
>>> adder = Adder()
52+
>>> adder.metadata # doctest: +ELLIPSIS
53+
<amaranth.lib.wiring.ComponentMetadata for <Adder object at ...>>
54+
>>> adder.metadata.as_json() # doctest: +SKIP
55+
{
56+
'interface': {
57+
'members': {
58+
'a': {
59+
'type': 'port',
60+
'name': 'a',
61+
'dir': 'in',
62+
'width': 32,
63+
'signed': False,
64+
'reset': '0'
65+
},
66+
'b': {
67+
'type': 'port',
68+
'name': 'b',
69+
'dir': 'in',
70+
'width': 32,
71+
'signed': False,
72+
'reset': '0'
73+
},
74+
'o': {
75+
'type': 'port',
76+
'name': 'o',
77+
'dir': 'out',
78+
'width': 33,
79+
'signed': False,
80+
'reset': '0'
81+
}
82+
},
83+
'annotations': {}
84+
}
85+
}
86+
87+
.. testcode::
88+
:hidden:
89+
90+
# The way doctest requires this object to be formatted is truly hideous, even with +NORMALIZE_WHITESPACE.
91+
assert adder.metadata.as_json() == {'interface': {'members': {'a': {'type': 'port', 'name': 'a', 'dir': 'in', 'width': 32, 'signed': False, 'reset': '0'}, 'b': {'type': 'port', 'name': 'b', 'dir': 'in', 'width': 32, 'signed': False, 'reset': '0'}, 'o': {'type': 'port', 'name': 'o', 'dir': 'out', 'width': 33, 'signed': False, 'reset': '0'}}, 'annotations': {}}}
92+
93+
94+
All metadata in Amaranth must adhere to a schema in the `JSON Schema`_ language, which is integral
95+
to its definition, and can be used to validate the generated JSON:
96+
97+
.. doctest::
98+
99+
>>> wiring.ComponentMetadata.validate(adder.metadata.as_json())
100+
101+
102+
Defining annotations
103+
--------------------
104+
105+
.. todo:: Write this.
106+
107+
108+
Publishing schemas
109+
------------------
110+
111+
.. todo:: Write this
112+
113+
An ``Annotation`` schema must have a ``"$id"`` property, which holds an URL that serves as its
114+
unique identifier. The suggested format of this URL is:
115+
116+
<protocol>://<domain>/schema/<package>/<version>/<path>.json
117+
118+
where:
119+
* ``<domain>`` is a domain name registered to the person or entity defining the annotation;
120+
* ``<package>`` is the name of the Python package providing the ``Annotation`` subclass;
121+
* ``<version>`` is the version of the aforementioned package;
122+
* ``<path>`` is a non-empty string specific to the annotation.
123+
124+
125+
Reference
126+
---------
127+
128+
.. todo::
129+
130+
Write this.
131+
132+
.. autoclass:: Annotation
133+
:no-members:
134+
:members: validate, origin, as_json
135+
136+
.. autoattribute:: schema
137+
:annotation: = { "$id": "...", ... }

docs/stdlib/wiring.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _wiring:
2+
13
Interfaces and connections
24
##########################
35

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ license = {file = "LICENSE.txt"}
1515
requires-python = "~=3.8"
1616
dependencies = [
1717
"importlib_resources; python_version<'3.9'", # for amaranth._toolchain.yosys
18+
"jsonschema~=4.20.0", # for amaranth.lib.meta
1819
"pyvcd>=0.2.2,<0.5", # for amaranth.sim.pysim
1920
"Jinja2~=3.0", # for amaranth.build
20-
"jsonschema~=4.20.0", # for amaranth.lib.meta
2121
]
2222

2323
[project.optional-dependencies]

0 commit comments

Comments
 (0)