This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2025-04-02T16:14:22.606Z

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
  configuration.
- Binary files are not included in this packed representation. Please refer to
  the Repository Structure section for a complete list of file paths, including
  binary files.

Additional Info:
----------------

================================================================
Directory Structure
================================================================
.github/
  workflows/
    pypi-publish.yml
    python-tests.yml
  dependabot.yml
examples/
  async_contaminated.py
  command_dead_simple.py
  command.py
  default.py
  docstring.py
  nested.py
  options.py
  positional_arguments.py
  sub_commands_named.py
  sub_commands.py
glacier/
  __init__.py
  core.py
  docstring.py
  misc.py
tests/
  test_core.py
  test_docstring.py
  utils.py
.gitignore
LICENSE
Makefile
pyproject.toml
README.md

================================================================
Files
================================================================

================
File: .github/workflows/pypi-publish.yml
================
name: pypi-publish

on:
  release:
    types: [published]

jobs:
  pytest:
    name: pytest
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master

      - name: Poetry
        uses: abatilo/actions-poetry@v2
        with:
          python_version: 3.9
          poetry_version: 1.4.2
          working-directory: ./

      - name: Poetry install
        run: poetry install

      - name: Poetry Publish
        run: poetry publish --build -u ${PYPI_USER} -p ${PYPI_PASSWORD}
        env:
          PYPI_USER: ${{ secrets.PYPI_USER }}
          PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}

================
File: .github/workflows/python-tests.yml
================
name: pythontests
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  pytest:
    name: pytest
    strategy:
      max-parallel: 2
      matrix:
        python-version: [3.9]
        poetry-version: [1.4.2]
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Run image
        uses: abatilo/actions-poetry@v2.1.0
        with:
          poetry-version: ${{ matrix.poetry-version }}

      - name: Run pytest
        run: poetry install

      - name: Run pytest
        run: poetry run coverage run --omit='./tests/**/*' --source=. -m pytest -vv --durations=10

      - name: create coverage xml
        if: ${{ github.ref == 'refs/heads/main' && matrix.python-version == 3.9 }}
        run: poetry run coverage xml

      - name: Upload coverage to Codecov
        if: ${{ github.ref == 'refs/heads/main' && matrix.python-version == 3.9 && matrix.ox == 'ubuntu-latest' }}
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: ./coverage.xml
          name: codecov-umbrella
          fail_ci_if_error: false

================
File: .github/dependabot.yml
================
version: 2

updates:
  - package-ecosystem: pub
    directory: /
    schedule:
      interval: weekly

================
File: examples/async_contaminated.py
================
from glacier import glacier


async def main() -> None:
    return


def sub_1() -> None:
    return


async def sub_2() -> None:
    return


if __name__ == '__main__':
    glacier({
        'main': main,
        'sub': [
            sub_1,
            sub_2,
        ],
    })

================
File: examples/command_dead_simple.py
================
from glacier import glacier


def main(name: str, verbose: bool = False) -> None:
    pass


if __name__ == '__main__':
    glacier(main)

================
File: examples/command.py
================
from enum import Enum

from glacier import glacier


class Env(Enum):
    DEV = 'development'
    PROD = 'production'


