Skip to content

Commit e759a51

Browse files
committed
Remove dependency on Python's built-in cmd module
The cmd2.Cmd class no longer inhertis from cmd.Cmd
1 parent 1c5ebe1 commit e759a51

File tree

5 files changed

+55
-20
lines changed

5 files changed

+55
-20
lines changed

cmd2/cmd2.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
# import this module, many of these imports are lazy-loaded
2727
# i.e. we only import the module when we use it.
2828
import argparse
29-
import cmd
3029
import contextlib
3130
import copy
3231
import functools
@@ -286,7 +285,7 @@ def remove(self, command_method: CommandFunc) -> None:
286285
del self._parsers[full_method_name]
287286

288287

289-
class Cmd(cmd.Cmd):
288+
class Cmd:
290289
"""An easy but powerful framework for writing line-oriented command interpreters.
291290
292291
Extends the Python Standard Library's cmd package by adding a lot of useful features
@@ -304,6 +303,9 @@ class Cmd(cmd.Cmd):
304303
# List for storing transcript test file names
305304
testfiles: ClassVar[list[str]] = []
306305

306+
DEFAULT_PROMPT = '(Cmd) '
307+
IDENTCHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'
308+
307309
def __init__(
308310
self,
309311
completekey: str = 'tab',
@@ -326,6 +328,7 @@ def __init__(
326328
auto_load_commands: bool = False,
327329
allow_clipboard: bool = True,
328330
suggest_similar_command: bool = False,
331+
intro: str = '',
329332
) -> None:
330333
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
331334
@@ -376,6 +379,7 @@ def __init__(
376379
:param suggest_similar_command: If ``True``, ``cmd2`` will attempt to suggest the most
377380
similar command when the user types a command that does
378381
not exist. Default: ``False``.
382+
"param intro: Intro banner to print when starting the application.
379383
"""
380384
# Check if py or ipy need to be disabled in this instance
381385
if not include_py:
@@ -384,11 +388,29 @@ def __init__(
384388
setattr(self, 'do_ipy', None) # noqa: B010
385389

386390
# initialize plugin system
387-
# needs to be done before we call __init__(0)
391+
# needs to be done before we most of the other stuff below
388392
self._initialize_plugin_system()
389393

390-
# Call super class constructor
391-
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
394+
# Configure a few defaults
395+
self.prompt = Cmd.DEFAULT_PROMPT
396+
self.identchars = Cmd.IDENTCHARS
397+
self.intro = intro
398+
self.use_rawinput = True
399+
400+
# What to use for standard input
401+
if stdin is not None:
402+
self.stdin = stdin
403+
else:
404+
self.stdin = sys.stdin
405+
406+
# What to use for standard output
407+
if stdout is not None:
408+
self.stdout = stdout
409+
else:
410+
self.stdout = sys.stdout
411+
412+
# Key used for tab completion
413+
self.completekey = completekey
392414

393415
# Attributes which should NOT be dynamically settable via the set command at runtime
394416
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
@@ -3086,7 +3108,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
30863108

30873109
# Initialize the redirection saved state
30883110
redir_saved_state = utils.RedirectionSavedState(
3089-
cast(TextIO, self.stdout), stdouts_match, self._cur_pipe_proc_reader, self._redirecting
3111+
self.stdout, stdouts_match, self._cur_pipe_proc_reader, self._redirecting
30903112
)
30913113

30923114
# The ProcReader for this command
@@ -3141,7 +3163,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
31413163
new_stdout.close()
31423164
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
31433165
redir_saved_state.redirecting = True
3144-
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
3166+
cmd_pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
31453167

31463168
self.stdout = new_stdout
31473169
if stdouts_match:
@@ -3293,6 +3315,19 @@ def default(self, statement: Statement) -> bool | None: # type: ignore[override
32933315
self.perror(err_msg, style=None)
32943316
return None
32953317

3318+
def completedefault(self, *_ignored: list[str]) -> list[str]:
3319+
"""Call to complete an input line when no command-specific complete_*() method is available.
3320+
3321+
By default, it returns an empty list.
3322+
3323+
"""
3324+
return []
3325+
3326+
def completenames(self, text: str, *_ignored: list[str]) -> list[str]:
3327+
"""Help provide tab-completion options for command names."""
3328+
dotext = 'do_' + text
3329+
return [a[3:] for a in self.get_names() if a.startswith(dotext)]
3330+
32963331
def _suggest_similar_command(self, command: str) -> str | None:
32973332
return suggest_similar(command, self.get_visible_commands())
32983333

@@ -4131,10 +4166,6 @@ def _build_help_parser(cls) -> Cmd2ArgumentParser:
41314166
)
41324167
return help_parser
41334168

