forked from extern/httpie-cli
Decouple parser definition from argparse (#1293)
This commit is contained in:
parent
7509dd4e6c
commit
77af4c7a5c
@ -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.
|
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`
|
### `httpie plugins`
|
||||||
|
|
||||||
`plugins` interface is a very simple plugin manager for installing, listing and uninstalling HTTPie plugins.
|
`plugins` interface is a very simple plugin manager for installing, listing and uninstalling HTTPie plugins.
|
||||||
|
@ -90,13 +90,19 @@ OUTPUT_OPTIONS = frozenset({
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Pretty
|
# Pretty
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyOptions(enum.Enum):
|
||||||
|
STDOUT_TTY_ONLY = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
PRETTY_MAP = {
|
PRETTY_MAP = {
|
||||||
'all': ['format', 'colors'],
|
'all': ['format', 'colors'],
|
||||||
'colors': ['colors'],
|
'colors': ['colors'],
|
||||||
'format': ['format'],
|
'format': ['format'],
|
||||||
'none': []
|
'none': []
|
||||||
}
|
}
|
||||||
PRETTY_STDOUT_TTY_ONLY = object()
|
PRETTY_STDOUT_TTY_ONLY = PrettyOptions.STDOUT_TTY_ONLY
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FORMAT_OPTIONS = [
|
DEFAULT_FORMAT_OPTIONS = [
|
||||||
|
File diff suppressed because it is too large
Load Diff
189
httpie/cli/options.py
Normal file
189
httpie/cli/options.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import argparse
|
||||||
|
import textwrap
|
||||||
|
import typing
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Any, Optional, Dict, List, Type, TypeVar
|
||||||
|
|
||||||
|
from httpie.cli.argparser import HTTPieArgumentParser
|
||||||
|
from httpie.cli.utils import LazyChoices
|
||||||
|
|
||||||
|
|
||||||
|
class Qualifiers(Enum):
|
||||||
|
OPTIONAL = auto()
|
||||||
|
ZERO_OR_MORE = auto()
|
||||||
|
SUPPRESS = auto()
|
||||||
|
|
||||||
|
|
||||||
|
def map_qualifiers(
|
||||||
|
configuration: Dict[str, Any], qualifier_map: Dict[Qualifiers, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: qualifier_map[value] if isinstance(value, Qualifiers) else value
|
||||||
|
for key, value in configuration.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PARSER_SPEC_VERSION = '0.0.1a0'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParserSpec:
|
||||||
|
program: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
epilog: Optional[str] = None
|
||||||
|
groups: List['Group'] = field(default_factory=list)
|
||||||
|
|
||||||
|
def finalize(self) -> 'ParserSpec':
|
||||||
|
if self.description:
|
||||||
|
self.description = textwrap.dedent(self.description)
|
||||||
|
if self.epilog:
|
||||||
|
self.epilog = textwrap.dedent(self.epilog)
|
||||||
|
for group in self.groups:
|
||||||
|
group.finalize()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_group(self, name: str, **kwargs) -> 'Group':
|
||||||
|
group = Group(name, **kwargs)
|
||||||
|
self.groups.append(group)
|
||||||
|
return group
|
||||||
|
|
||||||
|
def serialize(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'name': self.program,
|
||||||
|
'description': self.description,
|
||||||
|
'groups': [group.serialize() for group in self.groups],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Group:
|
||||||
|
name: str
|
||||||
|
description: str = ''
|
||||||
|
is_mutually_exclusive: bool = False
|
||||||
|
arguments: List['Argument'] = field(default_factory=list)
|
||||||
|
|
||||||
|
def finalize(self) -> None:
|
||||||
|
if self.description:
|
||||||
|
self.description = textwrap.dedent(self.description)
|
||||||
|
|
||||||
|
def add_argument(self, *args, **kwargs):
|
||||||
|
argument = Argument(list(args), kwargs.copy())
|
||||||
|
self.arguments.append(argument)
|
||||||
|
return argument
|
||||||
|
|
||||||
|
def serialize(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description or None,
|
||||||
|
'is_mutually_exclusive': self.is_mutually_exclusive,
|
||||||
|
'args': [argument.serialize() for argument in self.arguments],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Argument(typing.NamedTuple):
|
||||||
|
aliases: List[str]
|
||||||
|
configuration: Dict[str, Any]
|
||||||
|
|
||||||
|
def serialize(self) -> 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)
|
||||||
|
if action == 'lazy_choices':
|
||||||
|
choices = LazyChoices(self.aliases, **{'dest': None, **configuration})
|
||||||
|
configuration['choices'] = list(choices.load())
|
||||||
|
configuration['help'] = choices.help
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
if self.aliases:
|
||||||
|
result['options'] = self.aliases.copy()
|
||||||
|
else:
|
||||||
|
result['options'] = [configuration['metavar']]
|
||||||
|
result['is_positional'] = True
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
python_type = configuration.get('type')
|
||||||
|
if python_type is not None:
|
||||||
|
if hasattr(python_type, '__name__'):
|
||||||
|
type_name = python_type.__name__
|
||||||
|
else:
|
||||||
|
type_name = type(python_type).__name__
|
||||||
|
|
||||||
|
result['python_type_name'] = type_name
|
||||||
|
|
||||||
|
result.update({
|
||||||
|
key: value
|
||||||
|
for key, value in configuration.items()
|
||||||
|
if key in JSON_DIRECT_MIRROR_OPTIONS
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __getattr__(self, attribute_name):
|
||||||
|
if attribute_name in self.configuration:
|
||||||
|
return self.configuration[attribute_name]
|
||||||
|
else:
|
||||||
|
raise AttributeError(attribute_name)
|
||||||
|
|
||||||
|
|
||||||
|
ParserType = TypeVar('ParserType', bound=Type[argparse.ArgumentParser])
|
||||||
|
|
||||||
|
ARGPARSE_QUALIFIER_MAP = {
|
||||||
|
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
||||||
|
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
||||||
|
Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_argparse(
|
||||||
|
abstract_options: ParserSpec,
|
||||||
|
parser_type: ParserType = HTTPieArgumentParser,
|
||||||
|
) -> ParserType:
|
||||||
|
concrete_parser = parser_type(
|
||||||
|
prog=abstract_options.program,
|
||||||
|
description=abstract_options.description,
|
||||||
|
epilog=abstract_options.epilog,
|
||||||
|
)
|
||||||
|
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
||||||
|
|
||||||
|
for abstract_group in abstract_options.groups:
|
||||||
|
concrete_group = concrete_parser.add_argument_group(
|
||||||
|
title=abstract_group.name, description=abstract_group.description
|
||||||
|
)
|
||||||
|
if abstract_group.is_mutually_exclusive:
|
||||||
|
concrete_group = concrete_group.add_mutually_exclusive_group(required=False)
|
||||||
|
|
||||||
|
for abstract_argument in abstract_group.arguments:
|
||||||
|
concrete_group.add_argument(
|
||||||
|
*abstract_argument.aliases,
|
||||||
|
**map_qualifiers(
|
||||||
|
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return concrete_parser
|
||||||
|
|
||||||
|
|
||||||
|
JSON_DIRECT_MIRROR_OPTIONS = (
|
||||||
|
'choices',
|
||||||
|
'metavar'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
JSON_QUALIFIER_TO_OPTIONS = {
|
||||||
|
Qualifiers.OPTIONAL: {'is_optional': True},
|
||||||
|
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
||||||
|
Qualifiers.SUPPRESS: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
||||||
|
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
|
@ -17,9 +17,10 @@ from .context import Environment, Levels
|
|||||||
from .downloads import Downloader
|
from .downloads import Downloader
|
||||||
from .models import (
|
from .models import (
|
||||||
RequestsMessageKind,
|
RequestsMessageKind,
|
||||||
OutputOptions,
|
OutputOptions
|
||||||
)
|
)
|
||||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
from .output.models import ProcessingOptions
|
||||||
|
from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_SEPARATOR_BYTES
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
from .status import ExitStatus, http_status_to_exit_status
|
from .status import ExitStatus, http_status_to_exit_status
|
||||||
from .utils import unwrap_context
|
from .utils import unwrap_context
|
||||||
@ -169,6 +170,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
downloader = None
|
downloader = None
|
||||||
initial_request: Optional[requests.PreparedRequest] = None
|
initial_request: Optional[requests.PreparedRequest] = None
|
||||||
final_response: Optional[requests.Response] = None
|
final_response: Optional[requests.Response] = None
|
||||||
|
processing_options = ProcessingOptions.from_raw_args(args)
|
||||||
|
|
||||||
def separate():
|
def separate():
|
||||||
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
||||||
@ -183,12 +185,12 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
and chunk
|
and chunk
|
||||||
)
|
)
|
||||||
if should_pipe_to_stdout:
|
if should_pipe_to_stdout:
|
||||||
msg = requests.PreparedRequest()
|
return write_raw_data(
|
||||||
msg.is_body_upload_chunk = True
|
env,
|
||||||
msg.body = chunk
|
chunk,
|
||||||
msg.headers = initial_request.headers
|
processing_options=processing_options,
|
||||||
msg_output_options = OutputOptions.from_message(msg, body=True, headers=False)
|
headers=initial_request.headers
|
||||||
write_message(requests_message=msg, env=env, args=args, output_options=msg_output_options)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.download:
|
if args.download:
|
||||||
@ -222,9 +224,14 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
||||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=Levels.WARNING)
|
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=Levels.WARNING)
|
||||||
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
|
write_message(
|
||||||
body=do_write_body
|
requests_message=message,
|
||||||
))
|
env=env,
|
||||||
|
output_options=output_options._replace(
|
||||||
|
body=do_write_body
|
||||||
|
),
|
||||||
|
processing_options=processing_options
|
||||||
|
)
|
||||||
prev_with_body = output_options.body
|
prev_with_body = output_options.body
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
@ -45,6 +45,14 @@ COMMANDS = {
|
|||||||
},
|
},
|
||||||
'cli': {
|
'cli': {
|
||||||
'help': 'Manage HTTPie for Terminal',
|
'help': 'Manage HTTPie for Terminal',
|
||||||
|
'export-args': [
|
||||||
|
'Export available options for the CLI',
|
||||||
|
{
|
||||||
|
'flags': ['-f', '--format'],
|
||||||
|
'choices': ['json'],
|
||||||
|
'default': 'json'
|
||||||
|
}
|
||||||
|
],
|
||||||
'sessions': {
|
'sessions': {
|
||||||
'help': 'Manage HTTPie sessions',
|
'help': 'Manage HTTPie sessions',
|
||||||
'upgrade': [
|
'upgrade': [
|
||||||
|
@ -114,3 +114,28 @@ def cli_upgrade_all_sessions(env: Environment, args: argparse.Namespace) -> Exit
|
|||||||
session_name=session_name
|
session_name=session_name
|
||||||
)
|
)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_TO_CONTENT_TYPE = {
|
||||||
|
'json': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@task('export-args')
|
||||||
|
def cli_export(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||||
|
import json
|
||||||
|
from httpie.cli.definition import options
|
||||||
|
from httpie.cli.options import to_data
|
||||||
|
from httpie.output.writer import write_raw_data
|
||||||
|
|
||||||
|
if args.format == 'json':
|
||||||
|
data = json.dumps(to_data(options))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'Unexpected format value: {args.format}')
|
||||||
|
|
||||||
|
write_raw_data(
|
||||||
|
env,
|
||||||
|
data,
|
||||||
|
stream_kwargs={'mime_overwrite': FORMAT_TO_CONTENT_TYPE[args.format]},
|
||||||
|
)
|
||||||
|
return ExitStatus.SUCCESS
|
||||||
|
44
httpie/output/models.py
Normal file
44
httpie/output/models.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import argparse
|
||||||
|
from typing import Any, Dict, Union, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
from httpie.context import Environment
|
||||||
|
from httpie.cli.constants import PrettyOptions, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY
|
||||||
|
from httpie.cli.argtypes import PARSED_DEFAULT_FORMAT_OPTIONS
|
||||||
|
from httpie.output.formatters.colors import AUTO_STYLE
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessingOptions(NamedTuple):
|
||||||
|
"""Represents a set of stylistic options
|
||||||
|
that are used when deciding which stream
|
||||||
|
should be used."""
|
||||||
|
|
||||||
|
debug: bool = False
|
||||||
|
traceback: bool = False
|
||||||
|
|
||||||
|
stream: bool = False
|
||||||
|
style: str = AUTO_STYLE
|
||||||
|
prettify: Union[List[str], PrettyOptions] = PRETTY_STDOUT_TTY_ONLY
|
||||||
|
|
||||||
|
response_mime: Optional[str] = None
|
||||||
|
response_charset: Optional[str] = None
|
||||||
|
|
||||||
|
json: bool = False
|
||||||
|
format_options: Dict[str, Any] = PARSED_DEFAULT_FORMAT_OPTIONS
|
||||||
|
|
||||||
|
def get_prettify(self, env: Environment) -> List[str]:
|
||||||
|
if self.prettify is PRETTY_STDOUT_TTY_ONLY:
|
||||||
|
return PRETTY_MAP['all' if env.stdout_isatty else 'none']
|
||||||
|
else:
|
||||||
|
return self.prettify
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_raw_args(cls, options: argparse.Namespace) -> 'ProcessingOptions':
|
||||||
|
fetched_options = {
|
||||||
|
option: getattr(options, option)
|
||||||
|
for option in cls._fields
|
||||||
|
}
|
||||||
|
return cls(**fetched_options)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def show_traceback(self):
|
||||||
|
return self.debug or self.traceback
|
@ -34,7 +34,8 @@ class BaseStream(metaclass=ABCMeta):
|
|||||||
self,
|
self,
|
||||||
msg: HTTPMessage,
|
msg: HTTPMessage,
|
||||||
output_options: OutputOptions,
|
output_options: OutputOptions,
|
||||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
on_body_chunk_downloaded: Callable[[bytes], None] = None,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param msg: a :class:`models.HTTPMessage` subclass
|
:param msg: a :class:`models.HTTPMessage` subclass
|
||||||
@ -45,6 +46,7 @@ class BaseStream(metaclass=ABCMeta):
|
|||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.output_options = output_options
|
self.output_options = output_options
|
||||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||||
|
self.extra_options = kwargs
|
||||||
|
|
||||||
def get_headers(self) -> bytes:
|
def get_headers(self) -> bytes:
|
||||||
"""Return the headers' bytes."""
|
"""Return the headers' bytes."""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import argparse
|
|
||||||
import errno
|
import errno
|
||||||
from typing import IO, TextIO, Tuple, Type, Union
|
import requests
|
||||||
|
from typing import Any, Dict, IO, Optional, TextIO, Tuple, Type, Union
|
||||||
|
|
||||||
from ..cli.dicts import HTTPHeadersDict
|
from ..cli.dicts import HTTPHeadersDict
|
||||||
from ..context import Environment
|
from ..context import Environment
|
||||||
@ -10,8 +10,9 @@ from ..models import (
|
|||||||
HTTPMessage,
|
HTTPMessage,
|
||||||
RequestsMessage,
|
RequestsMessage,
|
||||||
RequestsMessageKind,
|
RequestsMessageKind,
|
||||||
OutputOptions
|
OutputOptions,
|
||||||
)
|
)
|
||||||
|
from .models import ProcessingOptions
|
||||||
from .processing import Conversion, Formatting
|
from .processing import Conversion, Formatting
|
||||||
from .streams import (
|
from .streams import (
|
||||||
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
||||||
@ -25,30 +26,31 @@ MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
|
|||||||
def write_message(
|
def write_message(
|
||||||
requests_message: RequestsMessage,
|
requests_message: RequestsMessage,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
|
||||||
output_options: OutputOptions,
|
output_options: OutputOptions,
|
||||||
|
processing_options: ProcessingOptions,
|
||||||
|
extra_stream_kwargs: Optional[Dict[str, Any]] = None
|
||||||
):
|
):
|
||||||
if not output_options.any():
|
if not output_options.any():
|
||||||
return
|
return
|
||||||
write_stream_kwargs = {
|
write_stream_kwargs = {
|
||||||
'stream': build_output_stream_for_message(
|
'stream': build_output_stream_for_message(
|
||||||
args=args,
|
|
||||||
env=env,
|
env=env,
|
||||||
requests_message=requests_message,
|
requests_message=requests_message,
|
||||||
output_options=output_options,
|
output_options=output_options,
|
||||||
|
processing_options=processing_options,
|
||||||
|
extra_stream_kwargs=extra_stream_kwargs
|
||||||
),
|
),
|
||||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||||
'outfile': env.stdout,
|
'outfile': env.stdout,
|
||||||
'flush': env.stdout_isatty or args.stream
|
'flush': env.stdout_isatty or processing_options.stream
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
if env.is_windows and 'colors' in args.prettify:
|
if env.is_windows and 'colors' in processing_options.get_prettify(env):
|
||||||
write_stream_with_colors_win(**write_stream_kwargs)
|
write_stream_with_colors_win(**write_stream_kwargs)
|
||||||
else:
|
else:
|
||||||
write_stream(**write_stream_kwargs)
|
write_stream(**write_stream_kwargs)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
show_traceback = args.debug or args.traceback
|
if processing_options.show_traceback and e.errno == errno.EPIPE:
|
||||||
if not show_traceback and e.errno == errno.EPIPE:
|
|
||||||
# Ignore broken pipes unless --traceback.
|
# Ignore broken pipes unless --traceback.
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
else:
|
else:
|
||||||
@ -94,11 +96,34 @@ def write_stream_with_colors_win(
|
|||||||
outfile.flush()
|
outfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def write_raw_data(
|
||||||
|
env: Environment,
|
||||||
|
data: Any,
|
||||||
|
*,
|
||||||
|
processing_options: Optional[ProcessingOptions] = None,
|
||||||
|
headers: Optional[HTTPHeadersDict] = None,
|
||||||
|
stream_kwargs: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
msg = requests.PreparedRequest()
|
||||||
|
msg.is_body_upload_chunk = True
|
||||||
|
msg.body = data
|
||||||
|
msg.headers = headers or HTTPHeadersDict()
|
||||||
|
msg_output_options = OutputOptions.from_message(msg, body=True, headers=False)
|
||||||
|
return write_message(
|
||||||
|
requests_message=msg,
|
||||||
|
env=env,
|
||||||
|
output_options=msg_output_options,
|
||||||
|
processing_options=processing_options or ProcessingOptions(),
|
||||||
|
extra_stream_kwargs=stream_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_output_stream_for_message(
|
def build_output_stream_for_message(
|
||||||
args: argparse.Namespace,
|
|
||||||
env: Environment,
|
env: Environment,
|
||||||
requests_message: RequestsMessage,
|
requests_message: RequestsMessage,
|
||||||
output_options: OutputOptions,
|
output_options: OutputOptions,
|
||||||
|
processing_options: ProcessingOptions,
|
||||||
|
extra_stream_kwargs: Optional[Dict[str, Any]] = None
|
||||||
):
|
):
|
||||||
message_type = {
|
message_type = {
|
||||||
RequestsMessageKind.REQUEST: HTTPRequest,
|
RequestsMessageKind.REQUEST: HTTPRequest,
|
||||||
@ -106,10 +131,12 @@ def build_output_stream_for_message(
|
|||||||
}[output_options.kind]
|
}[output_options.kind]
|
||||||
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
||||||
env=env,
|
env=env,
|
||||||
args=args,
|
processing_options=processing_options,
|
||||||
message_type=message_type,
|
message_type=message_type,
|
||||||
headers=requests_message.headers
|
headers=requests_message.headers
|
||||||
)
|
)
|
||||||
|
if extra_stream_kwargs:
|
||||||
|
stream_kwargs.update(extra_stream_kwargs)
|
||||||
yield from stream_class(
|
yield from stream_class(
|
||||||
msg=message_type(requests_message),
|
msg=message_type(requests_message),
|
||||||
output_options=output_options,
|
output_options=output_options,
|
||||||
@ -124,20 +151,21 @@ def build_output_stream_for_message(
|
|||||||
|
|
||||||
def get_stream_type_and_kwargs(
|
def get_stream_type_and_kwargs(
|
||||||
env: Environment,
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
processing_options: ProcessingOptions,
|
||||||
message_type: Type[HTTPMessage],
|
message_type: Type[HTTPMessage],
|
||||||
headers: HTTPHeadersDict,
|
headers: HTTPHeadersDict,
|
||||||
) -> Tuple[Type['BaseStream'], dict]:
|
) -> Tuple[Type['BaseStream'], dict]:
|
||||||
"""Pick the right stream type and kwargs for it based on `env` and `args`.
|
"""Pick the right stream type and kwargs for it based on `env` and `args`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
is_stream = args.stream
|
is_stream = processing_options.stream
|
||||||
|
prettify_groups = processing_options.get_prettify(env)
|
||||||
if not is_stream and message_type is HTTPResponse:
|
if not is_stream and message_type is HTTPResponse:
|
||||||
# If this is a response, then check the headers for determining
|
# If this is a response, then check the headers for determining
|
||||||
# auto-streaming.
|
# auto-streaming.
|
||||||
is_stream = headers.get('Content-Type') == 'text/event-stream'
|
is_stream = headers.get('Content-Type') == 'text/event-stream'
|
||||||
|
|
||||||
if not env.stdout_isatty and not args.prettify:
|
if not env.stdout_isatty and not prettify_groups:
|
||||||
stream_class = RawStream
|
stream_class = RawStream
|
||||||
stream_kwargs = {
|
stream_kwargs = {
|
||||||
'chunk_size': (
|
'chunk_size': (
|
||||||
@ -153,19 +181,19 @@ def get_stream_type_and_kwargs(
|
|||||||
}
|
}
|
||||||
if message_type is HTTPResponse:
|
if message_type is HTTPResponse:
|
||||||
stream_kwargs.update({
|
stream_kwargs.update({
|
||||||
'mime_overwrite': args.response_mime,
|
'mime_overwrite': processing_options.response_mime,
|
||||||
'encoding_overwrite': args.response_charset,
|
'encoding_overwrite': processing_options.response_charset,
|
||||||
})
|
})
|
||||||
if args.prettify:
|
if prettify_groups:
|
||||||
stream_class = PrettyStream if is_stream else BufferedPrettyStream
|
stream_class = PrettyStream if is_stream else BufferedPrettyStream
|
||||||
stream_kwargs.update({
|
stream_kwargs.update({
|
||||||
'conversion': Conversion(),
|
'conversion': Conversion(),
|
||||||
'formatting': Formatting(
|
'formatting': Formatting(
|
||||||
env=env,
|
env=env,
|
||||||
groups=args.prettify,
|
groups=prettify_groups,
|
||||||
color_scheme=args.style,
|
color_scheme=processing_options.style,
|
||||||
explicit_json=args.json,
|
explicit_json=processing_options.json,
|
||||||
format_options=args.format_options,
|
format_options=processing_options.format_options,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import shutil
|
|||||||
import json
|
import json
|
||||||
from httpie.sessions import SESSIONS_DIR_NAME
|
from httpie.sessions import SESSIONS_DIR_NAME
|
||||||
from httpie.status import ExitStatus
|
from httpie.status import ExitStatus
|
||||||
|
from httpie.cli.options import PARSER_SPEC_VERSION
|
||||||
from tests.utils import DUMMY_HOST, httpie
|
from tests.utils import DUMMY_HOST, httpie
|
||||||
from tests.fixtures import SESSION_FILES_PATH, SESSION_FILES_NEW, SESSION_FILES_OLD, read_session_file
|
from tests.fixtures import SESSION_FILES_PATH, SESSION_FILES_NEW, SESSION_FILES_OLD, read_session_file
|
||||||
|
|
||||||
@ -123,3 +124,15 @@ def test_httpie_sessions_upgrade_all(tmp_path, mock_env, extra_args, extra_varia
|
|||||||
assert read_session_file(refactored_session_file) == read_session_file(
|
assert read_session_file(refactored_session_file) == read_session_file(
|
||||||
expected_session_file, extra_variables=extra_variables
|
expected_session_file, extra_variables=extra_variables
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'load_func, extra_options', [
|
||||||
|
(json.loads, []),
|
||||||
|
(json.loads, ['--format=json'])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_cli_export(load_func, extra_options):
|
||||||
|
response = httpie('cli', 'export-args', *extra_options)
|
||||||
|
assert response.exit_status == ExitStatus.SUCCESS
|
||||||
|
assert load_func(response)['version'] == PARSER_SPEC_VERSION
|
||||||
|
60
tests/test_parser_schema.py
Normal file
60
tests/test_parser_schema.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from httpie.cli.options import ParserSpec, Qualifiers
|
||||||
|
|
||||||
|
|
||||||
|
def test_parser_serialization():
|
||||||
|
small_parser = ParserSpec("test_parser")
|
||||||
|
|
||||||
|
group_1 = small_parser.add_group("group_1")
|
||||||
|
group_1.add_argument("regular_arg", help="regular arg")
|
||||||
|
group_1.add_argument(
|
||||||
|
"variadic_arg",
|
||||||
|
metavar="META",
|
||||||
|
help=Qualifiers.SUPPRESS,
|
||||||
|
nargs=Qualifiers.ZERO_OR_MORE,
|
||||||
|
)
|
||||||
|
group_1.add_argument(
|
||||||
|
"-O",
|
||||||
|
"--opt-arg",
|
||||||
|
action="lazy_choices",
|
||||||
|
getter=lambda: ["opt_1", "opt_2"],
|
||||||
|
help_formatter=lambda state: ", ".join(state),
|
||||||
|
)
|
||||||
|
|
||||||
|
group_2 = small_parser.add_group("group_2")
|
||||||
|
group_2.add_argument("--typed", action="store_true", type=int)
|
||||||
|
|
||||||
|
definition = small_parser.finalize()
|
||||||
|
assert definition.serialize() == {
|
||||||
|
"name": "test_parser",
|
||||||
|
"description": None,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "group_1",
|
||||||
|
"description": None,
|
||||||
|
"is_mutually_exclusive": False,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"options": ["regular_arg"],
|
||||||
|
"description": "regular arg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"options": ["variadic_arg"],
|
||||||
|
"is_optional": True,
|
||||||
|
"is_variadic": True,
|
||||||
|
"metavar": "META",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"options": ["-O", "--opt-arg"],
|
||||||
|
"description": "opt_1, opt_2",
|
||||||
|
"choices": ["opt_1", "opt_2"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "group_2",
|
||||||
|
"description": None,
|
||||||
|
"is_mutually_exclusive": False,
|
||||||
|
"args": [{"options": ["--typed"], "python_type_name": "int"}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user