-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement RFC 30: Component metadata.
- Loading branch information
Showing
5 changed files
with
503 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from abc import abstractmethod, ABCMeta | ||
from collections.abc import Mapping | ||
from urllib.parse import urlparse | ||
|
||
import jsonschema | ||
|
||
|
||
__all__ = ["Annotation"] | ||
|
||
|
||
class Annotation(metaclass=ABCMeta): | ||
"""Signature annotation. | ||
A container for metadata that can be attached to a :class:`~amaranth.lib.wiring.Signature`. | ||
Annotation instances can be exported as JSON objects, whose structure is defined using the | ||
`JSON Schema <https://json-schema.org>`_ language. | ||
Schema URLs and annotation names | ||
-------------------------------- | ||
An ``Annotation`` schema must have a ``"$id"`` property, which holds an URL that serves as its | ||
unique identifier. This URL should have the following format: | ||
<protocol>://<domain>/schema/<package>/<version>/<path>.json | ||
where: | ||
* ``<domain>`` is a domain name registered to the person or entity defining the annotation; | ||
* ``<package>`` is the name of the Python package providing the ``Annotation`` subclass; | ||
* ``<version>`` is the version of the aforementioned package; | ||
* ``<path>`` is a non-empty string specific to the annotation. | ||
An ``Annotation`` name must be retrievable from the ``"$id"`` URL. It is the concatenation | ||
of the following, separated by '.': | ||
* ``<domain>``, reversed (e.g. ``"org.amaranth-lang"``); | ||
* ``<package>``; | ||
* ``<path>``, split using '/' as separator. | ||
For example, ``"https://example.github.io/schema/foo/1.0/bar/baz.json"`` is the URL of an | ||
annotation whose name is ``"io.github.example.foo.bar.baz"``. | ||
Attributes | ||
---------- | ||
name : :class:`str` | ||
Annotation name. | ||
schema : :class`Mapping` | ||
Annotation schema. | ||
""" | ||
|
||
name = property(abstractmethod(lambda: None)) # :nocov: | ||
schema = property(abstractmethod(lambda: None)) # :nocov: | ||
|
||
def __init_subclass__(cls, **kwargs): | ||
super().__init_subclass__(**kwargs) | ||
if not isinstance(cls.name, str): | ||
raise TypeError(f"Annotation name must be a string, not {cls.name!r}") | ||
if not isinstance(cls.schema, Mapping): | ||
raise TypeError(f"Annotation schema must be a dict, not {cls.schema!r}") | ||
|
||
# The '$id' keyword is optional in JSON schemas, but we require it. | ||
if "$id" not in cls.schema: | ||
raise ValueError(f"'$id' keyword is missing from Annotation schema: {cls.schema}") | ||
jsonschema.Draft202012Validator.check_schema(cls.schema) | ||
|
||
parsed_id = urlparse(cls.schema["$id"]) | ||
if not parsed_id.path.startswith("/schema/"): | ||
raise ValueError(f"'$id' URL path must start with '/schema/' ('{cls.schema['$id']}')") | ||
if not parsed_id.path.endswith(".json"): | ||
raise ValueError(f"'$id' URL path must have a '.json' suffix ('{cls.schema['$id']}')") | ||
|
||
_, _, package, version, *path = parsed_id.path[:-len(".json")].split("/") | ||
parsed_name = ".".join((*reversed(parsed_id.netloc.split(".")), package, *path)) | ||
if cls.name != parsed_name: | ||
raise ValueError(f"Annotation name '{cls.name}' must be obtainable from the '$id' " | ||
f"URL ('{cls.schema['$id']}'), but does not match '{parsed_name}'") | ||
|
||
@property | ||
@abstractmethod | ||
def origin(self): | ||
"""Annotation origin. | ||
The Python object described by this :class:`Annotation` instance. | ||
""" | ||
pass # :nocov: | ||
|
||
@abstractmethod | ||
def as_json(self): | ||
"""Translate to JSON. | ||
Returns | ||
------- | ||
:class:`Mapping` | ||
A JSON representation of this :class:`Annotation` instance. | ||
""" | ||
pass # :nocov: | ||
|
||
@classmethod | ||
def validate(cls, instance): | ||
"""Validate a JSON object. | ||
Parameters | ||
---------- | ||
instance : :class:`Mapping` | ||
The JSON object to validate. | ||
Raises | ||
------ | ||
:exc:`jsonschema.exceptions.ValidationError` | ||
If `instance` doesn't comply with :attr:`Annotation.schema`. | ||
""" | ||
jsonschema.validate(instance, schema=cls.schema) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.