def main(
    _path: str,
    name: str,
    env: Env,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    Args:
        _path: Positional argument representing the target file path.
        name: Name of this operation.
        env: Specifying this operation is for whether dev or prod.
        verbose: Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(env)
    print(verbose)
    return


if __name__ == '__main__':
    glacier(main)

================
File: examples/default.py
================
from glacier import glacier


def default(verbose: bool = False) -> None:
    print(verbose)


if __name__ == '__main__':
    glacier(default)

================
File: examples/docstring.py
================
from enum import Enum

from glacier import glacier


def main_google(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    Args:
        _path: Positional argument representing the target file path.
        name: Name of this operation.
        verbose: Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return


def main_numpy(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    Parameters
    ----------
    _path: str
        Positional argument representing the target file path.
    name: str
        Name of this operation.
    verbose: bool
        Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return


def main_restructured_text(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    :param _path: Positional argument representing the target file path.
    :param name: Name of this operation.
    :param verbose: Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return


if __name__ == '__main__':
    glacier({
        'google': main_google,
        'numpy': main_numpy,
        'restructured-text': main_restructured_text,
    })

================
File: examples/nested.py
================
from glacier import glacier


def main() -> None:
    return


def sub_1() -> None:
    return


def sub_2() -> None:
    return


if __name__ == '__main__':
    glacier({
        'main': main,
        'sub': [
            sub_1,
            sub_2,
        ],
    })

================
File: examples/options.py
================
from glacier import glacier


def all_options(a: str, b: str, c: str) -> None:
    print(a)
    print(b)
    print(c)


if __name__ == '__main__':
    glacier(all_options)

================
File: examples/positional_arguments.py
================
from glacier import glacier


def all_positional(_a: str, _b: str, _c: str) -> None:
    print(_a)
    print(_b)
    print(_c)


if __name__ == '__main__':
    glacier(all_positional)

================
File: examples/sub_commands_named.py
================
from glacier import glacier


def f1(name: str, verbose: bool = False) -> None:
    pass


def f2(name: str, verbose: bool = False) -> None:
    pass


def f3(name: str, verbose: bool = False) -> None:
    pass


if __name__ == '__main__':
    glacier({
        'run': f1,
        'build': f2,
        'test': f3,
    })

================
File: examples/sub_commands.py
================
from glacier import glacier


def run(name: str, verbose: bool = False) -> None:
    """ Run """
    pass


def build(name: str, verbose: bool = False) -> None:
    """ Build """
    pass


def test(name: str, verbose: bool = False) -> None:
    """ Test """
    return


if __name__ == '__main__':
    glacier([run, build, test])

================
File: glacier/__init__.py
================
from .core import glacier  # noqa

================
File: glacier/core.py
================
import functools
from enum import Enum
from inspect import Parameter, signature
from typing import (Any, Callable, Coroutine, Dict, List, Optional, Type,
                    TypeVar, Union)

import click
try:
    import click_completion
    loads_completion = True
except Exception:
    loads_completion = False
    ...
from click_help_colors import HelpColorsCommand, HelpColorsGroup

from glacier.docstring import (Doc, GoogleParser, NumpyParser, Parser,
                               RestructuredTextParser)
from glacier.misc import coro

"""
# TODO

- [x] Enum support.
- [ ] Parse python docstring to display help.
"""

T = TypeVar('T')


CONTEXT_SETTINGS = dict(
    help_option_names=['-h', '--help'],
    max_content_width=120,
)
DEFAULT_COLOR_OPTIONS = dict(
    help_headers_color='white',
    help_options_color='cyan',
)


GlacierFunction = Union[
    Callable[..., Any],
    Callable[..., Coroutine[Any, Any, Any]],
]


GlacierUnit = Union[
    List[GlacierFunction],
    Dict[str, GlacierFunction],
]


def get_enum_map(f: Callable[..., Any]) -> Dict[str, Dict[str, Any]]:
    sig = signature(f)

    # pick enum from signature
    enum_map: Dict[str, Dict[str, Any]] = {}
    for param in sig.parameters.values():
        if issubclass(param.annotation, Enum):
            enum_class = param.annotation
            enum_map.setdefault(param.name, {})
            for enum_entry in enum_class:
                enum_map[param.name][enum_entry.value] = enum_entry
    return enum_map


def glacier_wrap(
    f: Callable[..., Any],
    enum_map: Dict[str, Dict[str, Any]],
) -> Callable[..., Any]:
    """
    Return the new function which is click-compatible
    (has no enum signature arguments) from the arbitrary glacier compatible
    function
    """

    # Implemented the argument convert logic
    @functools.wraps(f)
    def wrapped(*args: Any, **kwargs: Any) -> Any:
        # convert args and kwargs
        converted_kwargs = {}
        for name, value in kwargs.items():
            if name in enum_map:
                converted_kwargs[name] = enum_map[name][value]
            else:
                converted_kwargs[name] = value

        return f(*args, **converted_kwargs)

    return wrapped


def _get_best_doc(docstring: str, arg_names: List[str]) -> Doc:
    """
    Detect the format of docstring and return best help generated from docstring.
    """
    parser_types: List[Type[Parser]] = [
        GoogleParser,
        NumpyParser,
        RestructuredTextParser,
    ]
    docs = [
        parser_type().parse(docstring=docstring)
        for parser_type in parser_types
    ]
    return max(docs, key=lambda doc: doc.get_matched_arg_count(arg_names))


def _get_click_command(
    f: Callable[..., Any],
    click_group: Optional[click.Group] = None,
) -> click.BaseCommand:
    f = coro(f)

    # Get signature
    sig = signature(f)

    # Get docstring
    docstring = f.__doc__
    if docstring:
        doc = _get_best_doc(
            docstring=docstring,
            arg_names=[param.name for param in sig.parameters.values()],
        )
        f.__doc__ = doc.description
        arg_help_d = {
            arg.name: arg.description
            for arg in doc.args
        }
    else:
        arg_help_d = {}

    # Precauclate Enum mappings
    enum_map = get_enum_map(f)

    # Return new function which interprets custom type such as Enum.
    click_f: Any = glacier_wrap(f, enum_map)

    # Decorate the function reversely.
    for param in reversed(list(sig.parameters.values())):
        if param.name.startswith('_'):
            # Positional argument
            click_f = click.argument(
                param.name,
                type=param.annotation,
                nargs=1,
            )(click_f)
        else:
            # Optional argument
            if param.default == Parameter.empty:
                common_kwargs = dict(
                    required=True,
                    help=arg_help_d.get(param.name, ''),
                )
            else:
                common_kwargs = dict(
                    default=param.default,
                    help=arg_help_d.get(param.name, ''),
                )
            if param.annotation == bool:
                # Boolean flag
                click_f = click.option(  # type: ignore
                    '--' + param.name.replace('_', '-'),
                    is_flag=True,
                    type=bool,
                    **common_kwargs,
                )(click_f)
            elif param.annotation == str or param.annotation == int:
                # string or boolean option
                click_f = click.option(  # type: ignore
                    '--' + param.name.replace('_', '-'),
                    type=param.annotation,
                    **common_kwargs,
                )(click_f)
            elif issubclass(param.annotation, Enum):
                click_f = click.option(  # type: ignore
                    '--' + param.name.replace('_', '-'),
                    type=click.Choice(list(enum_map[param.name].keys())),
                    **common_kwargs,
                )(click_f)

    if click_group:
        return click_group.command(  # type: ignore
            cls=HelpColorsCommand,
            context_settings=CONTEXT_SETTINGS,
            **DEFAULT_COLOR_OPTIONS,  # type: ignore
        )(click_f)
    else:
        return click.command(  # type: ignore
            cls=HelpColorsCommand,
            context_settings=CONTEXT_SETTINGS,
            **DEFAULT_COLOR_OPTIONS,  # type: ignore
        )(click_f)


def rename(
    f: Callable[..., T],
    name: str,
) -> Callable[..., T]:
    @functools.wraps(f)
    def wrapped(*args: Any, **kwargs: Any) -> T:
        return f(*args, **kwargs)
    wrapped.__name__ = name
    return wrapped


if loads_completion:
    @click.option(
        '-i',
        '--case-insensitive/--no-case-insensitive',
        help="Case insensitive completion",
    )
    @click.argument(
        'shell',
        required=False,
        type=click_completion.DocumentedChoice(click_completion.core.shells),
    )
    def show_completion(shell: str, case_insensitive: bool) -> None:
        """Show the click-completion-command completion code"""
        extra_env = {
            '_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE': 'ON'
        } if case_insensitive else {}
        click.echo(click_completion.core.get_code(shell, extra_env=extra_env))


def glacier_group(
    f: Union[
        List[GlacierFunction],
        Dict[str, Union[GlacierFunction, GlacierUnit]],
    ],
    parent_group: Optional[click.Group] = None,
    group_name: Optional[str] = None,
) -> click.Group:
    """
    Make click group
    """
    if parent_group is None:
        group_cls: Any = click  # type: ignore
    else:
        group_cls = parent_group

    def dummy_group() -> None:
        pass

    if group_name:
        dummy_group = rename(dummy_group, group_name)

    group = group_cls.group(  # type: ignore
        cls=HelpColorsGroup,
        context_settings=CONTEXT_SETTINGS,
        **DEFAULT_COLOR_OPTIONS,
    )(dummy_group)

    if isinstance(f, list):
        # List of functions are passed.
        # The declared name of functions are used as subcommand

        for _f in f:
            _get_click_command(coro(_f), group)

    elif isinstance(f, dict):
        # Dictionary of functions with custom subcommand name as key
        for name, _f in f.items():  # type: ignore
            if callable(_f):
                _get_click_command(rename(coro(_f), name), group)
            else:
                glacier_group(
                    _f,  # type: ignore
                    group,
                    name,
                )
    else:
        raise Exception("The arguments of glacier is wrong.")

    if parent_group is None and loads_completion:
        group.command(  # type: ignore
            cls=HelpColorsCommand,
            context_settings=CONTEXT_SETTINGS,
            **DEFAULT_COLOR_OPTIONS,  # type: ignore
        )(show_completion)

    return group  # type: ignore


def glacier(f: Union[
    GlacierFunction,
    List[GlacierFunction],
    Dict[str, Union[GlacierFunction, GlacierUnit]],
]) -> None:
    """
    Main function making function to command line entrypoint
    """

    if callable(f):
        # Only one function is passed.
        entry_point_f = _get_click_command(f)
    else:
        entry_point_f = glacier_group(f)  # type: ignore
    if loads_completion:
        click_completion.init()
    entry_point_f()

================
File: glacier/docstring.py
================
"""
Parser of docstring
"""
import re
from enum import Enum, auto
from typing import List
from dataclasses import dataclass

from typing_extensions import Protocol

GOOGLE_ARG_START_PATTERN = re.compile(r'^(\w+): ')
NUMPY_ARG_START_PATTERN = re.compile(r'^(\w+):')
RESTTXT_ARG_START_PATTERN = re.compile(r'^:param (\w+):')


@dataclass(init=False)
class DescriptionBuilder:

    last_is_line_break: bool
    description: str

    def __init__(self) -> None:
        self.last_is_line_break = True
        self.description = ''
        pass

    def add_line(self, line: str) -> None:
        if not line:
            if not self.description:
                return
            self.description += '\n'
            self.last_is_line_break = True
            return

        if self.last_is_line_break:
            self.description += line.strip()
        else:
            if self.description.endswith('-'):
                self.description = self.description[:-1] + line.strip()
            else:
                self.description += ' ' + line.strip()
        self.last_is_line_break = False

    def build(self) -> str:
        return self.description.strip()


@dataclass(frozen=True)
class Arg:
    name: str
    description: str

    @classmethod
    def of_lines_google(cls, lines: List[str]) -> List['Arg']:
        """
        Parse the lines of args as following and return the list.

        hoge: description of hoge
              that can be multilined.
        fuga: description of fuga.
        """
        args = []
        arg_name = ''
        arg_description_builder = DescriptionBuilder()
        for line in lines:
            m = re.search(GOOGLE_ARG_START_PATTERN, line.lstrip())
            if m:
                if arg_name:
                    args.append(Arg(
                        name=arg_name,
                        description=arg_description_builder.build(),
                    ))
                    arg_description_builder = DescriptionBuilder()
                arg_name = m.group(1)
                arg_description_builder.add_line(re.sub(
                    GOOGLE_ARG_START_PATTERN,
                    '',
                    line.lstrip(),
                ).strip())
            else:
                arg_description_builder.add_line(line.strip())

        if arg_name:
            args.append(Arg(
                name=arg_name,
                description=arg_description_builder.build(),
            ))
        return args

    @classmethod
    def of_lines_numpy(cls, lines: List[str]) -> List['Arg']:
        """
        Parse the lines of numpy args format as following and return the list.

        hoge: str
            description of hoge
        fuga:
            description of fuga.
        """
        args = []
        arg_name = ''
        arg_description_builder = DescriptionBuilder()
        for line in lines:
            m = re.search(NUMPY_ARG_START_PATTERN, line.lstrip())
            if m:
                if arg_name:
                    args.append(Arg(
                        name=arg_name,
                        description=arg_description_builder.build(),
                    ))
                    arg_description_builder = DescriptionBuilder()
                arg_name = m.group(1)
            else:
                arg_description_builder.add_line(line.strip())

        if arg_name:
            args.append(Arg(
                name=arg_name,
                description=arg_description_builder.build(),
            ))
        return args

    @classmethod
    def of_lines_resttxt(cls, lines: List[str]) -> List['Arg']:
        """
        Parse the lines of reStructuredText args format as following
        and return the list.

        :param hoge: description of hoge
        :param fuga: description of fuga
        """
        args = []
        arg_name = ''
        arg_description_builder = DescriptionBuilder()
        for line in lines:
            m = re.search(RESTTXT_ARG_START_PATTERN, line.lstrip())
            if m:
                if arg_name:
                    args.append(Arg(
                        name=arg_name,
                        description=arg_description_builder.build(),
                    ))
                    arg_description_builder = DescriptionBuilder()
                arg_name = m.group(1)
                arg_description_builder.add_line(re.sub(
                    RESTTXT_ARG_START_PATTERN,
                    '',
                    line.lstrip(),
                ).strip())
            else:
                arg_description_builder.add_line(line.strip())

        if arg_name:
            args.append(Arg(
                name=arg_name,
                description=arg_description_builder.build(),
            ))
        return args


@dataclass(frozen=True)
class Doc:
    description: str
    args: List[Arg]

    def get_matched_arg_count(self, real_args: List[str]) -> int:
        """ Get the number of given argument names which is also
        in docstring.
        """

        return sum([
            (arg.name in real_args) for arg in self.args
        ])


class Parser(Protocol):
    def parse(self, docstring: str) -> Doc:
        pass


class GoogleParser:
    def parse(self, docstring: str) -> Doc:
        docstring_lines = docstring.splitlines()
        description_builder = DescriptionBuilder()
        found_args_indicator = False
        ends_args_section = False
        args_lines = []
        for i, line in enumerate(docstring_lines):
            if line.strip() == 'Args:':
                found_args_indicator = True
                continue

            if not found_args_indicator:
                description_builder.add_line(line)
            elif not line.strip():
                ends_args_section = True
            elif not ends_args_section:
                args_lines.append(line)
        return Doc(description_builder.build(), Arg.of_lines_google(args_lines))


class NumpyParserState(Enum):
    IN_DESCRIPTION = auto()
    FOUND_PARAMETERS = auto()
    FOUND_PARAMETERS_LINE = auto()


class NumpyParser:

    def parse(self, docstring: str) -> Doc:
        docstring_lines = docstring.splitlines()
        description_builder = DescriptionBuilder()
        state: NumpyParserState = NumpyParserState.IN_DESCRIPTION
        args_lines = []
        for i, line in enumerate(docstring_lines):
            if state == NumpyParserState.IN_DESCRIPTION:
                if line.strip() == 'Parameters':
                    state = NumpyParserState.FOUND_PARAMETERS
                    continue
                description_builder.add_line(line)
            elif state == NumpyParserState.FOUND_PARAMETERS:
                if line.strip() == '----------':
                    state = NumpyParserState.FOUND_PARAMETERS_LINE
                    continue
            elif state == NumpyParserState.FOUND_PARAMETERS_LINE:
                args_lines.append(line)
        return Doc(description_builder.build(), Arg.of_lines_numpy(args_lines))


RESTTXT_ITEM_PATTERN = re.compile(r'^:(\w+) (\w+):')


class RestructuredTextParser:

    def parse(self, docstring: str) -> Doc:
        docstring_lines = docstring.splitlines()
        description_builder = DescriptionBuilder()
        started_args = False
        args_lines = []
        for i, line in enumerate(docstring_lines):
            m = re.search(RESTTXT_ITEM_PATTERN, line.lstrip())
            if m:
                item_name = m.group(1)
                if item_name == 'param':
                    started_args = True
                    args_lines.append(line)
                else:
                    break
            elif started_args:
                args_lines.append(line)
            else:
                description_builder.add_line(line)

        return Doc(description_builder.build(), Arg.of_lines_resttxt(args_lines))

================
File: glacier/misc.py
================
import asyncio
import inspect
from functools import wraps
from typing import Any


# https://github.com/pallets/click/issues/85#issuecomment-503464628
def coro(f: Any) -> Any:
    if not inspect.iscoroutinefunction(f):
        # not Coroutine
        return f

    @wraps(f)  # type: ignore
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        return (
            asyncio.get_event_loop()
            .run_until_complete(f(*args, **kwargs))
        )  # type: ignore
    return wrapper

================
File: tests/test_core.py
================
import unittest
from enum import Enum

from click.testing import CliRunner
from glacier.core import _get_click_command, glacier_group

from tests.utils import get_options, get_values


class Env(Enum):
    DEV = 'development'
    PROD = 'production'


def my_function_google(
    _path: str,
    name: str,
    age: int,
    is_test: bool,
    env: Env,
    verbose: bool = False,
) -> None:
    """
    This is my test function for generating CLI entrypoint.

    Args:
        _path: path input
        name: Name of the user.
        age: Age of the user.
        is_test: whether it is test or not.
        env: Environment where to run the CLI.
        verbose: If set, verbose output will be shown.
    """
    # Check if all arguments have the property type.
    assert type(_path) == str
    assert type(name) == str
    assert type(age) == int
    assert type(is_test) == bool
    assert isinstance(env, Env)
    assert type(verbose) == bool

    # Print all values
    print(f'_path={_path}')
    print(f'name={name}')
    print(f'age={age}')
    print(f'is_test={is_test}')
    print(f'env={env}')
    print(f'verbose={verbose}')
    return


def my_function_numpy_docstring(
    _path: str,
    name: str,
    age: int,
    is_test: bool,
    env: Env,
    verbose: bool = False,
) -> None:
    """
    This is my test function for generating CLI entrypoint.

    Parameters
    ----------
    _path: str
        path input
    name: str
        Name of the user.
    age: int
        Age of the user.
    is_test: bool
        whether it is test or not.
    env: Env
        Environment where to run the CLI.
    verbose: bool
        If set, verbose output will be shown.
    """
    # Check if all arguments have the property type.
    assert type(_path) == str
    assert type(name) == str
    assert type(age) == int
    assert type(is_test) == bool
    assert isinstance(env, Env)
    assert type(verbose) == bool

    # Print all values
    print(f'_path={_path}')
    print(f'name={name}')
    print(f'age={age}')
    print(f'is_test={is_test}')
    print(f'env={env}')
    print(f'verbose={verbose}')
    return


def my_function_restructured_text_docstring(
    _path: str,
    name: str,
    age: int,
    is_test: bool,
    env: Env,
    verbose: bool = False,
) -> None:
    """
    This is my test function for generating CLI entrypoint.

    :param _path: path input
    :param name: Name of the user.
    :param age: Age of the user.
    :param is_test: whether it is test or not.
    :param env: Environment where to run the CLI.
    :param verbose: If set, verbose output will be shown.
    """
    # Check if all arguments have the property type.
    assert type(_path) == str
    assert type(name) == str
    assert type(age) == int
    assert type(is_test) == bool
    assert isinstance(env, Env)
    assert type(verbose) == bool

    # Print all values
    print(f'_path={_path}')
    print(f'name={name}')
    print(f'age={age}')
    print(f'is_test={is_test}')
    print(f'env={env}')
    print(f'verbose={verbose}')
    return


class TestCore(unittest.TestCase):

    def test_glacier_ok(self) -> None:
        """
        Appropriate pattern (perfectly happy path).
        """
        for function in [
            my_function_google,
            my_function_numpy_docstring,
            my_function_restructured_text_docstring,
        ]:
            f = _get_click_command(function)
            runner = CliRunner()

            # All required arguments are provided, and
            # verbose (default value is set) is omitted.
            result = runner.invoke(f, [
                'path',
                '--name=taro',
                '--age=10',
                '--is-test',
                '--env=development',
            ])
            # No excption occurs
            assert not result.exception

            # Get output
            res_d = get_values(result.output)
            assert res_d['_path'] == 'path'
            assert res_d['name'] == 'taro'
            assert res_d['age'] == '10'
            assert res_d['is_test'] == 'True'
            assert res_d['env'] == 'Env.DEV'
            assert res_d['verbose'] == 'False'
        return

    def test_glacier_help(self) -> None:
        """
        Check if help of CLI is correct.
        """
        for function in [
            my_function_google,
            my_function_numpy_docstring,
            my_function_restructured_text_docstring,
        ]:
            f = _get_click_command(function)
            runner = CliRunner()
            result = runner.invoke(f, [
                '-h',
            ])
            # No exception occurs.
            assert not result.exception
            # Assert that docstring description is contained in help.
            assert (
                'This is my test function for generating CLI entrypoint.'
                in result.output
            )

            # Assert that options are displayed in order.
            help_options = get_options(result.output)

            # Assert the name (and its order) of options
            assert help_options[0].name == 'name'
            assert help_options[1].name == 'age'
            assert help_options[2].name == 'is-test'
            assert help_options[3].name == 'env'
            assert help_options[4].name == 'verbose'

            # Assert that desired description is included in each line
            assert 'Name of the user' in help_options[0].line
            assert 'Age of the user.' in help_options[1].line
            assert 'whether it is test or not.' in help_options[2].line
            assert 'Environment where to run the CLI.' in help_options[3].line
            assert 'If set, verbose output will be shown.' in help_options[4].line
        return

    def test_glacier_nestes_groups(self) -> None:
        """
        Check if CLI with nested click.group works correctly.
        """

        def a_1() -> None:
            print('a_1')

        def a_2() -> None:
            print('a_2')

        def b() -> None:
            print('b')

        f = glacier_group({
            'a': [
                a_1,
                a_2,
            ],
            'b': b,
        })
        runner = CliRunner()
        assert runner.invoke(f, [
            'a', 'a-1',
        ]).output == 'a_1\n'
        assert runner.invoke(f, [
            'a', 'a-2',
        ]).output == 'a_2\n'
        assert runner.invoke(f, [
            'b',
        ]).output == 'b\n'
        return


async def my_async_function(_a: str, b: int) -> None:
    print('foo')


class TestCoreAsync(unittest.TestCase):

    def test_async_function(_) -> None:
        f = _get_click_command(my_async_function)
        runner = CliRunner()
        result = runner.invoke(f, [
            'a',
            '--b=10',
        ])
        assert result.stdout.strip() == 'foo'

================
File: tests/test_docstring.py
================
import unittest

from glacier.docstring import Arg, Doc, GoogleParser, NumpyParser

from glacier.docstring import RestructuredTextParser


class TestArg(unittest.TestCase):
    def test_arg_of_lines_google(self) -> None:
        arg_lines = [
            'hoge: description of hoge',
            '      that can be multilined.',
            'fuga: description of fu-',
            '      ga.',
        ]
        args = Arg.of_lines_google(arg_lines)
        assert args == [
            Arg(name='hoge', description='description of hoge that can be multilined.'),
            Arg(name='fuga', description='description of fuga.'),
        ]

    def test_arg_of_lines_numpy(self) -> None:
        arg_lines = [
            'hoge: str',
            '      description of hoge following type',
            'fuga:',
            '      description of fuga',
        ]
        args = Arg.of_lines_numpy(arg_lines)
        assert args == [
            Arg(name='hoge', description='description of hoge following type'),
            Arg(name='fuga', description='description of fuga'),
        ]

    def test_arg_of_lines_resttxt(self) -> None:
        arg_lines = [
            ':param hoge: description of hoge.',
            ':param fuga: description of fuga',
            '             multilined.',
        ]
        args = Arg.of_lines_resttxt(arg_lines)
        assert args == [
            Arg(name='hoge', description='description of hoge.'),
            Arg(name='fuga', description='description of fuga multilined.'),
        ]


class TestDocstringGoogle(unittest.TestCase):

    def test_google_one_line_description(self) -> None:
        docstring = """ This is oneline docstring. """
        parser = GoogleParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is oneline docstring.',
            args=[],
        )
        return

    def test_google_simple(self) -> None:
        docstring = """
        This is a simple docstring.

        Args:
            foo: Description of foo.
            bar: Description of bar.
        """
        parser = GoogleParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is a simple docstring.',
            args=[
                Arg(name='foo', description='Description of foo.'),
                Arg(name='bar', description='Description of bar.'),
            ],
        )
        return


class TestDocstringNumpy(unittest.TestCase):

    def test_numpy_one_line_description(self) -> None:
        docstring = """ This is oneline docstring. """
        parser = NumpyParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is oneline docstring.',
            args=[],
        )
        return

    def test_numpy_simple(self) -> None:
        docstring = """
        This is a simple docstring.

        Parameters
        ----------
        foo: str
             Description of foo.
        bar: int
             Description of bar.
        """
        parser = NumpyParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is a simple docstring.',
            args=[
                Arg(name='foo', description='Description of foo.'),
                Arg(name='bar', description='Description of bar.'),
            ],
        )
        return


