Skip to content

Commit 9156618

Browse files
committed
Fixed bug where pyscripts could edit cmd2.Cmd.py_locals dictionary.
Fixed bug where cmd2 set sys.path[0] for a pyscript to its cwd instead of the script's directory. Fixed bug where sys.path was not being restored after a pyscript ran. Setting the following pyscript variables: __name__: __main__ __file__: script path (as typed) Removed do_py.run() function since it didn't handle arguments and offered no benefit over run_pyscript.
1 parent 013b9e0 commit 9156618

File tree

8 files changed

+88
-64
lines changed

8 files changed

+88
-64
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
* Corrected issue where the actual new value was not always being printed in do_set. This occurred in cases where
44
the typed value differed from what the setter had converted it to.
55
* Fixed bug where ANSI style sequences were not correctly handled in `utils.truncate_line()`.
6+
* Fixed bug where pyscripts could edit `cmd2.Cmd.py_locals` dictionary.
7+
* Fixed bug where cmd2 set sys.path[0] for a pyscript to cmd2's working directory instead of the
8+
script file's directory.
9+
* Fixed bug where sys.path was not being restored after a pyscript ran.
610
* Enhancements
711
* Renamed set command's `-l/--long` flag to `-v/--verbose` for consistency with help and history commands.
12+
* Setting the following pyscript variables:
13+
* `__name__`: __main__
14+
* `__file__`: script path (as typed)
815

916
## 0.10.0 (February 7, 2020)
1017
* Enhancements

cmd2/cmd2.py

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3093,8 +3093,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
30933093

30943094
# This is a hidden flag for telling do_py to run a pyscript. It is intended only to be used by run_pyscript
30953095
# after it sets up sys.argv for the script being run. When this flag is present, it takes precedence over all
3096-
# other arguments. run_pyscript uses this method instead of "py run('file')" because file names with
3097-
# 2 or more consecutive spaces cause issues with our parser, which isn't meant to parse Python statements.
3096+
# other arguments.
30983097
py_parser.add_argument('--pyscript', help=argparse.SUPPRESS)
30993098

