mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 09:13:25 +01:00
parent
493e98c833
commit
c2a0cef76e
@ -9,7 +9,8 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
`2.2.0-dev`_ (unreleased)
|
||||
-------------------------
|
||||
|
||||
* Added support for ``--ciphers`` (`#870`_).
|
||||
* Added ``--format-options`` to allow disabling sorting, etc. (`#128`_)
|
||||
* Added ``--ciphers`` to allow configuring OpenSSL ciphers (`#870`_).
|
||||
* Added support for ``$XDG_CONFIG_HOME`` (`#920`_).
|
||||
* Fixed built-in plugins-related circular imports (`#925`_).
|
||||
|
||||
@ -433,6 +434,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
.. _2.2.0-dev: https://github.com/jakubroztocil/httpie/compare/2.1.0...master
|
||||
|
||||
|
||||
.. _#128: https://github.com/jakubroztocil/httpie/issues/128
|
||||
.. _#488: https://github.com/jakubroztocil/httpie/issues/488
|
||||
.. _#840: https://github.com/jakubroztocil/httpie/issues/840
|
||||
.. _#870: https://github.com/jakubroztocil/httpie/issues/870
|
||||
|
14
README.rst
14
README.rst
@ -1395,6 +1395,20 @@ One of these options can be used to control output processing:
|
||||
Default for redirected output.
|
||||
==================== ========================================================
|
||||
|
||||
|
||||
You can control the applied formatting via the ``--format-options`` option.
|
||||
For example, this is how you would disable the default header and JSON key
|
||||
sorting, and specify a custom JSON indent size:
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http --format-options headers.sort=false,json.sort_keys=false,json.indent=2 httpbin.org/get
|
||||
|
||||
This is something you will typically store as one of the default options in your
|
||||
`config`_ file. See ``http --help`` for all the available formatting options.
|
||||
|
||||
|
||||
Binary data
|
||||
-----------
|
||||
|
||||
|
@ -2,9 +2,10 @@ import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from typing import Union, List, Optional
|
||||
from copy import deepcopy
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from httpie.cli.constants import SEPARATOR_CREDENTIALS
|
||||
from httpie.cli.constants import DEFAULT_FORMAT_OPTIONS, SEPARATOR_CREDENTIALS
|
||||
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
@ -181,3 +182,65 @@ def readable_file_arg(filename):
|
||||
return filename
|
||||
except IOError as ex:
|
||||
raise argparse.ArgumentTypeError(f'{filename}: {ex.args[1]}')
|
||||
|
||||
|
||||
|
||||
def parse_format_options(s: str, defaults: Optional[dict]) -> dict:
|
||||
"""
|
||||
Parse `s` and update `defaults` with the parsed values.
|
||||
|
||||
>>> parse_format_options(
|
||||
... defaults={'json': {'indent': 4, 'sort_keys': True}},
|
||||
... s='json.indent=2,json.sort_keys=False',
|
||||
... )
|
||||
{'json': {'indent': 2, 'sort_keys': False}}
|
||||
|
||||
"""
|
||||
value_map = {
|
||||
'true': True,
|
||||
'false': False,
|
||||
}
|
||||
options = deepcopy(defaults or {})
|
||||
for option in s.split(','):
|
||||
try:
|
||||
path, value = option.lower().split('=')
|
||||
section, key = path.split('.')
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'--format-options: invalid option: {option!r}')
|
||||
|
||||
if value in value_map:
|
||||
parsed_value = value_map[value]
|
||||
else:
|
||||
if value.isnumeric():
|
||||
parsed_value = int(value)
|
||||
else:
|
||||
parsed_value = value
|
||||
|
||||
if defaults is None:
|
||||
options.setdefault(section, {})
|
||||
else:
|
||||
try:
|
||||
default_value = defaults[section][key]
|
||||
except KeyError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'--format-options: invalid key: {path!r} in {option!r}')
|
||||
|
||||
default_type, parsed_type = type(default_value), type(parsed_value)
|
||||
if parsed_type is not default_type:
|
||||
raise argparse.ArgumentTypeError(
|
||||
'--format-options: invalid value type:'
|
||||
f' {value!r} in {option!r}'
|
||||
f' (expected {default_type.__name__}'
|
||||
f' got {parsed_type.__name__})'
|
||||
)
|
||||
|
||||
options[section][key] = parsed_value
|
||||
|
||||
return options
|
||||
|
||||
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
|
||||
s=','.join(DEFAULT_FORMAT_OPTIONS),
|
||||
defaults=None,
|
||||
)
|
||||
|
@ -83,6 +83,15 @@ PRETTY_MAP = {
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
|
||||
DEFAULT_FORMAT_OPTIONS = [
|
||||
'headers.sort=true',
|
||||
'json.format=true',
|
||||
'json.indent=4',
|
||||
'json.sort_keys=true',
|
||||
]
|
||||
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
|
@ -8,20 +8,23 @@ from textwrap import dedent, wrap
|
||||
from httpie import __doc__, __version__
|
||||
from httpie.cli.argparser import HTTPieArgumentParser
|
||||
from httpie.cli.argtypes import (
|
||||
KeyValueArgType, SessionNameValidator, readable_file_arg,
|
||||
KeyValueArgType, PARSED_DEFAULT_FORMAT_OPTIONS, SessionNameValidator,
|
||||
parse_format_options,
|
||||
readable_file_arg,
|
||||
)
|
||||
from httpie.cli.constants import (
|
||||
OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||
)
|
||||
from httpie.output.formatters.colors import (
|
||||
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
|
||||
)
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
from httpie.ssl import DEFAULT_SSL_CIPHERS, AVAILABLE_SSL_VERSION_ARG_MAPPING
|
||||
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
@ -206,9 +209,9 @@ output_processing.add_argument(
|
||||
default=DEFAULT_STYLE,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help="""
|
||||
Output coloring style (default is "{default}"). One of:
|
||||
Output coloring style (default is "{default}"). It can be One of:
|
||||
|
||||
{available_styles}
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
|
||||
@ -221,11 +224,38 @@ output_processing.add_argument(
|
||||
available_styles='\n'.join(
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
).strip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--format-options',
|
||||
type=lambda s: parse_format_options(
|
||||
s=s,
|
||||
defaults=PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
),
|
||||
default=PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
help="""
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) --pretty=all or --pretty=format.
|
||||
The following are the default options:
|
||||
|
||||
{option_list}
|
||||
|
||||
You can specify multiple comma-separated options. For example, this modifies
|
||||
the settings to disable the sorting of JSON keys and headers:
|
||||
|
||||
--format-options json.sort_keys=false,headers.sort=false
|
||||
|
||||
This is something you will typically put into your config file.
|
||||
|
||||
""".format(
|
||||
option_list='\n'.join(
|
||||
(8 * ' ') + option for option in DEFAULT_FORMAT_OPTIONS).strip()
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output options
|
||||
#######################################################################
|
||||
|
@ -3,6 +3,10 @@ from httpie.plugins import FormatterPlugin
|
||||
|
||||
class HeadersFormatter(FormatterPlugin):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['headers']['sort']
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
"""
|
||||
Sorts headers by name while retaining relative
|
||||
|
@ -4,11 +4,12 @@ import json
|
||||
from httpie.plugins import FormatterPlugin
|
||||
|
||||
|
||||
DEFAULT_INDENT = 4
|
||||
|
||||
|
||||
class JSONFormatter(FormatterPlugin):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['json']['format']
|
||||
|
||||
def format_body(self, body: str, mime: str) -> str:
|
||||
maybe_json = [
|
||||
'json',
|
||||
@ -26,8 +27,8 @@ class JSONFormatter(FormatterPlugin):
|
||||
# unicode escapes to improve readability.
|
||||
body = json.dumps(
|
||||
obj=obj,
|
||||
sort_keys=True,
|
||||
sort_keys=self.format_options['json']['sort_keys'],
|
||||
ensure_ascii=False,
|
||||
indent=DEFAULT_INDENT
|
||||
indent=self.format_options['json']['indent']
|
||||
)
|
||||
return body
|
||||
|
@ -152,6 +152,7 @@ def get_stream_type_and_kwargs(
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
format_options=args.format_options,
|
||||
)
|
||||
}
|
||||
else:
|
||||
|
@ -119,6 +119,7 @@ class FormatterPlugin(BasePlugin):
|
||||
"""
|
||||
self.enabled = True
|
||||
self.kwargs = kwargs
|
||||
self.format_options = kwargs['format_options']
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
"""Return processed `headers`
|
||||
|
@ -4,6 +4,7 @@
|
||||
[tool:pytest]
|
||||
# <https://docs.pytest.org/en/latest/customize.html>
|
||||
norecursedirs = tests/fixtures
|
||||
addopts = --tb=native
|
||||
|
||||
|
||||
[pycodestyle]
|
||||
|
@ -1,12 +1,15 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from tempfile import gettempdir
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import MockEnvironment, http, HTTP_OK, COLOR, CRLF
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.cli.argtypes import parse_format_options
|
||||
from httpie.output.formatters.colors import get_lexer
|
||||
from httpie.status import ExitStatus
|
||||
from utils import COLOR, CRLF, HTTP_OK, MockEnvironment, http
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stdout_isatty', [True, False])
|
||||
@ -58,19 +61,19 @@ class TestColors:
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
|
||||
argvalues=[
|
||||
('application/json', False, None, 'JSON'),
|
||||
('application/json', False, None, 'JSON'),
|
||||
('application/json+foo', False, None, 'JSON'),
|
||||
('application/foo+json', False, None, 'JSON'),
|
||||
('application/json-foo', False, None, 'JSON'),
|
||||
('application/x-json', False, None, 'JSON'),
|
||||
('foo/json', False, None, 'JSON'),
|
||||
('foo/json+bar', False, None, 'JSON'),
|
||||
('foo/bar+json', False, None, 'JSON'),
|
||||
('foo/json-foo', False, None, 'JSON'),
|
||||
('foo/x-json', False, None, 'JSON'),
|
||||
('application/x-json', False, None, 'JSON'),
|
||||
('foo/json', False, None, 'JSON'),
|
||||
('foo/json+bar', False, None, 'JSON'),
|
||||
('foo/bar+json', False, None, 'JSON'),
|
||||
('foo/json-foo', False, None, 'JSON'),
|
||||
('foo/x-json', False, None, 'JSON'),
|
||||
('application/vnd.comverge.grid+hal+json', False, None, 'JSON'),
|
||||
('text/plain', True, '{}', 'JSON'),
|
||||
('text/plain', True, 'foo', 'Text only'),
|
||||
('text/plain', True, '{}', 'JSON'),
|
||||
('text/plain', True, 'foo', 'Text only'),
|
||||
]
|
||||
)
|
||||
def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
|
||||
@ -83,7 +86,7 @@ class TestColors:
|
||||
|
||||
|
||||
class TestPrettyOptions:
|
||||
"""Test the --pretty flag handling."""
|
||||
"""Test the --pretty handling."""
|
||||
|
||||
def test_pretty_enabled_by_default(self, httpbin):
|
||||
env = MockEnvironment(colors=256)
|
||||
@ -138,6 +141,7 @@ class TestLineEndings:
|
||||
and as the headers/body separator.
|
||||
|
||||
"""
|
||||
|
||||
def _validate_crlf(self, msg):
|
||||
lines = iter(msg.splitlines(True))
|
||||
for header in lines:
|
||||
@ -171,3 +175,77 @@ class TestLineEndings:
|
||||
def test_CRLF_formatted_request(self, httpbin):
|
||||
r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get')
|
||||
self._validate_crlf(r)
|
||||
|
||||
|
||||
class TestFormatOptions:
|
||||
def test_header_formatting_options(self):
|
||||
def get_headers(sort):
|
||||
return http(
|
||||
'--offline', '--print=H',
|
||||
'--format-options', 'headers.sort=' + sort,
|
||||
'example.org', 'ZZZ:foo', 'XXX:foo',
|
||||
)
|
||||
|
||||
r_sorted = get_headers('true')
|
||||
r_unsorted = get_headers('false')
|
||||
assert r_sorted != r_unsorted
|
||||
assert f'XXX: foo{CRLF}ZZZ: foo' in r_sorted
|
||||
assert f'ZZZ: foo{CRLF}XXX: foo' in r_unsorted
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['options', 'expected_json'],
|
||||
argvalues=[
|
||||
# @formatter:off
|
||||
(
|
||||
'json.sort_keys=true,json.indent=4',
|
||||
json.dumps({'a': 0, 'b': 0}, indent=4),
|
||||
),
|
||||
(
|
||||
'json.sort_keys=false,json.indent=2',
|
||||
json.dumps({'b': 0, 'a': 0}, indent=2),
|
||||
),
|
||||
(
|
||||
'json.format=false',
|
||||
json.dumps({'b': 0, 'a': 0}),
|
||||
),
|
||||
# @formatter:on
|
||||
]
|
||||
)
|
||||
def test_json_formatting_options(self, options: str, expected_json: str):
|
||||
r = http(
|
||||
'--offline', '--print=B',
|
||||
'--format-options', options,
|
||||
'example.org', 'b:=0', 'a:=0',
|
||||
)
|
||||
assert expected_json in r
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['defaults', 'options_string', 'expected'],
|
||||
argvalues=[
|
||||
# @formatter:off
|
||||
({'foo': {'bar': 1}}, 'foo.bar=2', {'foo': {'bar': 2}}),
|
||||
({'foo': {'bar': True}}, 'foo.bar=false', {'foo': {'bar': False}}),
|
||||
({'foo': {'bar': 'a'}}, 'foo.bar=b', {'foo': {'bar': 'b'}}),
|
||||
# @formatter:on
|
||||
]
|
||||
)
|
||||
def test_parse_format_options(self, defaults, options_string, expected):
|
||||
actual = parse_format_options(s=options_string, defaults=defaults)
|
||||
assert expected == actual
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['options_string', 'expected_error'],
|
||||
argvalues=[
|
||||
('foo=2', 'invalid option'),
|
||||
('foo.baz=2', 'invalid key'),
|
||||
('foo.bar=false', 'expected int got bool'),
|
||||
]
|
||||
)
|
||||
def test_parse_format_options_errors(self, options_string, expected_error):
|
||||
defaults = {
|
||||
'foo': {
|
||||
'bar': 1
|
||||
}
|
||||
}
|
||||
with pytest.raises(argparse.ArgumentTypeError, match=expected_error):
|
||||
parse_format_options(s=options_string, defaults=defaults)
|
||||
|
Loading…
Reference in New Issue
Block a user