├── .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 | [](https://pypi.org/project/cliar)
2 | [](https://travis-ci.com/moigagoo/cliar)
3 | [](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 |
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 |
--------------------------------------------------------------------------------