From 77af4c7a5cb3d0b3e813c7f678099014c94ff6aa Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 8 Mar 2022 01:34:04 +0300 Subject: [PATCH] Decouple parser definition from argparse (#1293) --- docs/README.md | 21 ++ httpie/cli/constants.py | 8 +- httpie/cli/definition.py | 531 ++++++++++++++++++------------------ httpie/cli/options.py | 189 +++++++++++++ httpie/core.py | 29 +- httpie/manager/cli.py | 8 + httpie/manager/tasks.py | 25 ++ httpie/output/models.py | 44 +++ httpie/output/streams.py | 4 +- httpie/output/writer.py | 70 +++-- tests/test_httpie_cli.py | 13 + tests/test_parser_schema.py | 60 ++++ 12 files changed, 708 insertions(+), 294 deletions(-) create mode 100644 httpie/cli/options.py create mode 100644 httpie/output/models.py create mode 100644 tests/test_parser_schema.py diff --git a/docs/README.md b/docs/README.md index 81b0aa76..1c171bd8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2402,6 +2402,27 @@ For managing these plugins; starting with 3.0, we are offering a new plugin mana This command is currently in beta. +### `httpie cli` + +#### `httpie cli export-args` + +`httpie cli export-args` command can expose the parser specification of `http`/`https` commands +(like an API definition) to outside tools so that they can use this to build better interactions +over them (e.g offer auto-complete). + + +Available formats to export in include: +| format | Description | +|--------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `json` | Export the parser spec in JSON. The schema includes a top-level `version` parameter which should be interpreted in [semver](https://semver.org/). | + +You can use any of these formats with `--format` parameter, but the default one is `json`. + +```bash +$ httpie cli export-args | jq '"Program: " + .spec.name + ", Version: " + .version' +"Program: http, Version: 0.0.1a0" +``` + ### `httpie plugins` `plugins` interface is a very simple plugin manager for installing, listing and uninstalling HTTPie plugins. diff --git a/httpie/cli/constants.py b/httpie/cli/constants.py index 067aaabd..e8188938 100644 --- a/httpie/cli/constants.py +++ b/httpie/cli/constants.py @@ -90,13 +90,19 @@ OUTPUT_OPTIONS = frozenset({ }) # Pretty + + +class PrettyOptions(enum.Enum): + STDOUT_TTY_ONLY = enum.auto() + + PRETTY_MAP = { 'all': ['format', 'colors'], 'colors': ['colors'], 'format': ['format'], 'none': [] } -PRETTY_STDOUT_TTY_ONLY = object() +PRETTY_STDOUT_TTY_ONLY = PrettyOptions.STDOUT_TTY_ONLY DEFAULT_FORMAT_OPTIONS = [ diff --git a/httpie/cli/definition.py b/httpie/cli/definition.py index 806b04bc..5db5b390 100644 --- a/httpie/cli/definition.py +++ b/httpie/cli/definition.py @@ -1,67 +1,58 @@ -""" -CLI arguments definition. +from __future__ import annotations -""" -from argparse import (FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE) -from textwrap import dedent, wrap +import textwrap +from argparse import FileType -from .. import __doc__, __version__ -from .argparser import HTTPieArgumentParser -from .argtypes import ( - KeyValueArgType, SessionNameValidator, SSLCredentials, - readable_file_arg, response_charset_type, response_mime_type, -) -from .constants import ( - DEFAULT_FORMAT_OPTIONS, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS, - OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD, - OUT_RESP_BODY, OUT_RESP_HEAD, OUT_RESP_META, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, - RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, - SORTED_FORMAT_OPTIONS_STRING, - UNSORTED_FORMAT_OPTIONS_STRING, -) -from .utils import LazyChoices -from ..output.formatters.colors import ( - AUTO_STYLE, DEFAULT_STYLE, get_available_styles -) -from ..plugins.builtin import BuiltinAuthPlugin -from ..plugins.registry import plugin_manager -from ..sessions import DEFAULT_SESSIONS_DIR -from ..ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS +from httpie import __doc__, __version__ +from httpie.cli.argtypes import (KeyValueArgType, SessionNameValidator, + SSLCredentials, readable_file_arg, + response_charset_type, response_mime_type) +from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS, + OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, + OUT_RESP_HEAD, OUT_RESP_META, OUTPUT_OPTIONS, + OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, + PRETTY_STDOUT_TTY_ONLY, + SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, + 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, + 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 - -parser = HTTPieArgumentParser( - prog='http', +options = ParserSpec( + 'http', description=f'{__doc__.strip()} ', - epilog=dedent(''' + epilog=""" 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 - - '''), + """, ) -parser.register('action', 'lazy_choices', LazyChoices) + ####################################################################### # Positional arguments. ####################################################################### -positional = parser.add_argument_group( - title='Positional Arguments', - description=dedent(''' +positional_arguments = options.add_group( + 'Positional Arguments', + description=""" These arguments come after any flags and in the order they are listed here. Only URL is required. - - ''') + """, ) -positional.add_argument( + +positional_arguments.add_argument( dest='method', metavar='METHOD', - nargs=OPTIONAL, + nargs=Qualifiers.OPTIONAL, default=None, - help=''' + help=""" The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...). This argument can be omitted in which case HTTPie will use POST if there @@ -70,12 +61,12 @@ positional.add_argument( $ http example.org # => GET $ http example.org hello=world # => POST - ''' + """, ) -positional.add_argument( +positional_arguments.add_argument( dest='url', metavar='URL', - help=''' + help=""" The scheme defaults to 'http://' if the URL does not include one. (You can override this with: --default-scheme=https) @@ -84,15 +75,15 @@ positional.add_argument( $ http :3000 # => http://localhost:3000 $ http :/foo # => http://localhost/foo - ''' + """, ) -positional.add_argument( +positional_arguments.add_argument( dest='request_items', metavar='REQUEST_ITEM', - nargs=ZERO_OR_MORE, + nargs=Qualifiers.ZERO_OR_MORE, default=None, type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS), - help=r''' + help=r""" Optional key-value pairs to be included in the request. The separator used determines the type: @@ -130,66 +121,65 @@ positional.add_argument( field-name-with\:colon=value - ''' + """, ) ####################################################################### # Content type. ####################################################################### -content_type = parser.add_argument_group( - title='Predefined Content Types', - description=None -) +content_types = options.add_group('Predefined Content Types') -content_type.add_argument( - '--json', '-j', +content_types.add_argument( + '--json', + '-j', action='store_const', const=RequestType.JSON, dest='request_type', - help=''' + 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 (if not specified). - ''' + """, ) -content_type.add_argument( - '--form', '-f', +content_types.add_argument( + '--form', + '-f', action='store_const', const=RequestType.FORM, dest='request_type', - help=''' + help=""" Data items from the command line are serialized as form fields. The Content-Type is set to application/x-www-form-urlencoded (if not specified). The presence of any file fields results in a multipart/form-data request. - ''' + """, ) -content_type.add_argument( +content_types.add_argument( '--multipart', action='store_const', const=RequestType.MULTIPART, dest='request_type', - help=''' + help=""" Similar to --form, but always sends a multipart/form-data request (i.e., even without files). - ''' + """, ) -content_type.add_argument( +content_types.add_argument( '--boundary', - help=''' + help=""" Specify a custom boundary string for multipart/form-data requests. Only has effect only together with --form. - ''' + """, ) -content_type.add_argument( +content_types.add_argument( '--raw', - help=''' + help=""" This option allows you to pass raw request data without extra processing (as opposed to the structured request items syntax): @@ -204,55 +194,37 @@ content_type.add_argument( $ http pie.dev/post @data.txt - ''' + """, ) - ####################################################################### # Content processing. ####################################################################### -content_processing = parser.add_argument_group( - title='Content Processing Options', - description=None -) +processing_options = options.add_group('Content Processing Options') -content_processing.add_argument( - '--compress', '-x', +processing_options.add_argument( + '--compress', + '-x', action='count', default=0, - help=''' + help=""" Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate. Compression is skipped if it appears that compression ratio is negative. Compression can be forced by repeating the argument. - ''' + """, ) ####################################################################### # Output processing ####################################################################### -output_processing = parser.add_argument_group(title='Output Processing') - -output_processing.add_argument( - '--pretty', - dest='prettify', - default=PRETTY_STDOUT_TTY_ONLY, - choices=sorted(PRETTY_MAP.keys()), - help=''' - Controls output processing. The value can be "none" to not prettify - the output (default for redirected output), "all" to apply both colors - and formatting (default for terminal output), "colors", or "format". - - ''' -) - def format_style_help(available_styles): - return ''' + return """ Output coloring style (default is "{default}"). It can be one of: {available_styles} @@ -261,92 +233,109 @@ def format_style_help(available_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( + """.format( default=DEFAULT_STYLE, available_styles='\n'.join( f' {line.strip()}' - for line in wrap(', '.join(available_styles), 60) + for line in textwrap.wrap(', '.join(available_styles), 60) ).strip(), auto_style=AUTO_STYLE, ) +_sorted_kwargs = { + 'action': 'append_const', + 'const': SORTED_FORMAT_OPTIONS_STRING, + 'dest': 'format_options', +} +_unsorted_kwargs = { + 'action': 'append_const', + 'const': UNSORTED_FORMAT_OPTIONS_STRING, + 'dest': 'format_options', +} + +output_processing = options.add_group('Output Processing') + output_processing.add_argument( - '--style', '-s', + '--pretty', + dest='prettify', + default=PRETTY_STDOUT_TTY_ONLY, + choices=sorted(PRETTY_MAP.keys()), + help=""" + Controls output processing. The value can be "none" to not prettify + the output (default for redirected output), "all" to apply both colors + and formatting (default for terminal output), "colors", or "format". + + """, +) +output_processing.add_argument( + '--style', + '-s', dest='style', metavar='STYLE', default=DEFAULT_STYLE, action='lazy_choices', getter=get_available_styles, - help_formatter=format_style_help + help_formatter=format_style_help, ) -_sorted_kwargs = { - 'action': 'append_const', - 'const': SORTED_FORMAT_OPTIONS_STRING, - 'dest': 'format_options' -} -_unsorted_kwargs = { - 'action': 'append_const', - 'const': UNSORTED_FORMAT_OPTIONS_STRING, - 'dest': 'format_options' -} # The closest approx. of the documented resetting to default via --no-