Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
`Cmd._build_parser()`. This code had previously been restored to support backward
compatibility in `cmd2` 2.0 family.

- In an effort be consistent with the purpose of `self.stdout` and our own documentation, the
following changes were made.
- No longer redirecting `sys.stdout`.
- No longer capturing pyscript output written to `sys.stdout`.
- To assist with this change, calling `print()` within a pyscript now writes to
`self.stdout`. Calling `self.poutput()` within a pyscript is still preferred, but that
may not always be possible.

- Enhancements

- Simplified the process to set a custom parser for `cmd2's` built-in commands. See
Expand Down
25 changes: 10 additions & 15 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2856,7 +2856,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:

# Initialize the redirection saved state
redir_saved_state = utils.RedirectionSavedState(
cast(TextIO, self.stdout), sys.stdout, self._cur_pipe_proc_reader, self._redirecting
cast(TextIO, self.stdout), self._cur_pipe_proc_reader, self._redirecting
)

# The ProcReader for this command
Expand Down Expand Up @@ -2912,7 +2912,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
redir_saved_state.redirecting = True # type: ignore[unreachable]
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
sys.stdout = self.stdout = new_stdout
self.stdout = new_stdout

elif statement.output:
if statement.output_to:
Expand All @@ -2926,7 +2926,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
raise RedirectionError('Failed to redirect output') from ex

redir_saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
self.stdout = new_stdout

else:
# Redirecting to a paste buffer
Expand All @@ -2944,7 +2944,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
# create a temporary file to store output
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
redir_saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
self.stdout = new_stdout

if statement.output == constants.REDIRECTION_APPEND:
self.stdout.write(current_paste_buffer)
Expand Down Expand Up @@ -2972,9 +2972,8 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
# Close the file or pipe that stdout was redirected to
self.stdout.close()

# Restore the stdout values
# Restore self.stdout
self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout)
sys.stdout = cast(TextIO, saved_redir_state.saved_sys_stdout)

# Check if we need to wait for the process being piped to
if self._cur_pipe_proc_reader is not None:
Expand Down Expand Up @@ -4449,22 +4448,13 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
# Set up sys module for the Python console
self._reset_py_display()

cmd2_env.sys_stdout = sys.stdout
sys.stdout = self.stdout # type: ignore[assignment]

cmd2_env.sys_stdin = sys.stdin
sys.stdin = self.stdin # type: ignore[assignment]

return cmd2_env

def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
"""Restore cmd2 environment after exiting an interactive Python shell.

:param cmd2_env: the environment settings to restore
"""
sys.stdout = cmd2_env.sys_stdout # type: ignore[assignment]
sys.stdin = cmd2_env.sys_stdin # type: ignore[assignment]

# Set up readline for cmd2
if rl_type != RlType.NONE:
# Save py's history
Expand Down Expand Up @@ -4539,6 +4529,11 @@ def py_quit() -> None:
if self.self_in_py:
local_vars['self'] = self

# Since poutput() may not be available in a pyscript, like in the case when self_in_py is False,
# provide a version of print() which writes to self.stdout. This way, print's output can be
# captured and redirected.
local_vars['print'] = functools.partial(print, file=self.stdout)

