diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-02-14 16:28:41 -0500 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-02-14 16:28:41 -0500 |
commit | 9156618a56d635bb51261d019a3703a1b4e3b588 (patch) | |
tree | 3f35d07cf668ec156dac6e97e612569c83aa36c6 | |
parent | 013b9e0a2c75e17f8aa0e0f7cbe50d84d2f657d8 (diff) | |
download | cmd2-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.md | 7 | ||||
-rw-r--r-- | cmd2/cmd2.py | 92 | ||||
-rw-r--r-- | tests/pyscript/environment.py | 20 | ||||
-rw-r--r-- | tests/pyscript/recursive.py | 1 | ||||
-rw-r--r-- | tests/pyscript/run.py | 6 | ||||
-rw-r--r-- | tests/pyscript/to_run.py | 2 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 15 | ||||
-rw-r--r-- | tests/test_run_pyscript.py | 9 |
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" |