httpie-cli/httpie/cli/options.py
Batuhan Taskaya f9b5c2f696
Man page fixes (#1364)
- Highlighting for options (-x, --x) now doesn't strip the prefix (may be whitespace).
- Escape sequences are now cross-platform compatible (directly taken by groff/troff [man's renderer])
- Now we check for the section before displaying the man pages.
- On MacOS, there is HTTP(n) which is different from our HTTP(1). This used to conflict with it, and we showed the wrong page. Now we specifically ask foir HTTP(1).
- Errors that might happen (e.g non executable man command) is now suppressed. So in the worst case (if anything regarding man execution goes wrong), we'll always display the manual.
- Docs for man pages.
- HTTPie man pages.
- Epilog for the man pages (see also)
- Auto-generated comments.
2022-05-05 11:17:37 -07:00

250 lines
7.6 KiB
Python

import argparse
import textwrap
import typing
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, Optional, Dict, List, Tuple, Type, TypeVar
from httpie.cli.argparser import HTTPieArgumentParser
from httpie.cli.utils import Manual, LazyChoices
class Qualifiers(Enum):
OPTIONAL = auto()
ZERO_OR_MORE = auto()
ONE_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()
}
def drop_keys(
configuration: Dict[str, Any], key_blacklist: Tuple[str, ...]
):
return {
key: value
for key, value in configuration.items()
if key not in key_blacklist
}
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)
man_page_hint: Optional[str] = None
source_file: Optional[str] = None
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())
argument.post_init()
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 post_init(self):
"""Run a bunch of post-init hooks."""
# If there is a short help, then create the longer version from it.
short_help = self.configuration.get('short_help')
if (
short_help
and 'help' not in self.configuration
and self.configuration.get('action') != 'lazy_choices'
):
self.configuration['help'] = f'\n{short_help}\n\n'
def serialize(self, *, isolation_mode: bool = False) -> 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)
short_help = configuration.pop('short_help', None)
nested_options = configuration.pop('nested_options', None)
if action == 'lazy_choices':
choices = LazyChoices(
self.aliases,
**{'dest': None, **configuration},
isolation_mode=isolation_mode
)
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)
description = configuration.get('help')
if description and description is not Qualifiers.SUPPRESS:
result['short_description'] = short_help
result['description'] = description
if nested_options:
result['nested_options'] = nested_options
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
if value is not Qualifiers.SUPPRESS
})
return result
@property
def is_positional(self):
return len(self.aliases) == 0
@property
def is_hidden(self):
return self.configuration.get('help') is Qualifiers.SUPPRESS
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,
Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE
}
ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options')
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.spec = abstract_options
concrete_parser.register('action', 'lazy_choices', LazyChoices)
concrete_parser.register('action', 'manual', Manual)
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,
**drop_keys(map_qualifiers(
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
), ARGPARSE_IGNORE_KEYS)
)
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.ONE_OR_MORE: {'is_optional': False, 'is_variadic': True},
Qualifiers.SUPPRESS: {}
}
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
def parser_to_parser_spec(parser: argparse.ArgumentParser, **kwargs) -> ParserSpec:
"""Take an existing argparse parser, and create a spec from it."""
return ParserSpec(
program=parser.prog,
description=parser.description,
epilog=parser.epilog,
**kwargs
)