You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2026-04-24 19:53:55 +02:00
cmd: Implement httpie plugins interface (#1200)
This commit is contained in:
@@ -5,6 +5,13 @@ import pytest
|
||||
from pytest_httpbin import certs
|
||||
|
||||
from .utils import HTTPBIN_WITH_CHUNKED_SUPPORT_DOMAIN, HTTPBIN_WITH_CHUNKED_SUPPORT
|
||||
from .utils.plugins_cli import ( # noqa
|
||||
dummy_plugin,
|
||||
dummy_plugins,
|
||||
httpie_plugins,
|
||||
httpie_plugins_success,
|
||||
interface,
|
||||
)
|
||||
from .utils.http_server import http_server # noqa
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import pytest
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from tests.utils import httpie
|
||||
from tests.utils.plugins_cli import parse_listing
|
||||
|
||||
|
||||
def test_plugins_installation(httpie_plugins_success, interface, dummy_plugin):
|
||||
lines = httpie_plugins_success('install', dummy_plugin.path)
|
||||
assert lines[0].startswith(
|
||||
f'Installing {dummy_plugin.path}'
|
||||
)
|
||||
assert f'Successfully installed {dummy_plugin.name}-{dummy_plugin.version}' in lines
|
||||
assert interface.is_installed(dummy_plugin.name)
|
||||
|
||||
|
||||
def test_plugins_listing(httpie_plugins_success, interface, dummy_plugin):
|
||||
httpie_plugins_success('install', dummy_plugin.path)
|
||||
data = parse_listing(httpie_plugins_success('list'))
|
||||
|
||||
assert data == {
|
||||
dummy_plugin.name: dummy_plugin.dump()
|
||||
}
|
||||
|
||||
|
||||
def test_plugins_listing_multiple(interface, httpie_plugins_success, dummy_plugins):
|
||||
paths = [plugin.path for plugin in dummy_plugins]
|
||||
httpie_plugins_success('install', *paths)
|
||||
data = parse_listing(httpie_plugins_success('list'))
|
||||
|
||||
assert data == {
|
||||
plugin.name: plugin.dump()
|
||||
for plugin in dummy_plugins
|
||||
}
|
||||
|
||||
|
||||
def test_plugins_uninstall(interface, httpie_plugins_success, dummy_plugin):
|
||||
httpie_plugins_success('install', dummy_plugin.path)
|
||||
httpie_plugins_success('uninstall', dummy_plugin.name)
|
||||
assert not interface.is_installed(dummy_plugin.name)
|
||||
|
||||
|
||||
def test_plugins_listing_after_uninstall(interface, httpie_plugins_success, dummy_plugin):
|
||||
httpie_plugins_success('install', dummy_plugin.path)
|
||||
httpie_plugins_success('uninstall', dummy_plugin.name)
|
||||
|
||||
data = parse_listing(httpie_plugins_success('list'))
|
||||
assert len(data) == 0
|
||||
|
||||
|
||||
def test_plugins_uninstall_specific(interface, httpie_plugins_success):
|
||||
new_plugin_1 = interface.make_dummy_plugin()
|
||||
new_plugin_2 = interface.make_dummy_plugin()
|
||||
target_plugin = interface.make_dummy_plugin()
|
||||
|
||||
httpie_plugins_success('install', new_plugin_1.path, new_plugin_2.path, target_plugin.path)
|
||||
httpie_plugins_success('uninstall', target_plugin.name)
|
||||
|
||||
assert interface.is_installed(new_plugin_1.name)
|
||||
assert interface.is_installed(new_plugin_2.name)
|
||||
assert not interface.is_installed(target_plugin.name)
|
||||
|
||||
|
||||
def test_plugins_installation_failed(httpie_plugins, interface):
|
||||
plugin = interface.make_dummy_plugin(build=False)
|
||||
result = httpie_plugins('install', plugin.path)
|
||||
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert result.stderr.splitlines()[-1].strip().startswith("Can't install")
|
||||
|
||||
|
||||
def test_plugins_uninstall_non_existent(httpie_plugins, interface):
|
||||
plugin = interface.make_dummy_plugin(build=False)
|
||||
result = httpie_plugins('uninstall', plugin.name)
|
||||
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert (
|
||||
result.stderr.splitlines()[-1].strip()
|
||||
== f"Can't uninstall '{plugin.name}': package is not installed"
|
||||
)
|
||||
|
||||
|
||||
def test_plugins_double_uninstall(httpie_plugins, httpie_plugins_success, dummy_plugin):
|
||||
httpie_plugins_success("install", dummy_plugin.path)
|
||||
httpie_plugins_success("uninstall", dummy_plugin.name)
|
||||
|
||||
result = httpie_plugins("uninstall", dummy_plugin.name)
|
||||
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert (
|
||||
result.stderr.splitlines()[-1].strip()
|
||||
== f"Can't uninstall '{dummy_plugin.name}': package is not installed"
|
||||
)
|
||||
|
||||
|
||||
def test_plugins_cli_error_message_without_args():
|
||||
# No arguments
|
||||
result = httpie(no_debug=True)
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert 'usage: ' in result.stderr
|
||||
assert 'specify one of these' in result.stderr
|
||||
assert 'please use the http/https commands:' in result.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'example', [
|
||||
'pie.dev/get',
|
||||
'DELETE localhost:8000/delete',
|
||||
'POST pie.dev/post header:value a=b header_2:value x:=1'
|
||||
]
|
||||
)
|
||||
def test_plugins_cli_error_messages_with_example(example):
|
||||
result = httpie(*example.split(), no_debug=True)
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert 'usage: ' in result.stderr
|
||||
assert f'http {example}' in result.stderr
|
||||
assert f'https {example}' in result.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'example', [
|
||||
'plugins unknown',
|
||||
'plugins unknown.com A:B c=d',
|
||||
'unknown.com UNPARSABLE????SYNTAX',
|
||||
]
|
||||
)
|
||||
def test_plugins_cli_error_messages_invalid_example(example):
|
||||
result = httpie(*example.split(), no_debug=True)
|
||||
assert result.exit_status == ExitStatus.ERROR
|
||||
assert 'usage: ' in result.stderr
|
||||
assert f'http {example}' not in result.stderr
|
||||
assert f'https {example}' not in result.stderr
|
||||
+1
-1
@@ -6,7 +6,7 @@ import pytest_httpbin.certs
|
||||
import requests.exceptions
|
||||
import urllib3
|
||||
|
||||
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from httpie.status import ExitStatus
|
||||
|
||||
from .utils import HTTP_OK, TESTS_ROOT, http
|
||||
|
||||
+47
-5
@@ -7,12 +7,14 @@ import json
|
||||
import tempfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, List
|
||||
from typing import Any, Optional, Union, List, Iterable
|
||||
|
||||
import httpie.core as core
|
||||
import httpie.manager.__main__ as manager
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.config import Config
|
||||
from httpie.context import Environment
|
||||
from httpie.core import main
|
||||
|
||||
|
||||
# pytest-httpbin currently does not support chunked requests:
|
||||
@@ -58,10 +60,10 @@ class MockEnvironment(Environment):
|
||||
stdout_isatty = True
|
||||
is_windows = False
|
||||
|
||||
def __init__(self, create_temp_config_dir=True, **kwargs):
|
||||
def __init__(self, create_temp_config_dir=True, *, stdout_mode='b', **kwargs):
|
||||
if 'stdout' not in kwargs:
|
||||
kwargs['stdout'] = tempfile.TemporaryFile(
|
||||
mode='w+b',
|
||||
mode=f'w+{stdout_mode}',
|
||||
prefix='httpie_stdout'
|
||||
)
|
||||
if 'stderr' not in kwargs:
|
||||
@@ -177,6 +179,46 @@ class ExitStatusError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def normalize_args(args: Iterable[Any]) -> List[str]:
|
||||
return [str(arg) for arg in args]
|
||||
|
||||
|
||||
def httpie(
|
||||
*args,
|
||||
**kwargs
|
||||
) -> StrCLIResponse:
|
||||
"""
|
||||
Run HTTPie manager command with the given
|
||||
args/kwargs, and capture stderr/out and exit
|
||||
status.
|
||||
"""
|
||||
|
||||
env = kwargs.setdefault('env', MockEnvironment())
|
||||
cli_args = ['httpie']
|
||||
if not kwargs.pop('no_debug', False):
|
||||
cli_args.append('--debug')
|
||||
cli_args += normalize_args(args)
|
||||
exit_status = manager.main(
|
||||
args=cli_args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
try:
|
||||
response = StrCLIResponse(env.stdout.read())
|
||||
response.stderr = env.stderr.read()
|
||||
response.exit_status = exit_status
|
||||
response.args = cli_args
|
||||
finally:
|
||||
env.stdout.truncate(0)
|
||||
env.stderr.truncate(0)
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def http(
|
||||
*args,
|
||||
program_name='http',
|
||||
@@ -254,7 +296,7 @@ def http(
|
||||
|
||||
try:
|
||||
try:
|
||||
exit_status = main(args=complete_args, **kwargs)
|
||||
exit_status = core.main(args=complete_args, **kwargs)
|
||||
if '--download' in args:
|
||||
# Let the progress reporter thread finish.
|
||||
time.sleep(.5)
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import secrets
|
||||
import site
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Dict, Tuple
|
||||
from unittest.mock import patch
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.compat import importlib_metadata
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.plugins.manager import (
|
||||
enable_plugins,
|
||||
ENTRY_POINT_CLASSES as CLASSES,
|
||||
)
|
||||
|
||||
|
||||
def make_name() -> str:
|
||||
return 'httpie-' + secrets.token_hex(4)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntryPoint:
|
||||
name: str
|
||||
group: str
|
||||
|
||||
def dump(self) -> Dict[str, str]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plugin:
|
||||
interface: 'Interface'
|
||||
|
||||
name: str = field(default_factory=make_name)
|
||||
version: str = '1.0.0'
|
||||
entry_points: List[EntryPoint] = field(default_factory=list)
|
||||
|
||||
def build(self) -> None:
|
||||
'''
|
||||
Create an installable dummy plugin at the given path.
|
||||
|
||||
It will create a setup.py with the specified entry points,
|
||||
as well as dummy classes in a python module to imitate
|
||||
real plugins.
|
||||
'''
|
||||
|
||||
groups = defaultdict(list)
|
||||
for entry_point in self.entry_points:
|
||||
groups[entry_point.group].append(entry_point.name)
|
||||
|
||||
setup_eps = {
|
||||
group: [
|
||||
f'{name} = {self.import_name}:{name.title()}'
|
||||
for name in names
|
||||
]
|
||||
for group, names in groups.items()
|
||||
}
|
||||
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.path / 'setup.py', 'w') as stream:
|
||||
stream.write(textwrap.dedent(f'''
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='{self.name}',
|
||||
version='{self.version}',
|
||||
py_modules=['{self.import_name}'],
|
||||
entry_points={setup_eps!r},
|
||||
install_requires=['httpie']
|
||||
)
|
||||
'''))
|
||||
|
||||
with open(self.path / (self.import_name + '.py'), 'w') as stream:
|
||||
stream.write('from httpie.plugins import *\n')
|
||||
stream.writelines(
|
||||
f'class {name.title()}({CLASSES[group].__name__}): ...\n'
|
||||
for group, names in groups.items()
|
||||
for name in names
|
||||
)
|
||||
|
||||
def dump(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'version': self.version,
|
||||
'entry_points': [
|
||||
entry_point.dump()
|
||||
for entry_point in self.entry_points
|
||||
]
|
||||
}
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self.interface.path / self.name
|
||||
|
||||
@property
|
||||
def import_name(self) -> str:
|
||||
return self.name.replace('-', '_')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Interface:
|
||||
path: Path
|
||||
environment: Environment
|
||||
|
||||
def get_plugin(self, target: str) -> importlib_metadata.Distribution:
|
||||
with enable_plugins(self.environment.config.plugins_dir):
|
||||
return importlib_metadata.distribution(target)
|
||||
|
||||
def is_installed(self, target: str) -> bool:
|
||||
try:
|
||||
self.get_plugin(target)
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def make_dummy_plugin(self, build=True, **kwargs) -> Plugin:
|
||||
kwargs.setdefault('entry_points', [EntryPoint('test', 'httpie.plugins.auth.v1')])
|
||||
|
||||
plugin = Plugin(self, **kwargs)
|
||||
if build:
|
||||
plugin.build()
|
||||
return plugin
|
||||
|
||||
|
||||
def parse_listing(lines: List[str]) -> Dict[str, Any]:
|
||||
plugins = {}
|
||||
current_plugin = None
|
||||
|
||||
def parse_entry_point(line: str) -> Tuple[str, str]:
|
||||
entry_point, raw_group = line.strip().split()
|
||||
return entry_point, raw_group[1:-1]
|
||||
|
||||
def parse_plugin(line: str) -> Tuple[str, str]:
|
||||
plugin, raw_version = line.strip().split()
|
||||
return plugin, raw_version[1:-1]
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if line[0].isspace():
|
||||
# <indent> $entry_point ($group)
|
||||
assert current_plugin is not None
|
||||
entry_point, group = parse_entry_point(line)
|
||||
plugins[current_plugin]['entry_points'].append({
|
||||
'name': entry_point,
|
||||
'group': group
|
||||
})
|
||||
else:
|
||||
# $plugin ($version)
|
||||
current_plugin, version = parse_plugin(line)
|
||||
plugins[current_plugin] = {
|
||||
'version': version,
|
||||
'entry_points': []
|
||||
}
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def interface(tmp_path):
|
||||
from tests.utils import MockEnvironment
|
||||
|
||||
return Interface(
|
||||
path=tmp_path / 'interface',
|
||||
environment=MockEnvironment(stdout_mode='t')
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def dummy_plugin(interface):
|
||||
return interface.make_dummy_plugin()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def dummy_plugins(interface):
|
||||
# Multiple plugins with different configurations
|
||||
return [
|
||||
interface.make_dummy_plugin(),
|
||||
interface.make_dummy_plugin(
|
||||
version='3.2.0'
|
||||
),
|
||||
interface.make_dummy_plugin(
|
||||
entry_points=[
|
||||
EntryPoint('test_1', 'httpie.plugins.converter.v1'),
|
||||
EntryPoint('test_2', 'httpie.plugins.formatter.v1')
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpie_plugins(interface):
|
||||
from tests.utils import httpie
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
|
||||
def runner(*args):
|
||||
# Prevent installed plugins from showing up.
|
||||
original_plugins = plugin_manager.copy()
|
||||
clean_sys_path = set(sys.path).difference(site.getsitepackages())
|
||||
with patch('sys.path', list(clean_sys_path)):
|
||||
response = httpie('plugins', *args, env=interface.environment)
|
||||
plugin_manager.clear()
|
||||
plugin_manager.extend(original_plugins)
|
||||
return response
|
||||
|
||||
return runner
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpie_plugins_success(httpie_plugins):
|
||||
def runner(*args):
|
||||
response = httpie_plugins(*args)
|
||||
assert response.exit_status == ExitStatus.SUCCESS
|
||||
return response.splitlines()
|
||||
return runner
|
||||
Reference in New Issue
Block a user