Skip to content

Commit 070262e

Browse files
authored
Merge pull request #1080 from python-cmd2/ipy_locals
ipy command now includes all of self.py_locals in the IPython environment
2 parents f1aea75 + 1696cf1 commit 070262e

File tree

3 files changed

+66
-67
lines changed

3 files changed

+66
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
attribute added to the cmd2 instance itself.
3838
* Raising ``SystemExit`` or calling ``sys.exit()`` in a command or hook function will set ``self.exit_code``
3939
to the exit code used in those calls. It will also result in the command loop stopping.
40+
* ipy command now includes all of `self.py_locals` in the IPython environment
4041

4142
## 1.5.0 (January 31, 2021)
4243
* Bug Fixes

cmd2/cmd2.py

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@
4848
from contextlib import (
4949
redirect_stdout,
5050
)
51-
from pathlib import (
52-
Path,
53-
)
5451
from types import (
5552
ModuleType,
5653
)
@@ -168,7 +165,7 @@
168165
try:
169166
# noinspection PyUnresolvedReferences,PyPackageRequirements
170167
from IPython import ( # type: ignore[import]
171-
embed,
168+
start_ipython,
172169
)
173170
except ImportError: # pragma: no cover
174171
ipython_available = False
@@ -223,9 +220,9 @@ def __init__(
223220
stdin: Optional[TextIO] = None,
224221
stdout: Optional[TextIO] = None,
225222
*,
226-
persistent_history_file: Path = '',
223+
persistent_history_file: str = '',
227224
persistent_history_length: int = 1000,
228-
startup_script: Path = '',
225+
startup_script: str = '',
229226
silent_startup_script: bool = False,
230227
use_ipython: bool = False,
231228
allow_cli_args: bool = True,
@@ -4059,13 +4056,13 @@ def py_quit():
40594056
# This is to prevent pyscripts from editing it. (e.g. locals().clear()). It also ensures a pyscript's
40604057
# environment won't be filled with data from a previously run pyscript. Only make a shallow copy since
40614058
# it's OK for py_locals to contain objects which are editable in a pyscript.
4062-
localvars = dict(self.py_locals)
4063-
localvars[self.py_bridge_name] = py_bridge
4064-
localvars['quit'] = py_quit
4065-
localvars['exit'] = py_quit
4059+
local_vars = self.py_locals.copy()
4060+
local_vars[self.py_bridge_name] = py_bridge
4061+
local_vars['quit'] = py_quit
4062+
local_vars['exit'] = py_quit
40664063

40674064
if self.self_in_py:
4068-
localvars['self'] = self
4065+
local_vars['self'] = self
40694066

40704067
# Handle case where we were called by run_pyscript
40714068
if pyscript is not None:
@@ -4079,16 +4076,16 @@ def py_quit():
40794076
self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
40804077
return
40814078

4082-
localvars['__name__'] = '__main__'
4083-
localvars['__file__'] = expanded_filename
4079+
local_vars['__name__'] = '__main__'
4080+
local_vars['__file__'] = expanded_filename
40844081

40854082
# Place the script's directory at sys.path[0] just as Python does when executing a script
40864083
saved_sys_path = list(sys.path)
40874084
sys.path.insert(0, os.path.dirname(os.path.abspath(expanded_filename)))
40884085

40894086
else:
40904087
# This is the default name chosen by InteractiveConsole when no locals are passed in
4091-
localvars['__name__'] = '__console__'
4088+
local_vars['__name__'] = '__console__'
40924089

40934090
if args.command:
40944091
py_code_to_run = args.command
@@ -4100,7 +4097,7 @@ def py_quit():
41004097
py_bridge.cmd_echo = True
41014098

41024099
# Create the Python interpreter
4103-
interp = InteractiveConsole(locals=localvars)
4100+
interp = InteractiveConsole(locals=local_vars)
41044101

41054102
# Check if we are running Python code
41064103
if py_code_to_run:
@@ -4197,47 +4194,55 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]:
41974194
41984195
:return: True if running of commands should stop
41994196
"""
4197+
# noinspection PyPackageRequirements
4198+
from IPython.terminal.interactiveshell import (
4199+
TerminalInteractiveShell,
4200+
)
4201+
from IPython.terminal.ipapp import (
4202+
TerminalIPythonApp,
4203+
)
4204+
from traitlets.config.loader import (
4205+
Config as TraitletsConfig,
4206+
)
4207+
42004208
from .py_bridge import (
42014209
PyBridge,
42024210
)
42034211

4204-
# noinspection PyUnusedLocal
4205-
def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge):
4206-
"""
4207-
Embed an IPython shell in an environment that is restricted to only the variables in this function
4208-
4209-
:param cmd2_app: instance of the cmd2 app
4210-
:param py_bridge: a PyBridge
4211-
"""
4212-
# Create a variable pointing to py_bridge and name it using the value of py_bridge_name
4213-
exec("{} = py_bridge".format(cmd2_app.py_bridge_name))
4214-
4215-
# Add self variable pointing to cmd2_app, if allowed
4216-
if cmd2_app.self_in_py:
4217-
exec("self = cmd2_app")
4218-
4219-
# Delete these names from the environment so IPython can't use them
4220-
del cmd2_app
4221-
del py_bridge
4222-
4223-
# Start ipy shell
4224-
embed(
4225-
banner1=(
4226-
'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n'
4227-
'Run Python code from external files with: run filename.py\n'
4228-
),
4229-
exit_msg='Leaving IPython, back to {}'.format(sys.argv[0]),
4230-
)
4231-
42324212
if self.in_pyscript():
42334213
self.perror("Recursively entering interactive Python shells is not allowed")
42344214
return
42354215

42364216
try:
42374217
self._in_py = True
4238-
new_py_bridge = PyBridge(self)
4239-
load_ipy(self, new_py_bridge)
4240-
return new_py_bridge.stop
4218+
py_bridge = PyBridge(self)
4219+
4220+
# Make a copy of self.py_locals for the locals dictionary in the IPython environment we are creating.
4221+
# This is to prevent ipy from editing it. (e.g. locals().clear()). Only make a shallow copy since
4222+
# it's OK for py_locals to contain objects which are editable in ipy.
4223+
local_vars = self.py_locals.copy()
4224+
local_vars[self.py_bridge_name] = py_bridge
4225+
if self.self_in_py:
4226+
local_vars['self'] = self
4227+
4228+
# Configure IPython
4229+
config = TraitletsConfig()
4230+
config.InteractiveShell.banner2 = (
4231+
'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n'
4232+
'Run Python code from external files with: run filename.py\n'
4233+
)
4234+
4235+
# Start IPython
4236+
start_ipython(config=config, argv=[], user_ns=local_vars)
4237+
4238+
# The IPython application is a singleton and won't be recreated next time
4239+
# this function runs. That's a problem since the contents of local_vars
4240+
# may need to be changed. Therefore we must destroy all instances of the
4241+
# relevant classes.
4242+
TerminalIPythonApp.clear_instance()
4243+
TerminalInteractiveShell.clear_instance()
4244+
4245+
return py_bridge.stop
42414246
finally:
42424247
self._in_py = False
42434248

docs/features/embedded_python_shells.rst

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ arguments, it enters an interactive Python session. The session can call
88
your ``cmd2`` application while maintaining isolation.
99

1010
You may optionally enable full access to to your application by setting
11-
``self_in_py`` to ``True``. Enabling this flag adds ``self`` to the python
12-
session, which is a reference to your ``cmd2`` application. This can be useful
13-
for debugging your application.
11+
``self.self_in_py`` to ``True``. Enabling this flag adds ``self`` to the
12+
python session, which is a reference to your ``cmd2`` application. This can be
13+
useful for debugging your application.
14+
15+
Any local or global variable created within the Python session will not persist
16+
in the CLI's environment.
17+
18+
Anything in ``self.py_locals`` is always available in the Python environment.
1419

1520
The ``app`` object (or your custom name) provides access to application
1621
commands through raw commands. For example, any application command call be
1722
called with ``app("<command>")``.
1823

19-
::
20-
21-
>>> app('say --piglatin Blah')
22-
lahBay
23-
2424
More Python examples:
2525

2626
::
@@ -51,14 +51,6 @@ More Python examples:
5151
>>> quit()
5252
Python was here >
5353

54-
Using the ``py`` command is tightly integrated with your main ``cmd2``
55-
application and any variables created or changed will persist for the life of
56-
the application::
57-
58-
(Cmd) py x = 5
59-
(Cmd) py print(x)
60-
5
61-
6254
The ``py`` command also allows you to run Python scripts via ``py
6355
run('myscript.py')``. This provides a more complicated and more powerful
6456
scripting capability than that provided by the simple text file scripts
@@ -114,12 +106,13 @@ be present::
114106

115107
The ``ipy`` command enters an interactive IPython_ session. Similar to an
116108
interactive Python session, this shell can access your application instance via
117-
``self`` and any changes to your application made via ``self`` will persist.
118-
However, any local or global variable created within the ``ipy`` shell will not
119-
persist. Within the ``ipy`` shell, you cannot call "back" to your application
120-
with ``cmd("")``, however you can run commands directly like so::
109+
``self`` if ``self.self_in_py`` is ``True`` and any changes to your application
110+
made via ``self`` will persist. However, any local or global variable created
111+
within the ``ipy`` shell will not persist in the CLI's environment
121112

122-
self.onecmd_plus_hooks('help')
113+
Also, as in the interactive Python session, the ``ipy`` shell has access to the
114+
contents of ``self.py_locals`` and can call back into the application using the
115+
``app`` object (or your custom name).
123116

124117
IPython_ provides many advantages, including:
125118

0 commit comments

Comments
 (0)