summaryrefslogtreecommitdiff
path: root/cmd2/py_bridge.py
blob: 7570c1a8396057d91aab41620523711fe31d4f56 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# coding=utf-8
"""
Bridges calls made inside of a Python environment to the Cmd2 host app
while maintaining a reasonable degree of isolation between the two.
"""

import sys
from contextlib import (
    redirect_stderr,
    redirect_stdout,
)
from typing import (
    IO,
    TYPE_CHECKING,
    Any,
    List,
    NamedTuple,
    Optional,
    TextIO,
    cast,
)

from .utils import (  # namedtuple_with_defaults,
    StdSim,
)

if TYPE_CHECKING:  # pragma: no cover
    import cmd2


class CommandResult(NamedTuple):
    """Encapsulates the results from a cmd2 app command

    :stdout: str - output captured from stdout while this command is executing
    :stderr: str - output captured from stderr while this command is executing
    :stop: bool - return value of onecmd_plus_hooks after it runs the given
           command line.
    :data: possible data populated by the command.

    Any combination of these fields can be used when developing a scripting API
    for a given command. By default stdout, stderr, and stop will be captured
    for you. If there is additional command specific data, then write that to
    cmd2's last_result member. That becomes the data member of this tuple.

    In some cases, the data member may contain everything needed for a command
    and storing stdout and stderr might just be a duplication of data that
    wastes memory. In that case, the StdSim can be told not to store output
    with its pause_storage member. While this member is True, any output sent
    to StdSim won't be saved in its buffer.

    The code would look like this::

        if isinstance(self.stdout, StdSim):
            self.stdout.pause_storage = True

        if isinstance(sys.stderr, StdSim):
            sys.stderr.pause_storage = True

    See :class:`~cmd2.utils.StdSim` for more information.

    .. note::

       Named tuples are immutable. The contents are there for access,
       not for modification.
    """

    stdout: str = ''
    stderr: str = ''
    stop: bool = False
    data: Any = None

    def __bool__(self) -> bool:
        """Returns True if the command succeeded, otherwise False"""

        # If data has a __bool__ method, then call it to determine success of command
        if self.data is not None and callable(getattr(self.data, '__bool__', None)):
            return bool(self.data)

        # Otherwise check if stderr was filled out
        else:
            return not self.stderr


class PyBridge:
    """Provides a Python API wrapper for application commands."""

    def __init__(self, cmd2_app: 'cmd2.Cmd') -> None:
        self._cmd2_app = cmd2_app
        self.cmd_echo = False

        # Tells if any of the commands run via __call__ returned True for stop
        self.stop = False

    def __dir__(self) -> List[str]:
        """Return a custom set of attribute names"""
        attributes: List[str] = []
        attributes.insert(0, 'cmd_echo')
        return attributes

    def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResult:
        """
        Provide functionality to call application commands by calling PyBridge
        ex: app('help')
        :param command: command line being run
        :param echo: If provided, this temporarily overrides the value of self.cmd_echo while the
                     command runs. If True, output will be echoed to stdout/stderr. (Defaults to None)

        """
        if echo is None:
            echo = self.cmd_echo

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

        # Pause the storing of stdout until onecmd_plus_hooks enables it
        copy_cmd_stdout.pause_storage = True

        # This will be used to capture sys.stderr
        copy_stderr = StdSim(sys.stderr, echo=echo)

        self._cmd2_app.last_result = None

        stop = False
        try:
            self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout)
            with redirect_stdout(cast(IO[str], copy_cmd_stdout)):
                with redirect_stderr(cast(IO[str], copy_stderr)):
                    stop = self._cmd2_app.onecmd_plus_hooks(command, py_bridge_call=True)
        finally:
            with self._cmd2_app.sigint_protection:
                self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream)
                self.stop = stop or self.stop

        # Save the output. If stderr is empty, set it to None.
        result = CommandResult(
            stdout=copy_cmd_stdout.getvalue(),
            stderr=copy_stderr.getvalue(),
            stop=stop,
            data=self._cmd2_app.last_result,
        )
        return result