diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 9bf09b3b73..ed5ad4377a 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -413,7 +413,14 @@ def _guess_method(self): """ if self.args.method is None: # Invoked as `http URL'. - assert not self.args.request_items + if self.args.request_items: + self.error( + 'no HTTP method or URL detected but request items found. ' + 'Please make sure that the URL comes right after ' + 'the optional METHOD:\n' + ' http [METHOD] URL [REQUEST_ITEM ...]\n' + 'See https://httpie.io/docs/cli for more information.' + ) if self.has_input_data: self.args.method = HTTP_POST else: diff --git a/tests/test_cli.py b/tests/test_cli.py index 2cd27574af..31eb8f6622 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -316,6 +316,31 @@ def test_guess_when_method_set_but_invalid_and_item_exists(self): key='old_item', value='b', sep='=', orig='old_item=b'), ] + def test_guess_when_method_not_set_but_request_items_present(self): + """When method is None but request_items exist, _guess_method + should produce a user-friendly error instead of an AssertionError. + + This state can occur when argparse misassigns positional arguments + due to intermixed optional and positional arguments (e.g., + ``http POST --auth-type bearer --auth token URL``). + See https://github.com/httpie/cli/issues/1614 + """ + self.parser.args = argparse.Namespace() + self.parser.args.method = None + self.parser.args.url = 'http://example.com/' + self.parser.args.request_items = [ + KeyValueArg( + key='test', value='header', sep=':', orig='test:header') + ] + self.parser.args.ignore_stdin = False + self.parser.env = MockEnvironment() + # Patch print_usage since the parser isn't fully initialized in + # unit tests (no spec attribute). + self.parser.print_usage = lambda *a, **kw: None + with pytest.raises(SystemExit) as exc_info: + self.parser._guess_method() + assert exc_info.value.code == 2 + class TestNoOptions: diff --git a/tests/test_cli_ui.py b/tests/test_cli_ui.py index bb744cdc4e..ca8484c2ea 100644 --- a/tests/test_cli_ui.py +++ b/tests/test_cli_ui.py @@ -1,8 +1,36 @@ +import argparse +import io +import sys + import pytest import shutil import os from tests.utils import http + +# Detect whether argparse quotes choices in error messages. +# Python 3.12.4+ (and 3.13+) removed the quotes. +def _argparse_quotes_choices(): + p = argparse.ArgumentParser(prog='_probe') + p.add_argument('--c', choices=['x']) + buf = io.StringIO() + saved = sys.stderr + sys.stderr = buf + try: + p.parse_args(['--c', 'z']) + except SystemExit: + pass + finally: + sys.stderr = saved + return "'x'" in buf.getvalue() + + +def _fmt_argparse_choice(choice): + return f"'{choice}'" if _ARGPARSE_QUOTES else choice + + +_ARGPARSE_QUOTES = _argparse_quotes_choices() + NAKED_BASE_TEMPLATE = """\ usage: http {extra_args}[METHOD] URL [REQUEST_ITEM ...] @@ -27,7 +55,11 @@ NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG = NAKED_BASE_TEMPLATE.format( extra_args="--pretty {all, colors, format, none} ", - error_msg="argument --pretty: invalid choice: '$invalid' (choose from 'all', 'colors', 'format', 'none')" + error_msg=( + f"argument --pretty: invalid choice: '$invalid' " + f"(choose from {_fmt_argparse_choice('all')}, {_fmt_argparse_choice('colors')}, " + f"{_fmt_argparse_choice('format')}, {_fmt_argparse_choice('none')})" + ) ) diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 62814161ed..e4de8e50b1 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -14,7 +14,9 @@ CHARSET_TEXT_PAIRS = [ - ('big5', '卷首卷首卷首卷首卷卷首卷首卷首卷首卷首卷首卷首卷首卷首卷首卷首卷首卷首'), + # Use varied Traditional Chinese text so charset_normalizer can + # reliably distinguish big5 from other CJK encodings (e.g. johab). + ('big5', '天地玄黃宇宙洪荒日月盈昃辰宿列張寒來暑往秋收冬藏閏餘成歲律呂調陽雲騰致雨露結為霜'), ('windows-1250', 'Všichni lidé jsou si rovni. Všichni lidé jsou si rovni.'), (UTF8, 'Všichni lidé jsou si rovni. Všichni lidé jsou si rovni.'), ]