diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c41a0cacb..f56769001 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,6 +16,8 @@ jobs: - '3.10' - '3.11' - '3.12' + - '3.13' + - '3.14' - 'pypy-3.8' - 'pypy-3.9' - 'pypy-3.10' @@ -23,7 +25,7 @@ jobs: allow-failure: - false include: - - python-version: '3.13-dev' + - python-version: '3.15-dev' allow-failure: true continue-on-error: ${{ matrix.allow-failure }} name: 'test (${{ matrix.python-version }})' diff --git a/amaranth/cli.py b/amaranth/cli.py index 87ff10e2f..0fd351ec8 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, @@ -48,11 +48,11 @@ def main_runner(parser, args, design, platform=None, name="top", ports=()): fragment = Fragment.get(design, platform) 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") @@ -63,7 +63,8 @@ def main_runner(parser, args, design, platform=None, name="top", ports=()): if generate_type == "v": output = verilog.convert(fragment, 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 87624f0a4..d49f1c9cc 100644 --- a/amaranth/lib/data.py +++ b/amaranth/lib/data.py @@ -3,6 +3,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 * @@ -66,6 +70,9 @@ def width(self): """ return Shape.cast(self.shape).width + def __hash__(self): + return hash((self.shape, self.offset)) + def __eq__(self, other): """Compare fields. @@ -167,6 +174,11 @@ def as_shape(self): """ return unsigned(self.size) + def __hash__(self): + if not hasattr(self, "_Layout__hash"): + setattr(self, "_Layout__hash", hash((self.size, frozenset(iter(self))))) + return self.__hash + def __eq__(self, other): """Compare layouts. @@ -1124,6 +1136,9 @@ def __len__(self): f"`len()` can only be used on constants of array layout, not {self.__layout!r}") return self.__layout.length + def __hash__(self): + return hash((self.__target, self.__layout)) + def __eq__(self, other): if isinstance(other, View) and self.__layout == other._View__layout: return self.as_value() == other._View__target @@ -1198,7 +1213,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) @@ -1207,13 +1234,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) @@ -1224,6 +1252,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 b2a758515..8b8fc1c40 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, ValueCastable @@ -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/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py index fa063c034..95adc82fe 100644 --- a/amaranth/vendor/_gowin.py +++ b/amaranth/vendor/_gowin.py @@ -442,7 +442,7 @@ def _osc_div(self): "{{name}}.sdc": r""" // {{autogenerated}} {% for signal, frequency in platform.iter_signal_clock_constraints() -%} - create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] + create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{ "{" }}{{signal|hierarchy("/")}}{{ "}" }}] {% endfor %} {% for port, frequency in platform.iter_port_clock_constraints() -%} create_clock -name {{ "{" }}{{port.name}}{{ "}" }} -period {{1000000000/frequency}} [get_ports diff --git a/docs/changes.rst b/docs/changes.rst index 9a9ad6c34..ff58e6124 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -9,6 +9,7 @@ Documentation for past releases Documentation for past releases of the Amaranth language and toolchain is available online: +* `Amaranth 0.5.7 `_ * `Amaranth 0.5.6 `_ * `Amaranth 0.5.5 `_ * `Amaranth 0.5.4 `_ @@ -25,6 +26,12 @@ Documentation for past releases of the Amaranth language and toolchain is availa * `Amaranth 0.3 `_ +Version 0.5.8 +============= + +Updated to address compatibility with Python 3.14. + + Version 0.5.7 ============= diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py index bf2583eeb..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 @@ -74,6 +78,9 @@ def test_immutable(self): with self.assertRaises(AttributeError): data.Field(1, 0).offset = 1 + def test_hash(self): + hash(data.Field(unsigned(2), 1)) + class StructLayoutTestCase(FHDLTestCase): def test_construct(self): @@ -520,6 +527,9 @@ def test_signal_init(self): self.assertEqual(Signal(sl).as_value().init, 0) self.assertEqual(Signal(sl, init={"a": 0b1, "b": 0b10}).as_value().init, 5) + def test_hash(self): + hash(data.StructLayout({})) + class ViewTestCase(FHDLTestCase): def test_construct(self): @@ -1167,6 +1177,9 @@ def test_repr(self): s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2) self.assertRepr(s1, "Const(StructLayout({'a': unsigned(2)}), 2)") + def test_hash(self): + hash(data.Const(data.StructLayout({"a": unsigned(2)}), 2)) + class StructTestCase(FHDLTestCase): def test_construct(self): @@ -1310,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):