diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 97 | ||||
-rw-r--r-- | docs/features/embedded_python_shells.rst | 35 |
3 files changed, 66 insertions, 67 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3b9472..cde4f34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ attribute added to the cmd2 instance itself. * Raising ``SystemExit`` or calling ``sys.exit()`` in a command or hook function will set ``self.exit_code`` to the exit code used in those calls. It will also result in the command loop stopping. + * ipy command now includes all of `self.py_locals` in the IPython environment ## 1.5.0 (January 31, 2021) * Bug Fixes diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 68ad8201..c91fb4db 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -48,9 +48,6 @@ from collections import ( from contextlib import ( redirect_stdout, ) -from pathlib import ( - Path, -) from types import ( ModuleType, ) @@ -168,7 +165,7 @@ ipython_available = True try: # noinspection PyUnresolvedReferences,PyPackageRequirements from IPython import ( # type: ignore[import] - embed, + start_ipython, ) except ImportError: # pragma: no cover ipython_available = False @@ -223,9 +220,9 @@ class Cmd(cmd.Cmd): stdin: Optional[TextIO] = None, stdout: Optional[TextIO] = None, *, - persistent_history_file: Path = '', + persistent_history_file: str = '', persistent_history_length: int = 1000, - startup_script: Path = '', + startup_script: str = '', silent_startup_script: bool = False, use_ipython: bool = False, allow_cli_args: bool = True, @@ -4059,13 +4056,13 @@ class Cmd(cmd.Cmd): # This is to prevent pyscripts from editing it. (e.g. locals().clear()). It also ensures a pyscript's # environment won't be filled with data from a previously run pyscript. Only make a shallow copy since # it's OK for py_locals to contain objects which are editable in a pyscript. - localvars = dict(self.py_locals) - localvars[self.py_bridge_name] = py_bridge - localvars['quit'] = py_quit - localvars['exit'] = py_quit + local_vars = self.py_locals.copy() + local_vars[self.py_bridge_name] = py_bridge + local_vars['quit'] = py_quit + local_vars['exit'] = py_quit if self.self_in_py: - localvars['self'] = self + local_vars['self'] = self # Handle case where we were called by run_pyscript if pyscript is not None: @@ -4079,8 +4076,8 @@ class Cmd(cmd.Cmd): self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex)) return - localvars['__name__'] = '__main__' - localvars['__file__'] = expanded_filename + local_vars['__name__'] = '__main__' + local_vars['__file__'] = expanded_filename # Place the script's directory at sys.path[0] just as Python does when executing a script saved_sys_path = list(sys.path) @@ -4088,7 +4085,7 @@ class Cmd(cmd.Cmd): else: # This is the default name chosen by InteractiveConsole when no locals are passed in - localvars['__name__'] = '__console__' + local_vars['__name__'] = '__console__' if args.command: py_code_to_run = args.command @@ -4100,7 +4097,7 @@ class Cmd(cmd.Cmd): py_bridge.cmd_echo = True # Create the Python interpreter - interp = InteractiveConsole(locals=localvars) + interp = InteractiveConsole(locals=local_vars) # Check if we are running Python code if py_code_to_run: @@ -4197,47 +4194,55 @@ class Cmd(cmd.Cmd): :return: True if running of commands should stop """ + # noinspection PyPackageRequirements + from IPython.terminal.interactiveshell import ( + TerminalInteractiveShell, + ) + from IPython.terminal.ipapp import ( + TerminalIPythonApp, + ) + from traitlets.config.loader import ( + Config as TraitletsConfig, + ) + from .py_bridge import ( PyBridge, ) - # noinspection PyUnusedLocal - def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge): - """ - Embed an IPython shell in an environment that is restricted to only the variables in this function - - :param cmd2_app: instance of the cmd2 app - :param py_bridge: a PyBridge - """ - # Create a variable pointing to py_bridge and name it using the value of py_bridge_name - exec("{} = py_bridge".format(cmd2_app.py_bridge_name)) - - # Add self variable pointing to cmd2_app, if allowed - if cmd2_app.self_in_py: - exec("self = cmd2_app") - - # Delete these names from the environment so IPython can't use them - del cmd2_app - del py_bridge - - # Start ipy shell - embed( - banner1=( - 'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n' - 'Run Python code from external files with: run filename.py\n' - ), - exit_msg='Leaving IPython, back to {}'.format(sys.argv[0]), - ) - if self.in_pyscript(): self.perror("Recursively entering interactive Python shells is not allowed") return try: self._in_py = True - new_py_bridge = PyBridge(self) - load_ipy(self, new_py_bridge) - return new_py_bridge.stop + py_bridge = PyBridge(self) + + # Make a copy of self.py_locals for the locals dictionary in the IPython environment we are creating. + # This is to prevent ipy from editing it. (e.g. locals().clear()). Only make a shallow copy since + # it's OK for py_locals to contain objects which are editable in ipy. + local_vars = self.py_locals.copy() + local_vars[self.py_bridge_name] = py_bridge + if self.self_in_py: + local_vars['self'] = self + + # Configure IPython + config = TraitletsConfig() + config.InteractiveShell.banner2 = ( + 'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n' + 'Run Python code from external files with: run filename.py\n' + ) + + # Start IPython + start_ipython(config=config, argv=[], user_ns=local_vars) + + # The IPython application is a singleton and won't be recreated next time + # this function runs. That's a problem since the contents of local_vars + # may need to be changed. Therefore we must destroy all instances of the + # relevant classes. + TerminalIPythonApp.clear_instance() + TerminalInteractiveShell.clear_instance() + + return py_bridge.stop finally: self._in_py = False diff --git a/docs/features/embedded_python_shells.rst b/docs/features/embedded_python_shells.rst index 8ff65ffe..cbedf992 100644 --- a/docs/features/embedded_python_shells.rst +++ b/docs/features/embedded_python_shells.rst @@ -8,19 +8,19 @@ arguments, it enters an interactive Python session. The session can call your ``cmd2`` application while maintaining isolation. You may optionally enable full access to to your application by setting -``self_in_py`` to ``True``. Enabling this flag adds ``self`` to the python -session, which is a reference to your ``cmd2`` application. This can be useful -for debugging your application. +``self.self_in_py`` to ``True``. Enabling this flag adds ``self`` to the +python session, which is a reference to your ``cmd2`` application. This can be +useful for debugging your application. + +Any local or global variable created within the Python session will not persist +in the CLI's environment. + +Anything in ``self.py_locals`` is always available in the Python environment. The ``app`` object (or your custom name) provides access to application commands through raw commands. For example, any application command call be called with ``app("<command>")``. -:: - - >>> app('say --piglatin Blah') - lahBay - More Python examples: :: @@ -51,14 +51,6 @@ More Python examples: >>> quit() Python was here > -Using the ``py`` command is tightly integrated with your main ``cmd2`` -application and any variables created or changed will persist for the life of -the application:: - - (Cmd) py x = 5 - (Cmd) py print(x) - 5 - The ``py`` command also allows you to run Python scripts via ``py run('myscript.py')``. This provides a more complicated and more powerful scripting capability than that provided by the simple text file scripts @@ -114,12 +106,13 @@ be present:: The ``ipy`` command enters an interactive IPython_ session. Similar to an interactive Python session, this shell can access your application instance via -``self`` and any changes to your application made via ``self`` will persist. -However, any local or global variable created within the ``ipy`` shell will not -persist. Within the ``ipy`` shell, you cannot call "back" to your application -with ``cmd("")``, however you can run commands directly like so:: +``self`` if ``self.self_in_py`` is ``True`` and any changes to your application +made via ``self`` will persist. However, any local or global variable created +within the ``ipy`` shell will not persist in the CLI's environment - self.onecmd_plus_hooks('help') +Also, as in the interactive Python session, the ``ipy`` shell has access to the +contents of ``self.py_locals`` and can call back into the application using the +``app`` object (or your custom name). IPython_ provides many advantages, including: |