Skip to content

Commit 8bbdcd4

Browse files
committed
Merge branch 'master' into 3.0.0
2 parents e26278b + 0f102d5 commit 8bbdcd4

24 files changed

+133
-164
lines changed

cmd2/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""This simply imports certain things for backwards compatibility."""
22

33
import argparse
4+
import contextlib
45
import importlib.metadata as importlib_metadata
56
import sys
67

7-
try:
8+
with contextlib.suppress(importlib_metadata.PackageNotFoundError):
89
__version__ = importlib_metadata.version(__name__)
9-
except importlib_metadata.PackageNotFoundError: # pragma: no cover
10-
# package is not installed
11-
pass
1210

1311
from .ansi import (
1412
Bg,
@@ -54,7 +52,7 @@
5452
from .py_bridge import CommandResult
5553
from .utils import CompletionMode, CustomCompletionSettings, Settable, categorize
5654

57-
__all__: list[str] = [
55+
__all__: list[str] = [ # noqa: RUF022
5856
'COMMAND_NAME',
5957
'DEFAULT_SHORTCUTS',
6058
# ANSI Exports

cmd2/argparse_completer.py

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,7 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
9292
return False
9393

9494
# Flags can't have a space
95-
if ' ' in token:
96-
return False
97-
98-
# Starts like a flag
99-
return True
95+
return ' ' not in token
10096

10197

10298
class _ArgumentState:
@@ -368,34 +364,30 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
368364
# Otherwise treat as a positional argument
369365
else:
370366
# If we aren't current tracking a positional, then get the next positional arg to handle this token
371-
if pos_arg_state is None:
372-
# Make sure we are still have positional arguments to parse
373-
if remaining_positionals:
374-
action = remaining_positionals.popleft()
375-
376-
# Are we at a subcommand? If so, forward to the matching completer
377-
if action == self._subcommand_action:
378-
if token in self._subcommand_action.choices:
379-
# Merge self._parent_tokens and consumed_arg_values
380-
parent_tokens = {**self._parent_tokens, **consumed_arg_values}
381-
382-
# Include the subcommand name if its destination was set
383-
if action.dest != argparse.SUPPRESS:
384-
parent_tokens[action.dest] = [token]
385-
386-
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
387-
completer_type = self._cmd2_app._determine_ap_completer_type(parser)
388-
389-
completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens)
390-
391-
return completer.complete(
392-
text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set
393-
)
394-
# Invalid subcommand entered, so no way to complete remaining tokens
395-
return []
396-
397-
# Otherwise keep track of the argument
398-
pos_arg_state = _ArgumentState(action)
367+
if pos_arg_state is None and remaining_positionals:
368+
action = remaining_positionals.popleft()
369+
370+
# Are we at a subcommand? If so, forward to the matching completer
371+
if action == self._subcommand_action:
372+
if token in self._subcommand_action.choices:
373+
# Merge self._parent_tokens and consumed_arg_values
374+
parent_tokens = {**self._parent_tokens, **consumed_arg_values}
375+
376+
# Include the subcommand name if its destination was set
377+
if action.dest != argparse.SUPPRESS:
378+
parent_tokens[action.dest] = [token]
379+
380+
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
381+
completer_type = self._cmd2_app._determine_ap_completer_type(parser)
382+
383+
completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens)
384+
385+
return completer.complete(text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set)
386+
# Invalid subcommand entered, so no way to complete remaining tokens
387+
return []
388+
389+
# Otherwise keep track of the argument
390+
pos_arg_state = _ArgumentState(action)
399391

400392
# Check if we have a positional to consume this token
401393
if pos_arg_state is not None:

cmd2/argparse_custom.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -840,12 +840,8 @@ def _add_argument_wrapper(
840840
def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.Action) -> str:
841841
# Wrapper around ArgumentParser._get_nargs_pattern behavior to support nargs ranges
842842
nargs_range = action.get_nargs_range() # type: ignore[attr-defined]
843-
if nargs_range is not None:
844-
if nargs_range[1] == constants.INFINITY:
845-
range_max = ''
846-
else:
847-
range_max = nargs_range[1]
848-
843+
if nargs_range:
844+
range_max = '' if nargs_range[1] == constants.INFINITY else nargs_range[1]
849845
nargs_pattern = f'(-*A{{{nargs_range[0]},{range_max}}}-*)'
850846

851847
# if this is an optional action, -- is not allowed
@@ -1364,14 +1360,12 @@ def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type:
13641360
def error(self, message: str) -> NoReturn:
13651361
"""Custom override that applies custom formatting to the error message."""
13661362
lines = message.split('\n')
1367-
linum = 0
13681363
formatted_message = ''
1369-
for line in lines:
1364+
for linum, line in enumerate(lines):
13701365
if linum == 0:
13711366
formatted_message = 'Error: ' + line
13721367
else:
13731368
formatted_message += '\n ' + line
1374-
linum += 1
13751369

13761370
self.print_usage(sys.stderr)
13771371
formatted_message = ansi.style_error(formatted_message)

cmd2/cmd2.py

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
# setting is True
3131
import argparse
3232
import cmd
33+
import contextlib
3334
import copy
3435
import functools
3536
import glob
@@ -47,10 +48,6 @@
4748
namedtuple,
4849
)
4950
from collections.abc import Callable, Iterable, Mapping
50-
from contextlib import (
51-
redirect_stdout,
52-
suppress,
53-
)
5451
from types import (
5552
FrameType,
5653
ModuleType,
@@ -121,10 +118,8 @@
121118
)
122119

123120
# NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff
124-
try:
121+
with contextlib.suppress(ImportError):
125122
from IPython import start_ipython # type: ignore[import]
126-
except ImportError:
127-
pass
128123

129124
from .rl_utils import (
130125
RlType,
@@ -284,7 +279,7 @@ def remove(self, command_method: CommandFunc) -> None:
284279
class Cmd(cmd.Cmd):
285280
"""An easy but powerful framework for writing line-oriented command interpreters.
286281
287-
Extends the Python Standard Librarys cmd package by adding a lot of useful features
282+
Extends the Python Standard Library's cmd package by adding a lot of useful features
288283
to the out of the box configuration.
289284
290285
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
@@ -1088,9 +1083,8 @@ def add_settable(self, settable: Settable) -> None:
10881083
10891084
:param settable: Settable object being added
10901085
"""
1091-
if not self.always_prefix_settables:
1092-
if settable.name in self.settables and settable.name not in self._settables:
1093-
raise KeyError(f'Duplicate settable: {settable.name}')
1086+
if not self.always_prefix_settables and settable.name in self.settables and settable.name not in self._settables:
1087+
raise KeyError(f'Duplicate settable: {settable.name}')
10941088
self._settables[settable.name] = settable
10951089

10961090
def remove_settable(self, name: str) -> None:
@@ -1296,7 +1290,7 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
12961290
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
12971291
functional_terminal = False
12981292

1299-
if self.stdin.isatty() and self.stdout.isatty():
1293+
if self.stdin.isatty() and self.stdout.isatty(): # noqa: SIM102
13001294
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
13011295
functional_terminal = True
13021296

@@ -2725,8 +2719,8 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27252719
read_fd, write_fd = os.pipe()
27262720

27272721
# Open each side of the pipe
2728-
subproc_stdin = open(read_fd)
2729-
new_stdout: TextIO = cast(TextIO, open(write_fd, 'w'))
2722+
subproc_stdin = open(read_fd) # noqa: SIM115
2723+
new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) # noqa: SIM115
27302724

27312725
# Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
27322726
# our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
@@ -2756,7 +2750,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27562750
# like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process
27572751
# started OK, since the shell itself always starts. Therefore, we will wait a short time and check
27582752
# if the pipe process is still running.
2759-
with suppress(subprocess.TimeoutExpired):
2753+
with contextlib.suppress(subprocess.TimeoutExpired):
27602754
proc.wait(0.2)
27612755

27622756
# Check if the pipe process already exited
@@ -2776,7 +2770,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27762770
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
27772771
try:
27782772
# Use line buffering
2779-
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1))
2773+
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)) # noqa: SIM115
27802774
except OSError as ex:
27812775
raise RedirectionError(f'Failed to redirect because: {ex}')
27822776

@@ -2797,7 +2791,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27972791
# no point opening up the temporary file
27982792
current_paste_buffer = get_paste_buffer()
27992793
# create a temporary file to store output
2800-
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
2794+
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
28012795
redir_saved_state.redirecting = True
28022796
sys.stdout = self.stdout = new_stdout
28032797

@@ -2823,11 +2817,9 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
28232817
self.stdout.seek(0)
28242818
write_to_paste_buffer(self.stdout.read())
28252819

2826-
try:
2820+
with contextlib.suppress(BrokenPipeError):
28272821
# Close the file or pipe that stdout was redirected to
28282822
self.stdout.close()
2829-
except BrokenPipeError:
2830-
pass
28312823

28322824
# Restore the stdout values
28332825
self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout)
@@ -3081,11 +3073,9 @@ def _read_command_line(self, prompt: str) -> str:
30813073
"""
30823074
try:
30833075
# Wrap in try since terminal_lock may not be locked
3084-
try:
3076+
with contextlib.suppress(RuntimeError):
30853077
# Command line is about to be drawn. Allow asynchronous changes to the terminal.
30863078
self.terminal_lock.release()
3087-
except RuntimeError:
3088-
pass
30893079
return self.read_input(prompt, completion_mode=utils.CompletionMode.COMMANDS)
30903080
except EOFError:
30913081
return 'eof'
@@ -3622,7 +3612,7 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
36223612
result = io.StringIO()
36233613

