├── .github ├── dependabot.yml └── workflows │ ├── bump.yaml │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.rst ├── decli ├── __init__.py ├── application.py └── py.typed ├── examples └── demo.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── format ├── publish └── test └── tests ├── __init__.py ├── examples.py └── test_decli.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: flake8 11 | versions: 12 | - 3.9.0 13 | - dependency-name: pytest 14 | versions: 15 | - 6.2.2 16 | - dependency-name: mypy 17 | versions: 18 | - "0.800" 19 | -------------------------------------------------------------------------------- /.github/workflows/bump.yaml: -------------------------------------------------------------------------------- 1 | name: Bump version 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | bump-version: 10 | if: "!startsWith(github.event.head_commit.message, 'bump:')" 11 | runs-on: ubuntu-latest 12 | name: "Bump version and create changelog with commitizen" 13 | steps: 14 | - name: Check out 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 19 | - name: Create bump and changelog 20 | uses: commitizen-tools/commitizen-action@master 21 | with: 22 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish package to Pypi 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Setup python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.x' 19 | - name: Install Poetry 20 | uses: snok/install-poetry@v1 21 | with: 22 | version: latest 23 | virtualenvs-in-project: true 24 | virtualenvs-create: true 25 | - name: Install Dependencies 26 | run: | 27 | poetry install 28 | - name: Publish 29 | env: 30 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: | 33 | ./scripts/publish 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | # We should restore the cache and remove extra requirements installations after this is resolved https://github.com/actions/setup-python/issues/182 5 | 6 | name: Tests 7 | 8 | on: [pull_request] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Setup python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | architecture: x64 25 | - name: Install Poetry 26 | uses: snok/install-poetry@v1 27 | with: 28 | version: latest 29 | virtualenvs-in-project: true 30 | virtualenvs-create: true 31 | - name: Install Dependencies 32 | run: | 33 | poetry install --all-groups --no-interaction 34 | - name: Test and Lint 35 | run: | 36 | ./scripts/test 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | file: ./coverage.xml 42 | flags: unittests 43 | name: decli 44 | fail_ci_if_error: true 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Text editors 2 | .vscode/ 3 | todo 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | # ruff 111 | .ruff_cache 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v0.6.3 (2025-06-01) 4 | 5 | ### Refactor 6 | 7 | - update linter tools, python dependency and add types to application, remove black 8 | 9 | ## v0.6.2 (2024-04-28) 10 | 11 | ### Fix 12 | 13 | - add missing build-system 14 | 15 | ## v0.6.1 (2023-06-03) 16 | 17 | ### Fix 18 | 19 | - commitizen bumps version in decli/__init_.py 20 | 21 | ## v0.6.0 (2023-04-28) 22 | 23 | ### Feat 24 | 25 | - add py.typed 26 | 27 | ### Fix 28 | 29 | - improve type hints 30 | 31 | ## v0.5.2 (2020-07-26) 32 | 33 | ### Fix 34 | 35 | - **application**: add required subcommand support for python 3.6 36 | 37 | # 0.5.1 38 | 39 | ### Fixes 40 | 41 | * Logger changed from DEBUG to INFO 42 | 43 | # 0.5.0 44 | 45 | ### Features 46 | 47 | * **arguments:** exclusive groups are now supported 48 | 49 | # 0.4.0 50 | 51 | ### Features 52 | 53 | * **arguments:** it is possible to group arguments now 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Santiago 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Decli 3 | ====== 4 | 5 | Minimal declarative cli tool. 6 | 7 | .. image:: https://img.shields.io/codecov/c/github/Woile/decli.svg?style=flat-square 8 | :alt: Codecov 9 | :target: https://codecov.io/gh/Woile/decli 10 | 11 | .. image:: https://img.shields.io/pypi/v/decli.svg?style=flat-square 12 | :alt: PyPI 13 | :target: https://pypi.org/project/decli/ 14 | 15 | .. image:: https://img.shields.io/pypi/pyversions/decli.svg?style=flat-square 16 | :alt: PyPI - Python Version 17 | :target: https://pypi.org/project/decli/ 18 | 19 | 20 | .. code-block:: python 21 | 22 | from decli import cli 23 | 24 | data = { 25 | "prog": "myapp", 26 | "description": "Process some integers.", 27 | "arguments": [ 28 | { 29 | "name": "integers", 30 | "metavar": "N", 31 | "type": int, 32 | "nargs": "+", 33 | "help": "an integer for the accumulator", 34 | }, 35 | { 36 | "name": "--sum", 37 | "dest": "accumulate", 38 | "action": "store_const", 39 | "const": sum, 40 | "default": max, 41 | "help": "sum the integers (default: find the max)", 42 | }, 43 | ], 44 | } 45 | parser = cli(data) 46 | parser.parse_args() 47 | 48 | 49 | :: 50 | 51 | >> parser.print_help() 52 | usage: myapp [-h] [--sum] N [N ...] 53 | 54 | Process some integers. 55 | 56 | positional arguments: 57 | N an integer for the accumulator 58 | 59 | optional arguments: 60 | -h, --help show this help message and exit 61 | --sum sum the integers (default: find the max) 62 | 63 | 64 | :: 65 | 66 | In [4]: args = parser.parse_args("--sum 3 2 1".split()) 67 | 68 | In [5]: args.accumulate(args.integers) 69 | Out[5]: 6 70 | 71 | 72 | .. contents:: 73 | :depth: 2 74 | 75 | 76 | About 77 | ===== 78 | 79 | Decli is minimal wrapper around **argparse**. 80 | 81 | It's useful when writing big applications that have many arguments and subcommands, this way it'll be more clear. 82 | 83 | It's a minimal library to rapidly create an interface separated from your app. 84 | 85 | It's possible to use any argument from **argparse** and it works really well with it. 86 | 87 | Forget about copy pasting the argparse functions, if you are lazy like me, this library should be really handy! 88 | 89 | Many cases were tested, but it's possible that not everything was covered, so if you find anything, please report it. 90 | 91 | 92 | Installation 93 | ============ 94 | 95 | :: 96 | 97 | pip install -U decli 98 | 99 | or alternatively: 100 | 101 | :: 102 | 103 | poetry add decli 104 | 105 | 106 | Usage 107 | ====== 108 | 109 | Main cli 110 | -------- 111 | 112 | Create the dictionary in which the cli tool is declared. 113 | 114 | The same arguments argparse use are accepted, except parents, which is ignored. 115 | 116 | - prog - The name of the program (default: sys.argv[0]) 117 | - usage - The string describing the program usage (default: generated from arguments added to parser) 118 | - description - Text to display before the argument help (default: none) 119 | - epilog - Text to display after the argument help (default: none) 120 | - formatter_class - A class for customizing the help output 121 | - prefix_chars - The set of characters that prefix optional arguments (default: ‘-‘) 122 | - fromfile_prefix_chars - The set of characters that prefix files from which additional arguments should be read (default: None) 123 | - argument_default - The global default value for arguments (default: None) 124 | - conflict_handler - The strategy for resolving conflicting optionals (usually unnecessary) 125 | - add_help - Add a -h/--help option to the parser (default: True) 126 | - allow_abbrev - Allows long options to be abbreviated if the abbreviation is unambiguous. (default: True) 127 | 128 | More info in the `argparse page `_ 129 | 130 | Example: 131 | 132 | .. code-block:: python 133 | 134 | data = { 135 | "prog": "myapp", 136 | "description": "This app does something cool", 137 | "epilog": "And that's it" 138 | } 139 | 140 | 141 | Arguments 142 | --------- 143 | 144 | It's just a list with dictionaries. To add aliases just use a list instead of a string. 145 | 146 | Accepted values: 147 | 148 | 149 | - name: - Either a name or a list of option strings, e.g. foo or -f, --foo. 150 | - action - The basic type of action to be taken when this argument is encountered at the command line. 151 | - nargs - The number of command-line arguments that should be consumed. 152 | - const - A constant value required by some action and nargs selections. 153 | - default - The value produced if the argument is absent from the command line. 154 | - type - The type to which the command-line argument should be converted. 155 | - choices - A container of the allowable values for the argument. 156 | - required - Whether or not the command-line option may be omitted (optionals only). 157 | - help - A brief description of what the argument does. 158 | - metavar - A name for the argument in usage messages. 159 | - dest - The name of the attribute to be added to the object returned by parse_args(). 160 | 161 | 162 | More info about `arguments `_ 163 | 164 | Example: 165 | 166 | .. code-block:: python 167 | 168 | data = { 169 | "prog": "myapp", 170 | "description": "This app does something cool", 171 | "epilog": "And that's it", 172 | "arguments": [ 173 | { 174 | "name": "--foo" 175 | }, 176 | { 177 | "name": ["-b", "--bar"] 178 | } 179 | ] 180 | } 181 | 182 | 183 | Subcommands 184 | ----------- 185 | 186 | Just a dictionary where the most important key is **commands** which is a list of the commands. 187 | 188 | 189 | Accepted values: 190 | 191 | 192 | - title - title for the sub-parser group in help output; by default “subcommands” if description is provided, otherwise uses title for positional arguments 193 | - description - description for the sub-parser group in help output, by default None 194 | - commands - list of dicts describing the commands. Same arguments as the **main cli** are supported. And **func** which is really important. 195 | - prog - usage information that will be displayed with sub-command help, by default the name of the program and any positional arguments before the subparser argument 196 | - action - the basic type of action to be taken when this argument is encountered at the command line 197 | - dest - name of the attribute under which sub-command name will be stored; by default None and no value is stored 198 | - required - Whether or not a subcommand must be provided, by default False. 199 | - help - help for sub-parser group in help output, by default None 200 | - metavar - string presenting available sub-commands in help; by default it is None and presents sub-commands in form {cmd1, cmd2, ..} 201 | 202 | 203 | More info about `subcommands `_ 204 | 205 | Func 206 | ~~~~ 207 | 208 | Usually in a sub-command it's useful to specify to which function are they pointing to. That's why each command should have this parameter. 209 | 210 | 211 | When you are building an app which does multiple things, each function should be mapped to a command this way, using the **func** argument. 212 | 213 | Example: 214 | 215 | .. code-block:: python 216 | 217 | from decli import cli 218 | 219 | data = { 220 | "prog": "myapp", 221 | "description": "This app does something cool", 222 | "epilog": "And that's it", 223 | "subcommands": { 224 | "title": "main", 225 | "commands": [ 226 | { 227 | "name": "sum", 228 | "help": "new project", 229 | "func": sum, 230 | "arguments": [ 231 | { 232 | "name": "integers", 233 | "metavar": "N", 234 | "type": int, 235 | "nargs": "+", 236 | "help": "an integer for the accumulator", 237 | }, 238 | {"name": "--name", "nargs": "?"}, 239 | ], 240 | } 241 | ] 242 | } 243 | } 244 | 245 | parser = cli(data) 246 | args = parser.parse_args(["sum 1 2 3".split()]) 247 | args.func(args.integers) # Runs the sum of the integers 248 | 249 | Groups 250 | ------ 251 | 252 | Used to group the arguments based on conceptual groups. This only affects the shown **help**, nothing else. 253 | 254 | Example: 255 | 256 | .. code-block:: python 257 | 258 | data = { 259 | "prog": "app", 260 | "arguments": [ 261 | {"name": "foo", "help": "foo help", "group": "group1"}, 262 | {"name": "choo", "help": "choo help", "group": "group1"}, 263 | {"name": "--bar", "help": "bar help", "group": "group2"}, 264 | ] 265 | } 266 | parser = cli(data) 267 | parser.print_help() 268 | 269 | :: 270 | 271 | usage: app [-h] [--bar BAR] foo choo 272 | 273 | optional arguments: 274 | -h, --help show this help message and exit 275 | 276 | group1: 277 | foo foo help 278 | choo choo help 279 | 280 | group2: 281 | --bar BAR bar help 282 | 283 | 284 | Exclusive Groups 285 | ---------------- 286 | 287 | A mutually exclusive group allows to execute only one **optional** argument (starting with :code:`--`) from the group. 288 | If the condition is not met, it will show an error. 289 | 290 | Example: 291 | 292 | .. code-block:: python 293 | 294 | data = { 295 | "prog": "app", 296 | "arguments": [ 297 | {"name": "--foo", "help": "foo help", "exclusive_group": "group1"}, 298 | {"name": "--choo", "help": "choo help", "exclusive_group": "group1"}, 299 | {"name": "--bar", "help": "bar help", "exclusive_group": "group1"}, 300 | ] 301 | } 302 | parser = cli(data) 303 | parser.print_help() 304 | 305 | :: 306 | 307 | usage: app [-h] [--foo FOO | --choo CHOO | --bar BAR] 308 | 309 | optional arguments: 310 | -h, --help show this help message and exit 311 | --foo FOO foo help 312 | --choo CHOO choo help 313 | --bar BAR bar help 314 | 315 | :: 316 | 317 | In [1]: parser.parse_args("--foo 1 --choo 2".split()) 318 | 319 | usage: app [-h] [--foo FOO | --choo CHOO | --bar BAR] 320 | app: error: argument --choo: not allowed with argument --foo 321 | 322 | 323 | Groups and Exclusive groups 324 | --------------------------- 325 | 326 | It is not possible to have groups inside exclusive groups with **decli**. 327 | 328 | **Decli** will prevent from doing this by raising a :code:`ValueError`. 329 | 330 | It is possible to accomplish it with argparse, but the help message generated will be broken and the 331 | exclusion won't work. 332 | 333 | Parents 334 | ------- 335 | 336 | Sometimes, several cli share a common set of arguments. 337 | 338 | Rather than repeating the definitions of these arguments, 339 | one or more parent clis with all the shared arguments can be passed 340 | to :code:`parents=argument` to cli. 341 | 342 | More info about `parents `_ 343 | 344 | Example: 345 | 346 | .. code-block:: python 347 | 348 | parent_foo_data = { 349 | "add_help": False, 350 | "arguments": [{"name": "--foo-parent", "type": int}], 351 | } 352 | parent_bar_data = { 353 | "add_help": False, 354 | "arguments": [{"name": "--bar-parent", "type": int}], 355 | } 356 | parent_foo_cli = cli(parent_foo_data) 357 | parent_bar_cli = cli(parent_bar_data) 358 | 359 | parents = [parent_foo_cli, parent_bar_cli] 360 | 361 | data = { 362 | "prog": "app", 363 | "arguments": [ 364 | {"name": "foo"} 365 | ] 366 | } 367 | parser = cli(data, parents=parents) 368 | parser.print_help() 369 | 370 | :: 371 | 372 | usage: app [-h] [--foo-parent FOO_PARENT] [--bar-parent BAR_PARENT] foo 373 | 374 | positional arguments: 375 | foo 376 | 377 | optional arguments: 378 | -h, --help show this help message and exit 379 | --foo-parent FOO_PARENT 380 | --bar-parent BAR_PARENT 381 | 382 | 383 | Recipes 384 | ======= 385 | 386 | Subcommands 387 | ----------------- 388 | 389 | .. code-block:: python 390 | 391 | from decli import cli 392 | 393 | data = { 394 | "prog": "myapp", 395 | "formatter_class": argparse.RawDescriptionHelpFormatter, 396 | "description": "The software has subcommands which you can use", 397 | "subcommands": { 398 | "title": "main", 399 | "description": "main commands", 400 | "commands": [ 401 | { 402 | "name": "all", 403 | "help": "check every values is true", 404 | "func": all 405 | }, 406 | { 407 | "name": ["s", "sum"], 408 | "help": "new project", 409 | "func": sum, 410 | "arguments": [ 411 | { 412 | "name": "integers", 413 | "metavar": "N", 414 | "type": int, 415 | "nargs": "+", 416 | "help": "an integer for the accumulator", 417 | }, 418 | {"name": "--name", "nargs": "?"}, 419 | ], 420 | } 421 | ], 422 | }, 423 | } 424 | parser = cli(data) 425 | args = parser.parse_args(["sum 1 2 3".split()]) 426 | args.func(args.integers) # Runs the sum of the integers 427 | 428 | 429 | Minimal 430 | ------- 431 | 432 | This app does nothing, but it's the min we can have: 433 | 434 | .. code-block:: python 435 | 436 | from decli import cli 437 | 438 | parser = cli({}) 439 | parser.print_help() 440 | 441 | :: 442 | 443 | usage: ipython [-h] 444 | 445 | optional arguments: 446 | -h, --help show this help message and exit 447 | 448 | 449 | Positional arguments 450 | -------------------- 451 | 452 | .. code-block:: python 453 | 454 | from decli import cli 455 | 456 | data = { 457 | "arguments": [ 458 | { 459 | "name": "echo" 460 | } 461 | ] 462 | } 463 | parser = cli(data) 464 | args = parser.parse_args(["foo"]) 465 | 466 | :: 467 | 468 | In [11]: print(args.echo) 469 | foo 470 | 471 | 472 | Positional arguments with type 473 | ------------------------------ 474 | 475 | When a type is specified, the argument will be treated as that type, otherwise it'll fail. 476 | 477 | .. code-block:: python 478 | 479 | from decli import cli 480 | 481 | data = { 482 | "arguments": [ 483 | { 484 | "name": "square", 485 | "type": int 486 | } 487 | ] 488 | } 489 | parser = cli(data) 490 | args = parser.parse_args(["1"]) 491 | 492 | :: 493 | 494 | In [11]: print(args.echo) 495 | 1 496 | 497 | Optional arguments 498 | ------------------ 499 | 500 | .. code-block:: python 501 | 502 | from decli import cli 503 | 504 | data = { 505 | "arguments": [ 506 | { 507 | "name": "--verbose", 508 | "help": "increase output verbosity" 509 | } 510 | ] 511 | } 512 | parser = cli(data) 513 | args = parser.parse_args(["--verbosity 1"]) 514 | 515 | :: 516 | 517 | In [11]: print(args.verbosity) 518 | 1 519 | 520 | In [15]: args = parser.parse_args([]) 521 | 522 | In [16]: args 523 | Out[16]: Namespace(verbose=None) 524 | 525 | 526 | Flags 527 | ----- 528 | 529 | Flags are a boolean only (True/False) subset of options. 530 | 531 | .. code-block:: python 532 | 533 | from decli import cli 534 | 535 | data = { 536 | "arguments": [ 537 | { 538 | "name": "--verbose", 539 | "action": "store_true", # defaults to False 540 | }, 541 | { 542 | "name": "--noisy", 543 | "action": "store_false", # defaults to True 544 | } 545 | ] 546 | } 547 | parser = cli(data) 548 | 549 | 550 | 551 | 552 | Short options 553 | ------------- 554 | 555 | Used to add short versions of the options. 556 | 557 | .. code-block:: python 558 | 559 | data = { 560 | "arguments": [ 561 | { 562 | "name": ["-v", "--verbose"], 563 | "help": "increase output verbosity" 564 | } 565 | ] 566 | } 567 | 568 | 569 | Grouping 570 | -------- 571 | 572 | This is only possible using **arguments**. 573 | 574 | Only affect the way the help gets displayed. You might be looking for subcommands. 575 | 576 | 577 | .. code-block:: python 578 | 579 | data = { 580 | "prog": "mycli", 581 | "arguments": [ 582 | { 583 | "name": "--save", 584 | "group": "main", 585 | "help": "This save belongs to the main group", 586 | }, 587 | { 588 | "name": "--remove", 589 | "group": "main", 590 | "help": "This remove belongs to the main group", 591 | }, 592 | ], 593 | } 594 | parser = cli(data) 595 | parser.print_help() 596 | 597 | :: 598 | 599 | usage: mycli [-h] [--save SAVE] [--remove REMOVE] 600 | 601 | optional arguments: 602 | -h, --help show this help message and exit 603 | 604 | main: 605 | --save SAVE This save belongs to the main group 606 | --remove REMOVE This remove belongs to the main group 607 | 608 | 609 | Exclusive group 610 | --------------- 611 | 612 | This is only possible using **optional arguments**. 613 | 614 | 615 | .. code-block:: python 616 | 617 | data = { 618 | "prog": "mycli", 619 | "arguments": [ 620 | { 621 | "name": "--save", 622 | "exclusive_group": "main", 623 | "help": "This save belongs to the main group", 624 | }, 625 | { 626 | "name": "--remove", 627 | "exclusive_group": "main", 628 | "help": "This remove belongs to the main group", 629 | }, 630 | ], 631 | } 632 | parser = cli(data) 633 | parser.print_help() 634 | 635 | :: 636 | 637 | usage: mycli [-h] [--save SAVE | --remove REMOVE] 638 | 639 | optional arguments: 640 | -h, --help show this help message and exit 641 | --save SAVE This save belongs to the main group 642 | --remove REMOVE This remove belongs to the main group 643 | 644 | 645 | Combining Positional and Optional arguments 646 | ------------------------------------------- 647 | 648 | .. code-block:: python 649 | 650 | data = { 651 | "arguments": [ 652 | { 653 | "name": "square", 654 | "type": int, 655 | "help": "display a square of a given number" 656 | }, 657 | { 658 | "name": ["-v", "--verbose"], 659 | "action": "store_true", 660 | "help": "increase output verbosity" 661 | } 662 | ] 663 | } 664 | parser = cli(data) 665 | 666 | args = parser.parse_args() 667 | answer = args.square**2 668 | if args.verbose: 669 | print(f"the square of {args.square} equals {answer}") 670 | else: 671 | print(answer) 672 | 673 | 674 | More Examples 675 | ------------- 676 | 677 | Many examples from `argparse documentation `_ 678 | are covered in test/examples.py 679 | 680 | 681 | Contributing 682 | ============ 683 | 684 | 1. Clone the repo 685 | 2. Install dependencies 686 | 687 | :: 688 | 689 | poetry install 690 | 691 | 3. Format 692 | 693 | :: 694 | 695 | ./scripts/format 696 | 697 | 4. Run tests 698 | 699 | :: 700 | 701 | ./scripts/tests 702 | 703 | 704 | Contributing 705 | ============ 706 | 707 | **PRs are welcome!** 708 | -------------------------------------------------------------------------------- /decli/__init__.py: -------------------------------------------------------------------------------- 1 | from .application import cli 2 | 3 | __version__ = "0.6.3" 4 | 5 | __all__ = ("cli",) 6 | -------------------------------------------------------------------------------- /decli/application.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from argparse import ( 5 | ArgumentParser, 6 | _ArgumentGroup, 7 | _MutuallyExclusiveGroup, 8 | _SubParsersAction, 9 | ) 10 | from collections.abc import Generator, Iterable 11 | from copy import deepcopy 12 | from typing import Any, Callable 13 | 14 | config: dict = {} 15 | logger = logging.getLogger(__name__) 16 | logger.setLevel(logging.INFO) 17 | 18 | 19 | def init_config() -> dict[str, Any]: 20 | return {"prefix_chars": "-"} 21 | 22 | 23 | def ensure_list(name: str | list[str]) -> list[str]: 24 | if isinstance(name, str): 25 | return [name] 26 | return name 27 | 28 | 29 | def has_many_and_is_optional(names: list[str]) -> list[str]: 30 | """The arguments can have aliases only when they are optional. 31 | 32 | If this is not the case, then it raises an error. 33 | """ 34 | prefix_chars = config["prefix_chars"] 35 | is_optional = all(name.startswith(tuple(prefix_chars)) for name in names) 36 | 37 | if not is_optional and len(names) > 1: 38 | raise ValueError( 39 | f"Only optional arguments (starting with {prefix_chars}) can have aliases" 40 | ) 41 | return names 42 | 43 | 44 | def is_exclusive_group_or_not(arg: dict) -> None: 45 | if "exclusive_group" in arg and "group" in arg: 46 | raise ValueError("choose group or exclusive_group not both.") 47 | 48 | 49 | def validate_args(args: Iterable[dict]) -> Generator[dict, Any, None]: 50 | for arg in args: 51 | arg["name"] = ensure_list(arg["name"]) 52 | has_many_and_is_optional(arg["name"]) 53 | is_exclusive_group_or_not(arg) 54 | yield arg 55 | 56 | 57 | def get_or_create_group( 58 | parser: ArgumentParser, 59 | groups: dict, 60 | title: str | None = None, 61 | description: str | None = None, 62 | ) -> _ArgumentGroup | Any: 63 | group_parser = groups.get(title) 64 | if group_parser is None: 65 | group_parser = parser.add_argument_group(title, description) 66 | groups.update({title: group_parser}) 67 | return group_parser 68 | 69 | 70 | def get_or_create_exclusive_group( 71 | parser: ArgumentParser, 72 | groups: dict, 73 | title: str | None = None, 74 | required: bool = False, 75 | ) -> _MutuallyExclusiveGroup | Any: 76 | group_parser = groups.get(title) 77 | if group_parser is None: 78 | group_parser = parser.add_mutually_exclusive_group(required=required) 79 | groups.update({title: group_parser}) 80 | 81 | return group_parser 82 | 83 | 84 | def add_arguments(parser: ArgumentParser, args: list) -> None: 85 | groups: dict = {} 86 | exclusive_groups: dict = {} 87 | 88 | for arg in validate_args(args): 89 | logger.debug("arg: %s", arg) 90 | 91 | name: list = arg.pop("name") 92 | group_title: str | None = arg.pop("group", None) 93 | exclusive_group_title: str | None = arg.pop("exclusive_group", None) 94 | 95 | _parser: Any = parser 96 | if exclusive_group_title: 97 | logger.debug("Exclusive group: %s", exclusive_group_title) 98 | _parser = get_or_create_exclusive_group( 99 | parser, exclusive_groups, exclusive_group_title 100 | ) 101 | elif group_title: 102 | logger.debug("Group: %s", group_title) 103 | _parser = get_or_create_group(parser, groups, group_title) 104 | 105 | _parser.add_argument(*name, **arg) 106 | 107 | 108 | def add_subcommand(parser: _SubParsersAction[ArgumentParser], command: dict) -> None: 109 | args: list = command.pop("arguments", None) 110 | func: Callable | None = command.pop("func", None) 111 | 112 | names: list = ensure_list(command.pop("name")) 113 | name: str = names.pop(0) 114 | 115 | if names: 116 | command.update({"aliases": names}) 117 | 118 | command_parser = parser.add_parser(name, **command) 119 | 120 | if func: 121 | command_parser.set_defaults(func=func) 122 | if args: 123 | add_arguments(command_parser, args) 124 | 125 | 126 | def add_subparser(parser: ArgumentParser, subcommand: dict) -> None: 127 | commands: list = subcommand.pop("commands") 128 | 129 | # This design is for python 3.6 compatibility 130 | if "required" in subcommand: 131 | required = subcommand.pop("required") 132 | subparser = parser.add_subparsers(**subcommand) 133 | subparser.required = required 134 | else: 135 | subparser = parser.add_subparsers(**subcommand) 136 | 137 | for command in commands: 138 | add_subcommand(subparser, command) 139 | 140 | 141 | def add_parser( 142 | data: dict, parser_class: Callable[..., ArgumentParser], parents: list | None 143 | ) -> ArgumentParser: 144 | if parents is None: 145 | parents = [] 146 | 147 | args: list | None = data.pop("arguments", None) 148 | subcommands: dict | None = data.pop("subcommands", None) 149 | 150 | parser = parser_class(**data, parents=parents) 151 | 152 | if args: 153 | logger.debug("Adding arguments...") 154 | add_arguments(parser, args) 155 | 156 | if subcommands: 157 | logger.debug("Adding subcommands...") 158 | add_subparser(parser, subcommands) 159 | 160 | return parser 161 | 162 | 163 | def cli( 164 | data: dict, 165 | parser_class: Callable[..., ArgumentParser] = ArgumentParser, 166 | parents: list | None = None, 167 | ) -> ArgumentParser: 168 | """Create a cli application. 169 | 170 | This is the entrypoint. 171 | """ 172 | global config 173 | config = init_config() 174 | data = deepcopy(data) 175 | 176 | if data.get("prefix_chars"): 177 | config.update({"prefix_chars": data.get("prefix_chars")}) 178 | 179 | parser = add_parser(data, parser_class, parents) 180 | return parser 181 | -------------------------------------------------------------------------------- /decli/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woile/decli/54bf325371138854a4799a5007fae2233306ca57/decli/py.typed -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | """Simple app example""" 2 | 3 | from decli import cli 4 | 5 | data = { 6 | "prog": "app", 7 | "arguments": [ 8 | {"name": "--install", "action": "store_true", "group": "opas"}, 9 | {"name": "--purge", "action": "store_false", "group": "opas"}, 10 | ], 11 | "subcommands": { 12 | "title": "main", 13 | "commands": [ 14 | { 15 | "name": "commit", 16 | "arguments": [ 17 | { 18 | "name": "--bocha", 19 | "action": "store_true", 20 | "group": "opas", 21 | } 22 | ], 23 | } 24 | ], 25 | }, 26 | } 27 | 28 | parser = cli(data) 29 | args = parser.parse_args() 30 | print(args) 31 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appnope" 5 | version = "0.1.3" 6 | description = "Disable App Nap on macOS >= 10.9" 7 | optional = false 8 | python-versions = "*" 9 | groups = ["dev"] 10 | markers = "sys_platform == \"darwin\"" 11 | files = [ 12 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 13 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 14 | ] 15 | 16 | [[package]] 17 | name = "backcall" 18 | version = "0.2.0" 19 | description = "Specifications for callback functions passed in to an API" 20 | optional = false 21 | python-versions = "*" 22 | groups = ["dev"] 23 | files = [ 24 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 25 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2022.12.7" 31 | description = "Python package for providing Mozilla's CA Bundle." 32 | optional = false 33 | python-versions = ">=3.6" 34 | groups = ["dev"] 35 | files = [ 36 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 37 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 38 | ] 39 | 40 | [[package]] 41 | name = "charset-normalizer" 42 | version = "3.1.0" 43 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 44 | optional = false 45 | python-versions = ">=3.7.0" 46 | groups = ["dev"] 47 | files = [ 48 | {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, 49 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, 50 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, 51 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, 52 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, 53 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, 54 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, 55 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, 56 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, 57 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, 58 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, 59 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, 60 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, 61 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, 62 | {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, 63 | {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, 64 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, 65 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, 66 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, 67 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, 68 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, 69 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, 70 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, 71 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, 72 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, 73 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, 74 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, 75 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, 76 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, 77 | {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, 78 | {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, 79 | {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, 80 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, 81 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, 82 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, 83 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, 84 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, 85 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, 86 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, 87 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, 88 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, 89 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, 90 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, 91 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, 92 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, 93 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, 94 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, 95 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, 96 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, 97 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, 98 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, 99 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, 100 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, 101 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, 102 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, 103 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, 104 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, 105 | {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, 106 | {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, 107 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, 108 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, 109 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, 110 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, 111 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, 112 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, 113 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, 114 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, 115 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, 116 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, 117 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, 118 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, 119 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, 120 | {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, 121 | {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, 122 | {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, 123 | ] 124 | 125 | [[package]] 126 | name = "codecov" 127 | version = "2.1.13" 128 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 129 | optional = false 130 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 131 | groups = ["dev"] 132 | files = [ 133 | {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, 134 | {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, 135 | ] 136 | 137 | [package.dependencies] 138 | coverage = "*" 139 | requests = ">=2.7.9" 140 | 141 | [[package]] 142 | name = "colorama" 143 | version = "0.4.6" 144 | description = "Cross-platform colored terminal text." 145 | optional = false 146 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 147 | groups = ["dev"] 148 | markers = "sys_platform == \"win32\"" 149 | files = [ 150 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 151 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 152 | ] 153 | 154 | [[package]] 155 | name = "coverage" 156 | version = "7.2.3" 157 | description = "Code coverage measurement for Python" 158 | optional = false 159 | python-versions = ">=3.7" 160 | groups = ["dev"] 161 | files = [ 162 | {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, 163 | {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, 164 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, 165 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, 166 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, 167 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, 168 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, 169 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, 170 | {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, 171 | {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, 172 | {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, 173 | {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, 174 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, 175 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, 176 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, 177 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, 178 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, 179 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, 180 | {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, 181 | {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, 182 | {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, 183 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, 184 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, 185 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, 186 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, 187 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, 188 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, 189 | {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, 190 | {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, 191 | {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, 192 | {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, 193 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, 194 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, 195 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, 196 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, 197 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, 198 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, 199 | {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, 200 | {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, 201 | {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, 202 | {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, 203 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, 204 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, 205 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, 206 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, 207 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, 208 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, 209 | {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, 210 | {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, 211 | {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, 212 | {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, 213 | ] 214 | 215 | [package.dependencies] 216 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 217 | 218 | [package.extras] 219 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 220 | 221 | [[package]] 222 | name = "decorator" 223 | version = "5.1.1" 224 | description = "Decorators for Humans" 225 | optional = false 226 | python-versions = ">=3.5" 227 | groups = ["dev"] 228 | files = [ 229 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 230 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 231 | ] 232 | 233 | [[package]] 234 | name = "exceptiongroup" 235 | version = "1.1.1" 236 | description = "Backport of PEP 654 (exception groups)" 237 | optional = false 238 | python-versions = ">=3.7" 239 | groups = ["dev"] 240 | markers = "python_version < \"3.11\"" 241 | files = [ 242 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, 243 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, 244 | ] 245 | 246 | [package.extras] 247 | test = ["pytest (>=6)"] 248 | 249 | [[package]] 250 | name = "idna" 251 | version = "3.4" 252 | description = "Internationalized Domain Names in Applications (IDNA)" 253 | optional = false 254 | python-versions = ">=3.5" 255 | groups = ["dev"] 256 | files = [ 257 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 258 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 259 | ] 260 | 261 | [[package]] 262 | name = "iniconfig" 263 | version = "2.0.0" 264 | description = "brain-dead simple config-ini parsing" 265 | optional = false 266 | python-versions = ">=3.7" 267 | groups = ["dev"] 268 | files = [ 269 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 270 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 271 | ] 272 | 273 | [[package]] 274 | name = "ipdb" 275 | version = "0.13.13" 276 | description = "IPython-enabled pdb" 277 | optional = false 278 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 279 | groups = ["dev"] 280 | files = [ 281 | {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, 282 | {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, 283 | ] 284 | 285 | [package.dependencies] 286 | decorator = {version = "*", markers = "python_version > \"3.6\""} 287 | ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} 288 | tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} 289 | 290 | [[package]] 291 | name = "ipython" 292 | version = "7.34.0" 293 | description = "IPython: Productive Interactive Computing" 294 | optional = false 295 | python-versions = ">=3.7" 296 | groups = ["dev"] 297 | files = [ 298 | {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, 299 | {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, 300 | ] 301 | 302 | [package.dependencies] 303 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 304 | backcall = "*" 305 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 306 | decorator = "*" 307 | jedi = ">=0.16" 308 | matplotlib-inline = "*" 309 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 310 | pickleshare = "*" 311 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 312 | pygments = "*" 313 | setuptools = ">=18.5" 314 | traitlets = ">=4.2" 315 | 316 | [package.extras] 317 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] 318 | doc = ["Sphinx (>=1.3)"] 319 | kernel = ["ipykernel"] 320 | nbconvert = ["nbconvert"] 321 | nbformat = ["nbformat"] 322 | notebook = ["ipywidgets", "notebook"] 323 | parallel = ["ipyparallel"] 324 | qtconsole = ["qtconsole"] 325 | test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] 326 | 327 | [[package]] 328 | name = "jedi" 329 | version = "0.18.2" 330 | description = "An autocompletion tool for Python that can be used for text editors." 331 | optional = false 332 | python-versions = ">=3.6" 333 | groups = ["dev"] 334 | files = [ 335 | {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, 336 | {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, 337 | ] 338 | 339 | [package.dependencies] 340 | parso = ">=0.8.0,<0.9.0" 341 | 342 | [package.extras] 343 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 344 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 345 | testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 346 | 347 | [[package]] 348 | name = "matplotlib-inline" 349 | version = "0.1.6" 350 | description = "Inline Matplotlib backend for Jupyter" 351 | optional = false 352 | python-versions = ">=3.5" 353 | groups = ["dev"] 354 | files = [ 355 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 356 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 357 | ] 358 | 359 | [package.dependencies] 360 | traitlets = "*" 361 | 362 | [[package]] 363 | name = "mypy" 364 | version = "1.16.0" 365 | description = "Optional static typing for Python" 366 | optional = false 367 | python-versions = ">=3.9" 368 | groups = ["dev"] 369 | files = [ 370 | {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, 371 | {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, 372 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, 373 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, 374 | {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, 375 | {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, 376 | {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, 377 | {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, 378 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, 379 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, 380 | {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, 381 | {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, 382 | {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, 383 | {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, 384 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, 385 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, 386 | {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, 387 | {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, 388 | {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, 389 | {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, 390 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, 391 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, 392 | {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, 393 | {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, 394 | {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, 395 | {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, 396 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, 397 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, 398 | {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, 399 | {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, 400 | {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, 401 | {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, 402 | ] 403 | 404 | [package.dependencies] 405 | mypy_extensions = ">=1.0.0" 406 | pathspec = ">=0.9.0" 407 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 408 | typing_extensions = ">=4.6.0" 409 | 410 | [package.extras] 411 | dmypy = ["psutil (>=4.0)"] 412 | faster-cache = ["orjson"] 413 | install-types = ["pip"] 414 | mypyc = ["setuptools (>=50)"] 415 | reports = ["lxml"] 416 | 417 | [[package]] 418 | name = "mypy-extensions" 419 | version = "1.0.0" 420 | description = "Type system extensions for programs checked with the mypy type checker." 421 | optional = false 422 | python-versions = ">=3.5" 423 | groups = ["dev"] 424 | files = [ 425 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 426 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 427 | ] 428 | 429 | [[package]] 430 | name = "packaging" 431 | version = "23.1" 432 | description = "Core utilities for Python packages" 433 | optional = false 434 | python-versions = ">=3.7" 435 | groups = ["dev"] 436 | files = [ 437 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 438 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 439 | ] 440 | 441 | [[package]] 442 | name = "parso" 443 | version = "0.8.3" 444 | description = "A Python Parser" 445 | optional = false 446 | python-versions = ">=3.6" 447 | groups = ["dev"] 448 | files = [ 449 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 450 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 451 | ] 452 | 453 | [package.extras] 454 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 455 | testing = ["docopt", "pytest (<6.0.0)"] 456 | 457 | [[package]] 458 | name = "pathspec" 459 | version = "0.11.1" 460 | description = "Utility library for gitignore style pattern matching of file paths." 461 | optional = false 462 | python-versions = ">=3.7" 463 | groups = ["dev"] 464 | files = [ 465 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, 466 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, 467 | ] 468 | 469 | [[package]] 470 | name = "pexpect" 471 | version = "4.8.0" 472 | description = "Pexpect allows easy control of interactive console applications." 473 | optional = false 474 | python-versions = "*" 475 | groups = ["dev"] 476 | markers = "sys_platform != \"win32\"" 477 | files = [ 478 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 479 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 480 | ] 481 | 482 | [package.dependencies] 483 | ptyprocess = ">=0.5" 484 | 485 | [[package]] 486 | name = "pickleshare" 487 | version = "0.7.5" 488 | description = "Tiny 'shelve'-like database with concurrency support" 489 | optional = false 490 | python-versions = "*" 491 | groups = ["dev"] 492 | files = [ 493 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 494 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 495 | ] 496 | 497 | [[package]] 498 | name = "pluggy" 499 | version = "1.0.0" 500 | description = "plugin and hook calling mechanisms for python" 501 | optional = false 502 | python-versions = ">=3.6" 503 | groups = ["dev"] 504 | files = [ 505 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 506 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 507 | ] 508 | 509 | [package.extras] 510 | dev = ["pre-commit", "tox"] 511 | testing = ["pytest", "pytest-benchmark"] 512 | 513 | [[package]] 514 | name = "prompt-toolkit" 515 | version = "3.0.38" 516 | description = "Library for building powerful interactive command lines in Python" 517 | optional = false 518 | python-versions = ">=3.7.0" 519 | groups = ["dev"] 520 | files = [ 521 | {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, 522 | {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, 523 | ] 524 | 525 | [package.dependencies] 526 | wcwidth = "*" 527 | 528 | [[package]] 529 | name = "ptyprocess" 530 | version = "0.7.0" 531 | description = "Run a subprocess in a pseudo terminal" 532 | optional = false 533 | python-versions = "*" 534 | groups = ["dev"] 535 | markers = "sys_platform != \"win32\"" 536 | files = [ 537 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 538 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 539 | ] 540 | 541 | [[package]] 542 | name = "pygments" 543 | version = "2.15.1" 544 | description = "Pygments is a syntax highlighting package written in Python." 545 | optional = false 546 | python-versions = ">=3.7" 547 | groups = ["dev"] 548 | files = [ 549 | {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, 550 | {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, 551 | ] 552 | 553 | [package.extras] 554 | plugins = ["importlib-metadata ; python_version < \"3.8\""] 555 | 556 | [[package]] 557 | name = "pytest" 558 | version = "7.3.1" 559 | description = "pytest: simple powerful testing with Python" 560 | optional = false 561 | python-versions = ">=3.7" 562 | groups = ["dev"] 563 | files = [ 564 | {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, 565 | {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, 566 | ] 567 | 568 | [package.dependencies] 569 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 570 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 571 | iniconfig = "*" 572 | packaging = "*" 573 | pluggy = ">=0.12,<2.0" 574 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 575 | 576 | [package.extras] 577 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 578 | 579 | [[package]] 580 | name = "pytest-cov" 581 | version = "4.0.0" 582 | description = "Pytest plugin for measuring coverage." 583 | optional = false 584 | python-versions = ">=3.6" 585 | groups = ["dev"] 586 | files = [ 587 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, 588 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, 589 | ] 590 | 591 | [package.dependencies] 592 | coverage = {version = ">=5.2.1", extras = ["toml"]} 593 | pytest = ">=4.6" 594 | 595 | [package.extras] 596 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 597 | 598 | [[package]] 599 | name = "requests" 600 | version = "2.29.0" 601 | description = "Python HTTP for Humans." 602 | optional = false 603 | python-versions = ">=3.7" 604 | groups = ["dev"] 605 | files = [ 606 | {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, 607 | {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, 608 | ] 609 | 610 | [package.dependencies] 611 | certifi = ">=2017.4.17" 612 | charset-normalizer = ">=2,<4" 613 | idna = ">=2.5,<4" 614 | urllib3 = ">=1.21.1,<1.27" 615 | 616 | [package.extras] 617 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 618 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 619 | 620 | [[package]] 621 | name = "ruff" 622 | version = "0.11.12" 623 | description = "An extremely fast Python linter and code formatter, written in Rust." 624 | optional = false 625 | python-versions = ">=3.7" 626 | groups = ["dev"] 627 | files = [ 628 | {file = "ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc"}, 629 | {file = "ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3"}, 630 | {file = "ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa"}, 631 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012"}, 632 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a"}, 633 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7"}, 634 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a"}, 635 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13"}, 636 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be"}, 637 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd"}, 638 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef"}, 639 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5"}, 640 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02"}, 641 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c"}, 642 | {file = "ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6"}, 643 | {file = "ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832"}, 644 | {file = "ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5"}, 645 | {file = "ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603"}, 646 | ] 647 | 648 | [[package]] 649 | name = "setuptools" 650 | version = "67.7.2" 651 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 652 | optional = false 653 | python-versions = ">=3.7" 654 | groups = ["dev"] 655 | files = [ 656 | {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"}, 657 | {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"}, 658 | ] 659 | 660 | [package.extras] 661 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 662 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 663 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 664 | 665 | [[package]] 666 | name = "tomli" 667 | version = "2.0.1" 668 | description = "A lil' TOML parser" 669 | optional = false 670 | python-versions = ">=3.7" 671 | groups = ["dev"] 672 | markers = "python_full_version <= \"3.11.0a6\"" 673 | files = [ 674 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 675 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 676 | ] 677 | 678 | [[package]] 679 | name = "traitlets" 680 | version = "5.9.0" 681 | description = "Traitlets Python configuration system" 682 | optional = false 683 | python-versions = ">=3.7" 684 | groups = ["dev"] 685 | files = [ 686 | {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, 687 | {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, 688 | ] 689 | 690 | [package.extras] 691 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 692 | test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] 693 | 694 | [[package]] 695 | name = "typing-extensions" 696 | version = "4.13.2" 697 | description = "Backported and Experimental Type Hints for Python 3.8+" 698 | optional = false 699 | python-versions = ">=3.8" 700 | groups = ["dev"] 701 | files = [ 702 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 703 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 704 | ] 705 | 706 | [[package]] 707 | name = "urllib3" 708 | version = "1.26.15" 709 | description = "HTTP library with thread-safe connection pooling, file post, and more." 710 | optional = false 711 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 712 | groups = ["dev"] 713 | files = [ 714 | {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, 715 | {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, 716 | ] 717 | 718 | [package.extras] 719 | brotli = ["brotli (>=1.0.9) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] 720 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 721 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 722 | 723 | [[package]] 724 | name = "wcwidth" 725 | version = "0.2.6" 726 | description = "Measures the displayed width of unicode strings in a terminal" 727 | optional = false 728 | python-versions = "*" 729 | groups = ["dev"] 730 | files = [ 731 | {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, 732 | {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, 733 | ] 734 | 735 | [metadata] 736 | lock-version = "2.1" 737 | python-versions = ">=3.9" 738 | content-hash = "4f77e7707c7c97a57ee527a152cfe86a6dbe2b50bba70fcb70b1e03ab63f621b" 739 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "decli" 3 | version = "0.6.3" 4 | description = "Minimal, easy-to-use, declarative cli tool" 5 | authors = ["Santiago Fraire "] 6 | license = "MIT" 7 | readme = 'README.rst' 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.9" 11 | 12 | [tool.poetry.group.dev.dependencies] 13 | ipython = "^7.16" 14 | ipdb = "^0.13.13" 15 | pytest = "^7.3.1" 16 | pytest-cov = "^4.0.0" 17 | mypy = "^1.16.0" 18 | codecov = "^2.1.13" 19 | ruff = "^0.11.5" 20 | 21 | [tool.ruff] 22 | required-version = ">=0.11.5" 23 | line-length = 88 24 | 25 | [tool.ruff.lint] 26 | select = [ 27 | # flake8-annotations 28 | "ANN0", 29 | "ANN2", 30 | # pycodestyle 31 | "E", 32 | # Pyflakes 33 | "F", 34 | # pyupgrade 35 | "UP", 36 | # isort 37 | "I", 38 | # unsorted-dunder-all 39 | "RUF022", 40 | # unused-noqa 41 | "RUF100", 42 | ] 43 | ignore = ["E501", "D1", "D415"] 44 | 45 | [tool.ruff.lint.isort] 46 | known-first-party = ["decli", "tests"] 47 | 48 | [tool.mypy] 49 | files = ["decli", "tests"] 50 | disallow_untyped_decorators = true 51 | disallow_subclassing_any = true 52 | warn_return_any = true 53 | warn_redundant_casts = true 54 | warn_unused_ignores = true 55 | warn_unused_configs = true 56 | 57 | [tool.commitizen] 58 | name = "cz_conventional_commits" 59 | tag_format = "v$version" 60 | version_type = "pep440" 61 | version_provider = "poetry" 62 | update_changelog_on_bump = true 63 | major_version_zero = true 64 | version_files = ["decli/__init__.py:__version__"] 65 | 66 | [build-system] 67 | requires = ["poetry-core"] 68 | build-backend = "poetry.core.masonry.api" 69 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | ruff check --fix 4 | ruff format 5 | -------------------------------------------------------------------------------- /scripts/publish: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | poetry publish --build -u "$PYPI_USERNAME" -p "$PYPI_PASSWORD" 4 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | pr() { 4 | poetry run python -m "$@" 5 | } 6 | 7 | pr pytest --cov-report term-missing --cov-report=xml:coverage.xml --cov=decli "${1:-tests}" 8 | pr ruff check 9 | pr mypy 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woile/decli/54bf325371138854a4799a5007fae2233306ca57/tests/__init__.py -------------------------------------------------------------------------------- /tests/examples.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import textwrap 3 | 4 | from decli import cli 5 | 6 | 7 | def main_example() -> argparse.ArgumentParser: 8 | """https://docs.python.org/3/library/argparse.html#example""" 9 | data = { 10 | "description": "Process some integers.", 11 | "arguments": [ 12 | { 13 | "name": "integers", 14 | "metavar": "N", 15 | "type": int, 16 | "nargs": "+", 17 | "help": "an integer for the accumulator", 18 | }, 19 | { 20 | "name": "--sum", 21 | "dest": "accumulate", 22 | "action": "store_const", 23 | "const": sum, 24 | "default": max, 25 | "help": "sum the integers (default: find the max)", 26 | }, 27 | ], 28 | } 29 | parser = cli(data) 30 | return parser 31 | 32 | 33 | def complete_example() -> argparse.ArgumentParser: 34 | data = { 35 | "prog": "cz", 36 | "formatter_class": argparse.RawDescriptionHelpFormatter, 37 | "description": "The software does this and that", 38 | "epilog": "This is the epilooogpoe ", 39 | "arguments": [ 40 | { 41 | "name": "--debug", 42 | "action": "store_true", 43 | "default": False, 44 | "help": "use debug mode", 45 | }, 46 | { 47 | "name": ["-v", "--version"], 48 | "action": "store_true", 49 | "default": False, 50 | "help": "get the installed version", 51 | "group": "ops", 52 | }, 53 | {"name": "--save", "group": "ops"}, 54 | ], 55 | "subcommands": { 56 | "title": "main", 57 | "description": "main commands", 58 | "commands": [ 59 | { 60 | "name": "all", 61 | "help": "check every values is true", 62 | "func": all, 63 | }, 64 | { 65 | "name": ["sum", "s"], 66 | "help": "new project", 67 | "func": sum, 68 | "arguments": [ 69 | { 70 | "name": "integers", 71 | "metavar": "N", 72 | "type": int, 73 | "nargs": "+", 74 | "help": "an integer for the accumulator", 75 | }, 76 | {"name": "--name", "nargs": "?"}, 77 | ], 78 | }, 79 | ], 80 | }, 81 | } 82 | parser = cli(data) 83 | return parser 84 | 85 | 86 | def name_or_flags() -> dict: 87 | """https://docs.python.org/3/library/argparse.html#name-or-flags""" 88 | data = { 89 | "prog": "sti", 90 | "arguments": [{"name": ["-f", "--foo"]}, {"name": "bar"}], 91 | } 92 | return data 93 | 94 | 95 | def compose_clis_using_parents() -> list[argparse.ArgumentParser]: 96 | """ 97 | Sometimes, several cli share a common set of arguments. 98 | Rather than repeating the definitions of these arguments, 99 | one or more parent clis with all the shared arguments can be passed 100 | to parents= argument to cli. 101 | 102 | https://docs.python.org/3/library/argparse.html#parents 103 | """ 104 | parent_foo_data = { 105 | "add_help": False, 106 | "arguments": [{"name": "--foo-parent", "type": int}], 107 | } 108 | parent_bar_data = { 109 | "add_help": False, 110 | "arguments": [{"name": "--bar-parent", "type": int}], 111 | } 112 | parent_foo_cli = cli(parent_foo_data) 113 | parent_bar_cli = cli(parent_bar_data) 114 | 115 | parents = [parent_foo_cli, parent_bar_cli] 116 | return parents 117 | 118 | 119 | def using_formatter_class() -> dict: 120 | """https://docs.python.org/3/library/argparse.html#formatter-class""" 121 | data = { 122 | "prog": "PROG", 123 | "formatter_class": argparse.RawDescriptionHelpFormatter, 124 | "description": textwrap.dedent( 125 | """\ 126 | Please do not mess up this text! 127 | -------------------------------- 128 | I have indented it 129 | exactly the way 130 | I want it 131 | """ 132 | ), 133 | } 134 | return data 135 | 136 | 137 | def prefix_chars() -> dict: 138 | data = { 139 | "prog": "PROG", 140 | "prefix_chars": "+", 141 | "arguments": [{"name": ["+f", "++foo"]}, {"name": "++bar"}], 142 | } 143 | return data 144 | 145 | 146 | def grouping_arguments() -> dict: 147 | data = { 148 | "prog": "mycli", 149 | "arguments": [ 150 | { 151 | "name": "--new", 152 | "help": "This does not belong to a group but its a long help", 153 | }, 154 | { 155 | "name": "--init", 156 | "help": "This does not belong to a group but its a long help", 157 | }, 158 | { 159 | "name": "--run", 160 | "group": "app", 161 | "help": "This does not belong to a group", 162 | }, 163 | { 164 | "name": "--build", 165 | "group": "app", 166 | "help": "This does not belong to a group", 167 | }, 168 | { 169 | "name": ["--install", "--add"], 170 | "group": "package", 171 | "metavar": "package_name", 172 | "nargs": "+", 173 | "help": "This belongs to a group", 174 | }, 175 | { 176 | "name": "--remove", 177 | "group": "package", 178 | "help": "This belongs to a group", 179 | }, 180 | { 181 | "name": "--why", 182 | "group": "package", 183 | "help": "This belongs to a group", 184 | }, 185 | ], 186 | } 187 | return data 188 | 189 | 190 | def exclusive_group() -> dict: 191 | data = { 192 | "prog": "app", 193 | "arguments": [ 194 | {"name": "--install", "exclusive_group": "ops"}, 195 | {"name": "--purge", "exclusive_group": "ops"}, 196 | ], 197 | } 198 | return data 199 | -------------------------------------------------------------------------------- /tests/test_decli.py: -------------------------------------------------------------------------------- 1 | """Most of argparse examples rebuilt with climp.""" 2 | 3 | import argparse 4 | import unittest 5 | 6 | import pytest 7 | 8 | from decli import cli 9 | 10 | from . import examples 11 | 12 | 13 | class Test(unittest.TestCase): 14 | def test_main_example_ok(self) -> None: 15 | parser = examples.main_example() 16 | args = parser.parse_args("1 2 3 4".split()) 17 | 18 | assert args.accumulate(args.integers) == 4 19 | 20 | def test_main_example_sums_ok(self) -> None: 21 | parser = examples.main_example() 22 | args = parser.parse_args("1 2 3 4 --sum".split()) 23 | 24 | assert args.accumulate(args.integers) == 10 25 | 26 | def test_main_example_fails(self) -> None: 27 | parser = examples.main_example() 28 | 29 | with pytest.raises(SystemExit): 30 | args = parser.parse_args("a b c".split()) 31 | args.accumulate(args.integers) 32 | 33 | def test_complete_example(self) -> None: 34 | parser = examples.complete_example() 35 | args = parser.parse_args("sum 1 2 3".split()) 36 | 37 | assert args.func(args.integers) == 6 38 | 39 | def test_compose_clis_using_parents(self) -> None: 40 | data = {"prog": "daddy", "arguments": [{"name": "something"}]} 41 | parents = examples.compose_clis_using_parents() 42 | parser = cli(data, parents=parents) 43 | args = parser.parse_args(["--foo-parent", "2", "XXX"]) 44 | 45 | assert args.something == "XXX" 46 | assert args.foo_parent == 2 47 | 48 | def test_compose_clis_using_parents_and_arguments(self) -> None: 49 | data = {"prog": "daddy", "arguments": [{"name": "--something"}]} 50 | parents = examples.compose_clis_using_parents() 51 | parser = cli(data, parents=parents) 52 | args = parser.parse_args(["--something", "XXX"]) 53 | 54 | assert args.something == "XXX" 55 | 56 | def test_prefix_chars(self) -> None: 57 | data = examples.prefix_chars() 58 | parser = cli(data) 59 | args = parser.parse_args("+f X ++bar Y".split()) 60 | 61 | assert args.foo == "X" 62 | assert args.bar == "Y" 63 | 64 | def test_name_or_flags(self) -> None: 65 | data = examples.name_or_flags() 66 | parser = cli(data) 67 | 68 | args = parser.parse_args(["HELLO"]) 69 | assert args.bar == "HELLO" 70 | 71 | args = parser.parse_args(["BAR", "--foo", "FOO"]) 72 | assert args.bar == "BAR" 73 | assert args.foo == "FOO" 74 | 75 | def test_name_or_flags_fail(self) -> None: 76 | data = examples.name_or_flags() 77 | parser = cli(data) 78 | with pytest.raises(SystemExit): 79 | parser.parse_args(["--foo", "FOO"]) 80 | 81 | def test_cli_no_args(self) -> None: 82 | data = {"prog": "daddy", "description": "helloo"} 83 | parser = cli(data) 84 | args = parser.parse_args([]) 85 | 86 | assert args.__dict__ == {} 87 | 88 | def test_groups(self) -> None: 89 | data = examples.grouping_arguments() 90 | parser = cli(data) 91 | help_result = parser.format_help() 92 | 93 | assert "app" in help_result 94 | assert "package" in help_result 95 | 96 | def test_not_optional_arg_name_validation_fails(self) -> None: 97 | data = {"arguments": [{"name": ["f", "foo"]}]} 98 | with pytest.raises(ValueError): 99 | cli(data) 100 | 101 | def test_exclusive_group_ok(self) -> None: 102 | data = { 103 | "prog": "app", 104 | "arguments": [ 105 | { 106 | "name": "--install", 107 | "action": "store_true", 108 | "exclusive_group": "ops", 109 | }, 110 | { 111 | "name": "--purge", 112 | "action": "store_true", 113 | "exclusive_group": "ops", 114 | }, 115 | ], 116 | } 117 | parser = cli(data) 118 | args = parser.parse_args(["--install"]) 119 | assert args.install is True 120 | assert args.purge is False 121 | 122 | def test_exclusive_group_fails_when_same_group_called(self) -> None: 123 | data = { 124 | "prog": "app", 125 | "arguments": [ 126 | { 127 | "name": "--install", 128 | "action": "store_true", 129 | "exclusive_group": "opas", 130 | }, 131 | { 132 | "name": "--purge", 133 | "action": "store_true", 134 | "exclusive_group": "opas", 135 | }, 136 | ], 137 | } 138 | 139 | parser = cli(data) 140 | with pytest.raises(SystemExit): 141 | parser.parse_args("--install --purge".split()) 142 | 143 | def test_exclusive_group_and_group_together_fail(self) -> None: 144 | """ 145 | Note: 146 | 147 | Exclusive group requires at least one arg before adding groups 148 | """ 149 | data = { 150 | "prog": "app", 151 | "arguments": [ 152 | { 153 | "name": "--install", 154 | "action": "store_true", 155 | "exclusive_group": "ops", 156 | "group": "cmd", 157 | }, 158 | { 159 | "name": "--purge", 160 | "action": "store_true", 161 | "exclusive_group": "ops", 162 | "group": "cmd", 163 | }, 164 | {"name": "--fear", "exclusive_group": "ops"}, 165 | ], 166 | } 167 | 168 | with pytest.raises(ValueError): 169 | cli(data) 170 | 171 | def test_subcommand_required(self) -> None: 172 | data = { 173 | "prog": "cz", 174 | "description": ( 175 | "Commitizen is a cli tool to generate conventional commits.\n" 176 | "For more information about the topic go to " 177 | "https://conventionalcommits.org/" 178 | ), 179 | "formatter_class": argparse.RawDescriptionHelpFormatter, 180 | "arguments": [ 181 | {"name": "--debug", "action": "store_true", "help": "use debug mode"}, 182 | { 183 | "name": ["-n", "--name"], 184 | "help": "use the given commitizen (default: cz_conventional_commits)", 185 | }, 186 | ], 187 | "subcommands": { 188 | "title": "commands", 189 | "required": True, 190 | "commands": [ 191 | { 192 | "name": ["init"], 193 | "help": "init commitizen configuration", 194 | } 195 | ], 196 | }, 197 | } 198 | 199 | parser = cli(data) 200 | args = parser.parse_args(["-n", "cz_jira", "init"]) 201 | assert args.debug is False 202 | assert args.name == "cz_jira" 203 | --------------------------------------------------------------------------------