Skip to content

Commit b99c094

Browse files
authored
Merge pull request #560 from python-cmd2/py_enhancements
Py enhancements
2 parents fadb8d3 + 7e2497d commit b99c094

File tree

4 files changed

+85
-74
lines changed

4 files changed

+85
-74
lines changed

cmd2/cmd2.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,15 +2058,14 @@ def default(self, statement: Statement) -> None:
20582058
20592059
:param statement: Statement object with parsed input
20602060
"""
2061-
arg = statement.raw
20622061
if self.default_to_shell:
2063-
result = os.system(arg)
2062+
result = os.system(statement.command_and_args)
20642063
# If os.system() succeeded, then don't print warning about unknown command
20652064
if not result:
20662065
return
20672066

20682067
# Print out a message stating this is an unknown command
2069-
self.poutput('*** Unknown syntax: {}\n'.format(arg))
2068+
self.poutput('*** Unknown syntax: {}\n'.format(statement.command_and_args))
20702069

20712070
def pseudo_raw_input(self, prompt: str) -> str:
20722071
"""Began life as a copy of cmd's cmdloop; like raw_input but
@@ -2208,10 +2207,9 @@ def _cmdloop(self) -> bool:
22082207
# ----- Alias subcommand functions -----
22092208

22102209
def alias_create(self, args: argparse.Namespace):
2211-
""" Creates or overwrites an alias """
2210+
"""Create or overwrites an alias"""
22122211

22132212
# Validate the alias name
2214-
args.name = utils.strip_quotes(args.name)
22152213
valid, errmsg = self.statement_parser.is_valid_command(args.name)
22162214
if not valid:
22172215
self.perror("Invalid alias name: {}".format(errmsg), traceback_war=False)
@@ -2234,30 +2232,24 @@ def alias_create(self, args: argparse.Namespace):
22342232
self.poutput("Alias '{}' {}".format(args.name, result))
22352233

22362234
def alias_delete(self, args: argparse.Namespace):
2237-
""" Deletes aliases """
2235+
"""Delete aliases"""
22382236
if args.all:
22392237
self.aliases.clear()
22402238
self.poutput("All aliases deleted")
22412239
elif not args.name:
22422240
self.do_help('alias delete')
22432241
else:
2244-
# Get rid of duplicates and strip quotes since the argparse decorator for do_alias() preserves them
2245-
aliases_to_delete = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
2246-
2247-
for cur_name in aliases_to_delete:
2242+
for cur_name in utils.remove_duplicates(args.name):
22482243
if cur_name in self.aliases:
22492244
del self.aliases[cur_name]
22502245
self.poutput("Alias '{}' deleted".format(cur_name))
22512246
else:
22522247
self.perror("Alias '{}' does not exist".format(cur_name), traceback_war=False)
22532248

22542249
def alias_list(self, args: argparse.Namespace):
2255-
""" Lists some or all aliases """
2250+
"""List some or all aliases"""
22562251
if args.name:
2257-
# Get rid of duplicates and strip quotes since the argparse decorator for do_alias() preserves them
2258-
names_to_view = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
2259-
2260-
for cur_name in names_to_view:
2252+
for cur_name in utils.remove_duplicates(args.name):
22612253
if cur_name in self.aliases:
22622254
self.poutput("alias create {} {}".format(cur_name, self.aliases[cur_name]))
22632255
else:
@@ -2343,10 +2335,9 @@ def do_alias(self, args: argparse.Namespace):
23432335
# ----- Macro subcommand functions -----
23442336

23452337
def macro_create(self, args: argparse.Namespace):
2346-
""" Creates or overwrites a macro """
2338+
"""Create or overwrites a macro"""
23472339

23482340
# Validate the macro name
2349-
args.name = utils.strip_quotes(args.name)
23502341
valid, errmsg = self.statement_parser.is_valid_command(args.name)
23512342
if not valid:
23522343
self.perror("Invalid macro name: {}".format(errmsg), traceback_war=False)
@@ -2419,30 +2410,24 @@ def macro_create(self, args: argparse.Namespace):
24192410
self.poutput("Macro '{}' {}".format(args.name, result))
24202411

24212412
def macro_delete(self, args: argparse.Namespace):
2422-
""" Deletes macros """
2413+
"""Delete macros"""
24232414
if args.all:
24242415
self.macros.clear()
24252416
self.poutput("All macros deleted")
24262417
elif not args.name:
24272418
self.do_help('macro delete')
24282419
else:
2429-
# Get rid of duplicates and strip quotes since the argparse decorator for do_macro() preserves them
2430-
macros_to_delete = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
2431-
2432-
for cur_name in macros_to_delete:
2420+
for cur_name in utils.remove_duplicates(args.name):
24332421
if cur_name in self.macros:
24342422
del self.macros[cur_name]
24352423
self.poutput("Macro '{}' deleted".format(cur_name))
24362424
else:
24372425
self.perror("Macro '{}' does not exist".format(cur_name), traceback_war=False)
24382426