36243614
# try to redirect system stdout
3625-
with redirect_stdout(result):
3615+
with contextlib.redirect_stdout(result):
36263616
# save our internal stdout
36273617
stdout_orig = self.stdout
36283618
try:
@@ -3948,7 +3938,7 @@ def _reset_py_display() -> None:
39483938
# Delete any prompts that have been set
39493939
attributes = ['ps1', 'ps2', 'ps3']
39503940
for cur_attr in attributes:
3951-
with suppress(KeyError):
3941+
with contextlib.suppress(KeyError):
39523942
del sys.__dict__[cur_attr]
39533943

39543944
# Reset functions
@@ -4126,7 +4116,7 @@ def py_quit() -> None:
41264116

41274117
# Check if we are running Python code
41284118
if py_code_to_run:
4129-
try:
4119+
try: # noqa: SIM105
41304120
interp.runcode(py_code_to_run) # type: ignore[arg-type]
41314121
except BaseException: # noqa: BLE001, S110
41324122
# We don't care about any exception that happened in the Python code
@@ -4377,7 +4367,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
43774367
self.last_result = False
43784368

43794369
# -v must be used alone with no other options
4380-
if args.verbose:
4370+
if args.verbose: # noqa: SIM102
43814371
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
43824372
self.poutput("-v cannot be used with any other options")
43834373
return None

