├── .github └── workflows │ ├── docs.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── README.md ├── changelog.md ├── cliar ├── __init__.py ├── cliar.py └── utils.py ├── docs ├── README.md ├── foliant.yml ├── mkdocs.yml └── src │ ├── assets │ ├── greeter.py │ ├── terminal.ico │ └── terminal.svg │ ├── changelog.md │ ├── comparison.md │ ├── index.md │ └── tutorial.md ├── poetry.lock ├── pylintrc ├── pyproject.toml └── tests ├── test_async_fns.py ├── test_async_fns └── async_fns.py ├── test_basicmath.py ├── test_basicmath ├── basicmath.py └── numbers.txt ├── test_case_sensitive_args.py ├── test_case_sensitive_args └── case_sensitive_args.py ├── test_global_args.py ├── test_global_args └── global_args.py ├── test_multiword_args.py ├── test_multiword_args └── multiword_args.py ├── test_nested.py ├── test_nested └── nested.py ├── test_noroot.py └── test_noroot └── noroot.py /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - "Run Tests" 7 | branches: 8 | - develop 9 | types: 10 | - completed 11 | 12 | jobs: 13 | Docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.10' 20 | 21 | - name: Download Poetry 22 | run: curl -OsSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py 23 | 24 | - name: Install Poetry 25 | run: python install-poetry.py --preview -y && export PATH=$PATH:~/.poetry/bin 26 | 27 | - name: Install Package 28 | run: poetry install 29 | 30 | - name: Build Docs 31 | run: poetry run foliant make site -p docs 32 | 33 | - name: Deploy Docs 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./cliar-docs.mkdocs 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - "Deploy Docs" 7 | tags: 8 | - '*' 9 | types: 10 | - completed 11 | 12 | jobs: 13 | Publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.10' 20 | 21 | - name: Download Poetry 22 | run: curl -OsSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py 23 | 24 | - name: Install Poetry 25 | run: python install-poetry.py --preview -y && export PATH=$PATH:~/.poetry/bin 26 | 27 | - name: Install Package 28 | run: poetry install 29 | 30 | - name: Publish Package 31 | env: 32 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} 33 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 34 | run: poetry publish --build --username="$PYPI_USERNAME" --password="$PYPI_PASSWORD" 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | Tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.10' 16 | 17 | - name: Download Poetry 18 | run: curl -OsSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py 19 | 20 | - name: Install Poetry 21 | run: python install-poetry.py --preview -y && export PATH=$PATH:~/.poetry/bin 22 | 23 | - name: Install Package 24 | run: poetry install 25 | 26 | - name: Run Tests 27 | run: poetry run pytest --cov=cliar 28 | 29 | - name: Run Test Coverage 30 | run: poetry run codecov 31 | 32 | - name: Run Linter 33 | run: poetry run pylint cliar 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # VSCode project settings 99 | .vscode 100 | 101 | # Sublime Test project settings 102 | *.sublime-project 103 | *.sublime-workspace 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | *.mkdocs 112 | *.pdf 113 | .*cache 114 | __folianttmp__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![image](https://img.shields.io/pypi/v/cliar.svg)](https://pypi.org/project/cliar) 2 | [![Build Status](https://travis-ci.com/moigagoo/cliar.svg?branch=develop)](https://travis-ci.com/moigagoo/cliar) 3 | [![image](https://codecov.io/gh/moigagoo/cliar/branch/develop/graph/badge.svg)](https://codecov.io/gh/moigagoo/cliar) 4 | 5 | # Cliar 6 | 7 | **Cliar** is a Python package to help you create commandline interfaces. It focuses on simplicity and extensibility: 8 | 9 | - Creating a CLI is as simple as subclassing from `cliar.Cliar`. 10 | - Extending a CLI is as simple as subclassing from a `cliar.Cliar` subclass. 11 | 12 | Cliar's mission is to let you focus on the business logic instead of building an interface for it. At the same time, Cliar doesn't want to stand in your way, so it provides the means to customize the generated CLI. 13 | 14 | 15 | ## Installation 16 | 17 | ```shell 18 | $ pip install cliar 19 | ``` 20 | 21 | Cliar requires Python 3.6+ and is tested on Windows, Linux, and macOS. There are no dependencies outside Python's standard library. 22 | 23 | 24 | ## Basic Usage 25 | 26 | Let's create a commandline calculator that adds two floats: 27 | 28 | ```python 29 | from cliar import Cliar 30 | 31 | 32 | class Calculator(Cliar): 33 | '''Calculator app.''' 34 | 35 | def add(self, x: float, y: float): 36 | '''Add two numbers.''' 37 | 38 | print(f'The sum of {x} and {y} is {x+y}.') 39 | 40 | 41 | if __name__ == '__main__': 42 | Calculator().parse() 43 | ``` 44 | 45 | Save this code to `calc.py` and run it. Try different inputs: 46 | 47 | - Valid input: 48 | 49 | $ python calc.py add 12 34 50 | The sum of 12.0 and 34.0 is 46.0. 51 | 52 | - Invalid input: 53 | 54 | $ python calc.py add foo bar 55 | usage: calc.py add [-h] x y 56 | calc.py add: error: argument x: invalid float value: 'foo' 57 | 58 | - Help: 59 | 60 | $ python calc.py -h 61 | usage: calc.py [-h] {add} ... 62 | 63 | Calculator app. 64 | 65 | optional arguments: 66 | -h, --help show this help message and exit 67 | 68 | commands: 69 | {add} Available commands: 70 | add Add two numbers. 71 | 72 | - Help for `add` command: 73 | 74 | $ python calc.py add -h 75 | usage: calc.py add [-h] x y 76 | 77 | Add two numbers. 78 | 79 | positional arguments: 80 | x 81 | y 82 | 83 | optional arguments: 84 | -h, --help show this help message and exit 85 | 86 | A few things to note: 87 | 88 | - It's a regular Python class with a regular Python method. You don't need to learn any new syntax to use Cliar. 89 | 90 | - `add` method is converted to `add` command, its positional params are converted to positional commandline args. 91 | 92 | - There is no explicit conversion to float for `x` or `y` or error handling in the `add` method body. Instead, `x` and `y` are just treated as floats. Cliar converts the types using `add`'s type hints. Invalid input doesn't even reach your code. 93 | 94 | - `--help` and `-h` flags are added automatically and the help messages are generated from the docstrings. 95 | 96 | 97 | ## Setuptools and Poetry 98 | 99 | To invoke your CLI via an entrypoint, wrap `parse` call in a function and point to it in your `setup.py` or `pyproject.toml`. 100 | 101 | `calc.py`: 102 | 103 | ... 104 | def entry_point(): 105 | Calculator().parse() 106 | 107 | `setup.py`: 108 | 109 | setup( 110 | ... 111 | entry_points = { 112 | 'console_scripts': ['calc=calc:entry_point'], 113 | } 114 | ... 115 | ) 116 | 117 | `pyproject.toml`: 118 | 119 | ... 120 | [tool.poetry.scripts] 121 | calc = 'calc:entry_point' 122 | 123 | 124 | ## Read Next 125 | 126 | - [Tutorial →](https://moigagoo.github.io/cliar/tutorial/) 127 | - [Cliar vs. Click vs. docopt →](https://moigagoo.github.io/cliar/comparison/) 128 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 1.3.5 (October 21, 2021) 2 | 3 | - Switch from Travis to GitHub Actions. 4 | 5 | 6 | # 1.3.4 (December 11, 2019) 7 | 8 | - Add support for async handlers (per [#13](https://github.com/moigagoo/cliar/pull/13)). 9 | 10 | 11 | # 1.3.3 (November 29, 2019) 12 | 13 | - Fix [#11](https://github.com/moigagoo/cliar/issues/11): multiword optional args of any type other than `bool` couldn't be used. 14 | 15 | 16 | # 1.3.2 (July 23, 2019) 17 | 18 | - Global args are now stored in `self.global_args` instead of `self._root_args`. 19 | - Global args are now available in nested commands. [Read more](https://moigagoo.github.io/cliar/tutorial/#global-arguments). 20 | 21 | 22 | # 1.3.1 (July 22, 2019) [Removed from PyPI] 23 | 24 | - Commands can now access root command args via `self._root_args`. [Read more](https://moigagoo.github.io/cliar/tutorial/#root-command). 25 | 26 | 27 | # 1.3.0 (July 21, 2019) 28 | 29 | - Add support for nested commands. [Read more](https://moigagoo.github.io/cliar/tutorial/#nested-commands). 30 | - Fix incorrect mapping from handler params to optional CLI args. 31 | 32 | 33 | # 1.2.5 (June 30, 2019) 34 | 35 | - Prepare for postponed annotation evaluation, which will be the default in Python 4.0 (see #2). 36 | 37 | # 1.2.4 (June 27, 2019) 38 | 39 | - Add `show_defaults` param to `set_help` util. [Read more](https://moigagoo.github.io/cliar/tutorial/#argument-descriptions). 40 | 41 | # 1.2.3 (May 13, 2019) 42 | 43 | - Fix Python 3.7 incompatibility. 44 | - Add `set_sharg_map` to override or disable short arg names. 45 | 46 | # 1.2.2 (June 3, 2018) 47 | 48 | - Make `_root` not an abstract method. 49 | 50 | # 1.2.1 (June 2, 2018) 51 | 52 | - Fix critical bug that disallowed string params. 53 | 54 | # 1.2.0 (June 1, 2018) 55 | 56 | - Boolean handler params are converted into `store_true` arguments. Before that, params with default value of `True` were much confusingly converted into `store_false` arguments. 57 | - Support `List[int]` and similar arg types. If the param type is a subclass of `typing.Iterable` and has a type specified in brackets, it's converted into multivalue arg of the type in the brackets. 58 | - Do not print help whenever `_root` command is invoked. 59 | - Convert the `cliar` module into a package. 60 | - Add tests. 61 | - Switch to Poetry. 62 | 63 | # 1.1.9 64 | 65 | - Add the ability to set help messages for arguments. 66 | - Add the ability to set metavars for arguments. 67 | 68 | # 1.1.8 69 | 70 | - **[Breaks backward compatibility]** Base CLI class renamed from `CLI` to `Cliar`. 71 | - Fixed a bug where commandline args with dashes weren't mapped to corresponding param names with underscores. 72 | 73 | # 1.1.7 74 | 75 | - Add the ability to override mapping between commandline args and and handler params. By default, handler params correspond to args of the same name with underscores replaced with dashes. 76 | 77 | # 1.1.6 78 | 79 | - Underscores in handler names are now replaced with dashes when the corresponding command name is generated. 80 | 81 | # 1.1.5 82 | 83 | - Optional arguments are now prepended with '--', not '-'. 84 | - Short argument names are now generated from the long ones: `name` handler arg corresponds to `-n` and `--name` commandline args. 85 | - Python 2 support dropped. Python 3.5+ required. 86 | - Code refactored, type hints added. 87 | 88 | # 1.1.4 89 | 90 | - Code improvements for API documentation. 91 | 92 | # 1.1.3 93 | 94 | - Code cleanup. 95 | 96 | # 1.1.2 97 | 98 | - Setup: Python version check improved. 99 | 100 | # 1.1.1 101 | 102 | - Python 2: If only the _root handler was defined, a "too few agruments" error raised. Fixed. 103 | - If only the _root handler is defined, the commands subparser is not added anymore. 104 | - Packaging improved, the installation package now includes both Python 2 and 3 sources. 105 | 106 | # 1.1.0 107 | 108 | - Command descriptions did not preserve line breaks from docstrings. Fixed. 109 | 110 | # 1.0.9 111 | 112 | - Commands now use the first docstring line as help and the whole docstring as description. 113 | 114 | # 1.0.8 115 | 116 | - Description and help texts now preserve line breaks from docstrings. 117 | 118 | # 1.0.7 119 | 120 | - Support of multiple values for a single arg added. 121 | 122 | # 1.0.6 123 | 124 | - Command-line args are now parsed by explicitly calling the `.parse()` method. 125 | 126 | # 1.0.5 127 | 128 | - The `ignore` decorator added to exclude a method from being converted into a command. 129 | 130 | # 1.0.4 131 | 132 | - Nested CLI methods would not override parent methods. Fixed. 133 | 134 | # 1.0.3 135 | 136 | - Python 2 support added. 137 | 138 | # 1.0.2 139 | 140 | - Docstring added to the add_aliases function. 141 | - The set_name function is now less hacky. 142 | 143 | # 1.0.1 144 | 145 | - Alias support added with the "add_aliases" decorator. 146 | 147 | # 1.0.0 148 | 149 | - First version. Changelog started. 150 | -------------------------------------------------------------------------------- /cliar/__init__.py: -------------------------------------------------------------------------------- 1 | from .cliar import Cliar 2 | from .utils import set_help, set_metavars, set_arg_map, set_sharg_map, set_name, add_aliases, ignore 3 | 4 | 5 | __all__ = [ 6 | 'Cliar', 7 | 'set_help', 8 | 'set_metavars', 9 | 'set_arg_map', 10 | 'set_sharg_map', 11 | 'set_name', 12 | 'add_aliases', 13 | 'ignore' 14 | ] 15 | -------------------------------------------------------------------------------- /cliar/cliar.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, RawTextHelpFormatter 2 | from asyncio import get_event_loop 3 | from inspect import signature, getmembers, ismethod, isclass, iscoroutine 4 | from collections import OrderedDict 5 | from typing import List, Iterable, Callable, Set, Type, get_type_hints 6 | 7 | from .utils import ignore 8 | 9 | 10 | # pylint: disable=too-few-public-methods, protected-access, too-many-instance-attributes 11 | 12 | 13 | class _Arg: 14 | '''CLI command argument. 15 | 16 | Its attributes correspond to the homonymous params of the ``add_argument`` function. 17 | ''' 18 | 19 | def __init__(self): 20 | self.type = None 21 | self.default = None 22 | self.action = None 23 | self.nargs = None 24 | self.metavar = None 25 | self.help = None 26 | self.short_name = None 27 | 28 | 29 | class _Command: 30 | '''CLI command corresponding to a handler. 31 | 32 | Command args correspond to its handler args. 33 | ''' 34 | 35 | def __init__(self, handler: Callable): 36 | self.handler = handler 37 | 38 | self.arg_map = {} 39 | if hasattr(handler, '_arg_map'): 40 | self.arg_map = handler._arg_map 41 | 42 | self.sharg_map = {} 43 | if hasattr(handler, '_sharg_map'): 44 | self.sharg_map = handler._sharg_map 45 | 46 | self.metavar_map = {} 47 | if hasattr(handler, '_metavar_map'): 48 | self.metavar_map = handler._metavar_map 49 | 50 | self.name = handler.__name__.replace('_', '-') 51 | if hasattr(handler, '_command_name'): 52 | self.name = handler._command_name 53 | 54 | self.aliases = [] 55 | if hasattr(handler, '_command_aliases'): 56 | self.aliases = handler._command_aliases 57 | 58 | self.help_map = {} 59 | if hasattr(handler, '_help_map'): 60 | self.help_map = handler._help_map 61 | 62 | self.formatter_class = RawTextHelpFormatter 63 | if hasattr(handler, '_formatter_class'): 64 | self.formatter_class = handler._formatter_class 65 | 66 | self.args = self._get_args() 67 | 68 | @staticmethod 69 | def _get_origins(typ) -> Set[Type]: 70 | '''To properly parse arg types like ``typing.List[int]``, we need a way to determine that 71 | the type is based on ``list`` or ``tuple``. In Python 3.6, we'd use subclass check, 72 | but it doesn't work anymore in Python 3.7. In Python 3.7, the right way to do such a check 73 | is by looking at ``__origin__``. 74 | 75 | This method checks the type's ``__origin__`` to detect its origin in Python 3.7 76 | and ``__orig_bases__`` for Python 3.6. 77 | ''' 78 | 79 | origin = getattr(typ, '__origin__', None) 80 | orig_bases = getattr(typ, '__orig_bases__', ()) 81 | 82 | return set((origin, *orig_bases)) 83 | 84 | def _get_args(self) -> OrderedDict: 85 | '''Get command arguments from the parsed signature of its handler.''' 86 | 87 | args = OrderedDict() 88 | 89 | handler_signature = signature(self.handler) 90 | 91 | for param_name, param_data in handler_signature.parameters.items(): 92 | arg = _Arg() 93 | 94 | arg.help = self.help_map.get(param_name, '') 95 | 96 | arg.type = get_type_hints(self.handler).get(param_name) 97 | 98 | if param_data.default is not param_data.empty: 99 | arg.default = param_data.default 100 | 101 | if not arg.type: 102 | arg.type = type(arg.default) 103 | 104 | if arg.type == bool: 105 | arg.action = 'store_true' 106 | 107 | elif self._get_origins(arg.type) & {list, tuple}: 108 | if arg.default: 109 | arg.nargs = '*' 110 | else: 111 | arg.nargs = '+' 112 | 113 | if arg.type.__args__: 114 | arg.type = arg.type.__args__[0] 115 | 116 | if not arg.action and param_name in self.metavar_map: 117 | arg.metavar = self.metavar_map[param_name] 118 | 119 | if param_name not in self.arg_map: 120 | self.arg_map[param_name] = param_name.replace('_', '-') 121 | 122 | arg.short_name = self.sharg_map.get(param_name, self.arg_map[param_name][0]) 123 | 124 | args[self.arg_map[param_name]] = arg 125 | 126 | return args 127 | 128 | 129 | class Cliar: 130 | '''Base CLI class. 131 | 132 | Subclass from it to create your own CLIs. 133 | 134 | A few rules apply: 135 | 136 | - Regular methods are converted to commands. Such methods are called *handlers*. 137 | - Command args are generated from the corresponding method args. 138 | - Methods that start with an underscore are ignored. 139 | - ``self._root`` corresponds to the root command. Use it to define global args. 140 | ''' 141 | 142 | def __init__( 143 | self, 144 | parser_name: str or None = None, 145 | parent: Type['Cliar'] or None = None 146 | ): 147 | if parent: 148 | self._parser = parent._command_parsers.add_parser( 149 | parser_name, 150 | description=self.__doc__, 151 | help=self.__doc__, 152 | formatter_class=RawTextHelpFormatter 153 | ) 154 | else: 155 | self._parser = ArgumentParser( 156 | description=self.__doc__, 157 | formatter_class=RawTextHelpFormatter 158 | ) 159 | 160 | self._register_root_args() 161 | 162 | self._commands = {} 163 | self._subclis = [] 164 | 165 | handlers, subclis = self._get_handlers(), self._get_subclis() 166 | 167 | if handlers or subclis: 168 | self._command_parsers = self._parser.add_subparsers( 169 | title='commands' 170 | ) 171 | self._register_commands(handlers) 172 | 173 | for subcli_name, subcli_class in subclis.items(): 174 | self._subclis.append(subcli_class(subcli_name, self)) 175 | 176 | self.global_args = {} 177 | 178 | def _register_root_args(self): 179 | '''Register root args, i.e. params of ``self._root``, in the global argparser.''' 180 | 181 | self.root_command = _Command(self._root) 182 | self._parser.set_defaults(_command=self.root_command) 183 | 184 | for arg_name, arg in self.root_command.args.items(): 185 | self._register_arg(self._parser, arg_name, arg) 186 | 187 | @staticmethod 188 | def _register_arg(command_parser: ArgumentParser, arg_name: str, arg: _Arg): 189 | '''Register an arg in the specified argparser. 190 | 191 | :param command_parser: global argparser or a subparser corresponding to a CLI command 192 | :param str arg_name: handler param name without prefixing dashes 193 | :param arg: arg type, default value, and action 194 | ''' 195 | 196 | if arg.default is None: 197 | arg_prefixed_names = [] 198 | 199 | elif arg.short_name: 200 | arg_prefixed_names = ['-'+arg.short_name, '--'+arg_name] 201 | 202 | else: 203 | arg_prefixed_names = ['--'+arg_name] 204 | 205 | if arg.action: 206 | command_parser.add_argument( 207 | *arg_prefixed_names, 208 | dest=arg_name, 209 | default=arg.default, 210 | action=arg.action, 211 | help=arg.help 212 | ) 213 | 214 | elif arg.nargs: 215 | command_parser.add_argument( 216 | *arg_prefixed_names, 217 | dest=arg_name, 218 | type=arg.type, 219 | default=arg.default, 220 | nargs=arg.nargs, 221 | metavar=arg.metavar, 222 | help=arg.help 223 | ) 224 | 225 | else: 226 | command_parser.add_argument( 227 | *arg_prefixed_names, 228 | dest=arg_name, 229 | type=arg.type, 230 | default=arg.default, 231 | metavar=arg.metavar, 232 | help=arg.help 233 | ) 234 | 235 | def _get_handlers(self) -> List[Callable]: 236 | '''Get all handlers except ``self._root``.''' 237 | 238 | return ( 239 | method 240 | for method_name, method in getmembers(self, predicate=ismethod) 241 | if not method_name.startswith('_') and not hasattr(method, '_ignore') 242 | ) 243 | 244 | def _get_subclis(self): 245 | condition = lambda m: isclass(m) and issubclass(m, Cliar) 246 | 247 | return { 248 | member_name: member 249 | for member_name, member in getmembers(self, predicate=condition) 250 | if not member_name.startswith('_') 251 | } 252 | 253 | def _register_commands(self, handlers: Iterable[Callable]): 254 | '''Create parsers for commands from handlers (except for ``self._root``).''' 255 | 256 | for handler in handlers: 257 | command = _Command(handler) 258 | 259 | command_parser = self._command_parsers.add_parser( 260 | command.name, 261 | help=handler.__doc__.splitlines()[0] if handler.__doc__ else '', 262 | description=handler.__doc__, 263 | formatter_class=command.formatter_class, 264 | aliases=command.aliases 265 | ) 266 | command_parser.set_defaults(_command=command) 267 | 268 | for arg_name, arg in command.args.items(): 269 | self._register_arg(command_parser, arg_name, arg) 270 | 271 | self._commands[command.name] = command 272 | 273 | for alias in command.aliases: 274 | self._commands[alias] = command 275 | 276 | @ignore 277 | def parse(self): 278 | '''Parse commandline input, i.e. launch the CLI.''' 279 | 280 | args = self._parser.parse_args() 281 | 282 | command = args._command 283 | command_args = {arg: vars(args)[arg] for arg in command.args} 284 | inverse_arg_map = {arg: param for param, arg in command.arg_map.items()} 285 | handler_args = {inverse_arg_map[arg]: value for arg, value in command_args.items()} 286 | 287 | self.global_args = { 288 | arg: value 289 | for arg, value in vars(args).items() 290 | if arg not in ['_command', *command_args.keys()] 291 | } 292 | for subcli in self._subclis: 293 | subcli.global_args = self.global_args 294 | 295 | result = command.handler(**handler_args) 296 | 297 | if iscoroutine(result): 298 | result = get_event_loop().run_until_complete(result) 299 | 300 | if result == NotImplemented: 301 | command.handler.__self__._parser.print_help() 302 | 303 | def _root(self): 304 | '''The root command, which corresponds to the script being called without any command.''' 305 | 306 | # pylint: disable=no-self-use 307 | 308 | return NotImplemented 309 | -------------------------------------------------------------------------------- /cliar/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Dict 2 | from argparse import ArgumentDefaultsHelpFormatter 3 | 4 | 5 | # pylint: disable=too-few-public-methods,protected-access 6 | 7 | 8 | def set_help(help_map: Dict[str, str], show_defaults=False) -> Callable: 9 | '''Set help messages for arguments. 10 | 11 | :param help_map: mapping from handler param names to help messages 12 | :param show_defaults: show default values after argument help messages 13 | ''' 14 | def decorator(handler: Callable) -> Callable: 15 | '''Decorator returning command handler with a help message map.''' 16 | 17 | handler._help_map = help_map 18 | 19 | if show_defaults: 20 | handler._formatter_class = ArgumentDefaultsHelpFormatter 21 | 22 | return handler 23 | 24 | return decorator 25 | 26 | 27 | def set_metavars(metavar_map: Dict[str, str]) -> Callable: 28 | '''Override default metavars for arguments. 29 | 30 | By default, metavars are generated from arg names: ``foo`` → ``FOO``, `--bar`` → ``BAR``. 31 | 32 | :param metavar_map: mapping from handler param names to metavars 33 | ''' 34 | def decorator(handler: Callable) -> Callable: 35 | '''Decorator returning command handler with a custom metavar map.''' 36 | 37 | handler._metavar_map = metavar_map 38 | return handler 39 | 40 | return decorator 41 | 42 | 43 | def set_arg_map(arg_map: Dict[str, str]) -> Callable: 44 | '''Override mapping from handler params to commandline args. 45 | 46 | Be default, param names are used as arg names with underscores replaced with dashes. 47 | 48 | :param arg_map: mapping from handler param names to arg names 49 | ''' 50 | 51 | def decorator(handler: Callable) -> Callable: 52 | '''Decorator returning command handler with a custom arg map.''' 53 | 54 | handler._arg_map = arg_map 55 | return handler 56 | 57 | return decorator 58 | 59 | 60 | def set_sharg_map(sharg_map: Dict[str, str]) -> Callable: 61 | '''Override mapping from handler params to short commandline arg names. 62 | 63 | Be default, the first character of arg names are used as short arg names. 64 | 65 | :param arg_map: mapping from handler param names to short arg names 66 | ''' 67 | 68 | def decorator(handler: Callable) -> Callable: 69 | '''Decorator returning command handler with a custom shaarg map.''' 70 | 71 | handler._sharg_map = sharg_map 72 | return handler 73 | 74 | return decorator 75 | 76 | 77 | def set_name(name: str) -> Callable: 78 | '''Override the name of the CLI command. By default, commands are called the same 79 | as their corresponding handlers. 80 | 81 | :param name: new command name 82 | ''' 83 | 84 | if name == '': 85 | raise NameError('Command name cannot be empty') 86 | 87 | def decorator(handler: Callable) -> Callable: 88 | '''Decorator returning command handler with a custom command name.''' 89 | 90 | handler._command_name = name 91 | return handler 92 | 93 | return decorator 94 | 95 | 96 | def add_aliases(aliases: List[str]) -> Callable: 97 | '''Add command aliases. 98 | 99 | :param aliases: list of aliases 100 | ''' 101 | 102 | def decorator(handler: Callable) -> Callable: 103 | '''Decorator returning command handler with a list of aliases set for its command.''' 104 | 105 | handler._command_aliases = aliases 106 | return handler 107 | 108 | return decorator 109 | 110 | 111 | def ignore(handler: Callable) -> Callable: 112 | '''Exclude a method from being converted into a command. 113 | 114 | :param method: method to ignore 115 | ''' 116 | 117 | handler._ignore = True 118 | return handler 119 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Cliar Docs 2 | 3 | ## Build Instructions 4 | 5 | With Docker Compose: 6 | 7 | ```bash 8 | # Site: 9 | $ docker-compose run --rm site 10 | # PDF: 11 | $ docker-compose run --rm pdf 12 | ``` 13 | 14 | With pip and stuff (requires Python 3.6+, Pandoc, and TeXLive): 15 | 16 | ```bash 17 | $ pip install -r requirements.txt 18 | # Site: 19 | $ foliant make site 20 | # PDF: 21 | $ foliant make pdf 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/foliant.yml: -------------------------------------------------------------------------------- 1 | title: 'Cliar: Create modular Python CLIs with type annotations and inheritance' 2 | slug: cliar-docs 3 | 4 | chapters: 5 | - index.md 6 | - tutorial.md 7 | - comparison.md 8 | - changelog.md 9 | 10 | backend_config: 11 | mkdocs: 12 | mkdocs.yml: !include mkdocs.yml 13 | 14 | escape_code: true 15 | 16 | preprocessors: 17 | - includes: 18 | aliases: 19 | cliar: https://github.com/moigagoo/cliar.git 20 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | repo_url: https://github.com/moigagoo/cliar 2 | edit_uri: edit/develop/docs/src/ 3 | 4 | theme: 5 | name: material 6 | palette: 7 | primary: cyan 8 | accent: cyan 9 | font: 10 | text: PT Sans 11 | code: PT Mono 12 | logo: assets/terminal.svg 13 | favicon: assets/terminal.ico 14 | 15 | extra: 16 | social: 17 | - type: github 18 | link: https://github.com/moigagoo/cliar 19 | 20 | markdown_extensions: 21 | - codehilite 22 | - toc: 23 | permalink: true 24 | - admonition 25 | 26 | copyright: > 27 |

