Repository: cocotb/cocotb

Issue:
`import` order is important and may break
If `cocotb.triggers` is not imported before `cocotb.task` it throws `ImportError: cannot import name 'Task' from partially initialized module 'cocotb.task' (most likely due to a circular import)`. It is pretty clear how `cocotb.task` and `cocotb.trigger` are circularly dependent. Maybe there is a way to avoid this by design rather than depend on the particular import order in `cocotb/__init__.py`.


Discussion:

Resolving patch:
diff --git a/cocotb/decorators.py b/cocotb/decorators.py
--- a/cocotb/decorators.py
+++ b/cocotb/decorators.py
@@ -26,10 +26,9 @@
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import functools
-from typing import Callable, Optional, Sequence, Type, Union
+from typing import Any, Callable, Coroutine, Optional, Sequence, Type, TypeVar, Union
 
 import cocotb
-import cocotb.triggers
 from cocotb.log import SimLog
 from cocotb.task import _RunningCoroutine, _RunningTest
 from cocotb.utils import lazy_property
@@ -68,51 +67,46 @@ def __str__(self):
         return str(self._func.__qualname__)
 
 
-class function:
-    """Decorator class that allows a function to block.
+Result = TypeVar("Result")
+
+
+def function(func: Callable[..., Coroutine[Any, Any, Result]]) -> Callable[..., Result]:
+    """Decorator that turns a :term:`coroutine function` into a blocking function.
 
     This allows a coroutine that consumes simulation time
     to be called by a thread started with :class:`cocotb.external`;
     in other words, to internally block while externally
     appear to yield.
-    """
 
-    def __init__(self, func):
-        self._coro = cocotb.coroutine(func)
-
-    @lazy_property
-    def log(self):
-        return SimLog(f"cocotb.function.{self._coro.__qualname__}.{id(self)}")
+    .. versionchanged:: 2.0
+        No longer implemented as a unique type.
+        The ``log`` attribute is no longer available.
+    """
 
-    def __call__(self, *args, **kwargs):
-        return cocotb.scheduler._queue_function(self._coro(*args, **kwargs))
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        return cocotb.scheduler._queue_function(func(*args, **kwargs))
 
-    def __get__(self, obj, owner=None):
-        """Permit the decorator to be used on class methods
-        and standalone functions"""
-        return type(self)(self._coro._func.__get__(obj, owner))
+    return wrapper
 
 
-class external:
-    """Decorator to apply to an external function to enable calling from cocotb.
+def external(func: Callable[..., Result]) -> Callable[..., Coroutine[Any, Any, Result]]:
+    """Decorator that turns a blocking function into a :term:`coroutine function`.
 
     This turns a normal function that isn't a coroutine into a blocking coroutine.
     Currently, this creates a new execution thread for each function that is
     called.
-    Scope for this to be streamlined to a queue in future.
-    """
 
-    def __init__(self, func):
-        self._func = func
-        self._log = SimLog(f"cocotb.external.{self._func.__qualname__}.{id(self)}")
+    .. versionchanged:: 2.0
+        No longer implemented as a unique type.
+        The ``log`` attribute is no longer available.
+    """
 
-    def __call__(self, *args, **kwargs):
-        return cocotb.scheduler._run_in_executor(self._func, *args, **kwargs)
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        return cocotb.scheduler._run_in_executor(func, *args, **kwargs)
 
-    def __get__(self, obj, owner=None):
-        """Permit the decorator to be used on class methods
-        and standalone functions"""
-        return type(self)(self._func.__get__(obj, owner))
+    return wrapper
 
 
 class Test(coroutine):
@@ -172,7 +166,7 @@ def test(
     expect_error: Union[bool, Type[Exception], Sequence[Type[Exception]]] = (),
     skip: bool = False,
     stage: int = 0,
-) -> Callable[[Callable[..., None]], Test]:
+) -> Callable[[Callable[..., Coroutine[Any, Any, None]]], Test]:
     """
     Decorator to mark a Callable which returns a Coroutine as a test.
 
diff --git a/cocotb/triggers.py b/cocotb/triggers.py
--- a/cocotb/triggers.py
+++ b/cocotb/triggers.py
@@ -36,9 +36,9 @@
 from typing import Any, Coroutine, Optional, TypeVar, Union
 
 import cocotb
+import cocotb.task
 from cocotb import outcomes, simulator
 from cocotb.log import SimLog
-from cocotb.task import Task
 from cocotb.utils import (
     ParametrizedSingleton,
     get_sim_steps,
@@ -766,7 +766,7 @@ def __init__(self, *triggers):
 
         # Do some basic type-checking up front, rather than waiting until we
         # await them.
-        allowed_types = (Trigger, Waitable, Task)
+        allowed_types = (Trigger, Waitable, cocotb.task.Task)
         for trigger in self.triggers:
             if not isinstance(trigger, allowed_types):
                 raise TypeError(
@@ -781,7 +781,8 @@ def __repr__(self):
         return "{}({})".format(
             type(self).__qualname__,
             ", ".join(
-                repr(Join(t)) if isinstance(t, Task) else repr(t) for t in self.triggers
+                repr(Join(t)) if isinstance(t, cocotb.task.Task) else repr(t)
+                for t in self.triggers
             ),
         )
 
@@ -922,7 +923,7 @@ def __repr__(self):
 
 
 async def with_timeout(
-    trigger: Union[Trigger, Waitable, Task, Coroutine[Any, Any, T]],
+    trigger: Union[Trigger, Waitable, "cocotb.task.Task", Coroutine[Any, Any, T]],
     timeout_time: Union[float, Decimal],
     timeout_unit: str = "step",
     round_mode: Optional[str] = None,


Test patch:
diff --git a/tests/test_cases/test_external/test_external.py b/tests/test_cases/test_external/test_external.py
--- a/tests/test_cases/test_external/test_external.py
+++ b/tests/test_cases/test_external/test_external.py
@@ -100,9 +100,9 @@ async def test_time_in_function(dut):
     """
 
     @cocotb.function
-    def wait_cycles(dut, n):
+    async def wait_cycles(dut, n):
         for _ in range(n):
-            yield RisingEdge(dut.clk)
+            await RisingEdge(dut.clk)
 
     @external
     def wait_cycles_wrapper(dut, n):
@@ -311,9 +311,8 @@ async def test_function_returns_exception(dut):
     """
 
     @cocotb.function
-    def gen_func():
+    async def gen_func():
         return ValueError()
-        yield
 
     @external
     def ext():