24392427
def macro_list(self, args: argparse.Namespace):
2440-
""" Lists some or all macros """
2428+
"""List some or all macros"""
24412429
if args.name:
2442-
# Get rid of duplicates and strip quotes since the argparse decorator for do_macro() preserves them
2443-
names_to_view = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
2444-
2445-
for cur_name in names_to_view:
2430+
for cur_name in utils.remove_duplicates(args.name):
24462431
if cur_name in self.macros:
24472432
self.poutput("macro create {} {}".format(cur_name, self.macros[cur_name].value))
24482433
else:
@@ -2949,7 +2934,16 @@ def _reset_py_display() -> None:
29492934
sys.displayhook = sys.__displayhook__
29502935
sys.excepthook = sys.__excepthook__
29512936

2952-
py_parser = ACArgumentParser()
2937+
py_description = ("Invoke Python command or shell\n"
2938+
"\n"
2939+
"Note that, when invoking a command directly from the command line, this shell\n"
2940+
"has limited ability to parse Python statements into tokens. In particular,\n"
2941+
"there may be problems with whitespace and quotes depending on their placement.\n"
2942+
"\n"
2943+
"If you see strange parsing behavior, it's best to just open the Python shell by\n"
2944+
"providing no arguments to py and run more complex statements there.")
2945+
2946+
py_parser = ACArgumentParser(description=py_description)
29532947
py_parser.add_argument('command', help="command to run", nargs='?')
29542948
py_parser.add_argument('remainder', help="remainder of command", nargs=argparse.REMAINDER)
29552949

@@ -3000,6 +2994,8 @@ def run(filename: str):
30002994
if args.remainder:
30012995
full_command += ' ' + ' '.join(args.remainder)
30022996

2997+
# If running at the CLI, print the output of the command
2998+
bridge.cmd_echo = True
30032999
interp.runcode(full_command)
30043000

30053001
# If there are no args, then we will open an interactive Python console

cmd2/pyscript_bridge.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import argparse
1111
import functools
1212
import sys
13-
from typing import List, Callable
13+
from typing import List, Callable, Optional
1414

1515
from .argparse_completer import _RangeAction
1616
from .utils import namedtuple_with_defaults, StdSim
@@ -38,7 +38,7 @@ def __bool__(self):
3838
return not self.stderr and self.data is not None
3939

4040

41-
def _exec_cmd(cmd2_app, func: Callable, echo: bool):
41+
def _exec_cmd(cmd2_app, func: Callable, echo: bool) -> CommandResult:
4242
"""Helper to encapsulate executing a command and capturing the results"""
4343
copy_stdout = StdSim(sys.stdout, echo)
4444
copy_stderr = StdSim(sys.stderr, echo)
@@ -65,7 +65,7 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool):
6565

