Repository: cocotb/cocotb

Issue:
Make `cocotb.scheduler.create_task` a public API
By creating a top-level alias `cocotb.create_task`. This function will turn a coroutine into a Task, but will not schedule it. Later the user can schedule that Task using `cocotb.start_soon` or `cocotb.start`.


Discussion:

Resolving patch:
diff --git a/cocotb/__init__.py b/cocotb/__init__.py
--- a/cocotb/__init__.py
+++ b/cocotb/__init__.py
@@ -196,6 +196,18 @@ async def start(coro: Union[RunningTask, Coroutine]) -> RunningTask:
     return task
 
 
+def create_task(coro: Union[RunningTask, Coroutine]) -> RunningTask:
+    """
+    Constructs a coroutine into a Task without scheduling the Task.
+
+    The Task can later be scheduled with :func:`cocotb.fork`, :func:`cocotb.start`, or
+    :func:`cocotb.start_soon`.
+
+    .. versionadded:: 1.6.0
+    """
+    return cocotb.scheduler.create_task(coro)
+
+
 # FIXME is this really required?
 _rlock = threading.RLock()
 


Test patch:
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
@@ -10,12 +10,25 @@
 """
 import logging
 import re
+from typing import Coroutine
 
 import cocotb
-from cocotb.triggers import Join, Timer, RisingEdge, Trigger, NullTrigger, Combine, Event, ReadOnly, First
+import pytest
 from cocotb.clock import Clock
-from common import clock_gen
+from cocotb.decorators import RunningTask
+from cocotb.triggers import (
+    Combine,
+    Event,
+    First,
+    Join,
+    NullTrigger,
+    ReadOnly,
+    RisingEdge,
+    Timer,
+    Trigger,
+)
 
+from common import clock_gen
 
 test_flag = False
 
@@ -543,7 +556,7 @@ async def coro():
     await Timer(1, 'step')
     assert task1._finished
 
-    task2 = cocotb.scheduler.create_task(coro())
+    task2 = cocotb.create_task(coro())
     task3 = await cocotb.start(task2)
     assert task3 is task2
 
@@ -595,3 +608,55 @@ async def coro():
     assert coro_started is True
     await Timer(1, 'step')  # await a GPITrigger to ensure control returns to simulator
     assert sim_resumed is True
+
+
+@cocotb.test()
+async def test_create_task(_):
+
+    # proper construction from coroutines
+    async def coro():
+        pass
+
+    assert type(cocotb.create_task(coro())) == RunningTask
+
+    # proper construction from Coroutine objects
+    class CoroType(Coroutine):
+        def __init__(self):
+            self._coro = coro()
+
+        def send(self, value):
+            return self._coro.send(value)
+
+        def throw(self, exception):
+            self._coro.throw(exception)
+
+        def close(self):
+            self._coro.close()
+
+        def __await__(self):
+            yield from self._coro.__await__()
+
+    assert type(cocotb.create_task(CoroType())) == RunningTask
+
+    # fail if given async generators
+    async def agen():
+        yield None
+
+    with pytest.raises(TypeError):
+        cocotb.create_task(agen())
+
+    # fail if given coroutine function
+    with pytest.raises(TypeError):
+        cocotb.create_task(coro)
+
+    # fail if given Coroutine Type
+    with pytest.raises(TypeError):
+        cocotb.create_task(CoroType)
+
+    # fail if given async generator function
+    with pytest.raises(TypeError):
+        cocotb.create_task(agen)
+
+    # fail if given random type
+    with pytest.raises(TypeError):
+        cocotb.create_task(object())