cmd2/rl_utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Imports the proper Readline for the platform and provides utility functions for it."""
22

3+
import contextlib
34
import sys
45
from enum import (
56
Enum,
@@ -27,10 +28,8 @@
2728
import gnureadline as readline # type: ignore[import]
2829
except ImportError:
2930
# Note: If this actually fails, you should install gnureadline on Linux/Mac or pyreadline3 on Windows.
30-
try:
31+
with contextlib.suppress(ImportError):
3132
import readline # type: ignore[no-redef]
32-
except ImportError: # pragma: no cover
33-
pass
3433

3534

3635
class RlType(Enum):

cmd2/transcript.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ def _fetch_transcripts(self) -> None:
6464
self.transcripts = {}
6565
testfiles = cast(list[str], getattr(self.cmdapp, 'testfiles', []))
6666
for fname in testfiles:
67-
tfile = open(fname)
68-
self.transcripts[fname] = iter(tfile.readlines())
69-
tfile.close()
67+
with open(fname) as tfile:
68+
self.transcripts[fname] = iter(tfile.readlines())
7069

7170
def _test_transcript(self, fname: str, transcript: Iterator[str]) -> None:
7271
if self.cmdapp is None:

cmd2/utils.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import collections
5+
import contextlib
56
import functools
67
import glob
78
import inspect
@@ -530,7 +531,7 @@ class ByteBuf:
530531
"""Used by StdSim to write binary data and stores the actual bytes written."""
531532

532533
# Used to know when to flush the StdSim
533-
NEWLINES = [b'\n', b'\r']
534+
NEWLINES = (b'\n', b'\r')
534535

535536
def __init__(self, std_sim_instance: StdSim) -> None:
536537
self.byte_buf = bytearray()
@@ -646,11 +647,9 @@ def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) ->
646647
if isinstance(to_write, str):
647648
to_write = to_write.encode()
648649

649-
try:
650+
# BrokenPipeError can occur if output is being piped to a process that closed
651+
with contextlib.suppress(BrokenPipeError):
650652
stream.buffer.write(to_write)
651-
except BrokenPipeError:
652-
# This occurs if output is being piped to a process that closed
653-
pass
654653

655654

656655
class ContextFlag:
@@ -877,12 +876,8 @@ def align_text(
877876
line_styles = list(get_styles_dict(line).values())
878877

879878
# Calculate how wide each side of filling needs to be
880-
if line_width >= width:
881-
# Don't return here even though the line needs no fill chars.
882-
# There may be styles sequences to restore.
883-
total_fill_width = 0
884-
else:
885-
total_fill_width = width - line_width
879+
total_fill_width = 0 if line_width >= width else width - line_width
880+
# Even if the line needs no fill chars, there may be styles sequences to restore
886881

887882
if alignment == TextAlignment.LEFT:
888883
left_fill_width = 0

examples/cmd_as_argument.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class CmdLineApp(cmd2.Cmd):
2121

2222
# Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
2323
# default_to_shell = True # noqa: ERA001
24-
MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
25-
MUMBLE_FIRST = ['so', 'like', 'well']
26-
MUMBLE_LAST = ['right?']
24+
MUMBLES = ('like', '...', 'um', 'er', 'hmmm', 'ahh')
25+
MUMBLE_FIRST = ('so', 'like', 'well')
26+
MUMBLE_LAST = ('right?',)
2727

2828
def __init__(self) -> None:
2929
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)

examples/custom_parser.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
2525
def error(self, message: str) -> NoReturn:
2626
"""Custom override that applies custom formatting to the error message."""
2727
lines = message.split('\n')
28-
linum = 0
2928
formatted_message = ''
30-
for line in lines:
29+
for linum, line in enumerate(lines):
3130
if linum == 0:
3231
formatted_message = 'Error: ' + line
3332
else:
3433
formatted_message += '\n ' + line
35-
linum += 1
3634

3735
self.print_usage(sys.stderr)
3836

0 commit comments

Comments
 (0)