6666
class ArgparseFunctor:
6767
"""
68-
Encapsulates translating python object traversal
68+
Encapsulates translating Python object traversal
6969
"""
7070
def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser):
7171
self._echo = echo
@@ -169,7 +169,7 @@ def __call__(self, *args, **kwargs):
169169
# Check if there are any extra arguments we don't know how to handle
170170
for kw in kwargs:
171171
if kw not in self._args: # consumed_kw:
172-
raise TypeError('{}() got an unexpected keyword argument \'{}\''.format(
172+
raise TypeError("{}() got an unexpected keyword argument '{}'".format(
173173
self.__current_subcommand_parser.prog, kw))
174174

175175
if has_subcommand:
@@ -181,7 +181,7 @@ def _run(self):
181181
# look up command function
182182
func = self._cmd2_app.cmd_func(self._command_name)
183183
if func is None:
184-
raise AttributeError("{!r} object has no command called {!r}".format(self._cmd2_app.__class__.__name__,
184+
raise AttributeError("'{}' object has no command called '{}'".format(self._cmd2_app.__class__.__name__,
185185
self._command_name))
186186

187187
# reconstruct the cmd2 command from the python call
@@ -250,7 +250,10 @@ def __init__(self, cmd2_app):
250250
self.cmd_echo = False
251251

252252
def __getattr__(self, item: str):
253-
"""Check if the attribute is a command. If so, return a callable."""
253+
"""
254+
Provide functionality to call application commands as a method of PyscriptBridge
255+
ex: app.help()
256+
"""
254257
func = self._cmd2_app.cmd_func(item)
255258

256259
if func:
@@ -264,14 +267,26 @@ def wrap_func(args=''):
264267

265268
return wrap_func
266269
else:
267-
return super().__getattr__(item)
270+
# item does not refer to a command
271+
raise AttributeError("'{}' object has no attribute '{}'".format(self._cmd2_app.pyscript_name, item))
268272

269273
def __dir__(self):
270-
"""Return a custom set of attribute names to match the available commands"""
271-
commands = list(self._cmd2_app.get_all_commands())
272-
commands.insert(0, 'cmd_echo')
273-
return commands
274+
"""Return a custom set of attribute names"""
275+
attributes = self._cmd2_app.get_all_commands()
276+
attributes.insert(0, 'cmd_echo')
277+
return attributes
278+
279+
def __call__(self, args: str, echo: Optional[bool]=None) -> CommandResult:
280+
"""
281+
Provide functionality to call application commands by calling PyscriptBridge
282+
ex: app('help')
283+
:param args: The string being passed to the command
284+
:param echo: If True, output will be echoed while the command runs
285+
This temporarily overrides the value of self.cmd_echo
286+
"""
287+
if echo is None:
288+
echo = self.cmd_echo
274289

275-
def __call__(self, args: str):
276-
return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'),
277-
self.cmd_echo)
290+
return _exec_cmd(self._cmd2_app,
291+
functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'),
292+
echo)

tests/test_cmd2.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,22 +1866,6 @@ def test_alias_create(base_app, capsys):
18661866
out = run_cmd(base_app, 'alias list fake')
18671867
assert out == normalize('alias create fake pyscript')
18681868

1869-
def test_alias_quoted_name(base_app, capsys):
1870-
"""Demonstrate that names can be quoted in alias commands because they will be stripped"""
1871-
# Create the alias
1872-
out = run_cmd(base_app, 'alias create "fake" pyscript')
1873-
1874-
# The quotes on names are stripped
1875-
assert out == normalize("Alias 'fake' created")
1876-
1877-
# Look up the new alias and quote the name
1878-
out = run_cmd(base_app, 'alias list "fake"')
1879-
assert out == normalize('alias create fake pyscript')
1880-
1881-
# Delete the alias using quotes
1882-
out = run_cmd(base_app, 'alias delete "fake"')
1883-
assert out == normalize("Alias 'fake' deleted")
1884-
18851869
def test_alias_create_with_quoted_value(base_app, capsys):
18861870
"""Demonstrate that quotes in alias value will be preserved (except for redirectors)"""
18871871

@@ -1968,22 +1952,6 @@ def test_macro_create(base_app, capsys):
19681952
out = run_cmd(base_app, 'macro list fake')
19691953
assert out == normalize('macro create fake pyscript')
19701954

1971-
def test_macro_create_quoted_name(base_app, capsys):
1972-
"""Demonstrate that names can be quoted in macro commands because they will be stripped"""
1973-
# Create the macro
1974-
out = run_cmd(base_app, 'macro create "fake" pyscript')
1975-
1976-
# The quotes on names are stripped
1977-
assert out == normalize("Macro 'fake' created")
1978-
1979-
# Look up the new macro and quote the name
1980-
out = run_cmd(base_app, 'macro list "fake"')
1981-
assert out == normalize('macro create fake pyscript')
1982-
1983-
# Delete the macro using quotes
1984-
out = run_cmd(base_app, 'macro delete "fake"')
1985-
assert out == normalize("Macro 'fake' deleted")
1986-
19871955
def test_macro_create_with_quoted_value(base_app, capsys):
19881956
"""Demonstrate that quotes in macro value will be preserved (except for redirectors)"""
19891957
# Create the macro

tests/test_parsing.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,38 @@ def test_statement_is_immutable():
756756
statement.raw = 'baz'
757757

758758

759+
def test_is_valid_command_invalid(parser):
760+
# Empty command
761+
valid, errmsg = parser.is_valid_command('')
762+
assert not valid and 'cannot be an empty string' in errmsg
763+
764+
# Starts with shortcut
765+
valid, errmsg = parser.is_valid_command('!ls')
766+
assert not valid and 'cannot start with a shortcut' in errmsg
767+
768+
# Contains whitespace
769+
valid, errmsg = parser.is_valid_command('shell ls')
770+
assert not valid and 'cannot contain: whitespace, quotes,' in errmsg
771+
772+
# Contains a quote
773+
valid, errmsg = parser.is_valid_command('"shell"')
774+
assert not valid and 'cannot contain: whitespace, quotes,' in errmsg
775+
776+
# Contains a redirector
777+
valid, errmsg = parser.is_valid_command('>shell')
778+
assert not valid and 'cannot contain: whitespace, quotes,' in errmsg
779+
780+
# Contains a terminator
781+
valid, errmsg = parser.is_valid_command(';shell')
782+
assert not valid and 'cannot contain: whitespace, quotes,' in errmsg
783+
784+
def test_is_valid_command_valid(parser):
785+
# Empty command
786+
valid, errmsg = parser.is_valid_command('shell')
787+
assert valid
788+
assert not errmsg
789+
790+
759791
def test_macro_normal_arg_pattern():
760792
# This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
761793
from cmd2.parsing import MacroArg

0 commit comments

Comments
 (0)