Skip to content

Commit 99ec526

Browse files
committed
Fixed issue where run_pyscript failed if the script's filename had a space
1 parent 5b8c809 commit 99ec526

9 files changed

Lines changed: 86 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Fixed bug where multiline commands were having leading and ending spaces stripped. This would mess up quoted
66
strings that crossed multiple lines.
77
* Fixed a bug when appending to the clipboard where contents were in reverse order
8+
* Fixed issue where run_pyscript failed if the script's filename had a space
89
* Enhancements
910
* Greatly simplified using argparse-based tab completion. The new interface is a complete overhaul that breaks
1011
the previous way of specifying completion and choices functions. See header of [argparse_custom.py](https://github.com/python-cmd2/cmd2/blob/master/cmd2/argparse_custom.py)

cmd2/cmd2.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3245,42 +3245,55 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
32453245
py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run")
32463246
py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command")
32473247

3248+
# This is a hidden flag for telling do_py to run a pyscript. It is intended only to be used by run_pyscript
3249+
# after it sets up sys.argv for the script being run. When this flag is present, it takes precedence over all
3250+
# other arguments. run_pyscript uses this method instead of "py run('file')" because file names with spaces cause
3251+
# issues with our parser, which isn't meant to parse Python statements.
3252+
py_parser.add_argument('--pyscript', help=argparse.SUPPRESS)
3253+
32483254
# Preserve quotes since we are passing these strings to Python
32493255
@with_argparser(py_parser, preserve_quotes=True)
3250-
def do_py(self, args: argparse.Namespace) -> bool:
3251-
"""Invoke Python command or shell"""
3256+
def do_py(self, args: argparse.Namespace) -> Optional[bool]:
3257+
"""
3258+
Enter an interactive Python shell
3259+
:return: True if running of commands should stop
3260+
"""
32523261
from .py_bridge import PyBridge
32533262
if self._in_py:
32543263
err = "Recursively entering interactive Python consoles is not allowed."
32553264
self.perror(err)
3256-
return False
3265+
return
32573266

32583267
py_bridge = PyBridge(self)
3268+
py_code_to_run = ''
3269+
3270+
# Handle case where we were called by run_pyscript
3271+
if args.pyscript:
3272+
py_code_to_run = 'run({!r})'.format(args.pyscript)
3273+
3274+
elif args.command:
3275+
py_code_to_run = args.command
3276+
if args.remainder:
3277+
py_code_to_run += ' ' + ' '.join(args.remainder)
3278+
3279+
# Set cmd_echo to True so PyBridge statements like: py app('help')
3280+
# run at the command line will print their output.
3281+
py_bridge.cmd_echo = True
32593282

32603283
try:
32613284
self._in_py = True
32623285

3263-
# Support the run command even if called prior to invoking an interactive interpreter
32643286
def py_run(filename: str):
32653287
"""Run a Python script file in the interactive console.
3266-
:param filename: filename of *.py script file to run
3288+
:param filename: filename of script file to run
32673289
"""
32683290
expanded_filename = os.path.expanduser(filename)
32693291

3270-
if not expanded_filename.endswith('.py'):
3271-
self.pwarning("'{}' does not have a .py extension".format(expanded_filename))
3272-
selection = self.select('Yes No', 'Continue to try to run it as a Python script? ')
3273-
if selection != 'Yes':
3274-
return
3275-
3276-
# cmd_echo defaults to False for scripts. The user can always toggle this value in their script.
3277-
py_bridge.cmd_echo = False
3278-
32793292
try:
32803293
with open(expanded_filename) as f:
32813294
interp.runcode(f.read())
32823295
except OSError as ex:
3283-
self.pexcept("Error opening script file '{}': {}".format(expanded_filename, ex))
3296+
self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
32843297

32853298
def py_quit():
32863299
"""Function callable from the interactive Python console to exit that environment"""
@@ -3301,24 +3314,16 @@ def py_quit():
33013314
interp = InteractiveConsole(locals=localvars)
33023315
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
33033316

3304-
# Check if the user is running a Python statement on the command line
3305-
if args.command:
3306-
full_command = args.command
3307-
if args.remainder:
3308-
full_command += ' ' + ' '.join(args.remainder)
3309-
3310-
# Set cmd_echo to True so PyBridge statements like: py app('help')
3311-
# run at the command line will print their output.
3312-
py_bridge.cmd_echo = True
3313-
3317+
# Check if we are running Python code
3318+
if py_code_to_run:
33143319
# noinspection PyBroadException
33153320
try:
3316-
interp.runcode(full_command)
3321+
interp.runcode(py_code_to_run)
33173322
except BaseException:
3318-
# We don't care about any exception that happened in the interactive console
3323+
# We don't care about any exception that happened in the Python code
33193324
pass
33203325

3321-
# If there are no args, then we will open an interactive Python shell
3326+
# Otherwise we will open an interactive Python shell
33223327
else:
33233328
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
33243329
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
@@ -3360,21 +3365,31 @@ def py_quit():
33603365
help='arguments to pass to script', completer_method=path_complete)
33613366

