Skip to content

Update cmd from 3.13.5 #5920

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 2 commits into from
Jul 9, 2025
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
18 changes: 13 additions & 5 deletions Lib/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
functions respectively.
"""

import string, sys
import inspect, string, sys

__all__ = ["Cmd"]

Expand Down Expand Up @@ -108,7 +108,15 @@ def cmdloop(self, intro=None):
import readline
self.old_completer = readline.get_completer()
readline.set_completer(self.complete)
readline.parse_and_bind(self.completekey+": complete")
if readline.backend == "editline":
if self.completekey == 'tab':
# libedit uses "^I" instead of "tab"
command_string = "bind ^I rl_complete"
else:
command_string = f"bind {self.completekey} rl_complete"
else:
command_string = f"{self.completekey}: complete"
readline.parse_and_bind(command_string)
except ImportError:
pass
try:
Expand Down Expand Up @@ -210,9 +218,8 @@ def onecmd(self, line):
if cmd == '':
return self.default(line)
else:
try:
func = getattr(self, 'do_' + cmd)
except AttributeError:
func = getattr(self, 'do_' + cmd, None)
if func is None:
return self.default(line)
return func(arg)

Expand Down Expand Up @@ -298,6 +305,7 @@ def do_help(self, arg):
except AttributeError:
try:
doc=getattr(self, 'do_' + arg).__doc__
doc = inspect.cleandoc(doc)
if doc:
self.stdout.write("%s\n"%str(doc))
return
Expand Down
80 changes: 80 additions & 0 deletions Lib/test/support/pty_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Helper to run a script in a pseudo-terminal.
"""
import os
import selectors
import subprocess
import sys
from contextlib import ExitStack
from errno import EIO

from test.support.import_helper import import_module

def run_pty(script, input=b"dummy input\r", env=None):
pty = import_module('pty')
output = bytearray()
[master, slave] = pty.openpty()
args = (sys.executable, '-c', script)
proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env)
os.close(slave)
with ExitStack() as cleanup:
cleanup.enter_context(proc)
def terminate(proc):
try:
proc.terminate()
except ProcessLookupError:
# Workaround for Open/Net BSD bug (Issue 16762)
pass
cleanup.callback(terminate, proc)
cleanup.callback(os.close, master)
# Avoid using DefaultSelector and PollSelector. Kqueue() does not
# work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open
# BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4
# either (Issue 20472). Hopefully the file descriptor is low enough
# to use with select().
sel = cleanup.enter_context(selectors.SelectSelector())
sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE)
os.set_blocking(master, False)
while True:
for [_, events] in sel.select():
if events & selectors.EVENT_READ:
try:
chunk = os.read(master, 0x10000)
except OSError as err:
# Linux raises EIO when slave is closed (Issue 5380)
if err.errno != EIO:
raise
chunk = b""
if not chunk:
return output
output.extend(chunk)
if events & selectors.EVENT_WRITE:
try:
input = input[os.write(master, input):]
except OSError as err:
# Apparently EIO means the slave was closed
if err.errno != EIO:
raise
input = b"" # Stop writing
if not input:
sel.modify(master, selectors.EVENT_READ)


######################################################################
## Fake stdin (for testing interactive debugging)
######################################################################

class FakeInput:
"""
A fake input stream for pdb's interactive debugger. Whenever a
line is read, print it (to simulate the user typing it), and then
return it. The set of lines to return is specified in the
constructor; they should not have trailing newlines.
"""
def __init__(self, lines):
self.lines = lines

def readline(self):
line = self.lines.pop(0)
print(line)
return line + '\n'
57 changes: 46 additions & 11 deletions Lib/test/test_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import doctest
import unittest
import io
import textwrap
from test import support
from test.support.import_helper import import_module
from test.support.pty_helper import run_pty

class samplecmdclass(cmd.Cmd):
"""
Expand Down Expand Up @@ -244,23 +247,55 @@ def test_input_reset_at_EOF(self):
"(Cmd) *** Unknown syntax: EOF\n"))


class CmdPrintExceptionClass(cmd.Cmd):
"""
GH-80731
cmd.Cmd should print the correct exception in default()
>>> mycmd = CmdPrintExceptionClass()
>>> try:
... raise ValueError("test")
... except ValueError:
... mycmd.onecmd("not important")
(<class 'ValueError'>, ValueError('test'))
"""

def default(self, line):
print(sys.exc_info()[:2])


@support.requires_subprocess()
class CmdTestReadline(unittest.TestCase):
def setUpClass():
# Ensure that the readline module is loaded
# If this fails, the test is skipped because SkipTest will be raised
readline = import_module('readline')

def test_basic_completion(self):
script = textwrap.dedent("""
import cmd
class simplecmd(cmd.Cmd):
def do_tab_completion_test(self, args):
print('tab completion success')
return True

simplecmd().cmdloop()
""")

# 't' and complete 'ab_completion_test' to 'tab_completion_test'
input = b"t\t\n"

output = run_pty(script, input)

self.assertIn(b'ab_completion_test', output)
self.assertIn(b'tab completion success', output)

def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite())
return tests

def test_coverage(coverdir):
trace = support.import_module('trace')
tracer=trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,],
trace=0, count=1)
tracer.run('import importlib; importlib.reload(cmd); test_main()')
r=tracer.results()
print("Writing coverage results...")
r.write_results(show_missing=True, summary=True, coverdir=coverdir)

if __name__ == "__main__":
if "-c" in sys.argv:
test_coverage('/tmp/cmd.cover')
elif "-i" in sys.argv:
if "-i" in sys.argv:
samplecmdclass().cmdloop()
else:
unittest.main()
Loading