You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 22:42:05 +02:00
[Major] UI Enhancements (#1321)
* Refactor tests to use a text-based standard output. (#1318) * Implement new style `--help` (#1316) * Implement man page generation (#1317) * Implement rich progress bars. (#1324) * Man page deployment & isolation. (#1325) * Remove all unsorted usages in the CLI docs * Implement isolated mode for man page generation * Add a CI job for autogenerated files * Distribute man pages through PyPI * Pin the date for man pages. (#1326) * Hide suppressed arguments from --help/man pages (#1329) * Change download spinner to line (#1328) * Regenerate autogenerated files when pushed against to master. (#1339) * Highlight options (#1340) * Additional man page enhancements (#1341) * Group options by the parent category & highlight -o/--o * Display (and underline) the METAVAR on man pages. * Make help message processing more robust (#1342) * Inherit `help` from `short_help` * Don't mirror short_help directly. * Fixup the serialization * Use `pager` and `man` on `--manual` when applicable (#1343) * Run `man $program` on --manual * Page the output of `--manual` for systems that lack man pages * Improvements over progress bars (separate bar, status line, etc.) (#1346) * Redesign the --help layout. * Make our usage of rich compatible with 9.10.0 * Add `HTTPIE_NO_MAN_PAGES` * Make tests also patch os.get_terminal_size * Generate CLI spec from HTTPie & Man Page Hook (#1354) * Generate CLI spec from HTTPie & add man page hook * Use the full command space for the option headers
This commit is contained in:
@@ -155,6 +155,7 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.env.args = namespace = namespace or argparse.Namespace()
|
||||
self.args, no_options = super().parse_known_args(args, namespace)
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
@@ -557,19 +558,62 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||
self.args.format_options = parsed_options
|
||||
|
||||
def print_manual(self):
|
||||
from httpie.output.ui import man_pages
|
||||
|
||||
if man_pages.is_available(self.env.program_name):
|
||||
man_pages.display_for(self.env, self.env.program_name)
|
||||
return None
|
||||
|
||||
text = self.format_help()
|
||||
with self.env.rich_console.pager():
|
||||
self.env.rich_console.print(
|
||||
text,
|
||||
highlight=False
|
||||
)
|
||||
|
||||
def print_help(self):
|
||||
from httpie.output.ui import rich_help
|
||||
|
||||
for renderable in rich_help.to_help_message(self.spec):
|
||||
self.env.rich_console.print(renderable)
|
||||
|
||||
def print_usage(self, file):
|
||||
from rich.text import Text
|
||||
from httpie.output.ui import rich_help
|
||||
|
||||
whitelist = set()
|
||||
_, exception, _ = sys.exc_info()
|
||||
if (
|
||||
isinstance(exception, argparse.ArgumentError)
|
||||
and len(exception.args) >= 1
|
||||
and isinstance(exception.args[0], argparse.Action)
|
||||
and exception.args[0].option_strings
|
||||
):
|
||||
# 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.
|
||||
whitelist.add(exception.args[0].option_strings[0])
|
||||
|
||||
usage_text = Text('usage', style='bold')
|
||||
usage_text.append(':\n ')
|
||||
usage_text.append(rich_help.to_usage(self.spec, whitelist=whitelist))
|
||||
self.env.rich_error_console.print(usage_text)
|
||||
|
||||
def error(self, message):
|
||||
"""Prints a usage message incorporating the message to stderr and
|
||||
exits."""
|
||||
self.print_usage(sys.stderr)
|
||||
self.exit(
|
||||
2,
|
||||
self.env.rich_error_console.print(
|
||||
dedent(
|
||||
f'''
|
||||
error:
|
||||
[bold]error[/bold]:
|
||||
{message}
|
||||
|
||||
for more information:
|
||||
[bold]for more information[/bold]:
|
||||
run '{self.prog} --help' or visit https://httpie.io/docs/cli
|
||||
'''
|
||||
'''.rstrip()
|
||||
)
|
||||
)
|
||||
self.exit(2)
|
||||
|
@@ -16,19 +16,23 @@ from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS,
|
||||
SORTED_FORMAT_OPTIONS_STRING,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
|
||||
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
|
||||
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE,
|
||||
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE, BUNDLED_STYLES,
|
||||
get_available_styles)
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
|
||||
options = ParserSpec(
|
||||
'http',
|
||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||
epilog="""
|
||||
To learn more, you can try:
|
||||
-> running 'http --manual'
|
||||
-> visiting our full documentation at https://httpie.io/docs/cli
|
||||
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
https://github.com/httpie/httpie/issues
|
||||
""",
|
||||
@@ -52,6 +56,7 @@ positional_arguments.add_argument(
|
||||
metavar='METHOD',
|
||||
nargs=Qualifiers.OPTIONAL,
|
||||
default=None,
|
||||
short_help='The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).',
|
||||
help="""
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
@@ -66,9 +71,10 @@ positional_arguments.add_argument(
|
||||
positional_arguments.add_argument(
|
||||
dest='url',
|
||||
metavar='URL',
|
||||
short_help='The request URL.',
|
||||
help="""
|
||||
The scheme defaults to 'http://' if the URL does not include one.
|
||||
(You can override this with: --default-scheme=https)
|
||||
The request URL. Scheme defaults to 'http://' if the URL
|
||||
does not include one. (You can override this with: --default-scheme=http/https)
|
||||
|
||||
You can also use a shorthand for localhost
|
||||
|
||||
@@ -83,6 +89,17 @@ positional_arguments.add_argument(
|
||||
nargs=Qualifiers.ZERO_OR_MORE,
|
||||
default=None,
|
||||
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
||||
short_help=(
|
||||
'HTTPie’s request items syntax for specifying HTTP headers, JSON/Form'
|
||||
'data, files, and URL parameters.'
|
||||
),
|
||||
nested_options=[
|
||||
('HTTP Headers', 'Name:Value', 'Arbitrary HTTP header, e.g X-API-Token:123'),
|
||||
('URL Parameters', 'name==value', 'Querystring parameter to the URL, e.g limit==50'),
|
||||
('Data Fields', 'field=value', 'Data fields to be serialized as JSON (default) or Form Data (with --form)'),
|
||||
('Raw JSON Fields', 'field:=json', 'Data field for real JSON types.'),
|
||||
('File upload Fields', 'field@/dir/file', 'Path field for uploading a file.'),
|
||||
],
|
||||
help=r"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
@@ -136,6 +153,7 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.JSON,
|
||||
dest='request_type',
|
||||
short_help='(default) Serialize data items from the command line as a JSON object.',
|
||||
help="""
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
@@ -149,6 +167,7 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.FORM,
|
||||
dest='request_type',
|
||||
short_help='Serialize data items from the command line as form field data.',
|
||||
help="""
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
@@ -163,22 +182,21 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.MULTIPART,
|
||||
dest='request_type',
|
||||
help="""
|
||||
Similar to --form, but always sends a multipart/form-data
|
||||
request (i.e., even without files).
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Similar to --form, but always sends a multipart/form-data '
|
||||
'request (i.e., even without files).'
|
||||
)
|
||||
)
|
||||
content_types.add_argument(
|
||||
'--boundary',
|
||||
help="""
|
||||
Specify a custom boundary string for multipart/form-data requests.
|
||||
Only has effect only together with --form.
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Specify a custom boundary string for multipart/form-data requests. '
|
||||
'Only has effect only together with --form.'
|
||||
)
|
||||
)
|
||||
content_types.add_argument(
|
||||
'--raw',
|
||||
short_help='Pass raw request data without extra processing.',
|
||||
help="""
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
@@ -208,6 +226,7 @@ processing_options.add_argument(
|
||||
'-x',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Compress the content with Deflate algorithm.',
|
||||
help="""
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
@@ -223,22 +242,33 @@ processing_options.add_argument(
|
||||
#######################################################################
|
||||
|
||||
|
||||
def format_style_help(available_styles):
|
||||
return """
|
||||
def format_style_help(available_styles, *, isolation_mode: bool = False):
|
||||
text = """
|
||||
Output coloring style (default is "{default}"). It can be one of:
|
||||
|
||||
{available_styles}
|
||||
"""
|
||||
if isolation_mode:
|
||||
text += '\n\n'
|
||||
text += 'For finding out all available styles in your system, try:\n\n'
|
||||
text += ' $ http --style\n'
|
||||
text += textwrap.dedent("""
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
""")
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
""".format(
|
||||
if isolation_mode:
|
||||
available_styles = sorted(BUNDLED_STYLES)
|
||||
|
||||
available_styles_text = '\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in textwrap.wrap(', '.join(available_styles), 60)
|
||||
).strip()
|
||||
return text.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in textwrap.wrap(', '.join(available_styles), 60)
|
||||
).strip(),
|
||||
available_styles=available_styles_text,
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
|
||||
@@ -261,6 +291,7 @@ output_processing.add_argument(
|
||||
dest='prettify',
|
||||
default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
short_help='Control the processing of console outputs.',
|
||||
help="""
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
@@ -276,6 +307,7 @@ output_processing.add_argument(
|
||||
default=DEFAULT_STYLE,
|
||||
action='lazy_choices',
|
||||
getter=get_available_styles,
|
||||
short_help=f'Output coloring style (default is "{DEFAULT_STYLE}").',
|
||||
help_formatter=format_style_help,
|
||||
)
|
||||
|
||||
@@ -291,6 +323,7 @@ output_processing.add_argument(
|
||||
output_processing.add_argument(
|
||||
'--unsorted',
|
||||
**_unsorted_kwargs,
|
||||
short_help='Disables all sorting while formatting output.',
|
||||
help=f"""
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
@@ -301,6 +334,7 @@ output_processing.add_argument(
|
||||
output_processing.add_argument(
|
||||
'--sorted',
|
||||
**_sorted_kwargs,
|
||||
short_help='Re-enables all sorting options while formatting output.',
|
||||
help=f"""
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
@@ -312,6 +346,7 @@ output_processing.add_argument(
|
||||
'--response-charset',
|
||||
metavar='ENCODING',
|
||||
type=response_charset_type,
|
||||
short_help='Override the response encoding for terminal display purposes.',
|
||||
help="""
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
@@ -324,6 +359,7 @@ output_processing.add_argument(
|
||||
'--response-mime',
|
||||
metavar='MIME_TYPE',
|
||||
type=response_mime_type,
|
||||
short_help='Override the response mime type for coloring and formatting for the terminal.',
|
||||
help="""
|
||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||
|
||||
@@ -335,6 +371,7 @@ output_processing.add_argument(
|
||||
output_processing.add_argument(
|
||||
'--format-options',
|
||||
action='append',
|
||||
short_help='Controls output formatting.',
|
||||
help="""
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) --pretty=all or --pretty=format.
|
||||
@@ -368,6 +405,7 @@ output_options.add_argument(
|
||||
'-p',
|
||||
dest='output_options',
|
||||
metavar='WHAT',
|
||||
short_help='Options to specify what the console output should contain.',
|
||||
help=f"""
|
||||
String specifying what the output should contain:
|
||||
|
||||
@@ -390,6 +428,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_HEAD,
|
||||
short_help='Print only the response headers.',
|
||||
help=f"""
|
||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||
|
||||
@@ -401,6 +440,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_META,
|
||||
short_help='Print only the response metadata.',
|
||||
help=f"""
|
||||
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
||||
|
||||
@@ -412,6 +452,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_BODY,
|
||||
short_help='Print only the response body.',
|
||||
help=f"""
|
||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||
|
||||
@@ -424,20 +465,22 @@ output_options.add_argument(
|
||||
dest='verbose',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Make output more verbose.',
|
||||
help=f"""
|
||||
Verbose output. For the level one (with single `-v`/`--verbose`), print
|
||||
the whole request as well as the response. Also print any intermediary
|
||||
requests/responses (such as redirects). For the second level and higher,
|
||||
print these as well as the response metadata.
|
||||
|
||||
Level one is a shortcut for: --all --print={''.join(BASE_OUTPUT_OPTIONS)}
|
||||
Level two is a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
||||
Level one is a shortcut for: --all --print={''.join(sorted(BASE_OUTPUT_OPTIONS))}
|
||||
Level two is a shortcut for: --all --print={''.join(sorted(OUTPUT_OPTIONS))}
|
||||
""",
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
short_help='Show any intermediary requests/responses.',
|
||||
help="""
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
@@ -451,6 +494,7 @@ output_options.add_argument(
|
||||
'-P',
|
||||
dest='output_options_history',
|
||||
metavar='WHAT',
|
||||
short_help='--print for intermediary requests/responses.',
|
||||
help="""
|
||||
The same as --print, -p but applies only to intermediary requests/responses
|
||||
(such as redirects) when their inclusion is enabled with --all. If this
|
||||
@@ -464,6 +508,7 @@ output_options.add_argument(
|
||||
'-S',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Always stream the response body by line, i.e., behave like `tail -f`.',
|
||||
help="""
|
||||
Always stream the response body by line, i.e., behave like `tail -f'.
|
||||
|
||||
@@ -484,6 +529,7 @@ output_options.add_argument(
|
||||
type=FileType('a+b'),
|
||||
dest='output_file',
|
||||
metavar='FILE',
|
||||
short_help='Save output to FILE instead of stdout.',
|
||||
help="""
|
||||
Save output to FILE instead of stdout. If --download is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
@@ -497,6 +543,7 @@ output_options.add_argument(
|
||||
'-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Download the body to a file instead of printing it to stdout.',
|
||||
help="""
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with --output
|
||||
@@ -510,6 +557,7 @@ output_options.add_argument(
|
||||
dest='download_resume',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Resume an interrupted download (--output needs to be specified).',
|
||||
help="""
|
||||
Resume an interrupted download. Note that the --output option needs to be
|
||||
specified as well.
|
||||
@@ -521,6 +569,7 @@ output_options.add_argument(
|
||||
'-q',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Do not print to stdout or stderr, except for errors and warnings when provided once.',
|
||||
help="""
|
||||
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||
Provide twice to suppress warnings as well.
|
||||
@@ -544,21 +593,26 @@ sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=f"""
|
||||
short_help='Create, or reuse and update a session.',
|
||||
help="""
|
||||
Create, or reuse and update a session. Within a session, custom headers,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
Session files are stored in:
|
||||
|
||||
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
|
||||
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||
|
||||
https://httpie.io/docs/cli/config-file-directory
|
||||
""",
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
short_help='Create or read a session without updating it',
|
||||
help="""
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
@@ -571,33 +625,46 @@ sessions.add_argument(
|
||||
#######################################################################
|
||||
|
||||
|
||||
def format_auth_help(auth_plugins_mapping):
|
||||
auth_plugins = list(auth_plugins_mapping.values())
|
||||
return """
|
||||
def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
|
||||
text = """
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
{auth_types}
|
||||
"""
|
||||
|
||||
""".format(
|
||||
auth_plugins = list(auth_plugins_mapping.values())
|
||||
if isolation_mode:
|
||||
auth_plugins = [
|
||||
auth_plugin
|
||||
for auth_plugin in auth_plugins
|
||||
if issubclass(auth_plugin, BuiltinAuthPlugin)
|
||||
]
|
||||
text += '\n'
|
||||
text += 'For finding out all available authentication types in your system, try:\n\n'
|
||||
text += ' $ http --auth-type'
|
||||
|
||||
auth_types = '\n\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
''
|
||||
if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else f' (provided by {plugin.package_name})'
|
||||
),
|
||||
description=(
|
||||
''
|
||||
if not plugin.description
|
||||
else '\n '
|
||||
+ ('\n '.join(textwrap.wrap(plugin.description)))
|
||||
),
|
||||
)
|
||||
for plugin in auth_plugins
|
||||
)
|
||||
|
||||
return text.format(
|
||||
default=auth_plugins[0].auth_type,
|
||||
types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
''
|
||||
if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else f' (provided by {plugin.package_name})'
|
||||
),
|
||||
description=(
|
||||
''
|
||||
if not plugin.description
|
||||
else '\n '
|
||||
+ ('\n '.join(textwrap.wrap(plugin.description)))
|
||||
),
|
||||
)
|
||||
for plugin in auth_plugins
|
||||
),
|
||||
auth_types=auth_types,
|
||||
)
|
||||
|
||||
|
||||
@@ -608,6 +675,7 @@ authentication.add_argument(
|
||||
'-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS] | TOKEN',
|
||||
short_help='Credentials for the selected (-A) authentication method.',
|
||||
help="""
|
||||
For username/password based authentication mechanisms (e.g
|
||||
basic auth or digest auth) if only the username is provided
|
||||
@@ -623,16 +691,14 @@ authentication.add_argument(
|
||||
getter=plugin_manager.get_auth_plugin_mapping,
|
||||
sort=True,
|
||||
cache=False,
|
||||
short_help='The authentication mechanism to be used.',
|
||||
help_formatter=format_auth_help,
|
||||
)
|
||||
authentication.add_argument(
|
||||
'--ignore-netrc',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
""",
|
||||
short_help='Ignore credentials from .netrc.'
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@@ -645,9 +711,7 @@ network.add_argument(
|
||||
'--offline',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Build the request and print it but don’t actually send it.
|
||||
""",
|
||||
short_help='Build the request and print it but don’t actually send it.'
|
||||
)
|
||||
network.add_argument(
|
||||
'--proxy',
|
||||
@@ -655,6 +719,7 @@ network.add_argument(
|
||||
action='append',
|
||||
metavar='PROTOCOL:PROXY_URL',
|
||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||
short_help='String mapping of protocol to the URL of the proxy.',
|
||||
help="""
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
@@ -668,16 +733,14 @@ network.add_argument(
|
||||
'-F',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Follow 30x Location redirects.
|
||||
|
||||
""",
|
||||
short_help='Follow 30x Location redirects.'
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-redirects',
|
||||
type=int,
|
||||
default=30,
|
||||
short_help='The maximum number of redirects that should be followed (with --follow).',
|
||||
help="""
|
||||
By default, requests have a limit of 30 redirects (works with --follow).
|
||||
|
||||
@@ -687,11 +750,10 @@ network.add_argument(
|
||||
'--max-headers',
|
||||
type=int,
|
||||
default=0,
|
||||
help="""
|
||||
The maximum number of response headers to be read before giving up
|
||||
(default 0, i.e., no limit).
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'The maximum number of response headers to be read before '
|
||||
'giving up (default 0, i.e., no limit).'
|
||||
)
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
@@ -699,6 +761,7 @@ network.add_argument(
|
||||
type=float,
|
||||
default=0,
|
||||
metavar='SECONDS',
|
||||
short_help='The connection timeout of the request in seconds.',
|
||||
help="""
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
@@ -713,6 +776,7 @@ network.add_argument(
|
||||
'--check-status',
|
||||
default=False,
|
||||
action='store_true',
|
||||
short_help='Exit with an error status code if the server replies with an error.',
|
||||
help="""
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
@@ -729,20 +793,16 @@ network.add_argument(
|
||||
'--path-as-is',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
""",
|
||||
short_help='Bypass dot segment (/../ or /./) URL squashing.'
|
||||
)
|
||||
network.add_argument(
|
||||
'--chunked',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Enable streaming via chunked transfer encoding.
|
||||
The Transfer-Encoding header is set to chunked.
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Enable streaming via chunked transfer encoding. '
|
||||
'The Transfer-Encoding header is set to chunked.'
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@@ -754,6 +814,7 @@ ssl = options.add_group('SSL')
|
||||
ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
short_help='If "no", skip SSL verification. If a file path, use it as a CA bundle.',
|
||||
help="""
|
||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
@@ -765,6 +826,7 @@ ssl.add_argument(
|
||||
'--ssl',
|
||||
dest='ssl_version',
|
||||
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
|
||||
short_help='The desired protocol version to used.',
|
||||
help="""
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
@@ -776,6 +838,7 @@ ssl.add_argument(
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ciphers',
|
||||
short_help='A string in the OpenSSL cipher list format.',
|
||||
help=f"""
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
@@ -789,6 +852,7 @@ ssl.add_argument(
|
||||
'--cert',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
short_help='Specifys a local cert to use as client side SSL certificate.',
|
||||
help="""
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
@@ -800,6 +864,7 @@ ssl.add_argument(
|
||||
'--cert-key',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
short_help='The private key to use with SSL. Only needed if --cert is given.',
|
||||
help="""
|
||||
The private key to use with SSL. Only needed if --cert is given and the
|
||||
certificate file does not contain the private key.
|
||||
@@ -811,11 +876,12 @@ ssl.add_argument(
|
||||
'--cert-key-pass',
|
||||
default=None,
|
||||
type=SSLCredentials,
|
||||
help='''
|
||||
short_help='The passphrase to be used to with the given private key.',
|
||||
help="""
|
||||
The passphrase to be used to with the given private key. Only needed if --cert-key
|
||||
is given and the key file requires a passphrase.
|
||||
If not provided, you’ll be prompted interactively.
|
||||
'''
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@@ -828,50 +894,42 @@ troubleshooting.add_argument(
|
||||
'-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Do not attempt to read stdin.
|
||||
|
||||
""",
|
||||
short_help='Do not attempt to read stdin'
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help',
|
||||
default=Qualifiers.SUPPRESS,
|
||||
help="""
|
||||
Show this help message and exit.
|
||||
|
||||
""",
|
||||
short_help='Show this help message and exit.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--manual',
|
||||
action='manual',
|
||||
default=Qualifiers.SUPPRESS,
|
||||
short_help='Show the full manual.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help="""
|
||||
Show version and exit.
|
||||
|
||||
""",
|
||||
short_help='Show version and exit.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
""",
|
||||
short_help='Prints the exception traceback should one occur.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--default-scheme',
|
||||
default='http',
|
||||
help="""
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
""",
|
||||
short_help='The default scheme to use if not specified in the URL.'
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Print useful diagnostic information for bug reports.',
|
||||
help="""
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
@@ -3,15 +3,16 @@ import textwrap
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Optional, Dict, List, Type, TypeVar
|
||||
from typing import Any, Optional, Dict, List, Tuple, Type, TypeVar
|
||||
|
||||
from httpie.cli.argparser import HTTPieArgumentParser
|
||||
from httpie.cli.utils import LazyChoices
|
||||
from httpie.cli.utils import Manual, LazyChoices
|
||||
|
||||
|
||||
class Qualifiers(Enum):
|
||||
OPTIONAL = auto()
|
||||
ZERO_OR_MORE = auto()
|
||||
ONE_OR_MORE = auto()
|
||||
SUPPRESS = auto()
|
||||
|
||||
|
||||
@@ -24,6 +25,27 @@ def map_qualifiers(
|
||||
}
|
||||
|
||||
|
||||
def drop_keys(
|
||||
configuration: Dict[str, Any], key_blacklist: Tuple[str, ...]
|
||||
):
|
||||
return {
|
||||
key: value
|
||||
for key, value in configuration.items()
|
||||
if key not in key_blacklist
|
||||
}
|
||||
|
||||
|
||||
def _get_first_line(source: str) -> str:
|
||||
parts = []
|
||||
for line in source.strip().splitlines():
|
||||
line = line.strip()
|
||||
parts.append(line)
|
||||
if line.endswith("."):
|
||||
break
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
PARSER_SPEC_VERSION = '0.0.1a0'
|
||||
|
||||
|
||||
@@ -69,6 +91,7 @@ class Group:
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
argument = Argument(list(args), kwargs.copy())
|
||||
argument.post_init()
|
||||
self.arguments.append(argument)
|
||||
return argument
|
||||
|
||||
@@ -85,14 +108,32 @@ class Argument(typing.NamedTuple):
|
||||
aliases: List[str]
|
||||
configuration: Dict[str, Any]
|
||||
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
def post_init(self):
|
||||
"""Run a bunch of post-init hooks."""
|
||||
# If there is a short help, then create the longer version from it.
|
||||
short_help = self.configuration.get('short_help')
|
||||
if (
|
||||
short_help
|
||||
and 'help' not in self.configuration
|
||||
and self.configuration.get('action') != 'lazy_choices'
|
||||
):
|
||||
self.configuration['help'] = f'\n{short_help}\n\n'
|
||||
|
||||
def serialize(self, *, isolation_mode: bool = False) -> Dict[str, Any]:
|
||||
configuration = self.configuration.copy()
|
||||
|
||||
# Unpack the dynamically computed choices, since we
|
||||
# will need to store the actual values somewhere.
|
||||
action = configuration.pop('action', None)
|
||||
short_help = configuration.pop('short_help', None)
|
||||
nested_options = configuration.pop('nested_options', None)
|
||||
|
||||
if action == 'lazy_choices':
|
||||
choices = LazyChoices(self.aliases, **{'dest': None, **configuration})
|
||||
choices = LazyChoices(
|
||||
self.aliases,
|
||||
**{'dest': None, **configuration},
|
||||
isolation_mode=isolation_mode
|
||||
)
|
||||
configuration['choices'] = list(choices.load())
|
||||
configuration['help'] = choices.help
|
||||
|
||||
@@ -106,9 +147,13 @@ class Argument(typing.NamedTuple):
|
||||
qualifiers = JSON_QUALIFIER_TO_OPTIONS[configuration.get('nargs', Qualifiers.SUPPRESS)]
|
||||
result.update(qualifiers)
|
||||
|
||||
help_msg = configuration.get('help')
|
||||
if help_msg and help_msg is not Qualifiers.SUPPRESS:
|
||||
result['description'] = help_msg.strip()
|
||||
description = configuration.get('help')
|
||||
if description and description is not Qualifiers.SUPPRESS:
|
||||
result['short_description'] = short_help
|
||||
result['description'] = description
|
||||
|
||||
if nested_options:
|
||||
result['nested_options'] = nested_options
|
||||
|
||||
python_type = configuration.get('type')
|
||||
if python_type is not None:
|
||||
@@ -123,10 +168,19 @@ class Argument(typing.NamedTuple):
|
||||
key: value
|
||||
for key, value in configuration.items()
|
||||
if key in JSON_DIRECT_MIRROR_OPTIONS
|
||||
if value is not Qualifiers.SUPPRESS
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def is_positional(self):
|
||||
return len(self.aliases) == 0
|
||||
|
||||
@property
|
||||
def is_hidden(self):
|
||||
return self.configuration.get('help') is Qualifiers.SUPPRESS
|
||||
|
||||
def __getattr__(self, attribute_name):
|
||||
if attribute_name in self.configuration:
|
||||
return self.configuration[attribute_name]
|
||||
@@ -140,7 +194,9 @@ ARGPARSE_QUALIFIER_MAP = {
|
||||
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
||||
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
||||
Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE,
|
||||
Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE
|
||||
}
|
||||
ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options')
|
||||
|
||||
|
||||
def to_argparse(
|
||||
@@ -152,7 +208,9 @@ def to_argparse(
|
||||
description=abstract_options.description,
|
||||
epilog=abstract_options.epilog,
|
||||
)
|
||||
concrete_parser.spec = abstract_options
|
||||
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
||||
concrete_parser.register('action', 'manual', Manual)
|
||||
|
||||
for abstract_group in abstract_options.groups:
|
||||
concrete_group = concrete_parser.add_argument_group(
|
||||
@@ -164,9 +222,9 @@ def to_argparse(
|
||||
for abstract_argument in abstract_group.arguments:
|
||||
concrete_group.add_argument(
|
||||
*abstract_argument.aliases,
|
||||
**map_qualifiers(
|
||||
**drop_keys(map_qualifiers(
|
||||
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
||||
)
|
||||
), ARGPARSE_IGNORE_KEYS)
|
||||
)
|
||||
|
||||
return concrete_parser
|
||||
@@ -181,9 +239,19 @@ JSON_DIRECT_MIRROR_OPTIONS = (
|
||||
JSON_QUALIFIER_TO_OPTIONS = {
|
||||
Qualifiers.OPTIONAL: {'is_optional': True},
|
||||
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
||||
Qualifiers.ONE_OR_MORE: {'is_optional': False, 'is_variadic': True},
|
||||
Qualifiers.SUPPRESS: {}
|
||||
}
|
||||
|
||||
|
||||
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
||||
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
|
||||
|
||||
|
||||
def parser_to_parser_spec(parser: argparse.ArgumentParser) -> ParserSpec:
|
||||
"""Take an existing argparse parser, and create a spec from it."""
|
||||
return ParserSpec(
|
||||
program=parser.prog,
|
||||
description=parser.description,
|
||||
epilog=parser.epilog
|
||||
)
|
||||
|
@@ -4,20 +4,43 @@ from typing import Any, Callable, Generic, Iterator, Iterable, Optional, TypeVar
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class Manual(argparse.Action):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings,
|
||||
dest=argparse.SUPPRESS,
|
||||
default=argparse.SUPPRESS,
|
||||
help=None
|
||||
):
|
||||
super().__init__(
|
||||
option_strings=option_strings,
|
||||
dest=dest,
|
||||
default=default,
|
||||
nargs=0,
|
||||
help=help
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
parser.print_manual()
|
||||
parser.exit()
|
||||
|
||||
|
||||
class LazyChoices(argparse.Action, Generic[T]):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
getter: Callable[[], Iterable[T]],
|
||||
help_formatter: Optional[Callable[[T], str]] = None,
|
||||
help_formatter: Optional[Callable[[T, bool], str]] = None,
|
||||
sort: bool = False,
|
||||
cache: bool = True,
|
||||
isolation_mode: bool = False,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.getter = getter
|
||||
self.help_formatter = help_formatter
|
||||
self.sort = sort
|
||||
self.cache = cache
|
||||
self.isolation_mode = isolation_mode
|
||||
self._help: Optional[str] = None
|
||||
self._obj: Optional[Iterable[T]] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -33,7 +56,10 @@ class LazyChoices(argparse.Action, Generic[T]):
|
||||
@property
|
||||
def help(self) -> str:
|
||||
if self._help is None and self.help_formatter is not None:
|
||||
self._help = self.help_formatter(self.load())
|
||||
self._help = self.help_formatter(
|
||||
self.load(),
|
||||
isolation_mode=self.isolation_mode
|
||||
)
|
||||
return self._help
|
||||
|
||||
@help.setter
|
||||
|
Reference in New Issue
Block a user