Skip to content
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

Updated poutput to use new style_output definition. #1269

Merged
merged 6 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions cmd2/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,9 @@ def style(
# Default styles for printing strings of various types.
# These can be altered to suit an application's needs and only need to be a
# function with the following structure: func(str) -> str
style_output = functools.partial(style)
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text for normal output"""

style_success = functools.partial(style, fg=Fg.GREEN)
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify success"""

Expand Down
8 changes: 4 additions & 4 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ def _format_usage(

# build full usage string
format = self._format_actions_usage
action_usage = format(required_options + optionals + positionals, groups)
action_usage = format(required_options + optionals + positionals, groups) # type: ignore[arg-type]
usage = ' '.join([s for s in [prog, action_usage] if s])

# wrap the usage parts if it's too long
Expand All @@ -1128,9 +1128,9 @@ def _format_usage(

# break usage into wrappable parts
part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
req_usage = format(required_options, groups)
opt_usage = format(optionals, groups)
pos_usage = format(positionals, groups)
req_usage = format(required_options, groups) # type: ignore[arg-type]
opt_usage = format(optionals, groups) # type: ignore[arg-type]
pos_usage = format(positionals, groups) # type: ignore[arg-type]
req_parts = re.findall(part_regexp, req_usage)
opt_parts = re.findall(part_regexp, opt_usage)
pos_parts = re.findall(part_regexp, pos_usage)
Expand Down
170 changes: 134 additions & 36 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
ModuleType,
)
from typing import (
IO,
Any,
Callable,
Dict,
Expand Down Expand Up @@ -601,11 +602,14 @@
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')

cmdset.on_register(self)
methods = inspect.getmembers(
cmdset,
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
and hasattr(meth, '__name__')
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
methods = cast(
List[Tuple[str, Callable[..., Any]]],
inspect.getmembers(
cmdset,
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
and hasattr(meth, '__name__')
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
),
)

default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None)
Expand Down Expand Up @@ -1056,7 +1060,40 @@
"""
return ansi.strip_style(self.prompt)

def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
def print_to(
self,
dest: Union[TextIO, IO[str]],
msg: Any,
*,
end: str = '\n',
style: Optional[Callable[[str], str]] = None,
paged: bool = False,
chop: bool = False,
) -> None:
final_msg = style(msg) if style is not None else msg
if paged:
self.ppaged(final_msg, end=end, chop=chop, dest=dest)

Check warning on line 1075 in cmd2/cmd2.py

View check run for this annotation

Codecov / codecov/patch

cmd2/cmd2.py#L1075

Added line #L1075 was not covered by tests
else:
try:
ansi.style_aware_write(dest, f'{final_msg}{end}')
except BrokenPipeError:

Check warning on line 1079 in cmd2/cmd2.py

View check run for this annotation

Codecov / codecov/patch

cmd2/cmd2.py#L1079

Added line #L1079 was not covered by tests
# This occurs if a command's output is being piped to another
# process and that process closes before the command is
# finished. If you would like your application to print a
# warning message, then set the broken_pipe_warning attribute
# to the message you want printed.
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)

Check warning on line 1086 in cmd2/cmd2.py

View check run for this annotation

Codecov / codecov/patch

cmd2/cmd2.py#L1085-L1086

Added lines #L1085 - L1086 were not covered by tests

def poutput(
self,
msg: Any = '',
*,
end: str = '\n',
apply_style: bool = True,
paged: bool = False,
chop: bool = False,
) -> None:
"""Print message to self.stdout and appends a newline by default

Also handles BrokenPipeError exceptions for when a command's output has
Expand All @@ -1065,44 +1102,87 @@

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
where the message text already has the desired style. Defaults to True.
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
try:
ansi.style_aware_write(self.stdout, f"{msg}{end}")
except BrokenPipeError:
# This occurs if a command's output is being piped to another
# process and that process closes before the command is
# finished. If you would like your application to print a
# warning message, then set the broken_pipe_warning attribute
# to the message you want printed.
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)
self.print_to(self.stdout, msg, end=end, style=ansi.style_output if apply_style else None, paged=paged, chop=chop)

# noinspection PyMethodMayBeStatic
def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
def perror(
self,
msg: Any = '',
*,
end: str = '\n',
apply_style: bool = True,
paged: bool = False,
chop: bool = False,
) -> None:
"""Print message to sys.stderr

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
where the message text already has the desired style. Defaults to True.
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
if apply_style:
final_msg = ansi.style_error(msg)
else:
final_msg = str(msg)
ansi.style_aware_write(sys.stderr, final_msg + end)
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None, paged=paged, chop=chop)

def psuccess(
self,
msg: Any = '',
*,
end: str = '\n',
paged: bool = False,
chop: bool = False,
) -> None:
"""Writes to stdout applying ansi.style_success by default

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
self.print_to(self.stdout, msg, end=end, style=ansi.style_success, paged=paged, chop=chop)

def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
def pwarning(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the signature for pwarning() match that of pfailure() (i.e. remove apply_style).
This seems appropriate since we now have two wrappers for perror(). They should work the same way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was reluctant to make an API breaking change to pwarning(). I don't know if it's possible to mark a single argument deprecated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a deprecation to the parameter documentation. We can remove it in the major version revision.

self,
msg: Any = '',
*,
end: str = '\n',
apply_style: bool = True,
paged: bool = False,
chop: bool = False,
) -> None:
"""Wraps perror, but applies ansi.style_warning by default

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param apply_style: If True, then ansi.style_warning will be applied to the message text. Set to False in cases
where the message text already has the desired style. Defaults to True.
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
if apply_style:
msg = ansi.style_warning(msg)
self.perror(msg, end=end, apply_style=False)
self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning if apply_style else None, paged=paged, chop=chop)

def pfailure(
self,
msg: Any = '',
*,
end: str = '\n',
paged: bool = False,
chop: bool = False,
) -> None:
"""Writes to stderr applying ansi.style_error by default

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error, paged=paged, chop=chop)

Check warning on line 1185 in cmd2/cmd2.py

View check run for this annotation

Codecov / codecov/patch

cmd2/cmd2.py#L1185

Added line #L1185 was not covered by tests

def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
Expand Down Expand Up @@ -1131,23 +1211,39 @@

self.perror(final_msg, end=end, apply_style=False)

def pfeedback(self, msg: Any, *, end: str = '\n') -> None:
def pfeedback(
self,
msg: Any,
*,
end: str = '\n',
apply_style: bool = True,
paged: bool = False,
chop: bool = False,
) -> None:
"""For printing nonessential feedback. Can be silenced with `quiet`.
Inclusion in redirected output is controlled by `feedback_to_output`.

:param msg: object to print
:param end: string appended after the end of the message, default a newline
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
where the message text already has the desired style. Defaults to True.
:param paged: If True, pass the output through the configured pager.
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
"""
if not self.quiet:
if self.feedback_to_output:
self.poutput(msg, end=end)
else:
self.perror(msg, end=end, apply_style=False)
self.print_to(
self.stdout if self.feedback_to_output else sys.stderr,
msg,
end=end,
style=ansi.style_output if apply_style else None,
paged=paged,
chop=chop,
)

def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False, dest: Optional[Union[TextIO, IO[str]]] = None) -> None:
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.

Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when
Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when
stdout or stdin are not a fully functional terminal.