class TestDocstringRestructuredText(unittest.TestCase):

    def test_resttext_one_line_description(self) -> None:
        docstring = """ This is oneline docstring. """
        parser = NumpyParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is oneline docstring.',
            args=[],
        )
        return

    def test_resttext_simple(self) -> None:
        docstring = """
        This is a simple docstring.

        :param foo: Description of foo.
        :param bar: Description of bar
                    that is multilined.
        """
        parser = RestructuredTextParser()
        doc = parser.parse(docstring)
        assert doc == Doc(
            description='This is a simple docstring.',
            args=[
                Arg(name='foo', description='Description of foo.'),
                Arg(name='bar', description='Description of bar that is multilined.'),
            ],
        )
        return

================
File: tests/utils.py
================
import re
from typing import Dict, List
from dataclasses import dataclass


def get_values(output_str: str) -> Dict[str, str]:
    """
    Get the dictionary representing actual value passed to the function
    """
    res_d = {}
    for line in output_str.splitlines():
        m = re.match(r'(\w+)=(.+)', line.strip())
        assert m is not None
        res_d[m.group(1)] = m.group(2)
    return res_d


@dataclass(frozen=True)
class HelpOption:
    name: str
    line: str


def get_options(help_str: str) -> List[HelpOption]:
    """
    Get the option names obtained from the help generated by glacier.

    The output will be as follows

    Options:
      --name TEXT                     Name of this operation.  [required]
      --env [development|production]  Specifying this operation is for whether dev or prod.  [required]  # noqa
      --verbose                       Verbose output will be shown if set.
      -h, --help                      Show this message and exit.
    """
    options_found = False
    res: List[HelpOption] = []
    for line in help_str.splitlines():
        if line.startswith('Options'):
            options_found = True
            continue
        if options_found:
            if line.startswith('  '):
                m = re.search(r'^  --([\w-]+)', line)
                if m is None:
                    continue
                option_name = m.group(1)
                res.append(HelpOption(
                    name=option_name,
                    line=line.strip(),
                ))
            else:
                options_found = False
    return res

