Fix IPython history corruption when using launch_repl.

PiperOrigin-RevId: 508064761
Change-Id: I58fa2e4f35b57a1ab68c3172ba5c0d4495e40ced
This commit is contained in:
Saran Tunyasuvunakool
2023-02-08 06:36:52 -08:00
committed by Copybara-Service
parent 48e95817e4
commit 7d8e810ab1
3 changed files with 86 additions and 16 deletions
+12 -2
View File
@@ -2,9 +2,19 @@
Changelog Changelog
========= =========
Version 2.3.2 (February 7, 2023) Upcoming version (not yet released)
----------------------------------- -----------------------------------
Python bindings
^^^^^^^^^^^^^^^
- Fixed IPython history corruption when using ``launch_repl``. The ``launch_repl`` function now provides seamless
continuation of an IPython interactive shell session, and is no longer considered experimental feature.
Version 2.3.2 (February 7, 2023)
--------------------------------
General General
^^^^^^^ ^^^^^^^
@@ -54,7 +64,7 @@ Plugins
Version 2.3.1 (December 6, 2022) Version 2.3.1 (December 6, 2022)
----------------------------------- --------------------------------
Python bindings Python bindings
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
+4 -1
View File
@@ -88,11 +88,14 @@ PYBIND11_MODULE(_simulate, pymodule) {
.def_readonly("run", &mujoco::Simulate::run, .def_readonly("run", &mujoco::Simulate::run,
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def_property_readonly( .def_property(
"exitrequest", "exitrequest",
[](mujoco::Simulate& simulate) { [](mujoco::Simulate& simulate) {
return simulate.exitrequest.load(); return simulate.exitrequest.load();
}, },
[](mujoco::Simulate& simulate, bool exitrequest) {
simulate.exitrequest.store(exitrequest);
},
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def_property_readonly( .def_property_readonly(
+70 -13
View File
@@ -19,6 +19,7 @@ import code
import inspect import inspect
import math import math
import os import os
import sys
import threading import threading
import time import time
from typing import Callable, Optional, Tuple, Union from typing import Callable, Optional, Tuple, Union
@@ -213,7 +214,8 @@ def _launch_internal(model: Optional[mujoco.MjModel] = None,
data: Optional[mujoco.MjData] = None, data: Optional[mujoco.MjData] = None,
*, *,
run_physics_thread: bool = True, run_physics_thread: bool = True,
loader: Optional[_InternalLoaderType] = None) -> None: loader: Optional[_InternalLoaderType] = None,
simulate: Optional[Simulate] = None) -> None:
"""Internal API, so that the public API has more readable type annotations.""" """Internal API, so that the public API has more readable type annotations."""
if model is None and data is not None: if model is None and data is not None:
raise ValueError('mjData is specified but mjModel is not') raise ValueError('mjData is specified but mjModel is not')
@@ -233,7 +235,8 @@ def _launch_internal(model: Optional[mujoco.MjModel] = None,
loader = _loader loader = _loader
# The simulate object encapsulates the UI. # The simulate object encapsulates the UI.
simulate = Simulate() if simulate is None:
simulate = Simulate()
# Initialize GLFW. # Initialize GLFW.
if not glfw.init(): if not glfw.init():
@@ -269,25 +272,79 @@ def launch_from_path(path: str) -> None:
def launch_repl(model: mujoco.MjModel, data: mujoco.MjData) -> None: def launch_repl(model: mujoco.MjModel, data: mujoco.MjData) -> None:
"""EXPERIMENTAL FEATURE: Launches the Simulate GUI in REPL mode.""" """Launches the Simulate GUI in REPL mode."""
ipython_shell = None
try: try:
import IPython # pylint: disable=g-import-not-at-top import IPython # pylint: disable=g-import-not-at-top
has_ipython = True ipython_shell = IPython.get_ipython()
ipython_is_terminal_interactive_shell = isinstance(
ipython_shell,
IPython.terminal.interactiveshell.TerminalInteractiveShell)
except ImportError: except ImportError:
has_ipython = False ipython_is_terminal_interactive_shell = False
simulate = Simulate()
viewer_is_running = True
def start_shell(global_variables): def start_shell(global_variables):
if has_ipython and IPython.get_ipython() is not None: if ipython_is_terminal_interactive_shell:
locals().update(global_variables) ipython_shell.execution_count += 1
IPython.embed()
# A SQLite connection can only be used on the same thread that opened it.
# We cache the existing connection and reopen on the current thread.
old_db = ipython_shell.history_manager.db
ipython_shell.history_manager.init_db()
ipython_shell.history_manager.new_session()
try:
# Replicate IPython main loop without exiting on keyboard interrupt,
# unless the viewer window has already been closed.
# (https://github.com/ipython/ipython/blob/8.9.0/IPython/terminal/interactiveshell.py#L701)
while viewer_is_running and ipython_shell.keep_running:
print(ipython_shell.separate_in, end='')
try:
c = ipython_shell.prompt_for_code()
except EOFError:
if not ipython_shell.confirm_exit or ipython_shell.ask_yes_no(
'Do you really want to exit ([y]/n)?', 'y', 'n'):
ipython_shell.ask_exit()
if not ipython_shell.keep_running and simulate is not None:
simulate.exitrequest = True
else:
if c:
ipython_shell.run_cell(c, store_history=True)
finally:
# Close the temporary history DB connection and restore the old one.
ipython_shell.history_manager.end_session()
ipython_shell.history_manager.db.close()
ipython_shell.history_manager.db = old_db
ipython_shell.execution_count -= 1
else: else:
code.InteractiveConsole(locals=global_variables).interact() code.InteractiveConsole(locals=global_variables).interact()
repl_thread = threading.Thread( # End IPython history session on the main thread. We will need to open
target=start_shell, args=(inspect.stack()[1][0].f_globals,)) # a new session in the REPL thread.
repl_thread.start() if ipython_is_terminal_interactive_shell:
launch(model, data, run_physics_thread=False) ipython_shell.history_manager.end_session()
repl_thread.join()
try:
# Continue the IPython REPL session in a separate thread.
repl_thread = threading.Thread(
target=start_shell, args=(inspect.stack()[1][0].f_globals,))
repl_thread.start()
# Launch the viewer on the main thread.
_launch_internal(
model, data, run_physics_thread=False, simulate=simulate)
simulate = None
# Wait until the REPL thread quits, then restore the IPython history
# DB session on the main thread.
viewer_is_running = False
repl_thread.join()
finally:
if ipython_is_terminal_interactive_shell:
ipython_shell.history_manager.new_session()
if __name__ == '__main__': if __name__ == '__main__':