Skip to content

Rename load, _relative_load, and pyscript #701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* Added support for and testing with Python 3.8, starting with 3.8 beta
* Breaking Changes
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019 and is no longer supported by `cmd2`
* **Renamed Commands Notice**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magnitude of this change is a bit daunting, but everything looks good

* The following commands have been renamed. The old names will be supported until the next release.
* load --> run_script
* _relative_load --> _relative_run_script
* pyscript --> run_pyscript

## 0.9.13 (June 14, 2019)
* Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ examples/tab_au*.py @anselor
examples/tab_co*.py @kmvanbrunt

# Unit Tests
tests/pyscript/* @anselor
tests/pyscript/* @anselor @kmvanbrunt
tests/transcripts/* @kotfu
tests/__init__.py @kotfu
tests/conftest.py @kotfu @tleonhardt
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Click on image below to watch a short video demonstrating the capabilities of cm
Main Features
-------------
- Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent
- Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`)
- Python scripting of your application with ``pyscript``
- Text file scripting of your application with `run_script` (`@`) and `_relative_run_script` (`@@`)
- Python scripting of your application with ``run_pyscript``
- Run shell commands with ``!``
- Pipe command output to shell commands with `|`
- Redirect command output to file with `>`, `>>`
Expand All @@ -32,7 +32,7 @@ Main Features
- Special-character command shortcuts (beyond cmd's `?` and `!`)
- Command aliasing similar to bash `alias` command
- Macros, which are similar to aliases, but they can contain argument placeholders
- Ability to load commands at startup from an initialization script
- Ability to run commands at startup from an initialization script
- Settable environment parameters
- Parsing commands with arguments using `argparse`, including support for sub-commands
- Unicode character support
Expand All @@ -41,7 +41,7 @@ Main Features
- Support for Python 3.5+ on Windows, macOS, and Linux
- Trivial to provide built-in help for all commands
- Built-in regression testing framework for your applications (transcript-based testing)
- Transcripts for use with built-in regression can be automatically generated from `history -t` or `load -t`
- Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t`
- Alerts that seamlessly print while user enters text at prompt

Python 2.7 support is EOL
Expand Down Expand Up @@ -109,12 +109,12 @@ Instructions for implementing each feature follow.
- Simple scripting using ASCII text files with one command + arguments per line
- See the [Script files](https://cmd2.readthedocs.io/en/latest/freefeatures.html#script-files) section of the `cmd2` docs for more info
- See [script.txt](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/script.txt) for a trivial example script that can be
used in any `cmd2` application with the `load` command (or `@` shortcut)
used in any `cmd2` application with the `run_script` command (or `@` shortcut)

- Powerful and flexible built-in Python scripting of your application using the `pyscript` command
- Powerful and flexible built-in Python scripting of your application using the `run_pyscript` command
- Run arbitrary Python scripts within your `cmd2` application with the ability to also call custom `cmd2` commands
- No separate API for your end users to learn
- Syntax for calling `cmd2` commands in a `pyscript` is essentially identical to what they would enter on the command line
- Syntax for calling `cmd2` commands in a `run_pyscript` is essentially identical to what they would enter on the command line
- See the [Python](https://cmd2.readthedocs.io/en/latest/freefeatures.html#python) section of the `cmd2` docs for more info
- Also see the [python_scripting.py](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py)
example in conjunction with the [conditional.py](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/conditional.py) script
Expand Down
174 changes: 101 additions & 73 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
were using the standard library's cmd, while enjoying the extra features.

Searchable command history (commands: "history")
Load commands from file, save to file, edit commands in file
Run commands from file, save to file, edit commands in file
Multi-line commands
Special-character shortcut commands (beyond cmd's "@" and "!")
Special-character shortcut commands (beyond cmd's "?" and "!")
Settable environment parameters
Parsing commands with `argparse` argument parsers (flags)
Redirection to file or paste buffer (clipboard) with > or >>
Expand Down Expand Up @@ -327,7 +327,7 @@ class Cmd(cmd.Cmd):

Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
"""
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}
DEFAULT_EDITOR = utils.find_editor()

def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
Expand All @@ -343,7 +343,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
:param stdout: (optional) alternate output file object, if not specified, sys.stdout is used
:param persistent_history_file: (optional) file path to load a persistent cmd2 command history from
:param persistent_history_length: (optional) max number of history items to write to the persistent history file
:param startup_script: (optional) file path to a a script to load and execute at startup
:param startup_script: (optional) file path to a script to execute at startup
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
:param allow_cli_args: (optional) if True, then cmd2 will process command line arguments as either
commands to be run or, if -t is specified, transcript files to run.
Expand Down Expand Up @@ -398,7 +398,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
'timing': 'Report execution times'}

# Commands to exclude from the help menu and tab completion
self.hidden_commands = ['eof', '_relative_load']
self.hidden_commands = ['eof', '_relative_load', '_relative_run_script']

# Commands to exclude from the history command
# initialize history
Expand Down Expand Up @@ -429,7 +429,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
self._last_result = None

# Used load command to store the current script dir as a LIFO queue to support _relative_load command
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
self._script_dir = []

# Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
Expand Down Expand Up @@ -460,11 +460,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
# Commands that will run at the beginning of the command loop
self._startup_commands = []

# If a startup script is provided, then add it in the queue to load
# If a startup script is provided, then execute it in the startup commands
if startup_script is not None:
startup_script = os.path.abspath(os.path.expanduser(startup_script))
if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0:
self._startup_commands.append("load '{}'".format(startup_script))
self._startup_commands.append("run_script '{}'".format(startup_script))

# Transcript files to run instead of interactive command loop
self._transcript_files = None
Expand Down Expand Up @@ -3240,15 +3240,15 @@ def py_quit():

return bridge.stop

pyscript_parser = ACArgumentParser()
setattr(pyscript_parser.add_argument('script_path', help='path to the script file'),
run_pyscript_parser = ACArgumentParser()
setattr(run_pyscript_parser.add_argument('script_path', help='path to the script file'),
ACTION_ARG_CHOICES, ('path_complete',))
setattr(pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
help='arguments to pass to script'),
setattr(run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
help='arguments to pass to script'),
ACTION_ARG_CHOICES, ('path_complete',))

@with_argparser(pyscript_parser)
def do_pyscript(self, args: argparse.Namespace) -> bool:
@with_argparser(run_pyscript_parser)
def do_run_pyscript(self, args: argparse.Namespace) -> bool:
"""Run a Python script file inside the console"""
script_path = os.path.expanduser(args.script_path)
py_return = False
Expand All @@ -3270,9 +3270,16 @@ def do_pyscript(self, args: argparse.Namespace) -> bool:
finally:
# Restore command line arguments to original state
sys.argv = orig_args
if args.__statement__.command == "pyscript":
self.perror("pyscript has been renamed and will be removed in the next release, "
"please use run_pyscript instead\n",
traceback_war=False, err_color=Fore.LIGHTYELLOW_EX)

return py_return

# pyscript is deprecated
do_pyscript = do_run_pyscript

# Only include the do_ipy() method if IPython is available on the system
if ipython_available: # pragma: no cover
@with_argparser(ACArgumentParser())
Expand Down Expand Up @@ -3412,7 +3419,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
fobj.write('{}\n'.format(command.raw))
try:
self.do_edit(fname)
return self.do_load(fname)
return self.do_run_script(fname)
finally:
os.remove(fname)
elif args.output_file:
Expand Down Expand Up @@ -3623,94 +3630,115 @@ def _current_script_dir(self) -> Optional[str]:
else:
return None

load_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
"\n"
"Script should contain one command per line, just like the command would be\n"
"typed in the console.\n"
"\n"
"If the -r/--record_transcript flag is used, this command instead records\n"
"the output of the script commands to a transcript for testing purposes.\n"
)
run_script_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
"\n"
"Script should contain one command per line, just like the command would be\n"
"typed in the console.\n"
"\n"
"If the -r/--record_transcript flag is used, this command instead records\n"
"the output of the script commands to a transcript for testing purposes.\n"
)

load_parser = ACArgumentParser(description=load_description)
setattr(load_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file'),
run_script_parser = ACArgumentParser(description=run_script_description)
setattr(run_script_parser.add_argument('-t', '--transcript',
help='record the output of the script as a transcript file'),
ACTION_ARG_CHOICES, ('path_complete',))
setattr(load_parser.add_argument('script_path', help="path to the script file"),
setattr(run_script_parser.add_argument('script_path', help="path to the script file"),
ACTION_ARG_CHOICES, ('path_complete',))

@with_argparser(load_parser)
def do_load(self, args: argparse.Namespace) -> Optional[bool]:
@with_argparser(run_script_parser)
def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
"""
Run commands in script file that is encoded as either ASCII or UTF-8 text
:return: True if running of commands should stop
"""
expanded_path = os.path.abspath(os.path.expanduser(args.script_path))

# Make sure the path exists and we can access it
if not os.path.exists(expanded_path):
self.perror("'{}' does not exist or cannot be accessed".format(expanded_path), traceback_war=False)
return
# Wrap everything in a try/finally just to make sure the warning prints at end if `load` was called
try:
# Make sure the path exists and we can access it
if not os.path.exists(expanded_path):
self.perror("'{}' does not exist or cannot be accessed".format(expanded_path), traceback_war=False)
return

# Make sure expanded_path points to a file
if not os.path.isfile(expanded_path):
self.perror("'{}' is not a file".format(expanded_path), traceback_war=False)
return
# Make sure expanded_path points to a file
if not os.path.isfile(expanded_path):
self.perror("'{}' is not a file".format(expanded_path), traceback_war=False)
return

# Make sure the file is not empty
if os.path.getsize(expanded_path) == 0:
self.perror("'{}' is empty".format(expanded_path), traceback_war=False)
return
# Make sure the file is not empty
if os.path.getsize(expanded_path) == 0:
self.perror("'{}' is empty".format(expanded_path), traceback_war=False)
return

# Make sure the file is ASCII or UTF-8 encoded text
if not utils.is_text_file(expanded_path):
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False)
return
# Make sure the file is ASCII or UTF-8 encoded text
if not utils.is_text_file(expanded_path):
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False)
return

try:
# Read all lines of the script
with open(expanded_path, encoding='utf-8') as target:
script_commands = target.read().splitlines()
except OSError as ex: # pragma: no cover
self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex))
return
try:
# Read all lines of the script
with open(expanded_path, encoding='utf-8') as target:
script_commands = target.read().splitlines()
except OSError as ex: # pragma: no cover
self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex))
return

orig_script_dir_count = len(self._script_dir)
orig_script_dir_count = len(self._script_dir)

try:
self._script_dir.append(os.path.dirname(expanded_path))
try:
self._script_dir.append(os.path.dirname(expanded_path))

if args.transcript:
self._generate_transcript(script_commands, os.path.expanduser(args.transcript))
else:
return self.runcmds_plus_hooks(script_commands)
if args.transcript:
self._generate_transcript(script_commands, os.path.expanduser(args.transcript))
else:
return self.runcmds_plus_hooks(script_commands)

finally:
with self.sigint_protection:
# Check if a script dir was added before an exception occurred
if orig_script_dir_count != len(self._script_dir):
self._script_dir.pop()
finally:
with self.sigint_protection:
# Check if a script dir was added before an exception occurred
if orig_script_dir_count != len(self._script_dir):
self._script_dir.pop()
if args.__statement__.command == "load":
self.perror("load has been renamed and will be removed in the next release, "
"please use run_script instead\n",
traceback_war=False, err_color=Fore.LIGHTYELLOW_EX)

relative_load_description = load_description
relative_load_description += ("\n\n"
"If this is called from within an already-running script, the filename will be\n"
"interpreted relative to the already-running script's directory.")
# load has been deprecated
do_load = do_run_script

relative_load_epilog = ("Notes:\n"
" This command is intended to only be used within text file scripts.")
relative_run_script_description = run_script_description
relative_run_script_description += (
"\n\n"
"If this is called from within an already-running script, the filename will be\n"
"interpreted relative to the already-running script's directory.")

relative_load_parser = ACArgumentParser(description=relative_load_description, epilog=relative_load_epilog)
relative_load_parser.add_argument('file_path', help='a file path pointing to a script')
relative_run_script_epilog = ("Notes:\n"
" This command is intended to only be used within text file scripts.")

@with_argparser(relative_load_parser)
def do__relative_load(self, args: argparse.Namespace) -> Optional[bool]:
relative_run_script_parser = ACArgumentParser(description=relative_run_script_description,
epilog=relative_run_script_epilog)
relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')

@with_argparser(relative_run_script_parser)
def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]:
"""
Run commands in script file that is encoded as either ASCII or UTF-8 text
:return: True if running of commands should stop
"""
if args.__statement__.command == "_relative_load":
self.perror("_relative_load has been renamed and will be removed in the next release, "
"please use _relative_run_script instead\n",
traceback_war=False, err_color=Fore.LIGHTYELLOW_EX)

file_path = args.file_path
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
relative_path = os.path.join(self._current_script_dir or '', file_path)
return self.do_load(relative_path)
return self.do_run_script(relative_path)

# _relative_load has been deprecated
do__relative_load = do__relative_run_script

def run_transcript_tests(self, transcript_paths: List[str]) -> None:
"""Runs transcript tests for provided file(s).
Expand Down
2 changes: 1 addition & 1 deletion cmd2/pyscript_bridge.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# coding=utf-8
"""
Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable
Bridges calls made inside of a pyscript with the Cmd2 host app while maintaining a reasonable
degree of isolation between the two
"""

Expand Down
Loading