Repository: cocotb/cocotb

Issue:
LogicArray: Make type conversions more explicit
Looks like we weren't super clear about `LogicArray` being "data storage only", with no interpretation of the stored data. I had a look at the API and I think we can be clearer there as well.

Right now we have the following API in `LogicArray`, in line with what we had in `BinaryValue`:

* Interpret as signed integer: `LogicArray.signed_integer`
* Interpret as unsigned integer: `LogicArray.integer`

Comments about this API:

* It is confusing, because C/C++ would say an `int` is *signed*, while we say `integer` (without further qualification) is *unsigned*.
* I find properties slightly out of place here for two reasons:
  * We're operating on the LogicArray data and convert/interpret it. To me that's an action (and hence a method), not a property.
  * Passing additional arguments to the conversion, e.g. the byte order, requires a method.

To make things very explicit, I would go for:
* Interpret as integer: `LogicArray.as_int(byteorder='big', signed=False)`

Properties of this proposed API:
* This API matches closely the arguments of [`int.from_bytes()`](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) (which is similar in semantics)
* Using a method adds the ability to specify the byte order.
* We keep the naming we (and Python) had before: plain "integer" means "unsigned integer".
* We can easily extend this scheme for conversions to other data types, e.g., floats.

WDYT @ktbarrett and @cocotb/maintainers?


Discussion:
I'm fine with removing these and the proposed replacement. The only reason that `signed_integer` and `integer` were added was to maintain some amount of interface compatibility with BinaryValue to make transitioning easier, but you are correct in that there is an assumption of endianness that isn't a good idea.
I'm fine with implementing this feature as part of https://github.com/cocotb/cocotb/pull/3244.
I like the `LogicArray.as_int(byteorder='big', signed=False)` syntax and also the idea to extent it with float support... (would be better in a dedicated PR though)

EDIT: When building `LogicArray` we provide a range, example: `Range(7, "downto", 0)`, do we agree  that we rely on this information to infer the **the bit numbering** ?

