Skip to content

Commit 9b12dd2

Browse files
committed
Remove remaining use of readline in cmd2.py and replace it with prompt_toolkit
1 parent e46744a commit 9b12dd2

File tree

7 files changed

+38
-43
lines changed

7 files changed

+38
-43
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ cmd2/exceptions.py @kmvanbrunt @anselor
3838
cmd2/history.py @tleonhardt
3939
cmd2/parsing.py @kmvanbrunt
4040
cmd2/plugin.py @anselor
41+
cmd2/pt_utils.py @kmvanbrunt @tleonhardt
4142
cmd2/py_bridge.py @kmvanbrunt
4243
cmd2/rich_utils.py @kmvanbrunt
43-
cmd2/rl_utils.py @kmvanbrunt
4444
cmd2/string_utils.py @kmvanbrunt
4545
cmd2/styles.py @tleonhardt @kmvanbrunt
4646
cmd2/terminal_utils.py @kmvanbrunt

cmd2/cmd2.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
cast,
6464
)
6565

66+
import prompt_toolkit as pt
6667
import rich.box
6768
from rich.console import Group, RenderableType
6869
from rich.highlighter import ReprHighlighter
@@ -137,7 +138,6 @@
137138
)
138139
from .styles import Cmd2Style
139140

140-
# NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff
141141
with contextlib.suppress(ImportError):
142142
from IPython import start_ipython
143143

