Skip to content

Commit e29dfd4

Browse files
committed
lib.{data,wiring}: add Python 3.14 annotation support.
1 parent 7692e31 commit e29dfd4

File tree

3 files changed

+44
-6
lines changed

3 files changed

+44
-6
lines changed

amaranth/lib/data.py

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

711
from amaranth._utils import final
812
from amaranth.hdl import *
@@ -1209,7 +1213,19 @@ def __repr__(self):
12091213

12101214
class _AggregateMeta(ShapeCastable, type):
12111215
def __new__(metacls, name, bases, namespace):
1212-
if "__annotations__" not in namespace:
1216+
annotations = None
1217+
skipped_annotations = set()
1218+
wrapped_annotate = None
1219+
if annotationlib is not None:
1220+
if annotate := annotationlib.get_annotate_from_class_namespace(namespace):
1221+
annotations = annotationlib.call_annotate_function(
1222+
annotate, format=annotationlib.Format.VALUE)
1223+
def wrapped_annotate(format):
1224+
annos = annotationlib.call_annotate_function(annotate, format, owner=cls)
1225+
return {k: v for k, v in annos.items() if k not in skipped_annotations}
1226+
else:
1227+
annotations = namespace.get("__annotations__")
1228+
if annotations is None:
12131229
# This is a base class without its own layout. It is not shape-castable, and cannot
12141230
# be instantiated. It can be used to share behavior.
12151231
return type.__new__(metacls, name, bases, namespace)
@@ -1218,13 +1234,14 @@ def __new__(metacls, name, bases, namespace):
12181234
# be instantiated. It can also be subclassed, and used to share layout and behavior.
12191235
layout = dict()
12201236
default = dict()
1221-
for field_name in {**namespace["__annotations__"]}:
1237+
for field_name in {**annotations}:
12221238
try:
1223-
Shape.cast(namespace["__annotations__"][field_name])
1239+
Shape.cast(annotations[field_name])
12241240
except TypeError:
12251241
# Not a shape-castable annotation; leave as-is.
12261242
continue
1227-
layout[field_name] = namespace["__annotations__"].pop(field_name)
1243+
skipped_annotations.add(field_name)
1244+
layout[field_name] = annotations.pop(field_name)
12281245
if field_name in namespace:
12291246
default[field_name] = namespace.pop(field_name)
12301247
cls = type.__new__(metacls, name, bases, namespace)
@@ -1235,6 +1252,8 @@ def __new__(metacls, name, bases, namespace):
12351252
.format(", ".join(default.keys())))
12361253
cls.__layout = cls.__layout_cls(layout)
12371254
cls.__default = default
1255+
if wrapped_annotate is not None:
1256+
cls.__annotate__ = wrapped_annotate
12381257
return cls
12391258
else:
12401259
# 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, ValueCastable
@@ -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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from enum import Enum
22
import operator
33
from unittest import TestCase
4+
try:
5+
import annotationlib # py3.14+
6+
except ImportError:
7+
annotationlib = None # py3.13-
48

59
from amaranth.hdl import *
610
from amaranth.lib import data
@@ -1319,7 +1323,11 @@ class S(data.Struct):
13191323
c: str = "x"
13201324

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

13251333
def test_signal_like(self):

0 commit comments

Comments
 (0)