mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 09:13:25 +01:00
ff6f1887b0
* Refactor tests to use a text-based standard output. (#1318) * Implement new style `--help` (#1316) * Implement man page generation (#1317) * Implement rich progress bars. (#1324) * Man page deployment & isolation. (#1325) * Remove all unsorted usages in the CLI docs * Implement isolated mode for man page generation * Add a CI job for autogenerated files * Distribute man pages through PyPI * Pin the date for man pages. (#1326) * Hide suppressed arguments from --help/man pages (#1329) * Change download spinner to line (#1328) * Regenerate autogenerated files when pushed against to master. (#1339) * Highlight options (#1340) * Additional man page enhancements (#1341) * Group options by the parent category & highlight -o/--o * Display (and underline) the METAVAR on man pages. * Make help message processing more robust (#1342) * Inherit `help` from `short_help` * Don't mirror short_help directly. * Fixup the serialization * Use `pager` and `man` on `--manual` when applicable (#1343) * Run `man $program` on --manual * Page the output of `--manual` for systems that lack man pages * Improvements over progress bars (separate bar, status line, etc.) (#1346) * Redesign the --help layout. * Make our usage of rich compatible with 9.10.0 * Add `HTTPIE_NO_MAN_PAGES` * Make tests also patch os.get_terminal_size * Generate CLI spec from HTTPie & Man Page Hook (#1354) * Generate CLI spec from HTTPie & add man page hook * Use the full command space for the option headers
236 lines
6.4 KiB
Python
236 lines
6.4 KiB
Python
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()
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def dummy_plugin(interface):
|
|
return interface.make_dummy_plugin()
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def broken_plugin(interface):
|
|
base_plugin = interface.make_dummy_plugin()
|
|
with open(base_plugin.path / (base_plugin.import_name + '.py'), 'a') as stream:
|
|
stream.write('raise ValueError("broken plugin")\n')
|
|
return base_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, cli_mode: bool = True):
|
|
args = list(args)
|
|
if cli_mode:
|
|
args.insert(0, 'cli')
|
|
args.insert(cli_mode, 'plugins')
|
|
|
|
# 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(*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, cli_mode: bool = True):
|
|
response = httpie_plugins(*args, cli_mode=True)
|
|
assert response.exit_status == ExitStatus.SUCCESS
|
|
return response.splitlines()
|
|
return runner
|