@@ -254,6 +254,8 @@ class Cmd:
254254
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
255255
"""
256256

257+
DEFAULT_COMPLETEKEY = 'tab'
258+
257259
DEFAULT_EDITOR = utils.find_editor()
258260

259261
# Sorting keys for strings
@@ -267,7 +269,7 @@ class Cmd:
267269

268270
def __init__(
269271
self,
270-
completekey: str = 'tab',
272+
completekey: str = DEFAULT_COMPLETEKEY,
271273
stdin: TextIO | None = None,
272274
stdout: TextIO | None = None,
273275
*,
@@ -291,7 +293,7 @@ def __init__(
291293
) -> None:
292294
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
293295
294-
:param completekey: readline name of a completion key, default to Tab
296+
:param completekey: name of a completion key, default to Tab
295297
:param stdin: alternate input file object, if not specified, sys.stdin is used
296298
:param stdout: alternate output file object, if not specified, sys.stdout is used
297299
:param persistent_history_file: file path to load a persistent cmd2 command history from
@@ -369,6 +371,9 @@ def __init__(
369371

370372
# Key used for tab completion
371373
self.completekey = completekey
374+
if self.completekey != self.DEFAULT_COMPLETEKEY:
375+
# TODO(T or K): Configure prompt_toolkit `KeyBindings` with the custom key for completion # noqa: FIX002, TD003
376+
pass
372377

373378
# Attributes which should NOT be dynamically settable via the set command at runtime
374379
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
@@ -588,14 +593,14 @@ def __init__(
588593
# An optional hint which prints above tab completion suggestions
589594
self.completion_hint: str = ''
590595

591-
# Normally cmd2 uses readline's formatter to columnize the list of completion suggestions.
596+
# Normally cmd2 uses prompt-toolkit's formatter to columnize the list of completion suggestions.
592597
# If a custom format is preferred, write the formatted completions to this string. cmd2 will
593-
# then print it instead of the readline format. ANSI style sequences and newlines are supported
598+
# then print it instead of the prompt-toolkit format. ANSI style sequences and newlines are supported
594599
# when using this value. Even when using formatted_completions, the full matches must still be returned
595600
# from your completer function. ArgparseCompleter writes its tab completion tables to this string.
596601
self.formatted_completions: str = ''
597602

598-
# Used by complete() for readline tab completion
603+
# Used by complete() for prompt-toolkit tab completion
599604
self.completion_matches: list[str] = []
600605

601606
# Use this list if you need to display tab completion suggestions that are different than the actual text
@@ -1574,7 +1579,7 @@ def ppaged(
15741579
def _reset_completion_defaults(self) -> None:
15751580
"""Reset tab completion settings.
15761581
1577-
Needs to be called each time readline runs tab completion.
1582+
Needs to be called each time prompt-toolkit runs tab completion.
15781583
"""
15791584
self.allow_appended_space = True
15801585
self.allow_closing_quote = True
@@ -1970,12 +1975,12 @@ def complete_users() -> list[str]:
19701975
matches[index] += os.path.sep
19711976
self.display_matches[index] += os.path.sep
19721977

1973-
# Remove cwd if it was added to match the text readline expects
1978+
# Remove cwd if it was added to match the text prompt-toolkit expects
19741979
if cwd_added:
19751980
to_replace = cwd if cwd == os.path.sep else cwd + os.path.sep
19761981
matches = [cur_path.replace(to_replace, '', 1) for cur_path in matches]
19771982

1978-
# Restore the tilde string if we expanded one to match the text readline expects
1983+
# Restore the tilde string if we expanded one to match the text prompt-toolkit expects
19791984
if expanded_tilde_path:
19801985
matches = [cur_path.replace(expanded_tilde_path, orig_tilde_path, 1) for cur_path in matches]
19811986

@@ -2213,11 +2218,11 @@ def _perform_completion(
22132218
# Save the quote so we can add a matching closing quote later.
22142219
completion_token_quote = raw_completion_token[0]
22152220

2216-
# readline still performs word breaks after a quote. Therefore, something like quoted search
2221+
# prompt-toolkit still performs word breaks after a quote. Therefore, something like quoted search
22172222
# text with a space would have resulted in begidx pointing to the middle of the token we
22182223
# we want to complete. Figure out where that token actually begins and save the beginning
2219-
# portion of it that was not part of the text readline gave us. We will remove it from the
2220-
# completions later since readline expects them to start with the original text.
2224+
# portion of it that was not part of the text prompt-toolkit gave us. We will remove it from the
2225+
# completions later since prompt-toolkit expects them to start with the original text.
22212226
actual_begidx = line[:endidx].rfind(tokens[-1])
22222227

22232228
if actual_begidx != begidx:
@@ -2240,7 +2245,7 @@ def _perform_completion(
22402245
if not self.display_matches:
22412246
# Since self.display_matches is empty, set it to self.completion_matches
22422247
# before we alter them. That way the suggestions will reflect how we parsed
2243-
# the token being completed and not how readline did.
2248+
# the token being completed and not how prompt-toolkit did.
22442249
import copy
22452250

22462251
self.display_matches = copy.copy(self.completion_matches)
@@ -2290,12 +2295,12 @@ def complete(
22902295
) -> str | None:
22912296
"""Override of cmd's complete method which returns the next possible completion for 'text'.
22922297
2293-
This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
2298+
This completer function is called by prompt-toolkit as complete(text, state), for state in 0, 1, 2, …,
22942299
until it returns a non-string value. It should return the next possible completion starting with text.
22952300
2296-
Since readline suppresses any exception raised in completer functions, they can be difficult to debug.
2301+
Since prompt-toolkit suppresses any exception raised in completer functions, they can be difficult to debug.
22972302
Therefore, this function wraps the actual tab completion logic and prints to stderr any exception that
2298-
occurs before returning control to readline.
2303+
occurs before returning control to prompt-toolkit.
22992304
23002305
:param text: the current word that user is typing
23012306
:param state: non-negative integer
@@ -2575,7 +2580,7 @@ def postloop(self) -> None:
25752580
def parseline(self, line: str) -> tuple[str, str, str]:
25762581
"""Parse the line into a command name and a string containing the arguments.
25772582
2578-
:param line: line read by readline
2583+
:param line: line read by prompt-toolkit
25792584
:return: tuple containing (command, args, line)
25802585
"""
25812586
statement = self.statement_parser.parse_command_only(line)
@@ -3225,15 +3230,13 @@ def get_prompt() -> Any:
32253230
# Otherwise read from self.stdin
32263231
elif self.stdin.isatty():
32273232
# on a tty, print the prompt first, then read the line
3228-
self.poutput(prompt, end='')
3229-
self.stdout.flush()
3230-
line = self.stdin.readline()
3233+
line = pt.prompt(prompt)
32313234
if len(line) == 0:
32323235
raise EOFError
32333236
return line.rstrip('\n')
32343237
else:
32353238
# not a tty, just read the line
3236-
line = self.stdin.readline()
3239+
line = pt.prompt()
32373240
if len(line) == 0:
32383241
raise EOFError
32393242
return line.rstrip('\n')
@@ -4736,7 +4739,7 @@ def do_history(self, args: argparse.Namespace) -> bool | None:
47364739
if args.clear:
47374740
self.last_result = True
47384741

4739-
# Clear command and readline history
4742+
# Clear command and prompt-toolkit history
47404743
self.history.clear()
47414744