Created by Constantine Molchanov 28 | using Foliant.

29 | 30 |

Icons made by inipagistudio from www.flaticon.com is licensed by CC 3.0 BY

31 | 32 | google_analytics: 33 | - 'UA-120535275-1' 34 | - 'auto' 35 | -------------------------------------------------------------------------------- /docs/src/assets/greeter.py: -------------------------------------------------------------------------------- 1 | from math import factorial, tau, pi 2 | from datetime import datetime 3 | 4 | from cliar import Cliar, set_help, set_metavars, set_arg_map, set_sharg_map, add_aliases, set_name, ignore 5 | 6 | class Time(Cliar): 7 | def now(self, utc=False): 8 | now_ctime = datetime.utcnow().ctime() if utc else datetime.now().ctime() 9 | print(f'UTC time is {now_ctime}') 10 | 11 | class Utils(Cliar): 12 | time = Time 13 | 14 | class Greeter(Cliar): 15 | '''Greeter app created with Cliar.''' 16 | 17 | utils = Utils 18 | 19 | def _root(self, version=False): 20 | print('Greeter 1.0.0.' if version else 'Welcome to Greeter!') 21 | 22 | def _get_tau_value(self): 23 | return tau 24 | 25 | @ignore 26 | def get_pi_value(self): 27 | return pi 28 | 29 | def constants(self): 30 | if self.global_args.get('version'): 31 | print('Greeter 1.0.0.') 32 | 33 | print(f'τ = {self._get_tau_value()}') 34 | print(f'π = {self.get_pi_value()}') 35 | 36 | @set_name('factorial') 37 | def calculate_factorial(self, n: int): 38 | print(f'n! = {factorial(n)}') 39 | 40 | @add_aliases(['mientras', 'пока']) # Yes you can use non-Latin characters 41 | def goodbye(self, name): 42 | '''Say goodbye''' 43 | 44 | print(f'Goodbye {name}!') 45 | 46 | @set_arg_map({'n': 'repeat'}) 47 | @set_sharg_map({'n': 'n'}) 48 | @set_metavars({'name': 'NAME'}) 49 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 50 | def hello(self, name, n=1, shout=False): 51 | '''Say hello.''' 52 | 53 | greeting = f'Hello {name}!' 54 | 55 | for _ in range(n): 56 | print(greeting.upper() if shout else greeting) 57 | 58 | 59 | if __name__ == '__main__': 60 | Greeter().parse() 61 | -------------------------------------------------------------------------------- /docs/src/assets/terminal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moigagoo/cliar/2b8e9b503c25b24bcb1d6876d3255cff5fa113b7/docs/src/assets/terminal.ico -------------------------------------------------------------------------------- /docs/src/assets/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/src/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ../../changelog.md 4 | 5 | -------------------------------------------------------------------------------- /docs/src/comparison.md: -------------------------------------------------------------------------------- 1 | # Cliar vs. Click vs. docopt 2 | 3 | It may seem strange to develop yet another Python package for CLI creation when we already have great tools like Click and docopt. Why not use one of those? 4 | 5 | It turns out there's at least one area where Click and docopt just won't do—*modular CLI*. Below, I'll try to explain what I mean by modular CLIs and why they are important. A will also cover other things that make Cliar special. 6 | 7 | Name | Modular CLIs | DSL-free | Magic-free | Type casting | Pun in name 8 | ---- | ------------ | -------- | ---------- | ------------ | ----------- 9 | [Cliar](https://moigagoo.github.io/cliar/) | ✔ | ✔ | ✔ | ✔ | ✔ 10 | [Click](http://click.pocoo.org/) | ❌ | ✔ | ❌ | ✔ | ✔ 11 | [docopt](http://docopt.org/) | ❌ | ❌ | ✔ | ❌ | ❌ 12 | 13 | !!! note 14 | Of course, any tool lets you do anything. When I say "feature X is not supported by tool Y," I mean that the effort needed to implement X with Y is *in my opinion* too high. 15 | 16 | Conclusions are based on official docs of the tools being compared. 17 | 18 | Feel free to disagree. 19 | 20 | 21 | ## Modular CLIs 22 | 23 | Imagine you're developing a CLI-based audio player. You want it to have a ton of features but you don't want to develop them all on your own. The core version will only play a given file, but the rest of the functionality will be implemented with extensions. 24 | 25 | You want the usage of the player to be something like this: 26 | 27 | ```shell 28 | $ player play file.mp3 # Core functionality 29 | $ pip install player.ext.seek # Install extension 30 | $ player seek "1:23" # Extension-provided functionality 31 | ``` 32 | 33 | This approach has several benefits: 34 | 35 | - the user will be able to install only the parts they need 36 | - you will be able to delegate responsibility to the community 37 | - new commands are added via a unified API, which is the same for core and third-party developers 38 | 39 | So, your job is provide a way for third parties to add commands to the basic CLI and override existing commands. 40 | 41 | **With docopt** this is almost impossible since CLIs are declared in plaintext using a DSL: 42 | 43 | ```python 44 | '''Player. 45 | 46 | Usage: 47 | player play 48 | player (-h | --help) 49 | 50 | Options: 51 | -h --help Show this screen. 52 | ''' 53 | from docopt import docopt 54 | 55 | 56 | if __name__ == '__main__': 57 | arguments = docopt(__doc__) 58 | ``` 59 | 60 | Adding a new command means adding a line into a spec before it gets parsed, so the only way an extension can add a new command to the base CLI is by inserting lines into the base CLI spec. This is inconvenient if you're adding one command, but it's a nightmare if you're creating an API for adding unlimited commands: 61 | 62 | ```python 63 | '''Player. 64 | 65 | Usage: 66 | player play 67 | player (-h | --help) 68 | 69 | Options: 70 | -h --help Show this screen. 71 | ''' 72 | from docopt import docopt 73 | 74 | from player.ext.seek import insert_seek_command 75 | # "insert_seek_command" function inserts "player seek " 76 | # after "player play". You can already feel how quickly it gets old. 77 | if __name__ == '__main__': 78 | extended_doc = insert_seek_command(__doc__) 79 | arguments = docopt(extended_doc) 80 | ``` 81 | 82 | **With Click**, you can reuse commands from one CLI in another one: 83 | 84 | ```python 85 | # In file a: 86 | cli = click.Group() 87 | 88 | @cli.command() 89 | def cmd_a(): print("You called cmd_a") 90 | 91 | 92 | # In file b: 93 | 94 | from a import cli 95 | 96 | @cli.command() 97 | def cmd_b(): print("You called cmd_b") 98 | ``` 99 | 100 | > Thanks to /u/Brian for the [code sample](https://www.reddit.com/r/Python/comments/3j28oa/cliar_create_clis_clearly_cliar_103_documentation/culnqg2). 101 | 102 | However, you can't reuse commands from multiple third-party modules in one CLI, which is what we want. That's because command reuse relations are defined with decorators, and you can't decorate an imported function. In other words, you can create a new player that implements `seek` and borrows `play` from `player`, but you can't add `seek` into `player`. 103 | 104 | **With Cliar**, extending an existing CLI is trivial. Since in Cliar a CLI is a regular Python class, extending it means extending the class the most natural way—with inheritance. Just subclass your CLI from as many ``Cliar`` ancestors as you need: 105 | 106 | ```python 107 | from cliar import Cliar 108 | 109 | # Basic CLI: 110 | class BasicCLI(CLiar): 111 | def play(self, path): 112 | ... 113 | 114 | # Seek extension: 115 | class SeekCLI(Cliar): 116 | def seek(self, position): 117 | ... 118 | 119 | # Complete CLI: 120 | 121 | class CLI(BasicCLI, SeekCLI, *MoreExtensions): 122 | '''The complete CLI that borrows from the basic CLI and extensionss. 123 | 124 | Notice that the class body is empty: the logic is already implemented by the parents. 125 | ''' 126 | pass 127 | ``` 128 | 129 | Cliar relies on Python's standard mechanisms and doesn't reinvent the wheel when it comes to adding new features to objects. Python supports both single and multiple inheritance, so CLI extension goes both ways: you can create a completely new interface that borrows from an existing one or build an interface from extensions. 130 | 131 | 132 | ## DSL-Free 133 | 134 | DSLs should be avoided when pure Python is enough. A DSL requires time to learn, and the knowledge you gain is useless outside the scope of the DSL, which is by definition the app it's used in. 135 | 136 | !!! note 137 | 138 | This thought has been explained by Robert E Brewer in [The Zen in CherryPy](https://pyvideo.org/pycon-us-2010/pycon-2010--the-zen-of-cherrypy---111.html). 139 | 140 | **In Docopt**, you describe your CLI using a DSL. Then, you ask docopt to parse the commandline string and pass the extracted values to the business logic. The interface is completely separated from the business logic. 141 | 142 | It may seem a good idea until you actually start using docopt. What happens is you end up duplicating argument definitions all the time: 143 | 144 | ```python 145 | '''Player. 146 | 147 | Usage: 148 | player play 149 | player seek 150 | player (-h | --help) 151 | 152 | Options: 153 | -h --help Show this screen. 154 | ''' # one time 155 | from docopt import docopt 156 | 157 | 158 | def play(file): # two times 159 | ... 160 | 161 | def seek(position): 162 | ... 163 | 164 | if __name__ == '__main__': 165 | arguments = docopt(__doc__) 166 | 167 | if arguments.get('play'): # three times 168 | play(arguments['']) 169 | elif arguments.get('seek'): 170 | seek(arguments['']) 171 | ... # ...and it goes on and on and on. 172 | ``` 173 | 174 | Even in this toy example you can see how much redundant code this pattern spawns. 175 | 176 | **Click** and **Cliar** are DSL-free. Whereas docopt is "spec first," Click and Cliar are "code first": they generate the usage text from the code, not the other way around. 177 | 178 | 179 | ## Magic-Free 180 | 181 | *Magic* is unusual behavior driven by a hidden mechanism. It may give a short "wow" effect, but the price to pay is that code becomes harder to debug and harder to follow. Writing idiomatic Python generally means avoiding magic. 182 | 183 | To see if a tool is "magical," remove it from the code and see if the code breaks. 184 | 185 | **Docopt**, for example, is magic-free. If you remove the `__doc__` parsing part, the remaining code is still 100% valid Python. Removing docopt does not break you program, it just removes the commandline parsing functionality: 186 | 187 | ```python 188 | '''Player. 189 | 190 | Usage: 191 | player play 192 | player (-h | --help) 193 | 194 | Options: 195 | -h --help Show this screen. 196 | ''' 197 | # from docopt import docopt 198 | 199 | if __name__ == '__main__': 200 | # arguments = docopt(__doc__) 201 | pass 202 | ``` 203 | 204 | **Click**, on the other hand, is full of magic. Let's examine the hello world example from the [Click documentation](http://click.pocoo.org/): 205 | 206 | ```python 207 | import click 208 | 209 | @click.command() 210 | @click.option('--count', default=1, help='Number of greetings.') 211 | @click.option('--name', prompt='Your name', 212 | help='The person to greet.') 213 | def hello(count, name): 214 | """Simple program that greets NAME for a total of COUNT times.""" 215 | for x in range(count): 216 | click.echo('Hello %s!' % name) 217 | 218 | if __name__ == '__main__': 219 | hello() 220 | ``` 221 | 222 | Note that `hello` function accepts two positional arguments, `count` and `name`, but we call it without any arguments. That's because the params are added by the decorators based on the arguments of the decorator generators (`--count` and `--name`). This is broken code only forced to work by the magic of Click's decorators. 223 | 224 | **Cliar** is magic-free. Your CLI classes are regular Python classes. If you remove `Cliar` from its parents, the class will remain functional. It will continue to contain all the business logic, only without the CLI: 225 | 226 | ```python 227 | # from cliar import Cliar 228 | 229 | # class Player(Cliar): 230 | class Player(object): 231 | def play(self, file): 232 | print(f'Playing {file}') 233 | ``` 234 | 235 | Cliar's decorators like `set_name` or `add_aliases` can also be safely remove without breaking any code. 236 | 237 | 238 | ## Type Casting 239 | 240 | In commandline, any argument or flag value is a string. Converting strings to numbers and other types manually within business logic is tedious, requires dancing with exception handling, and, most importantly, has nothing to do with the business logic itself: it's a necessity induced by the fact the shell works only with strings and Python works with all sorts of types. 241 | 242 | **Docopt** doesn't attempt to cast types. It just parses a string into smaller ones in a nicely structured way, leaving all the necessary processing to the programmer: 243 | 244 | ```python 245 | args = docopt(__doc__) 246 | 247 | if args['play']: 248 | file = Path(args['']) 249 | ``` 250 | 251 | **Click** lets you define an argument and option type in the decorator constructor: 252 | 253 | ```python 254 | @click.argument('num', type=int) 255 | ``` 256 | 257 | If the type is not set, Click tries to infer it from the default value. It that's not set as well, string is assumed. 258 | 259 | **Cliar** lets you define argument and option type with type hints. The logic is similar to Click's: if the type hint is given, use it, if not, infer the type from the default value, otherwise assume string: 260 | 261 | ```python 262 | def play(file: Path, num=1) 263 | ``` 264 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | ../../README.md 2 | -------------------------------------------------------------------------------- /docs/src/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This quick tutorial will guide you through all Cliar's features. We'll start with a simple "Hello World" example and progressively add features to it. 4 | 5 | > Download the complete app: [greeter.py](assets/greeter.py) 6 | 7 | 8 | ## Hello World 9 | 10 | Here's the simplest "Hello World" program with Cliar: 11 | 12 | ```python 13 | from cliar import Cliar 14 | 15 | class Greeter(Cliar): 16 | def hello(self): 17 | print('Hello World!') 18 | 19 | if __name__ == '__main__': 20 | Greeter().parse() 21 | ``` 22 | 23 | In Cliar, CLI is a subclass of `Cliar`, with its methods turned into commands and their params into args. 24 | 25 | Save this code into `greeter.py` and run it: 26 | 27 | ```shell 28 | $ python greeter.py hello 29 | Hello World! 30 | ``` 31 | 32 | 33 | ## Optional Flags 34 | 35 | Let's add a `--shout` flag to `hello`: 36 | 37 | ```python 38 | def hello(self, shout=False): 39 | greeting = 'Hello World!' 40 | print(greeting.upper() if shout else greeting) 41 | 42 | ``` 43 | 44 | Try running `greeter.py` with and without the newly defined flag: 45 | 46 | ```shell 47 | $ python greeter.py hello --shout 48 | HELLO WORLD! 49 | 50 | $ python greeter.py hello -s 51 | HELLO WORLD! 52 | 53 | $ python greeter.py hello 54 | Hello World! 55 | ``` 56 | 57 | 58 | ## Positional Arguments 59 | 60 | Positional args are added the same way as flags. 61 | 62 | ```python 63 | def hello(self, name, shout=False): 64 | greeting = f'Hello {name}!' 65 | print(greeting.upper() if shout else greeting) 66 | ``` 67 | 68 | Try it: 69 | 70 | ```shell 71 | $ python greeter.py hello John 72 | Hello John! 73 | 74 | $ python greeter.py hello John --shout 75 | HELLO JOHN! 76 | 77 | $ python greeter.py hello -s John 78 | HELLO JOHN! 79 | 80 | $ python greeter.py hello 81 | usage: greeter.py hello [-h] [-s] name 82 | greeter.py hello: error: the following arguments are required: name 83 | ``` 84 | 85 | 86 | ## Help Messages 87 | 88 | Cliar automatically registers `--help` flag for the program itself and its every command: 89 | 90 | ```shell 91 | $ python greeter.py --help 92 | usage: greeter.py [-h] {hello} ... 93 | 94 | optional arguments: 95 | -h, --help show this help message and exit 96 | 97 | commands: 98 | {hello} Available commands: 99 | hello 100 | 101 | $ python greeter.py hello --help 102 | usage: greeter.py hello [-h] [-s] name 103 | 104 | positional arguments: 105 | name 106 | 107 | optional arguments: 108 | -h, --help show this help message and exit 109 | -s, --shout 110 | ``` 111 | 112 | Command help messages are generated from docstrings. Let's add them: 113 | 114 | ```python 115 | class Greeter(Cliar): 116 | '''Greeter app created with Cliar.''' 117 | 118 | def hello(self, name, shout=False): 119 | '''Say hello.''' 120 | 121 | greeting = f'Hello {name}!' 122 | print(greeting.upper() if shout else greeting) 123 | ``` 124 | 125 | and view the updated help message: 126 | 127 | ```shell 128 | $ python greeter.py -h 129 | usage: greeter.py [-h] {hello} ... 130 | 131 | Greeter app created with Cliar. 132 | 133 | optional arguments: 134 | -h, --help show this help message and exit 135 | 136 | commands: 137 | {hello} Available commands: 138 | hello Say hello. 139 | 140 | $ python greeter.py hello -h 141 | usage: greeter.py hello [-h] [-s] name 142 | 143 | Say hello. 144 | 145 | positional arguments: 146 | name 147 | 148 | optional arguments: 149 | -h, --help show this help message and exit 150 | -s, --shout 151 | ``` 152 | 153 | To add description for arguments, use `set_help` decorator: 154 | 155 | ```python 156 | from cliar import Cliar, set_help 157 | 158 | ... 159 | 160 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 161 | def hello(self, name, shout=False): 162 | '''Say hello.''' 163 | 164 | greeting = f'Hello {name}!' 165 | print(greeting.upper() if shout else greeting) 166 | ``` 167 | 168 | The decorator takes a mapping from param names to help messages. 169 | 170 | Call the help message for `hello` command: 171 | 172 | ```shell 173 | $ python greeter.py hello -h 174 | usage: greeter.py hello [-h] [-s] name 175 | 176 | Say hello. 177 | 178 | positional arguments: 179 | name Who to greet 180 | 181 | optional arguments: 182 | -h, --help show this help message and exit 183 | -s, --shout Shout the greeting 184 | ``` 185 | 186 | To show default values for flags, add `show_defaults = True` to `set_help`: 187 | 188 | ```python 189 | ... 190 | @set_help( 191 | {'name': 'Who to greet', 'shout': 'Shout the greeting'}, 192 | show_defaults = True 193 | ) 194 | def hello(self, name, shout=False): 195 | ... 196 | ``` 197 | 198 | Call help again to see the default value: 199 | 200 | ```shell 201 | $ python greeter.py hello -h 202 | usage: greeter.py hello [-h] [-s] name 203 | 204 | Say hello. 205 | 206 | positional arguments: 207 | name Who to greet 208 | 209 | optional arguments: 210 | -h, --help show this help message and exit 211 | -s, --shout Shout the greeting (default: False) 212 | ``` 213 | 214 | 215 | ## Metavars 216 | 217 | *Metavar* is a placeholder of a positional arg as it appears in the help message. By default, Cliar uses the param name as its metavar. So, for `name` param the metavar is called `name`: 218 | 219 | ```shell 220 | $ python greeter.py hello -h 221 | usage: greeter.py hello [-h] [-s] name 222 | 223 | Say hello. 224 | 225 | positional arguments: 226 | name Who to greet 227 | 228 | optional arguments: 229 | -h, --help show this help message and exit 230 | -s, --shout Set to shout the greeting 231 | ``` 232 | 233 | To set a different metavar for a param, usd `set_metavars` decorator: 234 | 235 | ```python 236 | from cliar import Cliar, set_help, set_metavars 237 | 238 | ... 239 | 240 | @set_metavars({'name': 'NAME'}) 241 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 242 | def hello(self, name, shout=False): 243 | '''Say hello.''' 244 | 245 | greeting = f'Hello {name}!' 246 | print(greeting.upper() if shout else greeting) 247 | ``` 248 | 249 | The decorator takes a mapping from param names to metavars. 250 | 251 | Call the help message for `hello`: 252 | 253 | ```shell 254 | usage: greeter.py hello [-h] [-s] NAME 255 | 256 | Say hello. 257 | 258 | positional arguments: 259 | NAME Who to greet 260 | 261 | optional arguments: 262 | -h, --help show this help message and exit 263 | -s, --shout Set to shout the greeting 264 | ``` 265 | 266 | 267 | ## Type Casting 268 | 269 | Cliar casts arg types of args on the fly. To use type casting, add type hints or default values to params. 270 | 271 | Let's add `-n` flag that will tell how many times to repeat the greeting: 272 | 273 | ```python 274 | @set_metavars({'name': 'NAME'}) 275 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 276 | def hello(self, name, n=1, shout=False): 277 | '''Say hello.''' 278 | 279 | greeting = f'Hello {name}!' 280 | 281 | for _ in range(n): 282 | print(greeting.upper() if shout else greeting) 283 | ``` 284 | 285 | Let's call `hello` with the new flag: 286 | 287 | ```shell 288 | $ python greeter.py hello John -n 2 289 | Hello John! 290 | Hello John! 291 | ``` 292 | 293 | If we pass a non-integer value to `-n`, an error occurs: 294 | 295 | ```shell 296 | $ python greeter.py hello John -n foo 297 | usage: greeter.py hello [-h] [-n N] [-s] NAME 298 | greeter.py hello: error: argument -n/--n: invalid int value: 'foo' 299 | ``` 300 | 301 | !!! hint 302 | 303 | You can use any callable as a param type, and it will be called to cast the param type during parsing. One useful example is using `open` as the param type: 304 | 305 | def read_from_file(input_file: open): 306 | lines = input_file.readlines() 307 | 308 | If you pass a path to such a param, Cliar will open it and pass the resulting file-like object to the handler body. And when the handler returns, Cliar will make sure the file gets closed. 309 | 310 | 311 | ## Argument Names 312 | 313 | By default, Cliar takes the param name, replaces underscores with dashes, and uses that as the corresponding arg name: `name` is turned into `--name`, and `upper_limit` into `--upper-limit`; the first letter is used as a short option: `-n` for `--name`, `-u` for `--upper-limit`. 314 | 315 | To use different arg names, use `set_arg_map` decorator: 316 | 317 | ```python 318 | from cliar import Cliar, set_help, set_metavars, set_arg_map 319 | 320 | ... 321 | 322 | @set_arg_map({'n': 'repeat'}) 323 | @set_metavars({'name': 'NAME'}) 324 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 325 | def hello(self, name, n=1, shout=False): 326 | '''Say hello.''' 327 | 328 | greeting = f'Hello {name}!' 329 | 330 | for _ in range(n): 331 | print(greeting.upper() if shout else greeting) 332 | ``` 333 | 334 | Now use `--repeat` or `-r` instead of `-n`: 335 | 336 | ```shell 337 | $ python greeter.py hello John --repeat 2 338 | Hello John! 339 | Hello John! 340 | 341 | $ python greeter.py hello John -r 2 342 | Hello John! 343 | Hello John! 344 | ``` 345 | 346 | !!! hint 347 | 348 | This decorator lets you use Python's reserved words like `--for` and `--with` as arg names. 349 | 350 | You can also override argument short names specifically, with `set_sharg_map` decorator: 351 | 352 | ```python 353 | from cliar import Cliar, set_help, set_metavars, set_arg_map, set_sharg_map 354 | 355 | ... 356 | 357 | @set_arg_map({'n': 'repeat'}) 358 | @set_sharg_map({'n': 'n'}) 359 | @set_metavars({'name': 'NAME'}) 360 | @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'}) 361 | 362 | def hello(self, name, n=1, shout=False): 363 | '''Say hello.''' 364 | 365 | greeting = f'Hello {name}!' 366 | 367 | for _ in range(n): 368 | print(greeting.upper() if shout else greeting) 369 | ``` 370 | 371 | Now you can use `-n` instead of `-r`: 372 | 373 | ```shell 374 | $ python greeter.py hello John --repeat 2 375 | Hello John! 376 | Hello John! 377 | 378 | $ python greeter.py hello John -n 2 379 | Hello John! 380 | Hello John! 381 | ``` 382 | 383 | This is useful when you have several arguments that start with the same letter, which creates a conflict between short arg names. 384 | 385 | To disable short argument variant entirely, set the short arg name to `None`: ```@set_sharg_map({'argname': None})```. 386 | 387 | 388 | ## Multiple Commands 389 | 390 | Adding more commands to the CLI simply means adding more methods to the CLI class: 391 | 392 | ```python 393 | class Greeter(Cliar): 394 | def goodbye(self, name): 395 | '''Say goodbye''' 396 | 397 | print(f'Goodbye {name}!') 398 | 399 | @set_arg_map({'n': 'repeat'}) 400 | ... 401 | ``` 402 | 403 | With this code addition, you can call `goodbye` command: 404 | 405 | ```shell 406 | $ python greeter.py goodbye Mary 407 | Goodbye Mary! 408 | ``` 409 | 410 | 411 | ## Nested Commands 412 | 413 | You can have any level of nested commands by adding Cliar CLIs as class attributes. 414 | 415 | For example, let's add a `utils` subcommand with its own `time` subcommand that has `now` command: 416 | 417 | ```python 418 | class Time(Cliar): 419 | def now(self, utc=False): 420 | now_ctime = datetime.utcnow().ctime() if utc else datetime.now().ctime() 421 | print(f'UTC time is {now_ctime}') 422 | 423 | class Utils(Cliar): 424 | time = Time 425 | 426 | class Greeter(Cliar): 427 | '''Greeter app created with in Cliar.''' 428 | 429 | utils = Utils 430 | 431 | def _root(self, version=False): 432 | ... 433 | ``` 434 | 435 | You can now call `now` command through `utils`: 436 | 437 | ```shell 438 | $ python greeter.py utils time now 439 | Local time is Sun Jul 21 15:25:52 2019 440 | 441 | $ python greeter.py utils time now --utc 442 | UTC time is Sun Jul 21 11:25:57 2019 443 | ``` 444 | 445 | 446 | ## Command Aliases 447 | 448 | To add aliases to a command, use `add_aliases` decorator: 449 | 450 | ```python 451 | from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases 452 | 453 | ... 454 | 455 | @add_aliases(['mientras', 'пока']) 456 | def goodbye(self, name): 457 | '''Say goodbye''' 458 | 459 | print(f'Goodbye {name}!') 460 | ``` 461 | 462 | Now you can call `goodbye` command by its aliases: 463 | 464 | ```shell 465 | $ python greeter.py mientras Maria 466 | Goodbye Maria! 467 | 468 | $ python greeter.py пока Маша 469 | Goodbye Маша! 470 | ``` 471 | 472 | 473 | ## Command Names 474 | 475 | By default, CLI commands are named after the corresponding methods. To override this behavior and set a custom command name, use `set_name` decorator: 476 | 477 | ```python 478 | from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases, set_name 479 | 480 | 481 | class Greeter(Cliar): 482 | '''Greeter app created with in Cliar.''' 483 | 484 | @set_name('factorial') # Name the command `factorial` 485 | def calculate_factorial(self, n: int): # because `calculate_factorial` 486 | '''Calculate factorial''' # is too long for CLI. 487 | 488 | print(f'n! = {factorial(n)}') 489 | ``` 490 | 491 | Now `calculate_factorial` is called with `factorial` command: 492 | 493 | ```shell 494 | $ python greeter.py factorial 4 495 | n! = 24 496 | 497 | $ python greeter.py calculate_factorial 4 498 | usage: greeter.py [-h] {factorial,goodbye,mientras,пока,hello} ... 499 | greeter.py: error: argument command: invalid choice: 'calculate_factorial' (choose from 'factorial', 'goodbye', 'mientras', 'пока', 'hello') 500 | ``` 501 | 502 | 503 | ## Ignore Methods 504 | 505 | By default, Cliar converts all non-static and non-class methods of the `Cliar` subclass into CLI commands. 506 | 507 | There are two ways to tell Cliar *not* to convert a method into a command: start its name with an underscore or use `ignore` decorator: 508 | 509 | ```python 510 | from math import factorial, tau, pi 511 | 512 | from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases, set_name, ignore 513 | 514 | 515 | class Greeter(Cliar): 516 | '''Greeter app created with in Cliar.''' 517 | 518 | def _get_tau_value(self): 519 | return tau 520 | 521 | @ignore 522 | def get_pi_value(self): 523 | return pi 524 | 525 | def constants(self): 526 | print(f'τ = {self._get_tau_value()}') 527 | print(f'π = {self.get_pi_value()}') 528 | 529 | ... 530 | ``` 531 | 532 | Only `constants` method will be exposed as a CLI command: 533 | 534 | ```shell 535 | $ python greeter.py constants 536 | τ = 6.283185307179586 537 | π = 3.141592653589793 538 | 539 | $ python greeter.py get-pi-value 540 | usage: greeter.py [-h] {factorial,constants,goodbye,mientras,пока,hello} ... 541 | greeter.py: error: argument command: invalid choice: 'get-pi-value' (choose from 'factorial', 'constants', 'goodbye', 'mientras', 'пока', 'hello') 542 | ``` 543 | 544 | 545 | ## Root Command 546 | 547 | To assign action to the root command, i.e. the script itself, define `_root` method: 548 | 549 | ```python 550 | class Greeter(Cliar): 551 | '''Greeter app created with in Cliar.''' 552 | 553 | def _root(self, version=False): 554 | print('Greeter 1.0.0.' if version else 'Welcome to Greeter!') 555 | ... 556 | ``` 557 | 558 | If you run `greeter.py` with `--version` or `-v` flag, you'll see its version. If you call `greeter.py` without any flags or commands, you'll see a welcome message: 559 | 560 | ```shell 561 | $ python greeter.py 562 | Welcome to Greeter! 563 | 564 | $ python greeter.py --version 565 | Greeter 1.0.0. 566 | ``` 567 | 568 | 569 | ## Global Arguments 570 | 571 | Global arguments defined in `_root` can be accessed in commands via `self.global_args`: 572 | 573 | ```python 574 | ... 575 | 576 | def constants(self): 577 | if self.global_args.get('version'): 578 | print('Greeter 1.0.0.') 579 | 580 | print(f'τ = {self._get_tau_value()}') 581 | print(f'π = {self.get_pi_value()}') 582 | 583 | ... 584 | ``` 585 | 586 | Run `constants` with `--version`: 587 | 588 | ```shell 589 | $ python greeter.py --version constants 590 | Greeter 1.0.0. 591 | τ = 6.283185307179586 592 | π = 3.141592653589793 593 | ``` 594 | 595 | This works with [nested commands](#nested-commands), too. Global arguments of nested commands override global arguments of their parents. 596 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.8.3" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = "~=3.6" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | setuptools = ">=20.0" 12 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 13 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 14 | wrapt = ">=1.11,<1.14" 15 | 16 | [[package]] 17 | name = "atomicwrites" 18 | version = "1.4.0" 19 | description = "Atomic file writes." 20 | category = "dev" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "21.2.0" 27 | description = "Classes Without Boilerplate" 28 | category = "dev" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 31 | 32 | [package.extras] 33 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 34 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 35 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 36 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 37 | 38 | [[package]] 39 | name = "certifi" 40 | version = "2021.10.8" 41 | description = "Python package for providing Mozilla's CA Bundle." 42 | category = "dev" 43 | optional = false 44 | python-versions = "*" 45 | 46 | [[package]] 47 | name = "charset-normalizer" 48 | version = "2.0.7" 49 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.5.0" 53 | 54 | [package.extras] 55 | unicode_backport = ["unicodedata2"] 56 | 57 | [[package]] 58 | name = "click" 59 | version = "8.0.3" 60 | description = "Composable command line interface toolkit" 61 | category = "dev" 62 | optional = false 63 | python-versions = ">=3.6" 64 | 65 | [package.dependencies] 66 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 67 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 68 | 69 | [[package]] 70 | name = "codecov" 71 | version = "2.1.12" 72 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 76 | 77 | [package.dependencies] 78 | coverage = "*" 79 | requests = ">=2.7.9" 80 | 81 | [[package]] 82 | name = "colorama" 83 | version = "0.4.4" 84 | description = "Cross-platform colored terminal text." 85 | category = "dev" 86 | optional = false 87 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 88 | 89 | [[package]] 90 | name = "contextlib2" 91 | version = "21.6.0" 92 | description = "Backports and enhancements for the contextlib module" 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.6" 96 | 97 | [[package]] 98 | name = "coverage" 99 | version = "6.0.2" 100 | description = "Code coverage measurement for Python" 101 | category = "dev" 102 | optional = false 103 | python-versions = ">=3.6" 104 | 105 | [package.extras] 106 | toml = ["tomli"] 107 | 108 | [[package]] 109 | name = "foliant" 110 | version = "1.0.12" 111 | description = "Modular, Markdown-based documentation generator that makes pdf, docx, html, and more." 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.6,<4.0" 115 | 116 | [package.dependencies] 117 | cliar = ">=1.3.2,<2.0.0" 118 | prompt_toolkit = ">=2.0,<3.0" 119 | pyyaml = ">=5.1.1,<6.0.0" 120 | 121 | [[package]] 122 | name = "foliantcontrib.escapecode" 123 | version = "1.0.4" 124 | description = "Preprocessor for Foliant to escape/unescape raw content." 125 | category = "dev" 126 | optional = false 127 | python-versions = "*" 128 | 129 | [package.dependencies] 130 | foliant = ">=1.0.4" 131 | 132 | [[package]] 133 | name = "foliantcontrib.includes" 134 | version = "1.1.13" 135 | description = "Powerful includes for Foliant doc maker." 136 | category = "dev" 137 | optional = false 138 | python-versions = "*" 139 | 140 | [package.dependencies] 141 | foliant = ">=1.0.12" 142 | "foliantcontrib.escapecode" = ">=1.0.3" 143 | "foliantcontrib.meta" = ">=1.3.0" 144 | 145 | [[package]] 146 | name = "foliantcontrib.meta" 147 | version = "1.3.3" 148 | description = "Metadata for Foliant." 149 | category = "dev" 150 | optional = false 151 | python-versions = "*" 152 | 153 | [package.dependencies] 154 | foliant = ">=1.0.4" 155 | "foliantcontrib.utils" = ">=1.0.2" 156 | schema = ">=0.7.0" 157 | 158 | [[package]] 159 | name = "foliantcontrib.mkdocs" 160 | version = "1.0.12" 161 | description = "MkDocs backend for Foliant documentation generator." 162 | category = "dev" 163 | optional = false 164 | python-versions = "*" 165 | 166 | [package.dependencies] 167 | foliant = ">=1.0.8" 168 | mkdocs = ">=1.0.4" 169 | 170 | [[package]] 171 | name = "foliantcontrib.utils" 172 | version = "1.0.3" 173 | description = "Utils for foliant plugin developers" 174 | category = "dev" 175 | optional = false 176 | python-versions = "*" 177 | 178 | [package.dependencies] 179 | foliant = ">=1.0.8" 180 | "foliantcontrib.meta" = ">=1.2.3" 181 | PyYAML = "*" 182 | 183 | [[package]] 184 | name = "ghp-import" 185 | version = "2.0.2" 186 | description = "Copy your docs directly to the gh-pages branch." 187 | category = "dev" 188 | optional = false 189 | python-versions = "*" 190 | 191 | [package.dependencies] 192 | python-dateutil = ">=2.8.1" 193 | 194 | [package.extras] 195 | dev = ["twine", "markdown", "flake8", "wheel"] 196 | 197 | [[package]] 198 | name = "idna" 199 | version = "3.3" 200 | description = "Internationalized Domain Names in Applications (IDNA)" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.5" 204 | 205 | [[package]] 206 | name = "importlib-metadata" 207 | version = "4.8.1" 208 | description = "Read metadata from Python packages" 209 | category = "dev" 210 | optional = false 211 | python-versions = ">=3.6" 212 | 213 | [package.dependencies] 214 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 215 | zipp = ">=0.5" 216 | 217 | [package.extras] 218 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 219 | perf = ["ipython"] 220 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 221 | 222 | [[package]] 223 | name = "iniconfig" 224 | version = "1.1.1" 225 | description = "iniconfig: brain-dead simple config-ini parsing" 226 | category = "dev" 227 | optional = false 228 | python-versions = "*" 229 | 230 | [[package]] 231 | name = "isort" 232 | version = "5.8.0" 233 | description = "A Python utility / library to sort Python imports." 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=3.6,<4.0" 237 | 238 | [package.extras] 239 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 240 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 241 | colors = ["colorama (>=0.4.3,<0.5.0)"] 242 | 243 | [[package]] 244 | name = "jinja2" 245 | version = "3.0.2" 246 | description = "A very fast and expressive template engine." 247 | category = "dev" 248 | optional = false 249 | python-versions = ">=3.6" 250 | 251 | [package.dependencies] 252 | MarkupSafe = ">=2.0" 253 | 254 | [package.extras] 255 | i18n = ["Babel (>=2.7)"] 256 | 257 | [[package]] 258 | name = "lazy-object-proxy" 259 | version = "1.6.0" 260 | description = "A fast and thorough lazy object proxy." 261 | category = "dev" 262 | optional = false 263 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 264 | 265 | [[package]] 266 | name = "markdown" 267 | version = "3.3.4" 268 | description = "Python implementation of Markdown." 269 | category = "dev" 270 | optional = false 271 | python-versions = ">=3.6" 272 | 273 | [package.dependencies] 274 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 275 | 276 | [package.extras] 277 | testing = ["coverage", "pyyaml"] 278 | 279 | [[package]] 280 | name = "markupsafe" 281 | version = "2.0.1" 282 | description = "Safely add untrusted strings to HTML/XML markup." 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=3.6" 286 | 287 | [[package]] 288 | name = "mccabe" 289 | version = "0.6.1" 290 | description = "McCabe checker, plugin for flake8" 291 | category = "dev" 292 | optional = false 293 | python-versions = "*" 294 | 295 | [[package]] 296 | name = "mergedeep" 297 | version = "1.3.4" 298 | description = "A deep merge function for 🐍." 299 | category = "dev" 300 | optional = false 301 | python-versions = ">=3.6" 302 | 303 | [[package]] 304 | name = "mkdocs" 305 | version = "1.2.3" 306 | description = "Project documentation with Markdown." 307 | category = "dev" 308 | optional = false 309 | python-versions = ">=3.6" 310 | 311 | [package.dependencies] 312 | click = ">=3.3" 313 | ghp-import = ">=1.0" 314 | importlib-metadata = ">=3.10" 315 | Jinja2 = ">=2.10.1" 316 | Markdown = ">=3.2.1" 317 | mergedeep = ">=1.3.4" 318 | packaging = ">=20.5" 319 | PyYAML = ">=3.10" 320 | pyyaml-env-tag = ">=0.1" 321 | watchdog = ">=2.0" 322 | 323 | [package.extras] 324 | i18n = ["babel (>=2.9.0)"] 325 | 326 | [[package]] 327 | name = "mkdocs-material" 328 | version = "4.6.3" 329 | description = "A Material Design theme for MkDocs" 330 | category = "dev" 331 | optional = false 332 | python-versions = "*" 333 | 334 | [package.dependencies] 335 | markdown = ">=3.2" 336 | mkdocs = ">=1.0" 337 | Pygments = ">=2.4" 338 | pymdown-extensions = ">=6.3" 339 | 340 | [[package]] 341 | name = "packaging" 342 | version = "21.0" 343 | description = "Core utilities for Python packages" 344 | category = "dev" 345 | optional = false 346 | python-versions = ">=3.6" 347 | 348 | [package.dependencies] 349 | pyparsing = ">=2.0.2" 350 | 351 | [[package]] 352 | name = "platformdirs" 353 | version = "2.4.0" 354 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 355 | category = "dev" 356 | optional = false 357 | python-versions = ">=3.6" 358 | 359 | [package.extras] 360 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 361 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 362 | 363 | [[package]] 364 | name = "pluggy" 365 | version = "1.0.0" 366 | description = "plugin and hook calling mechanisms for python" 367 | category = "dev" 368 | optional = false 369 | python-versions = ">=3.6" 370 | 371 | [package.dependencies] 372 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 373 | 374 | [package.extras] 375 | dev = ["pre-commit", "tox"] 376 | testing = ["pytest", "pytest-benchmark"] 377 | 378 | [[package]] 379 | name = "prompt-toolkit" 380 | version = "2.0.10" 381 | description = "Library for building powerful interactive command lines in Python" 382 | category = "dev" 383 | optional = false 384 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 385 | 386 | [package.dependencies] 387 | six = ">=1.9.0" 388 | wcwidth = "*" 389 | 390 | [[package]] 391 | name = "py" 392 | version = "1.10.0" 393 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 394 | category = "dev" 395 | optional = false 396 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 397 | 398 | [[package]] 399 | name = "pygments" 400 | version = "2.10.0" 401 | description = "Pygments is a syntax highlighting package written in Python." 402 | category = "dev" 403 | optional = false 404 | python-versions = ">=3.5" 405 | 406 | [[package]] 407 | name = "pylint" 408 | version = "2.11.1" 409 | description = "python code static checker" 410 | category = "dev" 411 | optional = false 412 | python-versions = "~=3.6" 413 | 414 | [package.dependencies] 415 | astroid = ">=2.8.0,<2.9" 416 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 417 | isort = ">=4.2.5,<6" 418 | mccabe = ">=0.6,<0.7" 419 | platformdirs = ">=2.2.0" 420 | toml = ">=0.7.1" 421 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 422 | 423 | [[package]] 424 | name = "pymdown-extensions" 425 | version = "9.0" 426 | description = "Extension pack for Python Markdown." 427 | category = "dev" 428 | optional = false 429 | python-versions = ">=3.6" 430 | 431 | [package.dependencies] 432 | Markdown = ">=3.2" 433 | 434 | [[package]] 435 | name = "pyparsing" 436 | version = "2.4.7" 437 | description = "Python parsing module" 438 | category = "dev" 439 | optional = false 440 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 441 | 442 | [[package]] 443 | name = "pytest" 444 | version = "6.2.5" 445 | description = "pytest: simple powerful testing with Python" 446 | category = "dev" 447 | optional = false 448 | python-versions = ">=3.6" 449 | 450 | [package.dependencies] 451 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 452 | attrs = ">=19.2.0" 453 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 454 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 455 | iniconfig = "*" 456 | packaging = "*" 457 | pluggy = ">=0.12,<2.0" 458 | py = ">=1.8.2" 459 | toml = "*" 460 | 461 | [package.extras] 462 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 463 | 464 | [[package]] 465 | name = "pytest-cov" 466 | version = "2.12.1" 467 | description = "Pytest plugin for measuring coverage." 468 | category = "dev" 469 | optional = false 470 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 471 | 472 | [package.dependencies] 473 | coverage = ">=5.2.1" 474 | pytest = ">=4.6" 475 | toml = "*" 476 | 477 | [package.extras] 478 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 479 | 480 | [[package]] 481 | name = "pytest-datadir" 482 | version = "1.3.1" 483 | description = "pytest plugin for test data directories and files" 484 | category = "dev" 485 | optional = false 486 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 487 | 488 | [package.dependencies] 489 | pytest = ">=2.7.0" 490 | 491 | [[package]] 492 | name = "python-dateutil" 493 | version = "2.8.2" 494 | description = "Extensions to the standard Python datetime module" 495 | category = "dev" 496 | optional = false 497 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 498 | 499 | [package.dependencies] 500 | six = ">=1.5" 501 | 502 | [[package]] 503 | name = "pyyaml" 504 | version = "5.4.1" 505 | description = "YAML parser and emitter for Python" 506 | category = "dev" 507 | optional = false 508 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 509 | 510 | [[package]] 511 | name = "pyyaml-env-tag" 512 | version = "0.1" 513 | description = "A custom YAML tag for referencing environment variables in YAML files. " 514 | category = "dev" 515 | optional = false 516 | python-versions = ">=3.6" 517 | 518 | [package.dependencies] 519 | pyyaml = "*" 520 | 521 | [[package]] 522 | name = "requests" 523 | version = "2.26.0" 524 | description = "Python HTTP for Humans." 525 | category = "dev" 526 | optional = false 527 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 528 | 529 | [package.dependencies] 530 | certifi = ">=2017.4.17" 531 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 532 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 533 | urllib3 = ">=1.21.1,<1.27" 534 | 535 | [package.extras] 536 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 537 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 538 | 539 | [[package]] 540 | name = "schema" 541 | version = "0.7.4" 542 | description = "Simple data validation library" 543 | category = "dev" 544 | optional = false 545 | python-versions = "*" 546 | 547 | [package.dependencies] 548 | contextlib2 = ">=0.5.5" 549 | 550 | [[package]] 551 | name = "setuptools" 552 | version = "58.2.0" 553 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 554 | category = "dev" 555 | optional = false 556 | python-versions = ">=3.6" 557 | 558 | [package.extras] 559 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "sphinx-inline-tabs", "sphinxcontrib-towncrier", "furo"] 560 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "mock", "flake8-2020", "virtualenv (>=13.0.0)", "pytest-virtualenv (>=1.2.7)", "wheel", "paver", "pip (>=19.1)", "jaraco.envs", "pytest-xdist", "sphinx", "jaraco.path (>=3.2.0)", "pytest-black (>=0.3.7)", "pytest-mypy"] 561 | 562 | [[package]] 563 | name = "six" 564 | version = "1.16.0" 565 | description = "Python 2 and 3 compatibility utilities" 566 | category = "dev" 567 | optional = false 568 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 569 | 570 | [[package]] 571 | name = "toml" 572 | version = "0.10.2" 573 | description = "Python Library for Tom's Obvious, Minimal Language" 574 | category = "dev" 575 | optional = false 576 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 577 | 578 | [[package]] 579 | name = "typed-ast" 580 | version = "1.4.3" 581 | description = "a fork of Python 2 and 3 ast modules with type comment support" 582 | category = "dev" 583 | optional = false 584 | python-versions = "*" 585 | 586 | [[package]] 587 | name = "typing-extensions" 588 | version = "3.10.0.2" 589 | description = "Backported and Experimental Type Hints for Python 3.5+" 590 | category = "dev" 591 | optional = false 592 | python-versions = "*" 593 | 594 | [[package]] 595 | name = "urllib3" 596 | version = "1.26.7" 597 | description = "HTTP library with thread-safe connection pooling, file post, and more." 598 | category = "dev" 599 | optional = false 600 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 601 | 602 | [package.extras] 603 | brotli = ["brotlipy (>=0.6.0)"] 604 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 605 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 606 | 607 | [[package]] 608 | name = "watchdog" 609 | version = "2.1.6" 610 | description = "Filesystem events monitoring" 611 | category = "dev" 612 | optional = false 613 | python-versions = ">=3.6" 614 | 615 | [package.extras] 616 | watchmedo = ["PyYAML (>=3.10)"] 617 | 618 | [[package]] 619 | name = "wcwidth" 620 | version = "0.2.5" 621 | description = "Measures the displayed width of unicode strings in a terminal" 622 | category = "dev" 623 | optional = false 624 | python-versions = "*" 625 | 626 | [[package]] 627 | name = "wrapt" 628 | version = "1.13.2" 629 | description = "Module for decorators, wrappers and monkey patching." 630 | category = "dev" 631 | optional = false 632 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 633 | 634 | [[package]] 635 | name = "zipp" 636 | version = "3.6.0" 637 | description = "Backport of pathlib-compatible object wrapper for zip files" 638 | category = "dev" 639 | optional = false 640 | python-versions = ">=3.6" 641 | 642 | [package.extras] 643 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 644 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 645 | 646 | [metadata] 647 | lock-version = "1.1" 648 | python-versions = "^3.6" 649 | content-hash = "8ada49032bdfcfc6efb415b5d2bf94a2bf48f6733bd27ec5c318248a8ca13e8d" 650 | 651 | [metadata.files] 652 | astroid = [ 653 | {file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"}, 654 | {file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"}, 655 | ] 656 | atomicwrites = [ 657 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 658 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 659 | ] 660 | attrs = [ 661 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 662 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 663 | ] 664 | certifi = [ 665 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 666 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 667 | ] 668 | charset-normalizer = [ 669 | {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, 670 | {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, 671 | ] 672 | click = [ 673 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 674 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 675 | ] 676 | codecov = [ 677 | {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, 678 | {file = "codecov-2.1.12-py3.8.egg", hash = "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635"}, 679 | {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"}, 680 | ] 681 | colorama = [ 682 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 683 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 684 | ] 685 | contextlib2 = [ 686 | {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, 687 | {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, 688 | ] 689 | coverage = [ 690 | {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, 691 | {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, 692 | {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, 693 | {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, 694 | {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, 695 | {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, 696 | {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, 697 | {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, 698 | {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, 699 | {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, 700 | {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, 701 | {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, 702 | {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, 703 | {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, 704 | {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, 705 | {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, 706 | {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, 707 | {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, 708 | {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, 709 | {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, 710 | {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, 711 | {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, 712 | {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, 713 | {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, 714 | {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, 715 | {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, 716 | {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, 717 | {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, 718 | {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, 719 | {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, 720 | {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, 721 | {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, 722 | {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, 723 | ] 724 | foliant = [ 725 | {file = "foliant-1.0.12-py3-none-any.whl", hash = "sha256:22c98a0c4a08383092e340929a88076cdfe91e18543e64421ff047e0b2890530"}, 726 | {file = "foliant-1.0.12.tar.gz", hash = "sha256:a2c9f3e2056e336e47bc8c2966b27a9937a09f6c33966f48d8a561c74a3f35bb"}, 727 | ] 728 | "foliantcontrib.escapecode" = [ 729 | {file = "foliantcontrib.escapecode-1.0.4-py3-none-any.whl", hash = "sha256:6fa69632e12177c6a32feebfda72ba4e65afe1da12e009a38f9179ebda0a83e7"}, 730 | {file = "foliantcontrib.escapecode-1.0.4.tar.gz", hash = "sha256:18879fd27ec8c700b7e8dba9b834691b32b10e43326a7ab20c923222886ceb6f"}, 731 | ] 732 | "foliantcontrib.includes" = [ 733 | {file = "foliantcontrib.includes-1.1.13-py3-none-any.whl", hash = "sha256:f651a41f120c68fcbb82f6271bca021a724a8ff44b058b94b07fe756d4fc95b5"}, 734 | {file = "foliantcontrib.includes-1.1.13.tar.gz", hash = "sha256:dfed61d2cc731b7be13edc75b4204fc5d96b0c3cdb981534de6b861a58850217"}, 735 | ] 736 | "foliantcontrib.meta" = [ 737 | {file = "foliantcontrib.meta-1.3.3-py3-none-any.whl", hash = "sha256:1aa9565af2207eb523fc63f7c147265cd47b334c6a7a3f5b35208a108f84cd87"}, 738 | {file = "foliantcontrib.meta-1.3.3.tar.gz", hash = "sha256:9d033dc95ef7beefb401a4382f021fa9c278d6c2630969e121ce88b86543e3fc"}, 739 | ] 740 | "foliantcontrib.mkdocs" = [ 741 | {file = "foliantcontrib.mkdocs-1.0.12-py3-none-any.whl", hash = "sha256:4b15dab5e7251646960fdfbaf5efbb5b6e32ef519310220b4a944ef8f46133ee"}, 742 | {file = "foliantcontrib.mkdocs-1.0.12.tar.gz", hash = "sha256:7dcb11662bc601ea66ca167edd324beb920a8209c9ce824d3c9d17c9164f3f06"}, 743 | ] 744 | "foliantcontrib.utils" = [ 745 | {file = "foliantcontrib.utils-1.0.3-py3-none-any.whl", hash = "sha256:f471c7340fdf0be3f07f34f1bf33358129ff5b3baf61dbc95510f72777b4777c"}, 746 | {file = "foliantcontrib.utils-1.0.3.tar.gz", hash = "sha256:e93ae3b2cda3090fc757a0f6ca35e062d19b71b1525bc52ff0ebace726c883a7"}, 747 | ] 748 | ghp-import = [ 749 | {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, 750 | {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, 751 | ] 752 | idna = [ 753 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 754 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 755 | ] 756 | importlib-metadata = [ 757 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 758 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 759 | ] 760 | iniconfig = [ 761 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 762 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 763 | ] 764 | isort = [ 765 | {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, 766 | {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, 767 | ] 768 | jinja2 = [ 769 | {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, 770 | {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, 771 | ] 772 | lazy-object-proxy = [ 773 | {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, 774 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, 775 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, 776 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, 777 | {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, 778 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, 779 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, 780 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, 781 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, 782 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, 783 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, 784 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, 785 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, 786 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, 787 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, 788 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, 789 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, 790 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, 791 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, 792 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, 793 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, 794 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, 795 | ] 796 | markdown = [ 797 | {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, 798 | {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, 799 | ] 800 | markupsafe = [ 801 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 802 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 803 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 804 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 805 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 806 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 807 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 808 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 809 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 810 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 811 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 812 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 813 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 814 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 815 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 816 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 817 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 818 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 819 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 820 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 821 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 822 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 823 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 824 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 825 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 826 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 827 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 828 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 829 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 830 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 831 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 832 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 833 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 834 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 835 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 836 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 837 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 838 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 839 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 840 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 841 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 842 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 843 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 844 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 845 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 846 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 847 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 848 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 849 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 850 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 851 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 852 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 853 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 854 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 855 | ] 856 | mccabe = [ 857 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 858 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 859 | ] 860 | mergedeep = [ 861 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 862 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 863 | ] 864 | mkdocs = [ 865 | {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, 866 | {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, 867 | ] 868 | mkdocs-material = [ 869 | {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, 870 | {file = "mkdocs_material-4.6.3-py2.py3-none-any.whl", hash = "sha256:7f3afa0a09c07d0b89a6a9755fdb00513aee8f0cec3538bb903325c80f66f444"}, 871 | ] 872 | packaging = [ 873 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 874 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 875 | ] 876 | platformdirs = [ 877 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 878 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 879 | ] 880 | pluggy = [ 881 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 882 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 883 | ] 884 | prompt-toolkit = [ 885 | {file = "prompt_toolkit-2.0.10-py2-none-any.whl", hash = "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31"}, 886 | {file = "prompt_toolkit-2.0.10-py3-none-any.whl", hash = "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4"}, 887 | {file = "prompt_toolkit-2.0.10.tar.gz", hash = "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"}, 888 | ] 889 | py = [ 890 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 891 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 892 | ] 893 | pygments = [ 894 | {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, 895 | {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, 896 | ] 897 | pylint = [ 898 | {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, 899 | {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, 900 | ] 901 | pymdown-extensions = [ 902 | {file = "pymdown-extensions-9.0.tar.gz", hash = "sha256:01e4bec7f4b16beaba0087a74496401cf11afd69e3a11fe95cb593e5c698ef40"}, 903 | {file = "pymdown_extensions-9.0-py3-none-any.whl", hash = "sha256:430cc2fbb30cef2df70edac0b4f62614a6a4d2b06462e32da4ca96098b7c1dfb"}, 904 | ] 905 | pyparsing = [ 906 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 907 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 908 | ] 909 | pytest = [ 910 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 911 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 912 | ] 913 | pytest-cov = [ 914 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 915 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 916 | ] 917 | pytest-datadir = [ 918 | {file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"}, 919 | {file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"}, 920 | ] 921 | python-dateutil = [ 922 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 923 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 924 | ] 925 | pyyaml = [ 926 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 927 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 928 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 929 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 930 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 931 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 932 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 933 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 934 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 935 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 936 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 937 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 938 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 939 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 940 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 941 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 942 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 943 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 944 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 945 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 946 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 947 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 948 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 949 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 950 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 951 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 952 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 953 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 954 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 955 | ] 956 | pyyaml-env-tag = [ 957 | {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, 958 | {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, 959 | ] 960 | requests = [ 961 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 962 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 963 | ] 964 | schema = [ 965 | {file = "schema-0.7.4-py2.py3-none-any.whl", hash = "sha256:cf97e4cd27e203ab6bb35968532de1ed8991bce542a646f0ff1d643629a4945d"}, 966 | {file = "schema-0.7.4.tar.gz", hash = "sha256:fbb6a52eb2d9facf292f233adcc6008cffd94343c63ccac9a1cb1f3e6de1db17"}, 967 | ] 968 | setuptools = [ 969 | {file = "setuptools-58.2.0-py3-none-any.whl", hash = "sha256:2551203ae6955b9876741a26ab3e767bb3242dafe86a32a749ea0d78b6792f11"}, 970 | {file = "setuptools-58.2.0.tar.gz", hash = "sha256:2c55bdb85d5bb460bd2e3b12052b677879cffcf46c0c688f2e5bf51d36001145"}, 971 | ] 972 | six = [ 973 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 974 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 975 | ] 976 | toml = [ 977 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 978 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 979 | ] 980 | typed-ast = [ 981 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 982 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 983 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 984 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 985 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 986 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 987 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 988 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 989 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 990 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 991 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 992 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 993 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 994 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 995 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 996 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 997 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 998 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 999 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 1000 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 1001 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 1002 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 1003 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 1004 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 1005 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 1006 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 1007 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 1008 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 1009 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 1010 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 1011 | ] 1012 | typing-extensions = [ 1013 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 1014 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 1015 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 1016 | ] 1017 | urllib3 = [ 1018 | {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, 1019 | {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, 1020 | ] 1021 | watchdog = [ 1022 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, 1023 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, 1024 | {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, 1025 | {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, 1026 | {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, 1027 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, 1028 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, 1029 | {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, 1030 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, 1031 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, 1032 | {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, 1033 | {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, 1034 | {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, 1035 | {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, 1036 | {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, 1037 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, 1038 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, 1039 | {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, 1040 | {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, 1041 | {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, 1042 | {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, 1043 | {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, 1044 | {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, 1045 | ] 1046 | wcwidth = [ 1047 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 1048 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 1049 | ] 1050 | wrapt = [ 1051 | {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, 1052 | {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, 1053 | {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, 1054 | {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, 1055 | {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, 1056 | {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, 1057 | {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, 1058 | {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, 1059 | {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, 1060 | {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, 1061 | {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, 1062 | {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, 1063 | {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, 1064 | {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, 1065 | {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, 1066 | {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, 1067 | {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, 1068 | {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, 1069 | {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, 1070 | {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, 1071 | {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, 1072 | {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, 1073 | {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, 1074 | {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, 1075 | {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, 1076 | {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, 1077 | {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, 1078 | {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, 1079 | {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, 1080 | {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, 1081 | {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, 1082 | {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, 1083 | {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, 1084 | {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, 1085 | {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, 1086 | {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, 1087 | {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, 1088 | {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, 1089 | {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, 1090 | {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, 1091 | {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, 1092 | {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, 1093 | {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, 1094 | {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, 1095 | ] 1096 | zipp = [ 1097 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 1098 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 1099 | ] 1100 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=missing-docstring, 58 | print-statement, 59 | parameter-unpacking, 60 | unpacking-in-except, 61 | old-raise-syntax, 62 | backtick, 63 | long-suffix, 64 | old-ne-operator, 65 | old-octal-literal, 66 | import-star-module-level, 67 | non-ascii-bytes-literal, 68 | invalid-unicode-literal, 69 | raw-checker-failed, 70 | bad-inline-option, 71 | locally-disabled, 72 | locally-enabled, 73 | file-ignored, 74 | suppressed-message, 75 | useless-suppression, 76 | deprecated-pragma, 77 | apply-builtin, 78 | basestring-builtin, 79 | buffer-builtin, 80 | cmp-builtin, 81 | coerce-builtin, 82 | execfile-builtin, 83 | file-builtin, 84 | long-builtin, 85 | raw_input-builtin, 86 | reduce-builtin, 87 | standarderror-builtin, 88 | unicode-builtin, 89 | xrange-builtin, 90 | coerce-method, 91 | delslice-method, 92 | getslice-method, 93 | setslice-method, 94 | no-absolute-import, 95 | old-division, 96 | dict-iter-method, 97 | dict-view-method, 98 | next-method-called, 99 | metaclass-assignment, 100 | indexing-exception, 101 | raising-string, 102 | reload-builtin, 103 | oct-method, 104 | hex-method, 105 | nonzero-method, 106 | cmp-method, 107 | input-builtin, 108 | round-builtin, 109 | intern-builtin, 110 | unichr-builtin, 111 | map-builtin-not-iterating, 112 | zip-builtin-not-iterating, 113 | range-builtin-not-iterating, 114 | filter-builtin-not-iterating, 115 | using-cmp-argument, 116 | eq-without-hash, 117 | div-method, 118 | idiv-method, 119 | rdiv-method, 120 | exception-message-attribute, 121 | invalid-str-codec, 122 | sys-max-int, 123 | bad-python3-import, 124 | deprecated-string-function, 125 | deprecated-str-translate-call, 126 | deprecated-itertools-function, 127 | deprecated-types-field, 128 | next-method-defined, 129 | dict-items-not-iterating, 130 | dict-keys-not-iterating, 131 | dict-values-not-iterating, 132 | deprecated-operator-function, 133 | deprecated-urllib-function, 134 | xreadlines-attribute, 135 | deprecated-sys-function, 136 | exception-escape, 137 | comprehension-escape 138 | 139 | # Enable the message, report, category or checker with the given id(s). You can 140 | # either give multiple identifier separated by comma (,) or put this option 141 | # multiple time (only on the command line, not in the configuration file where 142 | # it should appear only once). See also the "--disable" option for examples. 143 | enable=c-extension-no-member 144 | 145 | 146 | [REPORTS] 147 | 148 | # Python expression which should return a note less than 10 (10 is the highest 149 | # note). You have access to the variables errors warning, statement which 150 | # respectively contain the number of errors / warnings messages and the total 151 | # number of statements analyzed. This is used by the global evaluation report 152 | # (RP0004). 153 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 154 | 155 | # Template used to display messages. This is a python new-style format string 156 | # used to format the message information. See doc for all details 157 | #msg-template= 158 | 159 | # Set the output format. Available formats are text, parseable, colorized, json 160 | # and msvs (visual studio).You can also give a reporter class, eg 161 | # mypackage.mymodule.MyReporterClass. 162 | output-format=colorized 163 | 164 | # Tells whether to display a full report or only the messages 165 | reports=no 166 | 167 | # Activate the evaluation score. 168 | score=yes 169 | 170 | 171 | [REFACTORING] 172 | 173 | # Maximum number of nested blocks for function / method body 174 | max-nested-blocks=5 175 | 176 | # Complete name of functions that never returns. When checking for 177 | # inconsistent-return-statements if a never returning function is called then 178 | # it will be considered as an explicit return statement and no message will be 179 | # printed. 180 | never-returning-functions=optparse.Values,sys.exit 181 | 182 | 183 | [BASIC] 184 | 185 | # Naming style matching correct argument names 186 | argument-naming-style=snake_case 187 | 188 | # Regular expression matching correct argument names. Overrides argument- 189 | # naming-style 190 | #argument-rgx= 191 | 192 | # Naming style matching correct attribute names 193 | attr-naming-style=snake_case 194 | 195 | # Regular expression matching correct attribute names. Overrides attr-naming- 196 | # style 197 | #attr-rgx= 198 | 199 | # Bad variable names which should always be refused, separated by a comma 200 | bad-names=foo, 201 | bar, 202 | baz, 203 | toto, 204 | tutu, 205 | tata 206 | 207 | # Naming style matching correct class attribute names 208 | class-attribute-naming-style=any 209 | 210 | # Regular expression matching correct class attribute names. Overrides class- 211 | # attribute-naming-style 212 | #class-attribute-rgx= 213 | 214 | # Naming style matching correct class names 215 | class-naming-style=PascalCase 216 | 217 | # Regular expression matching correct class names. Overrides class-naming-style 218 | #class-rgx= 219 | 220 | # Naming style matching correct constant names 221 | const-naming-style=UPPER_CASE 222 | 223 | # Regular expression matching correct constant names. Overrides const-naming- 224 | # style 225 | #const-rgx= 226 | 227 | # Minimum line length for functions/classes that require docstrings, shorter 228 | # ones are exempt. 229 | docstring-min-length=-1 230 | 231 | # Naming style matching correct function names 232 | function-naming-style=snake_case 233 | 234 | # Regular expression matching correct function names. Overrides function- 235 | # naming-style 236 | #function-rgx= 237 | 238 | # Good variable names which should always be accepted, separated by a comma 239 | good-names=i, 240 | j, 241 | k, 242 | ex, 243 | Run, 244 | _ 245 | 246 | # Include a hint for the correct naming format with invalid-name 247 | include-naming-hint=no 248 | 249 | # Naming style matching correct inline iteration names 250 | inlinevar-naming-style=any 251 | 252 | # Regular expression matching correct inline iteration names. Overrides 253 | # inlinevar-naming-style 254 | #inlinevar-rgx= 255 | 256 | # Naming style matching correct method names 257 | method-naming-style=snake_case 258 | 259 | # Regular expression matching correct method names. Overrides method-naming- 260 | # style 261 | #method-rgx= 262 | 263 | # Naming style matching correct module names 264 | module-naming-style=snake_case 265 | 266 | # Regular expression matching correct module names. Overrides module-naming- 267 | # style 268 | #module-rgx= 269 | 270 | # Colon-delimited sets of names that determine each other's naming style when 271 | # the name regexes allow several styles. 272 | name-group= 273 | 274 | # Regular expression which should only match function or class names that do 275 | # not require a docstring. 276 | no-docstring-rgx=^_ 277 | 278 | # List of decorators that produce properties, such as abc.abstractproperty. Add 279 | # to this list to register other decorators that produce valid properties. 280 | property-classes=abc.abstractproperty 281 | 282 | # Naming style matching correct variable names 283 | variable-naming-style=snake_case 284 | 285 | # Regular expression matching correct variable names. Overrides variable- 286 | # naming-style 287 | #variable-rgx= 288 | 289 | 290 | [FORMAT] 291 | 292 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 293 | expected-line-ending-format= 294 | 295 | # Regexp for a line that is allowed to be longer than the limit. 296 | ignore-long-lines=^\s*(# )??$ 297 | 298 | # Number of spaces of indent required inside a hanging or continued line. 299 | indent-after-paren=4 300 | 301 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 302 | # tab). 303 | indent-string=' ' 304 | 305 | # Maximum number of characters on a single line. 306 | max-line-length=100 307 | 308 | # Maximum number of lines in a module 309 | max-module-lines=1000 310 | 311 | # List of optional constructs for which whitespace checking is disabled. `dict- 312 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 313 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 314 | # `empty-line` allows space-only lines. 315 | no-space-check=trailing-comma, 316 | dict-separator 317 | 318 | # Allow the body of a class to be on the same line as the declaration if body 319 | # contains single statement. 320 | single-line-class-stmt=no 321 | 322 | # Allow the body of an if to be on the same line as the test if there is no 323 | # else. 324 | single-line-if-stmt=no 325 | 326 | 327 | [LOGGING] 328 | 329 | # Logging modules to check that the string format arguments are in logging 330 | # function parameter format 331 | logging-modules=logging 332 | 333 | 334 | [MISCELLANEOUS] 335 | 336 | # List of note tags to take in consideration, separated by a comma. 337 | notes=FIXME, 338 | XXX, 339 | TODO 340 | 341 | 342 | [SIMILARITIES] 343 | 344 | # Ignore comments when computing similarities. 345 | ignore-comments=yes 346 | 347 | # Ignore docstrings when computing similarities. 348 | ignore-docstrings=yes 349 | 350 | # Ignore imports when computing similarities. 351 | ignore-imports=no 352 | 353 | # Minimum lines number of a similarity. 354 | min-similarity-lines=4 355 | 356 | 357 | [SPELLING] 358 | 359 | # Limits count of emitted suggestions for spelling mistakes 360 | max-spelling-suggestions=4 361 | 362 | # Spelling dictionary name. Available dictionaries: none. To make it working 363 | # install python-enchant package. 364 | spelling-dict= 365 | 366 | # List of comma separated words that should not be checked. 367 | spelling-ignore-words= 368 | 369 | # A path to a file that contains private dictionary; one word per line. 370 | spelling-private-dict-file= 371 | 372 | # Tells whether to store unknown words to indicated private dictionary in 373 | # --spelling-private-dict-file option instead of raising a message. 374 | spelling-store-unknown-words=no 375 | 376 | 377 | [TYPECHECK] 378 | 379 | # List of decorators that produce context managers, such as 380 | # contextlib.contextmanager. Add to this list to register other decorators that 381 | # produce valid context managers. 382 | contextmanager-decorators=contextlib.contextmanager 383 | 384 | # List of members which are set dynamically and missed by pylint inference 385 | # system, and so shouldn't trigger E1101 when accessed. Python regular 386 | # expressions are accepted. 387 | generated-members= 388 | 389 | # Tells whether missing members accessed in mixin class should be ignored. A 390 | # mixin class is detected if its name ends with "mixin" (case insensitive). 391 | ignore-mixin-members=yes 392 | 393 | # This flag controls whether pylint should warn about no-member and similar 394 | # checks whenever an opaque object is returned when inferring. The inference 395 | # can return multiple potential results while evaluating a Python object, but 396 | # some branches might not be evaluated, which results in partial inference. In 397 | # that case, it might be useful to still emit no-member and other checks for 398 | # the rest of the inferred objects. 399 | ignore-on-opaque-inference=yes 400 | 401 | # List of class names for which member attributes should not be checked (useful 402 | # for classes with dynamically set attributes). This supports the use of 403 | # qualified names. 404 | ignored-classes=optparse.Values,thread._local,_thread._local 405 | 406 | # List of module names for which member attributes should not be checked 407 | # (useful for modules/projects where namespaces are manipulated during runtime 408 | # and thus existing member attributes cannot be deduced by static analysis. It 409 | # supports qualified module names, as well as Unix pattern matching. 410 | ignored-modules= 411 | 412 | # Show a hint with possible names when a member name was not found. The aspect 413 | # of finding the hint is based on edit distance. 414 | missing-member-hint=yes 415 | 416 | # The minimum edit distance a name should have in order to be considered a 417 | # similar match for a missing member name. 418 | missing-member-hint-distance=1 419 | 420 | # The total number of similar names that should be taken in consideration when 421 | # showing a hint for a missing member. 422 | missing-member-max-choices=1 423 | 424 | 425 | [VARIABLES] 426 | 427 | # List of additional names supposed to be defined in builtins. Remember that 428 | # you should avoid to define new builtins when possible. 429 | additional-builtins= 430 | 431 | # Tells whether unused global variables should be treated as a violation. 432 | allow-global-unused-variables=yes 433 | 434 | # List of strings which can identify a callback function by name. A callback 435 | # name must start or end with one of those strings. 436 | callbacks=cb_, 437 | _cb 438 | 439 | # A regular expression matching the name of dummy variables (i.e. expectedly 440 | # not used). 441 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 442 | 443 | # Argument names that match this expression will be ignored. Default to name 444 | # with leading underscore 445 | ignored-argument-names=_.*|^ignored_|^unused_ 446 | 447 | # Tells whether we should check for unused import in __init__ files. 448 | init-import=no 449 | 450 | # List of qualified module names which can have objects that can redefine 451 | # builtins. 452 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,io 453 | 454 | 455 | [CLASSES] 456 | 457 | # List of method names used to declare (i.e. assign) instance attributes. 458 | defining-attr-methods=__init__, 459 | __new__, 460 | setUp 461 | 462 | # List of member names, which should be excluded from the protected access 463 | # warning. 464 | exclude-protected=_asdict, 465 | _fields, 466 | _replace, 467 | _source, 468 | _make 469 | 470 | # List of valid names for the first argument in a class method. 471 | valid-classmethod-first-arg=cls 472 | 473 | # List of valid names for the first argument in a metaclass class method. 474 | valid-metaclass-classmethod-first-arg=mcs 475 | 476 | 477 | [DESIGN] 478 | 479 | # Maximum number of arguments for function / method 480 | max-args=5 481 | 482 | # Maximum number of attributes for a class (see R0902). 483 | max-attributes=7 484 | 485 | # Maximum number of boolean expressions in a if statement 486 | max-bool-expr=5 487 | 488 | # Maximum number of branch for function / method body 489 | max-branches=12 490 | 491 | # Maximum number of locals for function / method body 492 | max-locals=15 493 | 494 | # Maximum number of parents for a class (see R0901). 495 | max-parents=7 496 | 497 | # Maximum number of public methods for a class (see R0904). 498 | max-public-methods=20 499 | 500 | # Maximum number of return / yield for function / method body 501 | max-returns=6 502 | 503 | # Maximum number of statements in function / method body 504 | max-statements=50 505 | 506 | # Minimum number of public methods for a class (see R0903). 507 | min-public-methods=2 508 | 509 | 510 | [IMPORTS] 511 | 512 | # Allow wildcard imports from modules that define __all__. 513 | allow-wildcard-with-all=no 514 | 515 | # Analyse import fallback blocks. This can be used to support both Python 2 and 516 | # 3 compatible code, which means that the block might have code that exists 517 | # only in one or another interpreter, leading to false positives when analysed. 518 | analyse-fallback-blocks=no 519 | 520 | # Deprecated modules which should not be used, separated by a comma 521 | deprecated-modules=optparse,tkinter.tix 522 | 523 | # Create a graph of external dependencies in the given file (report RP0402 must 524 | # not be disabled) 525 | ext-import-graph= 526 | 527 | # Create a graph of every (i.e. internal and external) dependencies in the 528 | # given file (report RP0402 must not be disabled) 529 | import-graph= 530 | 531 | # Create a graph of internal dependencies in the given file (report RP0402 must 532 | # not be disabled) 533 | int-import-graph= 534 | 535 | # Force import order to recognize a module as part of the standard 536 | # compatibility libraries. 537 | known-standard-library= 538 | 539 | # Force import order to recognize a module as part of a third party library. 540 | known-third-party=enchant 541 | 542 | 543 | [EXCEPTIONS] 544 | 545 | # Exceptions that will emit a warning when being caught. Defaults to 546 | # "Exception" 547 | overgeneral-exceptions=Exception 548 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cliar" 3 | version = "1.3.5" 4 | description = "Create CLIs with classes and type hints." 5 | license = "MIT" 6 | authors = ["Constantine Molchanov "] 7 | readme = "README.md" 8 | homepage = "https://moigagoo.github.io/cliar/" 9 | repository = "https://github.com/moigagoo/cliar/" 10 | documentation = "https://moigagoo.github.io/cliar/" 11 | keywords = ["cli", "commandline"] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.6" 15 | 16 | [tool.poetry.dev-dependencies] 17 | pytest = "^6.2" 18 | pylint = "^2.1" 19 | pytest-cov = "^2.5" 20 | codecov = "^2.0" 21 | pygments = "^2.2" 22 | pytest-datadir = "^1.0" 23 | foliant = "^1.0" 24 | "foliantcontrib.mkdocs" = "^1.0.5" 25 | "foliantcontrib.includes" = "^1.0" 26 | mkdocs-material = "^4.0" 27 | -------------------------------------------------------------------------------- /tests/test_async_fns.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_help(capfd, datadir): 5 | run(f'python {datadir/"async_fns.py"} wait -h', shell=True) 6 | 7 | output = capfd.readouterr().out 8 | 9 | assert '-s SECONDS-TO-WAIT, --seconds-to-wait SECONDS-TO-WAIT' in output 10 | 11 | def test_wait(capfd, datadir): 12 | seconds_to_wait = 1.0 13 | 14 | run( 15 | f'python {datadir/"async_fns.py"} wait -s "{seconds_to_wait}"', 16 | shell=True 17 | ) 18 | 19 | seconds_awaited = float(capfd.readouterr().out.strip()) 20 | assert round(seconds_awaited, 1) == round(seconds_to_wait, 1) 21 | -------------------------------------------------------------------------------- /tests/test_async_fns/async_fns.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from time import perf_counter 3 | 4 | from cliar import Cliar 5 | 6 | 7 | class AsyncFunctions(Cliar): 8 | async def wait(self, seconds_to_wait: float = 1.0): 9 | start = perf_counter() 10 | 11 | await asyncio.sleep(seconds_to_wait) 12 | 13 | print(perf_counter() - start) 14 | 15 | if __name__ == "__main__": 16 | AsyncFunctions().parse() 17 | -------------------------------------------------------------------------------- /tests/test_basicmath.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | from math import factorial, log 3 | 4 | 5 | def test_positional_args(capfd, datadir): 6 | x, y = 12, 34 7 | 8 | run(f'python {datadir/"basicmath.py"} add {x} {y}', shell=True) 9 | assert int(capfd.readouterr().out) == x + y 10 | 11 | 12 | def test_optional_args(capfd, datadir): 13 | x, power = 12, 3 14 | 15 | run(f'python {datadir/"basicmath.py"} power {x} --power {power}', shell=True) 16 | assert int(capfd.readouterr().out) == x ** power 17 | 18 | run(f'python {datadir/"basicmath.py"} power {x} -p {power}', shell=True) 19 | assert int(capfd.readouterr().out) == x ** power 20 | 21 | run(f'python {datadir/"basicmath.py"} power {x}', shell=True) 22 | assert int(capfd.readouterr().out) == x ** 2 23 | 24 | 25 | def test_no_args(capfd, datadir): 26 | from math import pi 27 | 28 | run(f'python {datadir/"basicmath.py"} pi', shell=True) 29 | assert float(capfd.readouterr().out) == pi 30 | 31 | run(f'python {datadir/"basicmath.py"} avg', shell=True) 32 | assert float(capfd.readouterr().out) == sum((1, 2, 3))/3 33 | 34 | 35 | def test_root_command(capfd, datadir): 36 | version = '0.1.0' 37 | 38 | run(f'python {datadir/"basicmath.py"}', shell=True) 39 | assert capfd.readouterr().out.strip() == 'Welcome to math!' 40 | 41 | run(f'python {datadir/"basicmath.py"} --version', shell=True) 42 | assert capfd.readouterr().out.strip() == version 43 | 44 | run(f'python {datadir/"basicmath.py"} -v', shell=True) 45 | assert capfd.readouterr().out.strip() == version 46 | 47 | 48 | def test_nargs(capfd, datadir): 49 | numbers = 1, 2, 42, 101 50 | 51 | run( 52 | f'python {datadir/"basicmath.py"} summ {" ".join(str(number) for number in numbers)}', 53 | shell=True 54 | ) 55 | assert int(capfd.readouterr().out) == sum(numbers) 56 | 57 | numbers = 1, 2, 42.2, 101.1 58 | 59 | run( 60 | f'python {datadir/"basicmath.py"} avg --numbers {" ".join(str(number) for number in numbers)}', 61 | shell=True 62 | ) 63 | assert float(capfd.readouterr().out) == sum(numbers)/len(numbers) 64 | 65 | run(f'python {datadir/"basicmath.py"} avg -n {" ".join(str(number) for number in numbers)}', shell=True) 66 | assert float(capfd.readouterr().out) == sum(numbers)/len(numbers) 67 | 68 | 69 | def test_negative_numbers(capfd, datadir): 70 | x, y = 12, -34 71 | 72 | run(f'python {datadir/"basicmath.py"} add {x} {y}', shell=True) 73 | assert int(capfd.readouterr().out) == x + y 74 | 75 | 76 | def test_type_casting(capfd, datadir): 77 | error = "argument {arg}: invalid int value: '{value}'" 78 | x, y = 12, 34.0 79 | 80 | run(f'python {datadir/"basicmath.py"} add {x} {y}', shell=True) 81 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(arg='y', value=y)) 82 | 83 | x, y = 12.0, 34.0 84 | 85 | run(f'python {datadir/"basicmath.py"} add {x} {y}', shell=True) 86 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(arg='x', value=x)) 87 | 88 | x, y = 'foo', 42 89 | 90 | run(f'python {datadir/"basicmath.py"} add {x} {y}', shell=True) 91 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(arg='x', value=x)) 92 | 93 | 94 | def test_missing_args(capfd, datadir): 95 | error = 'the following arguments are required: {args}' 96 | x = 12 97 | 98 | run(f'python {datadir/"basicmath.py"} add {x}', shell=True) 99 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(args='y')) 100 | 101 | run(f'python {datadir/"basicmath.py"} add', shell=True) 102 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(args='x, y')) 103 | 104 | 105 | def test_redundant_args(capfd, datadir): 106 | error = 'unrecognized arguments: {args}' 107 | x, y, z = 12, 34, 56 108 | 109 | run(f'python {datadir/"basicmath.py"} add {x} {y} {z}', shell=True) 110 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(args=z)) 111 | 112 | x, y, z = 12, 34, '-f' 113 | 114 | run(f'python {datadir/"basicmath.py"} add {x} {y} {z}', shell=True) 115 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(args=z)) 116 | 117 | x, y, z, q = 12, 34, '--flag', 'value' 118 | 119 | run(f'python {datadir/"basicmath.py"} add {x} {y} {z} {q}', shell=True) 120 | assert capfd.readouterr().err.splitlines()[-1].endswith(error.format(args=f'{z} {q}')) 121 | 122 | 123 | def test_open(capfd, datadir): 124 | filename = datadir / 'numbers.txt' 125 | with open(filename) as file: 126 | numbers = (float(line) for line in file.readlines()) 127 | 128 | run(f'python {datadir/"basicmath.py"} sumfile {filename}', shell=True) 129 | assert float(capfd.readouterr().out) == sum(numbers) 130 | 131 | 132 | def test_aliases(capfd, datadir): 133 | x, y = 12, 34 134 | 135 | run(f'python {datadir/"basicmath.py"} sum {x} {y}', shell=True) 136 | assert int(capfd.readouterr().out) == x + y 137 | 138 | run(f'python {datadir/"basicmath.py"} plus {x} {y}', shell=True) 139 | assert int(capfd.readouterr().out) == x + y 140 | 141 | 142 | def test_help(capfd, datadir): 143 | run(f'python {datadir/"basicmath.py"} --help', shell=True) 144 | assert 'Basic math operations.' in capfd.readouterr().out 145 | 146 | run(f'python {datadir/"basicmath.py"} -h', shell=True) 147 | assert 'Basic math operations.' in capfd.readouterr().out 148 | 149 | run(f'python {datadir/"basicmath.py"} add --help', shell=True) 150 | help_message = capfd.readouterr().out 151 | assert 'Add two numbers.' in help_message 152 | assert 'First operand' in help_message 153 | assert 'Second operand' in help_message 154 | 155 | run(f'python {datadir/"basicmath.py"} add -h', shell=True) 156 | help_message = capfd.readouterr().out 157 | assert 'Add two numbers.' in help_message 158 | assert 'First operand' in help_message 159 | assert 'Second operand' in help_message 160 | 161 | 162 | def test_ignore(capfd, datadir): 163 | n = 6 164 | 165 | run(f'python {datadir/"basicmath.py"} calculate-factorial {n}', shell=True) 166 | assert "invalid choice: 'calculate-factorial'" in capfd.readouterr().err.splitlines()[-1] 167 | 168 | 169 | def test_pseudonym(capfd, datadir): 170 | n = 6 171 | 172 | run(f'python {datadir/"basicmath.py"} fac 6', shell=True) 173 | assert int(capfd.readouterr().out) == factorial(n) 174 | 175 | 176 | def test_arg_map(capfd, datadir): 177 | x, base = 1000, 10 178 | 179 | run(f'python {datadir/"basicmath.py"} log {x} --to {base}', shell=True) 180 | assert float(capfd.readouterr().out) == log(x, base) 181 | 182 | run(f'python {datadir/"basicmath.py"} log {x} -t {base}', shell=True) 183 | assert float(capfd.readouterr().out) == log(x, base) 184 | 185 | 186 | def test_metavars(capfd, datadir): 187 | run(f'python {datadir/"basicmath.py"} log --help', shell=True) 188 | assert '-t BASE, --to BASE' in capfd.readouterr().out 189 | 190 | run(f'python {datadir/"basicmath.py"} log -h', shell=True) 191 | assert '-t BASE, --to BASE' in capfd.readouterr().out 192 | 193 | 194 | def test_show_defaults(capfd, datadir): 195 | run(f'python {datadir/"basicmath.py"} log -h', shell=True) 196 | assert '(default: 2.718281828459045)' in capfd.readouterr().out 197 | 198 | 199 | def test_set_name(): 200 | from pytest import raises 201 | 202 | from cliar import Cliar, set_name 203 | 204 | with raises(NameError) as excinfo: 205 | class _(Cliar): 206 | @set_name('') 207 | def _(self): 208 | pass 209 | 210 | assert 'Command name cannot be empty' in str(excinfo.value) 211 | 212 | 213 | def test_str_arg(capfd, datadir): 214 | message = 'Hello Cliar' 215 | run(f'python {datadir/"basicmath.py"} echo "{message}"', shell=True) 216 | assert capfd.readouterr().out.strip() == message 217 | -------------------------------------------------------------------------------- /tests/test_basicmath/basicmath.py: -------------------------------------------------------------------------------- 1 | from math import pi, e, log 2 | from typing import List 3 | 4 | from cliar import Cliar, add_aliases, set_help, ignore, set_name, set_arg_map, set_metavars 5 | 6 | 7 | class Math(Cliar): 8 | '''Basic math operations.''' 9 | 10 | @set_help({'x': 'First operand', 'y': 'Second operand'}) 11 | def add(self, x: int, y: int): 12 | '''Add two numbers.''' 13 | 14 | print(x+y) 15 | 16 | def power(self, x: int, power=2): 17 | print(x**power) 18 | 19 | def pi(self): 20 | print(pi) 21 | 22 | def echo(self, message: str): 23 | print(message) 24 | 25 | @add_aliases(['sum', 'plus']) 26 | def summ(self, numbers: List[int]): 27 | print(sum(numbers)) 28 | 29 | def avg(self, numbers: List[float] = [1, 2, 3]): 30 | print(sum(numbers)/len(numbers)) 31 | 32 | def sumfile(self, file: open): 33 | numbers = (float(line) for line in file.readlines()) 34 | print(sum(numbers)) 35 | 36 | @ignore 37 | def calculate_factorial(self, n: int, acc: int): 38 | if n == 0: 39 | return acc 40 | 41 | elif n > 0: 42 | return self.calculate_factorial(n-1, acc*n) 43 | 44 | else: 45 | raise ValueError('Cannot calculate factorial of negative number.') 46 | 47 | @set_name('fac') 48 | def factorial(self, n: int): 49 | print(self.calculate_factorial(n, 1)) 50 | 51 | @set_arg_map({'base': 'to'}) 52 | @set_metavars({'base': 'BASE'}) 53 | @set_help({'base': 'Log base'}, show_defaults=True) 54 | def log(self, x: float, base=e): 55 | print(log(x, base)) 56 | 57 | def _root(self, version=False): 58 | if version: 59 | print('0.1.0') 60 | 61 | else: 62 | print('Welcome to math!') 63 | 64 | 65 | if __name__ == '__main__': 66 | Math().parse() 67 | -------------------------------------------------------------------------------- /tests/test_basicmath/numbers.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 42 4 | 101 -------------------------------------------------------------------------------- /tests/test_case_sensitive_args.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_help(capfd, datadir): 5 | run(f'python {datadir/"case_sensitive_args.py"} -h', shell=True) 6 | 7 | output = capfd.readouterr().out 8 | 9 | assert '-u UNIQUE, --unique UNIQUE' in output 10 | assert '--url URL' in output 11 | assert '-U USERNAME, --username USERNAME' in output 12 | -------------------------------------------------------------------------------- /tests/test_case_sensitive_args/case_sensitive_args.py: -------------------------------------------------------------------------------- 1 | from cliar import Cliar, set_sharg_map 2 | 3 | 4 | class CaseSensitiveArgs(Cliar): 5 | @set_sharg_map({'url': None, 'username': 'U'}) 6 | def _root(self, unique='', url='', username=''): 7 | pass 8 | 9 | 10 | if __name__ == "__main__": 11 | CaseSensitiveArgs().parse() 12 | -------------------------------------------------------------------------------- /tests/test_global_args.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_connect(capfd, datadir): 5 | user = 'user' 6 | password = 'password' 7 | hostname = 'hostname' 8 | 9 | run( 10 | f'python {datadir/"global_args.py"} --as {user} -p {password} connect {hostname}', 11 | shell=True 12 | ) 13 | 14 | output = capfd.readouterr().out.strip() 15 | 16 | assert output == f'Connecting to {hostname}, user="{user}", password="{password}"' 17 | 18 | def test_upload(capfd, datadir): 19 | user = 'user' 20 | password = 'password' 21 | filename = 'filename' 22 | 23 | run( 24 | f'python {datadir/"global_args.py"} --as {user} -p {password} utils upload {filename}', 25 | shell=True 26 | ) 27 | 28 | output = capfd.readouterr().out.strip() 29 | 30 | assert output == f'Uploading {filename}, user="{user}", password="{password}"' 31 | -------------------------------------------------------------------------------- /tests/test_global_args/global_args.py: -------------------------------------------------------------------------------- 1 | from cliar import Cliar, set_arg_map, set_sharg_map 2 | 3 | class Utils(Cliar): 4 | def upload(self, filename: str): 5 | user = self.global_args['as'] 6 | password = self.global_args['password'] 7 | 8 | print(f'Uploading {filename}, user="{user}", password="{password}"') 9 | 10 | 11 | class GlobalArgs(Cliar): 12 | utils = Utils 13 | 14 | @set_arg_map({'user': 'as'}) 15 | @set_sharg_map({'user': None}) 16 | def _root(self, user='', password=''): 17 | pass 18 | 19 | def connect(self, host: str): 20 | user = self.global_args['as'] 21 | password = self.global_args['password'] 22 | 23 | print(f'Connecting to {host}, user="{user}", password="{password}"') 24 | 25 | 26 | GlobalArgs().parse() 27 | -------------------------------------------------------------------------------- /tests/test_multiword_args.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_help(capfd, datadir): 5 | run(f'python {datadir/"multiword_args.py"} say -h', shell=True) 6 | 7 | output = capfd.readouterr().out 8 | 9 | assert 'words-to-say' in output 10 | assert '-t, --to-upper' in output 11 | 12 | def test_say(capfd, datadir): 13 | words_to_say = 'hello world' 14 | repeat_words = 3 15 | 16 | run(f'python {datadir/"multiword_args.py"} say "{words_to_say}"', shell=True) 17 | assert capfd.readouterr().out.strip() == words_to_say 18 | 19 | run(f'python {datadir/"multiword_args.py"} say "{words_to_say}" --to-upper', shell=True) 20 | assert capfd.readouterr().out.strip() == words_to_say.upper() 21 | 22 | run(f'python {datadir/"multiword_args.py"} say "{words_to_say}" --repeat-words {repeat_words}', shell=True) 23 | assert capfd.readouterr().out.strip().splitlines() == [words_to_say] * repeat_words 24 | -------------------------------------------------------------------------------- /tests/test_multiword_args/multiword_args.py: -------------------------------------------------------------------------------- 1 | from cliar import Cliar 2 | 3 | 4 | class MultiwordArgs(Cliar): 5 | def say(self, words_to_say: str, to_upper=False, repeat_words: int = 1): 6 | for _ in range(repeat_words): 7 | if to_upper: 8 | print(words_to_say.upper()) 9 | else: 10 | print(words_to_say) 11 | 12 | if __name__ == "__main__": 13 | MultiwordArgs().parse() 14 | -------------------------------------------------------------------------------- /tests/test_nested.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_helps(capfd, datadir): 5 | run(f'python {datadir/"nested.py"} -h', shell=True) 6 | assert 'Git help.' in capfd.readouterr().out 7 | 8 | run(f'python {datadir/"nested.py"} remote -h', shell=True) 9 | assert 'Remote help.' in capfd.readouterr().out 10 | 11 | run(f'python {datadir/"nested.py"} remote add -h', shell=True) 12 | assert 'Remote add help.' in capfd.readouterr().out 13 | 14 | run(f'python {datadir/"nested.py"} flow -h', shell=True) 15 | assert 'Flow help.' in capfd.readouterr().out 16 | 17 | run(f'python {datadir/"nested.py"} flow feature start -h', shell=True) 18 | assert 'Feature start help.' in capfd.readouterr().out 19 | 20 | def test_git(capfd, datadir): 21 | run(f'python {datadir/"nested.py"}', shell=True) 22 | assert capfd.readouterr().out.strip() == 'Git root.' 23 | 24 | def test_remote(capfd, datadir): 25 | run(f'python {datadir/"nested.py"} remote', shell=True) 26 | assert capfd.readouterr().out.strip() == 'Remote root.' 27 | 28 | remote_name = 'foo' 29 | run(f'python {datadir/"nested.py"} remote add {remote_name}', shell=True) 30 | assert capfd.readouterr().out.strip() == f'Adding remote {remote_name}' 31 | 32 | run(f'python {datadir/"nested.py"} remote show', shell=True) 33 | assert capfd.readouterr().out.strip() == 'Showing all remotes' 34 | 35 | def test_flow(capfd, datadir): 36 | feature_name = 'bar' 37 | run(f'python {datadir/"nested.py"} flow feature start {feature_name}', shell=True) 38 | assert capfd.readouterr().out.strip() == f'Starting feature {feature_name}' 39 | -------------------------------------------------------------------------------- /tests/test_nested/nested.py: -------------------------------------------------------------------------------- 1 | from cliar import Cliar 2 | 3 | 4 | class Remote(Cliar): 5 | '''Remote help.''' 6 | 7 | def _root(self): 8 | print('Remote root.') 9 | 10 | def add(self, name: str): 11 | '''Remote add help.''' 12 | print(f'Adding remote {name}') 13 | 14 | def show(self): 15 | '''Remote show help.''' 16 | print('Showing all remotes') 17 | 18 | class Feature(Cliar): 19 | '''Feature help.''' 20 | 21 | def start(self, name: str): 22 | '''Feature start help.''' 23 | print(f'Starting feature {name}') 24 | 25 | class Flow(Cliar): 26 | '''Flow help.''' 27 | 28 | feature = Feature 29 | 30 | class Git(Cliar): 31 | '''Git help.''' 32 | 33 | remote = Remote 34 | 35 | def _root(self): 36 | print('Git root.') 37 | 38 | def branch(self, name): 39 | '''Git branch help.''' 40 | print(f'Setting branch to {name}') 41 | 42 | Git.flow = Flow 43 | 44 | Git().parse() 45 | -------------------------------------------------------------------------------- /tests/test_noroot.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | 4 | def test_help(capfd, datadir): 5 | run(f'python {datadir/"noroot.py"}', shell=True) 6 | assert 'Basic math operations.' in capfd.readouterr().out 7 | -------------------------------------------------------------------------------- /tests/test_noroot/noroot.py: -------------------------------------------------------------------------------- 1 | from math import pi, e, log 2 | from typing import List 3 | 4 | from cliar import Cliar, set_name 5 | 6 | 7 | class NoRoot(Cliar): 8 | '''Basic math operations.''' 9 | 10 | def add(self, x: int, y: int): 11 | print(x+y) 12 | 13 | 14 | if __name__ == '__main__': 15 | NoRoot().parse() 16 | --------------------------------------------------------------------------------