"""
Effortless argument parser.

Run Python functions from the command line with ``run(func)``.
"""

import ast
import collections.abc
import contextlib
import functools
import importlib
import inspect
import itertools
import re
import sys
import types
import typing
import warnings
from argparse import (
    REMAINDER, SUPPRESS,
    Action, ArgumentParser, RawTextHelpFormatter,
    ArgumentError, ArgumentTypeError)
from collections import defaultdict, namedtuple, Counter
from enum import Enum
from pathlib import PurePath
from types import MethodType
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

try:
    import importlib.metadata as _im
except ImportError:
    import importlib_metadata as _im
try:
    from pkgutil import resolve_name as _pkgutil_resolve_name
except ImportError:
    from pkgutil_resolve_name import resolve_name as _pkgutil_resolve_name
try:
    from typing import Annotated
except ImportError:
    from typing_extensions import Annotated
try:
    from typing import Literal
except ImportError:
    from typing_extensions import Literal

import docutils.core
from docutils.nodes import NodeVisitor, SkipNode, TextElement
from docutils.parsers.rst.states import Body

try:
    collections.Callable = collections.abc.Callable
    from sphinxcontrib.napoleon import Config, GoogleDocstring, NumpyDocstring
finally:
    if sys.version_info >= (3, 7):
        del collections.Callable

try:
    # colorama is a dependency on Windows to support ANSI escapes (from rst
    # markup).  It is optional on Unices, but can still be useful there as it
    # strips out ANSI escapes when the output is piped.
    from colorama import colorama_text as _colorama_text
except ImportError:
    _colorama_text = getattr(contextlib, 'nullcontext', contextlib.ExitStack)

try:
    __version__ = _im.version('defopt')
except ImportError:
    __version__ = '0+unknown'

__all__ = ['run', 'signature', 'bind', 'bind_known']

_PARAM_TYPES = ['param', 'parameter', 'arg', 'argument', 'key', 'keyword']
_TYPE_NAMES = ['type', 'kwtype']
_LIST_TYPES = [
    list,
    collections.abc.Iterable,
    getattr(collections.abc, "Collection", object()),
    collections.abc.Sequence,
]

_Doc = namedtuple('_Doc', ('first_line', 'text', 'params', 'raises'))
_Param = namedtuple('_Param', ('text', 'type'))
class _Raises(tuple): pass


if hasattr(typing, 'get_args'):
    _ti_get_args = typing.get_args
else:
    import typing_inspect as _ti
    # evaluate=True is default on Py>=3.7.
    _ti_get_args = functools.partial(_ti.get_args, evaluate=True)


if hasattr(typing, 'get_origin'):
    _ti_get_origin = typing.get_origin
else:
    def _ti_get_origin(tp):
        import typing_inspect as ti
        if ti.is_literal_type(tp):  # ti.get_origin returns None for Literals.
            return Literal
        origin = ti.get_origin(tp)
        return {  # Py<3.7.
            typing.List: list,
            typing.Iterable: collections.abc.Iterable,
            getattr(typing, 'Collection', object()):
                getattr(collections.abc, 'Collection', object()),
            typing.Sequence: collections.abc.Sequence,
            typing.Tuple: tuple,
        }.get(origin, origin)


# Modified from Py3.9's version, plus:
# - a fix to bpo#38956 (by omitting the extraneous help string),
# - support for short aliases for --no-foo, by moving negative flag generation
#   to _add_argument (where the negative aliases are available),
# - a hack (_CustomString) to simulate format_usage on Py<3.9 (_CustomString
#   relies on an Py<3.9 implementation detail: the usage string is built using
#   '%s' % option_strings[0] (so there is an additional call to str()) whereas
#   the option invocation help directly joins the strings).


class _CustomString(str):
    def __str__(self):
        return self.action.format_usage()


