diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 02f7967c7..64120011d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,14 +16,13 @@ jobs: - '3.11' - '3.12' - '3.13' + - '3.14' - 'pypy-3.9' - 'pypy-3.10' - 'pypy-3.11' allow-failure: - false include: - - python-version: '3.14' - allow-failure: true - python-version: '3.15-dev' allow-failure: true continue-on-error: ${{ matrix.allow-failure }} diff --git a/amaranth/cli.py b/amaranth/cli.py index 741970ca7..47c9c8693 100644 --- a/amaranth/cli.py +++ b/amaranth/cli.py @@ -22,16 +22,16 @@ def main_parser(parser=None): p_generate.add_argument("--no-src", dest="emit_src", default=True, action="store_false", help="suppress generation of source location attributes") p_generate.add_argument("generate_file", - metavar="FILE", type=argparse.FileType("w"), nargs="?", + metavar="FILE", type=str, nargs="?", help="write generated code to FILE") p_simulate = p_action.add_parser( "simulate", help="simulate the design") p_simulate.add_argument("-v", "--vcd-file", - metavar="VCD-FILE", type=argparse.FileType("w"), + metavar="VCD-FILE", type=str, help="write execution trace to VCD-FILE") p_simulate.add_argument("-w", "--gtkw-file", - metavar="GTKW-FILE", type=argparse.FileType("w"), + metavar="GTKW-FILE", type=str, help="write GTKWave configuration to GTKW-FILE") p_simulate.add_argument("-p", "--period", dest="sync_period", metavar="TIME", type=float, default=1e-6, @@ -47,11 +47,11 @@ def main_runner(parser, args, design, platform=None, name="top", ports=None): if args.action == "generate": generate_type = args.generate_type if generate_type is None and args.generate_file: - if args.generate_file.name.endswith(".il"): + if args.generate_file.endswith(".il"): generate_type = "il" - if args.generate_file.name.endswith(".cc"): + if args.generate_file.endswith(".cc"): generate_type = "cc" - if args.generate_file.name.endswith(".v"): + if args.generate_file.endswith(".v"): generate_type = "v" if generate_type is None: parser.error("Unable to auto-detect language, specify explicitly with -t/--type") @@ -62,7 +62,8 @@ def main_runner(parser, args, design, platform=None, name="top", ports=None): if generate_type == "v": output = verilog.convert(design, platform=platform, name=name, ports=ports, emit_src=args.emit_src) if args.generate_file: - args.generate_file.write(output) + with open(args.generate_file, "w") as f: + f.write(output) else: print(output) diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py index f6e512548..c33771f9c 100644 --- a/amaranth/lib/data.py +++ b/amaranth/lib/data.py @@ -2,6 +2,10 @@ from collections.abc import Mapping, Sequence import warnings import operator +try: + import annotationlib # py3.14+ +except ImportError: + annotationlib = None # py3.13- from amaranth._utils import final from amaranth.hdl import * @@ -1208,7 +1212,19 @@ def __repr__(self): class _AggregateMeta(ShapeCastable, type): def __new__(metacls, name, bases, namespace): - if "__annotations__" not in namespace: + annotations = None + skipped_annotations = set() + wrapped_annotate = None + if annotationlib is not None: + if annotate := annotationlib.get_annotate_from_class_namespace(namespace): + annotations = annotationlib.call_annotate_function( + annotate, format=annotationlib.Format.VALUE) + def wrapped_annotate(format): + annos = annotationlib.call_annotate_function(annotate, format, owner=cls) + return {k: v for k, v in annos.items() if k not in skipped_annotations} + else: + annotations = namespace.get("__annotations__") + if annotations is None: # This is a base class without its own layout. It is not shape-castable, and cannot # be instantiated. It can be used to share behavior. return type.__new__(metacls, name, bases, namespace) @@ -1217,13 +1233,14 @@ def __new__(metacls, name, bases, namespace): # be instantiated. It can also be subclassed, and used to share layout and behavior. layout = dict() default = dict() - for field_name in {**namespace["__annotations__"]}: + for field_name in {**annotations}: try: - Shape.cast(namespace["__annotations__"][field_name]) + Shape.cast(annotations[field_name]) except TypeError: # Not a shape-castable annotation; leave as-is. continue - layout[field_name] = namespace["__annotations__"].pop(field_name) + skipped_annotations.add(field_name) + layout[field_name] = annotations.pop(field_name) if field_name in namespace: default[field_name] = namespace.pop(field_name) cls = type.__new__(metacls, name, bases, namespace) @@ -1234,6 +1251,8 @@ def __new__(metacls, name, bases, namespace): .format(", ".join(default.keys()))) cls.__layout = cls.__layout_cls(layout) cls.__default = default + if wrapped_annotate is not None: + cls.__annotate__ = wrapped_annotate return cls else: # This is a class that has a base class with a layout and annotations. Such a class diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py index 99b380915..ef0a7499d 100644 --- a/amaranth/lib/wiring.py +++ b/amaranth/lib/wiring.py @@ -2,6 +2,10 @@ import enum import re import warnings +try: + import annotationlib # py3.14+ +except ImportError: + annotationlib = None # py3.13- from .. import tracer from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value @@ -1669,7 +1673,14 @@ def __init__(self, signature=None, *, src_loc_at=0): cls = type(self) members = {} for base in reversed(cls.mro()[:cls.mro().index(Component)]): - for name, annot in base.__dict__.get("__annotations__", {}).items(): + annotations = None + if annotationlib is not None: + if annotate := annotationlib.get_annotate_from_class_namespace(base.__dict__): + annotations = annotationlib.call_annotate_function( + annotate, format=annotationlib.Format.VALUE) + if annotations is None: + annotations = base.__dict__.get("__annotations__", {}) + for name, annot in annotations.items(): if name.startswith("_"): continue if type(annot) is Member: diff --git a/amaranth/tracer.py b/amaranth/tracer.py index 4c059d0f1..36b6ae745 100644 --- a/amaranth/tracer.py +++ b/amaranth/tracer.py @@ -58,8 +58,8 @@ def get_var_name(depth=2, default=_raise_exception): return code.co_cellvars[imm] else: return code.co_freevars[imm - len(code.co_cellvars)] - elif opc in ("LOAD_GLOBAL", "LOAD_NAME", "LOAD_ATTR", "LOAD_FAST", "LOAD_DEREF", - "DUP_TOP", "BUILD_LIST", "CACHE", "COPY"): + elif opc in ("LOAD_GLOBAL", "LOAD_NAME", "LOAD_ATTR", "LOAD_FAST", "LOAD_FAST_BORROW", + "LOAD_DEREF", "DUP_TOP", "BUILD_LIST", "CACHE", "COPY"): imm = 0 index += 2 else: diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py index d34987978..28aee6d97 100644 --- a/tests/test_lib_data.py +++ b/tests/test_lib_data.py @@ -1,6 +1,10 @@ from enum import Enum import operator from unittest import TestCase +try: + import annotationlib # py3.14+ +except ImportError: + annotationlib = None # py3.13- from amaranth.hdl import * from amaranth.lib import data @@ -1319,7 +1323,11 @@ class S(data.Struct): c: str = "x" self.assertEqual(data.Layout.cast(S), data.StructLayout({"a": unsigned(1)})) - self.assertEqual(S.__annotations__, {"b": int, "c": str}) + if annotationlib is not None: + annotations = annotationlib.get_annotations(S, format=annotationlib.Format.VALUE) + else: + annotations = S.__annotations__ + self.assertEqual(annotations, {"b": int, "c": str}) self.assertEqual(S.c, "x") def test_signal_like(self):