Repository: cocotb/cocotb

Issue:
Deprecate __setattr__ overload for setting a handle value
We already have two other ways to accomplish this and the [Zen of Python](https://www.python.org/dev/peps/pep-0020/) states "There should be one-- and preferably only one --obvious way to do it."

```python
handle.value = value
handle <= value
```

The `__setattr__` syntax depends on there being at least one additional level of hierarchy to be able to set a signal; and this can result in unintended behavior with newbies to Python. I have seen this happen at least 3 times.

```python
dut.handle = value  # okay
handle = value  # doh!
```
 
I think this should behavior should be deprecated.


Discussion:
I agree with having only one way of doing things, and that the current behavior is very error-prone. (handle = value is something I've seen often as well).

> I think this should behavior should be deprecated.

Which of the two behaviors?
The third behavior, which @ktbarrett didn't give an example of but otherwise described

Resolving patch:
diff --git a/cocotb/handle.py b/cocotb/handle.py
--- a/cocotb/handle.py
+++ b/cocotb/handle.py
@@ -315,6 +315,10 @@ def __setattr__(self, name, value):
         # then try handles
         sub = self.__get_sub_handle_by_name(name)
         if sub is not None:
+            warnings.warn(
+                "Setting values on handles using the ``dut.handle = value`` syntax is deprecated."
+                "Instead use the ``handle.value = value`` or ``handle <= value`` syntax",
+                DeprecationWarning, stacklevel=2)
             sub.value = value
             return
 
@@ -582,6 +586,10 @@ def __init__(self, handle, path):
     def __setitem__(self, index, value):
         """Provide transparent assignment to indexed array handles."""
         self[index].value = value
+        warnings.warn(
+            "Setting values on handles using the ``dut.handle[i] = value`` syntax is deprecated."
+            "Instead use the ``handle[i].value = value`` or ``handle[i] <= value`` syntax",
+            DeprecationWarning, stacklevel=2)
 
     def __getitem__(self, index):
         if isinstance(index, slice):


Test patch:
diff --git a/tests/test_cases/issue_120/issue_120.py b/tests/test_cases/issue_120/issue_120.py
--- a/tests/test_cases/issue_120/issue_120.py
+++ b/tests/test_cases/issue_120/issue_120.py
@@ -6,9 +6,9 @@
 
 
 async def send_data(dut):
-    dut.stream_in_valid = 1
+    dut.stream_in_valid <= 1
     await RisingEdge(dut.clk)
-    dut.stream_in_valid = 0
+    dut.stream_in_valid <= 0
 
 
 async def monitor(dut):
diff --git a/tests/test_cases/test_array/test_array.py b/tests/test_cases/test_array/test_array.py
--- a/tests/test_cases/test_array/test_array.py
+++ b/tests/test_cases/test_array/test_array.py
@@ -101,15 +101,15 @@ async def test_read_write(dut):
             _check_logic(tlog, dut.const_cmplx[2].b[1], 0xFF)
             _check_logic(tlog, dut.const_cmplx[2].b[2], 0xFF)
 
-    dut.select_in         = 2
+    dut.select_in         <= 2
 
     await Timer(10, "ns")
 
     tlog.info("Writing the signals!!!")
-    dut.sig_logic         = 1
-    dut.sig_logic_vec     = 0xCC
-    dut.sig_t2 = [0xCC, 0xDD, 0xEE, 0xFF]
-    dut.sig_t4 = [
+    dut.sig_logic         <= 1
+    dut.sig_logic_vec     <= 0xCC
+    dut.sig_t2 <= [0xCC, 0xDD, 0xEE, 0xFF]
+    dut.sig_t4 <= [
         [0x00, 0x11, 0x22, 0x33],
         [0x44, 0x55, 0x66, 0x77],
         [0x88, 0x99, 0xAA, 0xBB],
@@ -117,23 +117,23 @@ async def test_read_write(dut):
     ]
 
     if cocotb.LANGUAGE in ["vhdl"]:
-        dut.sig_bool          = 1
-        dut.sig_int           = 5000
-        dut.sig_real          = 22.54
-        dut.sig_char          = ord('Z')
-        dut.sig_str           = "Testing"
-        dut.sig_rec.a         = 1
-        dut.sig_rec.b[0]      = 0x01
-        dut.sig_rec.b[1]      = 0x23
-        dut.sig_rec.b[2]      = 0x45
-        dut.sig_cmplx[0].a    = 0
-        dut.sig_cmplx[0].b[0] = 0x67
-        dut.sig_cmplx[0].b[1] = 0x89
-        dut.sig_cmplx[0].b[2] = 0xAB
-        dut.sig_cmplx[1].a    = 1
-        dut.sig_cmplx[1].b[0] = 0xCD
-        dut.sig_cmplx[1].b[1] = 0xEF
-        dut.sig_cmplx[1].b[2] = 0x55
+        dut.sig_bool          <= 1
+        dut.sig_int           <= 5000
+        dut.sig_real          <= 22.54
+        dut.sig_char          <= ord('Z')
+        dut.sig_str           <= "Testing"
+        dut.sig_rec.a         <= 1
+        dut.sig_rec.b[0]      <= 0x01
+        dut.sig_rec.b[1]      <= 0x23
+        dut.sig_rec.b[2]      <= 0x45
+        dut.sig_cmplx[0].a    <= 0
+        dut.sig_cmplx[0].b[0] <= 0x67
+        dut.sig_cmplx[0].b[1] <= 0x89
+        dut.sig_cmplx[0].b[2] <= 0xAB
+        dut.sig_cmplx[1].a    <= 1
+        dut.sig_cmplx[1].b[0] <= 0xCD
+        dut.sig_cmplx[1].b[1] <= 0xEF
+        dut.sig_cmplx[1].b[2] <= 0x55
 
     await Timer(10, "ns")
 
@@ -167,17 +167,17 @@ async def test_read_write(dut):
         _check_logic(tlog, dut.port_cmplx_out[1].b[2], 0x55)
 
     tlog.info("Writing a few signal sub-indices!!!")
-    dut.sig_logic_vec[2]     = 0
+    dut.sig_logic_vec[2]     <= 0
     if cocotb.LANGUAGE in ["vhdl"] or not (cocotb.SIM_NAME.lower().startswith(("ncsim", "xmsim")) or
                                            (cocotb.SIM_NAME.lower().startswith("riviera") and
                                             cocotb.SIM_VERSION.startswith(("2016.06", "2016.10", "2017.02")))):
-        dut.sig_t6[1][3][2]      = 1
-        dut.sig_t6[0][2][7]      = 0
+        dut.sig_t6[1][3][2]      <= 1
+        dut.sig_t6[0][2][7]      <= 0
 
     if cocotb.LANGUAGE in ["vhdl"]:
-        dut.sig_str[2]           = ord('E')
-        dut.sig_rec.b[1][7]      = 1
-        dut.sig_cmplx[1].b[1][0] = 0
+        dut.sig_str[2]           <= ord('E')
+        dut.sig_rec.b[1][7]      <= 1
+        dut.sig_cmplx[1].b[1][0] <= 0
 
     await Timer(10, "ns")
 
diff --git a/tests/test_cases/test_array_simple/test_array_simple.py b/tests/test_cases/test_array_simple/test_array_simple.py
--- a/tests/test_cases/test_array_simple/test_array_simple.py
+++ b/tests/test_cases/test_array_simple/test_array_simple.py
@@ -39,19 +39,6 @@ async def test_1dim_array_handles(dut):
     _check_value(tlog, dut.array_3_downto_0, [0x70, 0x60, 0x50, 0x40])
     _check_value(tlog, dut.array_0_to_3    , [0x30, 0x20, 0x10, 0x00])
 
-    # Set values through HierarchyObject.__setattr__ method on 'dut' handle
-    dut.array_7_downto_4 = [0x00, 0x11, 0x22, 0x33]
-    dut.array_4_to_7     = [0x44, 0x55, 0x66, 0x77]
-    dut.array_3_downto_0 = [0x88, 0x99, 0xAA, 0xBB]
-    dut.array_0_to_3     = [0xCC, 0xDD, 0xEE, 0xFF]
-
-    await Timer(1000, 'ns')
-
-    _check_value(tlog, dut.array_7_downto_4, [0x00, 0x11, 0x22, 0x33])
-    _check_value(tlog, dut.array_4_to_7    , [0x44, 0x55, 0x66, 0x77])
-    _check_value(tlog, dut.array_3_downto_0, [0x88, 0x99, 0xAA, 0xBB])
-    _check_value(tlog, dut.array_0_to_3    , [0xCC, 0xDD, 0xEE, 0xFF])
-
 
 @cocotb.test(skip=cocotb.SIM_NAME.lower().startswith(("icarus", "ghdl")))
 async def test_ndim_array_handles(dut):
@@ -69,16 +56,6 @@ async def test_ndim_array_handles(dut):
 
     _check_value(tlog, dut.array_2d, [[0xF0, 0xE0, 0xD0, 0xC0], [0xB0, 0xA0, 0x90, 0x80]])
 
-    # Set values through HierarchyObject.__setattr__ method on 'dut' handle
-    dut.array_2d = [
-        [0x00, 0x11, 0x22, 0x33],
-        [0x44, 0x55, 0x66, 0x77]
-    ]
-
-    await Timer(1000, 'ns')
-
-    _check_value(tlog, dut.array_2d, [[0x00, 0x11, 0x22, 0x33], [0x44, 0x55, 0x66, 0x77]])
-
 
 @cocotb.test()
 async def test_1dim_array_indexes(dut):
@@ -119,21 +96,6 @@ async def test_1dim_array_indexes(dut):
     _check_value(tlog, dut.array_0_to_3[1]    , 0x7A)
     _check_value(tlog, dut.array_0_to_3[3]    , 0x42)
 
-    # Set sub-handle values through NonHierarchyIndexableObject.__setitem__
-    dut.array_7_downto_4[7] = 0x77
-    dut.array_4_to_7[4]     = 0x44
-    dut.array_3_downto_0[0] = 0x00
-    dut.array_0_to_3[1]     = 0x11
-    dut.array_0_to_3[3]     = 0x33
-
-    await Timer(1000, 'ns')
-
-    _check_value(tlog, dut.array_7_downto_4[7], 0x77)
-    _check_value(tlog, dut.array_4_to_7[4]    , 0x44)
-    _check_value(tlog, dut.array_3_downto_0[0], 0x00)
-    _check_value(tlog, dut.array_0_to_3[1]    , 0x11)
-    _check_value(tlog, dut.array_0_to_3[3]    , 0x33)
-
 
 @cocotb.test(skip=cocotb.SIM_NAME.lower().startswith(("icarus", "ghdl")))
 async def test_ndim_array_indexes(dut):
@@ -165,16 +127,6 @@ async def test_ndim_array_indexes(dut):
     _check_value(tlog, dut.array_2d[1][30], 0xAD)
     _check_value(tlog, dut.array_2d[1][28], 0xEF)
 
-    # Set sub-handle values through NonHierarchyIndexableObject.__setitem__
-    dut.array_2d[0]     = [0xBA, 0xBE, 0xCA, 0xFE]
-    dut.array_2d[1][29] = 0x12
-
-    await Timer(1000, 'ns')
-
-    _check_value(tlog, dut.array_2d[0][31], 0xBA)
-    _check_value(tlog, dut.array_2d[0][30], 0xBE)
-    _check_value(tlog, dut.array_2d[1]    , [0xDE, 0xAD, 0x12, 0xEF])
-
 
 @cocotb.test(expect_error=AttributeError if cocotb.SIM_NAME.lower().startswith(("icarus", "ghdl")) else ())
 async def test_struct(dut):
@@ -204,7 +156,7 @@ async def test_exceptions(dut):
     with assert_raises(TypeError):
         dut.array_7_downto_4 <= (0xF0, 0xE0, 0xD0, 0xC0)
     with assert_raises(TypeError):
-        dut.array_4_to_7      = Exception("Exception Object")
+        dut.array_4_to_7     <= Exception("Exception Object")
     with assert_raises(ValueError):
         dut.array_3_downto_0 <= [0x70, 0x60, 0x50]
     with assert_raises(ValueError):
diff --git a/tests/test_cases/test_cocotb/test_deprecated.py b/tests/test_cases/test_cocotb/test_deprecated.py
--- a/tests/test_cases/test_cocotb/test_deprecated.py
+++ b/tests/test_cases/test_cocotb/test_deprecated.py
@@ -9,6 +9,7 @@
 from contextlib import contextmanager
 from common import assert_raises
 from typing import List
+from cocotb._sim_versions import IcarusVersion
 
 
 @contextmanager
@@ -172,3 +173,24 @@ async def test_dict_signal_assignment_deprecated(dut):
     await Timer(1, 'step')
 
     assert dut.stream_in_data.value == pack_bit_vector(**d)
+
+
+@cocotb.test()
+async def test_assigning_setattr_syntax_deprecated(dut):
+    with assert_deprecated():
+        dut.stream_in_data = 1
+    with assert_raises(AttributeError):
+        # attempt to use __setattr__ syntax on signal that doesn't exist
+        dut.does_not_exist = 0
+
+
+icarus_under_11 = cocotb.SIM_NAME.lower().startswith("icarus") and (IcarusVersion(cocotb.SIM_VERSION) <= IcarusVersion("10.3 (stable)"))
+
+
+@cocotb.test(expect_error=Exception if icarus_under_11 else ())
+async def test_assigning_setitem_syntax_deprecated(dut):
+    with assert_deprecated():
+        dut.stream_in_data[0] = 1
+    with assert_raises(IndexError):
+        # attempt to use __setitem__ syntax on signal that doesn't exist
+        dut.stream_in_data[800000] = 1
diff --git a/tests/test_cases/test_cocotb/test_handle.py b/tests/test_cases/test_cocotb/test_handle.py
--- a/tests/test_cases/test_cocotb/test_handle.py
+++ b/tests/test_cases/test_cocotb/test_handle.py
@@ -298,7 +298,7 @@ async def test_real_assign_double(dut):
     timer_shortest = Timer(1, "step")
     await timer_shortest
     log.info("Setting the value %g" % val)
-    dut.stream_in_real = val
+    dut.stream_in_real <= val
     await timer_shortest
     await timer_shortest  # FIXME: Workaround for VHPI scheduling - needs investigation
     got = float(dut.stream_out_real)
diff --git a/tests/test_cases/test_discovery/test_discovery.py b/tests/test_cases/test_discovery/test_discovery.py
--- a/tests/test_cases/test_discovery/test_discovery.py
+++ b/tests/test_cases/test_discovery/test_discovery.py
@@ -157,24 +157,6 @@ async def access_single_bit(dut):
                          dut.stream_out_data_comb.value.integer, (1 << 2)))
 
 
-@cocotb.test(
-    # Icarus up to (including) 10.3 doesn't support bit-selects, see https://github.com/steveicarus/iverilog/issues/323
-    expect_error=IndexError if (cocotb.SIM_NAME.lower().startswith("icarus") and (IcarusVersion(cocotb.SIM_VERSION) <= IcarusVersion("10.3 (stable)"))) else (),
-    skip=cocotb.LANGUAGE in ["vhdl"])
-async def access_single_bit_assignment(dut):
-    """Access a single bit in a vector of the DUT using the assignment mechanism"""
-    dut.stream_in_data = 0
-    await Timer(1, "ns")
-    dut._log.info("%s = %d bits" %
-                  (dut.stream_in_data._path, len(dut.stream_in_data)))
-    dut.stream_in_data[2] = 1
-    await Timer(1, "ns")
-    if dut.stream_out_data_comb.value.integer != (1 << 2):
-        raise TestError("%s.%s != %d" %
-                        (dut.stream_out_data_comb._path,
-                         dut.stream_out_data_comb.value.integer, (1 << 2)))
-
-
 @cocotb.test(expect_error=IndexError)
 async def access_single_bit_erroneous(dut):
     """Access a non-existent single bit"""
diff --git a/tests/test_cases/test_force_release/test_force_release.py b/tests/test_cases/test_force_release/test_force_release.py
--- a/tests/test_cases/test_force_release/test_force_release.py
+++ b/tests/test_cases/test_force_release/test_force_release.py
@@ -13,8 +13,8 @@ async def test_force_release(dut):
     """
     log = logging.getLogger("cocotb.test")
     await Timer(10, "ns")
-    dut.stream_in_data = 4
-    dut.stream_out_data_comb = Force(5)
+    dut.stream_in_data <= 4
+    dut.stream_out_data_comb <= Force(5)
     await Timer(10, "ns")
     got_in = int(dut.stream_in_data)
     got_out = int(dut.stream_out_data_comb)
@@ -23,8 +23,8 @@ async def test_force_release(dut):
     if got_in == got_out:
         raise TestFailure("stream_in_data and stream_out_data_comb should not match when force is active!")
 
-    dut.stream_out_data_comb = Release()
-    dut.stream_in_data = 3
+    dut.stream_out_data_comb <= Release()
+    dut.stream_in_data <= 3
     await Timer(10, "ns")
     got_in = int(dut.stream_in_data)
     got_out = int(dut.stream_out_data_comb)
diff --git a/tests/test_cases/test_vhdl_libraries/test_ab.py b/tests/test_cases/test_vhdl_libraries/test_ab.py
--- a/tests/test_cases/test_vhdl_libraries/test_ab.py
+++ b/tests/test_cases/test_vhdl_libraries/test_ab.py
@@ -8,4 +8,4 @@ async def test(dut):
     #
     #   b.vhdl:9:5:@0ms:(report note): :a(structural):b@b(structural):
     #
-    dut.x = False
+    dut.x <= False