summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-07-21 00:21:28 -0400
committerGitHub <noreply@github.com>2019-07-21 00:21:28 -0400
commit73d1c34ae199415051a6209c726b099660944499 (patch)
tree31dcb1fd807e255a388e56b80dcf0279fd1899b0
parent578593f8a9011ad71d78cd30f231c79af570987e (diff)
parent4b91f6355795d7bd23ff46882fd7181a3a11a419 (diff)
downloadcmd2-git-73d1c34ae199415051a6209c726b099660944499.tar.gz
Merge pull request #737 from python-cmd2/renames
Changes made while preparing for PyOhio presentation
-rw-r--r--CHANGELOG.md6
-rw-r--r--cmd2/__init__.py2
-rw-r--r--cmd2/argparse_completer.py6
-rw-r--r--cmd2/argparse_custom.py6
-rw-r--r--cmd2/cmd2.py209
-rw-r--r--cmd2/py_bridge.py (renamed from cmd2/pyscript_bridge.py)8
-rw-r--r--tests/pyscript/stop.py2
-rw-r--r--tests/test_argparse_completer.py21
-rw-r--r--tests/test_cmd2.py9
-rw-r--r--tests/test_run_pyscript.py18
10 files changed, 190 insertions, 97 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33f5f354..98f04eeb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* Fixed bug where `history -v` was sometimes showing raw and expanded commands when they weren't different
* Fixed bug where multiline commands were having leading and ending spaces stripped. This would mess up quoted
strings that crossed multiple lines.
+ * Fixed a bug when appending to the clipboard where contents were in reverse order
* Enhancements
* Greatly simplified using argparse-based tab completion. The new interface is a complete overhaul that breaks
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)
@@ -31,6 +32,9 @@
sort, but it can be changed to a natural sort by setting the value to NATURAL_SORT_KEY.
* `StatementParser` now expects shortcuts to be passed in as dictionary. This eliminates the step of converting the
shortcuts dictionary into a tuple before creating `StatementParser`.
+ * Renamed `Cmd.pyscript_name` to `Cmd.py_bridge_name`
+ * Renamed `Cmd.pystate` to `Cmd.py_locals`
+ * Renamed `PyscriptBridge` to `PyBridge`
## 0.9.14 (June 29, 2019)
* Enhancements
@@ -122,7 +126,7 @@
of a `cmd2` based app, you will need to update your code to use `.history.get(1).statement.raw` instead.
* Removed internally used `eos` command that was used to keep track of when a text script's commands ended
* Removed `cmd2` member called `_STOP_AND_EXIT` since it was just a boolean value that should always be True
- * Removed `cmd2` member called `_should_quit` since `PyscriptBridge` now handles this logic
+ * Removed `cmd2` member called `_should_quit` since `PyBridge` now handles this logic
* Removed support for `cmd.cmdqueue`
* `allow_cli_args` is now an argument to __init__ instead of a `cmd2` class member
* **Python 3.4 EOL notice**
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 2b3bec5d..2653051a 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -15,4 +15,4 @@ from .argparse_custom import Cmd2ArgumentParser, CompletionItem
from .cmd2 import Cmd, Statement, EmptyStatement, categorize
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
from .constants import DEFAULT_SHORTCUTS
-from .pyscript_bridge import CommandResult
+from .py_bridge import CommandResult
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index e7f3a0e2..41cff0dd 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -7,6 +7,7 @@ See the header of argparse_custom.py for instructions on how to use these featur
"""
import argparse
+import numbers
import shutil
from typing import List, Union
@@ -499,6 +500,11 @@ class AutoCompleter(object):
# Since arg_choices can be any iterable type, convert to a list
arg_choices = list(arg_choices)
+ # If these choices are numbers, and have not yet been sorted, then sort them now
+ if not self._cmd2_app.matches_sorted and all(isinstance(x, numbers.Number) for x in arg_choices):
+ arg_choices.sort()
+ self._cmd2_app.matches_sorted = True
+
# Since choices can be various types like int, we must convert them to strings
for index, choice in enumerate(arg_choices):
if not isinstance(choice, str):
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 090abde1..668f41d6 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -154,13 +154,13 @@ from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
from .ansi import ansi_aware_write, style_error
+# Used in nargs ranges to signify there is no maximum
+INFINITY = float('inf')
+
############################################################################################################
# The following are names of custom argparse argument attributes added by cmd2
############################################################################################################
-# Used in nargs ranges to signify there is no maximum
-INFINITY = float('inf')
-
# A tuple specifying nargs as a range (min, max)
ATTR_NARGS_RANGE = 'nargs_range'
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 788e0294..b03e3280 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -89,10 +89,6 @@ try:
except ImportError: # pragma: no cover
ipython_available = False
-
-# optional attribute, when tagged on a function, allows cmd2 to categorize commands
-HELP_CATEGORY = 'help_category'
-
INTERNAL_COMMAND_EPILOG = ("Notes:\n"
" This command is for internal use and is not intended to be called from the\n"
" command line.")
@@ -110,20 +106,30 @@ NATURAL_SORT_KEY = utils.natural_keys
# Used as the command name placeholder in disabled command messages.
COMMAND_NAME = "<COMMAND_NAME>"
+############################################################################################################
+# The following are optional attributes added to do_* command functions
+############################################################################################################
+
+# The custom help category a command belongs to
+CMD_ATTR_HELP_CATEGORY = 'help_category'
+
+# The argparse parser for the command
+CMD_ATTR_ARGPARSER = 'argparser'
-def categorize(func: Union[Callable, Iterable], category: str) -> None:
+
+def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None:
"""Categorize a function.
The help command output will group this function under the specified category heading
- :param func: function to categorize
+ :param func: function or list of functions to categorize
:param category: category to put it in
"""
if isinstance(func, Iterable):
for item in func:
- setattr(item, HELP_CATEGORY, category)
+ setattr(item, CMD_ATTR_HELP_CATEGORY, category)
else:
- setattr(func, HELP_CATEGORY, category)
+ setattr(func, CMD_ATTR_HELP_CATEGORY, category)
def with_category(category: str) -> Callable:
@@ -146,12 +152,12 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
def arg_decorator(func: Callable):
@functools.wraps(func)
- def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- _, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
+ _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
- return func(cmd2_instance, parsed_arglist)
+ return func(cmd2_app, parsed_arglist)
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
cmd_wrapper.__doc__ = func.__doc__
@@ -185,15 +191,15 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
# noinspection PyProtectedMember
def arg_decorator(func: Callable):
@functools.wraps(func)
- def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
+ statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
if ns_provider is None:
namespace = None
else:
- namespace = ns_provider(cmd2_instance)
+ namespace = ns_provider(cmd2_app)
try:
args, unknown = argparser.parse_known_args(parsed_arglist, namespace)
@@ -201,7 +207,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
return
else:
setattr(args, '__statement__', statement)
- return func(cmd2_instance, args, unknown)
+ return func(cmd2_app, args, unknown)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
@@ -216,7 +222,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
cmd_wrapper.__doc__ = argparser.description
# Mark this function as having an argparse ArgumentParser
- setattr(cmd_wrapper, 'argparser', argparser)
+ setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
return cmd_wrapper
@@ -243,15 +249,15 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
# noinspection PyProtectedMember
def arg_decorator(func: Callable):
@functools.wraps(func)
- def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
+ statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
if ns_provider is None:
namespace = None
else:
- namespace = ns_provider(cmd2_instance)
+ namespace = ns_provider(cmd2_app)
try:
args = argparser.parse_args(parsed_arglist, namespace)
@@ -259,7 +265,7 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
return
else:
setattr(args, '__statement__', statement)
- return func(cmd2_instance, args)
+ return func(cmd2_app, args)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
@@ -274,7 +280,7 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
cmd_wrapper.__doc__ = argparser.description
# Mark this function as having an argparse ArgumentParser
- setattr(cmd_wrapper, 'argparser', argparser)
+ setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
return cmd_wrapper
@@ -412,9 +418,14 @@ class Cmd(cmd.Cmd):
# Command aliases and macros
self.macros = dict()
- self._pystate = {}
+ # Keeps track of typed command history in the Python shell
self._py_history = []
- self.pyscript_name = 'app'
+
+ # The name by which Python environments refer to the PyBridge to call app commands
+ self.py_bridge_name = 'app'
+
+ # Defines app-specific variables/functions available in Python shells and pyscripts
+ self.py_locals = {}
self.statement_parser = StatementParser(allow_redirection=allow_redirection,
terminators=terminators,
@@ -554,6 +565,10 @@ class Cmd(cmd.Cmd):
# values are DisabledCommand objects.
self.disabled_commands = dict()
+ # If any command has been categorized, then all other commands that haven't been categorized
+ # will display under this section in the help output.
+ self.default_category = 'Uncategorized'
+
# ----- Methods related to presenting output to the user -----
@property
@@ -637,6 +652,14 @@ class Cmd(cmd.Cmd):
final_msg = "{}".format(msg)
ansi.ansi_aware_write(sys.stderr, final_msg + end)
+ def pwarning(self, msg: Any, *, end: str = '\n') -> None:
+ """Apply the warning style to a message and print it to sys.stderr
+
+ :param msg: message to print (anything convertible to a str with '{}'.format() is OK)
+ :param end: string appended after the end of the message, default a newline
+ """
+ self.perror(ansi.style_warning(msg), end=end, apply_style=False)
+
def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
@@ -1442,12 +1465,14 @@ class Cmd(cmd.Cmd):
compfunc = getattr(self, 'complete_' + command, None)
if compfunc is None:
- # There's no completer function, next see if the command uses argparser
+ # There's no completer function, next see if the command uses argparse
func = self.cmd_func(command)
- if func and hasattr(func, 'argparser'):
+ argparser = getattr(func, CMD_ATTR_ARGPARSER, None)
+
+ if func is not None and argparser is not None:
import functools
compfunc = functools.partial(self._autocomplete_default,
- argparser=getattr(func, 'argparser'))
+ argparser=argparser)
else:
compfunc = self.completedefault
@@ -1699,13 +1724,13 @@ class Cmd(cmd.Cmd):
statement = self.statement_parser.parse_command_only(line)
return statement.command, statement.args, statement.command_and_args
- def onecmd_plus_hooks(self, line: str, pyscript_bridge_call: bool = False) -> bool:
+ def onecmd_plus_hooks(self, line: str, py_bridge_call: bool = False) -> bool:
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
:param line: line of text read from input
- :param pyscript_bridge_call: This should only ever be set to True by PyscriptBridge to signify the beginning
- of an app() call in a pyscript. It is used to enable/disable the storage of the
- command's stdout.
+ :param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
+ of an app() call from Python. It is used to enable/disable the storage of the
+ command's stdout.
:return: True if running of commands should stop
"""
import datetime
@@ -1745,7 +1770,7 @@ class Cmd(cmd.Cmd):
try:
# Get sigint protection while we set up redirection
with self.sigint_protection:
- if pyscript_bridge_call:
+ if py_bridge_call:
# Start saving command's stdout at this point
self.stdout.pause_storage = False
@@ -1794,7 +1819,7 @@ class Cmd(cmd.Cmd):
if not already_redirecting:
self._redirecting = False
- if pyscript_bridge_call:
+ if py_bridge_call:
# Stop saving command's stdout before command finalization hooks run
self.stdout.pause_storage = True
@@ -2089,7 +2114,8 @@ class Cmd(cmd.Cmd):
sys.stdout = self.stdout = new_stdout
if statement.output == constants.REDIRECTION_APPEND:
- self.poutput(get_paste_buffer())
+ self.stdout.write(get_paste_buffer())
+ self.stdout.flush()
return redir_error, saved_state
@@ -2695,11 +2721,13 @@ class Cmd(cmd.Cmd):
command = tokens[cmd_index]
matches = []
- # Check if this is a command with an argparse function
+ # Check if this command uses argparse
func = self.cmd_func(command)
- if func and hasattr(func, 'argparser'):
+ argparser = getattr(func, CMD_ATTR_ARGPARSER, None)
+
+ if func is not None and argparser is not None:
from .argparse_completer import AutoCompleter
- completer = AutoCompleter(getattr(func, 'argparser'), self)
+ completer = AutoCompleter(argparser, self)
matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx)
return matches
@@ -2726,11 +2754,12 @@ class Cmd(cmd.Cmd):
# Getting help for a specific command
func = self.cmd_func(args.command)
help_func = getattr(self, HELP_FUNC_PREFIX + args.command, None)
+ argparser = getattr(func, CMD_ATTR_ARGPARSER, None)
# If the command function uses argparse, then use argparse's help
- if func and hasattr(func, 'argparser'):
+ if func is not None and argparser is not None:
from .argparse_completer import AutoCompleter
- completer = AutoCompleter(getattr(func, 'argparser'), self)
+ completer = AutoCompleter(argparser, self)
tokens = [args.command] + args.subcommand
# Set end to blank so the help output matches how it looks when "command -h" is used
@@ -2769,11 +2798,11 @@ class Cmd(cmd.Cmd):
help_topics.remove(command)
# Non-argparse commands can have help_functions for their documentation
- if not hasattr(func, 'argparser'):
+ if not hasattr(func, CMD_ATTR_ARGPARSER):
has_help_func = True
- if hasattr(func, HELP_CATEGORY):
- category = getattr(func, HELP_CATEGORY)
+ if hasattr(func, CMD_ATTR_HELP_CATEGORY):
+ category = getattr(func, CMD_ATTR_HELP_CATEGORY)
cmds_cats.setdefault(category, [])
cmds_cats[category].append(command)
elif func.__doc__ or has_help_func:
@@ -2791,7 +2820,7 @@ class Cmd(cmd.Cmd):
self.poutput('{}'.format(str(self.doc_header)), end="\n\n")
for category in sorted(cmds_cats.keys(), key=self.default_sort_key):
self._print_topics(category, cmds_cats[category], verbose)
- self._print_topics('Other', cmds_doc, verbose)
+ self._print_topics(self.default_category, cmds_doc, verbose)
self.print_topics(self.misc_header, help_topics, 15, 80)
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
@@ -2826,7 +2855,7 @@ class Cmd(cmd.Cmd):
cmd_func = self.cmd_func(command)
# Non-argparse commands can have help_functions for their documentation
- if not hasattr(cmd_func, 'argparser') and command in topics:
+ if not hasattr(cmd_func, CMD_ATTR_ARGPARSER) and command in topics:
help_func = getattr(self, HELP_FUNC_PREFIX + command)
result = io.StringIO()
@@ -3199,13 +3228,13 @@ class Cmd(cmd.Cmd):
@with_argparser(py_parser, preserve_quotes=True)
def do_py(self, args: argparse.Namespace) -> bool:
"""Invoke Python command or shell"""
- from .pyscript_bridge import PyscriptBridge
+ from .py_bridge import PyBridge
if self._in_py:
err = "Recursively entering interactive Python consoles is not allowed."
self.perror(err)
return False
- bridge = PyscriptBridge(self)
+ py_bridge = PyBridge(self)
try:
self._in_py = True
@@ -3217,8 +3246,14 @@ class Cmd(cmd.Cmd):
"""
expanded_filename = os.path.expanduser(filename)
+ if not expanded_filename.endswith('.py'):
+ self.pwarning("'{}' does not have a .py extension".format(expanded_filename))
+ selection = self.select('Yes No', 'Continue to try to run it as a Python script? ')
+ if selection != 'Yes':
+ return
+
# cmd_echo defaults to False for scripts. The user can always toggle this value in their script.
- bridge.cmd_echo = False
+ py_bridge.cmd_echo = False
try:
with open(expanded_filename) as f:
@@ -3231,17 +3266,17 @@ class Cmd(cmd.Cmd):
raise EmbeddedConsoleExit
# Set up Python environment
- self._pystate[self.pyscript_name] = bridge
- self._pystate['run'] = py_run
- self._pystate['quit'] = py_quit
- self._pystate['exit'] = py_quit
+ 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
if self.locals_in_py:
- self._pystate['self'] = self
- elif 'self' in self._pystate:
- del self._pystate['self']
+ self.py_locals['self'] = self
+ elif 'self' in self.py_locals:
+ del self.py_locals['self']
- localvars = self._pystate
+ localvars = self.py_locals
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
@@ -3251,9 +3286,9 @@ class Cmd(cmd.Cmd):
if args.remainder:
full_command += ' ' + ' '.join(args.remainder)
- # Set cmd_echo to True so PyscriptBridge statements like: py app('help')
+ # Set cmd_echo to True so PyBridge statements like: py app('help')
# run at the command line will print their output.
- bridge.cmd_echo = True
+ py_bridge.cmd_echo = True
# noinspection PyBroadException
try:
@@ -3268,7 +3303,7 @@ class Cmd(cmd.Cmd):
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")'
- .format(self.pyscript_name))
+ .format(self.py_bridge_name))
saved_cmd2_env = None
@@ -3296,7 +3331,7 @@ class Cmd(cmd.Cmd):
finally:
self._in_py = False
- return bridge.stop
+ return py_bridge.stop
run_pyscript_parser = Cmd2ArgumentParser()
run_pyscript_parser.add_argument('script_path', help='path to the script file', completer_method=path_complete)
@@ -3334,23 +3369,31 @@ class Cmd(cmd.Cmd):
@with_argparser(Cmd2ArgumentParser())
def do_ipy(self, _: argparse.Namespace) -> None:
"""Enter an interactive IPython shell"""
- from .pyscript_bridge import PyscriptBridge
- bridge = PyscriptBridge(self)
-
+ from .py_bridge import PyBridge
banner = ('Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n'
'Run Python code from external files with: run filename.py\n')
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
- if self.locals_in_py:
- # noinspection PyUnusedLocal
- def load_ipy(cmd2_instance, app):
- embed(banner1=banner, exit_msg=exit_msg)
- load_ipy(self, bridge)
- else:
- # noinspection PyUnusedLocal
- def load_ipy(app):
- embed(banner1=banner, exit_msg=exit_msg)
- load_ipy(bridge)
+ def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge):
+ """
+ Embed an IPython shell in an environment that is restricted to only the variables in this function
+ :param cmd2_app: instance of the cmd2 app
+ :param py_bridge: a PyscriptBridge
+ """
+ # Create a variable pointing to py_bridge and name it using the value of py_bridge_name
+ exec("{} = py_bridge".format(cmd2_app.py_bridge_name))
+
+ # Add self variable pointing to cmd2_app, if allowed
+ if cmd2_app.locals_in_py:
+ exec("self = cmd2_app")
+
+ # Delete these names from the environment so IPython can't use them
+ del cmd2_app
+ del py_bridge
+
+ embed(banner1=banner, exit_msg=exit_msg)
+
+ load_ipy(self, PyBridge(self))
history_description = "View, run, edit, save, or clear previously entered commands"
@@ -3630,7 +3673,7 @@ class Cmd(cmd.Cmd):
# Check if all commands ran
if commands_run < len(history):
warning = "Command {} triggered a stop and ended transcript generation early".format(commands_run)
- self.perror(ansi.style_warning(warning))
+ self.pwarning(warning)
# finally, we can write the transcript out to the file
try:
@@ -3719,6 +3762,12 @@ class Cmd(cmd.Cmd):
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path))
return
+ if expanded_path.endswith('.py'):
+ self.pwarning("'{}' appears to be a Python file".format(expanded_path))
+ selection = self.select('Yes No', 'Continue to try to run it as a text script? ')
+ if selection != 'Yes':
+ return
+
try:
# Read all lines of the script
with open(expanded_path, encoding='utf-8') as target:
@@ -3965,7 +4014,7 @@ class Cmd(cmd.Cmd):
"""
for cmd_name in list(self.disabled_commands):
func = self.disabled_commands[cmd_name].command_function
- if hasattr(func, HELP_CATEGORY) and getattr(func, HELP_CATEGORY) == category:
+ if getattr(func, CMD_ATTR_HELP_CATEGORY, None) == category:
self.enable_command(cmd_name)
def disable_command(self, command: str, message_to_print: str) -> None:
@@ -4014,7 +4063,7 @@ class Cmd(cmd.Cmd):
for cmd_name in all_commands:
func = self.cmd_func(cmd_name)
- if hasattr(func, HELP_CATEGORY) and getattr(func, HELP_CATEGORY) == category:
+ if getattr(func, CMD_ATTR_HELP_CATEGORY, None) == category:
self.disable_command(cmd_name, message_to_print)
# noinspection PyUnusedLocal
diff --git a/cmd2/pyscript_bridge.py b/cmd2/py_bridge.py
index ac3dfd40..0a1b6ee7 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/py_bridge.py
@@ -1,6 +1,6 @@
# coding=utf-8
"""
-Bridges calls made inside of a pyscript with the Cmd2 host app while maintaining a reasonable
+Bridges calls made inside of a Python environment to the Cmd2 host app while maintaining a reasonable
degree of isolation between the two
"""
@@ -53,7 +53,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
return not self.stderr
-class PyscriptBridge(object):
+class PyBridge(object):
"""Provides a Python API wrapper for application commands."""
def __init__(self, cmd2_app):
self._cmd2_app = cmd2_app
@@ -70,7 +70,7 @@ class PyscriptBridge(object):
def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
"""
- Provide functionality to call application commands by calling PyscriptBridge
+ Provide functionality to call application commands by calling PyBridge
ex: app('help')
:param command: command line being run
:param echo: if True, output will be echoed to stdout/stderr while the command runs
@@ -95,7 +95,7 @@ class PyscriptBridge(object):
self._cmd2_app.stdout = copy_cmd_stdout
with redirect_stdout(copy_cmd_stdout):
with redirect_stderr(copy_stderr):
- stop = self._cmd2_app.onecmd_plus_hooks(command, pyscript_bridge_call=True)
+ stop = self._cmd2_app.onecmd_plus_hooks(command, py_bridge_call=True)
finally:
with self._cmd2_app.sigint_protection:
self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
diff --git a/tests/pyscript/stop.py b/tests/pyscript/stop.py
index e731218e..1578b057 100644
--- a/tests/pyscript/stop.py
+++ b/tests/pyscript/stop.py
@@ -2,7 +2,7 @@
app.cmd_echo = True
app('help')
-# This will set stop to True in the PyscriptBridge
+# This will set stop to True in the PyBridge
app('quit')
# Exercise py_quit() in unit test
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index d1e115d4..68a2320c 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -13,8 +13,8 @@ from cmd2 import with_argparser, Cmd2ArgumentParser, CompletionItem
from cmd2.utils import StdSim, basic_complete
from .conftest import run_cmd, complete_tester
-# Lists used in our tests
-static_int_choices_list = [-12, -1, -2, 0, 1, 2]
+# Lists used in our tests (there is a mix of sorted and unsorted on purpose)
+static_int_choices_list = [-1, 1, -2, 2, 0, -12]
static_choices_list = ['static', 'choices', 'stop', 'here']
choices_from_function = ['choices', 'function', 'chatty', 'smith']
choices_from_method = ['choices', 'method', 'most', 'improved']
@@ -353,12 +353,14 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completions):
('--function', 'ch', ['choices', 'chatty']),
('-m', '', choices_from_method),
('--method', 'm', ['method', 'most']),
- ('-i', '', [str(i) for i in static_int_choices_list]),
+ ('-i', '', static_int_choices_list),
('--int', '1', ['1 ']),
- ('--int', '-', ['-12', '-1', '-2']),
- ('--int', '-1', ['-12', '-1'])
+ ('--int', '-', [-1, -2, -12]),
+ ('--int', '-1', [-1, -12])
])
def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
+ import numbers
+
line = 'choices {} {}'.format(flag, text)
endidx = len(line)
begidx = endidx - len(text)
@@ -369,7 +371,14 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
else:
assert first_match is None
- assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)
+ # Numbers will be sorted in ascending order and then converted to strings by AutoCompleter
+ if all(isinstance(x, numbers.Number) for x in completions):
+ completions.sort()
+ completions = [str(x) for x in completions]
+ else:
+ completions.sort(key=ac_app.default_sort_key)
+
+ assert ac_app.completion_matches == completions
@pytest.mark.parametrize('pos, text, completions', [
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 1bdbea5f..4a9dca31 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -331,6 +331,15 @@ def test_run_script_with_binary_file(base_app, request):
out, err = run_cmd(base_app, 'run_script {}'.format(filename))
assert "is not an ASCII or UTF-8 encoded text file" in err[0]
+def test_run_script_with_python_file(base_app, request):
+ m = mock.MagicMock(name='input', return_value='2')
+ builtins.input = m
+
+ test_dir = os.path.dirname(request.module.__file__)
+ filename = os.path.join(test_dir, 'pyscript', 'stop.py')
+ out, err = run_cmd(base_app, 'run_script {}'.format(filename))
+ assert "appears to be a Python file" in err[0]
+
def test_run_script_with_utf8_file(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'scripts', 'utf8.txt')
diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py
index 50b1e301..ded95225 100644
--- a/tests/test_run_pyscript.py
+++ b/tests/test_run_pyscript.py
@@ -3,11 +3,18 @@
"""
Unit/functional testing for run_pytest in cmd2
"""
+import builtins
import os
-from cmd2 import plugin
+from cmd2 import plugin
from .conftest import run_cmd
+# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
+try:
+ import mock
+except ImportError:
+ from unittest import mock
+
HOOK_OUTPUT = "TEST_OUTPUT"
def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
@@ -36,6 +43,15 @@ def test_run_pyscript_with_nonexist_file(base_app):
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
assert "Error opening script file" in err[0]
+def test_run_pyscript_with_non_python_file(base_app, request):
+ m = mock.MagicMock(name='input', return_value='2')
+ builtins.input = m
+
+ test_dir = os.path.dirname(request.module.__file__)
+ filename = os.path.join(test_dir, 'scripts', 'help.txt')
+ out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename))
+ assert "does not have a .py extension" in err[0]
+
def test_run_pyscript_with_exception(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py')