Skip to content

Commit bef0774

Browse files
authored
Merge pull request #703 from python-cmd2/public_api
Minimize public API of cmd2.Cmd class
2 parents c12ba0f + eb1936e commit bef0774

27 files changed

+349
-381
lines changed

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
## 0.9.14 (TBD, 2019
1+
## 0.9.14 (TBD, 2019)
22
* Enhancements
33
* Added support for and testing with Python 3.8, starting with 3.8 beta
4+
* Improved information displayed during transcript testing
45
* Breaking Changes
56
* 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`
7+
* If you need to use Python 3.4, you should pin your requirements to use `cmd2` 0.9.13
8+
* Made lots of changes to minimize the public API of the `cmd2.Cmd` class
9+
* Attributes and methods we do not intend to be public now all begin with an underscore
10+
* We make no API stability guarantees about these internal functions
611
* **Renamed Commands Notice**
712
* The following commands have been renamed. The old names will be supported until the next release.
8-
* load --> run_script
9-
* _relative_load --> _relative_run_script
10-
* pyscript --> run_pyscript
13+
* `load` --> `run_script`
14+
* `_relative_load` --> `_relative_run_script`
15+
* `pyscript` --> `run_pyscript`
1116

1217
## 0.9.13 (June 14, 2019)
1318
* Bug Fixes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ class CmdLineApp(cmd2.Cmd):
241241

242242
def __init__(self):
243243
self.maxrepeats = 3
244-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
244+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
245245
shortcuts.update({'&': 'speak'})
246246

247247
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell

cmd2/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212

1313
from .cmd2 import Cmd, Statement, EmptyStatement, categorize
1414
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
15+
from .constants import DEFAULT_SHORTCUTS
1516
from .pyscript_bridge import CommandResult

cmd2/cmd2.py

Lines changed: 140 additions & 160 deletions
Large diffs are not rendered by default.

cmd2/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@
2424
COLORS_NEVER = 'Never'
2525
COLORS_TERMINAL = 'Terminal'
2626
COLORS_ALWAYS = 'Always'
27+
28+
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}

cmd2/parsing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
324324
This string is suitable for inclusion in an error message of your
325325
choice:
326326
327-
valid, errmsg = statement_parser.is_valid_command('>')
327+
valid, errmsg = _statement_parser.is_valid_command('>')
328328
if not valid:
329329
errmsg = "Alias {}".format(errmsg)
330330
"""

cmd2/pyscript_bridge.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
2323
2424
Any combination of these fields can be used when developing a scripting API for a given command.
2525
By default stdout, stderr, and stop will be captured for you. If there is additional command specific data,
26-
then write that to cmd2's _last_result member. That becomes the data member of this tuple.
26+
then write that to cmd2's last_result member. That becomes the data member of this tuple.
2727
2828
In some cases, the data member may contain everything needed for a command and storing stdout
2929
and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
@@ -88,7 +88,7 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
8888
# This will be used to capture sys.stderr
8989
copy_stderr = StdSim(sys.stderr, echo)
9090

91-
self._cmd2_app._last_result = None
91+
self._cmd2_app.last_result = None
9292

9393
stop = False
9494
try:
@@ -105,5 +105,5 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
105105
result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
106106
stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
107107
stop=stop,
108-
data=self._cmd2_app._last_result)
108+
data=self._cmd2_app.last_result)
109109
return result

cmd2/transcript.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
we need a mechanism to run each command in the transcript as
77
a unit test, comparing the expected output to the actual output.
88
9-
This file contains the classess necessary to make that work. These
10-
classes are used in cmd2.py::run_transcript_tests()
9+
This file contains the class necessary to make that work. This
10+
class is used in cmd2.py::run_transcript_tests()
1111
"""
1212
import re
1313
import unittest
@@ -27,27 +27,32 @@ class Cmd2TestCase(unittest.TestCase):
2727
"""
2828
cmdapp = None
2929

30-
def fetchTranscripts(self):
31-
self.transcripts = {}
32-
for fname in self.cmdapp.testfiles:
33-
tfile = open(fname)
34-
self.transcripts[fname] = iter(tfile.readlines())
35-
tfile.close()
36-
3730
def setUp(self):
3831
if self.cmdapp:
39-
self.fetchTranscripts()
32+
self._fetchTranscripts()
4033

4134
# Trap stdout
4235
self._orig_stdout = self.cmdapp.stdout
4336
self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
4437

38+
def tearDown(self):
39+
if self.cmdapp:
40+
# Restore stdout
41+
self.cmdapp.stdout = self._orig_stdout
42+
4543
def runTest(self): # was testall
4644
if self.cmdapp:
4745
its = sorted(self.transcripts.items())
4846
for (fname, transcript) in its:
4947
self._test_transcript(fname, transcript)
5048

49+
def _fetchTranscripts(self):
50+
self.transcripts = {}
51+
for fname in self.cmdapp.testfiles:
52+
tfile = open(fname)
53+
self.transcripts[fname] = iter(tfile.readlines())
54+
tfile.close()
55+
5156
def _test_transcript(self, fname: str, transcript):
5257
line_num = 0
5358
finished = False
@@ -205,8 +210,3 @@ def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str,
205210
# slash is not escaped, this is what we are looking for
206211
break
207212
return regex, pos, start
208-
209-
def tearDown(self):
210-
if self.cmdapp:
211-
# Restore stdout
212-
self.cmdapp.stdout = self._orig_stdout

cmd2/utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import glob
66
import os
77
import re
8+
import shutil
89
import subprocess
910
import sys
1011
import threading
@@ -348,6 +349,50 @@ def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]:
348349
return files
349350

350351

352+
def get_exes_in_path(starts_with: str) -> List[str]:
353+
"""Returns names of executables in a user's path
354+
355+
:param starts_with: what the exes should start with. leave blank for all exes in path.
356+
:return: a list of matching exe names
357+
"""
358+
# Purposely don't match any executable containing wildcards
359+
wildcards = ['*', '?']
360+
for wildcard in wildcards:
361+
if wildcard in starts_with:
362+
return []
363+
364+
# Get a list of every directory in the PATH environment variable and ignore symbolic links
365+
paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]
366+
367+
# Use a set to store exe names since there can be duplicates
368+
exes_set = set()
369+
370+
# Find every executable file in the user's path that matches the pattern
371+
for path in paths:
372+
full_path = os.path.join(path, starts_with)
373+
matches = files_from_glob_pattern(full_path + '*', access=os.X_OK)
374+
375+
for match in matches:
376+
exes_set.add(os.path.basename(match))
377+
378+
return list(exes_set)
379+
380+
381+
def center_text(msg: str, *, pad: str = ' ') -> str:
382+
"""Centers text horizontally for display within the current terminal, optionally padding both sides.
383+
384+
:param msg: message to display in the center
385+
:param pad: (optional) if provided, the first character will be used to pad both sides of the message
386+
:return: centered message, optionally padded on both sides with pad_char
387+
"""
388+
term_width = shutil.get_terminal_size().columns
389+
surrounded_msg = ' {} '.format(msg)
390+
if not pad:
391+
pad = ' '
392+
fill_char = pad[:1]
393+
return surrounded_msg.center(term_width, fill_char)
394+
395+
351396
class StdSim(object):
352397
"""
353398
Class to simulate behavior of sys.stdout or sys.stderr.

docs/argument_processing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ Here's what it looks like::
267267
if unknown:
268268
self.perror("dir does not take any positional arguments:", traceback_war=False)
269269
self.do_help('dir')
270-
self._last_result = CommandResult('', 'Bad arguments')
270+
self.last_result = CommandResult('', 'Bad arguments')
271271
return
272272

273273
# Get the contents as a list

docs/settingchanges.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the
3333

3434
class App(Cmd2):
3535
def __init__(self):
36-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
36+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
3737
shortcuts.update({'*': 'sneeze', '~': 'squirm'})
3838
cmd2.Cmd.__init__(self, shortcuts=shortcuts)
3939

examples/arg_print.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):
1919

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

examples/cmd_as_argument.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CmdLineApp(cmd2.Cmd):
2828
MUMBLE_LAST = ['right?']
2929

3030
def __init__(self):
31-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
31+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
3232
shortcuts.update({'&': 'speak'})
3333
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
3434
super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)

examples/colors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class CmdLineApp(cmd2.Cmd):
6363
MUMBLE_LAST = ['right?']
6464

6565
def __init__(self):
66-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
66+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
6767
shortcuts.update({'&': 'speak'})
6868
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
6969
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)

examples/decorator_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
class CmdLineApp(cmd2.Cmd):
2020
""" Example cmd2 application. """
2121
def __init__(self, ip_addr=None, port=None, transcript_files=None):
22-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
22+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
2323
shortcuts.update({'&': 'speak'})
2424
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
2525
super().__init__(use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'],

examples/example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class CmdLineApp(cmd2.Cmd):
2626
MUMBLE_LAST = ['right?']
2727

2828
def __init__(self):
29-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
29+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
3030
shortcuts.update({'&': 'speak'})
3131
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
3232
super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)

examples/hooks.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def add_whitespace_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.
6666
command_pattern = re.compile(r'^([^\s\d]+)(\d+)')
6767
match = command_pattern.search(command)
6868
if match:
69-
data.statement = self.statement_parser.parse("{} {} {}".format(
69+
data.statement = self._statement_parser.parse("{} {} {}".format(
7070
match.group(1),
7171
match.group(2),
7272
'' if data.statement.args is None else data.statement.args
@@ -76,7 +76,7 @@ def add_whitespace_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.
7676
def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
7777
"""A hook to make uppercase commands lowercase."""
7878
command = data.statement.command.lower()
79-
data.statement = self.statement_parser.parse("{} {}".format(
79+
data.statement = self._statement_parser.parse("{} {}".format(
8080
command,
8181
'' if data.statement.args is None else data.statement.args
8282
))
@@ -90,7 +90,7 @@ def abbrev_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.Postpars
9090
possible_cmds = [cmd for cmd in self.get_all_commands() if cmd.startswith(data.statement.command)]
9191
if len(possible_cmds) == 1:
9292
raw = data.statement.raw.replace(data.statement.command, possible_cmds[0], 1)
93-
data.statement = self.statement_parser.parse(raw)
93+
data.statement = self._statement_parser.parse(raw)
9494
return data
9595

9696
@cmd2.with_argument_list

examples/pirate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Pirate(cmd2.Cmd):
2929
"""A piratical example cmd2 application involving looting and drinking."""
3030
def __init__(self):
3131
"""Initialize the base class as well as this one"""
32-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
32+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
3333
shortcuts.update({'~': 'sing'})
3434
super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts)
3535

examples/plumbum_colors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class CmdLineApp(cmd2.Cmd):
6666
MUMBLE_LAST = ['right?']
6767

6868
def __init__(self):
69-
shortcuts = dict(self.DEFAULT_SHORTCUTS)
69+
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
7070
shortcuts.update({'&': 'speak'})
7171
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
7272
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)

examples/python_scripting.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def do_cd(self, arglist):
5858
if not arglist or len(arglist) != 1:
5959
self.perror("cd requires exactly 1 argument:", traceback_war=False)
6060
self.do_help('cd')
61-
self._last_result = cmd2.CommandResult('', 'Bad arguments')
61+
self.last_result = cmd2.CommandResult('', 'Bad arguments')
6262
return
6363

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

8585
if err:
8686
self.perror(err, traceback_war=False)
87-
self._last_result = cmd2.CommandResult(out, err, data)
87+
self.last_result = cmd2.CommandResult(out, err, data)
8888

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

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

116-
self._last_result = cmd2.CommandResult(data=contents)
116+
self.last_result = cmd2.CommandResult(data=contents)
117117

118118

119119
if __name__ == '__main__':

examples/scripts/conditional.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
app('cd {}'.format(directory))
2828

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

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

0 commit comments

Comments
 (0)