Repository: cocotb/cocotb

Issue:
Add support for integration with IPython's "autoawait" feature
At the moment, it is not possible to run cocotb coroutines from an IPython shell. 
IPython is able to run asynchronous code (see https://ipython.readthedocs.io/en/stable/interactive/autoawait.html), but support for this has to be added to the scheduler.




Discussion:
Should this be a scheduler "feature", or could we come up with something cleaner?

We could modify cocotb's start up so that once loaded cocotb will jump to a user specified entry point (we already do this sort of by passing cocotb the test module), which could be the regression manager, or could be IPython, or whatever. Ideally, the scheduler should be a stand-alone component: I should be able to use the scheduler in a purely Python environment. At that point, what we are trying to achieve is trivial.

Everything in cocotb is just too tightly coupled for my tastes.
I think step 1 is decoupling the concept of tests (ie, the regression manager) from the scheduler. I made a start on simplifying the interface, but they're still couple's.
Tried to make a start on this, but I can't work out how to set the hook:

https://gitter.im/ipython/ipython?at=5e8f21e09316f34b8d8ebed3

Resolving patch:
diff --git a/cocotb/ipython_support.py b/cocotb/ipython_support.py
new file mode 100644
--- /dev/null
+++ b/cocotb/ipython_support.py
@@ -0,0 +1,89 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+import IPython
+from IPython.terminal.ipapp import load_default_config
+from IPython.terminal.prompts import Prompts, Token
+
+import cocotb
+
+
+class SimTimePrompt(Prompts):
+    """ custom prompt that shows the sim time after a trigger fires """
+    _show_time = 1
+
+    def in_prompt_tokens(self, cli=None):
+        tokens = super().in_prompt_tokens()
+        if self._show_time == self.shell.execution_count:
+            tokens = [
+                (Token.Comment, "sim time: {}".format(cocotb.utils.get_sim_time())),
+                (Token.Text, "\n"),
+            ] + tokens
+        return tokens
+
+
+def _runner(shell, x):
+    """ Handler for async functions """
+    ret = cocotb.scheduler.queue_function(x)
+    shell.prompts._show_time = shell.execution_count
+    return ret
+
+
+async def embed(user_ns: dict = {}):
+    """
+    Start an ipython shell in the current coroutine.
+
+    Unlike using :func:`IPython.embed` directly, the :keyword:`await` keyword
+    can be used directly from the shell to wait for triggers.
+    The :keyword:`yield` keyword from the legacy :ref:`yield-syntax` is not supported.
+
+    This coroutine will complete only when the user exits the interactive session.
+
+    Args:
+        user_ns:
+            The variables to have made available in the shell.
+            Passing ``locals()`` is often a good idea.
+            ``cocotb`` will automatically be included.
+
+    Notes:
+
+        If your simulator does not provide an appropriate ``stdin``, you may
+        find you cannot type in the resulting shell. Using simulators in batch
+        or non-GUI mode may resolve this. This feature is experimental, and
+        not all simulators are supported.
+    """
+    # ensure cocotb is in the namespace, for convenience
+    default_ns = dict(cocotb=cocotb)
+    default_ns.update(user_ns)
+
+    # build the config to enable `await`
+    c = load_default_config()
+    c.TerminalInteractiveShell.loop_runner = lambda x: _runner(shell, x)
+    c.TerminalInteractiveShell.autoawait = True
+
+    # create a shell with access to the dut, and cocotb pre-imported
+    shell = IPython.terminal.embed.InteractiveShellEmbed(
+        user_ns=default_ns,
+        config=c,
+    )
+
+    # add our custom prompts
+    shell.prompts = SimTimePrompt(shell)
+
+    # start the shell in a background thread
+    @cocotb.external
+    def run_shell():
+        shell()
+    await run_shell()
+
+
+@cocotb.test()
+async def run_ipython(dut):
+    """ A test that launches an interactive Python shell.
+
+    Do not call this directly - use this as ``make MODULE=cocotb.ipython_support``.
+
+    Within the shell, a global ``dut`` variable pointing to the design will be present.
+    """
+    await cocotb.triggers.Timer(0)  # workaround for gh-637
+    await embed(user_ns=dict(dut=dut))


Test patch:
diff --git a/examples/dff/tests/Makefile b/examples/dff/tests/Makefile
--- a/examples/dff/tests/Makefile
+++ b/examples/dff/tests/Makefile
@@ -32,6 +32,3 @@ endif
 
 include $(shell cocotb-config --makefiles)/Makefile.inc
 include $(shell cocotb-config --makefiles)/Makefile.sim
-
-# list all required Python files here
-sim: $(MODULE).py