31003099
# Preserve quotes since we are passing these strings to Python
@@ -3104,65 +3103,66 @@ def do_py(self, args: argparse.Namespace) -> Optional[bool]:
31043103
Enter an interactive Python shell
31053104
:return: True if running of commands should stop
31063105
"""
3106+
def py_quit():
3107+
"""Function callable from the interactive Python console to exit that environment"""
3108+
raise EmbeddedConsoleExit
3109+
31073110
from .py_bridge import PyBridge
3111+
py_bridge = PyBridge(self)
3112+
saved_sys_path = None
3113+
31083114
if self.in_pyscript():
31093115
err = "Recursively entering interactive Python consoles is not allowed."
31103116
self.perror(err)
31113117
return
31123118

3113-
py_bridge = PyBridge(self)
3114-
py_code_to_run = ''
3115-
3116-
# Handle case where we were called by run_pyscript
3117-
if args.pyscript:
3118-
args.pyscript = utils.strip_quotes(args.pyscript)
3119-
3120-
# Run the script - use repr formatting to escape things which
3121-
# need to be escaped to prevent issues on Windows
3122-
py_code_to_run = 'run({!r})'.format(args.pyscript)
3123-
3124-
elif args.command:
3125-
py_code_to_run = args.command
3126-
if args.remainder:
3127-
py_code_to_run += ' ' + ' '.join(args.remainder)
3128-
3129-
# Set cmd_echo to True so PyBridge statements like: py app('help')
3130-
# run at the command line will print their output.
3131-
py_bridge.cmd_echo = True
3132-
31333119
try:
31343120
self._in_py = True
3121+
py_code_to_run = ''
31353122

3136-
def py_run(filename: str):
3137-
"""Run a Python script file in the interactive console.
3138-
:param filename: filename of script file to run
3139-
"""
3140-
expanded_filename = os.path.expanduser(filename)
3123+
# Locals for the Python environment we are creating
3124+
localvars = dict(self.py_locals)
3125+
localvars[self.py_bridge_name] = py_bridge
3126+
localvars['quit'] = py_quit
3127+
localvars['exit'] = py_quit
3128+
3129+
if self.self_in_py:
3130+
localvars['self'] = self
3131+
3132+
# Handle case where we were called by run_pyscript
3133+
if args.pyscript:
3134+
# Read the script file
3135+
expanded_filename = os.path.expanduser(utils.strip_quotes(args.pyscript))
31413136

31423137
try:
31433138
with open(expanded_filename) as f:
3144-
interp.runcode(f.read())
3139+
py_code_to_run = f.read()
31453140
except OSError as ex:
31463141
self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
3142+
return
31473143

3148-
def py_quit():
3149-
"""Function callable from the interactive Python console to exit that environment"""
3150-
raise EmbeddedConsoleExit
3144+
localvars['__name__'] = '__main__'
3145+
localvars['__file__'] = expanded_filename
31513146

3152-
# Set up Python environment
3153-
self.py_locals[self.py_bridge_name] = py_bridge
3154-
self.py_locals['run'] = py_run
3155-
self.py_locals['quit'] = py_quit
3156-
self.py_locals['exit'] = py_quit
3147+
# Place the script's directory at sys.path[0] just as Python does when executing a script
3148+
saved_sys_path = list(sys.path)
3149+
sys.path.insert(0, os.path.dirname(os.path.abspath(expanded_filename)))
31573150

3158-
if self.self_in_py:
3159-
self.py_locals['self'] = self
3160-
elif 'self' in self.py_locals:
3161-
del self.py_locals['self']
3151+
else:
3152+
# This is the default name chosen by InteractiveConsole when no locals are passed in
3153+
localvars['__name__'] = '__console__'
3154+
3155+
if args.command:
3156+
py_code_to_run = args.command
3157+
if args.remainder:
3158+
py_code_to_run += ' ' + ' '.join(args.remainder)
31623159

3163-
localvars = self.py_locals
3160+
# Set cmd_echo to True so PyBridge statements like: py app('help')
3161+
# run at the command line will print their output.
3162+
py_bridge.cmd_echo = True
3163+
3164+
# Create the Python interpreter
31643165
interp = InteractiveConsole(locals=localvars)
3165-
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
31663166

31673167
# Check if we are running Python code
31683168
if py_code_to_run:
@@ -3177,8 +3177,7 @@ def py_quit():
31773177
else:
31783178
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
31793179
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
3180-
'Non-Python commands can be issued with: {}("your command")\n'
3181-
'Run Python code from external script files with: run("script.py")'
3180+
'Non-Python commands can be issued with: {}("your command")'
31823181
.format(self.py_bridge_name))
31833182

31843183
saved_cmd2_env = None
@@ -3205,7 +3204,10 @@ def py_quit():
32053204
pass
32063205

32073206
finally:
3208-
self._in_py = False
3207+
with self.sigint_protection:
3208+
if saved_sys_path is not None:
3209+
sys.path = saved_sys_path
3210+
self._in_py = False
32093211

32103212
return py_bridge.stop
32113213

tests/pyscript/environment.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# flake8: noqa F821
2+
# Tests that cmd2 populates __name__, __file__, and sets sys.path[0] to our directory
3+
import os
4+
import sys
5+
app.cmd_echo = True
6+
7+
if __name__ != '__main__':
8+
print("Error: __name__ is: {}".format(__name__))
9+
quit()
10+
11+
if __file__ != sys.argv[0]:
12+
print("Error: __file__ is: {}".format(__file__))
13+
quit()
14+
15+
our_dir = os.path.dirname(os.path.abspath(__file__))
16+
if our_dir != sys.path[0]:
17+
print("Error: our_dir is: {}".format(our_dir))
18+
quit()
19+
20+
print("PASSED")

tests/pyscript/recursive.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed
66
"""
77
import os
8+
import sys
89

910
app.cmd_echo = True
1011
my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))

tests/pyscript/run.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

tests/pyscript/to_run.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

tests/test_cmd2.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,17 @@ def test_base_shell(base_app, monkeypatch):
205205

206206

207207
def test_base_py(base_app):
208-
# Create a variable and make sure we can see it
209-
out, err = run_cmd(base_app, 'py qqq=3')
210-
assert not out
208+
# Make sure py can't edit Cmd.py_locals. It used to be that cmd2 was passing its py_locals
209+
# dictionary to the py environment instead of a copy.
210+
base_app.py_locals['test_var'] = 5
211+
out, err = run_cmd(base_app, 'py del[locals()["test_var"]]')
212+
assert not out and not err
213+
assert base_app.py_locals['test_var'] == 5
211214

212-
out, err = run_cmd(base_app, 'py print(qqq)')
213-
assert out[0].rstrip() == '3'
215+
out, err = run_cmd(base_app, 'py print(test_var)')
216+
assert out[0].rstrip() == '5'
214217

215-
# Add a more complex statement
218+
# Try a print statement
216219
out, err = run_cmd(base_app, 'py print("spaces" + " in this " + "command")')
217220
assert out[0].rstrip() == 'spaces in this command'
218221

tests/test_run_pyscript.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,9 @@ def test_run_pyscript_stop(base_app, request):
117117
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
118118
assert stop
119119

120-
def test_run_pyscript_run(base_app, request):
120+
def test_run_pyscript_environment(base_app, request):
121121
test_dir = os.path.dirname(request.module.__file__)
122-
python_script = os.path.join(test_dir, 'pyscript', 'run.py')
123-
expected = 'I have been run'
122+
python_script = os.path.join(test_dir, 'pyscript', 'environment.py')
123+
out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script))
124124

125-
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
126-
assert expected in out
125+
assert out[0] == "PASSED"

0 commit comments

Comments
 (0)