:param msg: object to print
Expand All @@ -1157,11 +1253,13 @@
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
False -> causes lines longer than the screen width to wrap to the next line
- wrapping is ideal when you want to keep users from having to use horizontal scrolling
:param dest: Optionally specify the destination stream to write to. If unspecified, defaults to self.stdout

WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
"""
# msg can be any type, so convert to string before checking if it's blank
msg_str = str(msg)
dest = self.stdout if dest is None else dest

# Consider None to be no data to print
if msg is None or msg_str == '':
Expand Down Expand Up @@ -1195,7 +1293,7 @@
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
else:
self.poutput(msg_str, end=end)
ansi.style_aware_write(dest, f'{msg_str}{end}')
except BrokenPipeError:
# This occurs if a command's output is being piped to another process and that process closes before the
# command is finished. If you would like your application to print a warning message, then set the
Expand Down Expand Up @@ -4985,8 +5083,8 @@
if test_results.wasSuccessful():
ansi.style_aware_write(sys.stderr, stream.read())
finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='='))
self.poutput(finish_msg)
finish_msg = utils.align_center(finish_msg, fill_char='=')
self.psuccess(finish_msg)
else:
# Strip off the initial traceback which isn't particularly useful for end users
error_str = stream.read()
Expand Down
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@

# -- Options for Extensions -------------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {
'python 3': ('https://docs.python.org/3/', None),
}

# options for autodoc
autodoc_default_options = {'member-order': 'bysource'}
Expand Down
2 changes: 1 addition & 1 deletion examples/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def do_speak(self, args):

for _ in range(min(repetitions, self.maxrepeats)):
# .poutput handles newlines, and accommodates output redirection too
self.poutput(output_str)
self.poutput(output_str, apply_style=False)

def do_timetravel(self, _):
"""A command which always generates an error message, to demonstrate custom error colors"""
Expand Down
2 changes: 1 addition & 1 deletion examples/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def do_intro(self, _):
def do_echo(self, arg):
"""Example of a multiline command"""
fg_color = Fg[self.foreground_color.upper()]
self.poutput(style(arg, fg=fg_color))
self.poutput(style(arg, fg=fg_color), apply_style=False)


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion examples/pirate.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def do_quit(self, arg):

def do_sing(self, arg):
"""Sing a colorful song."""
self.poutput(cmd2.ansi.style(arg, fg=Fg[self.songcolor.upper()]))
self.poutput(cmd2.ansi.style(arg, fg=Fg[self.songcolor.upper()]), apply_style=False)

yo_parser = cmd2.Cmd2ArgumentParser()
yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'")
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1854,7 +1854,7 @@ def test_poutput_none(outsim_app):
def test_poutput_ansi_always(outsim_app):
msg = 'Hello World'
colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN)
outsim_app.poutput(colored_msg)
outsim_app.poutput(colored_msg, apply_style=False)
out = outsim_app.stdout.getvalue()
expected = colored_msg + '\n'
assert colored_msg != msg
Expand All @@ -1865,7 +1865,7 @@ def test_poutput_ansi_always(outsim_app):
def test_poutput_ansi_never(outsim_app):
msg = 'Hello World'
colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN)
outsim_app.poutput(colored_msg)
outsim_app.poutput(colored_msg, apply_style=False)
out = outsim_app.stdout.getvalue()
expected = msg + '\n'
assert colored_msg != msg
Expand Down