# Handle case where we were called by do_run_pyscript()
if pyscript is not None:
# Read the script file
Expand Down
13 changes: 4 additions & 9 deletions cmd2/py_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
"""

import sys
from contextlib import (
redirect_stderr,
redirect_stdout,
)
from contextlib import redirect_stderr
from typing import (
IO,
TYPE_CHECKING,
Expand All @@ -19,9 +16,7 @@
cast,
)

from .utils import ( # namedtuple_with_defaults,
StdSim,
)
from .utils import StdSim

if TYPE_CHECKING: # pragma: no cover
import cmd2
Expand Down Expand Up @@ -113,7 +108,7 @@ def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResul
if echo is None:
echo = self.cmd_echo

# This will be used to capture _cmd2_app.stdout and sys.stdout
# This will be used to capture _cmd2_app.stdout
copy_cmd_stdout = StdSim(cast(Union[TextIO, StdSim], self._cmd2_app.stdout), echo=echo)

# Pause the storing of stdout until onecmd_plus_hooks enables it
Expand All @@ -127,7 +122,7 @@ def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResul
stop = False
try:
self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout)
with redirect_stdout(cast(IO[str], copy_cmd_stdout)), redirect_stderr(cast(IO[str], copy_stderr)):
with redirect_stderr(cast(IO[str], copy_stderr)):
stop = self._cmd2_app.onecmd_plus_hooks(
command,
add_to_history=self._add_to_history,
Expand Down
21 changes: 15 additions & 6 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,22 @@
import sys
import threading
import unicodedata
from collections.abc import Callable, Iterable
from collections.abc import (
Callable,
Iterable,
)
from difflib import SequenceMatcher
from enum import Enum
from typing import TYPE_CHECKING, Any, Optional, TextIO, TypeVar, Union, cast, get_type_hints
from typing import (
TYPE_CHECKING,
Any,
Optional,
TextIO,
TypeVar,
Union,
cast,
get_type_hints,
)

from . import constants
from .argparse_custom import ChoicesProviderFunc, CompleterFunc
Expand Down Expand Up @@ -683,23 +695,20 @@ class RedirectionSavedState:
def __init__(
self,
self_stdout: Union[StdSim, TextIO],
sys_stdout: Union[StdSim, TextIO],
pipe_proc_reader: Optional[ProcReader],
saved_redirecting: bool,
) -> None:
"""RedirectionSavedState initializer.

:param self_stdout: saved value of Cmd.stdout
:param sys_stdout: saved value of sys.stdout
:param pipe_proc_reader: saved value of Cmd._cur_pipe_proc_reader
:param saved_redirecting: saved value of Cmd._redirecting.
"""
# Tells if command is redirecting
self.redirecting = False

# Used to restore values after redirection ends
# Used to restore self.stdout after redirection ends
self.saved_self_stdout = self_stdout
self.saved_sys_stdout = sys_stdout

# Used to restore values after command ends regardless of whether the command redirected
self.saved_pipe_proc_reader = pipe_proc_reader
Expand Down
2 changes: 1 addition & 1 deletion docs/features/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ be run by the [edit](./builtin_commands.md#edit) command.

### feedback_to_output

Controls whether feedback generated with the `cmd2.Cmd.pfeedback` method is sent to `sys.stdout` or
Controls whether feedback generated with the `cmd2.Cmd.pfeedback` method is sent to `self.stdout` or
`sys.stderr`. If `False` the output will be sent to `sys.stderr`

If `True` the output is sent to `stdout` (which is often the screen but may be
Expand Down
27 changes: 16 additions & 11 deletions tests/test_run_pyscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest

from cmd2 import (
cmd2,
plugin,
utils,
)
Expand All @@ -18,14 +19,6 @@
run_cmd,
)

HOOK_OUTPUT = "TEST_OUTPUT"


def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
"""A cmdfinalization_hook hook which requests application exit"""
print(HOOK_OUTPUT)
return data


def test_run_pyscript(base_app, request) -> None:
test_dir = os.path.dirname(request.module.__file__)
Expand Down Expand Up @@ -133,11 +126,23 @@ def test_run_pyscript_dir(base_app, request) -> None:
assert out[0] == "['cmd_echo']"


def test_run_pyscript_stdout_capture(base_app, request) -> None:
base_app.register_cmdfinalization_hook(cmdfinalization_hook)
def test_run_pyscript_stdout_capture(request) -> None:
class HookApp(cmd2.Cmd):
HOOK_OUTPUT = "TEST_OUTPUT"

def __init__(self) -> None:
super().__init__()
self.register_cmdfinalization_hook(self.cmdfinalization_hook)

def cmdfinalization_hook(self, data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
"""A cmdfinalization_hook prints output."""
self.poutput(self.HOOK_OUTPUT)
return data

hook_app = HookApp()
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'pyscript', 'stdout_capture.py')
out, err = run_cmd(base_app, f'run_pyscript {python_script} {HOOK_OUTPUT}')
out, err = run_cmd(hook_app, f'run_pyscript {python_script} {hook_app.HOOK_OUTPUT}')

assert out[0] == "PASSED"
assert out[1] == "PASSED"
Expand Down
Loading