47424745
if self.persistent_history_file:
@@ -5333,8 +5336,8 @@ def async_refresh_prompt(self) -> None: # pragma: no cover
53335336
53345337
One case where the onscreen prompt and self.prompt can get out of sync is
53355338
when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
5336-
To prevent overwriting readline's onscreen search prompt, self.prompt is updated
5337-
but readline's saved prompt isn't.
5339+
To prevent overwriting prompt-toolkit's onscreen search prompt, self.prompt is updated
5340+
but prompt-toolkit's saved prompt isn't.
53385341
53395342
Therefore when a user aborts a search, the old prompt is still on screen until they
53405343
press Enter or this method is called. Call need_prompt_refresh() in an async print
@@ -5494,7 +5497,7 @@ def cmdloop(self, intro: RenderableType = '') -> int:
54945497
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
54955498
signal.signal(signal.SIGTERM, self.termination_signal_handler)
54965499

5497-
# Grab terminal lock before the command line prompt has been drawn by readline
5500+
# Grab terminal lock before the command line prompt has been drawn by prompt-toolkit
54985501
self.terminal_lock.acquire()
54995502

55005503
# Always run the preloop first

docs/api/index.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ incremented according to the [Semantic Version Specification](https://semver.org
2424
- [cmd2.history](./history.md) - classes for storing the history of previously entered commands
2525
- [cmd2.parsing](./parsing.md) - classes for parsing and storing user input
2626
- [cmd2.plugin](./plugin.md) - data classes for hook methods
27+
- [cmd2.pt_utils](./pt_utils.md) - utilities related to prompt-toolkit
2728
- [cmd2.py_bridge](./py_bridge.md) - classes for bridging calls from the embedded python environment
2829
to the host app
2930
- [cmd2.rich_utils](./rich_utils.md) - common utilities to support Rich in cmd2 applications
30-
- [cmd2.rl_utils](./rl_utils.md) - imports the proper Readline for the platform and provides utility
31-
functions for it
3231
- [cmd2.string_utils](./string_utils.md) - string utility functions
3332
- [cmd2.styles](./styles.md) - cmd2-specific Rich styles and a StrEnum of their corresponding names
3433
- [cmd2.terminal_utils](./terminal_utils.md) - support for terminal control escape sequences

docs/api/pt_utils.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# cmd2.pt_utils
2+
3+
::: cmd2.pt_utils

docs/api/rl_utils.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ nav:
204204
- api/history.md
205205
- api/parsing.md
206206
- api/plugin.md
207+
- api/pt_utils.md
207208
- api/py_bridge.md
208209
- api/rich_utils.md
209-
- api/rl_utils.md
210210
- api/string_utils.md
211211
- api/styles.md
212212
- api/terminal_utils.md

tests/conftest.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@
1111
TypeVar,
1212
cast,
1313
)
14-
from unittest import mock
1514

1615
import pytest
1716

1817
import cmd2
1918
from cmd2 import rich_utils as ru
20-
from cmd2.rl_utils import readline
2119
from cmd2.utils import StdSim
2220

2321
# For type hinting decorators
@@ -122,8 +120,8 @@ def cmd_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
122120

123121
def complete_tester(text: str, line: str, begidx: int, endidx: int, app: cmd2.Cmd) -> str | None:
124122
"""This is a convenience function to test cmd2.complete() since
125-
in a unit test environment there is no actual console readline
126-
is monitoring. Therefore we use mock to provide readline data
123+
in a unit test environment there is no actual console prompt-toolkit
124+
is monitoring. Therefore we use mock to provide prompt-toolkit data
127125
to complete().
128126
129127
:param text: the string prefix we are attempting to match
@@ -145,13 +143,8 @@ def get_begidx() -> int:
145143
def get_endidx() -> int:
146144
return endidx
147145

148-
# Run the readline tab completion function with readline mocks in place
149-
with (
150-
mock.patch.object(readline, 'get_line_buffer', get_line),
151-
mock.patch.object(readline, 'get_begidx', get_begidx),
152-
mock.patch.object(readline, 'get_endidx', get_endidx),
153-
):
154-
return app.complete(text, 0, line, begidx, endidx)
146+
# Run the prompt-toolkit tab completion function with mocks in place
147+
return app.complete(text, 0, line, begidx, endidx)
155148

156149

157150
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:

0 commit comments

Comments
 (0)