class _BooleanOptionalAction(Action):
    def __init__(self, option_strings, **kwargs):
        self.negative_option_strings = []  # set by _add_argument
        if option_strings:
            cs = option_strings[0] = _CustomString(option_strings[0])
            cs.action = self
        super().__init__(option_strings, nargs=0, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string in self.option_strings:
            setattr(namespace, self.dest,
                    option_string not in self.negative_option_strings)

    def format_usage(self):
        return ' | '.join(self.option_strings)


class _DefaultList(list):
    """
    Marker type used to determine that a parameter corresponds to a varargs,
    and thus should have its default value hidden.  Varargs are unpacked during
    function call, so the caller won't see this type.
    """


def _check_in_list(_values, **kwargs):
    for k, v in kwargs.items():
        if v not in _values:
            raise ValueError(
                '{!r} must be one of {!r}, not {!r}'.format(k, _values, v))


def _extract_raises(sig):
    raises, = [  # typing_inspect does not allow fetching metadata, e.g. ti#82.
        arg for arg in getattr(sig.return_annotation, '__metadata__', [])
        if isinstance(arg, _Raises)]
    return raises


_unset = 'UNSET'


def _bind_or_bind_known(
    funcs: Union[Callable, List[Callable], Dict[str, Callable]], *,
    parsers: Dict[type, Callable[[str], Any]] = {},
    short: Optional[Dict[str, str]] = None,
    cli_options: Literal['kwonly', 'all', 'has_default'] = _unset,
    strict_kwonly=_unset,
    show_defaults: bool = True,
    show_types: bool = False,
    no_negated_flags: bool = False,
    version: Union[str, None, bool] = None,
    argparse_kwargs: dict = {},
    intermixed: bool = False,
    _known: bool = False,
    argv: Optional[List[str]] = None):
    if strict_kwonly == _unset:
        if cli_options == _unset:
            cli_options = 'kwonly'
    else:
        if cli_options != _unset:
            raise ValueError(
                "cannot pass both 'cli_options' and 'strict_kwonly'")
        warnings.warn(
            'strict_kwonly is deprecated and will be removed in an upcoming '
            'release', DeprecationWarning)
        cli_options = 'kwonly' if strict_kwonly else 'has_default'
    _check_in_list(['kwonly', 'all', 'has_default'], cli_options=cli_options)
    parser = _create_parser(
        funcs, parsers=parsers, short=short, cli_options=cli_options,
        show_defaults=show_defaults, show_types=show_types,
        no_negated_flags=no_negated_flags, version=version,
        argparse_kwargs=argparse_kwargs)
    with _colorama_text():
        if not intermixed:
            if not _known:
                args, rest = parser.parse_args(argv), []
            else:
                args, rest = parser.parse_known_args(argv)
        else:
            if sys.version_info < (3, 7):
                raise RuntimeError("'intermixed' requires Python>=3.7")
            if not _known:
                args, rest = parser.parse_intermixed_args(argv), []
            else:
                args, rest = parser.parse_known_intermixed_args(argv)
    parsed_args = vars(args)
    try:
        func = parsed_args.pop('_func')
    except KeyError:
        # Workaround for http://bugs.python.org/issue9253#msg186387 (and
        # https://bugs.python.org/issue29298 which blocks using required=True).
        parser.error('too few arguments')
    sig = signature(func)
    ba = sig.bind_partial()
    ba.arguments.update(parsed_args)
    call = functools.partial(func, *ba.args, **ba.kwargs)
    raises = _extract_raises(sig)

    @functools.wraps(call)
    def wrapper() -> sig.return_annotation:
        try:
            return call()
        except raises as e:
            sys.exit(e)

    return wrapper, rest


def bind(*args, **kwargs):
    """
    Process command-line arguments and bind arguments.

    This function takes the same parameters as `defopt.run`, but returns a
    `~functools.partial` object which represents the call that ``defopt.run``
    would execute.  In other words, ``call = defopt.bind(...); call()`` is
    equivalent to ``defopt.run(...)``.  Note that no argument needs to be
    passed explicitly to ``call``; everything is handled internally.

    The ``call.func`` attribute is a thin wrapper around one of the functions
    passed to `bind` (to suppress documented raisable exceptions).  The
    original function is accessible as ``call.func.__wrapped__``.

    The ``call.args`` and ``call.keywords`` attributes are set according to the
    command-line arguments.

    This API is provisional and may be adjusted depending on feedback.
    """
    call, rest = _bind_or_bind_known(*args, _known=False, **kwargs)
    assert not rest
    return call


def bind_known(*args, **kwargs):
    """
    Process command-line arguments and bind known arguments.

    This function behaves as `bind`, but returns a pair of 1) the
    `~functools.partial` callable, and 2) a list of unknown command-line
    arguments, as returned by `~argparse.ArgumentParser.parse_known_args`.

    This API is provisional and may be adjusted depending on feedback.
    """
    return _bind_or_bind_known(*args, _known=True, **kwargs)


def run(
    funcs: Union[Callable, List[Callable], Dict[str, Callable]], *,
    parsers: Dict[type, Callable[[str], Any]] = {},
    short: Optional[Dict[str, str]] = None,
    cli_options: Literal['kwonly', 'all', 'has_default'] = _unset,
    strict_kwonly=_unset,
    show_defaults: bool = True,
    show_types: bool = False,
    no_negated_flags: bool = False,
    version: Union[str, None, bool] = None,
    argparse_kwargs: dict = {},
    intermixed: bool = False,
    argv: Optional[List[str]] = None):
    """
    Process command-line arguments and run the given functions.

    *funcs* can be a single callable, which is parsed and run; or it can be
    a list of callables or mappable of strs to callables, in which case each
    one is given a subparser with its name (if *funcs* is a list) or the
    corresponding key (if *funcs* is a mappable), and only the chosen callable
    is run.

    :param funcs:
        Function or functions to process and run.
    :param parsers:
        Dictionary mapping types to parsers to use for parsing function
        arguments.
    :param short:
        Dictionary mapping parameter names (after conversion of underscores to
        dashes) to letters, to use as alternative short flags.  Defaults to
        `None`, which means to generate short flags for any non-ambiguous
        option.  Set to ``{}`` to completely disable short flags.
    :param cli_options:
        The default behavior ('kwonly') is to convert keyword-only parameters
        to command line flags, and non-keyword-only parameters with a default
        to optional positional command line parameters. 'all' turns all
        parameters into command-line flags. 'has_default' turns a parameter
        into a command-line flag if and only if it has a default value.
    :param strict_kwonly:
        Deprecated.  If `False`, all parameters with a default are converted
        into command-line flags. The default behavior (`True`) is to convert
        keyword-only parameters to command line flags, and non-keyword-only
        parameters with a default to optional positional command line
        parameters.
    :param show_defaults:
        Whether parameter defaults are appended to parameter descriptions.
    :param show_types:
        Whether parameter types are appended to parameter descriptions.
    :param no_negated_flags:
        If `False` (default), for any non-positional bool options, two flags
        are created: ``--foo`` and ``--no-foo``. If `True`, the ``--no-foo``
        is not created for every such option that has a default value `False`.
    :param version:
        If a string, add a ``--version`` flag which prints the given version
        string and exits.
        If `True`, the version string is auto-detected by searching for a
        ``__version__`` attribute on the module where the function is defined,
        and its parent packages, if any.  Error out if such a version cannot be
        found, or if multiple callables with different version strings are
        passed.
        If `None` (the default), behave as for `True`, but don't add a
        ``--version`` flag if no version string can be autodetected.
        If `False`, do not add a ``--version`` flag.
    :param argparse_kwargs:
        A mapping of keyword arguments that will be passed to the
        `~argparse.ArgumentParser` constructor.
    :param intermixed:
        Whether to use `~argparse.ArgumentParser.parse_intermixed_args` to
        parse the command line.  Intermixed parsing imposes many restrictions,
        listed in the `argparse` documentation.
    :param argv:
        Command line arguments to parse (default: ``sys.argv[1:]``).
    :return:
        The value returned by the function that was run.
    """
    call = bind(
        funcs, parsers=parsers, short=short, cli_options=cli_options,
        strict_kwonly=strict_kwonly, show_defaults=show_defaults,
        show_types=show_types, no_negated_flags=no_negated_flags,
        version=version, argparse_kwargs=argparse_kwargs,
        intermixed=intermixed, argv=argv)
    if not _extract_raises(inspect.signature(call, follow_wrapped=False)):
        call = call.__wrapped__  # Suppress a trivial traceback frame.
    return call()


def _recurse_functions(funcs, subparsers):
    if not isinstance(funcs, collections.abc.Mapping):
        # If this iterable is not a maping, then convert it to one using the
        # function name itself as the key, but replacing _ with -.
        try:
            funcs = {func.__name__.replace('_', '-'): func for func in funcs}
        except AttributeError as exc:
            # Do not allow a mapping inside of a list
            raise ValueError(
                'use dictionaries (mappings) for nesting; other iterables may '
                'only contain functions (callables)'
            ) from exc

    for name, func in funcs.items():
        if callable(func):
            # If this item is callable, then add it to the current
            # subparser using this name.
            subparser = subparsers.add_parser(
                name,
                formatter_class=RawTextHelpFormatter,
                help=_parse_docstring(inspect.getdoc(func)).first_line)
            yield func, subparser
        else:
            # If this item is not callable, then add this name as a new
            # subparser and recurse the the items.
            nestedsubparser = subparsers.add_parser(name)
            nestedsubparsers = nestedsubparser.add_subparsers()
            yield from _recurse_functions(func, nestedsubparsers)


def _create_parser(
        funcs, *,
        parsers={},
        short=None,
        cli_options='kwonly',
        show_defaults=True,
        show_types=False,
        no_negated_flags=False,
        version=None,
        argparse_kwargs={}):
    parser = ArgumentParser(
        **{**{'formatter_class': RawTextHelpFormatter}, **argparse_kwargs})
    version_sources = []
    if callable(funcs):
        _populate_parser(funcs, parser, parsers, short, cli_options,
                         show_defaults, show_types, no_negated_flags)
        version_sources.append(funcs)
    else:
        subparsers = parser.add_subparsers()
        for func, subparser in _recurse_functions(funcs, subparsers):
            _populate_parser(func, subparser, parsers, short, cli_options,
                             show_defaults, show_types, no_negated_flags)
            version_sources.append(func)
    if isinstance(version, str):
        version_string = version
    elif version is None or version:
        version_string = _get_version(version_sources)
        if version and version_string is None:
            raise ValueError('failed to autodetect version string')
    else:
        version_string = None
    if version_string is not None:
        parser.add_argument(
            '{0}{0}version'.format(parser.prefix_chars[0]),
            action='version', version=version_string)
    return parser


def _get_version(funcs):
    versions = {v for v in map(_get_version1, funcs) if v is not None}
    return versions.pop() if len(versions) == 1 else None


def _get_version1(func):
    module_name = getattr(func, '__module__', None)
    if not module_name:
        return
    if module_name == '__main__':
        f_globals = getattr(func, '__globals__', {})
        if f_globals.get('__spec__'):
            module_name = f_globals['__spec__'].name
        else:
            return f_globals.get('__version__')
    while True:
        try:
            return importlib.import_module(module_name).__version__
        except AttributeError:
            if '.' not in module_name:
                return
            module_name, _ = module_name.rsplit('.', 1)


class Parameter(inspect.Parameter):
    __slots__ = (*inspect.Parameter.__slots__, '_doc')
    doc = property(lambda self: self._doc)

    def __init__(self, *args, doc=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._doc = doc

    def replace(self, *, doc=inspect._void, **kwargs):
        copy = super().replace(**kwargs)
        copy._doc = self._doc if doc is inspect._void else doc
        return copy


def signature(func: Callable):
    """
    Return an enhanced signature for ``func``.

    This function behaves similarly to `inspect.signature`, with the following
    differences:

    - Private parameters (starting with an underscore) are not listed.
    - Parameter types are also read from ``func``'s docstring (if a parameter's
      type is specified both in the signature and the docstring, both types
      must match).
    - The docstring for each parameter is available as the
      `~inspect.Parameter`'s ``.doc`` attribute (in fact, a subclass of
      `~inspect.Parameter` is used).
    - The return type is `~typing.Annotated` with the documented raisable
      exception types, in wrapped in a private tuple subclass.
    """
    orig_sig = inspect.signature(func)
    orig_params = orig_sig.parameters.values()
    doc = _parse_docstring(inspect.getdoc(func))
    parameters = []
    for param in orig_params:
        if param.name.startswith('_'):
            if param.default is param.empty:
                raise ValueError(
                    'parameter {} of {}{} is private but has no default'
                    .format(param.name, func.__name__, orig_sig))
        else:
            parameters.append(Parameter(
                name=param.name, kind=param.kind, default=param.default,
                annotation=_get_type(func, param.name),
                doc=doc.params.get(param.name, _Param(None, None)).text))
    exc_types = _Raises(_get_type_from_doc(name, func.__globals__)
                        for name in doc.raises)
    return_annotation = Annotated[orig_sig.return_annotation, exc_types]
    return orig_sig.replace(
        parameters=parameters, return_annotation=return_annotation)


def _populate_parser(func, parser, parsers, short, cli_options,
                     show_defaults, show_types, no_negated_flags):
    sig = signature(func)
    doc = _parse_docstring(inspect.getdoc(func))
    parser.description = doc.text

    positionals = {
        name for name, param in sig.parameters.items()
        if ((cli_options == 'kwonly' or
             (param.default is param.empty and cli_options == 'has_default'))
            and not _is_list_like(param.annotation)
            and not _is_optional_list_like(param.annotation)
            and param.kind != param.KEYWORD_ONLY)}
    if short is None:
        count_initials = Counter(name[0] for name in sig.parameters
                                 if name not in positionals)
        if parser.add_help:
            count_initials['h'] += 1
        short = {name.replace('_', '-'): name[0] for name in sig.parameters
                 if name not in positionals and count_initials[name[0]] == 1}

    actions = []
    for name, param in sig.parameters.items():
        kwargs = {}
        if param.doc is not None:
            kwargs['help'] = param.doc.replace('%', '%%')
        type_ = param.annotation
        if param.kind == param.VAR_KEYWORD:
            raise ValueError('**kwargs not supported')
        hasdefault = param.default is not param.empty
        default = param.default if hasdefault else SUPPRESS
        required = not hasdefault and param.kind != param.VAR_POSITIONAL
        positional = name in positionals
        if type_ in [bool, typing.Optional[bool]] and not positional:
            action = ('store_true'
                      if no_negated_flags and default in [False, None]
                      else _BooleanOptionalAction)  # --name/--no-name
            actions.append(_add_argument(
                parser, name, short, action=action, default=default,
                required=required,  # Always False if `default is False`.
                **kwargs))  # Add help if available.
            continue
        # Always set a default, even for required parameters, so that we can
        # later (ab)use default == SUPPRESS (!= None) to detect required
        # parameters.
        kwargs['default'] = default
        if positional:
            kwargs['_positional'] = True
            if param.default is not param.empty:
                kwargs['nargs'] = '?'
            if param.kind == param.VAR_POSITIONAL:
                kwargs['nargs'] = '*'
                kwargs['default'] = _DefaultList()
        else:
            kwargs['required'] = required

        # If the type is an Optional container, extract only the container.
        union_args = _ti_get_args(type_) if _is_union_type(type_) else []
        if any(_is_container(subtype) for subtype in union_args):
            non_none = [arg for arg in union_args if arg is not type(None)]
            if len(non_none) != 1:
                raise ValueError('unsupported union including container type: '
                                 '{}'.format(type_))
            type_, = non_none

        if _is_list_like(type_):
            type_, = _ti_get_args(type_)
            kwargs['nargs'] = '*'
            if param.kind == param.VAR_POSITIONAL:
                kwargs['action'] = 'append'
                kwargs['default'] = _DefaultList()
        member_types = None

        if isinstance(type_, type) and issubclass(type_, Enum):
            # Enums must be checked first to handle enums-of-namedtuples.
            kwargs['type'] = _get_parser(type_, parsers)
            kwargs['metavar'] = '{' + ','.join(type_.__members__) + '}'
        elif _ti_get_origin(type_) is tuple:
            member_types = _ti_get_args(type_)
            num_members = len(member_types)
            # Variable-length tuples of homogenous type are specified like
            # Tuple[int, ...]
            if num_members == 2 and member_types[1] is Ellipsis:
                kwargs['nargs'] = '*'
                kwargs['action'] = _make_store_tuple_action_class(
                    tuple, member_types, parsers, is_variable_length=True)
            elif type(None) in union_args and parsers.get(type(None)):
                raise ValueError('Optional tuples and NoneType parsers cannot '
                                 'be used together due to ambiguity')
            else:
                kwargs['nargs'] = num_members
                kwargs['action'] = _make_store_tuple_action_class(
                    tuple, member_types, parsers)
        elif (isinstance(type_, type) and issubclass(type_, tuple)
              and hasattr(type_, '_fields')):
            # Before Py3.6, `_field_types` does not preserve order, so retrieve
            # the order from `_fields`.
            hints = typing.get_type_hints(type_)
            member_types = tuple(hints[field] for field in type_._fields)
            kwargs['nargs'] = len(member_types)
            kwargs['action'] = _make_store_tuple_action_class(
                type_, member_types, parsers)
            if not positional:  # http://bugs.python.org/issue14074
                kwargs['metavar'] = type_._fields
        else:
            kwargs['type'] = _get_parser(type_, parsers)
            if _ti_get_origin(type_) is Literal:
                kwargs['metavar'] = (
                    '{' + ','.join(map(str, _ti_get_args(type_))) + '}')
        actions.append(_add_argument(parser, name, short, **kwargs))
    for action in actions:
        _update_help_string(
            action, show_defaults=show_defaults, show_types=show_types)

    parser.set_defaults(_func=func)


def _add_argument(parser, name, short, _positional=False, **kwargs):
    negative_option_strings = []
    if _positional:
        args = [name]
    else:
        prefix_char = parser.prefix_chars[0]
        name = name.replace('_', '-')
        args = [prefix_char * 2 + name]
        if name in short:
            args.insert(0, prefix_char + short[name])
        if kwargs.get('action') == _BooleanOptionalAction:
            no_name = 'no-' + name
            if no_name in short:
                args.append(prefix_char + short[no_name])
                negative_option_strings.append(args[-1])
            args.append(prefix_char * 2 + no_name)
            negative_option_strings.append(args[-1])
    action = parser.add_argument(*args, **kwargs)
    if negative_option_strings:
        action.negative_option_strings = negative_option_strings
    return action


def _update_help_string(action, *, show_defaults, show_types):
    action_help = action.help or ''
    info = []
    if (show_types
            and action.type is not None
            and action.type.func not in [_make_enum_parser,
                                         _make_literal_parser]
            and '%(type)' not in action_help):
        info.append('type: %(type)s')
    if (show_defaults
            and action.const is not False  # i.e. action='store_false'.
            and not isinstance(action.default, _DefaultList)
            and '%(default)' not in action_help
            and action.default is not SUPPRESS):
        info.append(
            'default: {}'.format(action.default.name.replace('%', '%%'))
            if action.type is not None
               and action.type.func is _make_enum_parser
               and isinstance(action.default, action.type.args)
            else 'default: %(default)s')
    parts = [action.help, '({})'.format(', '.join(info)) if info else '']
    action.help = '\n'.join(filter(None, parts)) or ''


def _is_list_like(type_):
    return _ti_get_origin(type_) in _LIST_TYPES


def _is_container(type_):
    return _ti_get_origin(type_) in {*_LIST_TYPES, tuple}


def _is_union_type(type_):
    return _ti_get_origin(type_) in {Union, getattr(types, 'UnionType', '')}


def _is_optional_list_like(type_):
    # Assume a union with a list subtype is actually Optional[list[...]]
    # because this condition is enforced in other places
    return (_is_union_type(type_)
            and any(_is_list_like(subtype) for subtype in _ti_get_args(type_)))


def _get_type(func, name):
    """
    Retrieve a type from either documentation or annotations.

    If both are specified, they must agree exactly.
    """
    doc = _parse_docstring(inspect.getdoc(func))
    doc_type = doc.params.get(name, _Param(None, None)).type
    if doc_type is not None:
        doc_type = _get_type_from_doc(doc_type, func.__globals__)

    hints = typing.get_type_hints(func)
    try:
        hint = hints[name]
    except KeyError:
        hint_type = None
    else:
        param = inspect.signature(func).parameters[name]
        if (param.default is None
                and param.annotation != hint
                and Optional[param.annotation] == hint):
            # `f(x: tuple[int, int] = None)` means we support a tuple, but not
            # None (to constrain the number of arguments).
            hint = param.annotation
        hint_type = _get_type_from_hint(hint)

    chosen = [x is not None for x in [doc_type, hint_type]]
    if not any(chosen):
        raise ValueError('no type found for parameter {}'.format(name))
    if all(chosen) and doc_type != hint_type:
        raise ValueError('conflicting types found for parameter {}: {}, {}'
                         .format(name, doc.params[name].type, hint.__name__))
    return doc_type or hint_type


def _get_type_from_doc(name, globalns):
    if ' or ' in name:
        subtypes = [_get_type_from_doc(part, globalns)
                    for part in name.split(' or ')]
        if any(map(_is_list_like, subtypes)) and None not in subtypes:
            raise ValueError(
                'unsupported union including container type: {}'.format(name))
        return Union[tuple(subtype for subtype in subtypes)]
    if sys.version_info < (3, 9):  # Support "list[type]", "tuple[type]".
        globalns = {**globalns, 'tuple': Tuple, 'list': List}
    return _get_type_from_hint(eval(name, globalns))


def _get_type_from_hint(hint):
    if _is_list_like(hint):
        [type_] = _ti_get_args(hint)
        return List[type_]
    return hint


def _passthrough_role(
        name, rawtext, text, lineno, inliner, options={}, content=[]):
    return [TextElement(rawtext, text)], []


@contextlib.contextmanager
def _sphinx_common_roles():
    # See "Cross-referencing Python objects" section of
    # http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
    roles = [
        'mod', 'func', 'data', 'const', 'class', 'meth', 'attr', 'exc', 'obj']
    # No public unregistration API :(  Also done by sphinx.
    role_map = docutils.parsers.rst.roles._roles
    for role in roles:
        role_map[role] = role_map['py:' + role] = _passthrough_role
    try:
        yield
    finally:
        for role in roles:
            role_map.pop(role)
            role_map.pop('py:' + role)


@functools.lru_cache()
def _parse_docstring(doc):
    """Extract documentation from a function's docstring."""
    if doc is None:
        return _Doc('', '', {}, [])

    # Convert Google- or Numpy-style docstrings to RST.
    # (Should do nothing if not in either style.)
    # use_ivar avoids generating an unhandled .. attribute:: directive for
    # Attribute blocks, preferring a benign :ivar: field.
    cfg = Config(napoleon_use_ivar=True)
    doc = str(GoogleDocstring(doc, cfg))
    doc = str(NumpyDocstring(doc, cfg))

    with _sphinx_common_roles():
        tree = docutils.core.publish_doctree(
            # Disable syntax highlighting, as 1) pygments is not a dependency
            # 2) we don't render with colors and 3) SH breaks the assumption
            # that literal blocks contain a single text element.
            doc, settings_overrides={'syntax_highlight': 'none'})

    class Visitor(NodeVisitor):
        optional = [
            'document', 'docinfo',
            'field_list', 'field_body',
            'literal', 'problematic',
            # Introduced by our custom passthrough handlers, but the Visitor
            # will recurse into the inner text node by itself.
            'TextElement',
        ]

        def __init__(self, document):
            super().__init__(document)
            self.paragraphs = []
            self.start_lines = []
            self.params = defaultdict(dict)
            self.raises = []
            self._current_paragraph = None
            self._indent_iterator_stack = []
            self._indent_stack = []

        def _do_nothing(self, node):
            pass

        def visit_paragraph(self, node):
            self.start_lines.append(node.line)
            self._current_paragraph = []

        def depart_paragraph(self, node):
            text = ''.join(self._current_paragraph)
            text = ''.join(self._indent_stack) + text
            self._indent_stack = [
                ' ' * len(item) for item in self._indent_stack]
            text = text.replace('\n', '\n' + ''.join(self._indent_stack))
            self.paragraphs.append(text)
            self._current_paragraph = None

        visit_block_quote = visit_doctest_block = visit_paragraph
        depart_block_quote = depart_doctest_block = depart_paragraph

        def visit_Text(self, node):
            self._current_paragraph.append(node)

        depart_Text = _do_nothing

        visit_reference = depart_reference = _do_nothing

        def visit_target(self, node):
            if self._current_paragraph is None:
                raise SkipNode

            if node.get('refuri'):
                self._current_paragraph.append(" ({})".format(node['refuri']))
            else:
                self._current_paragraph.append(node.astext())
            raise SkipNode

        def visit_emphasis(self, node):
            self._current_paragraph.append('\033[3m')  # *foo*: italic

        def visit_strong(self, node):
            self._current_paragraph.append('\033[1m')  # **foo**: bold

        def visit_title_reference(self, node):
            self._current_paragraph.append('\033[4m')  # `foo`: underlined

        def _depart_markup(self, node):
            self._current_paragraph.append('\033[0m')

        depart_emphasis = depart_strong = depart_title_reference = \
            _depart_markup

        def visit_rubric(self, node):
            self.visit_paragraph(node)

        def depart_rubric(self, node):
            # Style consistent with "usage:", "positional arguments:", etc.
            self._current_paragraph[:] = [
                (t.lower() if t == t.title() else t) + ':'
                for t in self._current_paragraph]
            self.depart_paragraph(node)

        def visit_literal_block(self, node):
            text, = node
            self.start_lines.append(node.line)
            self.paragraphs.append(
                re.sub('^|\n', r'\g<0>    ', text))  # indent
            raise SkipNode

        def visit_bullet_list(self, node):
            self._indent_iterator_stack.append(
                (node['bullet'] + ' ' for _ in range(len(node))))

        def depart_bullet_list(self, node):
            self._indent_iterator_stack.pop()

        def visit_enumerated_list(self, node):
            enumtype = node['enumtype']
            fmt = {('(', ')'): 'parens',
                   ('', ')'): 'rparen',
                   ('', '.'): 'period'}[node['prefix'], node['suffix']]
            start = node.get('start', 1)
            enumerators = [Body(None).make_enumerator(i, enumtype, fmt)[0]
                           for i in range(start, start + len(node))]
            width = max(map(len, enumerators))
            enumerators = [enum.ljust(width) for enum in enumerators]
            self._indent_iterator_stack.append(iter(enumerators))

        def depart_enumerated_list(self, node):
            self._indent_iterator_stack.pop()

        def visit_list_item(self, node):
            self._indent_stack.append(next(self._indent_iterator_stack[-1]))

        def depart_list_item(self, node):
            self._indent_stack.pop()

        def visit_field(self, node):
            field_name_node, field_body_node = node
            field_name, = field_name_node
            parts = field_name.split()
            if len(parts) == 2:
                doctype, name = parts
                # docutils>=0.16 represents \* as \0* in the doctree.
                name = name.lstrip('*\0')
            elif len(parts) == 3:
                doctype, type_, name = parts
                name = name.lstrip('*\0')
                if doctype not in _PARAM_TYPES:
                    raise SkipNode
                if 'type' in self.params[name]:
                    raise ValueError('type defined twice for {}'.format(name))
                self.params[name]['type'] = type_
            else:
                raise SkipNode
            if doctype in _PARAM_TYPES:
                doctype = 'param'
            if doctype in _TYPE_NAMES:
                doctype = 'type'
            if doctype in ['param', 'type'] and doctype in self.params[name]:
                raise ValueError(
                    '{} defined twice for {}'.format(doctype, name))
            visitor = Visitor(self.document)
            field_body_node.walkabout(visitor)
            if doctype in ['param', 'type']:
                self.params[name][doctype] = ''.join(visitor.paragraphs)
            elif doctype in ['raises']:
                self.raises.append(name)
            raise SkipNode

        def visit_comment(self, node):
            self.paragraphs.append(comment_token)
            # Comments report their line as the *end* line of the comment.
            self.start_lines.append(
                node.line - node.children[0].count('\n') - 1)
            raise SkipNode

        def visit_system_message(self, node):
            raise SkipNode

    comment_token = object()
    visitor = Visitor(tree)
    tree.walkabout(visitor)

    tuples = {name: _Param(values.get('param'), values.get('type'))
              for name, values in visitor.params.items()}
    if visitor.paragraphs:
        text = []
        for start, paragraph, next_start in zip(
                visitor.start_lines,
                visitor.paragraphs,
                visitor.start_lines[1:] + [0]):
            if paragraph is comment_token:
                continue
            text.append(paragraph)
            # Insert two newlines to separate paragraphs by a blank line.
            # Actually, paragraphs may or may not already have a trailing
            # newline (e.g. text paragraphs do but literal blocks don't) but
            # argparse will strip extra newlines anyways.  This means that
            # extra blank lines in the original docstring will be stripped, but
            # this is less ugly than having a large number of extra blank lines
            # arising e.g. from skipped info fields (which are not rendered).
            # This means that list items are always separated by blank lines,
            # which is an acceptable tradeoff for now.
            text.append('\n\n')
        parsed = _Doc(text[0], ''.join(text), tuples, visitor.raises)
    else:
        parsed = _Doc('', '', tuples, visitor.raises)
    return parsed


def _get_parser(type_, parsers):
    if type_ in parsers:  # Not catching KeyError, to avoid exception chaining.
        parser = functools.partial(parsers[type_])
    elif (type_ in [str, int, float]
          or isinstance(type_, type) and issubclass(type_, PurePath)):
        parser = functools.partial(type_)
    elif type_ == bool:
        parser = functools.partial(_parse_bool)
    elif type_ == slice:
        parser = functools.partial(_parse_slice)
    elif type_ == type(None):
        parser = functools.partial(_parse_none)
    elif type_ == list:
        raise ValueError('unable to parse list (try list[type])')
    elif isinstance(type_, type) and issubclass(type_, Enum):
        parser = _make_enum_parser(type_)
    elif _is_constructible_from_str(type_):
        parser = functools.partial(type_)
    elif _is_union_type(type_):
        args = _ti_get_args(type_)
        if type(None) in args:
            # If None is in the Union, parse it first.  This only matters if
            # there's a custom parser for None, in which case the user should
            # normally have picked values that they *want* to be parsed as
            # None as opposed to anything else, e.g. strs, even if that was
            # possible.
            args = (type(None),
                    *[arg for arg in args if arg is not type(None)])
        parser = _make_union_parser(
            type_, [_get_parser(arg, parsers) for arg in args])
    elif _ti_get_origin(type_) is Literal:
        args = _ti_get_args(type_)
        parser = _make_literal_parser(
            type_, [_get_parser(type(arg), parsers) for arg in args])
    else:
        raise Exception('no parser found for type {}'.format(
            # typing types have no __name__.
            getattr(type_, '__name__', repr(type_))))
    # Set the name that the user expects to see in error messages (we always
    # return a temporary partial object so it's safe to set its __name__).
    # Unions and Literals don't have a __name__, but their str is fine.
    parser.__name__ = getattr(type_, '__name__', str(type_))
    return parser


def _parse_bool(string):
    if string.lower() in ['t', 'true', '1']:
        return True
    elif string.lower() in ['f', 'false', '0']:
        return False
    else:
        raise ValueError('{!r} is not a valid boolean string'.format(string))


def _parse_slice(string):
    slices = []

    class SliceVisitor(ast.NodeVisitor):
        def visit_Slice(self, node):
            start = ast.literal_eval(node.lower) if node.lower else None
            stop = ast.literal_eval(node.upper) if node.upper else None
            step = ast.literal_eval(node.step) if node.step else None
            slices.append(slice(start, stop, step))

    try:
        SliceVisitor().visit(ast.parse('_[{}]'.format(string)))
        sl, = slices
    except (SyntaxError, ValueError):
        raise ValueError('{!r} is not a valid slice string'.format(string))
    return sl


def _parse_none(string):
    raise ValueError('no string can be converted to None')


def _make_enum_parser(enum, value=None):
    if value is None:
        return functools.partial(_make_enum_parser, enum)
    try:
        return enum[value]
    except KeyError:
        raise ArgumentTypeError(
            'invalid choice: {!r} (choose from {})'.format(
                value, ', '.join(map(repr, enum.__members__))))


def _is_constructible_from_str(type_):
    try:
        sig = signature(type_)
        (argname, _), = sig.bind(object()).arguments.items()
    except TypeError:  # Can be raised by signature() or Signature.bind().
        return False
    except ValueError:
        # No relevant info in signature; continue below to also look in
        # `type_.__init__`, in the case where type_ is indeed a type.
        pass
    else:
        if sig.parameters[argname].annotation is str:
            return True
    if isinstance(type_, type):
        # signature() first checks __new__, if it is present.
        # `MethodType(type_.__init__, object())` binds the first parameter of
        # `__init__` -- similarly to `__init__.__get__(object(), type_)`, but
        # the latter can fail for types implemented in C (which may not support
        # binding arbitrary objects).
        return _is_constructible_from_str(MethodType(type_.__init__, object()))
    return False


def _make_union_parser(union, parsers, value=None):
    if value is None:
        return functools.partial(_make_union_parser, union, parsers)
    for p in parsers:
        try:
            return p(value)
        except (ValueError, ArgumentTypeError):
            pass
    raise ValueError(
        '{} could not be parsed as any of {}'.format(value, union))


def _make_literal_parser(literal, parsers, value=None):
    if value is None:
        return functools.partial(_make_literal_parser, literal, parsers)
    for arg, p in zip(_ti_get_args(literal), parsers):
        try:
            if p(value) == arg:
                return arg
        except ValueError:
            pass
    raise ArgumentTypeError(
        'invalid choice: {!r} (choose from {})'.format(
            value, ', '.join(map(repr, map(str, _ti_get_args(literal))))))


def _make_store_tuple_action_class(
    tuple_type, member_types, parsers, *, is_variable_length=False):
    if is_variable_length:
        parsers = itertools.repeat(_get_parser(member_types[0], parsers))
    else:
        parsers = [_get_parser(arg, parsers) for arg in member_types]

    class _StoreTupleAction(Action):
        def __call__(self, parser, namespace, values, option_string=None):
            try:
                value = tuple(p(value) for p, value in zip(parsers, values))
            except ArgumentTypeError as exc:
                raise ArgumentError(self, str(exc))
            if tuple_type is not tuple:
                value = tuple_type(*value)
            setattr(namespace, self.dest, value)

    return _StoreTupleAction


if __name__ == '__main__':
    def main(argv=None):
        parser = ArgumentParser()
        parser.add_argument(
            'function',
            help='package.name.function_name or package.name:function_name')
        parser.add_argument('args', nargs=REMAINDER)
        args = parser.parse_args(argv)
        func = _pkgutil_resolve_name(args.function)
        argparse_kwargs = (
            {'prog': ' '.join(sys.argv[:2])} if argv is None else {})
        retval = run(func, argv=args.args, argparse_kwargs=argparse_kwargs)
        sys.displayhook(retval)

    main()