33623367
@with_argparser(run_pyscript_parser)
3363-
def do_run_pyscript(self, args: argparse.Namespace) -> bool:
3364-
"""Run a Python script file inside the console"""
3365-
script_path = os.path.expanduser(args.script_path)
3368+
def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
3369+
"""
3370+
Run a Python script file inside the console
3371+
:return: True if running of commands should stop
3372+
"""
3373+
# Expand ~ before placing this path in sys.argv just as a shell would
3374+
args.script_path = os.path.expanduser(args.script_path)
3375+
3376+
# Add some protection against accidentally running a non-Python file. The happens when users
3377+
# mix up run_script and run_pyscript.
3378+
if not args.script_path.endswith('.py'):
3379+
self.pwarning("'{}' does not have a .py extension".format(args.script_path))
3380+
selection = self.select('Yes No', 'Continue to try to run it as a Python script? ')
3381+
if selection != 'Yes':
3382+
return
3383+
33663384
py_return = False
33673385

33683386
# Save current command line arguments
33693387
orig_args = sys.argv
33703388

33713389
try:
33723390
# Overwrite sys.argv to allow the script to take command line arguments
3373-
sys.argv = [script_path] + args.script_arguments
3374-
3375-
# Run the script - use repr formatting to escape things which
3376-
# need to be escaped to prevent issues on Windows
3377-
py_return = self.do_py("run({!r})".format(script_path))
3391+
sys.argv = [args.script_path] + args.script_arguments
3392+
py_return = self.do_py('--pyscript {}'.format(utils.quote_string_if_needed(args.script_path)))
33783393

33793394
except KeyboardInterrupt:
33803395
pass
@@ -3783,6 +3798,8 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
37833798
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path))
37843799
return
37853800

3801+
# Add some protection against accidentally running a Python file. The happens when users
3802+
# mix up run_script and run_pyscript.
37863803
if expanded_path.endswith('.py'):
37873804
self.pwarning("'{}' appears to be a Python file".format(expanded_path))
37883805
selection = self.select('Yes No', 'Continue to try to run it as a text script? ')

tests/pyscript/recursive.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
# flake8: noqa F821
4+
"""
5+
Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed
6+
"""
7+
import os
8+
9+
app.cmd_echo = True
10+
my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
11+
app('run_pyscript {}'.format(os.path.join(my_dir, 'stop.py')))

tests/pyscript/run.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# flake8: noqa F821
2+
import os
3+
4+
app.cmd_echo = True
5+
my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
6+
run(os.path.join(my_dir, 'to_run.py'))

tests/pyscript/to_run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# flake8: noqa F821
2+
print("I have been run")

tests/scripts/recursive.py

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

tests/test_cmd2.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def test_base_shell(base_app, monkeypatch):
236236
assert out == []
237237
assert m.called
238238

239+
239240
def test_base_py(base_app):
240241
# Create a variable and make sure we can see it
241242
out, err = run_cmd(base_app, 'py qqq=3')
@@ -263,17 +264,6 @@ def test_base_py(base_app):
263264
assert "NameError: name 'self' is not defined" in err
264265

265266

266-
@pytest.mark.skipif(sys.platform == 'win32',
267-
reason="Unit test doesn't work on win32, but feature does")
268-
def test_py_run_script(base_app, request):
269-
test_dir = os.path.dirname(request.module.__file__)
270-
python_script = os.path.join(test_dir, 'script.py')
271-
expected = 'This is a python script running ...'
272-
273-
out, err = run_cmd(base_app, "py run('{}')".format(python_script))
274-
assert expected in out
275-
276-
277267
def test_base_error(base_app):
278268
out, err = run_cmd(base_app, 'meow')
279269
assert "is not a recognized command" in err[0]

tests/test_run_pyscript.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_run_pyscript(base_app, request):
3232

3333
def test_run_pyscript_recursive_not_allowed(base_app, request):
3434
test_dir = os.path.dirname(request.module.__file__)
35-
python_script = os.path.join(test_dir, 'scripts', 'recursive.py')
35+
python_script = os.path.join(test_dir, 'pyscript', 'recursive.py')
3636
expected = 'Recursively entering interactive Python consoles is not allowed.'
3737

3838
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
@@ -41,7 +41,7 @@ def test_run_pyscript_recursive_not_allowed(base_app, request):
4141
def test_run_pyscript_with_nonexist_file(base_app):
4242
python_script = 'does_not_exist.py'
4343
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
44-
assert "Error opening script file" in err[0]
44+
assert "Error reading script file" in err[0]
4545

4646
def test_run_pyscript_with_non_python_file(base_app, request):
4747
m = mock.MagicMock(name='input', return_value='2')
@@ -54,7 +54,7 @@ def test_run_pyscript_with_non_python_file(base_app, request):
5454

5555
def test_run_pyscript_with_exception(base_app, request):
5656
test_dir = os.path.dirname(request.module.__file__)
57-
python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py')
57+
python_script = os.path.join(test_dir, 'pyscript', 'raises_exception.py')
5858
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
5959
assert err[0].startswith('Traceback')
6060
assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1]
@@ -91,7 +91,7 @@ def test_run_pyscript_stop(base_app, request):
9191
# Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop
9292
test_dir = os.path.dirname(request.module.__file__)
9393

94-
# help.py doesn't run any commands that returns True for stop
94+
# help.py doesn't run any commands that return True for stop
9595
python_script = os.path.join(test_dir, 'pyscript', 'help.py')
9696
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
9797
assert not stop
@@ -100,3 +100,11 @@ def test_run_pyscript_stop(base_app, request):
100100
python_script = os.path.join(test_dir, 'pyscript', 'stop.py')
101101
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
102102
assert stop
103+
104+
def test_run_pyscript_run(base_app, request):
105+
test_dir = os.path.dirname(request.module.__file__)
106+
python_script = os.path.join(test_dir, 'pyscript', 'run.py')
107+
expected = 'I have been run'
108+
109+
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
110+
assert expected in out

0 commit comments

Comments
 (0)