Skip to content

Commit 65a84ba

Browse files
committed
lib.{data,wiring}: add Python 3.14 annotation support.
1 parent dbd1f72 commit 65a84ba

File tree

3 files changed

+46
-6
lines changed

3 files changed

+46
-6
lines changed

amaranth/lib/data.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from collections.abc import Mapping, Sequence
33
import warnings
44
import operator
5+
try:
6+
import annotationlib # py3.14+
7+
except ImportError:
8+
annotationlib = None # py3.13-
59

610
from amaranth._utils import final
711
from amaranth.hdl import *
@@ -1208,22 +1212,36 @@ def __repr__(self):
12081212

12091213
class _AggregateMeta(ShapeCastable, type):
12101214
def __new__(metacls, name, bases, namespace):
1211-
if "__annotations__" not in namespace:
1215+
if "__annotations__" not in namespace and "__annotate_func__" not in namespace:
12121216
# This is a base class without its own layout. It is not shape-castable, and cannot
12131217
# be instantiated. It can be used to share behavior.
12141218
return type.__new__(metacls, name, bases, namespace)
12151219
elif all(not hasattr(base, "_AggregateMeta__layout") for base in bases):
1220+
annotations = None
1221+
skipped_annotations = set()
1222+
wrapped_annotate = None
1223+
if annotationlib is not None:
1224+
if annotate := annotationlib.get_annotate_from_class_namespace(namespace):
1225+
annotations = annotationlib.call_annotate_function(
1226+
annotate, format=annotationlib.Format.VALUE)
1227+
def wrapped_annotate(format):
1228+
annos = annotationlib.call_annotate_function(annotate, format, owner=cls)
1229+
return {k: v for k, v in annos.items() if k not in skipped_annotations}
1230+
if annotations is None:
1231+
annotations = namespace.get("__annotations__", {})
1232+
12161233
# This is a leaf class with its own layout. It is shape-castable and can
12171234
# be instantiated. It can also be subclassed, and used to share layout and behavior.
12181235
layout = dict()
12191236
default = dict()
1220-
for field_name in {**namespace["__annotations__"]}:
1237+
for field_name in {**annotations}:
12211238
try:
1222-
Shape.cast(namespace["__annotations__"][field_name])
1239+
Shape.cast(annotations[field_name])
12231240
except TypeError:
12241241
# Not a shape-castable annotation; leave as-is.
12251242
continue
1226-
layout[field_name] = namespace["__annotations__"].pop(field_name)
1243+
skipped_annotations.add(field_name)
1244+
layout[field_name] = annotations.pop(field_name)
12271245
if field_name in namespace:
12281246
default[field_name] = namespace.pop(field_name)
12291247
cls = type.__new__(metacls, name, bases, namespace)
@@ -1234,6 +1252,8 @@ def __new__(metacls, name, bases, namespace):
12341252
.format(", ".join(default.keys())))
12351253
cls.__layout = cls.__layout_cls(layout)
12361254
cls.__default = default
1255+
if wrapped_annotate is not None:
1256+
cls.__annotate__ = wrapped_annotate
12371257
return cls
12381258
else:
12391259
# This is a class that has a base class with a layout and annotations. Such a class

amaranth/lib/wiring.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
import enum
33
import re
44
import warnings
5+
try:
6+
import annotationlib # py3.14+
7+
except ImportError:
8+
annotationlib = None # py3.13-
59

610
from .. import tracer
711
from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value
@@ -1669,7 +1673,14 @@ def __init__(self, signature=None, *, src_loc_at=0):
16691673
cls = type(self)
16701674
members = {}
16711675
for base in reversed(cls.mro()[:cls.mro().index(Component)]):
1672-
for name, annot in base.__dict__.get("__annotations__", {}).items():
1676+
annotations = None
1677+
if annotationlib is not None:
1678+
if annotate := annotationlib.get_annotate_from_class_namespace(base.__dict__):
1679+
annotations = annotationlib.call_annotate_function(
1680+
annotate, format=annotationlib.Format.VALUE)
1681+
if annotations is None:
1682+
annotations = base.__dict__.get("__annotations__", {})
1683+
for name, annot in annotations.items():
16731684
if name.startswith("_"):
16741685
continue
16751686
if type(annot) is Member:

tests/test_lib_data.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from enum import Enum
2+
import sys
23
import operator
34
from unittest import TestCase
5+
try:
6+
import annotationlib # py3.14+
7+
except ImportError:
8+
annotationlib = None # py3.13-
49

510
from amaranth.hdl import *
611
from amaranth.lib import data
@@ -1319,7 +1324,11 @@ class S(data.Struct):
13191324
c: str = "x"
13201325

13211326
self.assertEqual(data.Layout.cast(S), data.StructLayout({"a": unsigned(1)}))
1322-
self.assertEqual(S.__annotations__, {"b": int, "c": str})
1327+
if annotationlib is not None:
1328+
annotations = annotationlib.get_annotations(S, format=annotationlib.Format.VALUE)
1329+
else:
1330+
annotations = S.__annotations__
1331+
self.assertEqual(annotations, {"b": int, "c": str})
13231332
self.assertEqual(S.c, "x")
13241333

13251334
def test_signal_like(self):

0 commit comments

Comments
 (0)