Skip to content

Minimize public API of cmd2.Cmd class #703

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 14 commits into from
Jun 24, 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
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
## 0.9.14 (TBD, 2019
## 0.9.14 (TBD, 2019)
* Enhancements
* Added support for and testing with Python 3.8, starting with 3.8 beta
* Improved information displayed during transcript testing
* 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`
* If you need to use Python 3.4, you should pin your requirements to use `cmd2` 0.9.13
* Made lots of changes to minimize the public API of the `cmd2.Cmd` class
* Attributes and methods we do not intend to be public now all begin with an underscore
* We make no API stability guarantees about these internal functions
* **Renamed Commands Notice**
* 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
* `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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class CmdLineApp(cmd2.Cmd):

def __init__(self):
self.maxrepeats = 3
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})

# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
Expand Down
1 change: 1 addition & 0 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@

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
300 changes: 140 additions & 160 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions cmd2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@
COLORS_NEVER = 'Never'
COLORS_TERMINAL = 'Terminal'
COLORS_ALWAYS = 'Always'

DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}
2 changes: 1 addition & 1 deletion cmd2/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
This string is suitable for inclusion in an error message of your
choice:

valid, errmsg = statement_parser.is_valid_command('>')
valid, errmsg = _statement_parser.is_valid_command('>')
if not valid:
errmsg = "Alias {}".format(errmsg)
"""
Expand Down
6 changes: 3 additions & 3 deletions cmd2/pyscript_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr

Any combination of these fields can be used when developing a scripting API for a given command.
By default stdout, stderr, and stop will be captured for you. If there is additional command specific data,
then write that to cmd2's _last_result member. That becomes the data member of this tuple.
then write that to cmd2's last_result member. That becomes the data member of this tuple.

In some cases, the data member may contain everything needed for a command and storing stdout
and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
Expand Down Expand Up @@ -88,7 +88,7 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
# This will be used to capture sys.stderr
copy_stderr = StdSim(sys.stderr, echo)

self._cmd2_app._last_result = None
self._cmd2_app.last_result = None

stop = False
try:
Expand All @@ -105,5 +105,5 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
stop=stop,
data=self._cmd2_app._last_result)
data=self._cmd2_app.last_result)
return result
30 changes: 15 additions & 15 deletions cmd2/transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
we need a mechanism to run each command in the transcript as
a unit test, comparing the expected output to the actual output.

This file contains the classess necessary to make that work. These
classes are used in cmd2.py::run_transcript_tests()
This file contains the class necessary to make that work. This
class is used in cmd2.py::run_transcript_tests()
"""
import re
import unittest
Expand All @@ -27,27 +27,32 @@ class Cmd2TestCase(unittest.TestCase):
"""
cmdapp = None

def fetchTranscripts(self):
self.transcripts = {}
for fname in self.cmdapp.testfiles:
tfile = open(fname)
self.transcripts[fname] = iter(tfile.readlines())
tfile.close()

def setUp(self):
if self.cmdapp:
self.fetchTranscripts()
self._fetchTranscripts()

# Trap stdout
self._orig_stdout = self.cmdapp.stdout
self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)

def tearDown(self):
if self.cmdapp:
# Restore stdout
self.cmdapp.stdout = self._orig_stdout

def runTest(self): # was testall
if self.cmdapp:
its = sorted(self.transcripts.items())
for (fname, transcript) in its:
self._test_transcript(fname, transcript)

def _fetchTranscripts(self):
self.transcripts = {}
for fname in self.cmdapp.testfiles:
tfile = open(fname)
self.transcripts[fname] = iter(tfile.readlines())
tfile.close()

def _test_transcript(self, fname: str, transcript):
line_num = 0
finished = False
Expand Down Expand Up @@ -205,8 +210,3 @@ def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str,
# slash is not escaped, this is what we are looking for
break
return regex, pos, start

def tearDown(self):
if self.cmdapp:
# Restore stdout
self.cmdapp.stdout = self._orig_stdout
45 changes: 45 additions & 0 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import glob
import os
import re
import shutil
import subprocess
import sys
import threading
Expand Down Expand Up @@ -348,6 +349,50 @@ def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]:
return files


def get_exes_in_path(starts_with: str) -> List[str]:
"""Returns names of executables in a user's path

:param starts_with: what the exes should start with. leave blank for all exes in path.
:return: a list of matching exe names
"""
# Purposely don't match any executable containing wildcards
wildcards = ['*', '?']
for wildcard in wildcards:
if wildcard in starts_with:
return []

# Get a list of every directory in the PATH environment variable and ignore symbolic links
paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]

# Use a set to store exe names since there can be duplicates
exes_set = set()

# Find every executable file in the user's path that matches the pattern
for path in paths:
full_path = os.path.join(path, starts_with)
matches = files_from_glob_pattern(full_path + '*', access=os.X_OK)

for match in matches:
exes_set.add(os.path.basename(match))

return list(exes_set)


def center_text(msg: str, *, pad: str = ' ') -> str:
"""Centers text horizontally for display within the current terminal, optionally padding both sides.

