diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a1d1f9..7ecf797d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Fixed displaying of status code without a status message on non-`auto` themes. ([#1300](https://github.com/httpie/httpie/issues/1300)) - Fixed redundant issuance of stdin detection warnings on some rare cases due to underlying implementation. ([#1303](https://github.com/httpie/httpie/pull/1303)) - Improved regulation of top-level arrays. ([#1292](https://github.com/httpie/httpie/commit/225dccb2186f14f871695b6c4e0bfbcdb2e3aa28)) +- Improved UI layout for standalone invocations. ([#1296](https://github.com/httpie/httpie/pull/1296)) - Double `--quiet` flags will now suppress all python level warnings. ([#1271](https://github.com/httpie/httpie/issues/1271)) ## [3.0.2](https://github.com/httpie/httpie/compare/3.0.1...3.0.2) (2022-01-24) diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index f9d6674b..c632774d 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -48,12 +48,39 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter): text = dedent(text).strip() + '\n\n' return text.splitlines() + def add_usage(self, usage, actions, groups, prefix=None): + # Only display the positional arguments + displayed_actions = [ + action + for action in actions + if not action.option_strings + ] + + _, exception, _ = sys.exc_info() + if ( + isinstance(exception, argparse.ArgumentError) + and len(exception.args) >= 1 + and isinstance(exception.args[0], argparse.Action) + ): + # add_usage path is also taken when you pass an invalid option, + # e.g --style=invalid. If something like that happens, we want + # to include to action that caused to the invalid usage into + # the list of actions we are displaying. + displayed_actions.insert(0, exception.args[0]) + + super().add_usage( + usage, + displayed_actions, + groups, + prefix="usage:\n " + ) + # TODO: refactor and design type-annotated data structures # for raw args + parsed args and keep things immutable. class BaseHTTPieArgumentParser(argparse.ArgumentParser): - def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs): - super().__init__(*args, formatter_class=formatter_class, **kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.env = None self.args = None self.has_stdin_data = False @@ -116,9 +143,9 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser): """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs): kwargs.setdefault('add_help', False) - super().__init__(*args, **kwargs) + super().__init__(*args, formatter_class=formatter_class, **kwargs) # noinspection PyMethodOverriding def parse_args( @@ -529,3 +556,21 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser): for options_group in format_options: parsed_options = parse_format_options(options_group, defaults=parsed_options) self.args.format_options = parsed_options + + def error(self, message): + """Prints a usage message incorporating the message to stderr and + exits.""" + self.print_usage(sys.stderr) + self.exit( + 2, + dedent( + f''' + error: + {message} + + For more information: + - Try running {self.prog} --help + - Or visiting https://httpie.io/docs/cli + ''' + ) + ) diff --git a/tests/test_cli_ui.py b/tests/test_cli_ui.py new file mode 100644 index 00000000..35faf37f --- /dev/null +++ b/tests/test_cli_ui.py @@ -0,0 +1,84 @@ +import pytest +import shutil +import os +import sys +from tests.utils import http + + +if sys.version_info >= (3, 9): + REQUEST_ITEM_MSG = "[REQUEST_ITEM ...]" +else: + REQUEST_ITEM_MSG = "[REQUEST_ITEM [REQUEST_ITEM ...]]" + + +NAKED_HELP_MESSAGE = f"""\ +usage: + http [METHOD] URL {REQUEST_ITEM_MSG} + +error: + the following arguments are required: URL + +For more information: + - Try running http --help + - Or visiting https://httpie.io/docs/cli + +""" + +NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG = f"""\ +usage: + http [--pretty {{all,colors,format,none}}] [METHOD] URL {REQUEST_ITEM_MSG} + +error: + argument --pretty: expected one argument + +For more information: + - Try running http --help + - Or visiting https://httpie.io/docs/cli + +""" + +NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG = f"""\ +usage: + http [--pretty {{all,colors,format,none}}] [METHOD] URL {REQUEST_ITEM_MSG} + +error: + argument --pretty: invalid choice: '$invalid' (choose from 'all', 'colors', 'format', 'none') + +For more information: + - Try running http --help + - Or visiting https://httpie.io/docs/cli + +""" + + +PREDEFINED_TERMINAL_SIZE = (160, 80) + + +@pytest.fixture(scope="function") +def ignore_terminal_size(monkeypatch): + """Some tests wrap/crop the output depending on the + size of the executed terminal, which might not be consistent + through all runs. + + This fixture ensures every run uses the same exact configuration. + """ + + def fake_terminal_size(*args, **kwargs): + return os.terminal_size(PREDEFINED_TERMINAL_SIZE) + + # Setting COLUMNS as an env var is required for 3.8< + monkeypatch.setitem(os.environ, 'COLUMNS', str(PREDEFINED_TERMINAL_SIZE[0])) + monkeypatch.setattr(shutil, 'get_terminal_size', fake_terminal_size) + + +@pytest.mark.parametrize( + 'args, expected_msg', [ + ([], NAKED_HELP_MESSAGE), + (['--pretty'], NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG), + (['pie.dev', '--pretty'], NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG), + (['--pretty', '$invalid'], NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG), + ] +) +def test_naked_invocation(ignore_terminal_size, args, expected_msg): + result = http(*args, tolerate_error_exit_status=True) + assert result.stderr == expected_msg