================
File: .gitignore
================
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/
doc/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# End of https://www.toptal.com/developers/gitignore/api/python
#
data

================
File: LICENSE
================
MIT License

Copyright (c) 2020 Hiroki Konishi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================
File: Makefile
================
.PHONY: lint
lint:
	pflake8 ./glacier ./tests
	mypy ./glacier ./tests
	python3 -m unittest discover -v

.PHONY: test
test:
	coverage run --omit='./tests/**/*' --source=. -m pytest -vvs --durations=10 --diff-type=split
	coverage report -m

.PHONY: publish
publish: lint
	@poetry build
	@poetry publish -u ${PYPI_USER} -p ${PYPI_PASSWORD}

.PHONY: clean
clean:
	rm -rf ./**/__pycache__
	rm -rf ./**/.mypy_cache

================
File: pyproject.toml
================
[tool.poetry]
name = "glacier"
version = "0.4.2"
description = ""
authors = ["Hiroki Konishi <relastle@gmail.com>"]
license = "MIT"
packages = [
  {include = "glacier"}
]
homepage = "https://github.com/relastle/glacier"
repository = "https://github.com/relastle/glacier"
documentation = "https://github.com/relastle/glacier"

[tool.poetry.dependencies]
python = "^3.9"
typing-extensions = "^4.1.1"
click = "^8.0.4"
click-help-colors = "^0.9.1"
importlib-metadata = "^4.11.3"