Resolving patch:
diff --git a/src/cocotb/handle.py b/src/cocotb/handle.py
--- a/src/cocotb/handle.py
+++ b/src/cocotb/handle.py
@@ -930,12 +930,12 @@ def _set_value(
                     return
 
                 if value < 0:
-                    value_ = LogicArray(
+                    value_ = LogicArray.from_signed(
                         value,
                         Range(len(self) - 1, "downto", 0),
                     )
                 else:
-                    value_ = LogicArray(
+                    value_ = LogicArray.from_unsigned(
                         value,
                         Range(len(self) - 1, "downto", 0),
                     )
@@ -963,9 +963,7 @@ def _set_value(
                 f"Unsupported type for value assignment: {type(value)} ({value!r})"
             )
 
-        schedule_write(
-            self, self._handle.set_signal_val_binstr, (action, value_.binstr)
-        )
+        schedule_write(self, self._handle.set_signal_val_binstr, (action, str(value_)))
 
     @property
     def value(self) -> LogicArray:
diff --git a/src/cocotb/types/logic_array.py b/src/cocotb/types/logic_array.py
--- a/src/cocotb/types/logic_array.py
+++ b/src/cocotb/types/logic_array.py
@@ -2,7 +2,9 @@
 # Licensed under the Revised BSD License, see LICENSE for details.
 # SPDX-License-Identifier: BSD-3-Clause
 import typing
+import warnings
 
+from cocotb._deprecation import deprecated
 from cocotb.types import ArrayLike
 from cocotb.types.logic import Logic, LogicConstructibleT
 from cocotb.types.range import Range
@@ -15,16 +17,11 @@ class LogicArray(ArrayLike[Logic]):
     .. currentmodule:: cocotb.types
 
     :class:`LogicArray`\ s can be constructed from either iterables of values
-    constructible into :class:`Logic`: like :class:`bool`, :class:`str`, :class:`int`;
-    or from integers.
-    If constructed from a positive integer, an unsigned bit representation is used to
-    construct the :class:`LogicArray`.
-    If constructed from a negative integer, a two's complement bit representation is
-    used.
+    constructible into :class:`Logic`: like :class:`bool`, :class:`str`, :class:`int`.
     Like :class:`Array`, if no *range* argument is given, it is deduced from the length
-    of the iterable or bit string used to initialize the variable.
+    of the iterable used to initialize the variable.
     If a *range* argument is given, but no value,
-    the array is filled with the default value of Logic().
+    the array is filled with the default value of ``Logic()``.
 
     .. code-block:: python3
 
@@ -34,15 +31,19 @@ class LogicArray(ArrayLike[Logic]):
         >>> LogicArray([0, True, "X"])
         LogicArray('01X', Range(2, 'downto', 0))
 
-        >>> LogicArray(0xA)  # picks smallest range that can fit the value
+        >>> LogicArray(range=Range(0, "to", 3))  # default values
+        LogicArray('XXXX', Range(0, 'to', 3))
+
+    :class:`LogicArray`\ s can be constructed from :class:`int`\ s using :meth:`from_unsigned` or :meth:`from_signed`.
+
+    .. code-block:: python3
+
+        >>> LogicArray.from_unsigned(0xA)  # picks smallest range that can fit the value
         LogicArray('1010', Range(3, 'downto', 0))
 
-        >>> LogicArray(-4, Range(0, "to", 3))  # will sign-extend
+        >>> LogicArray.from_signed(-4, Range(0, "to", 3))  # will sign-extend
         LogicArray('1100', Range(0, 'to', 3))
 
-        >>> LogicArray(range=Range(0, "to", 3))  # default values
-        LogicArray('XXXX', Range(0, 'to', 3))
-
     :class:`LogicArray`\ s support the same operations as :class:`Array`;
     however, it enforces the condition that all elements must be a :class:`Logic`.
 
@@ -80,13 +81,13 @@ class LogicArray(ArrayLike[Logic]):
     .. code-block:: python3
 
         >>> la = LogicArray("1010")
-        >>> la.binstr
+        >>> str(la)
         '1010'
 
-        >>> la.integer          # uses unsigned representation
+        >>> la.to_unsigned()
         10
 
-        >>> la.signed_integer   # uses two's complement representation
+        >>> la.to_signed()
         -6
 
     :class:`LogicArray`\ s also support element-wise logical operations: ``&``, ``|``,
@@ -109,64 +110,148 @@ class LogicArray(ArrayLike[Logic]):
         range: Indexing scheme of the array.
 
     Raises:
+        OverflowError: When given *value* cannot fit in given *range*.
         ValueError: When argument values cannot be used to construct an array.
         TypeError: When invalid argument types are used.
     """
 
-    __slots__ = ()
+    _value: typing.List[Logic]
+    _range: Range
 
     @typing.overload
-    def __init__(
-        self,
+    def __new__(
+        cls,
         value: typing.Union[int, typing.Iterable[LogicConstructibleT]],
         range: typing.Optional[Range] = None,
-    ): ...
+    ) -> "LogicArray": ...
 
     @typing.overload
-    def __init__(
-        self,
+    def __new__(
+        cls,
         value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None,
         *,
         range: Range,
-    ): ...
+    ) -> "LogicArray": ...
 
-    def __init__(
-        self,
+    def __new__(
+        cls,
         value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None,
         range: typing.Optional[Range] = None,
-    ) -> None:
+    ) -> "LogicArray":
+        if isinstance(value, int):
+            warnings.warn(
+                "Constructing a LogicArray from an integer is deprecated. "
+                "Use `LogicArray.from_signed(value)` or `LogicArray.from_unsigned(value)` instead.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+            if value < 0:
+                return cls.from_signed(value, range=range)
+            else:
+                return cls.from_unsigned(value, range=range)
+
+        self = super().__new__(cls)
+
+        # construct _value representation
         if value is None:
             if range is None:
                 raise ValueError(
                     "at least one of the value and range input parameters must be given"
                 )
             self._value = [Logic() for _ in range]
-        elif isinstance(value, int):
-            if value < 0:
-                bitlen = int.bit_length(value + 1) + 1
-            else:
-                bitlen = max(1, int.bit_length(value))
-            if range is None:
-                self._value = [Logic(v) for v in _int_to_bitstr(value, bitlen)]
-            else:
-                if bitlen > len(range):
-                    raise ValueError(f"{value} will not fit in {range}")
-                self._value = [Logic(v) for v in _int_to_bitstr(value, len(range))]
-        elif isinstance(value, typing.Iterable):
-            self._value = [Logic(v) for v in value]
         else:
-            raise TypeError(
-                f"cannot construct {type(self).__qualname__} from value of type {type(value).__qualname__}"
-            )
+            value_iter = iter(value)
+            self._value = [Logic(v) for v in value_iter]
+
+        # construct _range representation
         if range is None:
             self._range = Range(len(self._value) - 1, "downto", 0)
         else:
             self._range = range
+
+        # check that _value and _range align
         if len(self._value) != len(self._range):
-            raise ValueError(
+            raise OverflowError(
                 f"value of length {len(self._value)} will not fit in {self._range}"
             )
 
+        return self
+
+    @classmethod
+    def from_unsigned(
+        cls, value: int, range: typing.Optional[Range] = None
+    ) -> "LogicArray":
+        """Construct a :class:`LogicArray` from an :class:`int` by interpreting it as a bit vector with unsigned representation.
+
+        The :class:`int` is treated as an arbitrary-length bit vector with unsigned representation where the left-most bit is the most significant bit.
+        This bit vector is then constructed into a :class:`LogicArray`.
+
+        If *range* is not given, it defaults to ``Range(n_bits-1, "downto", 0)``,
+        where ``n_bits`` is the minimum number of bits necessary to hold the value.
+
+        If *range* is given and the value cannot fit in a :class:`LogicArray` of that size,
+        an :exc:`OverflowError` is raised.
+
+        Args:
+            value: The integer to convert.
+            range: A specific :class:`Range` to use as the bounds on the return :class:`LogicArray` object.
+
+        Returns:
+            A :class:`LogicArray` equivalent to the *value* by interpreting it as a bit vector with unsigned representation.
+
+        Raises:
+            OverflowError: When a :class:`LogicArray` of the given *range* can't hold the *value*.
+        """
+        if value < 0:
+            raise OverflowError(f"{value} not in bounds for an unsigned integer.")
+
+        bitlen = max(1, int.bit_length(value))
+
+        if range is None:
+            range = Range(bitlen - 1, "downto", 0)
+        elif bitlen > len(range):
+            raise OverflowError(
+                f"{value} will not fit in a LogicArray with bounds: {range!r}."
+            )
+
+        return LogicArray(_int_to_bitstr(value, len(range)), range=range)
+
+    @classmethod
+    def from_signed(
+        cls, value: int, range: typing.Optional[Range] = None
+    ) -> "LogicArray":
+        """Construct a :class:`LogicArray` from an :class:`int` by interpreting it as a bit vector with two's complement representation.
+
+        The :class:`int` is treated as an arbitrary-length bit vector with two's complement representation where the left-most bit is the most significant bit.
+        This bit vector is then constructed into a :class:`LogicArray`.
+
+        If *range* is not given, it defaults to ``Range(n_bits-1, "downto", 0)``,
+        where ``n_bits`` is the minimum number of bits necessary to hold the value.
+
+        If *range* is given and the value cannot fit in a :class:`LogicArray` of that size,
+        an :exc:`OverflowError` is raised.
+
+        Args:
+            value: The integer to convert.
+            range: A specific :class:`Range` to use as the bounds on the return :class:`LogicArray` object.
+
+        Returns:
+            A :class:`LogicArray` equivalent to the *value* by interpreting it as a bit vector with two's complement representation.
+
+        Raises:
+            OverflowError: When a :class:`LogicArray` of the given *range* can't hold the *value*.
+        """
+        bitlen = int.bit_length(value + 1) + 1
+
+        if range is None:
+            range = Range(bitlen - 1, "downto", 0)
+        elif bitlen > len(range):
+            raise OverflowError(
+                f"{value} will not fit in a LogicArray with bounds: {range!r}."
+            )
+
+        return LogicArray(_int_to_bitstr(value, len(range)), range=range)
+
     @property
     def range(self) -> Range:
         """:class:`Range` of the indexes of the array."""
@@ -179,7 +264,7 @@ def range(self, new_range: Range) -> None:
             raise TypeError("range argument must be of type 'Range'")
         if len(new_range) != len(self):
             raise ValueError(
-                f"{new_range!r} not the same length as old range ({self._range!r})."
+                f"{new_range!r} not the same length as old range: {self._range!r}."
             )
         self._range = new_range
 
@@ -200,7 +285,7 @@ def __eq__(
             return self._value == other._value
         elif isinstance(other, int):
             try:
-                return self.integer == other
+                return self.to_unsigned() == other
             except ValueError:
                 return False
         elif isinstance(other, (str, list, tuple)):
@@ -217,23 +302,75 @@ def count(self, value: Logic) -> int:
         return self._value.count(value)
 
     @property
+    @deprecated("`.binstr` property is deprecated. Use `str(value)` instead.")
     def binstr(self) -> str:
-        return "".join(str(bit) for bit in self)
+        """Convert the value to the :class:`str` literal representation.
+
+        .. deprecated:: 2.0
+        """
+        return str(self)
 
     @property
     def is_resolvable(self) -> bool:
+        """``True`` if all elements are ``0`` or ``1``."""
         return all(bit in (Logic(0), Logic(1)) for bit in self)
 
     @property
+    @deprecated("`.integer` property is deprecated. Use `value.to_unsigned()` instead.")
     def integer(self) -> int:
+        """Convert the value to an :class:`int` by interpreting it using unsigned representation.
+
+        The :class:`LogicArray` is treated as an arbitrary-length vector of bits
+        with the left-most bit being the most significant bit in the integer value.
+        The bit vector is then interpreted as an integer using unsigned representation.
+
+        Returns: An :class:`int` equivalent to the value by interpreting it using unsigned representation.
+
+        .. deprecated:: 2.0
+        """
+        return self.to_unsigned()
+
+    @property
+    @deprecated(
+        "`.signed_integer` property is deprecated. Use `value.to_signed()` instead."
+    )
+    def signed_integer(self) -> int:
+        """Convert the value to an :class:`int` by interpreting it using two's complement representation.
+
+        The :class:`LogicArray` is treated as an arbitrary-length vector of bits
+        with the left-most bit being the most significant bit in the integer value.
+        The bit vector is then interpreted as an integer using two's complement representation.
+
+        Returns: An :class:`int` equivalent to the value by interpreting it using two's complement representation.
+
+        .. deprecated:: 2.0
+        """
+        return self.to_signed()
+
+    def to_unsigned(self) -> int:
+        """Convert the value to an :class:`int` by interpreting it using unsigned representation.
+
+        The :class:`LogicArray` is treated as an arbitrary-length vector of bits
+        with the left-most bit being the most significant bit in the integer value.
+        The bit vector is then interpreted as an integer using unsigned representation.
+
+        Returns: An :class:`int` equivalent to the value by interpreting it using unsigned representation.
+        """
         value = 0
         for bit in self:
             value = value << 1 | int(bit)
         return value
 
-    @property
-    def signed_integer(self) -> int:
-        value = self.integer
+    def to_signed(self) -> int:
+        """Convert the value to an :class:`int` by interpreting it using two's complement representation.
+
+        The :class:`LogicArray` is treated as an arbitrary-length vector of bits
+        with the left-most bit being the most significant bit in the integer value.
+        The bit vector is then interpreted as an integer using two's complement representation.
+
+        Returns: An :class:`int` equivalent to the value by interpreting it using two's complement representation.
+        """
+        value = self.to_unsigned()
         if value >= (1 << (len(self) - 1)):
             value -= 1 << len(self)
         return value
@@ -314,13 +451,13 @@ def _translate_index(self, item: int) -> int:
             raise IndexError(f"index {item} out of range") from None
 
     def __repr__(self) -> str:
-        return f"{type(self).__qualname__}({self.binstr!r}, {self.range!r})"
+        return f"{type(self).__qualname__}({str(self)!r}, {self.range!r})"
 
     def __str__(self) -> str:
-        return self.binstr
+        return "".join(str(bit) for bit in self)
 
     def __int__(self) -> int:
-        return self.integer
+        return self.to_unsigned()
 
     def __and__(self, other: "LogicArray") -> "LogicArray":
         if isinstance(other, LogicArray):


Test patch:
diff --git a/examples/matrix_multiplier/tests/test_matrix_multiplier.py b/examples/matrix_multiplier/tests/test_matrix_multiplier.py
--- a/examples/matrix_multiplier/tests/test_matrix_multiplier.py
+++ b/examples/matrix_multiplier/tests/test_matrix_multiplier.py
@@ -60,7 +60,7 @@ def stop(self) -> None:
     async def _run(self) -> None:
         while True:
             await RisingEdge(self._clk)
-            if self._valid.value.binstr != "1":
+            if self._valid.value != "1":
                 await RisingEdge(self._valid)
                 continue
             self.values.put_nowait(self._sample())
@@ -126,8 +126,8 @@ def model(
             LogicArray(
                 sum(
                     [
-                        a_matrix[(i * A_COLUMNS_B_ROWS) + n].integer
-                        * b_matrix[(n * B_COLUMNS) + j].integer
+                        a_matrix[(i * A_COLUMNS_B_ROWS) + n].to_unsigned()
+                        * b_matrix[(n * B_COLUMNS) + j].to_unsigned()
                         for n in range(A_COLUMNS_B_ROWS)
                     ]
                 ),
diff --git a/examples/mixed_language/tests/test_mixed_language.py b/examples/mixed_language/tests/test_mixed_language.py
--- a/examples/mixed_language/tests/test_mixed_language.py
+++ b/examples/mixed_language/tests/test_mixed_language.py
@@ -97,7 +97,7 @@ async def mixed_language_functional_test(dut):
     for _ in range(1, 5):
         for i in range(1, 11):
             await RisingEdge(dut.clk)
-            previous_indata = dut.stream_in_data.value.integer
+            previous_indata = dut.stream_in_data.value.to_unsigned()
 
             # write stream in data
             dut.stream_in_data.value = i + 0x81FFFFFF2B00  # generate a magic number
@@ -111,7 +111,7 @@ async def mixed_language_functional_test(dut):
 
             # compare in and out data
             assert (
-                previous_indata == dut.stream_out_data.value.integer
+                previous_indata == dut.stream_out_data.value.to_unsigned()
             ), f"stream in data and stream out data were different in round {i}"
 
 
diff --git a/examples/mixed_signal/tests/test_regulator_plot.py b/examples/mixed_signal/tests/test_regulator_plot.py
--- a/examples/mixed_signal/tests/test_regulator_plot.py
+++ b/examples/mixed_signal/tests/test_regulator_plot.py
@@ -21,7 +21,7 @@ async def test_trim_vals(tb_hdl):
         tb_hdl.trim_val.value = trim_val
         await Timer(1, units="ns")
         trimmed_volt = await get_voltage(tb_hdl, probed_node)
-        actual_trim_val = tb_hdl.trim_val.value.signed_integer
+        actual_trim_val = tb_hdl.trim_val.value.to_signed()
         tb_hdl._log.info(
             f"trim_val={actual_trim_val} results in {probed_node}={trimmed_volt:.4} V"
         )
diff --git a/examples/mixed_signal/tests/test_regulator_trim.py b/examples/mixed_signal/tests/test_regulator_trim.py
--- a/examples/mixed_signal/tests/test_regulator_trim.py
+++ b/examples/mixed_signal/tests/test_regulator_trim.py
@@ -36,7 +36,7 @@ async def get_voltage(self, node):
         )  # waiting time needed for the analog values to be updated
         self.tb_hdl._log.debug(
             "trim value={}: {}={:.4} V".format(
-                self.tb_hdl.trim_val.value.signed_integer,
+                self.tb_hdl.trim_val.value.to_signed(),
                 self.analog_probe.node_to_probe.value.decode("ascii"),
                 self.analog_probe.voltage.value,
             )
diff --git a/tests/pytest/test_logic_array.py b/tests/pytest/test_logic_array.py
--- a/tests/pytest/test_logic_array.py
+++ b/tests/pytest/test_logic_array.py
@@ -14,39 +14,75 @@ def test_logic_array_constructor():
 
     assert LogicArray(range=Range(0, "to", 3)) == LogicArray("XXXX")
 
-    assert LogicArray(0) == LogicArray("0")
-    assert LogicArray(0xA7) == LogicArray("10100111")
-    assert LogicArray(-1) == LogicArray("1")
-
-    assert LogicArray(10, Range(5, "downto", 0)) == LogicArray("001010")
-    assert LogicArray(-2, Range(5, "downto", 0)) == LogicArray("111110")
-    with pytest.raises(ValueError):
-        LogicArray(10, Range(1, "to", 3))
-
     with pytest.raises(TypeError):
         LogicArray(object())
 
-    with pytest.raises(ValueError):
+    with pytest.raises(OverflowError):
         LogicArray("101010", Range(0, "to", 0))
 
     with pytest.raises(ValueError):
         LogicArray()
 
 
+def test_logic_array_constructor_deprecated():
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(0xA7) == LogicArray("10100111")
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(10, Range(5, "downto", 0)) == LogicArray("001010")
+
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(-1) == LogicArray("1")
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(-2, Range(5, "downto", 0)) == LogicArray("111110")
+
+    with pytest.raises(OverflowError):
+        with pytest.warns(DeprecationWarning):
+            LogicArray(10, Range(1, "to", 3))
+    with pytest.raises(OverflowError):
+        with pytest.warns(DeprecationWarning):
+            LogicArray(-45, Range(1, "to", 3))
+
+
+def test_logic_array_int_conversion():
+    assert LogicArray.from_unsigned(0xA7) == LogicArray("10100111")
+    assert LogicArray.from_unsigned(10, Range(5, "downto", 0)) == LogicArray("001010")
+    with pytest.raises(OverflowError):
+        LogicArray.from_unsigned(-10)
+    with pytest.raises(OverflowError):
+        LogicArray.from_unsigned(10, Range(1, "to", 3))
+
+    assert LogicArray.from_signed(-1) == LogicArray("1")
+    assert LogicArray.from_signed(-2, Range(5, "downto", 0)) == LogicArray("111110")
+    with pytest.raises(OverflowError):
+        LogicArray.from_signed(-45, Range(1, "to", 3))
+
+
 def test_logic_array_properties():
-    assert LogicArray(0).integer == 0
-    assert LogicArray(0).signed_integer == 0
-    assert LogicArray(0).binstr == "0"
-    assert LogicArray(10).integer == 10
-    assert LogicArray(10).signed_integer == -6
-    assert LogicArray(10).binstr == "1010"
-    assert LogicArray(-6).integer == 10
-    assert LogicArray(-6).signed_integer == -6
-    assert LogicArray(-6).binstr == "1010"
-    assert LogicArray(0).is_resolvable
+    assert LogicArray("01").is_resolvable
     assert not LogicArray("1X1").is_resolvable
 
 
+def test_logic_array_properties_deprecated():
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(0).integer == 0
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(0).signed_integer == 0
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(0).binstr == "0"
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(10).integer == 10
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(10).signed_integer == -6
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(10).binstr == "1010"
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(-6).integer == 10
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(-6).signed_integer == -6
+    with pytest.warns(DeprecationWarning):
+        assert LogicArray(-6).binstr == "1010"
+
+
 def test_logic_array_setattr():
     l = LogicArray("0000")
     l[1] = "X"
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
@@ -16,7 +16,7 @@ async def monitor(dut):
         await RisingEdge(dut.clk)
     await ReadOnly()
     assert (
-        dut.stream_in_valid.value.integer
+        dut.stream_in_valid.value == 1
     ), "stream_in_valid should be high on the 5th cycle"
 
 
diff --git a/tests/test_cases/issue_142/issue_142.py b/tests/test_cases/issue_142/issue_142.py
--- a/tests/test_cases/issue_142/issue_142.py
+++ b/tests/test_cases/issue_142/issue_142.py
@@ -3,7 +3,6 @@
 import cocotb
 from cocotb.clock import Clock
 from cocotb.triggers import RisingEdge
-from cocotb.types import LogicArray, Range
 
 
 @cocotb.test()
@@ -15,15 +14,12 @@ async def issue_142_overflow_error(dut):
     # Wider values are transparently converted to LogicArrays
     for value in [
         0,
-        0x7FFFFFFF,
-        0x7FFFFFFFFFFF,
-        LogicArray(
-            0x7FFFFFFFFFFFFF, Range(len(dut.stream_in_data_wide) - 1, "downto", 0)
-        ),
+        0x7F_FF_FF_FF,  # fits in a 32-bit signed int
+        0x7F_FF_FF_FF_FF_FF_FF_FF,  # requires >32-bit signed int, must be transparently converted to LogicArray
     ]:
         dut.stream_in_data_wide.value = value
         await RisingEdge(dut.clk)
-        assert dut.stream_in_data_wide.value.integer == value
+        assert dut.stream_in_data_wide.value == value
         dut.stream_in_data_wide.value = value
         await RisingEdge(dut.clk)
-        assert dut.stream_in_data_wide.value.integer == value
+        assert dut.stream_in_data_wide.value == value
diff --git a/tests/test_cases/issue_253/issue_253.py b/tests/test_cases/issue_253/issue_253.py
--- a/tests/test_cases/issue_253/issue_253.py
+++ b/tests/test_cases/issue_253/issue_253.py
@@ -7,10 +7,10 @@
 async def toggle_clock(dut):
     dut.clk.value = 0
     await Timer(10, "ns")
-    assert dut.clk.value.integer == 0, "Clock not set to 0 as expected"
+    assert dut.clk.value == 0, "Clock not set to 0 as expected"
     dut.clk.value = 1
     await Timer(10, "ns")
-    assert dut.clk.value.integer == 1, "Clock not set to 1 as expected"
+    assert dut.clk.value == 1, "Clock not set to 1 as expected"
 
 
 @cocotb.test()
diff --git a/tests/test_cases/issue_768_b/issue_768.py b/tests/test_cases/issue_768_b/issue_768.py
--- a/tests/test_cases/issue_768_b/issue_768.py
+++ b/tests/test_cases/issue_768_b/issue_768.py
@@ -10,7 +10,7 @@
 from cocotb.types import LogicArray, Range
 
 # this line is different between the two files
-value = LogicArray(0, Range(7, "downto", 0))
+value = LogicArray.from_unsigned(0, Range(7, "downto", 0))
 
 
 @cocotb.test()
diff --git a/tests/test_cases/test_cocotb/test_edge_triggers.py b/tests/test_cases/test_cocotb/test_edge_triggers.py
--- a/tests/test_cases/test_cocotb/test_edge_triggers.py
+++ b/tests/test_cases/test_cocotb/test_edge_triggers.py
@@ -43,14 +43,14 @@ async def count_edges_cycles(signal, edges):
 
 async def do_single_edge_check(dut, level):
     """Do test for rising edge"""
-    old_value = dut.clk.value.integer
+    old_value = dut.clk.value
     dut._log.info("Value of %s is %d" % (dut.clk._path, old_value))
     assert old_value != level
     if level == 1:
         await RisingEdge(dut.clk)
     else:
         await FallingEdge(dut.clk)
-    new_value = dut.clk.value.integer
+    new_value = dut.clk.value
     dut._log.info("Value of %s is %d" % (dut.clk._path, new_value))
     assert new_value == level, "%s not %d at end" % (dut.clk._path, level)
 
@@ -88,27 +88,27 @@ async def test_either_edge(dut):
     await Timer(1, "ns")
     dut.clk.value = 1
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 1
+    assert dut.clk.value == 1
     await Timer(10, "ns")
     dut.clk.value = 0
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 0
+    assert dut.clk.value == 0
     await Timer(10, "ns")
     dut.clk.value = 1
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 1
+    assert dut.clk.value == 1
     await Timer(10, "ns")
     dut.clk.value = 0
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 0
+    assert dut.clk.value == 0
     await Timer(10, "ns")
     dut.clk.value = 1
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 1
+    assert dut.clk.value == 1
     await Timer(10, "ns")
     dut.clk.value = 0
     await Edge(dut.clk)
-    assert dut.clk.value.integer == 0
+    assert dut.clk.value == 0
 
 
 @cocotb.test()
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
@@ -108,9 +108,9 @@ async def int_values_test(signal, n_bits, limits=_Limits.VECTOR_NBIT):
 
         if limits == _Limits.VECTOR_NBIT:
             if val < 0:
-                got = signal.value.signed_integer
+                got = signal.value.to_signed()
             else:
-                got = signal.value.integer
+                got = signal.value.to_unsigned()
         else:
             got = signal.value
 
@@ -416,6 +416,6 @@ async def test_assign_LogicArray(dut):
 async def test_assign_Logic(dut):
     dut.stream_in_ready.value = Logic("X")
     await Timer(1, "ns")
-    assert dut.stream_in_ready.value.binstr.lower() == "x"
+    assert dut.stream_in_ready.value == "x"
     with pytest.raises(ValueError):
         dut.stream_in_data.value = Logic("U")  # not the correct size
diff --git a/tests/test_cases/test_cocotb/test_scheduler.py b/tests/test_cases/test_cocotb/test_scheduler.py
--- a/tests/test_cases/test_cocotb/test_scheduler.py
+++ b/tests/test_cases/test_cocotb/test_scheduler.py
@@ -302,27 +302,13 @@ async def test_last_scheduled_write_wins(dut):
     """
     Test that the last scheduled write for a signal handle is the value that is written.
     """
-    log = logging.getLogger("cocotb.test")
-    e = Event()
-    dut.stream_in_data.setimmediatevalue(0)
-
-    async def first():
-        await Timer(1, "ns")
-        log.info("scheduling stream_in_data.value = 1")
-        dut.stream_in_data.value = 1
-        e.set()
-
-    async def second():
-        await Timer(1, "ns")
-        await e.wait()
-        log.info("scheduling stream_in_data.value = 2")
-        dut.stream_in_data.value = 2
-
-    await Combine(cocotb.start_soon(first()), cocotb.start_soon(second()))
-
+    dut.stream_in_data.value = 0
+    await Timer(10, "ns")
+    assert dut.stream_in_data.value == 0
+    dut.stream_in_data.value = 1
+    dut.stream_in_data.value = 2
     await ReadOnly()
-
-    assert dut.stream_in_data.value.integer == 2
+    assert dut.stream_in_data.value == 2
 
 
 # GHDL unable to put values on nested array types (gh-2588)
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
@@ -141,7 +141,7 @@ async def access_signal(dut):
     """Access a signal using the assignment mechanism"""
     dut.stream_in_data.setimmediatevalue(1)
     await Timer(1, "ns")
-    assert dut.stream_in_data.value.integer == 1
+    assert dut.stream_in_data.value == 1
 
 
 @cocotb.test(skip=LANGUAGE in ["vhdl"])
@@ -174,35 +174,21 @@ async def access_type_bit_verilog_metavalues(dut):
     await Timer(1, "ns")
     dut.mybits.value = LogicArray("XZ")
     await Timer(1, "ns")
-    print(dut.mybits.value.binstr)
     if SIM_NAME.startswith(("icarus", "ncsim", "xmsim")):
-        assert (
-            dut.mybits.value.binstr.lower() == "xz"
-        ), "The assigned value was not as expected"
+        assert dut.mybits.value == "xz"
     elif SIM_NAME.startswith(("riviera",)):
-        assert (
-            dut.mybits.value.binstr.lower() == "10"
-        ), "The assigned value was not as expected"
+        assert dut.mybits.value == "10"
     else:
-        assert (
-            dut.mybits.value.binstr.lower() == "00"
-        ), "The assigned value was incorrect"
+        assert dut.mybits.value == "00"
 
     dut.mybits.value = LogicArray("ZX")
     await Timer(1, "ns")
-    print(dut.mybits.value.binstr)
     if SIM_NAME.startswith(("icarus", "ncsim", "xmsim")):
-        assert (
-            dut.mybits.value.binstr.lower() == "zx"
-        ), "The assigned value was not as expected"
+        assert dut.mybits.value == "zx"
     elif SIM_NAME.startswith(("riviera",)):
-        assert (
-            dut.mybits.value.binstr.lower() == "01"
-        ), "The assigned value was not as expected"
+        assert dut.mybits.value == "01"
     else:
-        assert (
-            dut.mybits.value.binstr.lower() == "00"
-        ), "The assigned value was incorrect"
+        assert dut.mybits.value == "00"
 
 
 @cocotb.test(
@@ -225,7 +211,7 @@ async def access_single_bit(dut):
     await Timer(1, "ns")
     dut.stream_in_data[2].value = 1
     await Timer(1, "ns")
-    assert dut.stream_out_data_comb.value.integer == (1 << 2)
+    assert dut.stream_out_data_comb.value == (1 << 2)
 
 
 @cocotb.test()
@@ -411,22 +397,9 @@ async def access_boolean(dut):
 async def access_internal_register_array(dut):
     """Test access to an internal register array"""
 
-    # verilator does not support 4-state signals
-    # see https://veripool.org/guide/latest/languages.html#unknown-states
-    if SIM_NAME.startswith("verilator"):
-        expected_value = "00000000"
-    else:
-        expected_value = "XXXXXXXX"
-
-    assert (
-        dut.register_array[0].value.binstr == expected_value
-    ), "Failed to access internal register array value"
-
-    dut.register_array[1].setimmediatevalue(4)
+    dut.register_array[1].value = 4
     await Timer(1, "ns")
-    assert (
-        dut.register_array[1].value == 4
-    ), "Failed to set internal register array value"
+    assert dut.register_array[1].value == 4
 
 
 @cocotb.test(
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
@@ -1,5 +1,3 @@
-import logging
-
 import cocotb
 from cocotb.handle import Force, Release
 from cocotb.triggers import Timer
@@ -10,22 +8,13 @@ async def test_force_release(dut):
     """
     Test force and release on simulation handles
     """
-    log = logging.getLogger("cocotb.test")
     await Timer(10, "ns")
     dut.stream_in_data.value = 4
     dut.stream_out_data_comb.value = Force(5)
     await Timer(10, "ns")
-    got_in = dut.stream_in_data.value.integer
-    got_out = dut.stream_out_data_comb.value.integer
-    log.info("dut.stream_in_data = %d" % got_in)
-    log.info("dut.stream_out_data_comb = %d" % got_out)
-    assert got_in != got_out
+    assert dut.stream_in_data.value != dut.stream_out_data_comb.value
 
     dut.stream_out_data_comb.value = Release()
     dut.stream_in_data.value = 3
     await Timer(10, "ns")
-    got_in = dut.stream_in_data.value.integer
-    got_out = dut.stream_out_data_comb.value.integer
-    log.info("dut.stream_in_data = %d" % got_in)
-    log.info("dut.stream_out_data_comb = %d" % got_out)
-    assert got_in == got_out
+    assert dut.stream_in_data.value == dut.stream_out_data_comb.value
diff --git a/tests/test_cases/test_package/test_package.py b/tests/test_cases/test_package/test_package.py
--- a/tests/test_cases/test_package/test_package.py
+++ b/tests/test_cases/test_package/test_package.py
@@ -28,47 +28,47 @@ async def test_package_access(_) -> None:
 
     assert isinstance(pkg1.bit_1_param, LogicObject)
     assert pkg1.bit_1_param._path == "cocotb_package_pkg_1::bit_1_param"
-    assert pkg1.bit_1_param.value.integer == 1
+    assert pkg1.bit_1_param.value == 1
 
     assert isinstance(pkg1.bit_2_param, LogicObject)
     assert pkg1.bit_2_param._path == "cocotb_package_pkg_1::bit_2_param"
-    assert pkg1.bit_2_param.value.integer == 3
+    assert pkg1.bit_2_param.value == 3
 
     assert isinstance(pkg1.bit_600_param, LogicObject)
     assert pkg1.bit_600_param._path == "cocotb_package_pkg_1::bit_600_param"
-    assert pkg1.bit_600_param.value.integer == 12345678912345678912345689
+    assert pkg1.bit_600_param.value == 12345678912345678912345689
 
     assert isinstance(pkg1.byte_param, LogicObject)
     assert pkg1.byte_param._path == "cocotb_package_pkg_1::byte_param"
-    assert pkg1.byte_param.value.integer == 100
+    assert pkg1.byte_param.value == 100
 
     assert isinstance(pkg1.shortint_param, LogicObject)
     assert pkg1.shortint_param._path == "cocotb_package_pkg_1::shortint_param"
-    assert pkg1.shortint_param.value.integer == 63000
+    assert pkg1.shortint_param.value == 63000
 
     assert isinstance(pkg1.int_param, LogicObject)
     assert pkg1.int_param._path == "cocotb_package_pkg_1::int_param"
-    assert pkg1.int_param.value.integer == 50
+    assert pkg1.int_param.value == 50
 
     assert isinstance(pkg1.longint_param, LogicObject)
     assert pkg1.longint_param._path == "cocotb_package_pkg_1::longint_param"
-    assert pkg1.longint_param.value.integer == 0x11C98C031CB
+    assert pkg1.longint_param.value == 0x11C98C031CB
 
     assert isinstance(pkg1.integer_param, LogicObject)
     assert pkg1.integer_param._path == "cocotb_package_pkg_1::integer_param"
-    assert pkg1.integer_param.value.integer == 125000
+    assert pkg1.integer_param.value == 125000
 
     assert isinstance(pkg1.logic_130_param, LogicObject)
     assert pkg1.logic_130_param._path == "cocotb_package_pkg_1::logic_130_param"
-    assert pkg1.logic_130_param.value.integer == 0x8C523EC7DC553A2B
+    assert pkg1.logic_130_param.value == 0x8C523EC7DC553A2B
 
     assert isinstance(pkg1.reg_8_param, LogicObject)
     assert pkg1.reg_8_param._path == "cocotb_package_pkg_1::reg_8_param"
-    assert pkg1.reg_8_param.value.integer == 200
+    assert pkg1.reg_8_param.value == 200
 
     assert isinstance(pkg1.time_param, LogicObject)
     assert pkg1.time_param._path == "cocotb_package_pkg_1::time_param"
-    assert pkg1.time_param.value.integer == 0x2540BE400
+    assert pkg1.time_param.value == 0x2540BE400
 
     pkg2 = cocotb.packages.cocotb_package_pkg_2
     assert isinstance(pkg2, HierarchyObject)