Repository: amaranth-lang/amaranth
Pull request #970 (2023-11-27 14:52:34)

Several minor fixes in `lib.wiring`

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
Attention: `4 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`57748a6`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/57748a66a657cdec8399a3edfa628fe5e0c7d050?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 83.76% compared to head [(`ae605fb`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 83.71%.

| [Files](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Patch % | Lines |
|---|---|---|
| [amaranth/lib/wiring.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvbGliL3dpcmluZy5weQ==) | 75.00% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #970      +/-   ##
==========================================
- Coverage   83.76%   83.71%   -0.06%     
==========================================
  Files          54       54              
  Lines        7811     7822      +11     
  Branches     1913     1918       +5     
==========================================
+ Hits         6543     6548       +5     
- Misses       1059     1063       +4     
- Partials      209      211       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/970?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


Patch:
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index 75eac1c..bb8b4cb 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -353,6 +353,12 @@ class Signature(metaclass=SignatureMeta):
     def members(self):
         return self.__members
 
+    @members.setter
+    def members(self, new_members):
+        # The setter is called when `sig.members += ...` is used.
+        if new_members is not self.__members:
+            raise AttributeError("property 'members' of 'Signature' object cannot be set")
+
     def __eq__(self, other):
         other_unflipped = other.flip() if type(other) is FlippedSignature else other
         if type(self) is type(other_unflipped) is Signature:
@@ -394,7 +400,7 @@ class Signature(metaclass=SignatureMeta):
                     dimension, *rest_of_dimensions = dimensions
                     for index in range(dimension):
                         yield from iter_dimensions(value[index], rest_of_dimensions,
-                                                   path=(path, index))
+                                                   path=(*path, index))
 
             yield from iter_dimensions(value, dimensions=member.dimensions, path=path)
 
@@ -499,9 +505,9 @@ def _gettypeattr(obj, attr):
             return cls.__dict__[attr]
         except KeyError:
             pass
-    # In case there is `__getattr__` on the metaclass, or just to generate an `AttributeError` with
-    # the standard message.
-    return type(obj).attr
+    # Call `getattr` In case there is `__getattr__` on the metaclass, or just to generate
+    # an `AttributeError` with the standard message.
+    return getattr(type(obj), attr)
 
 
 # To simplify implementation and reduce API surface area `FlippedSignature` is made final. This
@@ -518,6 +524,11 @@ class FlippedSignature:
     def members(self):
         return FlippedSignatureMembers(self.__unflipped.members)
 
+    @members.setter
+    def members(self, new_members):
+        if new_members.flip() is not self.__unflipped.members:
+            raise AttributeError("property 'members' of 'FlippedSignature' object cannot be set")
+
     def __eq__(self, other):
         if type(other) is FlippedSignature:
             # Trivial case.
@@ -550,10 +561,16 @@ class FlippedSignature:
             return getattr(self.__unflipped, name)
 
     def __setattr__(self, name, value):
-        try: # descriptor first
-            _gettypeattr(self.__unflipped, name).__set__(self, value)
-        except AttributeError:
-            setattr(self.__unflipped, name, value)
+        if name == "members":
+            # Although `sig.flip().members` does not call `__getattr__` but directly invokes
+            # the descriptor of the `FlippedSignature.members` property, `sig.flip().members +=`
+            # does call `__setattr__`, and this must be special-cased for the setter to work.
+            FlippedSignature.members.__set__(self, value)
+        else:
+            try: # descriptor first
+                _gettypeattr(self.__unflipped, name).__set__(self, value)
+            except AttributeError:
+                setattr(self.__unflipped, name, value)
 
     def __delattr__(self, name):
         try: # descriptor first
@@ -594,7 +611,7 @@ class FlippedInterface:
 
     # FIXME: document this logic
 
-    # See the note in ``FlippedSignature``. In addition, these accessors also handle flipping of
+    # See the note in `FlippedSignature`. In addition, these accessors also handle flipping of
     # an interface member.
 
     def __getattr__(self, name):
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index 43d1abf..163b9fa 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -1,5 +1,10 @@
 /* Some of our section titles are looong */
-.wy-menu-vertical { width: 340px; }
+.wy-nav-side, .wy-side-scroll, .wy-menu-vertical { width: 340px; }
+.wy-side-nav-search { width: 325px; }
+.wy-nav-content-wrap { margin-left: 340px; }
+
+/* We don't have a version picker widget */
+.wy-nav-side { padding-bottom: 0; }
 
 /* Many of our diagnostics are even longer */
 .rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre { white-space: pre-wrap; }


Test patch:
diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py
index 75a0583..c08f5dd 100644
--- a/tests/test_lib_wiring.py
+++ b/tests/test_lib_wiring.py
@@ -374,6 +374,18 @@ class SignatureTestCase(unittest.TestCase):
                 r"^Cannot add members to a frozen signature$"):
             sig.members += {"b": Out(1)}
 
+    def test_members_plus_equal(self):
+        sig = Signature({})
+        # This invokes the setter of Signature.members.
+        sig.members += {"a": In(1)}
+        self.assertIn("a", sig.members)
+
+    def test_members_equal_wrong(self):
+        sig = Signature({})
+        with self.assertRaisesRegex(AttributeError,
+                r"property 'members' of 'Signature' object cannot be set"):
+            sig.members = SignatureMembers({})
+
     def assertFlattenedSignature(self, actual, expected):
         for (a_path, a_member, a_value), (b_path, b_member, b_value) in zip(actual, expected):
             self.assertEqual(a_path, b_path)
@@ -385,8 +397,8 @@ class SignatureTestCase(unittest.TestCase):
         intf = sig.create()
         self.assertFlattenedSignature(sig.flatten(intf), [
             (("a",), In(1), intf.a),
-            ((("b",), 0), Out(2), intf.b[0]),
-            ((("b",), 1), Out(2), intf.b[1])
+            (("b", 0), Out(2), intf.b[0]),
+            (("b", 1), Out(2), intf.b[1])
         ])
 
     def test_flatten_sig(self):
@@ -595,6 +607,18 @@ class FlippedSignatureTestCase(unittest.TestCase):
         fsig.x()
         self.assertEqual(x_type, S)
 
+    def test_members_plus_equal(self):
+        sig = Signature({})
+        # This invokes the setter of FlippedSignature.members.
+        sig.flip().members += {"a": In(1)}
+        self.assertIn("a", sig.members)
+
+    def test_members_equal_wrong(self):
+        sig = Signature({})
+        with self.assertRaisesRegex(AttributeError,
+                r"property 'members' of 'FlippedSignature' object cannot be set"):
+            sig.flip().members = SignatureMembers({})
+
 
 class InterfaceTestCase(unittest.TestCase):
     pass