Skip to content

Commit 4ae2f3b

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

File tree

3 files changed

+45
-6
lines changed

3 files changed

+45
-6
lines changed

amaranth/lib/data.py

Lines changed: 23 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,7 +1212,19 @@ def __repr__(self):
12081212

12091213
class _AggregateMeta(ShapeCastable, type):
12101214
def __new__(metacls, name, bases, namespace):
1211-
if "__annotations__" not in namespace:
1215+
annotations = None
1216+
skipped_annotations = set()
1217+
wrapped_annotate = None
1218+
if annotationlib is not None:
1219+
if annotate := annotationlib.get_annotate_from_class_namespace(namespace):
1220+
annotations = annotationlib.call_annotate_function(
1221+
annotate, format=annotationlib.Format.VALUE)
1222+
def wrapped_annotate(format):
1223+
annos = annotationlib.call_annotate_function(annotate, format, owner=cls)
1224+
return {k: v for k, v in annos.items() if k not in skipped_annotations}
1225+
else:
1226+
annotations = namespace.get("__annotations__")
1227+
if annotations is None:
12121228
# This is a base class without its own layout. It is not shape-castable, and cannot
12131229
# be instantiated. It can be used to share behavior.
12141230
return type.__new__(metacls, name, bases, namespace)
@@ -1217,13 +1233,14 @@ def __new__(metacls, name, bases, namespace):
12171233
# be instantiated. It can also be subclassed, and used to share layout and behavior.
12181234
layout = dict()
12191235
default = dict()
1220-
for field_name in {**namespace["__annotations__"]}:
1236+
for field_name in {**annotations}:
12211237
try:
1222-
Shape.cast(namespace["__annotations__"][field_name])
1238+
Shape.cast(annotations[field_name])
12231239
except TypeError:
12241240
# Not a shape-castable annotation; leave as-is.
12251241
continue
1226-
layout[field_name] = namespace["__annotations__"].pop(field_name)
1242+
skipped_annotations.add(field_name)
1243+
layout[field_name] = annotations.pop(field_name)
12271244
if field_name in namespace:
12281245
default[field_name] = namespace.pop(field_name)
12291246
cls = type.__new__(metacls, name, bases, namespace)
@@ -1234,6 +1251,8 @@ def __new__(metacls, name, bases, namespace):
12341251
.format(", ".join(default.keys())))
12351252
cls.__layout = cls.__layout_cls(layout)
12361253
cls.__default = default
1254+
if wrapped_annotate is not None:
1255+
cls.__annotate__ = wrapped_annotate
12371256
return cls
12381257
else:
12391258
# 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)