[tool.poetry.dev-dependencies]
pytest = "==6.0.1"
flake8 = "==3.8.3"
autopep8 = "==1.5.4"
pyflakes = "==2.2.0"
coverage = "==5.2.1"
pyproject-flake8 = "^0.0.1-alpha.2"
mypy = "^0.910"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

[tool.flake8]
max-line-length = 88
ignore = "E402 F541 W504"

[tool.mypy]
ignore_missing_imports = true
warn_redundant_casts = true
strict_optional = true
no_implicit_optional = true
show_error_context = true
show_column_numbers = true
disallow_untyped_calls = true
disallow_untyped_defs = true
warn_return_any = true
warn_unused_ignores = false

================
File: README.md
================
![image](https://user-images.githubusercontent.com/6816040/91929753-0fe7bf00-ed1a-11ea-8c95-793e2a20fd07.png)

# glacier

glacier is a python CLI building library for minimalists.

<a href="https://pypi.org/project/glacier/"><img src="https://img.shields.io/pypi/v/glacier?color=395c81"/></a>
<a href="https://pypi.org/project/glacier/"><img src="https://img.shields.io/pypi/pyversions/glacier?color=395c81"/></a>
<a href="https://pypi.org/project/glacier/"><img src="https://img.shields.io/pypi/l/glacier?color=395c81"/></a>

<a href="https://github.com/relastle/glacier/actions?query=workflow%3Apythontests"><img src="https://github.com/relastle/glacier/workflows/pythontests/badge.svg"/></a>
<a href="https://github.com/relastle/glacier/actions?query=workflow%3Apypi-publish"><img src="https://github.com/relastle/glacier/workflows/pypi-publish/badge.svg"/></a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Frelastle%2Fglacier?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Frelastle%2Fglacier.svg?type=shield"/></a>
<a href="https://codecov.io/gh/relastle/glacier"><img src="https://codecov.io/gh/relastle/glacier/branch/main/graph/badge.svg?token=IBCYZODDY3"/></a>

* [glacier](#glacier)
  * [Installation](#installation)
  * [Quick start](#quick-start)
  * [Basic Usage](#basic-usage)
     * [CLI without subcommand](#cli-without-subcommand)
     * [CLI with subcommands](#cli-with-subcommands)
        * [Pass a list of functions](#pass-a-list-of-functions)
        * [Pass a dictionary of functions](#pass-a-dictionary-of-functions)
     * [Async entrypoint support](#async-entrypoint-support)
     * [Positional argument](#positional-argument)
     * [Options](#options)
     * [Default value for optional argument](#default-value-for-optional-argument)
     * [Help with docstring](#help-with-docstring)
        * [Google Style](#google-style)
        * [Numpy Style](#numpy-style)
        * [reStructuredText Style](#restructuredtext-style)
     * [Supported types](#supported-types)
  * [Note](#note)
     * [Philosophy](#apple-philosophy)
     * [Warnings](#construction-warnings)
  * [Related works](#related-works)
  * [<a href="./LICENSE">LICENSE</a>](#license)

## Installation

```python
pip install glacier
```

## Quick start

You only have to call `glacier` against the entrypoint function.

```python
from glacier import glacier


def main(name: str, verbose: bool = False) -> None:
    pass


if __name__ == '__main__':
    glacier(main)
```

Then, you can see help 🍰.

![quick start help](https://user-images.githubusercontent.com/6816040/92337363-fd87cf80-f0e3-11ea-8902-d0488fbd8547.png)


## Basic Usage

### CLI without subcommand

If you just call `glacier` to a function, it will invoke it as stand-alone CLI
(like the example in [Quick start](https://github.com/relastle/glacier#quick-start)).

### CLI with subcommands

You can easily construct CLI with subcommands in the following two ways.

#### Pass a list of functions

```python
from glacier import glacier


def run(name: str, verbose: bool = False) -> None:
    """ Run """
    pass


def build(name: str, verbose: bool = False) -> None:
    """ Build """
    pass


def test(name: str, verbose: bool = False) -> None:
    """ Test """
    return


if __name__ == '__main__':
    glacier([run, build, test])
```

If you passes a lift of function, glacier constructs the CLI with subcommands whose names are the same as the declared function names.
In this example, the subcommans will be `run`,  `build`, and `test`.


![sub commands help](https://user-images.githubusercontent.com/6816040/92397064-108cb500-f161-11ea-9cb2-0f0a1c4da2f5.png)


#### Pass a dictionary of functions

You can easily give the different name as the subcommand name from any declared name of the function.
Just give a dictionary (key will be a subcommand name).


```python
from glacier import glacier


def f1(name: str, verbose: bool = False) -> None:
    pass


def f2(name: str, verbose: bool = False) -> None:
    pass


def f3(name: str, verbose: bool = False) -> None:
    pass


if __name__ == '__main__':
    glacier({
        'run': f1,
        'build': f2,
        'test': f3,
    })
```

This works exactly the same as the previous example.

This interface makes it very easy to build a simple CLI tool from an existing project.

### Async entrypoint support

You sometimes want your async function to be a CLI entrypoint.
Only you have to do is just passing the async function as if it were `sync` function.

The example below combine two async functions and a sync function into CLI
with nested subcommand structure.


```python
from glacier import glacier


async def main() -> None:
    return


def sub_1() -> None:
    return


async def sub_2() -> None:
    return


if __name__ == '__main__':
    glacier({
        'main': main,
        'sub': [
            sub_1,
            sub_2,
        ],
    })
```

### Positional argument

If the name of function argument is underscore-prefiexed, it is understood as positional argument.

```python
from glacier import glacier


def all_positional(_a: str, _b: str, _c: str) -> None:
    print(_a)
    print(_b)
    print(_c)


if __name__ == '__main__':
    glacier(all_positional)
```

The above example is invoked as follows

```bash
<command_name> <value of a> <value of b> <value of c>
```

### Options

All other (non-underscore-prefixed) arguments are understood as options.

```python
from glacier import glacier


def all_options(a: str, b: str, c: str) -> None:
    print(a)
    print(b)
    print(c)


if __name__ == '__main__':
    glacier(all_options)
```

The above example is invoked as follows

```bash
<command_name> --a <value of a> --b <value of b> --c <value of c>
```

### Default value for optional argument

If you set the default value for function argument, it also defines the default value for CLI option.


```python
from glacier import glacier


def default(verbose: bool = False) -> None:
    print(verbose)


if __name__ == '__main__':
    glacier(default)
```

The above example is invoked as follows

```bash
<command_name> # Just call without flag (`False` will be printed)
```

or

```bash
<command_name> --verbose # Call with flag (`True` will be printed)
```

### Help with docstring

Help message for options or command itself can be provided with python docstring.

Following style of doctrings are supported

- [Google Style](https://github.com/relastle/glacier#google-style)
- [Numpy Style](https://github.com/relastle/glacier#numpy-style)
- [reStructuredText Style](https://github.com/relastle/glacier#restructuredtext-style)

The functions with docstring below will produce the exact the same help message with fed into `glacier`.
(You don't need to specify which docstring style is used 😄)

#### Google Style

```python
def main_google(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    Args:
        _path: Positional argument representing the target file path.
        name: Name of this operation.
        verbose: Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return
```

#### Numpy Style

```python
def main_numpy(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    Parameters
    ----------
    _path: str
        Positional argument representing the target file path.
    name: str
        Name of this operation.
    verbose: bool
        Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return
```

#### reStructuredText Style


```python
def main_restructured_text(
    _path: str,
    name: str,
    verbose: bool = False,
) -> None:
    """
    This is my simple entry point of CLI.

    :param _path: Positional argument representing the target file path.
    :param name: Name of this operation.
    :param verbose: Verbose output will be shown if set.
    """
    print(_path)
    print(name)
    print(verbose)
    return
```


### Supported types

- [x] int
- [x] str
- [x] bool
- [x] Enum
- [ ] List[int]
- [ ] List[str]

## Note

### :apple: Philosophy

- This library is made for building CLI quickly especially for personal use, so the features provided by it is not rich.

- If you want to build really user-friend CLI or that in production, I suggest that you use [click](https://click.palletsprojects.com/en/7.x/) (actually glacier uses it internally), or other full-stack CLI builing libraries.

### :construction: Warnings

- Please note that any destructive change (backward incompatible) can be done without any announcement.
- This plugin is in a very early stage of development.  Feel free to report problems or submit feature requests in [Issues](https://github.com/relastle/glacier/issues).

## Related works

- [google/python-fire: Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object.](https://github.com/google/python-fire)
- [tiangolo/typer: Typer, build great CLIs. Easy to code. Based on Python type hints.](https://github.com/tiangolo/typer)

## [LICENSE](./LICENSE)

MIT
