提交 279e387d 编写于 作者: M Mickaël Schoentgen

WIP

上级 50f57f8c
......@@ -75,6 +75,8 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
) -> argparse.Namespace:
self.env = env
self.args, no_options = super().parse_known_args(args, namespace)
if self.args.prompt:
return self.args
if self.args.debug:
self.args.traceback = True
self.has_stdin_data = (
......
......@@ -2,7 +2,7 @@
CLI arguments definition.
"""
from argparse import (FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE)
from argparse import FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE
from textwrap import dedent, wrap
from .. import __doc__, __version__
......@@ -73,6 +73,7 @@ positional.add_argument(
positional.add_argument(
dest='url',
metavar='URL',
nargs=OPTIONAL,
help='''
The scheme defaults to 'http://' if the URL does not include one.
(You can override this with: --default-scheme=https)
......@@ -840,3 +841,12 @@ troubleshooting.add_argument(
'''
)
troubleshooting.add_argument(
'--prompt',
action='store_true',
default=False,
help='''
Start the shell!
'''
)
......@@ -29,6 +29,10 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
Return exit status code.
"""
if '--prompt' in args:
from .prompt.cli import cli
return cli(sys.argv[2:])
program_name, *args = args
env.program_name = os.path.basename(program_name)
args = decode_raw_args(args, env.stdin_encoding)
......
prompt @ 8922a771
Subproject commit 8922a77156a7dc96bac9e3e94fe900bb17f976c2
......@@ -9,6 +9,7 @@ import httpie
# Note: keep requirements here to ease distributions packaging
tests_require = [
'pexpect',
'pytest',
'pytest-httpbin>=0.0.6',
'responses',
......@@ -20,12 +21,12 @@ dev_require = [
'flake8-deprecated',
'flake8-mutable',
'flake8-tuple',
'jinja2',
'pyopenssl',
'pytest-cov',
'pyyaml',
'twine',
'wheel',
'Jinja2'
]
install_requires = [
'charset_normalizer>=2.0.0',
......@@ -34,6 +35,11 @@ install_requires = [
'Pygments>=2.5.2',
'requests-toolbelt>=0.9.1',
'setuptools',
# Prompt
'click>=5.0',
'parsimonious>=0.6.2',
'prompt-toolkit>=2.0.0,<3.0.0',
'pyyaml>=3.0',
]
install_requires_win_only = [
'colorama>=0.2.4',
......@@ -79,6 +85,7 @@ setup(
'console_scripts': [
'http = httpie.__main__:main',
'https = httpie.__main__:main',
'http-prompt=httpie.prompt.cli:cli',
],
},
python_requires='>=3.6',
......
import os
import shutil
import sys
import tempfile
import unittest
class TempAppDirTestCase(unittest.TestCase):
"""Set up temporary app data and config directories before every test
method, and delete them afterwards.
"""
def setUp(self):
# Create a temp dir that will contain data and config directories
self.temp_dir = tempfile.mkdtemp()
if sys.platform == 'win32':
self.homes = {
# subdir_name: envvar_name
'data': 'LOCALAPPDATA',
'config': 'LOCALAPPDATA'
}
else:
self.homes = {
# subdir_name: envvar_name
'data': 'XDG_DATA_HOME',
'config': 'XDG_CONFIG_HOME'
}
# Used to restore
self.orig_envvars = {}
for subdir_name, envvar_name in self.homes.items():
if envvar_name in os.environ:
self.orig_envvars[envvar_name] = os.environ[envvar_name]
os.environ[envvar_name] = os.path.join(self.temp_dir, subdir_name)
def tearDown(self):
# Restore envvar values
for name in self.homes.values():
if name in self.orig_envvars:
os.environ[name] = self.orig_envvars[name]
else:
del os.environ[name]
shutil.rmtree(self.temp_dir)
def make_tempfile(self, data='', subdir_name=''):
"""Create a file under self.temp_dir and return the path."""
full_tempdir = os.path.join(self.temp_dir, subdir_name)
if not os.path.exists(full_tempdir):
os.makedirs(full_tempdir)
if isinstance(data, str):
data = data.encode()
with tempfile.NamedTemporaryFile(dir=full_tempdir, delete=False) as f:
f.write(data)
return f.name
from httpie.prompt.context import Context
def test_creation():
context = Context('http://example.com')
assert context.url == 'http://example.com'
assert context.options == {}
assert context.headers == {}
assert context.querystring_params == {}
assert context.body_params == {}
assert not context.should_exit
def test_creation_with_longer_url():
context = Context('http://example.com/a/b/c/index.html')
assert context.url == 'http://example.com/a/b/c/index.html'
assert context.options == {}
assert context.headers == {}
assert context.querystring_params == {}
assert context.body_params == {}
assert not context.should_exit
def test_eq():
c1 = Context('http://localhost')
c2 = Context('http://localhost')
assert c1 == c2
c1.options['--verify'] = 'no'
assert c1 != c2
def test_copy():
c1 = Context('http://localhost')
c2 = c1.copy()
assert c1 == c2
assert c1 is not c2
def test_update():
c1 = Context('http://localhost')
c1.headers['Accept'] = 'application/json'
c1.querystring_params['flag'] = '1'
c1.body_params.update({
'name': 'John Doe',
'email': 'john@example.com'
})
c2 = Context('http://example.com')
c2.headers['Content-Type'] = 'text/html'
c2.body_params['name'] = 'John Smith'
c1.update(c2)
assert c1.url == 'http://example.com'
assert c1.headers == {
'Accept': 'application/json',
'Content-Type': 'text/html'
}
assert c1.querystring_params == {'flag': '1'}
assert c1.body_params == {
'name': 'John Smith',
'email': 'john@example.com'
}
def test_spec():
c = Context('http://localhost', spec={
'paths': {
'/users': {
'get': {
'parameters': [
{'name': 'username', 'in': 'path'},
{'name': 'since', 'in': 'query'},
{'name': 'Accept'}
]
}
},
'/orgs/{org}': {
'get': {
'parameters': [
{'name': 'org', 'in': 'path'},
{'name': 'featured', 'in': 'query'},
{'name': 'X-Foo', 'in': 'header'}
]
}
}
}
})
assert c.url == 'http://localhost'
root_children = list(sorted(c.root.children))
assert len(root_children) == 2
assert root_children[0].name == 'orgs'
assert root_children[1].name == 'users'
orgs_children = list(sorted(root_children[0].children))
assert len(orgs_children) == 1
org_children = list(sorted(list(orgs_children)[0].children))
assert len(org_children) == 2
assert org_children[0].name == 'X-Foo'
assert org_children[1].name == 'featured'
users_children = list(sorted(root_children[1].children))
assert len(users_children) == 2
assert users_children[0].name == 'Accept'
assert users_children[1].name == 'since'
def test_override():
"""Parameters can be defined at path level
"""
c = Context('http://localhost', spec={
'paths': {
'/users': {
'parameters': [
{'name': 'username', 'in': 'query'},
{'name': 'Accept', 'in': 'header'}
],
'get': {
'parameters': [
{'name': 'custom1', 'in': 'query'}
]
},
'post': {
'parameters': [
{'name': 'custom2', 'in': 'query'},
]
},
},
'/orgs': {
'parameters': [
{'name': 'username', 'in': 'query'},
{'name': 'Accept', 'in': 'header'}
],
'get': {}
}
}
})
assert c.url == 'http://localhost'
root_children = list(sorted(c.root.children))
# one path
assert len(root_children) == 2
assert root_children[0].name == 'orgs'
assert root_children[1].name == 'users'
orgs_methods = list(sorted(list(root_children)[0].children))
# path parameters are used even if no method parameter
assert len(orgs_methods) == 2
assert next(filter(lambda i: i.name == 'username', orgs_methods), None) is not None
assert next(filter(lambda i: i.name == 'Accept', orgs_methods), None) is not None
users_methods = list(sorted(list(root_children)[1].children))
# path and methods parameters are merged
assert len(users_methods) == 4
assert next(filter(lambda i: i.name == 'username', users_methods), None) is not None
assert next(filter(lambda i: i.name == 'custom1', users_methods), None) is not None
assert next(filter(lambda i: i.name == 'custom2', users_methods), None) is not None
assert next(filter(lambda i: i.name == 'Accept', users_methods), None) is not None
from httpie.prompt.context import Context
from httpie.prompt.context import transform as t
def test_extract_args_for_httpie_main_get():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.querystring_params.update({
'page': '2',
'limit': '10'
})
args = t.extract_args_for_httpie_main(c, method='get')
assert args == ['GET', 'http://localhost/things', 'limit==10', 'page==2',
'Accept:text/html', 'Authorization:ApiKey 1234']
def test_extract_args_for_httpie_main_post():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.options.update({
'--verify': 'no',
'--form': None
})
c.body_params.update({
'full name': 'Jane Doe',
'email': 'jane@example.com'
})
args = t.extract_args_for_httpie_main(c, method='post')
assert args == ['--form', '--verify', 'no',
'POST', 'http://localhost/things',
'email=jane@example.com', 'full name=Jane Doe',
'Accept:text/html', 'Authorization:ApiKey 1234']
def test_extract_raw_json_args_for_httpie_main_post():
c = Context('http://localhost/things')
c.body_json_params.update({
'enabled': True,
'items': ['foo', 'bar'],
'object': {
'id': 10,
'name': 'test'
}
})
args = t.extract_args_for_httpie_main(c, method='post')
assert args == ['POST', 'http://localhost/things',
'enabled:=true', 'items:=["foo", "bar"]',
'object:={"id": 10, "name": "test"}']
def test_format_to_httpie_get():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.querystring_params.update({
'page': '2',
'limit': '10',
'name': ['alice', 'bob bob']
})
output = t.format_to_httpie(c, method='get')
assert output == ("http GET http://localhost/things "
"limit==10 name==alice 'name==bob bob' page==2 "
"Accept:text/html 'Authorization:ApiKey 1234'\n")
def test_format_to_httpie_post():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.options.update({
'--verify': 'no',
'--form': None
})
c.body_params.update({
'full name': 'Jane Doe',
'email': 'jane@example.com'
})
output = t.format_to_httpie(c, method='post')
assert output == ("http --form --verify=no POST http://localhost/things "
"email=jane@example.com 'full name=Jane Doe' "
"Accept:text/html 'Authorization:ApiKey 1234'\n")
def test_format_to_http_prompt_1():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.querystring_params.update({
'page': '2',
'limit': '10'
})
output = t.format_to_http_prompt(c)
assert output == ("cd http://localhost/things\n"
"limit==10\n"
"page==2\n"
"Accept:text/html\n"
"'Authorization:ApiKey 1234'\n")
def test_format_to_http_prompt_2():
c = Context('http://localhost/things')
c.headers.update({
'Authorization': 'ApiKey 1234',
'Accept': 'text/html'
})
c.options.update({
'--verify': 'no',
'--form': None
})
c.body_params.update({
'full name': 'Jane Doe',
'email': 'jane@example.com'
})
output = t.format_to_http_prompt(c)
assert output == ("--form\n"
"--verify=no\n"
"cd http://localhost/things\n"
"email=jane@example.com\n"
"'full name=Jane Doe'\n"
"Accept:text/html\n"
"'Authorization:ApiKey 1234'\n")
def test_format_raw_json_string_to_http_prompt():
c = Context('http://localhost/things')
c.body_json_params.update({
'bar': 'baz',
})
output = t.format_to_http_prompt(c)
assert output == ("cd http://localhost/things\n"
"bar:='\"baz\"'\n")
def test_extract_httpie_options():
c = Context('http://localhost')
c.options.update({
'--verify': 'no',
'--form': None
})
output = t._extract_httpie_options(c, excluded_keys=['--form'])
assert output == ['--verify', 'no']
import json
import os
import sys
import unittest
from unittest.mock import patch, DEFAULT
from click.testing import CliRunner
from requests.models import Response
from .base import TempAppDirTestCase
from httpie.prompt import xdg
from httpie.prompt.context import Context
from httpie.prompt.cli import cli, execute, ExecutionListener
def run_and_exit(cli_args=None, prompt_commands=None):
"""Run http-prompt executable, execute some prompt commands, and exit."""
if cli_args is None:
cli_args = []
# Make sure last command is 'exit'
if prompt_commands is None:
prompt_commands = ['exit']
else:
prompt_commands += ['exit']
# Fool cli() so that it believes we're running from CLI instead of pytest.
# We will restore it at the end of the function.
orig_argv = sys.argv
sys.argv = ['http-prompt'] + cli_args
try:
with patch.multiple('httpie.prompt.cli',
prompt=DEFAULT, execute=DEFAULT) as mocks:
mocks['execute'].side_effect = execute
# prompt() is mocked to return the command in 'prompt_commands' in
# sequence, i.e., prompt() returns prompt_commands[i-1] when it is
# called for the ith time
mocks['prompt'].side_effect = prompt_commands
result = CliRunner().invoke(cli, cli_args)
context = mocks['execute'].call_args[0][1]
return result, context
finally:
sys.argv = orig_argv
class TestCli(TempAppDirTestCase):
def test_without_args(self):
result, context = run_and_exit(['http://localhost'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://localhost')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {})
def test_incomplete_url1(self):
result, context = run_and_exit(['://example.com'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {})
def test_incomplete_url2(self):
result, context = run_and_exit(['//example.com'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {})
def test_incomplete_url3(self):
result, context = run_and_exit(['example.com'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {})
def test_httpie_oprions(self):
url = 'http://example.com'
custom_args = '--auth value: name=foo'
result, context = run_and_exit([url] + custom_args.split())
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {'--auth': 'value:'})
self.assertEqual(context.body_params, {'name': 'foo'})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {})
def test_persistent_context(self):
result, context = run_and_exit(['//example.com', 'name=bob', 'id==10'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {'name': 'bob'})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {'id': ['10']})
result, context = run_and_exit()
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {'name': 'bob'})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {'id': ['10']})
def test_cli_args_bypasses_persistent_context(self):
result, context = run_and_exit(['//example.com', 'name=bob', 'id==10'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {'name': 'bob'})
self.assertEqual(context.headers, {})
self.assertEqual(context.querystring_params, {'id': ['10']})
result, context = run_and_exit(['//example.com', 'sex=M'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(context.url, 'http://example.com')
self.assertEqual(context.options, {})
self.assertEqual(context.body_params, {'sex': 'M'})
self.assertEqual(context.headers, {})
def test_config_file(self):
# Config file is not there at the beginning
config_path = os.path.join(xdg.get_config_dir(), 'config.py')
self.assertFalse(os.path.exists(config_path))
# After user runs it for the first time, a default config file should
# be created
result, context = run_and_exit(['//example.com'])
self.assertEqual(result.exit_code, 0)
self.assertTrue(os.path.exists(config_path))
def test_cli_arguments_with_spaces(self):
result, context = run_and_exit(['example.com', "name=John Doe",
"Authorization:Bearer API KEY"])