4134-
# Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
4135-
if getattr(cmd.Cmd, 'complete_help', None) is not None:
4136-
delattr(cmd.Cmd, 'complete_help')
4137-
41384169
@with_argparser(_build_help_parser)
41394170
def do_help(self, args: argparse.Namespace) -> None:
41404171
"""List available commands or provide detailed help for a specific command."""
@@ -4640,7 +4671,7 @@ def do_shell(self, args: argparse.Namespace) -> None:
46404671
**kwargs,
46414672
)
46424673

4643-
proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
4674+
proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
46444675
proc_reader.wait()
46454676

46464677
# Save the return code of the application for use in a pyscript
@@ -5359,7 +5390,7 @@ def _generate_transcript(
53595390
transcript += command
53605391

53615392
# Use a StdSim object to capture output
5362-
stdsim = utils.StdSim(cast(TextIO, self.stdout))
5393+
stdsim = utils.StdSim(self.stdout)
53635394
self.stdout = cast(TextIO, stdsim)
53645395

53655396
# then run the command and let the output go into our buffer
@@ -5385,7 +5416,7 @@ def _generate_transcript(
53855416
with self.sigint_protection:
53865417
# Restore altered attributes to their original state
53875418
self.echo = saved_echo
5388-
self.stdout = cast(TextIO, saved_stdout)
5419+
self.stdout = saved_stdout
53895420

53905421
# Check if all commands ran
53915422
if commands_run < len(history):
@@ -5880,7 +5911,7 @@ def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_
58805911
"""
58815912
self.perror(message_to_print, style=None)
58825913

5883-
def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override]
5914+
def cmdloop(self, intro: str = '') -> int: # type: ignore[override]
58845915
"""Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop().
58855916
58865917
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
@@ -5922,11 +5953,11 @@ def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override]
59225953
self._run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
59235954
else:
59245955
# If an intro was supplied in the method call, allow it to override the default
5925-
if intro is not None:
5956+
if intro:
59265957
self.intro = intro
59275958

59285959
# Print the intro, if there is one, right after the preloop
5929-
if self.intro is not None:
5960+
if self.intro:
59305961
self.poutput(self.intro)
59315962

59325963
# And then call _cmdloop() to enter the main loop

cmd2/py_bridge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def __call__(self, command: str, *, echo: bool | None = None) -> CommandResult:
137137
)
138138
finally:
139139
with self._cmd2_app.sigint_protection:
140-
self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream)
140+
self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout.inner_stream)
141141
if stdouts_match:
142142
sys.stdout = self._cmd2_app.stdout
143143

cmd2/transcript.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def setUp(self) -> None:
4646

4747
# Trap stdout
4848
self._orig_stdout = self.cmdapp.stdout
49-
self.cmdapp.stdout = cast(TextIO, utils.StdSim(cast(TextIO, self.cmdapp.stdout)))
49+
self.cmdapp.stdout = cast(TextIO, utils.StdSim(self.cmdapp.stdout))
5050

5151
def tearDown(self) -> None:
5252
"""Instructions that will be executed after each test method."""

docs/migrating/why.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ of [cmd][cmd] will add many features to an application without any further modif
2222
to `cmd2` will also open many additional doors for making it possible for developers to provide a
2323
top-notch interactive command-line experience for their users.
2424

25+
!!! warning
26+
27+
As of version 4.0.0, `cmd2` does not have an actual dependency on `cmd`. It is API compatible, but
28+
the `cmd2.Cmd` class no longer inherits from `cmd.Cmd`.
29+
2530
## Automatic Features
2631

2732
After switching from [cmd][cmd] to `cmd2`, your application will have the following new features and

mkdocs.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ plugins:
7878
show_if_no_docstring: true
7979
preload_modules:
8080
- argparse
81-
- cmd
8281
inherited_members: true
8382
members_order: source
8483
separate_signature: true

0 commit comments

Comments
 (0)