summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-02-14 16:28:41 -0500
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-02-14 16:28:41 -0500
commit9156618a56d635bb51261d019a3703a1b4e3b588 (patch)
tree3f35d07cf668ec156dac6e97e612569c83aa36c6
parent013b9e0a2c75e17f8aa0e0f7cbe50d84d2f657d8 (diff)
downloadcmd2-git-9156618a56d635bb51261d019a3703a1b4e3b588.tar.gz
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.
-rw-r--r--CHANGELOG.md7
-rw-r--r--cmd2/cmd2.py92
-rw-r--r--tests/pyscript/environment.py20
-rw-r--r--tests/pyscript/recursive.py1
-rw-r--r--tests/pyscript/run.py6
-rw-r--r--tests/pyscript/to_run.py2
-rwxr-xr-xtests/test_cmd2.py15
-rw-r--r--tests/test_run_pyscript.py9
8 files changed, 88 insertions, 64 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9d71831..edf5f93f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,8 +3,15 @@
* Corrected issue where the actual new value was not always being printed in do_set. This occurred in cases where
the typed value differed from what the setter had converted it to.
* Fixed bug where ANSI style sequences were not correctly handled in `utils.truncate_line()`.
+ * Fixed bug where pyscripts could edit `cmd2.Cmd.py_locals` dictionary.
+ * Fixed bug where cmd2 set sys.path[0] for a pyscript to cmd2's working directory instead of the
+ script file's directory.
+ * Fixed bug where sys.path was not being restored after a pyscript ran.
* Enhancements
* Renamed set command's `-l/--long` flag to `-v/--verbose` for consistency with help and history commands.
+ * Setting the following pyscript variables:
+ * `__name__`: __main__
+ * `__file__`: script path (as typed)
## 0.10.0 (February 7, 2020)
* Enhancements
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 8f2cdca3..c7a0fa6d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -3093,8 +3093,7 @@ class Cmd(cmd.Cmd):
# This is a hidden flag for telling do_py to run a pyscript. It is intended only to be used by run_pyscript
# after it sets up sys.argv for the script being run. When this flag is present, it takes precedence over all
- # other arguments. run_pyscript uses this method instead of "py run('file')" because file names with
- # 2 or more consecutive spaces cause issues with our parser, which isn't meant to parse Python statements.
+ # other arguments.
py_parser.add_argument('--pyscript', help=argparse.SUPPRESS)
# Preserve quotes since we are passing these strings to Python
@@ -3104,65 +3103,66 @@ class Cmd(cmd.Cmd):
Enter an interactive Python shell
:return: True if running of commands should stop
"""
+ def py_quit():
+ """Function callable from the interactive Python console to exit that environment"""
+ raise EmbeddedConsoleExit
+
from .py_bridge import PyBridge
+ py_bridge = PyBridge(self)
+ saved_sys_path = None
+
if self.in_pyscript():
err = "Recursively entering interactive Python consoles is not allowed."
self.perror(err)
return
- py_bridge = PyBridge(self)
- py_code_to_run = ''
-
- # Handle case where we were called by run_pyscript
- if args.pyscript:
- args.pyscript = utils.strip_quotes(args.pyscript)
-
- # Run the script - use repr formatting to escape things which
- # need to be escaped to prevent issues on Windows
- py_code_to_run = 'run({!r})'.format(args.pyscript)
-
- elif args.command:
- py_code_to_run = args.command
- if args.remainder:
- py_code_to_run += ' ' + ' '.join(args.remainder)
-
- # Set cmd_echo to True so PyBridge statements like: py app('help')
- # run at the command line will print their output.
- py_bridge.cmd_echo = True
-
try:
self._in_py = True
+ py_code_to_run = ''
- def py_run(filename: str):
- """Run a Python script file in the interactive console.
- :param filename: filename of script file to run
- """
- expanded_filename = os.path.expanduser(filename)
+ # Locals for the Python environment we are creating
+ localvars = dict(self.py_locals)
+ localvars[self.py_bridge_name] = py_bridge
+ localvars['quit'] = py_quit
+ localvars['exit'] = py_quit
+
+ if self.self_in_py:
+ localvars['self'] = self
+
+ # Handle case where we were called by run_pyscript
+ if args.pyscript:
+ # Read the script file
+ expanded_filename = os.path.expanduser(utils.strip_quotes(args.pyscript))
try:
with open(expanded_filename) as f:
- interp.runcode(f.read())
+ py_code_to_run = f.read()
except OSError as ex:
self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
+ return
- def py_quit():
- """Function callable from the interactive Python console to exit that environment"""
- raise EmbeddedConsoleExit
+ localvars['__name__'] = '__main__'
+ localvars['__file__'] = expanded_filename
- # Set up Python environment
- self.py_locals[self.py_bridge_name] = py_bridge
- self.py_locals['run'] = py_run
- self.py_locals['quit'] = py_quit
- self.py_locals['exit'] = py_quit
+ # Place the script's directory at sys.path[0] just as Python does when executing a script
+ saved_sys_path = list(sys.path)
+ sys.path.insert(0, os.path.dirname(os.path.abspath(expanded_filename)))
- if self.self_in_py:
- self.py_locals['self'] = self
- elif 'self' in self.py_locals:
- del self.py_locals['self']
+ else:
+ # This is the default name chosen by InteractiveConsole when no locals are passed in
+ localvars['__name__'] = '__console__'
+
+ if args.command:
+ py_code_to_run = args.command
+ if args.remainder:
+ py_code_to_run += ' ' + ' '.join(args.remainder)
- localvars = self.py_locals
+ # Set cmd_echo to True so PyBridge statements like: py app('help')
+ # run at the command line will print their output.
+ py_bridge.cmd_echo = True
+
+ # Create the Python interpreter
interp = InteractiveConsole(locals=localvars)
- interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
# Check if we are running Python code
if py_code_to_run:
@@ -3177,8 +3177,7 @@ class Cmd(cmd.Cmd):
else:
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
- 'Non-Python commands can be issued with: {}("your command")\n'
- 'Run Python code from external script files with: run("script.py")'
+ 'Non-Python commands can be issued with: {}("your command")'
.format(self.py_bridge_name))
saved_cmd2_env = None
@@ -3205,7 +3204,10 @@ class Cmd(cmd.Cmd):
pass
finally:
- self._in_py = False
+ with self.sigint_protection:
+ if saved_sys_path is not None:
+ sys.path = saved_sys_path
+ self._in_py = False
return py_bridge.stop
diff --git a/tests/pyscript/environment.py b/tests/pyscript/environment.py
new file mode 100644
index 00000000..5e4d93d6
--- /dev/null
+++ b/tests/pyscript/environment.py
@@ -0,0 +1,20 @@
+# flake8: noqa F821
+# Tests that cmd2 populates __name__, __file__, and sets sys.path[0] to our directory
+import os
+import sys
+app.cmd_echo = True
+
+if __name__ != '__main__':
+ print("Error: __name__ is: {}".format(__name__))
+ quit()
+
+if __file__ != sys.argv[0]:
+ print("Error: __file__ is: {}".format(__file__))
+ quit()
+
+our_dir = os.path.dirname(os.path.abspath(__file__))
+if our_dir != sys.path[0]:
+ print("Error: our_dir is: {}".format(our_dir))
+ quit()
+
+print("PASSED")
diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py
index 21550592..7f02bb78 100644
--- a/tests/pyscript/recursive.py
+++ b/tests/pyscript/recursive.py
@@ -5,6 +5,7 @@
Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed
"""
import os
+import sys
app.cmd_echo = True
my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
diff --git a/tests/pyscript/run.py b/tests/pyscript/run.py
deleted file mode 100644
index 47250a10..00000000
--- a/tests/pyscript/run.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# flake8: noqa F821
-import os
-
-app.cmd_echo = True
-my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
-run(os.path.join(my_dir, 'to_run.py'))
diff --git a/tests/pyscript/to_run.py b/tests/pyscript/to_run.py
deleted file mode 100644
index b207952d..00000000
--- a/tests/pyscript/to_run.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# flake8: noqa F821
-print("I have been run")
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 376658e5..41528612 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -205,14 +205,17 @@ def test_base_shell(base_app, monkeypatch):
def test_base_py(base_app):
- # Create a variable and make sure we can see it
- out, err = run_cmd(base_app, 'py qqq=3')
- assert not out
+ # Make sure py can't edit Cmd.py_locals. It used to be that cmd2 was passing its py_locals
+ # dictionary to the py environment instead of a copy.
+ base_app.py_locals['test_var'] = 5
+ out, err = run_cmd(base_app, 'py del[locals()["test_var"]]')
+ assert not out and not err
+ assert base_app.py_locals['test_var'] == 5
- out, err = run_cmd(base_app, 'py print(qqq)')
- assert out[0].rstrip() == '3'
+ out, err = run_cmd(base_app, 'py print(test_var)')
+ assert out[0].rstrip() == '5'
- # Add a more complex statement
+ # Try a print statement
out, err = run_cmd(base_app, 'py print("spaces" + " in this " + "command")')
assert out[0].rstrip() == 'spaces in this command'
diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py
index d717758c..811fd688 100644
--- a/tests/test_run_pyscript.py
+++ b/tests/test_run_pyscript.py
@@ -117,10 +117,9 @@ def test_run_pyscript_stop(base_app, request):
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
assert stop
-def test_run_pyscript_run(base_app, request):
+def test_run_pyscript_environment(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', 'run.py')
- expected = 'I have been run'
+ python_script = os.path.join(test_dir, 'pyscript', 'environment.py')
+ out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script))
- out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert expected in out
+ assert out[0] == "PASSED"