:param msg: message to display in the center
:param pad: (optional) if provided, the first character will be used to pad both sides of the message
:return: centered message, optionally padded on both sides with pad_char
"""
term_width = shutil.get_terminal_size().columns
surrounded_msg = ' {} '.format(msg)
if not pad:
pad = ' '
fill_char = pad[:1]
return surrounded_msg.center(term_width, fill_char)


class StdSim(object):
"""
Class to simulate behavior of sys.stdout or sys.stderr.
Expand Down
2 changes: 1 addition & 1 deletion docs/argument_processing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ Here's what it looks like::
if unknown:
self.perror("dir does not take any positional arguments:", traceback_war=False)
self.do_help('dir')
self._last_result = CommandResult('', 'Bad arguments')
self.last_result = CommandResult('', 'Bad arguments')
return

# Get the contents as a list
Expand Down
2 changes: 1 addition & 1 deletion docs/settingchanges.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the

class App(Cmd2):
def __init__(self):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'*': 'sneeze', '~': 'squirm'})
cmd2.Cmd.__init__(self, shortcuts=shortcuts)

Expand Down
2 changes: 1 addition & 1 deletion examples/arg_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):

def __init__(self):
# Create command shortcuts which are typically 1 character abbreviations which can be used in place of a command
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'$': 'aprint', '%': 'oprint'})
super().__init__(shortcuts=shortcuts)

Expand Down
2 changes: 1 addition & 1 deletion examples/cmd_as_argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']

def __init__(self):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
Expand Down
2 changes: 1 addition & 1 deletion examples/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']

def __init__(self):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
Expand Down
2 changes: 1 addition & 1 deletion examples/decorator_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class CmdLineApp(cmd2.Cmd):
""" Example cmd2 application. """
def __init__(self, ip_addr=None, port=None, transcript_files=None):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'],
Expand Down
2 changes: 1 addition & 1 deletion examples/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']

def __init__(self):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
Expand Down
6 changes: 3 additions & 3 deletions examples/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def add_whitespace_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.
command_pattern = re.compile(r'^([^\s\d]+)(\d+)')
match = command_pattern.search(command)
if match:
data.statement = self.statement_parser.parse("{} {} {}".format(
data.statement = self._statement_parser.parse("{} {} {}".format(
match.group(1),
match.group(2),
'' if data.statement.args is None else data.statement.args
Expand All @@ -76,7 +76,7 @@ def add_whitespace_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.
def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"""A hook to make uppercase commands lowercase."""
command = data.statement.command.lower()
data.statement = self.statement_parser.parse("{} {}".format(
data.statement = self._statement_parser.parse("{} {}".format(
command,
'' if data.statement.args is None else data.statement.args
))
Expand All @@ -90,7 +90,7 @@ def abbrev_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.Postpars
possible_cmds = [cmd for cmd in self.get_all_commands() if cmd.startswith(data.statement.command)]
if len(possible_cmds) == 1:
raw = data.statement.raw.replace(data.statement.command, possible_cmds[0], 1)
data.statement = self.statement_parser.parse(raw)
data.statement = self._statement_parser.parse(raw)
return data

@cmd2.with_argument_list
Expand Down
2 changes: 1 addition & 1 deletion examples/pirate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Pirate(cmd2.Cmd):
"""A piratical example cmd2 application involving looting and drinking."""
def __init__(self):
"""Initialize the base class as well as this one"""
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'~': 'sing'})
super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts)

Expand Down
2 changes: 1 addition & 1 deletion examples/plumbum_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']

def __init__(self):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
Expand Down
8 changes: 4 additions & 4 deletions examples/python_scripting.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def do_cd(self, arglist):
if not arglist or len(arglist) != 1:
self.perror("cd requires exactly 1 argument:", traceback_war=False)
self.do_help('cd')
self._last_result = cmd2.CommandResult('', 'Bad arguments')
self.last_result = cmd2.CommandResult('', 'Bad arguments')
return

# Convert relative paths to absolute paths
Expand All @@ -84,7 +84,7 @@ def do_cd(self, arglist):

if err:
self.perror(err, traceback_war=False)
self._last_result = cmd2.CommandResult(out, err, data)
self.last_result = cmd2.CommandResult(out, err, data)

# Enable tab completion for cd command
def complete_cd(self, text, line, begidx, endidx):
Expand All @@ -100,7 +100,7 @@ def do_dir(self, args, unknown):
if unknown:
self.perror("dir does not take any positional arguments:", traceback_war=False)
self.do_help('dir')
self._last_result = cmd2.CommandResult('', 'Bad arguments')
self.last_result = cmd2.CommandResult('', 'Bad arguments')
return

# Get the contents as a list
Expand All @@ -113,7 +113,7 @@ def do_dir(self, args, unknown):
self.stdout.write(fmt.format(f))
self.stdout.write('\n')

self._last_result = cmd2.CommandResult(data=contents)
self.last_result = cmd2.CommandResult(data=contents)


if __name__ == '__main__':
Expand Down
4 changes: 2 additions & 2 deletions examples/scripts/conditional.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
app('cd {}'.format(directory))

# Conditionally do something based on the results of the last command
if self._last_result:
if self.last_result:
print('\nContents of directory {!r}:'.format(directory))
app('dir -l')
print('{}\n'.format(self._last_result.data))
print('{}\n'.format(self.last_result.data))

# Change back to where we were
print('Changing back to original directory: {!r}'.format(original_dir))
Expand Down
Loading